Node SDK
@syntarie/tracking-node is the server-side counterpart to the browser SDK.
It is intentionally narrower than the browser entry: there is no
auto-capture, no DOM, no offline queue, and no anonymous-id generation.
Server-side code already knows who the user is — the SDK refuses to invent
an anonymous id.
Install
Section titled “Install”npm install @syntarie/tracking-nodeRequires Node 20 or newer. ESM-only.
Initialise
Section titled “Initialise”import { init } from '@syntarie/tracking-node';
init({ siteId: 'site_marketing', host: 'https://collect.example.com', // optional: batchSize: 20, flushIntervalMs: 5_000, maxBufferedEvents: 10_000,});InitOptions
Section titled “InitOptions”| Field | Type | Default | Notes |
|---|---|---|---|
siteId | string | required | Throws on empty. |
host | string | required | Throws on empty. |
batchSize | number | 20 | Throws on non-positive. |
flushIntervalMs | number | 5_000 | Throws on non-positive. |
maxBufferedEvents | number | 10_000 | Throws on non-positive. Older events are oldest-dropped above this cap. |
transport | TransportOptions | — | Inject a custom fetch (e.g. corp proxy). |
Calling init more than once throws. The browser SDK is permissive here
because hot-reloading bundles re-execute module top-level; in a long-lived
Node process double-init is almost always a bug.
Identify
Section titled “Identify”import { identify } from '@syntarie/tracking-node';
identify('user_42', { plan: 'pro' });Sets the process-wide identified user. Every subsequent track() call
that does not supply a userId attribute is attributed to this user. If a
different prior user was set, the SDK emits a merge event before the new
identify.
import { track } from '@syntarie/tracking-node';
// Uses process-wide identity:track('subscription_renewed', { plan: 'pro' });
// Or override per call:track( 'subscription_renewed', { plan: 'pro' }, { userId: 'user_42', timestamp: Date.now() },);Resolution order for the user: options.userId → process-wide identity →
throw. The SDK never silently attributes server events to “anon” — that
would surface as “where are my events?” debugging weeks later.
import { flush } from '@syntarie/tracking-node';
process.on('SIGTERM', async () => { await flush(); process.exit(0);});Drains the in-memory buffer. Resolves once the underlying request settles.
No-op when init has not run.
Custom transport
Section titled “Custom transport”import { init, type InitOptions } from '@syntarie/tracking-node';
const options: InitOptions = { siteId: 'site_marketing', host: 'https://collect.example.com', transport: { fetch: customFetch, // e.g. wrapped with a corp HTTP agent },};init(options);TransportOptions is part of the public contract — its precise shape is
documented in the source and follows the same semver guarantees as the rest
of the surface.
What this SDK does NOT do
Section titled “What this SDK does NOT do”- No anonymous-id generation. Use the browser SDK on the client side and
identify()for known server-side users. - No autocapture. There are no
pageviewevents from a server runtime. - No durable on-disk queue. The buffer is in-memory and bounded by
maxBufferedEvents. If you need durability across crashes, persist events to your own queue and emit them on resume. - No retry orchestration on the public surface. The transport handles transient failures internally; permanent failures (4xx with no recovery) are dropped after logging.