Skip to content

Compass Web SDK: Events & Reliability

Last updatedView as Markdown

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.
recommendation_exposureA rendered recommendation became visible. Carries attributionContext.
recommendation_clickThe user clicked a rendered recommendation. Carries attributionContext.
catch_up_shownA Catch Up card segment became visible (one per material segment). Carries attributionContext.
catch_up_clickThe reader clicked through to a story in the Catch Up card. Carries attributionContext.
catch_up_dismissThe reader completes the takeover by reaching the “I’m caught up” end state. An early close (the ✕, Escape, or navigating away) does not fire it. Carries attributionContext.

The recommendation_exposure and recommendation_click types carry an attributionContext and are how host-rendered surfaces close the Confirmed-CTR loop — you rarely emit them by hand. See Recommendation Attribution.

The catch_up_* types are emitted only by the Catch Up card Experience, which fires them itself — you never call track() for them. They carry the card’s exposureId and, on catch_up_shown / catch_up_click, the moved segment’s attributionId, tagged source: "fy-catchup", so a click confirms against its impression. Like every SDK event they are consent-gated: none fire until the reader has granted consent.

These are the event types the browser SDK emits — a client-side subset of the full Compass 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.
timestampDate | string | numberOverrides the event timestamp (default: capture time). Accepts a Date, a parseable ISO-8601 string, or epoch milliseconds; an unparseable value falls back to the capture time.
attributionContextAttributionContextAttribution for recommendation_exposure / recommendation_click events. See Recommendation Attribution.

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, so you do not have to call track() on every page. The SDK watches the arc-compass:item-id tag: a page_view fires each time that tag reads back a value different from the one it last reported, and the arc-compass:site-id tag is read at the same moment. That covers the initial load and every route change in a single-page app.

When no item tag ever resolves 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

Write the item-id tag whenever your route knows the ID: before or after pushState, in a render effect, or once an awaited fetch resolves. There is no ordering you have to honour. The SDK reads the tag when you write it, so it cannot read it too early, however long your router, data loader, and render take.

// React Router + any head manager. No SDK call, and no ordering requirement.
function Article({ id }) {
const { data } = useArticle(id); // resolves after the route has rendered
return (
<>
<Helmet>
{data && <meta name="arc-compass:item-id" content={data.itemId} />}
</Helmet>
{/* … */}
</>
);
}

Two route changes produce no page_view:

  • A change that lands on the same item. A query or hash change, or a replaceState that only normalises the URL, is not a new view of new content.
  • A route with no item, such as a homepage or a section front. Clear the tag, or leave it out, on those routes. Returning to an article you viewed earlier fires again.

If you want a page_view per URL rather than per item, or you would rather drive capture yourself, 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 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: 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 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_id from the event’s fields, so identical re-sends collapse to the same event downstream — see the events endpoint in the API Developer Guide. 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