Lightweight Pageview Tracking With Application Insights
This website is hosted as a static website on
Azure Static Web Apps
. It's an easy and cheap way of hosting simple static websites, but there are some limitations. One of those limitations is the lack of pageview metrics in the portal.
While you can see some simple site hit numbers in the metrics dashboard of the website, it lacks any information about which page was loaded or where it came from.
You could choose to add Google Analytics to the website, but I don't really favour that from a privacy point of view. There are more privacy conscious alternatives available like
Plausible
and
Fathom
, but those require either a paid plan or setting up your own hosted instance.
Within the Azure ecosystem there is also the choice of
Application Insights
. While most people might think this is used to track errors and performance metrics for a website, it is also very usable for pageview tracking. For static web apps the advice generally given is to use the Application Insights JavaScript snippet.
But in my
continued drive
to keep things fast and lightweight, I was looking at this with a critical eye.
Adding the default Application Insights JavaScript snippet will actually load a JS file of 130 KB (minified). Did I really need that much if I just needed some simple pageview tracking?
There is also a
basic version
available that removes some functionality, but that still leaves a JS file of 76 KB.
I decided to have a look at what the Application Insights script actually does when you load a page through the browser's dev tools.
It turns out that really the only important thing (for me at least) that happens is a POST request to the Application Insights ingestion endpoint with some JSON containing information about the current page load.
So I created a script that would do just that, with some code to get the relevant information as well. After some testing this is the script I ended up with:
const generateUUID = () => {
let
d = new Date().getTime(),
d2 = (performance &&
performance.now &&
(performance.now() * 1000)) || 0;
return
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
let r = Math.random() * 16;
if (d > 0) {
r = (d + r) % 16 | 0;
d = Math.floor(d / 16);
} else {
r = (d2 + r) % 16 | 0;
d2 = Math.floor(d2 / 16);
}
return (c == 'x' ? r : (r & 0x7 | 0x8)).toString(16);
});
};
const getLoadDuration = async () => {
let navigationTiming =
(performance &&
performance.getEntriesByType &&
performance.getEntriesByType('navigation')) || [];
if (navigationTiming.length === 0) {
return undefined;
}
let totalMs = Math.trunc(navigationTiming[0].duration);
if (totalMs === 0) {
// page not fully loaded yet, so wait and try again
return await
new Promise(resolve => setTimeout(resolve, 100))
.then(() => getLoadDuration());
}
let
ms = ('' + totalMs % 1000).padStart(3, '0'),
sec = ('' + Math.floor(totalMs / 1000) % 60).padStart(2, '0'),
duration = `00.00:00:${sec}.${ms}`;
return Promise.resolve(duration);
};
const getSessionId = () => {
let sessionId =
(sessionStorage &&
sessionStorage.getItem('sessionId')) || '';
if (!sessionId) {
sessionId = generateUUID();
(sessionStorage &&
sessionStorage.setItem('sessionId', sessionId));
}
return sessionId;
};
const trackPageview = async () => {
let id = generateUUID();
fetch(
'https://xxxxxxxxxxxx.in.applicationinsights.azure.com/v2/track',
{
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=UTF-8'
},
body: JSON.stringify(
[
{
data: {
baseData: {
id: id,
duration: await getLoadDuration(),
measurements: {},
name: document.title,
properties: {
refUri: document.referrer
},
url: document.location.href,
ver: 2
},
baseType: 'PageviewData'
},
iKey: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
name: 'Microsoft.ApplicationInsights.' +
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' +
'.Pageview',
tags: {
'ai.device.id': 'browser',
'ai.device.type': 'Browser',
'ai.internal.sdkVersion': 'javascript:2.8.4',
'ai.operation.id': id,
'ai.operation.name': document.location.pathname,
'ai.session.id': getSessionId()
},
time: new Date().toISOString()
}
]
)
}
);
};
trackPageview();
There are a few helper functions to start with:
- generateUUID: will generate a UUID that is used to set a unique ID for the event that gets sent over, and also to generate the session ID.
- getLoadDuration: uses the browser's Navigation Timing API to get the current page's load time and returns it in a timespan string.
- getSessionId: gets the current user's session ID from the browser's session storage, or creates and stores it there first if it's a new user session.
And then the main function here is trackPageView where the POST request is created and sent. This function is then called at the bottom of the script. And the script file containing the above is then loaded at the bottom of my webpage body to avoid any render blocking issues.
Since I'm not bothered anymore about supporting Internet Explorer, I am able to use modern JS in the script like arrow functions, string interpolation and the Fetch API.
That all helps to keep this script very small: it's only 1.7 KB (minified).
So this allows me now to view pageview data in the Application Insights dashboard in Azure, while still keeping my site load times very low.
If you wish to use the same script, then there are few variables you would need to update:
- The hostname of the URL used in the fetch command should be the same as the ingestion endpoint hostname in your Application Insights' connection string.
- The iKey and name properties in the JSON of the fetch command body should contain your Application Insights' instrumentation key.
After this the script should send the pageview data on page load to your Application Insights instance.
[Update 31 July 2023]
I've updated the code example above to truncate the duration figure, as otherwise it might include a double timestamp with fractional digits. That would result in an invalid duration string being sent to Application Insights.