The Beacon API is a browser API that maybe not many 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 very well-supported in modern browsers, including Chrome, Edge, Firefox, Safari, and many mobile browsers. For detailed browser support information, visit the caniuse.com site: https://caniuse.com/#feat=beacon
The API is straightforward 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, String 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 exists was to solve a specific problem analytics software had in
the browser. To collect as much 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 canceled 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 are going to 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.
false
seems to be returned when browser-specific limits were hit. See this Stack Overflow answer for more information.
During my tests, I've never reached these limits and, therefore, never encountered the false
return value.
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 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');
The first parameter represents a standard URL. You can send data with query parameters
navigator.sendBeacon('heartbeat?id=123');
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);
or a FormData object
// FormData
const formData = new FormData();
formData.append("session", "12345");
formData.append("id", 11);
navigator.sendBeacon('usageFormData', formData);
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);
or an 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);
sendBeacon()
sends a POST request with the data as the 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 examples, I use a Spring Boot backend. 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 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>
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 });
});
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.
The Page Lifecycle API, which defines events like unload
, is not recommended for sending analytics
data due to its unreliability, especially on mobile devices.
The pagehide
event is generally a better choice for detecting page unloads.
More details about the Page Lifecycle API and its caveats are available here:
https://developer.chrome.com/docs/web-platform/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));
});
Using performance.timing is deprecated.
Use the PerformanceObserver API for collecting performance metrics.
For more details on the Performance API, refer to:
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 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);
};
The error handler receives these parameters that describe the error.
message
: error message as a stringsource
: URL of the script where the error was raisedlineno
: Line number where the error was raisedcolno
: Column number for the line where the error occurrederror
: Error object.
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 permit 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));
});
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' 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.
The ReportingObserver API offers a way to programmatically access browser warnings and interventions. For further details, refer to: https://developer.chrome.com/docs/capabilities/web-apis/reporting-observer
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();
Each time the browser issues a warning, the call-back function passed to the ReportingObserver constructor is called.
Visit this page to learn more about the ReportingObserver API:
https://developer.chrome.com/docs/capabilities/web-apis/reporting-observer
Generic Sensor API ¶
The Generic Sensor API provides access to device sensors like accelerometers.
For current browser support and more information, see:
https://developer.chrome.com/docs/capabilities/web-apis/generic-sensor
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;
};
Conclusion ¶
This concludes our exploration of the Beacon API and its use cases. The Beacon API is useful for sending data in the background from the browser to a server.
Browser Compatibility: The Beacon API has excellent support in modern browsers. However, always test your implementation thoroughly across different browsers and versions to ensure consistent functionality.
Data Limits: The Beacon API is designed for sending small amounts of data. There are limits to the size of data that can be sent in a single beacon request. While the exact limits vary by browser, they are typically around 64KB.
Security: If you are sending sensitive data using the Beacon API, ensure appropriate security measures are in place on your server.
Alternative: In many cases, instead of the Beacon API you can send data with the Fetch API. Consider this if you need more control over the request or require a response from the server.