npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@traceten/sdk-node

v1.0.0

Published

Traceten server-side SDK for Node.js — send AI-traffic pageviews and conversions from your backend.

Readme

@traceten/sdk-node

npm version License: MIT 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-node

Node 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 401 without 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. siteId is 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/events in 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 return 401.
  • 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 onError is 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 test

License

MIT © Traceten — see LICENSE.

Links