npm package: @kanban-pm/sdk-node
Description: Server-side Node.js analytics SDK for Kanban PM
Latest version: 0.1.0

# @kanban-pm/sdk-node

Server-side Node.js analytics SDK for Kanban PM. Built for reliability — every server event matters.

## Installation

```bash
npm install @kanban-pm/sdk-node
```

## Quick Start — Long-Running Server

For Express, Fastify, NestJS, or any long-running process:

```typescript
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:

```typescript
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:

```typescript
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.

```typescript
// 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:

```typescript
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:

```typescript
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:

```typescript
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 |

```typescript
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:

```typescript
// 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:

```typescript
// 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.