@layers/node
v2.1.0
Published
Layers Analytics Node.js SDK — thin wrapper over Rust core via WASM
Downloads
1,231
Readme
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
npm install @layers/node
# or
yarn add @layers/node
# or
pnpm add @layers/nodeQuick Start
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
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. |
const layers = new LayersNode({
appId: 'your-app-id',
environment: 'production',
flushIntervalMs: 5000,
flushThreshold: 50,
maxQueueSize: 50000,
shutdownFlushTimeoutMs: 10000,
handleSignals: true
});Core API
Event Tracking
track(distinctId: string, eventName: string, properties?: EventProperties): voidTrack an event for a specific user. The distinctId is required on every call.
layers.track('user_123', 'purchase_completed', {
product_id: 'sku_123',
price: 9.99,
currency: 'USD'
});Screen Tracking
screen(distinctId: string, screenName: string, properties?: EventProperties): voidlayers.screen('user_123', 'Dashboard', { tab: 'overview' });User Properties
setUserProperties(distinctId: string, properties: UserProperties): voidlayers.setUserProperties('user_123', {
email: '[email protected]',
plan: 'premium',
company: 'Acme Corp'
});Consent Management
setConsent(consent: ConsentState): void
getConsentState(): ConsentStateConsent applies globally to the SDK instance, not per-user.
layers.setConsent({ analytics: true, advertising: false });Session & Queue
getSessionId(): string
queueDepth(): numberFlush & Shutdown
// Flush all queued events
async flush(): Promise<void>
// Graceful shutdown: flush remaining events (with timeout), then stop
async shutdown(): Promise<void>Error Handling
on(event: 'error', listener: (error: Error) => void): this
off(event: 'error', listener: (error: Error) => void): thislayers.on('error', (error) => {
console.error('Layers error:', error.message);
});Express Middleware
Import from @layers/node/express:
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
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']
}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 pathstatus_code-- Response status coderesponse_time_ms-- Response time in milliseconds (iftrackResponseTimeis true)
User Identification
The middleware resolves the distinctId from request headers:
X-User-IdheaderX-App-User-Idheader- 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:
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.
// 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
interface RequestContext {
distinctId: string;
properties: Record<string, unknown>;
}
function withLayersContext<T>(context: RequestContext, fn: () => T): T;
function getLayersContext(): RequestContext | undefined;Server Page View Tracking
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.
// In a Next.js Server Component or API route
trackServerPageView(layers, '/dashboard');
// With explicit distinctId
trackServerPageView(layers, '/dashboard', 'user_123');Server Action Tracking
function trackServerAction(
layers: LayersNode,
actionName: string,
properties?: Record<string, unknown>,
distinctId?: string
): void;Tracks a server_action event with action_name as a property.
// 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
// 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
// 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.
// 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:
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:
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 trueTypeScript Types
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';