Server-Side Deduplication
Because the engagement plugin retries delivery aggressively (sendBeacon →
fetch keepalive → a replayed localStorage queue), and because the browser
itself can silently re-deliver a sendBeacon, your server will sometimes see the
same logical event twice. Every beacon carries three identifiers so you can
collapse those retries and reconstruct each page-view’s stream in order.
| Field | Location | Meaning |
|---|---|---|
messageId | top-level | UUID, unique per beacon — the dedup key for “is this the same physical send?” Collapse exact retries on this. |
page_view_id | properties | UUID, stable for one view of one URL; rolls on SPA nav (analytics.page() / popstate) and bfcache restore, not on tab-switch-back. |
sequence | properties | Integer counter, increments per beacon within a page_view_id. Order by this to reconstruct a view’s beacon stream. |
How to use them
Section titled “How to use them”- Exact-retry dedup — collapse on
messageId. Two rows with the samemessageIdare the same physical send; keep one. - Reconstruct a page-view — group by
page_view_idand order bysequence. That yields the ordered beacon stream for a single view of a single URL: the initial dispatch, each heartbeat, and the terminal beacon. - Sum engagement correctly — within a
page_view_id, each beacon’ssecondsis the delta since the previous beacon (heartbeats use peek-then-drain), so total engaged time for the view is the sum across the sequence, capped atmaxSeconds.
Why client-side dedup isn’t enough
Section titled “Why client-side dedup isn’t enough”The plugin has an in-process dedup flag, but it can’t catch retries that happen
outside the page’s lifetime — a sendBeacon the browser re-delivers after
unload, or a queued beacon replayed on the next page load. Those only meet on
the server, which is why the identifiers travel on the wire. The server is the
authoritative dedup boundary.
Example wire shape
Section titled “Example wire shape”{ "messageId": "ajs-next-...", "properties": { "pathname": "/articles/foo", "seconds": 30, "exitType": "heartbeat", "page_view_id": "pv_...", "sequence": 2, "scroll_depth_max_px": 1840 }}