mrkeyoor.com_
Thu 06 Aug 23:53 UTC
npmInfraupdated 06 Aug 2026

dd-trace

dd-trace is Datadog's official APM tracer for Node.js. You load it before the rest of your application and it patches supported libraries at require time (http, express, pg, redis, kafkajs, and dozens more), so every request produces a distributed trace without per-library code. Spans are sent to a Datadog Agent process running next to your app on port 8126, and the Agent forwards them to Datadog's backend. The same package also carries most of Datadog's other Node.js products behind config flags: runtime metrics, the continuous profiler, log-trace correlation, AppSec and IAST, dynamic instrumentation, and LLM observability. It is not a general-purpose tracing library: spans go only to the Datadog Agent, and the Agent needs an API key from a Datadog account, so without a subscription the data has nowhere to go.

Verdict

If your company pays for Datadog, this is the correct, actively maintained way to get Node.js traces into it, and the preload and ESM flags are the only real friction. If you are not a Datadog customer it does nothing for you, and even if you are, OpenTelemetry with Datadog's OTLP intake is the more portable long-term bet.

API stability4/5init, trace, startSpan, and use have kept the same shape across several majors. New majors are driven by Node.js EOL rather than API rewrites (v6 on 2026-07-02 mainly raised the floor to Node 22), and each old line gets a documented year of maintenance, but that also means a forced major bump roughly every 18 months.
Docs4/5Setup, every env var, ESM, and bundling are covered on docs.datadoghq.com, plus a typedoc API reference at datadog.github.io/dd-trace-js. The content is good but split across the README, the docs site, and the API reference, and search engines frequently land you on pages for older release lines.
Maintenance5/5Pushed 2026-08-06, releases about weekly, and two lines maintained in parallel (6.9.0 and 5.120.0 released the same day) with published EOL dates per line. 251 open issues counting PRs is modest for a tracer that instruments dozens of libraries.
Ecosystem4/5About 9.7M weekly downloads and integrations for most of the Node.js server ecosystem, but it is a single-vendor project: 831 GitHub stars, direction set entirely by Datadog, and the community is by definition limited to Datadog customers.

Use it if

  • Your company already pays for Datadog and you want Node.js services in the same APM view as your other services, with the least instrumentation code
  • You want auto-instrumentation of a long list of frameworks and clients from a single preload flag instead of wiring each library by hand
  • You need the extras in one dependency: runtime metrics, continuous profiler, log injection, and AppSec all ship inside this package behind flags
  • You run polyglot services and need distributed traces to cross language boundaries, which Datadog's tracers handle by propagating context in headers automatically
Skip it if

Setup reality

npm install is the easy part; loading order is the real setup. The tracer must load before every module it instruments, and the reliable way is NODE_OPTIONS='--require dd-trace/init', because in transpiled TypeScript your imports hoist above a tracer.init() call in the same file and you silently lose instrumentation. ESM apps need a different flag entirely, node --import dd-trace/initialize.mjs, and picking the wrong ESM entry point (register.js registers hooks but never calls init) is a common way to get zero traces with zero errors. Nothing appears until a Datadog Agent is reachable on port 8126, so local dev and CI need the agent container, and runtime metrics additionally need DogStatsD on port 8125. Release cadence is roughly weekly across two maintained lines (6.9.0 and 5.120.0 both shipped on 2026-08-05), so expect frequent dependabot noise.

Patterns

Load the tracer before everything with --requireinit-preload-cjs

# CommonJS apps: preload so nothing can beat it
NODE_OPTIONS='--require dd-trace/init' node server.js

# equivalent direct form
node --require dd-trace/init server.js

dd-trace patches modules at require time, so it must load before express, pg, and friends. dd-trace/init calls init() for you, configured purely from DD_* env vars. If a library loads first you simply get no spans for it, with no error telling you why.

Initialize in code without the import-order trapinit-programmatic

// tracer.js
const tracer = require('dd-trace').init({
  service: 'checkout-api',
  env: 'production',
  version: '1.4.2'
})
module.exports = tracer

// server.js, very first line:
require('./tracer')
const express = require('express')

In TypeScript and ESM, import statements hoist above any code in the same file, so calling init() at the top of your entry file still runs after your other imports have loaded. Keep init in its own module imported first, or skip the whole problem with --require.

Instrument an ESM app with --importesm-import

node --import dd-trace/initialize.mjs server.mjs

# or via env, useful in Docker CMD
NODE_OPTIONS='--import dd-trace/initialize.mjs' node server.mjs

ESM needs loader hooks, and initialize.mjs both registers them and calls init(). dd-trace/register.js registers the hooks but never initializes the tracer, and the legacy --loader dd-trace/loader-hook.mjs form also exists, so the wrong entry point gives you a silent no-op.

Set service, env, and version the Datadog wayunified-service-tagging

