Zero-config distributed tracing and performance monitoring for Express and NestJS applications.
- Zero Configuration - Works out of the box with sensible defaults
- Automatic Instrumentation - No code changes needed
- Express Support - Simple middleware integration
- NestJS Support - Module and interceptor-based tracing
- TypeScript First - Full type definitions included
- HTTP Request Tracing - Track every request, route, and handler
- Database Tracing - Automatic query instrumentation for PostgreSQL, MySQL, MongoDB, Redis
- Client IP Capture - Automatic IP detection for DDoS & traffic analysis
- Error Tracking - Capture exceptions with full context
- Code Monitoring - Live debugging with breakpoints and variable inspection
- Metrics API - Counter, Gauge, and Histogram metrics with automatic OTLP export
- Low Overhead - < 5% performance impact
npm install @tracekit/node-apmconst express = require('express');
const tracekit = require('@tracekit/node-apm');
const app = express();
// Initialize TraceKit
tracekit.init({
apiKey: process.env.TRACEKIT_API_KEY,
serviceName: 'my-express-app',
});
// Add middleware (must be before routes)
app.use(tracekit.middleware());
// Your routes
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(3000);import express from 'express';
import * as tracekit from '@tracekit/node-apm';
const app = express();
// Initialize TraceKit
tracekit.init({
apiKey: process.env.TRACEKIT_API_KEY!,
serviceName: 'my-express-app',
});
// Add middleware
app.use(tracekit.middleware());
// Your routes
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(3000);// app.module.ts
import { Module } from '@nestjs/common';
import { TracekitModule } from '@tracekit/node-apm/nestjs';
@Module({
imports: [
TracekitModule.forRoot({
apiKey: process.env.TRACEKIT_API_KEY!,
serviceName: 'my-nestjs-app',
}),
],
})
export class AppModule {}That's it! Your app is now automatically traced.
Debug your application locally without creating a cloud account using TraceKit Local UI.
# Install Local UI globally
npm install -g @tracekit/local-ui
# Start it
tracekit-localThe Local UI will start at http://localhost:9999 and automatically open in your browser.
When running in development mode (NODE_ENV=development), the SDK automatically:
- Detects if Local UI is running at
http://localhost:9999 - Sends traces to both Local UI and cloud (if API key is present)
- Falls back gracefully if Local UI is not available
No code changes needed! Just set NODE_ENV=development:
export NODE_ENV=development
export TRACEKIT_API_KEY=your-key # Optional - works without it!
node app.jsYou'll see traces appear in real-time at http://localhost:9999.
- Real-time trace viewing in your browser
- Works completely offline
- No cloud account required
- Zero configuration
- Automatic cleanup (1000 traces max, 1 hour retention)
To use Local UI without cloud sending:
# Don't set TRACEKIT_API_KEY
export NODE_ENV=development
node app.jsTraces will only go to Local UI.
To disable automatic Local UI detection:
export NODE_ENV=production
# or don't run Local UITraceKit includes production-safe code monitoring for live debugging without redeployment.
import * as tracekit from '@tracekit/node-apm';
// Enable code monitoring
const client = tracekit.init({
apiKey: process.env.TRACEKIT_API_KEY!,
serviceName: 'my-app',
enableCodeMonitoring: true, // Enable live debugging
});Add checkpoints anywhere in your code to capture variable state and stack traces:
// In any service or controller
app.post('/checkout', async (req, res) => {
const cart = req.body.cart;
const userId = req.body.userId;
// Capture snapshot at this point
await client.captureSnapshot('checkout-validation', {
userId,
cartItems: cart.items.length,
totalAmount: cart.total,
});
// Process payment...
const result = await processPayment(cart);
// Another checkpoint
await client.captureSnapshot('payment-complete', {
userId,
paymentId: result.paymentId,
success: result.success,
});
res.json(result);
});- Auto-Registration: First call to
captureSnapshot()automatically creates breakpoints in TraceKit - Smart Matching: Breakpoints match by function name + label (stable across code changes)
- Background Sync: SDK polls for active breakpoints every 30 seconds
- Production Safe: No performance impact when breakpoints are inactive
Snapshots include:
- Variables: Local variables at capture point
- Stack Trace: Full call stack with file/line numbers
- Request Context: HTTP method, URL, headers, query params
- Execution Time: When the snapshot was captured
import { Injectable, Inject } from '@nestjs/common';
import { SnapshotClient } from '@tracekit/node-apm';
@Injectable()
export class PaymentService {
constructor(
@Inject('TRACEKIT_SNAPSHOT_CLIENT')
private snapshotClient?: SnapshotClient
) {}
async processPayment(order: Order) {
// Automatic snapshot capture
await this.snapshotClient?.checkAndCaptureWithContext('payment-processing', {
orderId: order.id,
amount: order.amount,
});
// ... payment logic
}
}Get your API key at https://app.tracekit.dev
The SDK automatically scans snapshot variables for sensitive data before sending to the backend. Enabled by default when code monitoring is active.
Auto-detected patterns: passwords, API keys, tokens, credit cards, emails, SSNs, JWTs, AWS keys, Stripe keys, private keys.
Name-based redaction — Variables with sensitive names are redacted as [REDACTED:sensitive_name]:
// Input: { password: 'hunter2', api_key: 'sk-abc123' }
// Output: { password: '[REDACTED:sensitive_name]', api_key: '[REDACTED:sensitive_name]' }Matched names: password, passwd, pwd, secret, token, key, credential, api_key, apikey. Uses letter-based boundaries (not \b) to correctly match names like api_key and user_token.
Value-based redaction — Values matching known patterns are redacted as [REDACTED:type]:
// Input: { card: '4111-1111-1111-1111' }
// Output: { card: '[REDACTED:credit_card]' }Server-side toggle to disable code monitoring per service. Controlled entirely from the TraceKit dashboard or API — no code changes needed.
# Disable code monitoring for a service
curl -X POST https://app.tracekit.dev/api/services/my-service/kill-switch \
-H "Content-Type: application/json" \
-d '{"enabled": true}'- When enabled, the SDK stops capturing snapshots immediately
- Polling frequency reduces to 60s
- When disabled, captures resume automatically
The SDK auto-discovers an SSE endpoint from the poll response. Once connected, breakpoint changes (create/update/delete) and kill switch events are received instantly without waiting for the next poll cycle.
- Falls back to polling if the SSE connection fails
- Reconnects automatically on disconnect
- No configuration required — works out of the box when
enableCodeMonitoringistrue
Protects the host application if the TraceKit backend becomes unreachable.
- After 3 capture failures within 60 seconds, code monitoring pauses automatically
- Resumes after a 5-minute cooldown period
- Configurable at init:
tracekit.init({
apiKey: process.env.TRACEKIT_API_KEY!,
serviceName: 'my-app',
enableCodeMonitoring: true,
circuitBreaker: {
failureThreshold: 5, // failures before opening
failureWindowMs: 120_000, // window for counting failures (ms)
cooldownMs: 600_000, // wait before retrying (ms)
},
});TraceKit APM includes a powerful metrics API for tracking application performance and business metrics.
TraceKit supports three types of metrics:
- Counter: Monotonically increasing values (requests, errors, events)
- Gauge: Point-in-time values that can go up or down (active connections, queue size)
- Histogram: Value distributions (request duration, payload sizes)
import * as tracekit from '@tracekit/node-apm';
const client = tracekit.init({
apiKey: process.env.TRACEKIT_API_KEY!,
serviceName: 'my-app',
});
// Create metrics
const requestCounter = client.counter('http.requests.total', {
service: 'my-app'
});
const activeRequestsGauge = client.gauge('http.requests.active', {
service: 'my-app'
});
const requestDurationHistogram = client.histogram('http.request.duration', {
unit: 'ms'
});
// Use metrics in your handlers
app.use((req, res, next) => {
const startTime = Date.now();
activeRequestsGauge.inc();
res.on('finish', () => {
requestCounter.inc();
activeRequestsGauge.dec();
const duration = Date.now() - startTime;
requestDurationHistogram.record(duration);
});
next();
});Counters track monotonically increasing values:
const counter = client.counter('events.processed', { type: 'order' });
// Increment by 1
counter.inc();
// Add custom amount
counter.add(5);Gauges track values that can increase or decrease:
const gauge = client.gauge('queue.size', { queue: 'orders' });
// Set to specific value
gauge.set(42);
// Increment
gauge.inc();
// Decrement
gauge.dec();Histograms track distributions of values:
const histogram = client.histogram('api.response.size', { unit: 'bytes' });
// Record a value
histogram.record(1024);
histogram.record(2048);import express from 'express';
import * as tracekit from '@tracekit/node-apm';
const app = express();
const client = tracekit.init({
apiKey: process.env.TRACEKIT_API_KEY!,
serviceName: 'express-app',
});
app.use(tracekit.middleware());
// Initialize metrics
const requestCounter = client.counter('http.requests.total');
const activeRequests = client.gauge('http.requests.active');
const requestDuration = client.histogram('http.request.duration', { unit: 'ms' });
const errorCounter = client.counter('http.errors.total');
// Metrics middleware
app.use((req, res, next) => {
const start = Date.now();
activeRequests.inc();
res.on('finish', () => {
requestCounter.inc();
activeRequests.dec();
requestDuration.record(Date.now() - start);
if (res.statusCode >= 400) {
errorCounter.inc();
}
});
next();
});
app.get('/', (req, res) => {
res.send('Hello World');
});
app.listen(3000);import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import * as tracekit from '@tracekit/node-apm';
@Injectable()
export class MetricsMiddleware implements NestMiddleware {
private requestCounter = tracekit.getClient().counter('http.requests.total');
private activeRequests = tracekit.getClient().gauge('http.requests.active');
private requestDuration = tracekit.getClient().histogram('http.request.duration', { unit: 'ms' });
use(req: Request, res: Response, next: NextFunction) {
const start = Date.now();
this.activeRequests.inc();
res.on('finish', () => {
this.requestCounter.inc();
this.activeRequests.dec();
this.requestDuration.record(Date.now() - start);
});
next();
}
}Add tags to metrics for dimensional analysis:
const counter = client.counter('api.requests', {
service: 'payment-api',
region: 'us-east-1',
environment: 'production'
});
counter.inc();const requests = client.counter('http.requests', { method: 'POST', endpoint: '/api/orders' });
const duration = client.histogram('http.duration', { endpoint: '/api/orders' });
const errors = client.counter('http.errors', { code: '500' });const queries = client.counter('db.queries', { operation: 'SELECT' });
const queryDuration = client.histogram('db.query.duration', { unit: 'ms' });
const connections = client.gauge('db.connections.active');const orders = client.counter('orders.created');
const revenue = client.histogram('orders.amount', { unit: 'usd' });
const inventory = client.gauge('inventory.stock', { product: 'laptop' });Metrics are automatically buffered and exported in batches:
- Buffer size: 100 metrics
- Flush interval: 10 seconds
- Endpoint: Automatically resolved to
/v1/metrics
Metrics are sent to TraceKit using OTLP format and appear in your dashboard with full dimensional analysis.
import * as tracekit from '@tracekit/node-apm';
tracekit.init({
// Required: Your TraceKit API key
apiKey: process.env.TRACEKIT_API_KEY,
// Optional: Service name (default: 'node-app')
serviceName: 'my-service',
// Optional: TraceKit endpoint (default: 'https://app.tracekit.dev/v1/traces')
endpoint: 'https://app.tracekit.dev/v1/traces',
// Optional: Enable/disable tracing (default: true)
enabled: process.env.NODE_ENV !== 'development',
// Optional: Sample rate 0.0-1.0 (default: 1.0 = 100%)
sampleRate: 0.5, // Trace 50% of requests
// Optional: Enable live code debugging (default: false)
enableCodeMonitoring: true, // Enable breakpoints and snapshots
// Optional: Map hostnames to service names for service graph
serviceNameMappings: {
'localhost:8082': 'payment-service',
'localhost:8083': 'user-service',
},
});import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TracekitModule } from '@tracekit/node-apm/nestjs';
@Module({
imports: [
ConfigModule.forRoot(),
TracekitModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
apiKey: config.get('TRACEKIT_API_KEY')!,
serviceName: config.get('APP_NAME', 'my-app'),
enabled: config.get('NODE_ENV') !== 'development',
enableCodeMonitoring: config.get('TRACEKIT_CODE_MONITORING_ENABLED', false),
}),
}),
],
})
export class AppModule {}TraceKit automatically instruments outgoing HTTP calls to create service dependency graphs. This enables you to see which services talk to each other in your distributed system.
When your service makes an HTTP request to another service:
- ✅ TraceKit creates a CLIENT span for the outgoing request
- ✅ Trace context is automatically injected into request headers (
traceparent) - ✅ The receiving service creates a SERVER span linked to your CLIENT span
- ✅ TraceKit maps the dependency: YourService → TargetService
TraceKit automatically instruments these HTTP libraries:
- ✅
http/https(Node.js built-in modules) - ✅
fetch(Node 18+ native fetch API) - ✅
axios(works via http module) - ✅
node-fetch(works via http module) - ✅
got,superagent, etc. (work via http module)
Zero configuration required! Just make HTTP calls as normal:
import axios from 'axios';
import fetch from 'node-fetch';
// All of these automatically create CLIENT spans:
await fetch('http://payment-service/charge');
await axios.get('http://inventory-service/check');
http.get('http://user-service/profile/123', callback);TraceKit intelligently extracts service names from URLs:
| URL | Extracted Service Name |
|---|---|
http://payment-service:3000 |
payment-service |
http://payment.internal |
payment |
http://payment.svc.cluster.local |
payment |
https://api.example.com |
api.example.com |
This works seamlessly with:
- Kubernetes service names
- Internal DNS names
- Docker Compose service names
- External APIs
For local development or when service names can't be inferred from hostnames, use serviceNameMappings:
tracekit.init({
apiKey: process.env.TRACEKIT_API_KEY,
serviceName: 'my-service',
// Map localhost URLs to actual service names
serviceNameMappings: {
'localhost:8082': 'payment-service',
'localhost:8083': 'user-service',
'localhost:8084': 'inventory-service',
'localhost:5001': 'analytics-service',
},
});
// Now requests to localhost:8082 will show as "payment-service" in the service graph
const response = await fetch('http://localhost:8082/charge');
// -> Creates CLIENT span with peer.service = "payment-service"This is especially useful when:
- Running microservices locally on different ports
- Using Docker Compose with localhost networking
- Testing distributed tracing in development
Visit your TraceKit dashboard to see:
- Service Map: Visual graph showing which services call which
- Service List: Table of all services with health metrics
- Service Detail: Deep dive on individual services with upstream/downstream dependencies
If you need to disable automatic HTTP client instrumentation:
tracekit.init({
apiKey: process.env.TRACEKIT_API_KEY,
autoInstrumentHttpClient: false, // Disable auto-instrumentation
});Every HTTP request to your service is automatically traced with:
- Route path and HTTP method
- Request URL and query parameters
- HTTP status code
- Request duration
- User agent and client IP
- Controller and handler names (NestJS)
Every HTTP request from your service is automatically traced with:
- Target URL and HTTP method
- HTTP status code
- Request duration
peer.serviceattribute for service dependency mapping
All database operations are automatically traced with zero configuration:
PostgreSQL (pg library):
- SQL query statements
- Query parameters
- Database name
- Response time
MySQL (mysql/mysql2 libraries):
- SQL query statements
- Query parameters
- Database name
- Response time
MongoDB:
- Collection operations (find, insert, update, delete)
- Query filters
- Database and collection names
- Response time
Redis:
- Commands (GET, SET, HGET, etc.)
- Keys accessed
- Response time
Example trace hierarchy:
GET /users/:id (kind: Server)
├─ SELECT * FROM users WHERE id = $1 (kind: Client, db.system: postgresql)
├─ GET user:123:cache (kind: Client, db.system: redis)
└─ INSERT INTO audit_logs... (kind: Client, db.system: postgresql)
All exceptions are automatically captured with:
- Exception type and message
- Full stack trace
- Request context
- Handler information
import { getClient } from '@tracekit/node-apm';
app.get('/custom', async (req, res) => {
const client = getClient();
const span = client.startSpan('my-operation', null, {
'user.id': req.user?.id,
'custom.attribute': 'value',
});
try {
const result = await doSomething();
client.endSpan(span, {
'result.count': result.length,
});
res.json(result);
} catch (error) {
client.recordException(span, error as Error);
client.endSpan(span, {}, 'ERROR');
throw error;
}
});import { Injectable, Inject } from '@nestjs/common';
import { TracekitClient } from '@tracekit/node-apm/nestjs';
@Injectable()
export class MyService {
constructor(
@Inject('TRACEKIT_CLIENT') private tracekit: TracekitClient
) {}
async doSomething() {
const span = this.tracekit.startSpan('custom-operation', null, {
'operation.type': 'database',
});
try {
const result = await this.database.query();
this.tracekit.endSpan(span, {
'rows.count': result.length,
});
return result;
} catch (error) {
this.tracekit.recordException(span, error as Error);
this.tracekit.endSpan(span, {}, 'ERROR');
throw error;
}
}
}tracekit.init({
apiKey: process.env.TRACEKIT_API_KEY!,
enabled: process.env.NODE_ENV === 'production',
});tracekit.init({
apiKey: process.env.TRACEKIT_API_KEY!,
sampleRate: 0.1, // Trace 10% of requests
});TraceKit APM is designed to have minimal performance impact:
- < 5% overhead on average request time
- Asynchronous trace sending (doesn't block responses)
- Automatic batching and compression
- Configurable sampling for high-traffic apps
Full TypeScript support with type definitions included:
import { TracekitClient, TracekitConfig, Span } from '@tracekit/node-apm';
const config: TracekitConfig = {
apiKey: 'your-key',
serviceName: 'my-app',
};
const attributes: Record<string, any> = {
'user.id': 123,
'request.path': '/api/users',
};
// Using the client
const client = new TracekitClient(config);
const span: Span = client.startSpan('my-operation', null, attributes);- Node.js 16.x or higher
- Express 4.x or 5.x (for Express support)
- NestJS 10.x (for NestJS support)
const express = require('express');
const tracekit = require('@tracekit/node-apm');
const app = express();
tracekit.init({
apiKey: process.env.TRACEKIT_API_KEY,
serviceName: 'express-example',
});
app.use(tracekit.middleware());
app.get('/users', async (req, res) => {
const users = await db.getUsers();
res.json(users);
});
app.listen(3000);// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
// app.module.ts
import { Module } from '@nestjs/common';
import { TracekitModule } from '@tracekit/node-apm/nestjs';
import { UsersModule } from './users/users.module';
@Module({
imports: [
TracekitModule.forRoot({
apiKey: process.env.TRACEKIT_API_KEY!,
serviceName: 'nestjs-example',
}),
UsersModule,
],
})
export class AppModule {}
// users.controller.ts
import { Controller, Get } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private usersService: UsersService) {}
@Get()
findAll() {
return this.usersService.findAll();
}
}- Documentation: https://app.tracekit.dev/docs
- Issues: https://github.com/Tracekit-Dev/node-apm/issues
- Email: support@tracekit.dev
MIT License. See LICENSE for details.
Built with ❤️ by the TraceKit team.