npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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/node

Quick 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): 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 Tracking

screen(distinctId: string, screenName: string, properties?: EventProperties): void
layers.screen('user_123', 'Dashboard', { tab: 'overview' });

User Properties

setUserProperties(distinctId: string, properties: UserProperties): void
layers.setUserProperties('user_123', {
  email: '[email protected]',
  plan: 'premium',
  company: 'Acme Corp'
});

Consent Management

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

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

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

Session & Queue

getSessionId(): string
queueDepth(): number

Flush & 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): this
layers.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 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:

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 true

TypeScript 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';