Worked review 1: a Node checkout service
The feature
Section titled “The feature”The prompt was: “Add Datadog APM and log correlation to our Express checkout service. It uses pino. Service is checkout-api, environment production, send traces to the Datadog Agent.” What follows is the first draft an assistant returns for that prompt. It starts, it serves requests, it prints JSON logs — and nothing in it throws at startup. Every finding below was produced by running it.
How it was proven, and against what:
dd-trace6.17.0 (the harness pin intools/probe/versions.json),express5.2.1,pino10.3.1,typescript7.0.2, Node 24.13.0 on this Mac.- A fake trace-agent: a 20-line Node HTTP server that accepts
PUT /v0.4/traces, decodes the msgpack body, and prints one line per span. The service under review is pointed at it, so nothing is ever sent to Datadog and no API key exists anywhere. - The
tools/verify-snippets.mjs--optionsmode, which checks everytracer.init({...})key and everyDD_*name against the pinned.d.tsand the merged env-var reference.
The generated code
Section titled “The generated code”// @expect-error as generated -- every finding below comes from running this fileconst express = require('express');const pino = require('pino');const tracer = require('dd-trace').init({ service: 'checkout-api', env: 'Production', version: '1.0', log_injection: true, analytics: true, agentUrl: process.env.TRACE_URL || 'http://127.0.0.1:28126', traceSampleRate: 0.5, flushInterval: 200,});
const logger = pino({ base: { service: 'checkout', env: 'prod' } });const app = express();
app.get('/health', (req, res) => res.send('ok'));
app.post('/charge', async (req, res) => { const span = tracer.scope().active(); try { logger.info('charging card'); throw new Error('card declined'); } catch (err) { span.setTag('error', true); logger.error({ msg: 'charge failed' }); res.status(200).json({ ok: false }); }});
app.listen(process.env.PORT || 3100, () => logger.info('listening'));And the Deployment fragment that came with it, so the container “sends traces to the Agent”:
# @expect-error DD_LOG_INJECTION is not a dd-trace variable; DD_AGENT_HOST=localhost is the pod itself, not the node Agentenv: - name: DD_LOG_INJECTION value: "true" - name: DD_AGENT_HOST value: localhostThe review walk
Section titled “The review walk”dd-trace initialised after express and pino were already loaded
Section titled “dd-trace initialised after express and pino were already loaded”The first two lines require the framework and the logger; only then does init() run. Run as generated, with the fake agent listening and two requests made (GET /health, POST /charge):
$ curl -s -XPOST localhost:3100/chargeTypeError: Cannot read properties of null (reading 'setTag') at .../bad/server.js:26:10 at Layer.handleRequest (.../node_modules/router/lib/layer.js:152:17)
fake agent: (no spans received at all)
{"level":30,"time":1790736292381,"service":"checkout","env":"prod","msg":"charging card"}Three failures from one cause. express was loaded before the tracer hooked require, so it was never instrumented: there is no express.request span, tracer.scope().active() is null (hence the TypeError on the first real request), the fake agent received nothing, and the pino line has no dd object — no trace id, so no log-to-trace link. The same code with only the two require lines moved below init() printed, for the same two requests:
DATADOG TRACER INTEGRATIONS LOADED - ["fs","net","dns","http","router","express","pino"]{"level":30,...,"msg":"charging card","dd":{"trace_id":"6abc77b500000000453cf38eaa9ba509","span_id":"2662234485403838737",...}}The docs say the same thing for the logger half: in docs.datadoghq.com/tracing/other_telemetry/connect_logs_and_traces/nodejs/ the snippet is commented “This line must come before importing the logger.”
🐛 Spot the bug
const express = require('express');const pino = require('pino');const tracer = require('dd-trace').init({ ... });Show the answer
dd-trace patches modules as they are required, so it has to be initialised before anything it should instrument — the web framework, the HTTP client, the database driver and the logger. AI assistants put init() “after the imports” because that is where configuration usually goes. Two later findings below were invisible in this run until this one was fixed: with no active span, none of the tag or log problems could even execute. Fix: init() on the first line, or preload with node --require dd-trace/init app.js.
log_injection — a snake_case option that does nothing
Section titled “log_injection — a snake_case option that does nothing”The next block asks for log_injection: true. tsc --strict against the pinned typings, one key at a time:
$ tsc --noEmit --strict opts.tsopts.ts(6,3): error TS2561: Object literal may only specify known properties, but 'log_injection' does not exist in type 'TracerOptions'. Did you mean to write 'logInjection'?Plain JavaScript accepts the unknown key silently. This one is dangerous because it appears to work: the tracer default is on, so logs are injected anyway. Prove it with the opposite value — log_injection: false still leaves "log_injection_enabled":true in the startup config dump (the same result the bug catalog entry 9 recorded). The moment someone tries to turn it off, or the default changes, the line is a lie.
🐛 Spot the bug
log_injection: true,Show the answer
The real option is logInjection (TracerOptions.logInjection, index.d.ts line 551 of the installed package). In a JavaScript file nothing rejects the wrong spelling; TypeScript and the harness --options mode both do. Fix: logInjection: true, or omit it entirely because it already defaults to true, or set DD_LOGS_INJECTION.
agentUrl — traces go to 127.0.0.1:8126, not where the code says
Section titled “agentUrl — traces go to 127.0.0.1:8126, not where the code says”The service was told to send to http://127.0.0.1:28126. Two fake agents ran side by side, one on 28126 and one on the default 8126:
agent A (28126, the URL in code): 0 spansagent B (default 8126): 6 spansThe tracer’s own startup line agrees: "agent_url":"http://127.0.0.1:8126/". agentUrl is not a dd-trace option (tsc: TS2353 ... 'agentUrl' does not exist in type 'TracerOptions'); the real key is url, or the environment variable DD_TRACE_AGENT_URL. Locally this works by coincidence, because 8126 is where a developer’s Agent listens. In a cluster, spans would flow to the pod’s own localhost:8126 and vanish.
🐛 Spot the bug
agentUrl: process.env.TRACE_URL || 'http://127.0.0.1:28126',Show the answer
An invented option is a bug in the config, but the outage happens in the network: the spans are sent, somewhere else, with no error. Fix: delete the line and set DD_TRACE_AGENT_URL (or DD_AGENT_HOST) in the environment — see bug 11 for what value that should be in Kubernetes.
analytics and traceSampleRate — options that never existed
Section titled “analytics and traceSampleRate — options that never existed”Same family, different consequence. tsc says TS2353 for analytics and TS2561 ... Did you mean to write 'sampleRate'? for traceSampleRate. The harness --options reports all of them at once, which tsc does not (it stops at the first excess property):
ERROR src/content/docs/en/x/bad.mdx:fence #2: tracer.init({...}) unknown key(s) [log_injection, agentUrl, traceSampleRate, analytics] ([email protected] TracerOptions)The behavioural proof is a count. 40 requests through a tracer initialised with the invented key vs the real sampleRate: 0.5, reading _sampling_priority_v1 from the captured payloads:
{traceSampleRate: 0.5, analytics: true} -> {priority 1: 40}{sampleRate: 0.5} -> {priority 2: 15, priority -1: 25}The invented key kept 100 percent of the traces; the real one produced a mix of “keep” (2) and “reject” (-1). Note that the rejected traces were still sent in this run to the fake agent with priority -1 — the tracer marks them, and the Agent and the ingestion controls decide what is dropped (docs.datadoghq.com/tracing/trace_pipeline/ingestion_mechanisms/). Whether a 50 percent head sample is a good idea is a separate question the trace sampling pipeline covers; the point here is that the line the author believed was cutting volume was doing nothing.
🐛 Spot the bug
analytics: true,traceSampleRate: 0.5,Show the answer
Neither key exists in TracerOptions. analytics is not in the pinned typings at all, and traceSampleRate is a plausible neighbour of sampleRate (the compiler suggests exactly that). Fix: delete analytics; use sampleRate or, better, DD_TRACE_SAMPLE_RATE / sampling rules, and only if the requirement really is head sampling.
env and version hard-coded — the Kubernetes env vars lose
Section titled “env and version hard-coded — the Kubernetes env vars lose”The service is deployed with DD_ENV=prod and DD_VERSION=2.4.1 (from the pod labels, as unified service tagging prescribes). The code says env: 'Production', version: '1.0'. With those environment variables set, the tracer’s config dump and the spans both took the code values:
"env":"Production", "tags":{"service":"checkout-api","env":"Production","version":"1.0", ...}express.request POST /charge env=Production version=1.0Options passed to init() take precedence over DD_* variables — proven here, not assumed. So the deploy pipeline’s DD_ENV=prod and DD_VERSION=2.4.1 are silently discarded, every deploy reports version 1.0, and Production is a different env value from the prod used by every other service (the tracer forwards it verbatim; how the platform then normalises tags is a Datadog-side rule — see docs.datadoghq.com/getting_started/tagging/unified_service_tagging/).
🐛 Spot the bug
service: 'checkout-api',env: 'Production',version: '1.0',Show the answer
Unified service tagging wants env, service and version set once, outside the code, so the same image runs in staging and prod and the deploy tooling owns the version. Putting them in init() freezes them into the image and overrides the environment. Fix: require('dd-trace').init() with no identity options, and DD_ENV, DD_SERVICE, DD_VERSION from the pod (bug 10).
The logger’s own service and env contradict the injected ones
Section titled “The logger’s own service and env contradict the injected ones”pino({ base: { service: 'checkout', env: 'prod' } }) is the assistant’s attempt at “unified tagging in the logs”. Once injection works, one log line carries two answers:
{"level":30,...,"service":"checkout","env":"prod","msg":"charging card", "dd":{"trace_id":"6abc77b5...","service":"checkout-api","version":"1.0","env":"Production"}}checkout vs checkout-api, prod vs Production. Datadog treats a JSON log’s top-level service as the log’s service and also recognises dd.service (docs.datadoghq.com/logs/log_configuration/pipelines/, “Service attribute” lists service, syslog.appname, dd.service). The docs I read do not say which wins when both are present in one line, so the honest statement is: the outcome depends on the org’s pipeline, and no offline tool can tell you. The review can still see that two different values were emitted by one process.
🐛 Spot the bug
const logger = pino({ base: { service: 'checkout', env: 'prod' } });Show the answer
The injected dd block already carries service, env and version from the tracer, and DD_SERVICE/DD_ENV drive both. Hard-coding a second copy in the logger guarantees they drift. Fix: pino() with no base identity fields — one source of truth (the environment), read by the tracer, which stamps the logs.
setTag('error', true) — on the wrong span, with no message
Section titled “setTag('error', true) — on the wrong span, with no message”With init fixed, the handler ran. What the fake agent received for POST /charge:
express.request POST /charge error=0 http.status_code=200router.middleware handle error=0router.middleware <anonymous> error=1tracer.scope().active() inside a route handler is not the request’s root span; it is the innermost span — here the Express router.middleware layer. The error:1 landed there. The root express.request span, the one APM’s service error rate and the service page are computed from, has error=0. And because the value was the boolean true, there is no error.message, error.type or error.stack on any span. In the fixed run, the same failure produces:
express.request POST /charge error=1 http.status_code=502payment.charge payment.charge error=1 error.type=Error error.message=card declined🐛 Spot the bug
const span = tracer.scope().active();...span.setTag('error', true);Show the answer
Two mistakes in three lines. active() returns whichever span is current, not the root. And setTag('error', true) flags a span but records no cause; passing the Error object (setTag('error', err)) records error.message, error.type and error.stack. The clean idiom is to wrap the work in your own span with tracer.trace('payment.charge', fn) — an exception thrown (or a promise rejected) inside fn marks that span as an error, with the message, automatically.
A failed charge answered with 200 OK
Section titled “A failed charge answered with 200 OK”res.status(200).json({ ok: false }). The express.request span above shows http.status_code=200, error=0. dd-trace’s HTTP server plugin decides “error” by status: validateStatus defaults to code => code < 500 (index.d.ts, HttpServer.validateStatus). A declined card returned as 200 is a success as far as every APM monitor, the service error rate and any availability SLO are concerned. With 502, the same run marks the root span error=1.
🐛 Spot the bug
res.status(200).json({ ok: false });Show the answer
That is an application design choice made invisible to observability. Return a status that matches the outcome (a genuine server-side or upstream failure is 5xx; a client-caused decline may legitimately be 4xx, which the default does not count as an error — set validateStatus if your definition of an error differs). Do not choose the status code to satisfy a dashboard, but do not hide a failure from one either.
/health is traced on every probe
Section titled “/health is traced on every probe”The first fake-agent listing above contains express.request GET /health — a full trace, plus two router.middleware spans, for each liveness and readiness probe. One probe every 10 seconds is 8,640 probe traces per pod per day of pure noise, and it dilutes latency and error statistics for the service. With tracer.use('http', { server: { blocklist: ['/health'] } }) (Http.blocklist in the typings; type-checks with tsc), the same two requests produced only the /charge trace: the /health request did not appear in the fake agent at all.
🐛 Spot the bug
app.get('/health', (req, res) => res.send('ok'));Show the answer
Nothing is wrong with the route; the bug is what is missing next to it. Probe traffic should be excluded from tracing with the http plugin’s blocklist (or filter), placed after init() and before the server starts. The ingestion-side alternative — a sampling rule that drops the resource — works too but still costs the tracer work; the docs for both are under docs.datadoghq.com/tracing/.
DD_LOG_INJECTION — a variable that does not exist
Section titled “DD_LOG_INJECTION — a variable that does not exist”In the Deployment, injection was “enabled” with DD_LOG_INJECTION=true. The harness, on a copy of that fence:
ERROR src/content/docs/en/x/bad.mdx:fence #1: unknown env var DD_LOG_INJECTIONand the runtime proof, same code, only the variable differs, reading the config dump and one log line:
DD_LOG_INJECTION=false -> "log_injection_enabled":true {"msg":"hi","dd":{"service":"x","version":"1.0.0"}}DD_LOGS_INJECTION=false -> "log_injection_enabled":false {"msg":"hi"}The real name is DD_LOGS_INJECTION (plural). The unknown one is ignored, which is the same “silently accepted” failure as the snake_case option, one layer further out.
🐛 Spot the bug
env:- name: DD_LOG_INJECTION value: "true"Show the answer
Environment variable names are matched literally and unknown ones are not reported by the tracer. The pinned harness checks every DD_* name in a collected fence against dd-trace’s own supported-configuration list. Fix: DD_LOGS_INJECTION — or delete it, since the Node tracer’s default is already on.
DD_AGENT_HOST=localhost — the pod, not the node Agent
Section titled “DD_AGENT_HOST=localhost — the pod, not the node Agent”localhost inside a pod is that pod. The tracer honours it exactly as written:
DD_AGENT_HOST=localhost -> "agent_url":"http://localhost:8126/"DD_AGENT_HOST=10.20.30.40 -> "agent_url":"http://10.20.30.40:8126/"In Kubernetes the Datadog Agent normally runs as a DaemonSet, so the address is the node’s IP. From the docs (docs.datadoghq.com/containers/kubernetes/apm/): “The application container needs the DD_AGENT_HOST environment variable that points to status.hostIP”, with a fieldRef on status.hostIP, or use the Admission Controller. Whether the node Agent accepts the traffic is a separate Agent-side setting (hostPort or the host-network / Unix socket option) — that part is the subject of the next worked review.
🐛 Spot the bug
- name: DD_AGENT_HOSTvalue: localhostShow the answer
localhost works on a laptop with a local Agent, which is exactly why an assistant writes it. In a pod it points at a port nothing listens on, and the traces are dropped without any error the application can see. Fix: valueFrom.fieldRef.fieldPath: status.hostIP.
The corrected code
Section titled “The corrected code”const tracer = require('dd-trace').init({ logInjection: true });tracer.use('http', { server: { blocklist: ['/health'] } });
const express = require('express');const pino = require('pino');
const logger = pino();const app = express();
app.get('/health', (req, res) => res.send('ok'));
app.post('/charge', async (req, res) => { try { await tracer.trace('payment.charge', async () => { logger.info('charging card'); throw new Error('card declined'); }); } catch (err) { logger.error({ err }, 'charge failed'); res.status(502).json({ ok: false, error: 'payment_failed' }); // choose the status that matches what happened }});
app.listen(process.env.PORT || 3100, () => logger.info('listening'));apiVersion: apps/v1kind: Deploymentmetadata: name: checkout-api labels: tags.datadoghq.com/env: prod tags.datadoghq.com/service: checkout-api tags.datadoghq.com/version: 2.4.1spec: selector: matchLabels: app: checkout-api template: metadata: labels: app: checkout-api tags.datadoghq.com/env: prod tags.datadoghq.com/service: checkout-api tags.datadoghq.com/version: 2.4.1 spec: containers: - name: checkout-api image: registry.example.com/checkout-api:2.4.1 env: - name: DD_ENV valueFrom: fieldRef: fieldPath: metadata.labels['tags.datadoghq.com/env'] - name: DD_SERVICE valueFrom: fieldRef: fieldPath: metadata.labels['tags.datadoghq.com/service'] - name: DD_VERSION valueFrom: fieldRef: fieldPath: metadata.labels['tags.datadoghq.com/version'] - name: DD_AGENT_HOST valueFrom: fieldRef: fieldPath: status.hostIP - name: DD_LOGS_INJECTION value: "true"Run for real (fake agent on the default port, DD_ENV=prod DD_SERVICE=checkout-api DD_VERSION=2.4.1, one GET /health and one POST /charge). The fake agent printed exactly these four spans and no /health trace:
express.request POST /charge error=1 env=prod version=2.4.1 http.status_code=502router.middleware handle error=0 env=prod version=2.4.1router.middleware <anonymous> error=0 env=prod version=2.4.1payment.charge payment.charge error=1 env=prod version=2.4.1 error.type=Error error.message=card declinedand the log lines now carry a single, consistent identity:
{"level":30,...,"msg":"charging card","dd":{"trace_id":"6abc77ea0000000033867be986a3a994","span_id":"598578184621079437","service":"checkout-api","version":"2.4.1","env":"prod"}}{"level":50,...,"err":{"type":"Error","message":"card declined",...},"msg":"charge failed","dd":{...same trace_id...}}The YAML was not applied to a cluster (no kind cluster exists for the course harness; --k8s mode reports and exits clean). What was checked is every DD_* name in it, by the harness --options mode.
What changed and why
Section titled “What changed and why”| # | Bug | Fix | Proven by |
|---|---|---|---|
| 1 | init() after express/pino are loaded | init() on line 1 | no spans, null span, no dd in logs vs all three present |
| 2 | log_injection (snake_case) | logInjection or omit | tsc TS2561; config dump |
| 3 | agentUrl invented | DD_TRACE_AGENT_URL / DD_AGENT_HOST | agent on 28126 got 0 spans, 8126 got 6 |
| 4 | analytics, traceSampleRate invented | delete; real sampleRate only if intended | 40 of 40 priority 1 vs 15/25 |
| 5 | env/version hard-coded in init() | identity from DD_ENV/DD_SERVICE/DD_VERSION | spans show Production/1.0 despite DD_ENV=prod |
| 6 | service/env also hard-coded in the pino base | one source; injected dd block | two different values in one line |
| 7 | setTag('error', true) on scope().active() | tracer.trace(...) wrapping the work | error=1 on router.middleware, 0 on root |
| 8 | Failure answered with 200 | status that matches the outcome | root span error=0, status 200 vs error=1, 502 |
| 9 | /health traced | tracer.use('http', { server: { blocklist } }) | probe trace present vs absent |
| 10 | DD_LOG_INJECTION | DD_LOGS_INJECTION (or omit) | harness “unknown env var”; config dump |
| 11 | DD_AGENT_HOST=localhost in a pod | fieldRef: status.hostIP | config dump; docs URL |
What the reviewer writes in the PR
Section titled “What the reviewer writes in the PR”Not approvable as-is — 11 things, in the order they bite:
init()must be the first line. Everythingrequired before it is un-instrumented: today there are no Express spans,scope().active()isnull(the first/chargethrows) and the logs have no trace id.- Four invented options (
log_injection,agentUrl,analytics,traceSampleRate) — none exist inTracerOptions; runtscornpm run verify.agentUrlin particular means traces go to:8126, not where the code says.- Do not put
env,versionorservicein code, or in the pinobase. They override the pod’sDD_*values (proven) and disagree with the injectedddblock. One source: the environment.- Error handling:
setTag('error', true)marks a router layer, not the root, with no message. Wrap intracer.trace(...). And a declined charge must not be200.- Block
/healthin thehttpplugin.- Deployment:
DD_LOG_INJECTIONis not a variable (DD_LOGS_INJECTIONis, and it is the default anyway), andDD_AGENT_HOSTmust come fromstatus.hostIP, notlocalhost.
Mental Model
Section titled “Mental Model”flowchart TD A["require order: express + pino first"] -->|"1: nothing instrumented"| B["tracer.init options"] B -->|"2, 3, 4: invented keys ignored"| C["tracer config"] B -->|"5: code beats DD_ENV / DD_VERSION"| C C -->|"3, 11: wrong agent address"| D["Agent"] E["pino base service + env"] -->|"6: contradicts dd block"| F["log line"] G["route handler"] -->|"7: error on inner span"| H["spans"] G -->|"8: 200 on failure"| H I["/health probe"] -->|"9: traced"| H J["pod env"] -->|"10: unknown variable"| C
Four of the eleven bugs (2, 3, 4, 10) are names nobody checks: JavaScript and the tracer accept any key, and the environment accepts any variable. tsc and the harness --options mode close that gap for free. Bug 1 is an ordering error no option checker sees. The other six (5, 6, 7, 8, 9, 11) type-check and start; only a captured payload, a config dump or a log line shows them. That is the reason this review used a fake agent and not just a linter — and why bug 1 goes first: it hid the rest.
Trade-offs
Section titled “Trade-offs”- Config in code vs environment. Code-level
init({ service, env, version })is easy to read and wrong for a multi-environment image; the environment is invisible in the source but is the only thing the deploy tooling controls. Choose the environment for identity; keep code options for behaviour (logInjection, plugin settings). blocklistvs an ingestion rule. Blocking/healthin the tracer removes the work and the data at the source; a Datadog-side sampling rule can be changed without a deploy but the spans are still created and sent.- Fake agent vs real Agent. A local receiver shows exactly what the tracer sent and nothing about what Datadog does with it. Normalisation, ingestion sampling and pipeline behaviour stay docs-only claims in this review, labelled as such.
Common Misconceptions
Section titled “Common Misconceptions”- “If
init()accepted my options object, the options are valid.” Proven above: all four invented keys changed nothing, with no warning at any log level. Onlytsc, the harness or the config dump tell you. - “
tracer.scope().active()is the request span.” Proven above: inside an Express route it is the innermostrouter.middlewarespan; tagging it left the root aterror=0. - “Setting
envin code and alsoDD_ENVon the pod is belt and braces.” Proven above: the code value wins, silently. Two sources mean the wrong one wins. - “An HTTP 200 with
ok: falseis fine as long as the log sayserror.” Every APM error-rate monitor is computed from spans, not from log text.