Beacon

The global navigator object reflects the identity of the browser and describes its state. Navigator properties refer to the platform on which the browser is functioning, indicate the user language, or extract information corresponding to the value of the User-Agent request header. In addition to revealing the chief traits of the browser, the navigator has a number of methods enhancing the browser capabilities. One of recently introduced methods is sendBeacon() - a lightweight alternative to the XMLHttpRequest: the method allows the browser to send small chunks of HTTP data to a remote server.

Beacon is primarily designed to meet the needs of analytics and diagnostics code. Such code can monitor performance of a Web site, track the user behavior, register a period of time spent by the user on the site, count the number of clicks, or fix other criteria of the user's interest in the site contents. Analytics data is usually sent as a payload of a synchronous XMLHttpRequest in an unload event handler. The synchronous XHR has its own drawbacks: it "freezes" the page and slows down the next navigation. Using sendBeacon() makes the data transfer less painstaking: when the method is called the browser forms an asynchronous POST request. This operation returns a boolean value showing the state of the HTTP message: if the data is successfully queued for transfer, the method returns true.

Beacon Data Representation

The beacon data can be represented as a string, a Blob, a typed array, or a FormData instance. The data is passed to the sendBeacon() method along with the destination URL of the server application. Here's an example of attaching an unload event listener to the window object and sending a piece of analytics data to a server script:

window.addEventListener("unload", sendData, false);
. . .
function sendData() {
 navigator.sendBeacon("http:/example.com/save-analytics-data.php", analyticsData);
}

To exemplify the posting of the beacon data as a string, we'll consider the following scenario: the client-side code gathers elementary information about the browser identity, then sends the collected data to the server.

var data={"userAgent": navigator.userAgent, "platform": navigator.platform, "language": navigator.language};
navigator.sendBeacon("http://example.com/save-beacon-data.php", JSON.stringify(data));

The browser encodes the beacon data as UTF-8 and builds a conventional POST request:

POST http://example.com/save-beacon-data.php HTTP/1.1
general headers
request headers
Content-Length: 119 entity header
Content-Type: text/plain;charset=UTF-8 entity header

. . . beacon data as text. . . entity body

Let's assume the server keeps the aggregated data in an SQLite database. The database may have a BROWSERS table with three columns:

$db = new SQLite3("browsers.db");
$obj=json_decode($HTTP_RAW_POST_DATA);
$ua=$obj->{"userAgent"}; $os=$obj->{"platform"}; $lang=$obj->{"language"};
$sql='INSERT INTO BROWSERS VALUES ("' .$ua.'","'.$os.'","'.$lang.'")';
$db->exec($sql);
$db->close();

It is evident that the beacon functionality is not reduced to analytics only: it can be easily employed in any scenario demanding that the user data should be saved before the Web page unloads. For example, an online graphics editor handles pointer events dispatched to a canvas and in this way enables the user to "draw" on the canvas. The user may close the application window at any point of time, but the sketch already created should be preserved anyway. To achieve the purpose, the client-side code can save the canvas as a Blob and then upload the Blob to the server backend:

document.getElementById("online-editor-canvas").toBlob(sendCanvasData);
function sendCanvasData(blob) {
 navigator.sendBeacon("http://example.com/save-canvas-as-png.php", blob);
}

In this case the browser builds the HTTP request according to correlation between the Blob and its possible MIME type. The Blob derived from the canvas is posted as a PNG image:

POST http://example.com/save-canvas-as-png.php HTTP/1.1
general headers
request headers
Content-Length: 3100 entity header
Content-Type: image/png entity header

. . . PNG data . . . entity body

The beacon data posted as an opaque binary object can be saved as a file on the server or inserted in a database. We assume that the database in the example below has an IMAGES table with a column for binary large objects:

. . . saving the Blob as PNG . . .
$file=fopen("canvas.png", "w");
fwrite($file, $HTTP_RAW_POST_DATA);
fclose($file);
. . . placing the Blob in the database . . .
$images = new SQLite3("images.db");
$stmt=$images->prepare('INSERT INTO IMAGES VALUES (:data)');
$stmt->bindValue(':data', $HTTP_RAW_POST_DATA, SQLITE3_BLOB);
$stmt->execute();
$images->close();

The same canvas could be saved as an ArrayBufferView; the typed array is then sent to the server as the beacon data:

var canvas=document.getElementById("online-editor-canvas");
var context=canvas.getContext("2d");
var imageData=context.getImageData(0, 0, 500, 500);
var samples=imageData.data; // Uint8ClampedArray instance
navigator.sendBeacon("http://example.com/save-rgba.php", samples);

The browser preparing a request to send the ArrayBufferView qualifies the request entity as application/octet-stream:

Content-Type: application/octet-stream

The last scenario deals with the beacon data represented as a FormData object. Sending a FormData instance may prove useful in situations when the user fills in a form, interrupts his work, then returns to the same form after a while. The user data already inserted in the form should be saved as a draft:

var formData=new FormData(document.forms[0]);
navigator.sendBeacon("http://example.com/save-form-data.php", formData);

This time the browser represents the request entity body as multipart/form-data:

request line
general headers
request headers
Content-Length: 43395 entity header
Content-Type: multipart/form-data; boundary=---------------------------15882401912257 entity header

-----------------------------15882401912257
Content-Disposition: form-data; name="name" entity part header

Bertie entity part body
-----------------------------15882401912257
Content-Disposition: form-data; name="surname" entity part header

Wooster entity part body
-----------------------------15882401912257
Content-Disposition: form-data; name="age" entity part header

29 entity part body
-----------------------------15882401912257
Content-Disposition: form-data; name="email" entity part header

wooster@leave-it-to-jeeves.com entity part body
-----------------------------15882401912257
Content-Disposition: form-data; name="phone-number" entity part header

+1111111111 entity part body
-----------------------------15882401912257
Content-Disposition: form-data; name="CV"; filename="CV.pdf" entity part header
Content-Type: application/pdf entity part header

. . . PDF file contents . . . entity part body

-----------------------------15882401912257--

The curriculum vitae of a prospective employee can be stored in a server database:

$CV = new SQLite3("cv.db");
$stmt=$CV->prepare('INSERT INTO CV VALUES (:name, :surname, :age, :email, :phone, :cvfile)');
$stmt->bindValue(':name', $_POST["name"], SQLITE3_TEXT);
$stmt->bindValue(':surname', $_POST["surname"], SQLITE3_TEXT);
$stmt->bindValue(':age', $_POST["age"], SQLITE3_INTEGER);
$stmt->bindValue(':email', $_POST["email"], SQLITE3_TEXT);
$stmt->bindValue(':phone', $_POST["phone-number"], SQLITE3_TEXT);
move_uploaded_file($_FILES["CV"]["tmp_name"], "CV.pdf");
$file=fopen("CV.pdf", "r");
$stmt->bindValue(':cvfile', $file, SQLITE3_BLOB);
$stmt->execute();
fclose($file);
$CV->close();

Let's summarize. The sendBeacon() method is efficient enough for the developer to save himself the trouble of finding tweaks to send data when the document unloads. The main application of the beacon is the posting of the aggregated analytics data. However, the beacon can be used in other scenarios obliging the developer to save a snapshot of data and upload it to the server. Multiple formats of the beacon data may pass a series of transformations: a text, binary objects, or form items sent by the browser can undergo further changes on the server before being stored.