npm package: @layers/node
Description: Layers Analytics Node.js SDK — thin wrapper over Rust core via WASM
Author: Layers Team
Latest version: 2.2.0

# Layers Node.js SDK

`@layers/node` is the Layers analytics SDK for server-side Node.js applications. It provides event tracking, screen tracking, user property management, consent management, Express middleware for automatic HTTP request tracking, and Next.js integration with `AsyncLocalStorage` for request context propagation.

Unlike client-side SDKs that store a user ID per session, the Node.js SDK requires a `distinctId` on every call. This is the correct pattern for servers handling concurrent requests from many users.

## Requirements

- Node.js 18+

## Installation

```bash
npm install @layers/node
# or
yarn add @layers/node
# or
pnpm add @layers/node
```

## Quick Start

```typescript
import { LayersNode } from '@layers/node';

const layers = new LayersNode({
  appId: 'your-app-id',
  environment: 'production'
});

// Track events with a distinctId
layers.track('user_123', 'page_view', { path: '/dashboard' });

// Track screen views
layers.screen('user_123', 'Dashboard');

// Set user properties
layers.setUserProperties('user_123', { plan: 'premium' });

// Flush on demand
await layers.flush();

// Graceful shutdown
await layers.shutdown();
```

## Configuration

### LayersNodeConfig

```typescript
interface LayersNodeConfig {
  appId: string;
  environment: 'development' | 'staging' | 'production';
  enableDebug?: boolean;
  baseUrl?: string;
  flushIntervalMs?: number;
  flushThreshold?: number;
  maxQueueSize?: number;
  maxBatchSize?: number;
  shutdownFlushTimeoutMs?: number;
  handleSignals?: boolean;
  persistenceDir?: string;
}
```

| Option                   | Type          | Default                          | Description                                                                             |
| ------------------------ | ------------- | -------------------------------- | --------------------------------------------------------------------------------------- |
| `appId`                  | `string`      | _required_                       | Your Layers application identifier.                                                     |
| `environment`            | `Environment` | _required_                       | `'development'`, `'staging'`, or `'production'`.                                        |
| `enableDebug`            | `boolean`     | `false`                          | Enable verbose console logging.                                                         |
| `baseUrl`                | `string`      | `"https://in.layers.com"`        | Custom ingest API endpoint.                                                             |
| `flushIntervalMs`        | `number`      | `10000`                          | Automatic flush interval in milliseconds. Server SDKs default to 10s for lower latency. |
| `flushThreshold`         | `number`      | `20`                             | Queue size that triggers an automatic flush.                                            |
| `maxQueueSize`           | `number`      | `10000`                          | Maximum events in the queue before dropping.                                            |
| `maxBatchSize`           | `number`      | `undefined`                      | Maximum events sent in a single HTTP batch.                                             |
| `shutdownFlushTimeoutMs` | `number`      | `5000`                           | Maximum time (ms) to wait for a final flush during shutdown.                            |
| `handleSignals`          | `boolean`     | `true`                           | Register SIGTERM/SIGINT handlers for graceful shutdown.                                 |
| `persistenceDir`         | `string`      | `os.tmpdir()/layers-sdk/<appId>` | Directory for event persistence files.                                                  |

```typescript
const layers = new LayersNode({
  appId: 'your-app-id',
  environment: 'production',
  flushIntervalMs: 5000,
  flushThreshold: 50,
  maxQueueSize: 50000,
  shutdownFlushTimeoutMs: 10000,
  handleSignals: true
});
```

## Core API

### Event Tracking

```typescript
track(distinctId: string, eventName: string, properties?: EventProperties): void
```

Track an event for a specific user. The `distinctId` is required on every call.

```typescript
layers.track('user_123', 'purchase_completed', {
  product_id: 'sku_123',
  price: 9.99,
  currency: 'USD'
});
```

### Screen Tracking

```typescript
screen(distinctId: string, screenName: string, properties?: EventProperties): void
```

```typescript
layers.screen('user_123', 'Dashboard', { tab: 'overview' });
```

### User Properties

```typescript
setUserProperties(distinctId: string, properties: UserProperties): void
```

```typescript
layers.setUserProperties('user_123', {
  email: 'user@example.com',
  plan: 'premium',
  company: 'Acme Corp'
});
```

### Consent Management

```typescript
setConsent(consent: ConsentState): void
getConsentState(): ConsentState
```

Consent applies globally to the SDK instance, not per-user.

```typescript
layers.setConsent({ analytics: true, advertising: false });
```

### Session & Queue

```typescript
getSessionId(): string
queueDepth(): number
```

### Flush & Shutdown

```typescript
// Flush all queued events
async flush(): Promise<void>

// Graceful shutdown: flush remaining events (with timeout), then stop
async shutdown(): Promise<void>
```

### Error Handling

```typescript
on(event: 'error', listener: (error: Error) => void): this
off(event: 'error', listener: (error: Error) => void): this
```

```typescript
layers.on('error', (error) => {
  console.error('Layers error:', error.message);
});
```

## Express Middleware

Import from `@layers/node/express`:

```typescript
import { LayersNode } from '@layers/node';
import { layersExpressMiddleware } from '@layers/node/express';
import express from 'express';

const app = express();
const layers = new LayersNode({
  appId: 'your-app-id',
  environment: 'production'
});

app.use(layersExpressMiddleware(layers));
```

### Middleware Options

```typescript
interface LayersExpressOptions {
  trackRequests?: boolean; // Track each HTTP request. Default: true
  trackResponseTime?: boolean; // Include response_time_ms. Default: true
  ignorePaths?: string[]; // Paths to skip. Default: ['/health', '/healthz', '/ready', '/favicon.ico']
}
```

