v6.4.0 proposal - #9368
Conversation
…ndor-minor-and-patch-dependencies group across 1 directory (#9325) Bumps the vendor-minor-and-patch-dependencies group with 1 update in the /vendor directory: [protobufjs](https://github.com/protobufjs/protobuf.js). Updates `protobufjs` from 8.6.5 to 8.7.0 - [Release notes](https://github.com/protobufjs/protobuf.js/releases) - [Changelog](https://github.com/protobufjs/protobuf.js/blob/master/CHANGELOG.md) - [Commits](protobufjs/protobuf.js@protobufjs-v8.6.5...protobufjs-v8.7.0) --- updated-dependencies: - dependency-name: protobufjs dependency-version: 8.7.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: vendor-minor-and-patch-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
If the OIDC token exchange fails due to a transient infrastructure issue, jobs were failing before tests even ran. With continue-on-error, ci/init.js gracefully skips reporting when DD_API_KEY is absent and the tests still execute normally. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* perf(graphql): reuse filtered variables across resolve spans
The graphql.resolve variable tags ran the user-supplied config.variables
filter once for every instrumented field that carries arguments, even though
graphql coerces variableValues once per execute and hands the same object to
every resolver. Memoize the filtered result against the last-seen
variableValues object so the filter runs once per operation.
The cache is a single { key, value } slot compared by identity, not a WeakMap.
graphql hands every resolver in one execute the same variableValues object, so
the common case is a bare `===` hit with no per-operation allocation. A nested
execute() sharing the same object contextValue reuses the outer rootCtx but
carries its own variableValues; the identity compare recomputes for it and the
later inner fields reuse the slot, so each field's tags stay correct. A
regression test covers that nested case.
Microbenchmark (Node v26.3.1, V8 14.6, n=2M x 7 trials, drop best+worst),
per-operation cost of rootCtx setup + N arg-bearing field filters:
1 field, 1 object: baseline 53 ns weakmap 73 ns (+37%) slot 54 ns (+2%)
3 fields, 1 object: baseline 163 ns weakmap 82 ns (-49%) slot 56 ns (-65%)
10 fields,1 object: baseline 519 ns weakmap 113 ns(-78%) slot 61 ns (-88%)
3 fields, 2 objects: baseline 160 ns weakmap 147 ns(-8%) slot 114 ns(-29%)
10 fields,2 objects: baseline 506 ns weakmap 177 ns(-65%) slot 124 ns(-75%)
A WeakMap regresses the single-field common case (its allocation costs more
than one filter call); the identity slot wins in every scenario.
* test(graphql): exercise the memoized variables filter across sibling fields
A single operation selecting two argument-bearing fields is the only shape that
reaches the cached return in #filterVariables; every prior variables test picks
one field and only ever hits the miss path. Assert both sibling resolve spans
draw their own tags from the one shared filtered object and that the filter runs
once for the resolve path rather than per field.
…health-check (#9134) * fix(graphql): trace resolvers reached through unions and interfaces The resolver-wrapping traversal only descended through lists and non-nulls, so it never reached object types referenced solely as union members or interface implementations. graphql resolves the concrete type at runtime, so an Apollo Federation subgraph (whose entity types are reachable only through the synthetic `_Entity` union returned by `_entities`) produced no graphql.resolve spans for those fields. Descend into union members via getTypes() and interface implementations via schema.getPossibleTypes(), keyed off the realm-agnostic `Symbol.toStringTag` discriminator so an interface without an explicit resolveType is still covered. patchedTypes keeps the walk one-time per type, so cyclic abstract types terminate and there is no per-resolve cost. Fixes: #1057 * fix(graphql): skip the Apollo Federation health-check operation Apollo Gateway's polling subgraph health check sends a fixed `query __ApolloServiceHealthCheck__ { __typename }` on every poll interval, burying real traffic under health-check noise. Skip that named operation by exact name before span creation. The skip is unconditional because the name is spec-reserved (the `__` prefix) and the operation carries no user input, so there is nothing for AppSec or IAST to inspect and no legitimate operation to collide with. The gateway's other probe, the anonymous startup `{ __typename }` query, is left alone on purpose: it fires only on startup and schema update, so it is not a source of recurring noise. Fixes: #1057 * fix(graphql): harden federation health-check skip and interface walk 1. The health-check skip matched the reserved operation name alone. Since operation names are client-controlled, a request naming itself `__ApolloServiceHealthCheck__` while selecting a real field (or using a mutation, or adding selections beside `__typename`) was silently dropped from tracing and from the AppSec/IAST resolver channels. Confirm the gateway's exact spec-fixed shape (`query __ApolloServiceHealthCheck__ { __typename }`) before skipping so the reserved name can't become a tracing/security bypass. 2. `getPossibleTypes` is schema-specific, but the interface-implementation descent shared the global `patchedTypes` guard. Two schemas reusing one interface instance meant the first schema's walk marked the interface and the second schema's implementations were never wrapped, so their resolvers went untraced. Key the interface descent per (schema, interface) instead. * fix(graphql): wrap resolvers per schema when types are shared The resolver-wrapping walk terminated on a global per-type guard, but the walk is schema-dependent: union members and interface implementations are read through the schema (getTypes / getPossibleTypes) and differ between schemas. Two schemas sharing any type on the path to an abstract type — a reused root Query, a shared interface — meant the first schema's walk marked that type, and the second schema's walk returned at it before reaching its own members or implementations, leaving those resolvers untraced. Key the walk's visited set per (schema, type) so each schema runs its own full traversal. Resolver wrapping is already idempotent via patchedResolvers, so re-walking a type shared across schemas never double-wraps a resolver. Fixes: #1057 * fix(graphql): skip parse and validate spans for the health-check The health-check skip only suppressed the execute and resolver spans, so a gateway poll still emitted graphql.parse and graphql.validate. graphql runs both before execute, so every cold poll (and every poll on servers without a document cache, e.g. graphql-yoga) left two heartbeat spans a customer's operation filter would flag. Detect the poll from the raw query string at parse — the only input parse has — and mark the produced document so validate and execute skip it too. Apollo Server caches parsed+validated documents in its documentStore and reuses the same document object across polls, so a warm poll reaches execute with the already-marked document; when a health-check document reaches execute without ever passing through the instrumented parse (pre-populated cache, hand-built document), the operation-shape check remains the backstop. Fixes: #1057 * fix(graphql): require exact Apollo health-check documents Health-check operation names and parsed documents are caller-controlled. Requiring Apollo's complete constant shape keeps transformed or multi-operation documents, and operations with variables or directives, on the traced AppSec and IAST path.
Cypress full titles omit the spec path, so equal titles in separate files shared retry history and produced incorrect final status.
This prevents coverage, logs, or telemetry payloads from being lost when traces flush before their IPC sends complete.
* chore(deps-dev): update sonarjs for TypeScript 7 SonarJS 4.1 reads compiler enums removed in TypeScript 7 and crashes before ESLint can run. * test(vitest): use the sandboxed TypeScript compiler Vitest 3 resolves a bare tsc command from the inherited npm PATH, which made sandboxed typechecks use the repository compiler instead of their pinned dependency.
* refactor(express): trace middleware via layer prototype dispatch Middleware tracing replaced `layer.handle` with a wrapper and re-exposed the user handler through an enumerable `_datadog_orig` back-reference, the only property loopback's `_findLayerByHandler` could follow to tag a layer with its phase. That ties the tracer to loopback's private reflection heuristic, where the property name and its enumerability are both load-bearing. Wrap the host `Layer` prototype dispatch (`handle_request`/`handle_error`, and `handleRequest`/`handleError` on router >=2) and read per-layer metadata from a WeakMap, leaving `layer.handle` the user's function. loopback's first lookup (`layer.handle === handler`) then matches with no tracer-specific contract, so the back-reference, the `express-async-errors` `__handle` branch, and the `_name` write-back onto the user handler are no longer needed. A synchronous throw or a rejected promise becomes `next(error)` in the host dispatch, so the wrapped `next` captures both without a tracer-side try/catch. Refs: #9062 * fix(express): trace middleware on express <4.3.0 (no prototype dispatch) express <4.3.0 has no `Layer.prototype.handle_request`/`handle_error`; the router invokes `layer.handle` directly, so wrapping the prototype traced nothing and dropped middleware, route-handler, and code-origin spans there. Replace `layer.handle` in place (arity preserved so the host still routes error handlers) when the layer exposes no dispatch method. express 4.3.0+, express 5, the router package, and loopback keep `handle` pristine via the prototype wraps. Also point the router-helper spec at the renamed `setLayerMeta` helper. * test(router): cover express-async-errors on prototype-dispatch hosts express-async-errors redefines `handle` as a getter/setter that stores the wrapped handler in `__handle` and patches `handle` only, never `handle_request`. On express 4.3.0+ the tracer's prototype-dispatch wrap therefore has to survive that patch, the arity gate has to read the real 3-arg handler, and a rejected async handler has to reach the wrapped `next` as `next(error)`. Pin that path so a regression surfaces here instead of in a downstream tracer test. * fix(express): restore error publishing for throwing middleware on express <4.6.0 On express <4.6.0 the router has no `Layer` prototype dispatch: it runs `layer.handle` directly and, on a synchronous throw, catches outside the layer and calls its own `next(error)` — never the wrapped `next` the tracer installed. The prototype-dispatch rewrite dropped the legacy handle wrap's catch, so a throwing middleware or route handler no longer published `middleware:error` / `middleware:next` / `middleware:finish`: the span lost its error tag and lingered on the stack until request finish. Restore the catch in `wrapLegacyHandle` to publish error/next/finish and rethrow, mirroring `wrapNext`. The prototype hosts (express >=4.6.0, express 5, router) already route the throw through the wrapped next, so only the legacy path needs it. Also correct the version boundary in the surrounding comments: `handle_request` landed in express 4.6.0, not 4.3.0, so the legacy path covers `>=4.0.0 <4.6.0`. Refs: #9067 (comment) * fix(express): publish middleware next/finish once per continuation A handler that calls `next()` and then rejects (`async (req, res, next) => { next(); await bg() }` where `bg()` rejects) makes the host invoke the same wrapped `next` twice: the clean `next()` finishes and pops the middleware span, then the host's rejection pass calls `next(error)` again. Since the middleware stack is already empty, that second pass tagged the error on the parent span, inventing a request-level error the middleware never surfaced. Guard the wrapped continuation so its error/next/finish publishes fire at most once per dispatch; the second call still forwards to the host chain untouched. Add the missing JSDoc on `hasLayerDispatch`.
…ies (#8948) * chore(electron): add electron package exclusion config * chore(electron): add electron package generation script and initial package.electron.json Adds scripts/generate-electron-package.js to automatically generate a version of package.json with excluded optional dependencies for the dd-trace-electron package variant. Supports --check flag for CI validation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(electron): wire electron package verify into lint * chore(electron): add electron publish script * docs(electron): document Dependabot behavior for package.electron.json * chore(electron): move generation to publish time, add release jobs, gitignore generated file * feat(electron): add Electron entrypoint and make OpenFeature self-registering Adds Tracer.registerFeature() / NoopProxy.registerNoop() so subsystems can opt into the tracer without being hardcoded in proxy.js. OpenFeature uses this via a new register.js that self-registers on require. The Electron entrypoint (index.electron.js) simply omits that require, so OpenFeature and its native dep (@datadog/openfeature-node-server) are absent from the Electron build with no conditional logic in the core. index.js delegates to index.electron.js after calling register.js, keeping both entrypoints in sync with zero duplication. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(proxy): adapt spec to OpenFeature self-registration OpenFeature is no longer hardcoded in proxy.js so the test now calls registerFeature() explicitly with the mocked modules instead of passing them through proxyquire. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(electron): extract bootstrap into shared module Both index.js and index.electron.js now require ./src/bootstrap directly rather than one depending on the other. index.js remains the only entrypoint that registers OpenFeature. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(electron): use src/index.js in bootstrap to respect DD_TRACE_ENABLED bootstrap.js was requiring ./proxy directly, bypassing src/index.js which handles DD_TRACE_ENABLED=false and Jest worker environments by returning the noop proxy instead. This caused global._ddtrace to always be the full Tracer in contexts where it should be the noop. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(plugins): evict src/bootstrap from require cache on tracer rebuild The agent test helper clears specific module cache entries before rebuilding the tracer. src/bootstrap.js was not in the eviction list, so after global._ddtrace was deleted the cached bootstrap module still exported the stale (deleted) proxy instance instead of re-running initialization. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: add CODEOWNERS entries for electron entrypoints and release workflow Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(electron): fix abort.integration telemetry missing with OpenFeature Requiring openfeature/register.js from index.js previously loaded proxy.js as a side-effect (to call Tracer.registerFeature), which transitively loaded datadog-instrumentations/register.js. That module registers logAbortedIntegrations with beforeExitHandlers, but at that point the global symbol was not yet set up by bootstrap.js, so the optional-chaining silently no-oped and the handler was never registered. Introduce a feature-registry module that openfeature/register.js pushes to directly without requiring proxy.js. proxy.js and noop/proxy.js read from it at class-definition time (after the global symbol is set up by bootstrap.js). This keeps all initialization gated on !global._ddtrace. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: add CODEOWNERS entries for bootstrap and feature-registry Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ci(electron): refactor release jobs for electron package Fix broken `needs: publish-latest` reference by renaming to `publish-electron` with `needs: publish`. Drop the v5.x branch guard since the job already only runs when publish succeeds (which itself is gated to release branch pushes via setup). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(electron): address Codex review findings - Pass computed npm dist-tag to electron publish so maintenance branches don't erroneously move the latest tag - Make electron publish idempotent: skip if the version is already on the registry, matching the behaviour of the regular publish step - Exclude oxc-parser from the electron package (native bindings) - Restore @DataDog/libdatadog exclusion; crashtracking is not supported in Electron — wrap its start() in a local try/catch so a missing libdatadog doesn't abort tracer initialization Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
undici 8.7.0 (nodejs/undici#5116) changed the default so a plain HTTP request through an HTTP proxy forwards an absolute-form request instead of tunneling via CONNECT. The ProxyAgent CONNECT-span regression test then sees no CONNECT request at all and fails asserting a finished CONNECT span. Passing proxyTunnel: true restores the tunnel; the option is a no-op on undici < 6.22.0, where CONNECT was always used. Refs: nodejs/undici#5116
Bumps the cloud-and-messaging group with 1 update in the /packages/dd-trace/test/plugins/versions directory: [bullmq](https://github.com/taskforcesh/bullmq). Updates `bullmq` from 5.80.1 to 5.80.2 - [Release notes](https://github.com/taskforcesh/bullmq/releases) - [Commits](taskforcesh/bullmq@v5.80.1...v5.80.2) --- updated-dependencies: - dependency-name: bullmq dependency-version: 5.80.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: cloud-and-messaging ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…9183) * fix(aws-sdk): record kinesis DSM checkpoint only when context ships When a Kinesis record exceeds the 1 MiB cap, injectToMessage bailed after setDSMCheckpoint had already run: it recorded a produce checkpoint, advanced the pathway context, and tagged pathway.hash on the span, yet params.Data was never written so nothing shipped on the wire. The producer left a dangling pathway edge no consumer completes and a span tagged with a hash for a message it never sent. Gate on the trace-context payload before setDSMCheckpoint so a record we cannot ship records no checkpoint, advances no pathway, and tags no hash. The bounded dd-pathway-ctx-base64 bytes are added after the gate; they cannot realistically tip a record that already cleared 1 MiB with room for them. EventBridge shares this checkpoint-before-gate pattern and is tracked separately in #8476; this change is Kinesis-only. * refactor(aws-sdk): track message injection with a boolean, drop isEmpty The kinesis, sqs, and sns producers decided whether to attach `_datadog` by re-scanning the freshly built `ddInfo` carrier with an `isEmpty` for-in helper. A local `injected` flag set at each population site carries the same fact without the per-send rescan or the shared helper, and matches `DsmPathwayCodec.encode`'s own `!dataStreamsContext.hash` guard on the sns path so an empty pathway context no longer counts as injected. Replace the faked kinesis inject-to-message spec — an `Object.create(Kinesis.prototype)` instance with a stubbed tracer — with a real agent-loaded regression in kinesis.dsm.spec.js: a record that only clears the 1 MiB cap before the trace context is attached must record no produce checkpoint, asserted through the `putRecord` span carrying no `pathway.hash`. Drop the sqs unit case that asserted a carrier state (`injectTraceContext` true, `inject` a no-op) no real caller reaches. * refactor(core): return whether inject wrote anything into the carrier `Tracer.inject` and every propagator now report whether they wrote any context, so the AWS SDK messaging producers stop attaching an empty `_datadog` message attribute when nothing was injected. Previously they assumed a write happened whenever injection was attempted, which shipped an empty carrier under `DD_TRACE_PROPAGATION_STYLE_INJECT=none` and whenever a disabled-APM-tracing (standalone ASM) trace was stripped after injection. The standalone ASM strip runs on the `dd-trace:span:inject` channel after the propagator writes, so the text_map propagator re-checks the `_dd.p.ts` trace tag when APM tracing is disabled to keep the returned value truthful without rescanning the carrier. * test: align inject test doubles with the boolean carrier-write return `Tracer.inject` now reports whether it wrote context into the carrier, and the AWS SDK producers read that instead of rescanning with `isEmpty`. Four expectations still modelled the old `undefined` return and failed in CI: 1. sqs-inject-to-message: stubs mutate the carrier but returned `undefined`, so the producer dropped `_datadog`; return `true` from the writing stubs and `false` from the no-op default. 2. dd-trace-api: `inject` forwards its return through the public API, now `false` for the empty test args; assert `false`. 3. pathway: type the no-hash `encode` argument concretely to clear jsdoc/reject-any-type. 4. kinesis.dsm: `assert.match(resource, /^putRecord/)` clears eslint-prefer-assert-match. * fix(aws-sdk): reserve the DSM pathway field before the kinesis size gate The size gate ran on the trace-context payload before DsmPathwayCodec.encode appended the fixed-size dd-pathway-ctx-base64 field. A record sitting within 55 bytes of the 1 MiB cap cleared the gate, recorded a checkpoint, and then wrote an over-cap params.Data that Kinesis rejects, failing the user's putRecord. Reserve the pathway field's bytes in the gate so a record that only fits without it records no checkpoint and never ships over the cap. * fix(types): mirror inject boolean return in the v5 type surface Tracer.inject now reports whether it wrote context into the carrier, and index.d.ts was updated to return boolean. index.d.v5.ts still declared void. inject exists on v5 and this return is not v6-only, so the v5 release type swap (scripts/release/swap-v5-types.js) would ship a stale void declaration and hide the return value from v5 TypeScript users even though the runtime provides it. Mirror the signature and JSDoc so the two public surfaces diverge only by the intended v6 cleanups. Refs: #9183 (comment) * fix(types): keep public inject void on v5/v6, expose boolean on v7 The prior commit widened Tracer.inject's declared return from void to boolean across the public surface. That is a breaking TypeScript change: under strict mode `const x: void = tracer.inject(...)` and returning `tracer.inject(...)` from a void-typed function stop compiling. It must not ship on a shipping major. The runtime already returns the boolean and the internal AWS SDK producers consume it through the unwrapped DatadogTracer (a plugin's `this.tracer` resolves to `_tracer._tracer`, and the DSM checkpointer holds that inner tracer directly), never through the public proxy. So the boolean stays an internal signal regardless of what the public `.d.ts` promises. Freeze the contract per major: 1. index.d.v5.ts and the new index.d.v6.ts declare `inject(): void` — the current shipping majors keep their contract. 2. index.d.ts (master is 7.0.0-pre) declares `inject(): boolean` as the next-major public API, documented in MIGRATING.md under 6.0 to 7.0. Generalize the release type swap: swap-v5-types.js only handled v5, so a v6 release would publish the v7 index.d.ts. Rename it to swap-legacy-types.js and pick index.d.v<major>.ts by the release major; the newest major has no frozen file and is a no-op. Adding a future frozen surface is now a single file drop. The .v<major>.ts files stay out of the npm `files` array by design — the swap copies the chosen one over index.d.ts at pack time, so only index.d.ts ships. Refs: #9183 (comment) * fix(propagation): return lazily created carriers Standalone ASM can remove trace headers while leaving baggage in the carrier. A boolean result reported false for that non-empty carrier, causing AWS messaging producers to drop valid context. Return the carrier internally after its first write while keeping the public tracer API void at the proxy boundary. Kinesis limits cover Data plus PartitionKey. Include both before recording a DSM checkpoint and allow records at the exact default limit. No-write injection measured 11.96-12.21 ns eager vs 6.92-6.98 ns lazy; one write in eight measured 29.30-29.46 ns vs 26.09-26.66 ns on Node 24.18.0 / V8 13.6.233.17, 5M iterations x 7 trials with best and worst dropped.
* feat(http2): trace core-API HTTP/2 servers
A server using the raw stream API (`createServer().on('stream', ...)`) produced
no server span, because the instrumentation only hooked the compatibility
`request` event. This adapts the `Http2Stream` and its pseudo-header map into the
minimal req/res shape the shared web lifecycle consumes, so the core path now
gets the same span, context propagation, and header tagging as the compatibility
path.
A compatibility server emits both `request` and `stream` for every request, so
the `stream` branch only creates a span when the server has no `request`
listener; otherwise the request would be double-instrumented.
Fixes: #312
* fix(http2): default core-stream status to 200 when no response was sent
A core-API stream aborted before `stream.respond()` runs (client RST, server
`stream.close(code)`, a throwing handler) has an empty `stream.sentHeaders`, so
the adapter reported `res.statusCode` as `undefined`. `web.js` then ran
`validateStatus(undefined)`, which is falsy, tagged the span as an error, and
dropped the `http.status_code` tag — diverging from the compatibility path,
whose `Http2ServerResponse.statusCode` defaults to 200 for the same abort. The
adapter now falls back to 200 so both paths agree.
Drive-by fix:
* Pin the stream-adapter req/res shape with a `@typedef` naming the `web.js` /
`url.js` / `ip_extractor.js` fields it must satisfy, so a new read added there
fails the type check instead of silently resolving to `undefined` on the core
path only.
* Record at `bindEmit` that finish is single-sourced from the stream's one
`close` event, which is why the `!req.stream` idempotency-guard bypass in
`web.js` is harmless here.
Fixes: #312
* fix(http2): keep the server span active for mixed stream/request servers
A server that registers both a raw `stream` listener and a compatibility
`request` listener emits a single `stream` event whose internal listener
synthesizes the `request`. The previous gate created the span only on the
synthesized `request`, which fires nested in one stream listener, so the
application's own `stream` listener ran with no active span and any child
spans or handler duration it produced were lost.
1. Such mixed servers now create the span from the `stream` event, keeping
it active across the application's stream listener; the synthesized
`request` reuses it instead of creating a second span. A request-only
server still traces on `request` so the compatibility response keeps its
richer req/res, and the per-request cost stays a single `WeakSet.has`.
2. The `with configured headers` test reconfigured the plugin with a second
`agent.load` nested under the suite that already loaded it, starting a
second mock agent and leaking the first server's handle; it now reloads
in place so a single agent runs.
Drive-by fix:
* Correct the stream-adapter `@typedef`: `res.statusCode` is the numeric
`:status` pseudo-header, and `res.getHeader` can return a string array.
* fix(http2): hand mixed-server requests the real req/res via an adopt channel
A server with both a raw `stream` listener and a compatibility `request`
listener creates its span from the `stream` event using a throwaway adapter.
The synthesized `request` off the same stream then found no context, so a
user's `request` handler calling `web.setRoute`/`web.setFramework` never
reached the span and the finish `hooks.request` received the adapter instead
of the real `Http2ServerRequest`/`Http2ServerResponse`.
The instrumentation now publishes `apm:http2:server:request:adopt` with the
real req/res, and the plugin points the stream-backed context at them. The
join key is the shared `Http2Stream`: `web.linkContextToStream` keys the
context on the stream so the second request resolves to the first's span. That
key is written only for mixed servers (`ctx.adoptable`), so the common
single-listener request pays no extra per-request map write.
* fix(http2): do not trace gRPC-owned HTTP/2 servers
@grpc/grpc-js builds its transport on a core Node `Http2Server` and registers
a raw `stream` listener with no `request` listener. The re-enabled http2
server instrumentation traces exactly that shape, so every gRPC call gained a
`web.request` span on top of its `grpc.server` span, and because the stream
event fires before gRPC's handler, `web.request` became the top frame.
The gRPC instrumentation now marks the server it owns and the http2
instrumentation skips any marked server, so a gRPC call keeps a single span
with gRPC as the top frame. Suppression is unconditional: disabling gRPC
tracing does not resurrect these as `web.request` spans, since a gRPC call over
HTTP/2 has no meaningful HTTP request/response semantics (`:path` is
`/pkg.Svc/Method`, status is always 200 + trailers) and surfacing it as one
would surprise a user who turned gRPC off.
The mark is a module-local `Symbol` on the server instance, read once per emit
behind the existing `hasSubscribers` gate (~1.6 ns; a WeakSet lookup measured
~3.5x slower and needs a shared mutable container across the two
instrumentations). It is set from `_setupHandlers`, the single server-creation
funnel present across the supported range — the newer `createHttp2Server` does
not exist on older versions — and it runs at bind time, before any request
reaches the wrapped `emit`.
* test(grpc): connect non-gRPC probe over IPv4
Linux runners resolve localhost to ::1, but the gRPC fixture listens on IPv4, so the probe failed before reaching the server.
* test(http2): cover stream handler crash path
A synchronous throw from a core stream handler terminates the process, so exercise it in a forked fixture and let NYC collect the child coverage instead of excluding the error path.
…#9177) * test(agent): reject when no matching trace arrives before the timeout An `assertSomeTraces` / `expectSomeSpan` expectation whose span never arrives left the returned promise unsettled: the rejection timeout only fired when errors had accumulated, so with zero payloads the callback did nothing and the spec hung until Mocha's 5s per-test timeout killed it with a generic "Timeout of 5000ms exceeded" instead of failing at the intended 1s window with the real cause. The rejection timeout now always settles the promise: the accumulated first/aggregate error when a payload matched but assertions failed, and otherwise a fresh error naming the timeout window so a missing span points the reader at "no matching trace received within <timeoutMs>ms". * test(agent): add assertNoTraces and give slow spans room after timeout reject Rejecting the assertSomeTraces promise at its timeout broke tests that relied on the previous never-settling behaviour: 1. "Should not be traced" cases registered an expectation that must never match, left the promise unawaited, and passed via a separate setTimeout(done). The reject now fires into that path, calling done() twice or surfacing a stray "No matching trace". A new assertNoTraces resolves when nothing matches before the timeout and rejects only when a forbidden trace arrives; the negative sites move to it. 2. Positive tests whose span legitimately lands after 1s (restify <5 uncaught exceptions ~5s, the openai/anthropic dead-baseURL retry backoff, kafka consumer delivery, aws-sdk cold connect, the http Node-20 timeout cases) now set an explicit timeoutMs. openai/anthropic instead drop retries with maxRetries: 0 since the endpoint is dead on purpose. The LLMObs harness gets a longer window because every call is a real VCR round-trip. 3. mongodb-core's BigInt test returns done() early on the version that throws synchronously; the assertion now registers only once the command starts, so no expectation is left to reject afterwards. * test(agent): fail loudly on leaked trace expectations An assertSomeTraces expectation that is armed but never consumed can keep its rejection timer running after the test ends. The timer then rejects a promise nobody awaits a whole timeout window later, surfacing as an unhandled rejection on an unrelated test. 1. Each expectation promise carries a cancel() that disarms its timer and drops the handler; the consumer that armed it cancels it in its own teardown. 2. The framework reset() disarms every still-armed expectation, then throws to fail the just-finished test by name. Bare subscribe() handlers have no timer and are never counted. 3. The invalid-type SDK test no longer races getEvents() against a timeout, which abandoned a poll loop that kept draining span requests and starving later tests. * test(agent): drop settled handlers and fix the leaks the guard exposed This PR's teardown leak guard counts any handler left in the set as armed, but runCallbackAgainstTraces kept its handler on the timeout and rejectFirst settle paths, so the guard rejected expectations it had already settled. Drop the handler on every settle path. With the guard trustworthy, three specs leaked real expectations: 1. aws-sdk sqs: timeoutMs sat on `.catch()` rather than assertSomeTraces, so the suppressed-consumer expectation ran on the 1000ms default and stayed armed past the 250ms done(). 2. google-cloud-pubsub dsm and index: the DSM checkpoint expectations were never awaited. Register before publishing, then await, and read the expected pathway hash little-endian to match DataStreamsProcessor's readBigUInt64LE decode. * test(aws-sdk): make the disabling tests deterministic `should allow disabling a specific service` and its sqs consumer-span-kind sibling paired two fire-and-forget assertSomeTraces expectations on a 100ms window with a fixed setTimeout(done, 250) asserting a match counter. On a cold localstack the enabled request's span lands after the 100ms window, so its expectation times out, the counter stays 0, and the guard asserts 0 !== 1. Assert the disabled request's absence with assertNoTraces and await the enabled request's assertSomeTraces, which is what assertNoTraces documents in place of a bare assertSomeTraces plus a separate setTimeout(done, …). * test(claude-agent-sdk): give the agentic-call assertion room under the timeout This PR makes assertSomeTraces reject at its timeout instead of waiting out the mocha deadline. The full-agentic-call test registers the expectation, then spawns the claude subprocess and runs several steps before the trace flushes, which takes longer than the 1000ms default, so the expectation rejected before the trace ever arrived. Opt into a 10s timeoutMs and raise the block's mocha timeout to 15s.
Bumps the test-versions group with 1 update in the /packages/dd-trace/test/plugins/versions directory: [pnpm](https://github.com/pnpm/pnpm/tree/HEAD/pnpm11/pnpm). Updates `pnpm` from 11.11.0 to 11.12.0 - [Release notes](https://github.com/pnpm/pnpm/releases) - [Changelog](https://github.com/pnpm/pnpm/blob/main/pnpm11/pnpm/CHANGELOG.md) - [Commits](https://github.com/pnpm/pnpm/commits/v11.12.0/pnpm11/pnpm) --- updated-dependencies: - dependency-name: pnpm dependency-version: 11.12.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: test-versions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…with 1 update (#9367) Bumps the gh-actions-packages group with 1 update in the / directory: [slackapi/slack-github-action](https://github.com/slackapi/slack-github-action). Bumps the gh-actions-packages group with 1 update in the /.github/workflows directory: [slackapi/slack-github-action](https://github.com/slackapi/slack-github-action). Updates `slackapi/slack-github-action` from 3.0.3 to 3.0.4 - [Release notes](https://github.com/slackapi/slack-github-action/releases) - [Changelog](https://github.com/slackapi/slack-github-action/blob/main/CHANGELOG.md) - [Commits](slackapi/slack-github-action@45a88b9...fc46ded) Updates `slackapi/slack-github-action` from 3.0.3 to 3.0.4 - [Release notes](https://github.com/slackapi/slack-github-action/releases) - [Changelog](https://github.com/slackapi/slack-github-action/blob/main/CHANGELOG.md) - [Commits](slackapi/slack-github-action@45a88b9...fc46ded) --- updated-dependencies: - dependency-name: slackapi/slack-github-action dependency-version: 3.0.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gh-actions-packages - dependency-name: slackapi/slack-github-action dependency-version: 3.0.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gh-actions-packages ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Overall package sizeSelf size: 6.77 MB Dependency sizes| name | version | self size | total size | |------|---------|-----------|------------| | import-in-the-middle | 3.3.1 | 122.62 kB | 438.86 kB | | opentracing | 0.14.7 | 194.81 kB | 194.81 kB | | dc-polyfill | 0.1.11 | 25.74 kB | 25.74 kB |🤖 This report was automatically generated by heaviest-objects-in-the-universe |
🎉 All green!🧪 All tests passed 🔄 Datadog retried 1 test - 1 passed on retry 🎯 Code Coverage (details) 🔗 Commit SHA: 77b0d3b | Docs | Datadog PR Page | Give us feedback! |
Assertions that resolved on a valid prefix could miss duplicate or late payloads emitted before the child finished. Apply the existing post-exit drain collector to repeated-run, finalization, and retry contracts, and make its timing tests deterministic.
Disabling git upload left readiness unresolved, so settings requests waited for the 60-second fallback.
Falsy fallback logic replaced an explicit zero retry budget with defaults, scheduling retries the backend disabled.
Clamp short failed-test durations at zero and cover the regression through a real Vitest integration run. Co-authored-by: Ruben Bridgewater <ruben.bridgewater@datadoghq.com>
Node does not guarantee that Date.now() reports the full delay when a setTimeout callback runs, so the exact 200 ms lower bound occasionally observed 199 ms. Advancing fake timers across the 199/200 ms boundary pins the contract without depending on scheduler timing.
Apollo Gateway reaches the Mercurius request boundary before the graphql-js hooks, so each poll still emitted a graphql.request span after its nested spans were suppressed. Match the same exact fixed query at that boundary so health-check-shaped user operations remain traced.
) Remote configuration that disables and re-enables tracing could leave OpenFeature consumers bound to an orphaned provider and leave AI Guard integrations unsubscribed. Preserve lazily initialized feature modules across reconfiguration and re-enable their integrations when tracing resumes. Co-authored-by: Bo Stendal Sørensen <bo@stendal-sorensen.net> Co-authored-by: Sameeran Kunche <sameeran.kunche@datadoghq.com
* test(ai): cover compatible SDK provider version matrix * test(ai): use latest compatible ESM dependencies
Real applications load LoopBack through its public entry once during bootstrap. Keep that path in the compatibility test and give its eager initialization ten seconds without repeating the work for every case.
Runtime-gated declarations could leave the first active range as the unversioned target even when a newer compatible range was also active.
6b315e2 to
bd293ef
Compare
BenchmarksBenchmark execution time: 2026-07-16 11:27:54 Comparing candidate commit 77b0d3b in PR branch Found 0 performance improvements and 0 performance regressions! Performance is the same for 2314 metrics, 44 unstable metrics.
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## v6.x #9368 +/- ##
=======================================
Coverage ? 96.90%
=======================================
Files ? 923
Lines ? 123011
Branches ? 21338
=======================================
Hits ? 119202
Misses ? 3809
Partials ? 0 Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…rve OTel span private-field access (#9152) * fix(ai): replace Object.create(span) with delegating wrapper to preserve OTel span private-field access * fix(ai): replace Object.create(span) with delegating wrapper to preserve OTel span private-field access and chaining * test(ai): replace exported wrapTracer unit tests with E2E regression in index.spec.js * fix(ai): replace Object.create(span) with delegating wrapper to preserve OTel span private-field access * fix(ai): replace Object.create(span) with delegating wrapper to preserve OTel span private-field access and chaining * test(ai): replace exported wrapTracer unit tests with E2E regression in index.spec.js * test(ai): eslint ignore non used statusCode var
bd293ef to
77b0d3b
Compare
There was a problem hiding this comment.
More details
Validated 7 distinct risk areas across the release bundle. The DsmPathwayCodec.encode now returns its carrier (previously void), and the existing DSM callers in amqplib/bullmq/kafkajs/pubsub/rhea discard the return value — they remain correct via side-effects. All adversarial scenarios for kinesis size-gating, vitest negative duration, worker flush counter, EFD zero retry budget, Apollo health-check skipping, per-schema walkedTypes, and Jest suite-path fallback passed cleanly.
📊 Validated against 22 scenarios · Open Bits AI session
🤖 Datadog Autotest · Commit bd293ef · What is Autotest? · Any feedback? Reach out in #autotest
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bd293ef219
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| injectedCarrier = this | ||
| ._injectTraceparent(spanContext, injectedCarrier ?? carrier, injectTraceContext) ?? injectedCarrier | ||
|
|
||
| if (injectedCarrier === undefined) return |
There was a problem hiding this comment.
Publish inject subscribers even when built-ins add no carrier
When callers provide an existing carrier but the configured propagation styles do not write any built-in headers (for example DD_TRACE_PROPAGATION_STYLE_INJECT=none or baggage-only with no baggage), this early return prevents dd-trace:span:inject subscribers from running. LLMObs relies on that channel to append _dd.p.llmobs_* values to x-datadog-tags even when the carrier starts empty, so those contexts stop propagating in those configurations; consider publishing to subscribers with the provided carrier before deciding nothing was injected.
Useful? React with 👍 / 👎.
| @@ -0,0 +1,3 @@ | |||
| 'use strict' | |||
|
|
|||
| module.exports = require('./src/bootstrap') | |||
There was a problem hiding this comment.
Register feature noops before bootstrapping electron
This electron entry point bypasses packages/dd-trace/index.js, so src/openfeature/register never runs before bootstrap constructs the singleton. Because both NoopProxy and the real proxy copy entries from feature-registry only in their constructors, dd-trace-electron instances end up without the public openfeature noop/provider path even though the shared typings still expose tracer.openfeature; require the register module here (or from bootstrap) before constructing the proxy.
Useful? React with 👍 / 👎.
| let tags = existing || '' | ||
| if (parentId) tags += `${tags ? ',' : ''}${PROPAGATED_PARENT_ID_KEY}=${parentId}` | ||
| if (mlApp) tags += `${tags ? ',' : ''}${PROPAGATED_ML_APP_KEY}=${mlApp}` | ||
| if (sessionId) tags += `${tags ? ',' : ''}${PROPAGATED_SESSION_ID_KEY}=${sessionId}` |
There was a problem hiding this comment.
Validate session IDs before appending x-datadog-tags
When an LLMObs session ID contains a comma or non-ASCII character, this writes it directly into x-datadog-tags without the validation/length checks used by the normal trace-tag injector. Downstream extraction splits this header on commas and aborts extraction if any tag pair is invalid, so one user-supplied session ID can make the receiving service drop all propagated Datadog trace tags from the header; skip or sanitize invalid session IDs before appending them.
Useful? React with 👍 / 👎.
Features
Fixes
Performance
Internal (CI, Testing, Benchmarking)