> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fourdos.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Engagement SDK

> A gtag-style TypeScript client for POST /v1/track - batching, retries, and beacon drain built in

The engagement SDK is a zero-dependency TypeScript client for
[`POST /v1/track`](/walkthroughs/trigger-an-event). It runs in browsers (with
`sendBeacon` drain on page hide) and in Node 18+ (global `fetch`).

<Note>
  The SDK is not yet published to npm - your 4D contact provides the package
  during integration onboarding. Everything it does rides on the public
  `/v1/track` endpoint, so you can always start with
  [plain HTTP](/walkthroughs/trigger-an-event) and adopt the SDK later without
  changing anything server-side.
</Note>

## Quick start

```ts theme={null}
import { createEngagementClient } from "engagement-sdk";

const engage = createEngagementClient({
  key: "ek_live_…",                             // publishable tracking key (not a secret)
  endpoint: "https://api.develop.fourdos.dev"   // events POST to <endpoint>/v1/track
});

// Who is engaging - the id YOUR system knows the user by (member external id):
engage.identify("cust-123");

// Fire tagged engagement events:
engage.track(["world_cup_26", "england", "kane"]);
engage.track(["world_cup_26", "highlights"], {
  action: "content_view",
  metadata: { content_id: "article-99" }
});
```

Events are batched (up to 20 per request, the server cap), auto-flushed every
2s while anything is queued, and drained via `navigator.sendBeacon` when the
page is hidden or unloading. Every event carries a client-generated UUID
`event_id` which the server dedupes on, so retries after network failures can
never double-count tag affinity.

## Identity

The platform only records engagement for **known members**. Identity comes
from one of:

```ts theme={null}
engage.identify("cust-123");                       // member external id (shorthand)
engage.identify({ memberExternalId: "cust-123" }); // same, explicit
engage.identify({ userId: "8a0f…uuid" });          // platform user id

engage.track(["kane"], { memberExternalId: "cust-456" }); // per-event override
```

Events tracked with no identity available are dropped and reported through
`onError` - call `identify()` as soon as you know who the user is.

## Delivery semantics

| Condition                                            | Behaviour                                                                             |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------- |
| Network error / HTTP 429 / 5xx                       | Batch re-queued; retried on next flush (up to `maxAttempts`, default 3)               |
| HTTP 401/403/422                                     | Batch dropped (configuration error - retrying cannot succeed); surfaced via `onError` |
| Per-event `rejected` outcome (e.g. `unknown_member`) | Surfaced via `onError`; not retried                                                   |
| Per-event `duplicate` outcome                        | Treated as success (the event already landed)                                         |
| Queue overflow (default 200)                         | Oldest events dropped; surfaced via `onError`                                         |

`track()` never throws. The only error surface is the `onError` callback:

```ts theme={null}
const engage = createEngagementClient({
  key, endpoint,
  onError: (e) => console.warn("[engagement]", e.code, e.message)
});
```

## Flushing

```ts theme={null}
await engage.flush();     // send everything queued now
await engage.shutdown();  // final flush + detach timers/listeners (SPA teardown)
```

## Config reference

| Option            | Default            | Notes                                     |
| ----------------- | ------------------ | ----------------------------------------- |
| `key`             | -                  | Publishable `ek_live_…` tracking key      |
| `endpoint`        | -                  | API base URL                              |
| `identity`        | -                  | Initial identity (else call `identify()`) |
| `flushIntervalMs` | `2000`             | `0` disables timed flushing               |
| `maxBatchSize`    | `20`               | Clamped to the server cap (20)            |
| `maxQueueSize`    | `200`              | Oldest dropped beyond this                |
| `maxAttempts`     | `3`                | Send attempts per event                   |
| `fetch`           | `globalThis.fetch` | Injectable for tests/custom transports    |
| `onError`         | no-op              | Drop/transport/rejection reporting        |
| `autoFlushOnHide` | auto               | Browser pagehide/visibility beacon drain  |

## Tags

Tags are trimmed + lowercased client-side (matching server normalisation) and
must fit `^[a-z0-9][a-z0-9_.:-]{0,63}$` - max 10 per event. Invalid tags are
filtered out; an event whose tags are all invalid is dropped and reported.

The tracking key is **publishable** - it ships in client code like an
analytics measurement id. Abuse controls are the key's per-origin allowlist
and per-key rate limiting
([issue keys via the API](/walkthroughs/trigger-an-event)), not key secrecy.
