atrixANALYTICS
SDKs

Node.js and Bun

@atrix.dev/analytics-node: batched, gzipped, retrying server-side capture that never throws.

For Node.js 18+ and Bun. Events are queued in memory, batched, gzipped and sent in the background with retries. No method throws, and a missing or invalid key turns the client into a silent no-op, so analytics can never take your API down.

npm i @atrix.dev/analytics-node      # or: bun add @atrix.dev/analytics-node

Quickstart

import { Atrix } from "@atrix.dev/analytics-node";

const atrix = new Atrix(process.env.ATRIX_PUBLIC_KEY ?? "");   // atx_pk_eu_production_…

atrix.capture({
  distinctId: "user_42",                                       // your user id, never an email or phone number
  event: "subscription_renewed",
  properties: { plan: "growth", amount: 14900, currency: "USD" },   // money: integer minor units + ISO currency
  groups: { company: "acme" },
});

// Link the browser or app visitor to the account at sign-in (anonId comes from the client SDK).
atrix.identify({ distinctId: "user_42", anonId: "0190f2a4-5b6c-7d8e-9f01-23456789abcd", set: { plan: "growth" } });

// Link a billing customer so provider webhooks are attributed to this user.
atrix.setRevenueCustomer({ distinctId: "user_42", customerId: "cus_Nffrfeu", provider: "stripe" });

// Company or team profiles.
atrix.groupIdentify({ groupType: "company", groupKey: "acme", properties: { name: "Acme", seats: 40 } });

// Before the process exits (in serverless handlers: at the end of every invocation).
await atrix.shutdown();

The capture host comes from the key: atx_pk_eu_… sends to https://eu.i.analytics.atrix.dev. Pass host to send through a proxy.

Feature flags

import { Atrix } from "@atrix.dev/analytics-node";

const atrix = new Atrix(process.env.ATRIX_PUBLIC_KEY ?? "", {
  flagsSecretKey: process.env.ATRIX_FLAGS_SECRET,   // atx_svc_…: evaluate locally
});

const enabled = await atrix.isFeatureEnabled("new-checkout", "user_42", { personProperties: { plan: "pro" } });
const variant = await atrix.getFeatureFlag("pricing", "user_42");          // "test" | "control" | false | undefined
const config = await atrix.getFeatureFlagPayload("pricing", "user_42");

With flagsSecretKey, flags are evaluated in your process from /flags/definitions, refreshed every 30 s with an ETag. A flag that needs something you did not pass (a cohort, a missing property) falls back to a remote /flags call. Without the secret, every read calls /flags. Reads emit $feature_flag_called once per user, flag and value. More in Feature flags.

Options

OptionDefault
hostfrom the keyCapture origin override.
flushAt20Send when this many events are queued.
flushIntervalMs30 000Send at least this often.
maxBatchSize100Events per request.
maxQueueSize / maxQueueBytes1000 / 5 MBThe oldest events are dropped past this and reported as $dropped_events.
requestTimeoutMs10 000Per request.
consent"granted""pending" holds events until setConsent("granted"); "denied" never touches the network.
compressiontrueGzip bodies.
beforeSend(event) => event | null: edit or drop every event. A throwing hook drops the event.
propertyDenylist[]Keys stripped from properties, $set and $set_once after beforeSend.
superProperties{}Stamped on every event, for example { service: "billing-api" }. register() adds more at runtime.
flushOnExittrueFlush when the event loop empties (beforeExit).
flagsSecretKeyEnables local flag evaluation.
flagsPollIntervalMs30 000How often local definitions refresh.
sendFeatureFlagEventstrueEmit $feature_flag_called on reads.
debugfalseLog problems with console.warn.

Behaviour

  • Every event gets a UUIDv7 when it is captured, and retries resend the same uuid. Pass your own uuid (a UUIDv7) to make a capture idempotent across your own retries.
  • A 413 splits the batch in half, recursively; one event that is still too large is dropped.
  • Timers are unref'd, so the SDK never keeps your process alive. await atrix.shutdown() makes sure the queue is delivered; flushOnExit covers scripts that simply end.
  • Server-side identity is stateless: each call names its distinctId. Merges happen server-side from identify({ anonId }); there is no alias.

On this page