DD_SERVICE=checkout-api \
DD_ENV=production \
DD_VERSION=1.4.2 \
NODE_OPTIONS='--require dd-trace/init' node server.js

These three tags are how Datadog joins traces, logs, metrics, and deploy tracking for one service. Prefer env vars so the same build can be promoted between environments; if you set both, programmatic init options win over the env vars.

Create a custom span around a functioncustom-span-trace

const tracer = require('dd-trace')

async function chargeCard (order) {
  return tracer.trace('charge.card', { resource: order.gateway }, async (span) => {
    span.setTag('order.id', order.id)
    return gateway.charge(order)
  })
}

trace() activates the span for everything inside the function, finishes it when the returned promise settles, and tags the error on rejection. This is the API you want most of the time; startSpan is for when start and finish happen in different places.

startSpan when begin and end live in different placesmanual-span-activate

const span = tracer.startSpan('queue.process', {
  tags: { 'queue.name': 'emails' }
})

try {
  await tracer.scope().activate(span, () => handle(job))
} catch (err) {
  span.setTag('error', err)
  throw err
} finally {
  span.finish()
}

startSpan does not activate the span: auto-instrumented calls and child spans only nest under it inside scope().activate(). Nothing finishes it for you either, and an unfinished span holds its whole trace back from being reported.

Tag the active span from anywhereadd-span-tags

const span = tracer.scope().active()
if (span) {
  span.setTag('customer.plan', user.plan)
  span.setTag('cart.size', cart.items.length)
}

// tags on every span, no code:
// DD_TAGS=team:payments,region:eu-west-1

active() returns null outside any span (startup code, detached timers), so always guard it. Use DD_TAGS for tags that every span from the process should carry instead of repeating setTag calls.

Turn off noisy or unwanted integrationsdisable-integrations

const tracer = require('dd-trace').init()
tracer.use('dns', false)
tracer.use('net', false)

// or without touching code:
// DD_TRACE_DISABLED_INSTRUMENTATIONS=dns,net

dns and net spans mostly duplicate what the http spans already tell you. Be careful disabling integrations that carry context between services, like http or messaging clients: you also lose distributed trace propagation through them.

Correlate logs with traceslog-injection-correlation

const tracer = require('dd-trace').init({
  logInjection: true  // or DD_LOGS_INJECTION=true
})

const pino = require('pino')
const logger = pino()
logger.info('charging card')
// record now includes dd.trace_id, dd.span_id, dd.service, dd.env, dd.version

Off by default, and it only works with loggers the tracer instruments (pino, winston, bunyan), which therefore must also load after the tracer. The correlation only pays off if the logs are shipped to Datadog too.

Enable runtime metrics and the continuous profilerruntime-metrics-profiling

const tracer = require('dd-trace').init({
  runtimeMetrics: true,  // DD_RUNTIME_METRICS_ENABLED=true
  profiling: true        // DD_PROFILING_ENABLED=true
})

Runtime metrics (event loop, GC, heap) leave over DogStatsD on port 8125, a separate port from the 8126 trace intake, so the agent must have it open. Profiling relies on the optional @datadog/pprof native module and shows up in Continuous Profiler, which Datadog bills separately from APM.

Control sample rate and drop noisy endpointssampling-config

const tracer = require('dd-trace').init({
  sampleRate: 0.2,  // DD_TRACE_SAMPLE_RATE
  samplingRules: [
    { sampleRate: 1, name: 'charge.card' },
    { sampleRate: 0, resource: 'GET /healthz' }
  ],
  rateLimit: 100    // DD_TRACE_RATE_LIMIT
})

Rules match on service, name, resource, and tags; a sampleRate of 0 drops matches, which is the standard way to keep health checks out of your ingested volume. With no sampling config at all, the rate is deferred to the agent.

Bundle with esbuild without losing instrumentationesbuild-bundle

// build.js
const ddPlugin = require('dd-trace/esbuild')

require('esbuild').build({
  entryPoints: ['server.js'],
  bundle: true,
  platform: 'node',
  outfile: 'dist/server.js',
  plugins: [ddPlugin],
  external: ['@datadog/native-metrics', '@datadog/pprof', '@datadog/native-appsec']
})

Bundlers inline requires, which defeats the tracer's module patching, so the plugin is mandatory, not an optimization. The @datadog/* native binaries cannot be bundled and must stay external and present in node_modules at runtime. v6 also ships a dd-trace/webpack plugin for webpack builds.

Alternatives

PackageRegistryPick it when
@opentelemetry/sdk-nodenpmYou want vendor-neutral auto-instrumentation; Datadog accepts OTLP, so you can keep the backend and drop the lock-in
newrelicnpmYour org is on New Relic instead: same all-in-one vendor agent model, different backend
elastic-apm-nodenpmYou run the Elastic stack and want traces in Kibana rather than a separate SaaS bill
prom-clientnpmYou only need metrics and dashboards, not distributed tracing, and want to stay off paid APM entirely