This document maintains the list of alternatives that have been considered to achieve better beaconing. Note that only PendingBeacon API has ever been implemented.
See the fetch() with PendingRequest API explainer.
See the PendingBeacon API explainer.
A DOM-based API was considered as an alternative.
This API would consist of a new possible beacon
value for the rel
attribute
on the link tag, which developers could use to indicate a beacon,
and then use standard DOM manipulation calls to change the data, cancel the beacon, etc.
The stateful JS API was preferred to avoid beacon concerns intruding into the DOM, and because a ‘DOM-based’ API would still require scripting in many cases anyway (populating beacon data as the user interacts with the page, for example).
Another alternative is to introduce (yet) another page lifecycle event,
that would be essentially the unload
event, but supported by the BFCache -
that is, its presence would not disable the BFCache, and the browser would execute this callback even on eviction from the BFCache.
This was rejected because it would require allowing pages frozen in the BFCache to execute a JavaScript callback,
and it would not be possible to restrict what that callback does
(so, a callback could do things other than sending a beacon, which is not safe).
It also doesn’t allow for other niceties such as resilience against crashes or batching of beacons,
and complicates the already sufficiently complicated page lifecycle.
Another alternative is to extend the Fetch API to support the requirements.
The existing Fetch with keepalive
option, combined with visibilitychagne
listener, can approximate part of (1):
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
fetch('/send_beacon', {keepalive: true});
// response may be dropped.
}
});
or a new option deferSend
may be introduced to cover the entire (1):
// defer request sending on `hidden` or bfcahce eviction etc.
fetch('/send_beacon', {deferSend: true});
// Promise may not resolve and response may be dropped.
However, there are several problem with this approach:
- The Fetch API shape is not designed for this (1) purpose. Fundamentally,
window.fetch
returns a Promise with Response to resolve, which don't make sense for beaconing at page discard that doesn't expect to process response. - The (1) mechanism is too unrelated to be added to the Fetch API. Even just with a new option, bundling it with a visibility event-specific behavior just seems wrong in terms of the API's scope.
- The Fetch API does not support updating request URL or data. This is simply not possible with its API shape. Users have to re-fetch if any update happens.
The above problems suggest that a new API is neccessary for our purpose.
NOTE: Discussions in WebKit's standard position.
Another alternative is to extend the [navigator.sendBeacon
] API:
navigator.sendBeacon(url): bool
navigator.sendBeacon(url, data): bool
To meet the requirements and to make the new API backward compatible, we propose the following shape:
navigator.sendBeacon(url, data, fetchOptions): PendingBeacon
An optional dictionary argument fetchOptions
can be passed in, which changes the return value from bool
to PendingBeacon
proposed in the above section. Some details to note:
- The proposal would like to support both
POST
andGET
requests. As the existing API only supportPOST
beacons, passing infetchOptions
withmethod: GET
should enable queuingGET
beacons. fetchOptions
can only be a subset of the Fetch API'sRequestInit
object:method
: one ofGET
orPOST
.headers
: supports custom headers, which unblocks #50.body
: not supported. POST body should be indata
argument.credentials
: enforcingsame-origin
to be consistent.cache
: not supported.redirect
: enforcingfollow
.referrer
: enforcing same-origin URL.referrerPolicy
: enforcingsame-origin
.keepalive
: enforcingtrue
.integrity
: not supported.signal
: not supported.- The reason why
signal
andAbortController
are not desired is that we needs more than just aborting the requests. It is essential to check a beacon's pending states and to update or accumulate data. Supporting these requirements via the returnedPendingBeacon
object allows more flexibility.
- The reason why
priority
: enforcingauto
.
data
: ForGET
beacon, it must benull
orundefined
.- The return value must supports updating request URL or data, hence
PendingBeacon
object.
- The above API itself is enough for the requirements (2) and (3), but cannot achieve the requirement (1), delaying the request.
- The function name
sendBeacon
semantic doesn't make sense for the "delaying" behavior. - Combing the subset of
fetchOptions
along with the existingdata
parameter are error-proning.
To imprvoe from "Extending navigator.sendBeacon()
API, it's better with a new function:
navigator.queueBeacon(url, fetchOptions, beaconOptions): PendingBeacon
This proposal gets rid of the data
parameter, and request body should be put into fetchOptions.body
directly.
The extra beaconOptions
is a dictionary taking backgroundTimeout
and timeout
to support the optional timeout after bfcache or hidden requirement.
At the end, this proposal also requires an entirely new API, just under the existing navigator
namespace. The advantage is that we might be able to merge this proposal into w3c/beacon and eliminate the burden to maintain a new spec.
This is similar to the proposed PendingBeacon API but there is no pending
and no setData()
.
There are 2 classes of beacon with a base class that has
url
method
sendNow()
deactivate()
- API for specifying timeouts
With these APIs, the page cannot check whether the beacon has been sent already.
It's unclear that these APIs can satisfy all use cases. If they can, they have the advantage of being easier to implement and simple to use.
Has appendData(data)
which appends new data to the beacon's payload.
The beacon will flush queued payload according to the timeouts and the browser state.
The use-case is for continuously logging events that are accumulated on the server-side.
Has replaceData(data)
which replaces the current the beacon's payload.
The beacon will send the payload according to the timeouts and the browser state.
If a payload has been sent already, replaceData simply stores a new payload to be sent in the future.
The use case is for logging a total-so-far. The server would typically only pay attention to the latest value.