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-nodeQuickstart
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
| Option | Default | |
|---|---|---|
host | from the key | Capture origin override. |
flushAt | 20 | Send when this many events are queued. |
flushIntervalMs | 30 000 | Send at least this often. |
maxBatchSize | 100 | Events per request. |
maxQueueSize / maxQueueBytes | 1000 / 5 MB | The oldest events are dropped past this and reported as $dropped_events. |
requestTimeoutMs | 10 000 | Per request. |
consent | "granted" | "pending" holds events until setConsent("granted"); "denied" never touches the network. |
compression | true | Gzip 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. |
flushOnExit | true | Flush when the event loop empties (beforeExit). |
flagsSecretKey | Enables local flag evaluation. | |
flagsPollIntervalMs | 30 000 | How often local definitions refresh. |
sendFeatureFlagEvents | true | Emit $feature_flag_called on reads. |
debug | false | Log 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;flushOnExitcovers scripts that simply end. - Server-side identity is stateless: each call names its
distinctId. Merges happen server-side fromidentify({ anonId }); there is noalias.