Sending analytics data to a server is crucial for understanding user behavior on your website. Historically, there were several ways to do this, each with limitations.
First came image tags. Image tags (often called "tracking pixels") work by embedding a small, usually transparent image on the page. The clever aspect is that every time the image is loaded, a request is made to the server, allowing data to be passed via the URL's query string. This was an early method of sending data without requiring JavaScript. However, the downsides include the inability to send large or complex data, reliance on HTTP GET requests (limiting the amount of data), and potential privacy concerns as the data could be easily exposed in the URL. Additionally, image tags lack reliability for analytics since the image might not load before the user leaves the page, resulting in lost data.
Sending data using XMLHttpRequest and then fetch were improvements. They enabled sending more data, different types of data, and provided control over when they were sent — e.g., when a button is clicked rather than whenever the image tag loads.
But they were still flawed for the purposes of sending analytics data in particular, because we typically want to gather analytics data at the end of the page lifecycle — we want to know what was done and what led to the user navigating to a new page — and these methods aren’t really optimized for sending data after a page is closing.
Fetch has an option, keepalive, that helps ensure that data will be sent, but it has limitations. For example, the keepalive option doesn’t guarantee that data will be sent if the user closes the browser or loses connectivity.
Today, the best practice is to use the navigator.sendBeacon API, designed specifically for sending payloads up to 64kb with minimal overhead. Unlike fetch or XMLHttpRequest, sendBeacon is intended to work even when the user is leaving the page, it’s widely available, and pretty reliable — making it the best choice for tracking analytics.
SendBeacon works like fetch but with fewer options: you invoke it with a URL and a payload of data. The browser synchronously returns true or false, and the browser handles the rest. That’s it. No confirmation or additional info. This makes it super easy to implement, but it means you don’t really know what happened with your data.
The true/false value you get back from the browser doesn’t tell you that the payload arrived successfully at your endpoint — it doesn’t even mean that the payload was sent. It just means that the browser added it to its queue — which is handled internally by the browser. Whether it gets sent or not depends on various factors including how many other items are in the queue, network conditions, and the total size of payloads in the queue.
To avoid problems:
- Make sure your payload is within the 64kb size limit — don’t try sending it if it is.
- Don't overload the browser's queue — too many items that add up to over 64kb can also cause a problem.
- If a payload is rejected, use a custom queue and try again later. I handled this by adding blobs to a queue that I cycle through later — say, using requestIdleCallback.