Web & Node SDK
Capture events from the browser or Node with @azfive/capture — autocapture, identity, super properties, feature flags, and batched delivery.
@azfive/capture is the reference implementation of the AZ-Five SDK spec — a batteries-included analytics SDK for the browser (anonymous-id persistence, autocapture, sessions, flags, batching with sendBeacon on unload) that also runs server-side in Node with a secret key.
Install
npm install @azfive/capture
Or from a CDN — every npm release is served by jsDelivr; the UMD bundle exposes the AzFiveCapture global:
<script src="https://cdn.jsdelivr.net/npm/@azfive/capture@0.1.0/dist/azfive-capture.umd.js"></script>
<script>
AzFiveCapture.init('azfive_pub_…', { host: 'https://app.az-five.com' });
</script>
Initialize
Create a public ingest token (Settings → API Keys → Public Ingest Tokens), scoped to your site’s origins — it’s origin-validated fail-closed and safe to ship in page source:
import { init, azfive } from '@azfive/capture';
init('azfive_pub_…', { host: 'https://app.az-five.com', project: 'web' });
The SDK is framework-agnostic. In React (or any bundled app), create a singleton once and import it anywhere:
// analytics.ts
import { createClient } from '@azfive/capture';
export const analytics = createClient('azfive_pub_…', { host: 'https://app.az-five.com' });
Node (server-side)
On a server, use a secret events:write key via the secretKey config — events go to /api/v1/events and you supply distinct_id per event; no persistence or autocapture runs:
import { createClient } from '@azfive/capture';
const azfive = createClient('unused', { host: 'https://app.az-five.com', secretKey: 'azfive_…' });
azfive.capture('invoice_paid', { distinct_id: 'user-42', amount: 99 });
await azfive.flush();
Capture events
azfive.capture('signed_up', { plan: 'pro' });
// Super properties — merged into every subsequent event
azfive.register({ deployment: 'eu-1' });
azfive.register_once({ first_touch: 'ad-campaign' });
azfive.unregister('deployment');
Autocapture is on by default: az.page_view / az.page_leave (including SPA route changes) and DOM clicks as az.interaction with element chains. Mark sensitive elements with data-vz-no-capture (or the .vz-no-capture class); password and hidden inputs are never captured. Disable per feature with autocapture: false / captureClicks: false.
Identify & person properties
azfive.identify('ava@example.com', { plan: 'pro' }); // merges the anonymous session
azfive.people.set({ plan: 'enterprise' });
azfive.people.set_once({ signup_date: '2026-08-01' });
azfive.alias('legacy-77');
// on logout:
await azfive.flush();
azfive.reset(); // new anonymous id; reset(true) also rotates the device id
identify emits az.identify carrying the previous anonymous id (so pre-login events merge into the person) and refetches flags. Calling it again with the same id only updates properties. Opt-out controls: opt_in_capturing() / opt_out_capturing() / has_opted_out_capturing() — opt-out drops events at capture time, stops any running session recording, and survives reset(). For banner-driven choices use consent instead.
Feature flags
Flags load from POST /api/v1/decide on init and are cached stale-while-revalidate, refetched after identify() and reset():
const unsubscribe = azfive.onFlags((flags) => {
const variant = azfive.getFlag('exp-checkout'); // false | true | 'variant'
if (azfive.isFlagEnabled('new-billing')) {
const payload = azfive.getFlagPayload('new-billing'); // no exposure event
}
});
await azfive.reloadFlags(); // force a refetch
getFlag and isFlagEnabled fire an az.flag_called exposure (deduped per session:flag:value) — that exposure drives experiments. getFlagPayload never does.
Flush & shutdown
Delivery is automatic (10 events / 5 s), with sendBeacon draining the queue on page unload. flush() forces delivery now and returns a promise; destroy() removes listeners and timers (React unmount / SPA teardown).
Consent
Without a consent config the client behaves as it always has: everything on, opt-out
only. Set one and the client resolves a posture from the visitor’s answer:
| Posture | Capture | Session replay | Device storage | Visitor id |
|---|---|---|---|---|
full | yes | yes | localStorage (or your persistence) | persisted anonymous id |
cookieless | yes | no | none | server-derived, rotates daily |
none | no | no | none | — |
const client = createClient(TOKEN, {
consent: {
pendingPolicy: 'full', // before they answer. 'cookieless' or 'none' = opt-in-first
deniedPolicy: 'cookieless', // after they decline. Default 'none'
},
});
if (client.getConsent() === 'pending') showYourBanner();
acceptButton.onclick = () => client.setConsent('granted');
declineButton.onclick = () => client.setConsent('denied');
setConsent is persisted and applied immediately. Granting can start session replay
without another /v1/decide round trip. Denying stops a running recorder, drops
anything captured but not yet sent, and deletes every stored key except the answer
itself — which has to survive to be honoured.
getPosture() returns what the client is actually doing, after consent, config and
opt-out signals are resolved together. An opt-out outranks a grant: a visitor who
accepted but also sends Do-Not-Track is not captured.
Cookieless mode
cookieless: true pins the client to the cookieless posture regardless of consent — for
sites that would rather not run a banner. Events carry $cookieless, and the server
derives the visitor id from a salted, daily-rotating digest of the request, scoped to
one org and project. Unique-visitor counts stay usable within a day; nothing accumulates
across days, across sites, or on the device. Cookieless events always store a truncated
IP, whatever the deployment’s AZFIVE_GEOIP_ANONYMIZE_IP setting.
The trade-off is real: with no device storage there is no cross-page session on a multi-page site, no cross-domain identity handoff, and no replay.
Reference
Config
| Option | Default | Notes |
|---|---|---|
host | page origin | Required in Node |
project | 'default' | Logical event stream within the org |
autocapture | true | az.page_view / az.page_leave incl. SPA routes |
captureClicks | true | DOM clicks as az.interaction |
persistence | 'localStorage' | Falls back to cookie, then memory |
batchSize | 10 | Queue length that triggers a flush |
flushIntervalMs | 5000 | Periodic flush |
maxQueueSize | 1000 | Drop-oldest on overflow |
optOut | false | Start opted out (persisted opt-out wins) |
respectDoNotTrack | false | Treat browser DNT as opt-out |
consent | — | { pendingPolicy, deniedPolicy } — see Consent |
cookieless | false | Pin to the cookieless posture; no banner needed |
gzip | false | Gzip request bodies (fetch path only) |
tokenInQuery | false | Send token as ?token= instead of a header |
maskAllText | false | Redact element text in click autocapture |
sessionIdleTimeoutMs | 30 min | Session rotation on inactivity |
disableDecide | false | Skip flags/surveys/recording bootstrap |
enableSessionRecording | true | Hard-off switch; the server’s project setting decides |
enableSurveys | true | Hard-off switch for survey popovers |
sessionRecording | — | Recorder privacy: maskAllInputs (default true), maskTextSelector, blockSelector |
assetsUrl | {host}/static | Where the lazy recorder/surveys bundles load from |
secretKey | — | Node only: secret events:write key → /api/v1/events |
storage | — | Custom persistence backend (wrapper SDKs) |
platformProps | — | Extra static properties (wrapper SDKs) |
disableBrowserFeatures | false | Non-DOM runtimes: skip autocapture/unload hooks |
libName | 'azfive-capture' | $sdk value; wrapper SDKs override |
API
init / createClient · capture · identify · alias · reset · getDistinctId · getSessionId · register / register_once / unregister · people.set / people.set_once · opt_in_capturing / opt_out_capturing / has_opted_out_capturing · setConsent / getConsent / getPosture · getFlag / isFlagEnabled / getFlagPayload / onFlags / reloadFlags · getSurveys · flush · destroy.
Behavior notes
- Sessions rotate after 30 minutes idle or 24 hours total;
$session_id(+$tab_idper tab) is stamped on every event. In Node/secret-key mode there is no session (getSessionId()returns''). - Custom event names: anything except the reserved
az.prefix, ≤ 200 characters; properties ≤ 64 KiB serialized. - A
429re-queues the batch and retries next flush; failed batches re-queue at the front. The public API never throws for delivery or flag problems.