Skip to content

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:

EventTypical use
page_viewA new page or route is shown.
clickThe user clicks a tracked element.
video_startVideo playback begins.
video_progressVideo has reached a checkpoint (eventValue = fraction watched, 0.0–1.0).
video_completeVideo reaches 100% (or near-100%).
searchThe user runs a search query.
shareThe 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):

FieldTypeDescription
itemIdstringOverrides the resolved item ID for this event only.
siteIdstringOverrides the resolved site ID for this event only.
eventValuenumberNumeric value (e.g. video watch fraction).
sessionIdstringOverrides the SDK-managed session ID for this event.
timestampDateOverrides the event timestamp (default: now).

Item ID resolution

The collector requires an item_id on every event. The SDK resolves it in this order:

  1. props.itemId passed to track().
  2. <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:

  1. props.siteId passed to track().
  2. <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.maxEvents reached (default 20).
  • batch.flushIntervalMs elapsed since the last flush (default 2000 ms).
  • The page enters visibilitychangehidden (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: 5xx responses and network / CORS errors.
  • Not retried: 4xx (including 429). A 4xx will 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.error under debug, and onError (if set) is called.
Unload flushes get a minimal budget

A flush triggered by visibilitychangehidden 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.

See also