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:

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:

  1. 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.
  2. 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.