# Compass Web SDK: Events & Reliability

Source: https://dev.arcxp.com/content-recommendations/sdk/events-reliability/

The track() API, event types, item-ID and site-ID resolution, automatic page_view capture, and the envelope shape — plus how the Compass Web SDK batches, retries, and delivers events reliably across the page lifecycle.

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

```ts
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.                                                                                                                                                          |
| recommendation\_exposure | A rendered recommendation became visible. Carries attributionContext.                                                                                                             |
| recommendation\_click    | The user clicked a rendered recommendation. Carries attributionContext.                                                                                                           |
| catch\_up\_shown         | A Catch Up card segment became visible (one per material segment). Carries attributionContext.                                                                                    |
| catch\_up\_click         | The reader clicked through to a story in the Catch Up card. Carries attributionContext.                                                                                           |
| catch\_up\_dismiss       | The 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](https://dev.arcxp.com/content-recommendations/sdk/attribution/).

The `catch_up_*` types are emitted only by the [Catch Up card](https://dev.arcxp.com/content-recommendations/sdk/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](https://dev.arcxp.com/content-recommendations/content-recommendations-api-developer-guide/#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 \| string     | number                                                                                                                                                                                        | Overrides 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. |
| attributionContext | AttributionContext | Attribution for recommendation\_exposure / recommendation\_click events. See [Recommendation Attribution](https://dev.arcxp.com/content-recommendations/sdk/attribution/#attribution-fields). |                                                                                                                                                                                 |

## 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`.

> **Caution:**
> 
> An `item_id` that does not match the ingested ID orphans interaction history and degrades recommendation quality. See [Item ID Guidance](https://dev.arcxp.com/content-recommendations/content-recommendations-item-id-guidance/)for how to choose and maintain item IDs.

For multi-page apps, set the meta tag once in HTML:

```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`.

```html
<meta name="arc-compass:site-id" content="the-daily" />
```

> **Note:**
> 
> The meta tag is read **once at `init()`** and cached for `track()` calls (the same as `item_id`). In a single-page app, updating the tag after navigation does **not** change what a later manual `track()` emits — pass `siteId` per call to override it (or `siteId: null` to emit none). Automatic `page_view` capture is the exception: it reads both tags at the moment it fires.

## 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:

```js
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.

```jsx
// 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:

```js
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:

```js
ArcCompass.track("search", { itemId: "search:" + encodeURIComponent(query) });
```

## Envelope shape

The SDK builds and POSTs event envelopes shaped like this:

```json
{
  "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.

> **Note:**
> 
> `navigator.sendBeacon` was considered and rejected: it does not support custom request headers (`X-API-Key`).

### 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](https://dev.arcxp.com/content-recommendations/sdk/integration-guide/#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.

> **Caution:**
> 
> The collector validates a batch atomically: if a single event fails validation (for example, an `item_id` over the collector’s length limit), the entire `POST /events/batch` is rejected with `422` and none of its events are stored. Because `4xx` is terminal, the SDK then drops that whole batch — up to `batch.maxEvents` events, not just the offending one. Wire `onError` to surface these, and keep any synthetic IDs (such as for `search`) within the collector’s `item_id` constraints.

##### 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](https://dev.arcxp.com/content-recommendations/content-recommendations-api-developer-guide/#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

- [Compass Web SDK: Integration Guide](https://dev.arcxp.com/content-recommendations/sdk/integration-guide/)
- [Compass Web SDK: Consent & Identity](https://dev.arcxp.com/content-recommendations/sdk/consent-identity/)
- [Compass Web SDK: Recommendation Attribution](https://dev.arcxp.com/content-recommendations/sdk/attribution/)
- [Item ID Guidance](https://dev.arcxp.com/content-recommendations/content-recommendations-item-id-guidance/)