```typescript
app.use(
  layersExpressMiddleware(layers, {
    trackRequests: true,
    trackResponseTime: true,
    ignorePaths: ['/health', '/healthz', '/ready', '/favicon.ico', '/metrics']
  })
);
```

### What It Tracks

For each HTTP request (excluding ignored paths), the middleware tracks an `http_request` event with:

- `method` -- HTTP method (GET, POST, etc.)
- `path` -- Request path
- `status_code` -- Response status code
- `response_time_ms` -- Response time in milliseconds (if `trackResponseTime` is true)

### User Identification

The middleware resolves the `distinctId` from request headers:

1. `X-User-Id` header
2. `X-App-User-Id` header
3. Falls back to `'anonymous'`

The header value is sanitized: must match `[a-zA-Z0-9_@.+-]` and be at most 128 characters.

## Next.js Integration

Import from `@layers/node/nextjs`:

```typescript
import { LayersNode } from '@layers/node';
import {
  getLayersContext,
  trackServerAction,
  trackServerPageView,
  withLayersContext
} from '@layers/node/nextjs';
```

### AsyncLocalStorage Context

The Next.js integration uses `AsyncLocalStorage` to propagate request context (user identity and properties) through the request lifecycle.

```typescript
// In middleware or API route
withLayersContext({ distinctId: userId, properties: { role: 'admin' } }, async () => {
  // Inside this callback, getLayersContext() returns the context
  const ctx = getLayersContext();
  layers.track(ctx!.distinctId, 'api_call', { endpoint: '/users' });
});
```

### Request Context

```typescript
interface RequestContext {
  distinctId: string;
  properties: Record<string, unknown>;
}

function withLayersContext<T>(context: RequestContext, fn: () => T): T;
function getLayersContext(): RequestContext | undefined;
```

### Server Page View Tracking

```typescript
function trackServerPageView(layers: LayersNode, path: string, distinctId?: string): void;
```

Tracks a `page_view` event. Resolves `distinctId` from the current `AsyncLocalStorage` context, falling back to the provided `distinctId` or `'anonymous'`. Automatically adds `server_rendered: true` and any context properties.

```typescript
// In a Next.js Server Component or API route
trackServerPageView(layers, '/dashboard');

// With explicit distinctId
trackServerPageView(layers, '/dashboard', 'user_123');
```

### Server Action Tracking

```typescript
function trackServerAction(
  layers: LayersNode,
  actionName: string,
  properties?: Record<string, unknown>,
  distinctId?: string
): void;
```

Tracks a `server_action` event with `action_name` as a property.

```typescript
// In a Next.js Server Action
'use server';

export async function createPost(formData: FormData) {
  trackServerAction(layers, 'create_post', {
    title: formData.get('title')
  });
  // ... create post
}
```

### Next.js Middleware Example

```typescript
// middleware.ts
import { withLayersContext } from '@layers/node/nextjs';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const userId = request.headers.get('x-user-id') ?? 'anonymous';

  return withLayersContext(
    { distinctId: userId, properties: { path: request.nextUrl.pathname } },
    () => NextResponse.next()
  );
}
```

### Next.js API Route Example

```typescript
// app/api/users/route.ts
import { layers } from '@/lib/layers';

export async function GET(request: Request) {
  const userId = request.headers.get('x-user-id') ?? 'anonymous';

  layers.track(userId, 'api_call', {
    method: 'GET',
    path: '/api/users'
  });

  return Response.json({ users: [] });
}
```

## Signal Handling

By default (`handleSignals: true`), the SDK registers `SIGTERM` and `SIGINT` handlers that flush remaining events before the process exits. This is critical for:

- Kubernetes pod termination
- Docker container shutdown
- Ctrl+C during development

All `LayersNode` instances are flushed on signal receipt. Set `handleSignals: false` if you manage process signals yourself.

```typescript
// Disable automatic signal handling
const layers = new LayersNode({
  appId: 'your-app-id',
  environment: 'production',
  handleSignals: false
});

// Handle shutdown yourself
process.on('SIGTERM', async () => {
  await layers.shutdown();
  process.exit(0);
});
```

## Event Persistence

Events are persisted to the filesystem (default: `os.tmpdir()/layers-sdk/<appId>`) so they survive process restarts. Configure the directory:

```typescript
const layers = new LayersNode({
  appId: 'your-app-id',
  environment: 'production',
  persistenceDir: '/var/data/layers'
});
```

## Automatic Behaviors

- **Periodic flush**: Events are flushed every `flushIntervalMs` (default 10s).
- **Signal handling**: SIGTERM/SIGINT trigger graceful shutdown with flush.
- **Event persistence**: Events are persisted to disk and rehydrated on restart.
- **Retry with backoff**: Failed network requests are retried automatically.
- **Circuit breaker**: Repeated failures temporarily disable network calls.

## Multiple Instances

You can create multiple `LayersNode` instances for different apps:

```typescript
const layersApp1 = new LayersNode({
  appId: 'app-1',
  environment: 'production'
});

const layersApp2 = new LayersNode({
  appId: 'app-2',
  environment: 'production'
});

// Both are flushed on SIGTERM if handleSignals is true
```

## TypeScript Types

```typescript
import type {
  ConsentState,
  Environment,
  ErrorListener,
  EventProperties,
  LayersNodeConfig,
  UserProperties
} from '@layers/node';
import type { LayersExpressOptions } from '@layers/node/express';
import type { RequestContext } from '@layers/node/nextjs';
```