@atrix.dev/analytics-node
v0.1.1
Published
Atrix Analytics for Node.js and Bun servers: batched, gzipped, retrying event capture that never throws.
Readme
@atrix.dev/analytics-node
Server-side Atrix Analytics for Node.js ≥ 18 and Bun. Events are queued in memory, batched and gzipped, and sent in the background with retries. No method ever throws, and a missing or invalid key turns the client into a silent no-op, so analytics can never take your API down.
bun add @atrix.dev/analytics-node # or: npm i @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.id, // 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: user.companyId },
});
// Link the browser/app visitor to the account at sign-in (anonId comes from the client SDK).
atrix.identify({ distinctId: user.id, anonId: req.body.atrixAnonId, set: { plan: "growth" } });
// Link a billing customer to the user, so revenue webhooks from the provider are attributed to them
// ($revenue_customer). provider: "stripe" | "revenuecat" | "app_store" | "google_play"; unknown values are dropped.
atrix.setRevenueCustomer({ distinctId: user.id, customerId: stripeCustomer.id, provider: "stripe" });
// What serving a customer cost you ($cost), so the revenue `margin` insight can show gross margin per group.
// costMinor is an integer in minor units of `currency`; kind is ai | infra | support | ...; invalid cost is dropped.
atrix.captureCost({
distinctId: user.id,
costMinor: 125,
currency: "USD",
kind: "ai",
groups: { organization: org.id },
model: "claude-sonnet", tokensIn: 1200, tokensOut: 340,
});
// Company/team profiles.
atrix.groupIdentify({ groupType: "company", groupKey: "acme", properties: { name: "Acme", seats: 40 } });
// Before the process exits (serverless handlers: at the end of every invocation).
await atrix.shutdown();The region's capture host comes from the key: atx_pk_<region>_<env>_<22> sends to
https://<region>.i.analytics.atrix.dev. Pass host to use a proxy.
Feature flags
const atrix = new Atrix(process.env.ATRIX_KEY!, { flagsSecretKey: process.env.ATRIX_FLAGS_SECRET });
if (await atrix.isFeatureEnabled("new-checkout", user.id, { personProperties: { plan: user.plan } })) { ... }
const variant = await atrix.getFeatureFlag("pricing", user.id); // "test" | "control" | false | undefined
const config = await atrix.getFeatureFlagPayload("pricing", user.id);With flagsSecretKey flags are evaluated in-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 /flags.
Without it every read calls /flags. Reads emit $feature_flag_called once per user, flag and value.
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 | In-memory bound. The oldest events are dropped and reported as $dropped_events. |
| requestTimeoutMs | 10 000 | Per request. |
| consent | granted | pending holds events in memory 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, e.g. { service: "billing-api" }. register() adds more at runtime. |
| flushOnExit | true | Flush when the event loop empties (beforeExit). |
| debug | false | Log problems with console.warn. |
Behaviour
- Every event gets a UUIDv7 when it is captured; retries resend the same uuid and capture dedupes on it. Pass
your own
uuid(a UUIDv7) to make a capture idempotent across your own retries. - Flushes at 20 events, every 30 s, on
flush()and onshutdown(). One request in flight at a time, at most 100 events per request. - A 413 splits the batch in half, recursively; a single event that is still too large is dropped. Network
errors, timeouts, 408, 429 and 5xx retry with exponential backoff and jitter, honouring
Retry-After. Other 4xx drop the batch. - Timers are
unref'd: the SDK never keeps your process alive. Callawait atrix.shutdown()to make sure the queue is delivered;flushOnExitcovers scripts that simply end. - Server-side identity is stateless. Each call names its
distinctId; junk ids (guest,null,undefined, …) are refused. There is noalias: merges happen server-side fromidentify({ anonId }). - Conformance: this SDK passes the shared
sdks/specsuite under both Bun and Node (bun run conformance).
MIT licensed.
