Skip to content

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

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:

PostureCaptureSession replayDevice storageVisitor id
fullyesyeslocalStorage (or your persistence)persisted anonymous id
cookielessyesnononeserver-derived, rotates daily
nonenononone
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

OptionDefaultNotes
hostpage originRequired in Node
project'default'Logical event stream within the org
autocapturetrueaz.page_view / az.page_leave incl. SPA routes
captureClickstrueDOM clicks as az.interaction
persistence'localStorage'Falls back to cookie, then memory
batchSize10Queue length that triggers a flush
flushIntervalMs5000Periodic flush
maxQueueSize1000Drop-oldest on overflow
optOutfalseStart opted out (persisted opt-out wins)
respectDoNotTrackfalseTreat browser DNT as opt-out
consent{ pendingPolicy, deniedPolicy } — see Consent
cookielessfalsePin to the cookieless posture; no banner needed
gzipfalseGzip request bodies (fetch path only)
tokenInQueryfalseSend token as ?token= instead of a header
maskAllTextfalseRedact element text in click autocapture
sessionIdleTimeoutMs30 minSession rotation on inactivity
disableDecidefalseSkip flags/surveys/recording bootstrap
enableSessionRecordingtrueHard-off switch; the server’s project setting decides
enableSurveystrueHard-off switch for survey popovers
sessionRecordingRecorder privacy: maskAllInputs (default true), maskTextSelector, blockSelector
assetsUrl{host}/staticWhere the lazy recorder/surveys bundles load from
secretKeyNode only: secret events:write key → /api/v1/events
storageCustom persistence backend (wrapper SDKs)
platformPropsExtra static properties (wrapper SDKs)
disableBrowserFeaturesfalseNon-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_id per 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 429 re-queues the batch and retries next flush; failed batches re-queue at the front. The public API never throws for delivery or flag problems.