
Security News
Feross on the 10 Minutes or Less Podcast: Nobody Reads the Code
Socket CEO Feross Aboukhadijeh joins 10 Minutes or Less, a podcast by Ali Rohde, to discuss the recent surge in open source supply chain attacks.
@layers/node
Advanced tools
@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.
npm install @layers/node
# or
yarn add @layers/node
# or
pnpm add @layers/node
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();
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
});
track(distinctId: string, eventName: string, properties?: EventProperties): void
Track 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(distinctId: string, screenName: string, properties?: EventProperties): void
layers.screen('user_123', 'Dashboard', { tab: 'overview' });
setUserProperties(distinctId: string, properties: UserProperties): void
layers.setUserProperties('user_123', {
email: 'user@example.com',
plan: 'premium',
company: 'Acme Corp'
});
setConsent(consent: ConsentState): void
getConsentState(): ConsentState
Consent applies globally to the SDK instance, not per-user.
layers.setConsent({ analytics: true, advertising: false });
getSessionId(): string
queueDepth(): number
// Flush all queued events
async flush(): Promise<void>
// Graceful shutdown: flush remaining events (with timeout), then stop
async shutdown(): Promise<void>
on(event: 'error', listener: (error: Error) => void): this
off(event: 'error', listener: (error: Error) => void): this
layers.on('error', (error) => {
console.error('Layers error:', error.message);
});
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));
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']
})
);
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 (if trackResponseTime is true)The middleware resolves the distinctId from request headers:
X-User-Id headerX-App-User-Id header'anonymous'The header value is sanitized: must match [a-zA-Z0-9_@.+-] and be at most 128 characters.
Import from @layers/node/nextjs:
import { LayersNode } from '@layers/node';
import {
getLayersContext,
trackServerAction,
trackServerPageView,
withLayersContext
} from '@layers/node/nextjs';
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' });
});
interface RequestContext {
distinctId: string;
properties: Record<string, unknown>;
}
function withLayersContext<T>(context: RequestContext, fn: () => T): T;
function getLayersContext(): RequestContext | undefined;
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');
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
}
// 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()
);
}
// 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: [] });
}
By default (handleSignals: true), the SDK registers SIGTERM and SIGINT handlers that flush remaining events before the process exits. This is critical for:
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);
});
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'
});
flushIntervalMs (default 10s).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 true
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';
FAQs
Layers Analytics Node.js SDK — thin wrapper over Rust core via WASM
We found that @layers/node demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 4 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Socket CEO Feross Aboukhadijeh joins 10 Minutes or Less, a podcast by Ali Rohde, to discuss the recent surge in open source supply chain attacks.

Research
/Security News
Campaign of 108 extensions harvests identities, steals sessions, and adds backdoors to browsers, all tied to the same C2 infrastructure.

Security News
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.