Serverless Tracking on a budget
October 27, 2019
Since launching this website I had the idea to have some form of hit counter to see how many people visit each page. It is also very important for me to respect visitor’s privacy. I was looking a bit around, but did not find a good solution which fulfills this simple usecase for free. Therefore I decided to write my own tracking.
Netlify functions
My website is already deployed on Netlify so naturally I was looking into options there. I know that my website is not high volume, which makes the free tier of Netlify functions a perfect fit to glue the events from the website to some serverless data store. My website consists of a Gatsby project deployed on netlify, so I had to figure out how to deploy the function alongside.
Note: All code is included in the Github Sources of this blog.
Setting up a function consists of multiple steps (assuming that your project uses yarn to build):
-
Create a netlify.toml and add:
[build] functions = "lambda" publish = "public" command = "yarn build"
yarn add netlify-lambda
- Add
&& netlify-lambda build src/lambda
to the build step inpackage.json
- Add
"postinstall": "netlify-lambda install src/lambda",
topackage.json
- Create folder
src/lambda
. I decided tonpm init
in the folder, since my function needsaws-sdk
- Create a dummy function in
filename.js
, e.g.
export async function handler(event, context) {
return {
statusCode: 200,
body: JSON.stringify({ message: `Hello world` })
};
}
- Commit, push and wait for netlify deployment
- Do a request to
yoursite.com/.netlify/functions/filename
to check whether it works - For local dev
netlify-lambda serve src/lambda
can be used. It exposes the functions on port9000
, so you can requestlocalhost:9000/.netlify/functions/filename
Storing the data
Now that we have a foundation to deploy netify functions we can focus back on the initial goal, which was creating a hit counter. I first thought about using google sheets api, but didn’t like the documentation somehow and knew already how to do it with DynamoDB.
DynamoDB is a column based serverless distributed key-value store. The basic idea is that data is partitioned and queryable by partition key with an optional sort key for ordering. AWS documentation is quite extensive, please check it out for more details.
We will store (and increment) the count for every page on a daily basis. The partition key will be the page
and the sort key will be the date
. This is optimized for getting all data for a page. If you want to query date first you could make date the partition key or add a global secondary index to the table, which supports the other access pattern.
DynamoDB is available in the free tier of AWS, so the only prequisite is having an AWS account. I will quickly walk you through the steps to set it up (This might be hard if you do not have any AWS experience. Follow the documentation of aws if it is unclear):
- Log into AWS console
- Select IAM, create a User (note down the credentials for the user (
ACCESS_KEY_ID
,SECRET_ACCESS_KEY
)) -
Add a policy to the User granting
"dynamodb:UpdateItem"
on your table. The policy will look similar to this:{ "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": [ "dynamodb:UpdateItem" ], "Resource": "arn:aws:dynamodb:*:*:table/tracking" } ] }
- Create the table with primary partition key (HASH) with name
page
and typeS
and secondary keydate
also of typeS
- If you want to play around locally you would need to install aws cli and create a config + credentials file for this user
- As next step we will share the credentials inside netlify. Go to your site > Settings > Build & Deploy > Environment. There create two variables holding the AWS credentials. I named them
KEY_ID
andSECRECT_ACCESS_KEY
. Note that you cannot useAWS_
prefixes.
Tracking function
I’m using the following code inside of the function to increment the counter by one on every call to the function. Don’t be afraid if you have never seen any DynamoDB code. It’s using the updateItem
operation with ADD
as the UpdateExpression. If there is no item in the database which matches the keys a new one is created where the count field will be 0 (and then incremented to 1).
Note that I have to manually pass the credentials to the DynamoDB instance. To mark the page I’m leveraging the query parameters of the function, so calls will look like: yoursite.com/.netlify/functions/hit?page=your-page
const DynamoDB = require("aws-sdk").DynamoDB
const dynamoDB = new DynamoDB({
region: "eu-west-1",
accessKeyId: process.env.KEY_ID,
secretAccessKey: process.env.SECRECT_ACCESS_KEY,
})
exports.handler = async (event, context) => {
await dynamoDB
.updateItem({
ExpressionAttributeNames: {
"#CT": "count",
},
ExpressionAttributeValues: {
":inc": {
N: "1",
},
},
Key: {
page: {
S: event.queryStringParameters.page,
},
date: {
S: new Date().toISOString().slice(0, 10),
},
},
ReturnValues: "UPDATED_NEW",
TableName: "tracking",
UpdateExpression: "ADD #CT :inc",
})
.promise()
return {
statusCode: 200,
body: "OK",
}
}
Add this to src/lambda/hit.js
and run yarn add aws-sdk
inside src/lambda
folder. Deploy the function and test with manual requests if the counters get updated in DynamoDB (check out via aws console). Note: For me it’s enough to be able to check stats in aws console, so I won’t guide further than storing the data.
Hooking up the tracking
I’m using Gatsby as engine for creating the page. It is based on react, so I’m writing a custom react hook to call my function:
import { useEffect } from "react"
export const useTracking = page =>
useEffect(() => {
const trackHit = async () => {
try {
await fetch(`/.netlify/functions/hit?page=${page}`)
} catch (e) {}
}
trackHit()
}, [page])
It takes a page parameter and calls the function with it via http (based on fetch api). This hook now can be used in all pages to track hits. Added to the remark content of the blog this might look like:
import { useTracking } from "../tracking/tracking"
const Talks = props => {
useTracking("/talks")
...
}
Note that your React components need to be function components to work with hooks. You can also do this without hooks on componentDidMount
.
That’s it! Commit, push, deploy and test it. You should now see the test hits you’re generating in DynamoDB. Afterwards reset the DynamoDB to be empty and let traffic flow in.
Note: All code is in the Github Sources of this blog.
If you have any questions or want to provide feedback (I’d be very happy), feel free to reach out via twitter: @m4nl5r
Personal blog by Martin Lechner
Thoughts about (functional) programming