@kanban-pm/sdk-node
v0.1.0
Published
Server-side Node.js analytics SDK for Kanban PM
Maintainers
Readme
@kanban-pm/sdk-node
Server-side Node.js analytics SDK for Kanban PM. Built for reliability — every server event matters.
Installation
npm install @kanban-pm/sdk-nodeQuick Start — Long-Running Server
For Express, Fastify, NestJS, or any long-running process:
import Analytics from '@kanban-pm/sdk-node';
// Initialize once at application startup
await Analytics.init(process.env.ANALYTICS_KEY!, {
endpoint: 'https://your-app.com/api/ingest',
validateOnInit: true, // validate API key eagerly at boot
batchMode: true, // batch events for throughput
flushInterval: 2000,
serviceName: 'api-server',
serviceVersion: process.env.APP_VERSION,
});
// Track events — fire and forget by default
Analytics.track({
userId: 'user_123',
event: 'payment_completed',
properties: { value: 99, currency: 'USD' },
});Quick Start — Serverless (Vercel Functions, AWS Lambda)
In serverless, the process may exit before a batch flush completes. Use direct sends with await mode for critical events:
import Analytics from '@kanban-pm/sdk-node';
await Analytics.init(process.env.ANALYTICS_KEY!, {
endpoint: 'https://your-app.com/api/ingest',
validateOnInit: false, // skip network call on cold start
batchMode: false, // no batching in serverless
serviceName: 'payment-webhook',
});
// Await critical events to ensure delivery before the function exits
await Analytics.track(
{ userId: 'user_123', event: 'payment_completed', properties: { value: 99 } },
{ await: true },
);Quick Start — Edge Runtime (Vercel Edge, Cloudflare Workers)
Edge runtimes have no process, os, or shutdown hooks. The SDK handles this automatically:
import Analytics from '@kanban-pm/sdk-node';
await Analytics.init(process.env.ANALYTICS_KEY!, {
endpoint: 'https://your-app.com/api/ingest',
validateOnInit: false,
batchMode: false,
serviceName: 'edge-middleware',
});
// Use await mode for reliability
await Analytics.track(
{ userId: 'user_123', event: 'edge_request' },
{ await: true },
);Fire and Forget vs Await Mode
By default, track(), identify(), and group() are fire-and-forget: they return void immediately and send the event in the background.
// Fire and forget (default) — returns void
Analytics.track({ userId: 'u1', event: 'page_viewed' });
// Await mode — returns Promise<void>, throws on failure
await Analytics.track(
{ userId: 'u1', event: 'payment_completed', properties: { value: 99 } },
{ await: true },
);When to use each:
| Mode | Use when... | |------|-------------| | Fire and forget | Non-critical events (page views, clicks, feature usage) | | Await | Critical events (payments, subscriptions, user creation) where you need delivery confirmation |
In await mode, the SDK retries on network errors and 5xx responses with exponential backoff. It throws typed errors on non-retryable failures (400, 401).
Batching Guide
Enable batch mode for high-throughput long-running servers:
await Analytics.init(apiKey, {
batchMode: true,
batchSize: 50, // flush when 50 events accumulate
flushInterval: 2000, // or every 2 seconds, whichever comes first
});
// Events are queued and sent in batches
Analytics.track({ userId: 'u1', event: 'item_viewed' });
Analytics.track({ userId: 'u2', event: 'item_viewed' });
// Manual flush
const result = await Analytics.flush();
console.log(`Sent: ${result.sent}, Failed: ${result.failed}`);The SDK automatically flushes on SIGTERM, SIGINT, and beforeExit to prevent data loss during deployments.
Request-Scoped Tracking with withContext()
Attach a requestId or traceId to every event fired during a single request without passing it to every track() call:
import Analytics from '@kanban-pm/sdk-node';
// Express middleware example
app.use((req, res, next) => {
req.analytics = Analytics.withContext({
requestContext: {
requestId: req.id,
traceId: req.headers['x-trace-id'],
ip: req.ip,
},
});
next();
});
// In your route handler — requestId is automatically included
app.post('/api/checkout', async (req, res) => {
await req.analytics.track(
{ userId: req.user.id, event: 'checkout_completed', properties: { total: 99 } },
{ await: true },
);
res.json({ success: true });
});The scoped instance shares the parent's queue and transport — it only adds context.
Typed Event Catalog
For compile-time safety over which events and properties are valid:
import { AnalyticsNode, createCatalog } from '@kanban-pm/sdk-node';
const analytics = new AnalyticsNode();
await analytics.init(apiKey, options);
const catalog = createCatalog(analytics, {
events: {
payment_completed: {
value: 0, // number
currency: '', // string
planId: '', // string
},
subscription_created: {
planId: '',
billingInterval: '' as 'monthly' | 'annual',
trialDays: 0,
},
},
});
// TypeScript enforces correct event names and property shapes:
catalog.track(
{ userId: 'u1' },
'payment_completed',
{ value: 99, currency: 'USD', planId: 'pro' },
); // ✓ compiles
catalog.track(
{ userId: 'u1' },
'payment_completed',
{ amount: 99 }, // ✗ TypeScript error — 'amount' not in schema
);
catalog.track(
{ userId: 'u1' },
'wrong_event', // ✗ TypeScript error — event not in catalog
{},
);Error Handling Reference
All errors extend AnalyticsError with a code property:
| Error | Code | When |
|-------|------|------|
| AuthenticationError | AUTHENTICATION_ERROR | Invalid API key (401) |
| ValidationError | VALIDATION_ERROR | Missing identity, SDK not initialized |
| NetworkError | NETWORK_ERROR | Network failure, non-retryable HTTP errors |
| RateLimitError | RATE_LIMIT_ERROR | Server returned 429. Has retryAfterMs |
| TimeoutError | TIMEOUT_ERROR | Request exceeded timeout |
import { AuthenticationError, RateLimitError } from '@kanban-pm/sdk-node';
try {
await Analytics.track({ userId, event: 'payment' }, { await: true });
} catch (err) {
if (err instanceof AuthenticationError) {
// API key is invalid — check configuration
} else if (err instanceof RateLimitError) {
// Back off for err.retryAfterMs
}
}In fire-and-forget mode, errors are silently swallowed (or logged to console in debug mode).
Middleware
Add middleware to enrich, filter, or transform events:
// Enrich all events with deployment info
Analytics.use((event, next) => {
next({
...event,
properties: {
...event.properties,
deploymentId: process.env.DEPLOYMENT_ID,
},
});
});
// Drop internal/test events
Analytics.use((event, next) => {
if (event.properties?.internal) return; // dropped
next(event);
});Environment Variables
| Variable | Description |
|----------|-------------|
| ANALYTICS_KEY | Your project API key |
| NODE_ENV | Auto-populated in context.server.environment |
| APP_VERSION | Pass as serviceVersion option |
Identity Model
Three identity patterns:
// 1. User-attributed event
Analytics.track({ userId: 'user_123', event: 'purchase' });
// 2. System event (no user)
Analytics.track({ anonymousId: 'cron', event: 'daily_digest_sent' });
// 3. Tenant/group event (B2B)
Analytics.track({ groupId: 'org_456', event: 'seat_added' });At least one of userId, anonymousId, or groupId must be present. The SDK throws ValidationError synchronously if all three are absent.
API Reference
Analytics.init(apiKey, options?)
Initialize the SDK. Call once at application startup.
Analytics.track(event, options?)
Track an event. Returns void (fire-and-forget) or Promise<void> (await mode).
Analytics.identify(event, options?)
Identify a user with traits. userId is required.
Analytics.group(event, options?)
Associate a user with a group. groupId is required.
Analytics.flush()
Manually flush all queued events. Returns Promise<{ sent, failed }>.
Analytics.shutdown()
Graceful shutdown: flush queue, wait for in-flight events, teardown.
Analytics.withContext(options)
Create a scoped instance with pre-set request context.
Analytics.use(middleware)
Add a middleware function to the pipeline.
