@traceten/sdk-node
v1.0.0
Published
Traceten server-side SDK for Node.js — send AI-traffic pageviews and conversions from your backend.
Maintainers
Readme
@traceten/sdk-node
The Traceten server-side SDK for Node.js. Send AI-traffic pageviews and revenue events from your backend over authenticated, ad-blocker-resistant HTTP.
Use this when the browser snippet cannot run: server-rendered flows, webhooks, mobile/API backends, or when you want delivery that ad-blockers and privacy browsers cannot strip.
Install
npm install @traceten/sdk-node
# or: pnpm add @traceten/sdk-node
# or: yarn add @traceten/sdk-nodeNode 18 or newer. The only runtime dependency is undici.
Quickstart
import { Client } from "@traceten/sdk-node";
const traceten = new Client({
siteId: "ttid_7Rb4TrC1dTbnD8w3s1TS12", // your site key, from the dashboard's install page
host: "https://ingest.traceten.com",
// Required. A secret: load it from your environment, never hardcode it.
apiKey: process.env.TRACETEN_API_KEY!,
});
// A pageview. visitorId comes from your own request context (a cookie you set,
// a user id, etc.) — the server has no Traceten cookie to read.
traceten.page({
url: "https://acme.com/pricing",
referrer: "https://chat.openai.com/",
visitorId: "123e4567-e89b-42d3-a456-426614174000",
});
// A conversion. visitorId is required so the revenue can be attributed.
traceten.track("subscription_started", {
visitorId: "123e4567-e89b-42d3-a456-426614174000",
valueCents: 4900,
currency: "usd",
});
// Flush and stop the background timer on shutdown.
await traceten.close();page() and track() return immediately. They buffer the event and send it in
the background. They never throw on network problems, they throw only on
programmer errors (a bad URL, a missing visitorId, a malformed event name).
The API key
apiKey is required. Create a key in the dashboard under Settings -> API
keys, or use the key shown once when you created the site.
Keep it on your server. It is a secret: never put it in client-side code, a
mobile app, or a public repository. It is not the same value as siteId, which
is public and already embedded in your pages.
The key does two things:
- Gets you in. The SDK posts to the authenticated ingestion endpoints, which
return
401without a valid key. - Gets you your own quota. Authenticated traffic is rate-limited on a bucket
tied to the key, separate from the shared per-site bucket.
siteIdis public, so anyone who can read your page source can send events under it. With a key, that traffic cannot exhaust your allowance and 429 your conversion calls.
The client validates the key's shape when you construct it and throws if it is missing or malformed. An integration that silently drops every event is worse than one that fails on its first line.
Permissions
A key carries a set of permissions that decide which endpoints it can reach.
This SDK sends to /v1/server/*, which requires ingest:write. Tick that
permission when you create the key.
The client cannot check this for you. Permissions live on the server and the key
looks identical either way, so a key without ingest:write is rejected with the
same 401 as an invalid one.
Grant only what you need. A key used solely for server-side ingestion does not need permission to read your analytics or erase visitor data, and if it leaks it cannot do either.
To rotate a key: create the new one, deploy it, then revoke the old one. Revocation normally takes effect within about a minute. If our database is unreachable at that moment, an edge location that was already using the key may keep honouring it for up to about fifteen minutes more, so that a database blip cannot silently drop your events.
Identifiers
Because it runs on your server, the SDK has no cookie and no DOM. You supply
visitorId (and optionally sessionId) from your own request context. A
visitorId is either a UUID or an h:<64-hex> identify hash. The SDK never
fabricates one.
The robust way to get this value is window.traceten.getVisitorId(), called
client-side and forwarded to your backend (a form field, a fetch body, a
header) — it always resolves the current cookie, so it keeps working if a
customer turns cross-subdomain cookies on or off later. If you read the
cookie by name instead, its name depends on the site's cookie scope:
cross-subdomain cookies are off by default, giving plain _traceten_vid;
once a customer enables it, the cookie becomes _traceten_vid_ followed by
eight characters of the site key. The install page shows the exact current
name. Read that name exactly, never by prefix: two Traceten sites under one
registered domain each set their own cookie, and a prefix match picks
whichever the browser happens to list first, which merges two visitors the
suffix exists to keep apart. If a request carries no such value, send the
event without a visitorId rather than inventing one.
API
new Client(options)
| Option | Type | Default | Notes |
| --------------- | ----------------------- | ---------- | ------------------------------------------------------------ |
| siteId | string | (required) | Your site key (ttid_…, case-sensitive) from the dashboard's install page. 1-64 chars, A-Z a-z 0-9 _ -. |
| host | string | (required) | Ingest base URL. Absolute https (http on loopback only). |
| apiKey | string | (required) | Secret key, sent as Authorization: Bearer. See below. |
| flushAt | number | 50 | Flush the events queue at this size. Clamped to [1, 50]. |
| flushInterval | number | 5000 | Background flush cadence in ms. |
| maxRetries | number | 3 | Retry attempts on 5xx / 429 / network error. |
| timeoutMs | number | 10000 | Per-request timeout in ms. |
| onError | (err: Error) => void | - | Called when a batch is permanently dropped. |
| flushOnExit | boolean | true | Register best-effort flush on beforeExit/SIGTERM/SIGINT.|
page(props)
| Prop | Type | Default | Notes |
| ----------- | ---------------- | ------------ | ---------------------------------------- |
| url | string | (required) | Valid URL, up to 2048 chars. |
| referrer | string | "" | |
| visitorId | string | - | UUID or h:<hash>. |
| sessionId | string | - | |
| userAgent | string | - | The end user's User-Agent, if you know it.|
| eventName | string | "pageview" | Must match ^[a-z][a-z0-9_-]*$. |
| timestamp | Date \| string | now | |
The userAgent you pass is what gets recorded as the visitor's user agent.
Omit it and the event falls back to the User-Agent this SDK's HTTP client
sent, which describes your server, not the visitor. User agent is an input to
Traceten's traffic classification, so passing the real one materially improves
your results.
track(name, opts)
name must match ^[a-z][a-z0-9_]*$ (no hyphen).
| Option | Type | Default | Notes |
| ------------ | ---------------- | ---------- | ------------------------------------------------- |
| visitorId | string | (required) | UUID or h:<hash>. Required to attribute revenue.|
| properties | object | {} | Arbitrary JSON. |
| valueCents | number | - | Non-negative integer, minor units. -> value_cents.|
| currency | string | - | ISO-4217, lowercased on send. |
| sessionId | string | - | |
| timestamp | Date \| string | now | |
goal(name, opts)
Records a goal completion. Same arguments and same endpoint as track(), with
one difference: the reserved names below are rejected.
traceten.goal("demo_booked", {
visitorId: "123e4567-e89b-42d3-a456-426614174000",
properties: { plan: "pro" },
});A goal is a custom event. goal("demo_booked", ...) and
track("demo_booked", ...) send exactly the same payload, and the goal is
listed by GET /v1/goals either way. Use goal() when the event is something
you want to count and put in a funnel, and track() when you are recording
revenue.
Reserved names. These belong to the Stripe and Shopify integrations, which
emit them for real subscription and payment events. goal() throws on them, so
a name collision cannot quietly corrupt your revenue funnel:
payment, free_trial, trial_started, trial_converted,
subscription_started, subscription_upgraded, subscription_downgraded,
subscription_renewed, subscription_cancel_scheduled,
subscription_reactivated, subscription_ended.
track() still accepts them, because that is how those events are legitimately
sent.
Whitespace is not trimmed. goal(" signup ") throws. The browser snippet
trims a name read from an HTML attribute, because attribute values pick up
whitespace from how the page is formatted; a name written in server code does
not, so a stray space is a bug worth surfacing rather than quietly fixing.
Property keys and values are both stored, and both are readable back.
GET /v1/goals/{name}/properties returns every property key sent with a goal
and that key's most common values. Do not put an email address, a person's name,
or a postal address in either half of a property.
Ingestion drops a property whose key is exactly email, phone, name,
password, token, ssn, credit_card or card_number, and redacts email,
phone, card and national-ID patterns inside string values. It has no pattern
for a personal name or a street address, and it does not scan keys at all, so
{"full_name": "Alice Chen"} is stored and returned exactly as sent.
payment(props): Promise<PaymentResult>
Records a payment from ANY payment processor (POST /v1/server/payments). The
only method here that sends immediately and reports a delivery failure to the
caller: a dropped pageview is a dropped pageview, a dropped payment is missing
revenue.
const res = await traceten.payment({
transactionId: "pay_9fK2mQ", // required — the processor's id. Idempotency key.
amount: 49.99, // required — MAJOR unit, not cents
currency: "USD", // required — ISO-4217, sent uppercase
provider: "dodo", // optional — your label. Defaults to "api".
email: "[email protected]", // optional — hashed server-side, never stored
visitorId, // optional — a stronger match than the email
customerId: "cus_123", // optional
renewal: false, // optional
refunded: false, // optional — never send a negative amount
isFreeTrial: false, // optional — implied by amount: 0
settlementAmount: 45.50, // optional — your processor's own converted figure
settlementCurrency: "USD", // optional — must be set together with settlementAmount
});
res.status; // "recorded" | "trial" | "refunded" | "duplicate"amount is the MAJOR unit, the opposite of track()'s valueCents: 49.99
for $49.99, 5000 for ¥5000.
settlementAmount/settlementCurrency are a fallback for when currency
isn't one Traceten can price natively (the ~30 ECB-published codes): your
processor's own conversion of the payment into a currency it can price, so
the payment isn't dropped. Set both or neither — sending one without the
other causes the SDK to omit both.
Re-posting the same transactionId returns "duplicate" and creates nothing,
which is why a 5xx is retried here. It rejects on an invalid field and after
every retry is exhausted.
⚠️ Do NOT send payments here for a processor you have also connected natively. Traceten would record the payment twice and overstate your revenue.
flush(): Promise<void>
Sends everything queued now. Resolves when every in-flight request settles.
close(): Promise<void> / shutdown(): Promise<void>
Flushes, stops the background timer, and removes the exit hooks. Idempotent.
After close(), page(), track() and goal() throw.
Delivery and retries
- Pageviews go to
POST {host}/v1/server/eventsin batch envelopes of up to 50. - Conversions go to
POST {host}/v1/server/conversions, one body per event. - Every request carries
Authorization: Bearer <apiKey>. Without a valid key these endpoints return401. - On 5xx, 429, or a network/timeout error, the batch is retried with
exponential backoff and full jitter, up to
maxRetries. - On any other 4xx the payload is malformed, so it is dropped and
onErroris called. It is not retried.
Full docs
See API.md for the complete guide, or docs.traceten.com/sdks/node for the hosted version.
Versioning
This package follows Semantic Versioning. Before 1.0.0,
minor versions may include breaking changes — pin an exact version in production
until then. See CHANGELOG.md for release history.
Contributing
Issues and pull requests are welcome. For anything beyond a small fix, please open an issue first to discuss the change. Run the checks below before submitting a PR — CI enforces the same steps on every pull request:
npm install
npm run typecheck
npm run build
npm testLicense
MIT © Traceten — see LICENSE.
Links
- Documentation
- Traceten — AI traffic attribution for the AI search era
- Issues
- Changelog
- Python SDK · Go SDK · AI crawler tracking
