@v-tilt/node
v1.0.0
Published
vTilt analytics SDK for Node.js
Readme
@v-tilt/node
Server-side analytics SDK for vTilt. Capture events, identify users, and track exceptions from Node.js, Cloudflare Workers, Deno, and other server runtimes. The wire format matches the browser SDK, so server and browser events converge on the same person record automatically.
Install
npm install @v-tilt/node
# or: pnpm add @v-tilt/nodeRequires Node.js >= 18 (for the global fetch). Works in Cloudflare Workers
with nodejs_compat enabled.
Quick start
import { VTiltNode } from "@v-tilt/node";
const vtilt = new VTiltNode(process.env.VTILT_TRACKER_TOKEN!, {
host: "https://your-vtilt-instance.com",
});
vtilt.capture({
distinctId: "user_123",
event: "purchase",
properties: { amount: 99.99 },
});
// Always flush before the process exits.
await vtilt.shutdown();capture() is non-blocking: events are queued in memory and flushed in
batches. Call flush() to send immediately, or shutdown() before exit.
Per-request context
Set identity once per request instead of repeating it on every call:
app.use((req, res, next) => {
vtilt.setContext({
distinctId: req.user?.id,
anonymousId: req.cookies?.vt_anon,
ip: req.ip, // enables GeoIP enrichment (see below)
});
res.on("finish", () => vtilt.clearContext());
next();
});Identify & alias
// Set properties for a known user
vtilt.identify({ distinctId: "user_123", properties: { plan: "pro" } });
// Link an anonymous browser session to the authenticated user
vtilt.identify({
distinctId: "user_123",
anonymousId: "anon_from_browser",
properties: { email: "[email protected]" },
});
// Link two known ids
vtilt.alias({ distinctId: "user_123", alias: "legacy_id_456" });Enrichment parity with the browser
A server cannot observe the end user's browser, so the SDK never sends
$browser / $os / device properties. It can carry GeoIP and the original
IP / User-Agent / referrer when you forward them from the incoming request:
vtilt.capture({
distinctId: "user_123",
event: "page_view",
ip: req.ip, // -> $ip, enables GeoIP exactly like the browser
userAgent: req.headers["user-agent"], // -> $raw_user_agent (optional)
referrer: req.headers["referer"], // -> $referrer (optional)
});GeoIP is smart by default: it runs only when an end-user ip is available.
When no IP is forwarded the SDK sets $geoip_disable so the caller's own server
IP is never geolocated. Force it either way with the disableGeoip option or the
per-call disableGeoip flag.
Global (super) properties
vtilt.register({ app_version: "2.1.0", environment: "production" });
vtilt.unregister("app_version");Registered properties are merged into every event at the lowest precedence
(per-event properties win on collisions). You can also pass
globalProperties to the constructor.
Error tracking
try {
doWork();
} catch (err) {
vtilt.captureException(err, { distinctId: "user_123" });
}Emits a $exception event with $exception_type, $exception_message, and
$exception_stack_trace_raw.
Lifecycle hooks
const off = vtilt.on("error", (err) => console.error("flush failed", err));
vtilt.on("flush", (batch) => metrics.increment("events.sent", batch.length));
// off() to unsubscribebefore_send lets you mutate or drop events before they are queued:
const vtilt = new VTiltNode(token, {
before_send: (event) => {
if (event.event === "debug_event") return null; // drop
event.payload.server = "api-1";
return event;
},
});GDPR
vtilt.optOut(); // drop all events
vtilt.optIn(); // resume
vtilt.isOptedOut();Serverless / edge
In short-lived runtimes (Lambda, Workers) there may be no later flush. Use the immediate variants, which send a single event and await the request:
await vtilt.captureImmediate({
distinctId: "user_123",
event: "webhook_received",
});Configuration
| Option | Default | Description |
| ------------------ | ----------------------- | --------------------------------------------------------- |
| host | http://localhost:3000 | API base URL (no trailing slash) |
| flushAt | 20 | Queue size that triggers a flush |
| flushInterval | 10000 | Periodic flush interval (ms) |
| maxBatchSize | 100 | Max events per HTTP request |
| maxQueueSize | 1000 | Max queued events; oldest dropped when exceeded |
| requestTimeout | 10000 | HTTP request timeout (ms) |
| fetchRetryCount | 3 | Retries for failed flushes |
| fetchRetryDelay | 3000 | Delay between retries (ms) |
| disableGeoip | undefined (smart) | true/false to force; smart mode keys off forwarded IP |
| globalProperties | {} | Super properties merged into every event |
| before_send | — | Hook(s) to mutate/drop events |
| optOut | false | Start opted out (GDPR) |
| compression | gzip-js | Body compression (gzip-js or none) |
| disabled | false | Kill switch — all methods are no-ops |
| fetch | global fetch | Custom fetch implementation |
| debug | false | Console debug logging |
License
MIT
