Compass Web SDK: Events & Reliability
This page covers what you send (the track() API and event shape) and how the
SDK gets it there reliably (batching, page-unload delivery, and retries).
Tracking events
ArcCompass.track(eventType, props?);eventType is one of:
| Event | Typical use |
|---|---|
page_view | A new page or route is shown. |
click | The user clicks a tracked element. |
video_start | Video playback begins. |
video_progress | Video has reached a checkpoint (eventValue = fraction watched, 0.0–1.0). |
video_complete | Video reaches 100% (or near-100%). |
search | The user runs a search query. |
share | The user shares an item. |
These are the event types the browser SDK emits — a client-side subset of the
full Content Recommendations taxonomy. For the complete server-side catalog
(including article_save, deepest_scroll, and engaged_read, which are
ingested through the API rather than the browser SDK), see Supported Event Types.
props (all optional):
| Field | Type | Description |
|---|---|---|
itemId | string | Overrides the resolved item ID for this event only. |
siteId | string | Overrides the resolved site ID for this event only. |
eventValue | number | Numeric value (e.g. video watch fraction). |
sessionId | string | Overrides the SDK-managed session ID for this event. |
timestamp | Date | Overrides the event timestamp (default: now). |
Item ID resolution
The collector requires an item_id on every event. The SDK resolves it in this
order:
props.itemIdpassed totrack().<meta name="arc-compass:item-id" content="…">read from the page.
The resolved item_id must match the ID the content was ingested under. The
SDK forwards it verbatim and never derives it from the URL or invents a default.
If neither source resolves, the event is dropped with a console.warn.
For multi-page apps, set the meta tag once in HTML:
<meta name="arc-compass:item-id" content="ARTICLE-ABC123" />Site ID resolution
site_id attributes an event to the Site it originated on, for tenants that run
more than one Site. Unlike item_id, it is optional — single-site tenants
omit it entirely. The SDK resolves it in the same order as item_id:
props.siteIdpassed totrack().<meta name="arc-compass:site-id" content="…">read from the page.
Use the same Site identifier the content was ingested under (the site_id on the
content record) so events and items line up. When neither source resolves, the
event is still sent — just without a site_id; it does not affect the
collector’s derived event_id.
<meta name="arc-compass:site-id" content="the-daily" />Automatic page_view
By default the SDK fires a page_view for you — on initial load and on SPA
route changes — so you do not have to call track() on every page. Each fire
re-reads the arc-compass:item-id meta tag (and the arc-compass:site-id tag),
so the event is attributed to the current page’s content and Site. When the item
tag is absent for a page, no page_view fires (the SDK warns once) and you
should call track('page_view', { itemId }) yourself.
Turn it off with the autoPageView init option:
ArcCompass.init({ domain, apiKey, autoPageView: false });Single-page apps
Automatic capture detects navigation via the History API (pushState,
replaceState, popstate). It is reliable when the host updates the
arc-compass:item-id tag per route (server-rendered pages, or Next/Helmet-style
head management). Pure client-rendered apps that do not update the tag should set
autoPageView: false and track each route explicitly:
router.afterEach((to) => { ArcCompass.track("page_view", { itemId: to.params.articleId });});search and other item-less events
search events do not naturally correspond to a content item. Because the
collector requires an item_id on every event, pass a synthetic ID per call:
ArcCompass.track("search", { itemId: "search:" + encodeURIComponent(query) });Envelope shape
The SDK builds and POSTs event envelopes shaped like this:
{ "user_id": "…", "item_id": "…", "event_type": "page_view", "timestamp": "2026-03-27T14:30:00.000Z", "session_id": "…", "site_id": "the-daily", "event_value": 0.75}event_value and site_id are omitted when not provided.
Reliability
Batching
Events are buffered in memory and sent in batches via POST /events/batch. A
flush is triggered by whichever fires first:
batch.maxEventsreached (default 20).batch.flushIntervalMselapsed since the last flush (default 2000 ms).- The page enters
visibilitychange→hidden(tab backgrounded, app switched, etc.). - The page emits
pagehide(hard nav, tab close). - The host calls
ArcCompass.flush().
The collector caps requests at 100 events; the batcher splits larger flushes into multiple requests automatically.
Page-unload delivery
Both visibilitychange and pagehide trigger a fetch call with
keepalive: true. The browser keeps the request alive past page teardown, so
events fired late in the page lifecycle still reach the collector.
Failure handling
A batch send that hits a transient failure is retried with bounded
exponential back-off; everything else is dropped immediately. The SDK never
throws — failures surface only when debug: true is set, or through the
onError callback for terminal batch drops (see Error visibility).
Retry policy
- Retried:
5xxresponses and network / CORS errors. - Not retried:
4xx(including429). A4xxwill not succeed on re-send, and rate-limit handling is the collector’s concern, not the client’s — so these are terminal and the batch is dropped. - Back-off: exponential with full jitter — each retry waits a random
interval in
[0, 1s × 2^attempt](nominal ~1s then ~2s). - Bounds: at most 2 retries (3 attempts total) and at most 30s of total wall-clock across attempts and waits, whichever comes first.
- When the budget is exhausted, the batch is dropped, the last error is logged
via
console.errorunderdebug, andonError(if set) is called.
Unload flushes get a minimal budget
A flush triggered by visibilitychange
→ hidden or pagehide runs a single keepalive attempt with no
retries. The page is being torn down, so blocking on back-off would risk
losing the request entirely; the one keepalive request already survives past
teardown.
No cross-page persistence
Events are not persisted across page loads. A
persistent retry queue (localStorage / IndexedDB) would raise consent and
correctness questions — replaying events captured under one consent state after
the state has changed — so it is deliberately out of scope.
De-duplication (why retries are safe)
Retries re-POST the byte-identical
request body: the batcher drains and builds each envelope once, then the
transport replays that exact payload on every attempt. The collector derives a
stable event identity from the event’s natural fields (user_id, item_id,
event_type, timestamp, session_id, attribution) when the client omits an
ID, so identical re-sends collapse to the same event downstream. The SDK
therefore does not mint its own event ID.
In-memory queue cap
The queue holds at most queue.maxBuffered events (default 500). On overflow,
the oldest event is dropped to make room and a console.warn fires so engineers
notice sustained collector outages during development.
Concurrency
Flushes are serialized — at most one HTTP request is in flight at a time. Subsequent flush triggers wait for the in-flight request to resolve before draining more events.