oakdata-node
v1.0.0
Published
OakData server-side analytics SDK — capture events from Node.js, Next.js server components, API routes, and edge functions.
Maintainers
Readme
oakdata-node
OakData's server-side analytics SDK — capture trusted events (signups, payments, jobs) from Node.js, Next.js server code, route handlers, and edge/serverless functions.
Companion to the browser tracker
oakdata-js. Use the browser SDK for
pageviews, autocapture, and replay; use this one for events the browser can't
see. Full docs: https://oakdata.co/docs
Install
npm install oakdata-nodeAuthenticate
Use a secret key (oak_sec_…) from your project settings — server-only,
never NEXT_PUBLIC_:
OAK_SECRET_KEY=oak_sec_xxxxxxxxxxxxxxxxxxxxxxxxServerless / short-lived (Next.js, edge)
Server functions are short-lived, so send immediately and await shutdown()
before they return — flushAt: 1, flushInterval: 0:
import { OakClient } from 'oakdata-node'
const oak = new OakClient(process.env.OAK_SECRET_KEY!, {
host: process.env.NEXT_PUBLIC_OAK_HOST,
flushAt: 1,
flushInterval: 0,
})
oak.capture({
distinctId: user.id,
event: 'subscription_started',
properties: { plan: 'pro', mrr: 49 },
})
await oak.shutdown()When you're tracking inbound requests server-side, forward the visitor's
User-Agent and ip so OakData can attribute and verify bot/crawler traffic
(otherwise the only UA the ingest endpoint sees is your server's):
oak.capture({
distinctId,
event: '$pageview',
userAgent: req.headers['user-agent'], // classify bot traffic
ip: req.headers['x-forwarded-for'], // verify GPTBot/Googlebot claims
})Full bot detection in one line
AI crawlers — GPTBot, ClaudeBot, CCBot, Bytespider, PerplexityBot — never run
JavaScript, so the browser snippet can't see them. Capturing requests on the
server is the only way to surface (and verify) that traffic. The middleware does
it for you: one $pageview per inbound page request, with the visitor's UA + IP
forwarded automatically.
Express / Connect:
const oak = new OakClient(process.env.OAK_SECRET_KEY!)
app.use(oak.expressMiddleware())Next.js middleware (or any Web Request):
// middleware.ts
import { oak } from './lib/oak'
export function middleware(request: NextRequest) {
oak.trackRequest(request)
return NextResponse.next()
}Static assets (.js, .css, images, /_next/…) and non-GET/HEAD requests
are skipped by default. Anonymous requests collapse into one visitor per IP + UA
so a crawler shows up as a single row. Customize with options:
| Option | Type | Default | Notes |
| ------------------- | ----------------------------- | ------------ | -------------------------------------------------- |
| event | string | $pageview | Event name to capture |
| distinctId | (meta) => string ⏐ undefined| IP+UA hash | Return a real user id when you have one |
| shouldTrack | (meta) => boolean | — | Full override of the track/skip decision |
| ignorePaths | (string ⏐ RegExp)[] | [] | Paths to never track |
| trackAssets | boolean | false | Also track static-asset requests |
| requireHtmlAccept | boolean | false | Only track Accept: text/html (drops some bots) |
Long-running servers
In a persistent process (Express, a worker), create the client once and let it batch. Flush on graceful exit:
const oak = new OakClient(process.env.OAK_SECRET_KEY!) // batches: 20 events / 10s
process.on('SIGTERM', async () => {
await oak.shutdown()
process.exit(0)
})API
new OakClient(secretKey, options)
oak.capture({ distinctId, event, properties?, groups?, timestamp?, userAgent?, ip? })
oak.identify({ distinctId, properties? }) // properties = user traits
oak.alias({ distinctId, alias }) // link a new id
oak.groupIdentify({ groupType, groupKey, properties? })
oak.expressMiddleware(options?) // Express/Connect: app.use(...)
oak.trackRequest(request, options?) // Next.js middleware / Web Request
await oak.flush() // send queued events now
await oak.shutdown() // flush + stop the timerOptions
| Option | Type | Default | Notes |
| ---------------- | --------- | -------------------- | -------------------------------------------- |
| host | string | https://oakdata.co | Posts to ${host}/api/oak/ingest |
| ingestPath | string | /api/oak/ingest | Override behind a reverse proxy |
| flushAt | number | 20 | Flush after N queued events |
| flushInterval | number | 10000 | Flush every N ms (0 disables the timer) |
| requestTimeout | number | 10000 | Per-request network timeout (ms) |
| maxRetries | number | 3 | Retries on 429 / 5xx / network errors |
| disabled | boolean | false | Make every call a no-op (tests / env gating) |
| debug | boolean | false | Log queueing, flushes, and errors |
| fetch | fetch | global fetch | Custom fetch (Node 18+ has one built in) |
Notes
- Calls never throw. Transient transport failures retry with exponential
backoff and are dropped if still failing — analytics can't take down your app
(
debug: trueto surface them). - Identity. Pass the same
distinctIdyou calloak.identify(userId)with in the browser SDK so server and client events resolve to one user. - Sessions are synthetic. Each
distinctIdgets one in-process session UUID withsession_numberalways1. Server events are not browser sessions and do not rotate on a 30-minute timeout. - Requires Node 18+ for the built-in global
fetchandcrypto.randomUUID.
License
MIT
