A closer look at the Beacon API

Published: September 05, 2018  •  javascript

The Beacon API is a browser API that maybe not a lot of people heard of or us it in their daily work, unless they do a lot of analytics reporting in the browser.

The Beacon API is well-supported in most browsers, except (you already guess it) IE11. Visit the caniuse.com site to see the current support for this API: https://caniuse.com/#feat=beacon

The API is very simple and consists of just one method in the navigator object.

  navigator.sendBeacon(url [, data]);

The Beacon API sends a POST request to the URL you provide as the first parameter with optional data that you pass as the second parameter. The Beacon API supports ArrayBufferView, Blob, DOMString and FormData as second parameter. The API does not expect a response, if your back end sends one the browser ignores it.

The reason why the Beacon API exist was to solve a specific problem analytics software had in the browser. To collect as many data as possible analytics libraries tried to send the collected data at the last possible moment to the server. This moment is the unload handler. Unfortunately asynchronous requests that are initiated in the unload handler are cancelled by the browser. As a workaround the libraries sent synchronous XMLHTTPRequest requests, but this had the problem that it delayed the navigation to the next page.

The solution is the Beacon API. Requests sent with this API will be sent to the server even when the user already navigated away to the next page.

The method sendBeacon() returns true if the browser was successfully able to queue the request and false otherwise. In my tests I was not able to force a false return value, even when the browser was offline the method returned true. If somebody knows under what circumstances the method returns false, let me know Feedback.

The browser tries to send the request immediately after internally queueing it. The Beacon API only tries to send the request once and it does not care if the URL is wrong, the server is not responding, sends an error back or if the browser is offline. The Chrome browser just prints an error message into the console and then deletes the request from the queue. There is no retry mechanism built in, nor does the browser queues these requests while offline and then sends them the next time the device is online.


Usage

An application can call the sendBeacon method without any data.

    navigator.sendBeacon('heartbeat');

usage.html

The first parameter represents a normal URL, you could therefore send data with query parameters

    navigator.sendBeacon('heartbeat?id=123');

usage.html

The second parameter of the sendBeacon method data is either a string

    // String
    const data = JSON.stringify({
      location: window.location,
      time: Date()
    });
    navigator.sendBeacon('usageString', data);

usage.html

or a FormData object

    // FormData
    const formData = new FormData();
    formData.append("session", "12345");
    formData.append("id", 11);
    navigator.sendBeacon('usageFormData', formData);

usage.html

or a Blob

    // Blob
    const ua = JSON.stringify({ ua: navigator.userAgent, now: performance.now() });
    const headers = { type: 'application/json' };
    const blob = new Blob([ua], headers);
    navigator.sendBeacon('usageBlob', blob);

usage.html

or a ArrayBufferView. In this example a Uint8Array.

    // ArrayBufferView
    const string = '=======This is a text, sent compressed to the server=======';
    const enc = new TextEncoder();
    const encoded = enc.encode(string);
    const compressed = pako.deflate(encoded);

    navigator.sendBeacon('usageArrayBufferView', compressed);

usage.html

sendBeacon() sends a POST request with the data as body to the server. There is no way to change the http method, the Beacon API only supports POST requests and it does not expect a response.

I wrote a Spring Boot backend for testing these examples. You find the source code that handles these requests here: https://github.com/ralscha/blog/blob/master/beacon/src/main/java/ch/rasc/beacon/UsageController.java


Examples

Next we look at a few examples that use the Beacon API to send data from a browser to a server. For all these example I use a Spring Boot back end. You find the source code here:
https://github.com/ralscha/blog/blob/master/beacon/src/main/java/ch/rasc/beacon/ExamplesController.java

Unless we want to send data in the unload event handler, the Beacon API does not offer any additional benefits. Only in the first two examples the Beacon API is required because the application sends data in the unload handler. In all the other examples we could simply use the Fetch API or the XMLHTTPRequest object.


Page Lifecycle

First we look at an example, that sends data in the unload handler. We have a simple index.html page

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Lifecycle: Page 1</title>
</head>

<body>
  <h1>Lifecycle: This is Page 1</h1>

  <div>
    <a href="page2.html">Go to Page 2</a>
  </div>
  <script src="main.js"></script>
</body>

</html>

index.html

that runs this JavaScript code

const analytics = { start: performance.now(), visibility: [] };

window.addEventListener('unload', event => {
  analytics.stop = performance.now();
  navigator.sendBeacon('../lifecycle', JSON.stringify(analytics));
});

