Skip to content

Server-Side Deduplication

Because the engagement plugin retries delivery aggressively (sendBeaconfetch 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.

FieldLocationMeaning
messageIdtop-levelUUID, unique per beacon — the dedup key for “is this the same physical send?” Collapse exact retries on this.
page_view_idpropertiesUUID, stable for one view of one URL; rolls on SPA nav (analytics.page() / popstate) and bfcache restore, not on tab-switch-back.
sequencepropertiesInteger counter, increments per beacon within a page_view_id. Order by this to reconstruct a view’s beacon stream.
  • Exact-retry dedup — collapse on messageId. Two rows with the same messageId are the same physical send; keep one.
  • Reconstruct a page-view — group by page_view_id and order by sequence. 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’s seconds is 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 at maxSeconds.

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.

{
"messageId": "ajs-next-...",
"properties": {
"pathname": "/articles/foo",
"seconds": 30,
"exitType": "heartbeat",
"page_view_id": "pv_...",
"sequence": 2,
"scroll_depth_max_px": 1840
}
}