@tracepath/node
v0.4.0
Published
Node.js SDK for Tracepath
Maintainers
Readme
Tracepath
Tracing SDKs for Node.js and browsers, with an optional local terminal dashboard and an OTLP/HTTP JSON exporter.
Version 0.4.0 adds opt-in mysql2 promise wrappers, per-trace query summaries, separate connection-acquisition/query-call measurements, and local JSON trace export. This viewer is for local development; production SDK deployments require validation with your own Collector/backend and workload.
Packages and requirements
| Package | Purpose | | --- | --- | | @tracepath/core | Spans, sampling, propagation, processors, OTLP export | | @tracepath/node | AsyncLocalStorage context, HTTP/HTTPS instrumentation, Express middleware | | @tracepath/browser | Fetch instrumentation, LCP/TTFB measurements | | @tracepath/cli | Local development dashboard and HTTP relay |
Node.js 22.13 or later is required for Node/CLI and repository tooling. Browsers need Fetch, Headers, AbortController, Web Crypto, and Performance APIs. Browser builds are ESM; core and Node provide ESM and CommonJS with matching declarations.
Node.js
Install with npm install @tracepath/node. Initialize before loading libraries that capture HTTP methods. For ESM applications, use a bootstrap module and dynamic import:
// bootstrap.mjs
import { init } from '@tracepath/node'
init({
service: 'orders-api',
environment: 'production',
dashboard: false,
otlpUrl: 'http://127.0.0.1:4318/v1/traces',
sampleRate: 0.1,
})
await import('./server.mjs')// server.mjs
import express from 'express'
import { getTracer, tracepathMiddleware, shutdown } from '@tracepath/node'
const app = express()
app.use(tracepathMiddleware(getTracer()))
app.get('/orders/:id', async (req, res) => {
const order = await getTracer().startActiveSpan('load-order', async span => {
span.setAttribute('db.operation', 'SELECT')
return { id: req.params.id }
})
res.json(order)
})
const server = app.listen(3000)
let stopping = false
async function stop() {
if (stopping) return
stopping = true
const deadline = setTimeout(() => process.exit(1), 15000)
try {
await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()))
await shutdown()
clearTimeout(deadline)
} catch (error) { console.error(error); process.exitCode = 1; clearTimeout(deadline) }
}
process.once('SIGTERM', stop)
process.once('SIGINT', stop)The application owns signal handling and draining incoming requests. The SDK does not install process signal handlers. init() is idempotent: the first configuration wins until shutdown() completes. forceFlush() and shutdown() reject when export fails, so callers can detect losses.
HTTP/HTTPS request and get are instrumented, preserving request options. Native Node fetch/Undici, HTTP/2, PostgreSQL, ORM internals, and libraries that captured methods before initialization are not auto-instrumented. Use manual spans for these operations. Express middleware records route templates, finishes on response finish/close, and marks server failures or interrupted responses as errors.
MySQL query tracing
Install mysql2 alongside @tracepath/node. After initializing the SDK, wrap your promise pool once and use the returned object:
import mysql from 'mysql2/promise'
import { getTracer, instrumentMysql2Pool } from '@tracepath/node'
const nativePool = mysql.createPool({
host: '127.0.0.1', user: process.env.DB_USER,
password: process.env.DB_PASSWORD, database: process.env.DB_NAME,
connectionLimit: 5,
})
const pool = instrumentMysql2Pool(nativePool, getTracer(), {
captureQueryShape: true, // Opt in after reviewing SQL identifiers for sensitive data.
})
const [rows] = await pool.execute('SELECT id FROM users WHERE id = ?', [42])
// On application shutdown, drain requests/background jobs, then:
await pool.end()The wrapper records query/execute duration, error status and row/affected-row counts, while preserving results and bound parameters sent to the database. Pool query/execute acquire and release a connection automatically; getConnection() returns a traced connection for transactions, which the application must release. instrumentMysql2Connection(connection, tracer, options) supports an existing promise connection. Callback APIs, streams, PoolCluster and automatic ORM integration are not supported. Original pool references remain uninstrumented.
db.client.connection.wait_ms measures acquisition including connection creation/handshake. db.client.query.duration_ms measures the driver query call including network and server waits. It is not pure SQL execution time. Explicitly acquired connections have a separate mysql.acquire span; query spans correctly show acquisition as unmeasured, rather than zero.
Query shape capture is off by default. When enabled, SQL is parsed and normalized inside the SDK before export. Bound values are never captured. String/numeric literals are replaced with placeholders and ordinary comments removed. Unsupported or ambiguous grammar, multi-statements, quoted identifiers, executable comments/hints, and backslash-escaped strings omit the query shape; timing still works. SQL identifiers remain visible, so this is not personal-data detection. Names/case variants are not guaranteed to group. Raw database error messages are never copied by the wrapper; opt-in exception capture elsewhere in the application remains the caller's responsibility.
The dashboard groups captured query shapes within each retained trace, showing call count, errors, cumulative duration, acquisition/query duration and measurement coverage, plus p50/p95 operation duration. Cumulative time can exceed request wall time when operations overlap. Queries with omitted shapes are never merged into a misleading common group.
Browser
import { init, shutdown } from '@tracepath/browser'
init({
service: 'web',
devMode: false,
otlpUrl: '/v1/traces',
sampleRate: 0.1,
propagateToOrigins: ['https://api.example.com'],
})
await fetch('/api/orders')
// On application teardown: await shutdown()Use a same-origin ingestion proxy with server-side credentials, validation, quotas, and access controls. Never embed a backend ingestion secret in a browser bundle. Trace headers are injected only for same-origin URLs and exact origins explicitly listed in propagateToOrigins; no-cors requests are excluded. Cross-origin APIs must allow the traceparent header in their CORS configuration.
Request headers, Request objects, cancellation signals, credentials, bodies, and method overrides are preserved. Telemetry export bypasses the fetch wrapper. ignoreFetchUrls accepts strings or regular expressions.
Portable/browser active context is synchronous. It does not promise automatic context across await. For async manual work, pass explicit context or supply a compatible async contextManager:
import { ROOT_CONTEXT, setSpanContext } from '@tracepath/core'
const root = tracer.startSpan('interaction')
try {
const context = setSpanContext(ROOT_CONTEXT, root.getSpanContext())
await doWork()
const child = tracer.startSpan('render', { context })
child.end()
} finally { root.end() }LCP and TTFB are supported where their browser APIs are available. LCP is finalized on the first interaction, page hiding, or shutdown. INP, CLS, FCP, and back-forward-cache lifecycle tracking are not implemented. Hidden-page flushing uses keepalive fetch but browser termination, network failures, and browser keepalive byte quotas can still drop telemetry; delivery is best effort.
Local dashboard
npx @tracepath/cli dashboard --port 4317The dashboard binds only to 127.0.0.1. Configure the Node SDK with dashboard: true and dashboardUrl: 'ws://127.0.0.1:4317'. It is a development viewer, not a remotely exposed production collector.
Press j/k to select a trace, 1 for spans, 2 for errors, 3 for query groups, and e to export the selected trace. --export-dir <directory> controls the local export directory (default tracepath-traces). Exports use versioned JSON, exclusive filenames and restrictive creation permissions where supported. Review custom telemetry before sharing. Exports are live retained snapshots with completeness: "unknown", truncation and eviction metadata; absence of a span does not demonstrate idle time. No HTTP endpoint exposes trace exports.
Retention is limited to 50 traces, 200 spans per trace and an 8 MiB serialized-span budget. Heap overhead, the bounded error view and transient network buffers are additional. The viewer is not a durable store. Export before data is evicted. Optional bearer authentication is recommended on shared development machines.
The CLI accepts POST /__tracepath/spans on the same port with JSON { spans: [...] }. Proxy this path through your development server for browser relay mode. The example Express server includes this proxy outside production. Browser relay mode is enabled with devMode: true and relayUrl: '/__tracepath/spans'.
The server rejects unapproved Origin headers, validates spans, limits messages to 256 KiB, batches to 128 spans, WebSocket clients to 16, and ingestion to 1,000 spans/messages per second. Optional bearer authentication uses TRACEPATH_DASHBOARD_TOKEN on the CLI and dashboardToken on the Node SDK. Proxy credentials belong on the server. Remote hosting is not supported by the CLI.
Data handling and delivery
- Built-in HTTP spans use generic method names or Express route templates. Client paths have queries/fragments removed; browser spans do not capture URLs. IP addresses and request/response bodies are not automatically recorded.
- Attribute keys matching authorization, cookie, password, secret, token, or API key are removed. Default limits are 128 attributes, 1,024 characters per string, 128 values per array, 128 events, and 128 links per span.
- Exception type is recorded by default. Messages and stacks require privacy: { captureExceptionDetails: true }. Custom span names, attribute values, and explicitly supplied status messages remain your responsibility; do not put personal data or secrets in them.
- privacy.maxAttributes, maxAttributeLength, and maxEvents configure span limits. batch configures maxQueueSize (512), maxExportBatchSize (128), scheduledDelayMillis (5,000), and exportTimeoutMillis (30,000).
- Processors serialize exports and bound queues. Failed batches are dropped and counted; this SDK has no durable queue or retry guarantee. A timed-out exporter halts its processor to bound outstanding work. Processor diagnostics exposes queuedSpans, droppedSpans, failedExports, and halted. Reinitialize after shutdown to recover a halted processor.
- Production deployments should export to an OpenTelemetry Collector that handles durable buffering, retries, authentication, and backend routing. OTLP resource and instrumentation-scope metadata, events, links, and status messages are preserved. Collector partial rejection is reported as an export failure.
- Root sampling uses a generated trace ID; child spans follow the parent decision. Unsampled spans still propagate valid IDs with the sampled flag cleared.
Development and publishing
pnpm install --frozen-lockfile
pnpm check
pnpm release:checkcheck builds all packages, runs TypeScript checks, ESLint, and regression/integration tests. release:check also audits dependencies and creates npm tarballs, rejects local/workspace dependencies, checks their manifests and exports, installs them with npm into an isolated consumer without overrides, and verifies ESM/CommonJS imports, declarations, CLI execution, and lifecycle behavior. Generated artifacts live in .release/.
Version 0.3.1 fixes npm installation, terminal shutdown, and multiline exception ingestion. Version 0.3.0 introduced lifecycle, browser propagation, and privacy changes. See CHANGELOG.md and RELEASE.md. The old fix-publish.ps1 script is disabled. Public package dependencies use ordinary semver (for example ^0.3.1), so npm packs valid manifests without workspace conversion. The release command publishes only hash-verified tarballs. Direct directory publishing is disabled.
For real database verification run pnpm test:db with TRACEPATH_MYSQL_PORT, TRACEPATH_MYSQL_USER and TRACEPATH_MYSQL_PASSWORD pointing at a test database. release:check additionally exercises the exact installed tarballs against that database when TRACEPATH_MYSQL_PORT is set. CI includes Ubuntu/Windows with Node 22/24 and a Linux MySQL/MariaDB job; configured workflows are not evidence of a successful run. See RELEASE.md for the outstanding platform gates and examples/mysql-app for a runnable Express/manual/background demo.