document.addEventListener('visibilitychange', event => {
  analytics.visibility.push({ state: document.visibilityState, ts: event.timeStamp });
});


main.js

When the user clicks on the "Go to Page 2" link the browser emits the unload event and our code sends some analytics data to the server. Because we use the Beacon API, we make sure that the browser does not cancel the call and sends the data to the server even we have already navigated away from the page. Because the sendBeacon() call is asynchronous the user does not have to wait for the page navigation to happen.

If you want to learn more about the Page Lifecycle API visit this article:
https://developers.google.com/web/updates/2018/07/page-lifecycle-api


Performance

The second example is almost identical to the first. This time we send the built-in performance.timing object to the server. The Performance interface provides access to performance-related information for the current page.

window.addEventListener('unload', event => {
  navigator.sendBeacon('../performance', JSON.stringify(performance.timing));
});


main.js

More information about the Performance API:
https://developer.mozilla.org/en-US/docs/Web/API/Performance


Global Error Handling

When a browser encounters an error, he prints a message into the browser console. For the developers this is not very useful because they usually cannot look on to the user's computer. To solve that we can send the messages that the browser emits to the server. On the server we can store them into a database, send emails to the developers or automatically open a ticket in an issue management system.

To install a global error handler we only have to listen for the error event on the window object.

    window.onerror = function (msg, url, line, col, error) {
      const formData = new FormData();
      formData.append('url', url);
      formData.append('line', line);
      formData.append('col', col);
      formData.append('error', error);
      navigator.sendBeacon('../clientError', formData);
    };

index.html

The error handler receives these parameters that describe the error

Alternatively we can install a global error handler with addEventListener. In this case the handler method has a different signature and only receives an event object.

   window.addEventListener('error', event => { ... })

The event object contains these read-only properties:
event.message, event.filename, event.lineno, event.colno and event.error. They correspond to the parameters of the onerror handler.


Position

With the Geolocation API a web application can access the position of a user. For privacy reasons, the browser must ask the user if he wants to give permission to report his location data.

If the user gives permission we can listen on location changes with the navigator.geolocation.watchPosition method. The example then sends the latitude, longitude and timestamp with the Beacon API to the server.

      navigator.geolocation.watchPosition(pos => {

        document.getElementById('latitude').innerHTML = pos.coords.latitude;
        document.getElementById('longitude').innerHTML = pos.coords.longitude;

        const position = {
          latitude: pos.coords.latitude,
          longitude: pos.coords.longitude,
          ts: pos.timestamp
        };
        navigator.sendBeacon('../position', JSON.stringify(position));
      });

index.html

Visit MDN for more information about the GeoLocation API:
https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API


Reporting Observer

If you are surfing the web with Chrome and you look into the browser console you maybe already noticed some [Intervention] and [Deprecation] warnings.

Like error messages these warnings could be useful for a developer to improve his application. Unfortunately we cannot see them on the users's browsers and users do not notice them because they usually don't open the browser console.

Unfortunately the error event handler does not capture these messages, because these warnings are generated directly by the browser. The error handler is only called for runtime errors caused by our code.

To fill this gap Google added a new API to Chrome 69: ReportingObserver. It provides a programmatic way to access these browser warnings.

    const observer = new ReportingObserver((reports, observer) => {
      for (const report of reports) {
        navigator.sendBeacon('../reportObserver', JSON.stringify(report.body, ['id', 'columnNumber', 'lineNumber', 'message', 'sourceFile']));
      }
    }, { types: ['intervention', 'deprecation'], buffered: true });

    observer.observe();

index.html

Each time the browser issues a warning the callback function passed to the ReportingObserver constructor is called.

Visit this page to learn more about the ReportingObserver API:
https://developers.google.com/web/updates/2018/07/reportingobserver


Generic Sensor API

Another experimental API and, at the time of writing this blog post, only available in Google's Chrome browser is the Generic Sensor API. It gives a web application access to all kind of sensors that a modern smartphone has built-in.

In this example we access the accelerometer, listen for the reading event and send the sensor data to the server.

    const sensor = new Accelerometer();
    sensor.start();

    sensor.onreading = () => {
      const formData = new FormData();
      formData.append('x', sensor.x);
      formData.append('y', sensor.y);
      formData.append('z', sensor.y);
      navigator.sendBeacon('../sensor', formData);

      document.getElementById('x').innerText = sensor.x;
      document.getElementById('y').innerText = sensor.y;
      document.getElementById('z').innerText = sensor.z;
    };

index.html

Visit this page for more information:
https://developers.google.com/web/updates/2017/09/sensors-for-the-web