An Idiosyncratic Blog

πŸ›° Analytics using Beacon API and Ping

Published on
β€’ 6 minutes read

Having some kind of data tracking mechanism in applications is essential to understand user behaviour and how you can improve your application to provide better experience to your users. From getting information on how a user interacts with a component, how far they’ve scrolled on a page, or which pages have they visited before they follow a CTA, tracking is fundamental to understanding how a user interacts with your webpage and provides valuable insights which helps grow your business.

Apart from understanding user behaviour, we can leverage tracking to collect logs from applications for debugging, create save points for user, or run A/B testing.

The OG

Usually when we want to send data to the server, we rely on an XMLHttpRequest or the Fetch API. These are asnychronous methods which enable us to send any amount of data to the server. But there is a drawback with these APIs. Suppose that you want to track data and send it but the user decides to leave the page and go somewhere else, if your request is still in-flight the data will never reach the server because it gets cancelled by the browser when user navigates away from your page.

function logStuff(){
  // data to send
  const data = {...};

  // prepare to send our data as FORM encoded
  const params = [];
  for (const name in data) {
    if (data.hasOwnProperty(name)) {
      params.push(encodeURIComponent(name) + '=' + encodeURIComponent(data[name]));
    }
  }
  const paramsJoined = params.join('&');

  // open a POST
  const xhr = new XMLHttpRequest();
  xhr.open('POST', 'https://service.com/api/analytics');
  xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');

  // send!
  xhr.send(paramsJoined);
}

window.addEventListener('unload', logStuff);

We listen to unload event on the document and send data to the server. The trouble here is that code running on one of the unload events can block execution and delay the unloading of the page. If unloading of the current page is delayed, then the loading next page is also delayed, and this leads to a sluggish navigation experience.

We'd have to keep in mind the network speed and latency involved in receiving and processing the request, and responding back to the client.

Another way to track is using redirects and query params. When a user clicks a CTA, instead of taking the user directly to the destination, we redirect the user to an intermediary service, which gathers all the required data, and then redirects the user to the destination service.

<!-- Using a redirection service to track CTA clicks -->
<a href="https://analytics.service.com?redirect=https://destination.service.com"></a>

The Alternatives

There are two strategies which we can leverage, depending on the browser support:

Beacon API

The Beacon API is used to send an asynchronous and non-blocking request to a web server. The request does not expect a response. Unlike requests made using XMLHttpRequest or the Fetch API, the browser guarantees to initiate beacon requests before the page is unloaded and to run them to completion.

The request will be executed only after more important tasks have completed their processing. This is important to know, because this means that the request won't necessarily trigger right away in some cases.

const status = {
  true: 'πŸš€ successful!',
  false: 'πŸ“ˆ unsuccessful!'
}
const logStuff = function() {
  // Test that we have support
  if (!navigator.sendBeacon) return true;

  // URL for the analytics serivice
  const url = '/api/analytics';

  // Prepare the data
  const data = JSON.stringify({...})

  // πŸš€
  const result = navigator.sendBeacon(url, data);
  console.log(status[result]) // => true | false
};

window.addEventListener('beforeunload', logStuff);

The result is boolean, true if the browser accepted and queued the request, and false if there was some problem.

If there is no Beacon support, we return true let the browser do it's thing. Returning false would cancel the event and stop the page from unloading, which we do not want.

The Ping Attribute

This is one of those attributes that's hidden within the docs, and not many realize the potential of it. The ping attribute allows you to execute a POST request to an URL or list of URLs specified, it sends the text PING as the request payload.

<!-- For a single request URL -->
<a href="https://service.com" ping="https://service.com/api/analytics"></a>

<!-- For multiple request URLS -->
<a
  href="https://service.com"
  ping="https://service.com/api/analytics/foo https://service-two.com/api/analytics/bar"
>
</a>

Now a drawback here is that we cannot send a custom POST body. Instead, we can use query parameters to send data, and since this is a POST request, we can leverage cookies to add additional information to the request.

<!-- For a request URL with query params -->
<a href="https://service.com" ping="https://service.com/api/analytics?page=product-one"></a>

This property is not supported by all browsers but the fact that many popular browsers support it is an advantage.

Wrapping up

Beacon API and the ping attribute are some useful ways to track and log analytics data when you are unable to use the old ways of an XMLHttpRequest or the Fetch API. The goal of this post was to show how we can leverage alternate APIs to implement logging and tracking in a non-blocking and performant manner.

Remember to always take into consideration your user's privacy and do not take it lightly, make sure to only track what is necessary and keep data anonymized. Also respect the DNT: 1 headers when applicable.

Until next time! ✌🏽