Product analytics for Node.js
For events that must be trusted (payments, server jobs, webhooks) use the Pug Node SDK. It names the user explicitly rather than inferring a browser session, and the same private key reads profiles and insights back into your own code.
Add Pug to Node.js
Four steps, start to first insight. Everything below uses the Node SDK exactly as it ships. There is no Node.js wrapper in between.
-
Install the Node SDK
A separate package from the browser one, because it does a different job: no autocapture, no ambient session, and read access on top of ingestion.
npm install @pug-sh/node # or: pnpm add @pug-sh/node · yarn add @pug-sh/node · bun add @pug-sh/node -
Construct one client with your private key
Create the client once per process and reuse it, it owns the batching queue. The constructor throws unless the key starts with prv_, so a public key pasted in by mistake fails loudly at boot instead of silently dropping events.
src/pug.tsimport { Pug } from '@pug-sh/node' export const pug = new Pug({ apiKey, process.env.PUG_PRIVATE_KEY!, // prv_…: never ship this to a browser }) -
Track the events you can’t trust a browser for
Every server call names the user, so there is no session to infer and nothing an ad blocker can drop. track() never throws (invalid input and a closed client are logged and dropped), so it is safe to call from inside a webhook handler without a try/catch.
src/webhooks/stripe.tsimport { pug } from '../pug' export async function onInvoicePaid(event: StripeEvent) { const userId = event.data.object.metadata.userId pug.track(userId, 'order_completed', { amount: event.data.object.amount_paid / 100, currency: 'USD', }) await pug.identify(userId, { plan: 'pro', billing_status: 'active' }) } -
Read the data back where you need it
The private key also authorizes reads: the same profiles, activity, and insight queries the dashboard runs. Reads are request/response and throw PugError with the underlying code, so you own the timeout and retry policy; ingestion still never throws.
src/reports.tsimport { PugError } from '@pug-sh/node' import { pug } from './pug' try { const profile = await pug.profiles.getByExternalId('user_123') for await (const p of pug.profiles.list()) { console.log(p.externalId) } } catch (err) { if (err instanceof PugError) console.error(err.code, err.message) }
What to watch for in Node.js
The specifics that decide whether the numbers you see are the numbers that happened.
A private key is not a public key
The browser key (pub_…) is write-scoped and meant to be shipped. The server key (prv_…) additionally authorizes reads of every profile and insight in the project. It belongs in server environment variables only, and the SDK refuses to start without one.
Use the same id as the browser
Server events land on the same profile as client events when both name the same user id, the one you pass to identify() in the browser. Get that right and one person’s client and server activity share a single timeline.
Ingestion never throws, reads do
track() and identify() log and drop rather than propagate, so analytics can never take down a request handler. Read methods run in your control flow and throw PugError carrying the Connect code and cause.
Not on Node? Use the HTTP API
The SDK is a convenience layer over the same Connect services your backend can call directly with a private key, so Go, Python, Ruby, or anything that can POST JSON reaches the identical endpoints.
Product analytics in your Node.js app
-
Trusted server events
track() names the user explicitly (no ambient browser session), so payments, webhooks, and jobs land on the right profile.
-
Profile traits from your backend
identify() writes plan, role, or billing state from the one place that actually knows them.
-
Reads, not just writes
A private key unlocks profiles, activity, and the same insight queries the dashboard runs, usable inside your own backend logic.
-
Pairs with the Web SDK
Client autocapture and authoritative server events share one timeline per person when both name the same user.
Read it in the docs
The Node SDK reference goes deeper than a setup page can: every option, every event, and the edge cases.
Pug + Node.js: common questions
When should I use the Node SDK instead of the Web SDK?
Use the Node SDK for events that must be trustworthy or happen off the browser, completed payments, subscription changes, background jobs, webhooks. Use the Web SDK for in-page autocapture. They write to the same profile, so a person’s client and server events live on one timeline.
Does the Node SDK autocapture?
No. Autocapture is a browser feature. On the server you call track() explicitly with the user id and event name. That’s by design: server events are deliberate rather than ambient. The server still enriches every event it receives with geo and bot-detection properties you don’t set yourself.
Can the Node SDK read data back?
Yes. With a private key it exposes profiles (get, get by external id, and an auto-paginating list), activity (feed, event explorer, heatmap, profile stats), and insights (the same query spec the dashboard uses), so Pug data can drive your own backend logic.
Will a failed analytics call break my request handler?
No. track() and identify() never throw: invalid input, a closed client, and transport failures are logged and dropped. Only the read methods throw, and they throw PugError carrying the Connect code, because those run inside your control flow and you need to decide what to do.
What if my backend isn’t Node?
Call the same Connect services directly over HTTP with your private key. The SDK is a convenience wrapper, not a gate: any runtime that can POST JSON can send events and run reads.
Add Pug to your Node.js app.
Open-source product analytics with unified profiles. Self-host under AGPL-3.0, or use the free cloud during open beta.
Questions? Email hello@pug.sh