avada-behavior-sdk
v0.2.6
Published
Node/browser SDK for emitting **Avada product behavior events** to the ingest pipeline (`POST /ingest/events`). Events are typed against the central event registry, batched client-side, and streamed into `avada_product_raw.{appId}_sdk_events`.
Readme
avada-behavior-sdk
Node/browser SDK for emitting Avada product behavior events to the ingest pipeline (POST /ingest/events). Events are typed against the central event registry, batched client-side, and streamed into avada_product_raw.{appId}_sdk_events.
npm i avada-behavior-sdkPublished publicly on npm as avada-behavior-sdk. Inside this monorepo, use the workspace package instead.
Prerequisite (Data Platform provisions once): an ingest API key and a shop-signing HMAC secret for your
appId, minted at Admin → Behavior Keys (/admin/behavior-keys). Until the app is registered there, every event is rejected.
Quick start — Cloud Functions / Koa (recommended)
The simplest path. Import the serverless preset, give it your appId, done. It reads the standard env vars, signs every event, disables the background timer (Cloud Functions freeze between requests), flushes after each emit, and never throws — analytics can't break business logic.
import { createAvadaTracker } from "avada-behavior-sdk/serverless";
export const tracker = createAvadaTracker("ageVerification");
// Emit anything the app is allowed to emit — typed against the registry.
await tracker.track("installed", shopId, { plan_at_install: "free" });
await tracker.track("settings_updated", shopId, { changed_keys: ["theme"] });
await tracker.track("verification_submitted", shopId, { campaign_id, age: 21 });That's the whole integration. No config file, no crypto, no HTTP wiring.
Required env
createAvadaTracker(appId) reads these — no options needed in the common case:
| Env var | Required | Purpose |
|---|---|---|
| BEHAVIOR_SDK_API_KEY | ✅ | Bearer ingest key. |
| BEHAVIOR_SDK_SIGNING_SECRET | ✅ | Shop-signing HMAC secret (signs every event). |
| APP_ENV | | Must be exactly production for prod routing; anything else (or unset) routes to the staging table. |
| APP_VERSION | | Stamped as app_version (default 1.0.0). |
⚠️ If
APP_ENVisn'tproduction, events still send (HTTP 200) but land in..._sdk_events_stagingand never reach production analytics — the #1 cause of "events sent but missing." See Environment routing.If either credential is missing,
tracker.enabledisfalseand all emits are silent no-ops (safe in dev/CI).
tracker.track(eventName, shopId, props)
One uniform call for every event. eventName and props are type-checked against the registry; the tracker fills in shop_id, shop_signature, and occurred_at for you.
await tracker.track("feature_enabled", shopId, {
feature_name: "auto_apply_rules",
actor_type: "merchant_user",
});Also exported from this entry:
createShopSignature(appId, shopId, occurredAt, secret)— the raw signer, byte-matched to the ingest verifier (use it if you sign elsewhere).toNumber(value)— coerce request values to a finite number orundefined(handy for numeric props likemrr,agethat arrive as strings).
Thin sugar over track — equivalent, use them or track directly, your call:
tracker.installed(shopId, props); // → track("installed", …)
tracker.uninstalled(shopId, props);
tracker.subscriptionStarted(shopId, props); // → track("subscription_started", …)
tracker.planChanged(shopId, props);
tracker.subscriptionCancelled(shopId, props);
tracker.featureToggled(shopId, { featureName, enabled }); // picks feature_enabled / feature_disabled
tracker.settingsUpdated(shopId, props); // defaults changed_keys: [] + actor_typeOptional config (createAvadaTracker(appId, options))
Pass options only when you need to deviate. Any option overrides its env default (options.x ?? env), so you can use different env var names or hardcode values.
| Option | Default | Notes |
|---|---|---|
| apiKey | BEHAVIOR_SDK_API_KEY | Pass to read from a different env var. |
| signingSecret | BEHAVIOR_SDK_SIGNING_SECRET | |
| appVersion | APP_VERSION ?? "1.0.0" | |
| environment | APP_ENV === "production" ? "production" : "staging" | Must be "production" \| "staging" \| "test". |
| autoFlush | true | Flush after every emit. Set false + call tracker.flush() once at request end if a handler emits many events. |
| endpoint | shared production ingest | Override for local/dev stacks. |
| fetchImpl | global fetch | Inject for non-fetch runtimes / tests. |
| logger | console | debug/warn/error. |
// Different env var names + environment driven by the app's own config:
export const tracker = createAvadaTracker("ageVerification", {
apiKey: process.env.MY_BEHAVIOR_KEY,
signingSecret: process.env.MY_BEHAVIOR_SECRET,
environment: appConfig.isProduction ? "production" : "staging",
});tracker also exposes enabled (boolean), flush(), and close().
Advanced — batching emits with autoFlush
The SDK never sends events one-by-one over the wire: track() puts an event in an in-memory buffer, and flush() POSTs the whole buffer as a single batch. autoFlush controls when that flush happens.
autoFlush: true (default) — flush after every track(), so one HTTP POST per event.
- ✅ Reliable and zero-thought: on Cloud Functions the instance can freeze the moment your handler returns, so sending immediately guarantees the event isn't lost.
- ➖ One network round-trip per emit adds latency to the response.
autoFlush: false — track() only buffers; you call tracker.flush() once to send everything in a single POST.
export const tracker = createAvadaTracker("ageVerification", { autoFlush: false });
// In a handler that emits several events:
await tracker.track("installed", shopId, { plan_at_install: "free" });
await tracker.track("feature_enabled", shopId, { feature_name: "x" });
await tracker.track("settings_updated", shopId, { changed_keys: ["theme"] });
await tracker.flush(); // ← one POST for all three, instead of three- ✅ Collapses N emits in one request into a single round-trip — less latency, fewer requests.
- ➖ You must remember to
flush(). If the instance freezes before you flush, the whole buffer is lost (the background timer is disabled in serverless, so it won't drain on its own).
Rule of thumb: keep the default true when handlers emit 0–1 events (the common case). Switch to false only for a handler that emits many events in one request, and flush once at the end.
Advanced — core client (createBehaviorClient)
For long-running servers or browser code, use the lower-level client directly. It buffers and flushes automatically on a timer (flushIntervalMs), so you don't flush per-event — but you sign shop_signature yourself and manage shutdown.
import { createBehaviorClient } from "avada-behavior-sdk";
const client = createBehaviorClient({
appId: "orderLimit",
apiKey: process.env.AVADA_BEHAVIOR_KEY!,
appVersion: "2.14.0",
environment: "production",
});
// Defaults merged into every subsequent event until overridden.
client.identify({
shop_id: "123456",
shop_signature: serverGeneratedShopSignature, // you generate this — see shop_signature
shop_plan_at_event: "pro",
});
client.track("feature_enabled", {
feature_name: "auto_apply_rules",
actor_type: "merchant_user",
source: "admin",
});
await client.flush(); // force-send buffered events now
await client.close(); // flush + stop the timer (call on shutdown)Events are buffered and flushed automatically (by batchSize or flushIntervalMs); you only need flush()/close() for immediate delivery or graceful shutdown.
On Cloud Functions, prefer the serverless quick start — it sets
flushIntervalMs: 0and flushes per emit so events aren't lost when the instance freezes.
Full ClientOptions reference
| Option | Required | Default | Notes |
|---|---|---|---|
| appId | ✅ | — | Registry app id, e.g. orderLimit. |
| apiKey | ✅ | — | Behavior ingest key for this app. |
| appVersion | ✅ | — | Stamped onto every event as app_version. |
| environment | ✅ | — | production | staging | test. Controls routing (see below). |
| endpoint | | https://analytics.avada.net/ingest/events | Override for local/dev stacks. |
| batchSize | | 50 | Auto-flush once this many events are buffered. |
| flushIntervalMs | | 5000 | Periodic flush timer; 0 disables it. |
| maxBufferSize | | 1000 | Oldest events are dropped past this (overflow → onError). |
| maxRetries | | 5 | Per-batch retry attempts before dropping. |
| retryBaseDelayMs | | 100 | Base for exponential backoff. |
| maxEventBytes | | 65536 | Single events larger than this are rejected. |
| maxBatchBytes | | 524288 | Batch JSON byte ceiling. |
| onError / onFlush | | — | Observability hooks. |
| logger | | console | debug/warn/error. |
| fetchImpl | | global fetch | Inject for non-fetch runtimes/tests. |
Environment routing
The SDK stamps environment into event_properties and the ingest service routes by it:
production→avada_product_raw.{appId}_sdk_events(feeds the canonicalavada_product_behavior.eventsETL).- anything else (
staging,test, or absent) →avada_product_raw.{appId}_sdk_events_staging, which never reaches production analytics.
So non-prod traffic can be inspected in isolation — just point the staging build at the staging table.
shop_signature
Every event carrying a shop_id must be signed with the app's HMAC secret over the tuple (app_id, shop_id, occurred_at) → v1=HMAC-SHA256(...). The ingest verifier rejects mismatches with 403.
- Serverless preset: signs automatically — you never touch it.
- Core client: you generate the signature on your backend (use the exported
createShopSignaturefromavada-behavior-sdk/serverless) and pass it viaidentify({ shop_signature })or per-event props. Never put the secret in frontend code; the SDK only transports the signature.
What the SDK does and doesn't do
- Auto-stamps
app_version,sdk_version(kept in lockstep withpackage.jsonvia codegen —src/generated/version.ts), andenvironmentonto every event. - Generates a per-event ULID
source_doc_idused for idempotency/dedup across retries. - Never writes directly to
avada_product_behavior.events. - Never sends
retention_class— the ingest service derives it fromconfig/events/registry.yaml.
Related
This is the front-of-app behavior lane. For backend-only ingestion of records that already exist in an app's own system (e.g. a resolved support conversation), use the separate source-events lane instead — see docs/data-platform/behavior-sdk/source-events-ingest-guide.md.
