dd-trace 5.114.0 → 5.116.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/README.electron.md +7 -0
  2. package/README.md +17 -0
  3. package/ci/diagnose.js +2100 -0
  4. package/ci/init.js +23 -2
  5. package/ci/runbook.md +198 -0
  6. package/ci/test-optimization-validation/approval-artifacts.js +143 -0
  7. package/ci/test-optimization-validation/approval.js +299 -0
  8. package/ci/test-optimization-validation/artifact-id.js +20 -0
  9. package/ci/test-optimization-validation/bounded-json.js +121 -0
  10. package/ci/test-optimization-validation/ci-command-candidate.js +83 -0
  11. package/ci/test-optimization-validation/ci-discovery.js +181 -0
  12. package/ci/test-optimization-validation/ci-remediation.js +210 -0
  13. package/ci/test-optimization-validation/cli.js +903 -0
  14. package/ci/test-optimization-validation/command-blocker.js +81 -0
  15. package/ci/test-optimization-validation/command-output-policy.js +206 -0
  16. package/ci/test-optimization-validation/command-runner.js +707 -0
  17. package/ci/test-optimization-validation/command-suitability.js +471 -0
  18. package/ci/test-optimization-validation/executable-approval.js +48 -0
  19. package/ci/test-optimization-validation/executable.js +480 -0
  20. package/ci/test-optimization-validation/generated-file-policy.js +63 -0
  21. package/ci/test-optimization-validation/generated-files.js +351 -0
  22. package/ci/test-optimization-validation/generated-verifier.js +196 -0
  23. package/ci/test-optimization-validation/init-probe-preload.js +163 -0
  24. package/ci/test-optimization-validation/init-probe.js +246 -0
  25. package/ci/test-optimization-validation/late-initialization.js +63 -0
  26. package/ci/test-optimization-validation/local-command.js +163 -0
  27. package/ci/test-optimization-validation/manifest-loader.js +133 -0
  28. package/ci/test-optimization-validation/manifest-scaffold.js +738 -0
  29. package/ci/test-optimization-validation/manifest-schema.js +862 -0
  30. package/ci/test-optimization-validation/offline-fixtures.js +327 -0
  31. package/ci/test-optimization-validation/offline-output.js +406 -0
  32. package/ci/test-optimization-validation/payload-normalizer.js +72 -0
  33. package/ci/test-optimization-validation/plan-writer.js +1120 -0
  34. package/ci/test-optimization-validation/preflight-runner.js +114 -0
  35. package/ci/test-optimization-validation/redaction.js +331 -0
  36. package/ci/test-optimization-validation/report-writer.js +1508 -0
  37. package/ci/test-optimization-validation/safe-files.js +203 -0
  38. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +159 -0
  39. package/ci/test-optimization-validation/scenarios/basic-reporting.js +657 -0
  40. package/ci/test-optimization-validation/scenarios/ci-wiring.js +780 -0
  41. package/ci/test-optimization-validation/scenarios/early-flake-detection.js +141 -0
  42. package/ci/test-optimization-validation/scenarios/helpers.js +539 -0
  43. package/ci/test-optimization-validation/scenarios/test-management.js +218 -0
  44. package/ci/test-optimization-validation/setup-runner.js +97 -0
  45. package/ci/test-optimization-validation/static-diagnosis.js +159 -0
  46. package/ci/test-optimization-validation/test-output.js +129 -0
  47. package/ci/test-optimization-validation-manifest.schema.json +1 -0
  48. package/ci/validate-test-optimization.js +3 -0
  49. package/ext/exporters.js +1 -0
  50. package/index.d.ts +121 -8
  51. package/index.electron.js +3 -0
  52. package/init.js +18 -0
  53. package/initialize.mjs +1 -1
  54. package/loader-hook.mjs +28 -18
  55. package/openfeature.d.ts +1 -0
  56. package/openfeature.js +4 -0
  57. package/package.json +15 -8
  58. package/packages/datadog-instrumentations/src/ai.js +37 -26
  59. package/packages/datadog-instrumentations/src/child_process.js +1 -1
  60. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
  61. package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +9 -3
  62. package/packages/datadog-instrumentations/src/cucumber.js +24 -10
  63. package/packages/datadog-instrumentations/src/cypress-config.js +398 -52
  64. package/packages/datadog-instrumentations/src/express.js +20 -2
  65. package/packages/datadog-instrumentations/src/fastify.js +7 -11
  66. package/packages/datadog-instrumentations/src/grpc/server.js +18 -0
  67. package/packages/datadog-instrumentations/src/helpers/hook.js +27 -6
  68. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +8 -8
  69. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +28 -2
  70. package/packages/datadog-instrumentations/src/helpers/router-helper.js +8 -8
  71. package/packages/datadog-instrumentations/src/http2/server.js +143 -17
  72. package/packages/datadog-instrumentations/src/jest.js +36 -4
  73. package/packages/datadog-instrumentations/src/mariadb.js +1 -1
  74. package/packages/datadog-instrumentations/src/mocha/main.js +0 -2
  75. package/packages/datadog-instrumentations/src/mocha/utils.js +0 -4
  76. package/packages/datadog-instrumentations/src/mongodb.js +3 -3
  77. package/packages/datadog-instrumentations/src/mongoose.js +3 -3
  78. package/packages/datadog-instrumentations/src/mquery.js +2 -2
  79. package/packages/datadog-instrumentations/src/mysql.js +1 -1
  80. package/packages/datadog-instrumentations/src/next.js +24 -0
  81. package/packages/datadog-instrumentations/src/nyc.js +0 -2
  82. package/packages/datadog-instrumentations/src/oracledb.js +2 -2
  83. package/packages/datadog-instrumentations/src/pg.js +2 -2
  84. package/packages/datadog-instrumentations/src/playwright.js +159 -22
  85. package/packages/datadog-instrumentations/src/process.js +3 -0
  86. package/packages/datadog-instrumentations/src/router.js +191 -106
  87. package/packages/datadog-instrumentations/src/selenium.js +52 -26
  88. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +2 -2
  89. package/packages/datadog-instrumentations/src/vitest-main.js +0 -2
  90. package/packages/datadog-instrumentations/src/ws.js +2 -1
  91. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +2 -2
  92. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +2 -1
  93. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +41 -16
  94. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +21 -21
  95. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +14 -8
  96. package/packages/datadog-plugin-aws-sdk/src/util.js +2 -24
  97. package/packages/datadog-plugin-cucumber/src/index.js +1 -1
  98. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +24 -62
  99. package/packages/datadog-plugin-cypress/src/index.js +6 -1
  100. package/packages/datadog-plugin-cypress/src/support.js +92 -26
  101. package/packages/datadog-plugin-electron/src/ipc.js +1 -1
  102. package/packages/datadog-plugin-graphql/src/execute.js +177 -59
  103. package/packages/datadog-plugin-graphql/src/parse.js +22 -1
  104. package/packages/datadog-plugin-graphql/src/request.js +32 -1
  105. package/packages/datadog-plugin-graphql/src/utils.js +42 -0
  106. package/packages/datadog-plugin-graphql/src/validate.js +15 -3
  107. package/packages/datadog-plugin-http2/src/client.js +1 -1
  108. package/packages/datadog-plugin-http2/src/server.js +38 -6
  109. package/packages/datadog-plugin-jest/src/util.js +21 -6
  110. package/packages/datadog-plugin-langchain/src/handlers/embedding.js +2 -1
  111. package/packages/datadog-plugin-langchain/src/handlers/language_models.js +2 -1
  112. package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
  113. package/packages/datadog-plugin-playwright/src/index.js +177 -64
  114. package/packages/datadog-plugin-router/src/index.js +11 -2
  115. package/packages/datadog-plugin-selenium/src/index.js +5 -36
  116. package/packages/datadog-plugin-vitest/src/index.js +4 -2
  117. package/packages/datadog-plugin-ws/src/close.js +2 -1
  118. package/packages/datadog-plugin-ws/src/producer.js +2 -1
  119. package/packages/datadog-plugin-ws/src/receiver.js +2 -1
  120. package/packages/datadog-plugin-ws/src/server.js +2 -1
  121. package/packages/dd-trace/index.electron.js +3 -0
  122. package/packages/dd-trace/index.js +2 -37
  123. package/packages/dd-trace/src/aiguard/sdk.js +1 -1
  124. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
  125. package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +3 -1
  126. package/packages/dd-trace/src/azure_metadata.js +2 -1
  127. package/packages/dd-trace/src/bootstrap.js +39 -0
  128. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +93 -28
  129. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  130. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/index.js +160 -0
  131. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +288 -0
  132. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/payload-projection.js +84 -0
  133. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +369 -0
  134. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +60 -0
  135. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +55 -5
  136. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +22 -6
  137. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +5 -0
  138. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +62 -7
  139. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +9 -2
  140. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +6 -2
  141. package/packages/dd-trace/src/ci-visibility/rum.js +7 -0
  142. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
  143. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  144. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +137 -9
  145. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +173 -9
  146. package/packages/dd-trace/src/ci-visibility/test-screenshot.js +90 -0
  147. package/packages/dd-trace/src/config/generated-config-types.d.ts +20 -0
  148. package/packages/dd-trace/src/config/helper.js +1 -0
  149. package/packages/dd-trace/src/config/index.js +10 -0
  150. package/packages/dd-trace/src/config/supported-configurations.json +83 -0
  151. package/packages/dd-trace/src/constants.js +7 -0
  152. package/packages/dd-trace/src/datastreams/pathway.js +6 -4
  153. package/packages/dd-trace/src/exporter.js +2 -0
  154. package/packages/dd-trace/src/exporters/common/client-library-headers.js +21 -0
  155. package/packages/dd-trace/src/exporters/common/request.js +59 -30
  156. package/packages/dd-trace/src/exporters/common/url.js +15 -1
  157. package/packages/dd-trace/src/feature-registry.js +29 -0
  158. package/packages/dd-trace/src/llmobs/constants/tags.js +1 -0
  159. package/packages/dd-trace/src/llmobs/experiments/client.js +113 -0
  160. package/packages/dd-trace/src/llmobs/experiments/dataset.js +154 -0
  161. package/packages/dd-trace/src/llmobs/experiments/experiment.js +283 -0
  162. package/packages/dd-trace/src/llmobs/experiments/index.js +152 -0
  163. package/packages/dd-trace/src/llmobs/experiments/noop.js +30 -0
  164. package/packages/dd-trace/src/llmobs/experiments/result.js +34 -0
  165. package/packages/dd-trace/src/llmobs/index.js +10 -2
  166. package/packages/dd-trace/src/llmobs/noop.js +6 -0
  167. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +20 -3
  168. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +2 -2
  169. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  170. package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +8 -0
  171. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +48 -14
  172. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +43 -0
  173. package/packages/dd-trace/src/llmobs/sdk.js +17 -0
  174. package/packages/dd-trace/src/llmobs/tagger.js +79 -7
  175. package/packages/dd-trace/src/llmobs/telemetry.js +2 -1
  176. package/packages/dd-trace/src/llmobs/util.js +32 -0
  177. package/packages/dd-trace/src/noop/proxy.js +5 -4
  178. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +322 -0
  179. package/packages/dd-trace/src/openfeature/configuration_source.js +90 -0
  180. package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -23
  181. package/packages/dd-trace/src/openfeature/index.js +0 -2
  182. package/packages/dd-trace/src/openfeature/noop.js +1 -28
  183. package/packages/dd-trace/src/openfeature/register.js +35 -0
  184. package/packages/dd-trace/src/openfeature/remote_config.js +15 -18
  185. package/packages/dd-trace/src/openfeature/require-provider.js +18 -0
  186. package/packages/dd-trace/src/opentelemetry/span-ending-hook.js +10 -0
  187. package/packages/dd-trace/src/opentelemetry/span.js +5 -0
  188. package/packages/dd-trace/src/opentracing/propagation/log.js +11 -2
  189. package/packages/dd-trace/src/opentracing/propagation/text_map.js +80 -14
  190. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +10 -1
  191. package/packages/dd-trace/src/opentracing/tracer.js +7 -1
  192. package/packages/dd-trace/src/plugins/ci_plugin.js +20 -6
  193. package/packages/dd-trace/src/plugins/util/git.js +3 -2
  194. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +29 -5
  195. package/packages/dd-trace/src/plugins/util/test.js +65 -8
  196. package/packages/dd-trace/src/plugins/util/web.js +12 -0
  197. package/packages/dd-trace/src/proxy.js +99 -10
  198. package/packages/dd-trace/src/span_processor.js +5 -0
  199. package/packages/dd-trace/src/standalone/index.js +0 -36
  200. package/packages/dd-trace/src/telemetry/send-data.js +2 -2
  201. package/packages/dd-trace/src/util.js +26 -0
  202. package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
  203. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  204. package/vendor/dist/protobufjs/index.js +1 -1
  205. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  206. package/vendor/dist/shell-quote/index.js +1 -1
  207. package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +0 -17
@@ -5,7 +5,7 @@ const dc = require('dc-polyfill')
5
5
  const { storage } = require('../../datadog-core')
6
6
  const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
7
7
  const GraphQLParsePlugin = require('./parse')
8
- const { extractErrorIntoSpanEvent, getOperation, getSignature } = require('./utils')
8
+ const { extractErrorIntoSpanEvent, getOperation, getSignature, isApolloHealthCheck } = require('./utils')
9
9
 
10
10
  const legacyStorage = storage('legacy')
11
11
 
@@ -26,12 +26,24 @@ const contexts = new WeakMap()
26
26
  const instrumentedArgs = new WeakSet()
27
27
 
28
28
  const patchedResolvers = new WeakSet()
29
- const patchedTypes = new WeakSet()
29
+
30
+ // Visited types per caller-owned schema. The walk reaches union members and
31
+ // interface implementations through the schema (`getTypes`/`getPossibleTypes`),
32
+ // so it differs per schema: a global guard would stop the second schema at any
33
+ // type the first already walked and leave its own implementations unwrapped.
34
+ // `patchedResolvers` keeps wrapping idempotent, so re-walking a shared type is
35
+ // safe and this set only terminates cycles.
36
+ const walkedTypes = new WeakMap()
30
37
 
31
38
  // Module-level fast path: skip the resolver-side WeakMap lookup entirely
32
39
  // when depth=0 disables resolver instrumentation.
33
40
  let depthDisabled = false
34
41
 
42
+ // Initial key for the per-operation variables-filter cache. A unique sentinel
43
+ // so the first #filterVariables call never falsely matches, even when the
44
+ // operation's variableValues is undefined.
45
+ const NO_VARIABLES_CACHED = Symbol('noVariablesCached')
46
+
35
47
  class AbortError extends Error {
36
48
  constructor (message) {
37
49
  super(message)
@@ -111,6 +123,16 @@ class GraphQLExecutePlugin extends TracingPlugin {
111
123
  const name = operation?.name?.value
112
124
  const source = this.config.source && docSource
113
125
 
126
+ // Apollo Server may execute a cached document without parsing it first.
127
+ // Match the full gateway operation here so caller-owned AST transformations
128
+ // cannot suppress execute/resolver AppSec and IAST channels.
129
+ if (name === '__ApolloServiceHealthCheck__' &&
130
+ document.definitions.length === 1 &&
131
+ isApolloHealthCheck(operation)) {
132
+ ctx.ddSkipped = true
133
+ return ctx.currentStore
134
+ }
135
+
114
136
  ctx.collapse = this.config.collapse
115
137
 
116
138
  const signature = getSignature(document, name, type, this.config.signature)
@@ -165,9 +187,9 @@ class GraphQLExecutePlugin extends TracingPlugin {
165
187
 
166
188
  const schema = args.schema
167
189
  if (schema) {
168
- wrapFields(schema._queryType)
169
- wrapFields(schema._mutationType)
170
- wrapFields(schema._subscriptionType)
190
+ wrapFields(schema._queryType, schema)
191
+ wrapFields(schema._mutationType, schema)
192
+ wrapFields(schema._subscriptionType, schema)
171
193
  }
172
194
 
173
195
  const rootCtx = {
@@ -175,10 +197,13 @@ class GraphQLExecutePlugin extends TracingPlugin {
175
197
  config: this.config,
176
198
  fields: new Map(),
177
199
  pathCache: new Map(),
178
- collapsedPathCache: this.config.collapse ? { byPath: new Map(), byParent: new Map() } : undefined,
179
200
  abortController,
180
201
  executeSpan: span,
181
202
  plugin: this,
203
+ // graphql.resolve variable tags: memoize the filtered result for the
204
+ // last-seen variableValues object (see #filterVariables).
205
+ filteredVariablesKey: NO_VARIABLES_CACHED,
206
+ filteredVariables: undefined,
182
207
  }
183
208
  ctx.ddRootCtx = rootCtx
184
209
  if (isWeakMapKey(contextValue)) {
@@ -202,9 +227,13 @@ class GraphQLExecutePlugin extends TracingPlugin {
202
227
  if (!span) return
203
228
 
204
229
  // Synchronous execute() throw (e.g. execute(null, doc)) — error handler
205
- // already tagged the span, just finish it.
230
+ // already tagged the span.
206
231
  if (ctx.error) {
207
- this.#drain(ctx, span)
232
+ if (ctx.ddAborted) {
233
+ span.finish()
234
+ } else {
235
+ this.#finishSpan(ctx, span)
236
+ }
208
237
  return ctx.parentStore
209
238
  }
210
239
 
@@ -213,10 +242,7 @@ class GraphQLExecutePlugin extends TracingPlugin {
213
242
  if (typeof result?.then === 'function') {
214
243
  result.then(
215
244
  (res) => this.#finishSpan(ctx, span, res),
216
- (err) => {
217
- span.setTag('error', err)
218
- this.#drain(ctx, span)
219
- }
245
+ (err) => this.#finishSpan(ctx, span, undefined, err)
220
246
  )
221
247
  } else {
222
248
  this.#finishSpan(ctx, span, result)
@@ -235,8 +261,16 @@ class GraphQLExecutePlugin extends TracingPlugin {
235
261
  }
236
262
  }
237
263
 
238
- #finishSpan (ctx, span, res) {
239
- this.config.hooks.execute(span, ctx.ddArgs, res)
264
+ /**
265
+ * @param {object} ctx
266
+ * @param {import('../../dd-trace/src/opentracing/span')} span
267
+ * @param {import('graphql').ExecutionResult} [res]
268
+ * @param {unknown} [error]
269
+ */
270
+ #finishSpan (ctx, span, res, error) {
271
+ if (error !== undefined) {
272
+ span.setTag('error', error)
273
+ }
240
274
 
241
275
  if (res?.errors?.length) {
242
276
  span.setTag('error', res.errors[0])
@@ -245,17 +279,16 @@ class GraphQLExecutePlugin extends TracingPlugin {
245
279
  }
246
280
  }
247
281
 
248
- this.#drain(ctx, span)
249
- }
250
-
251
- #drain (ctx, span) {
252
- span.finish()
253
282
  if (ctx.ddContextValue) {
254
283
  contexts.delete(ctx.ddContextValue)
255
284
  }
256
285
  if (ctx.ddInstrumentedArgs) {
257
286
  instrumentedArgs.delete(ctx.ddInstrumentedArgs)
258
287
  }
288
+
289
+ this.config.hooks.execute(span, ctx.ddArgs, res)
290
+
291
+ span.finish()
259
292
  }
260
293
 
261
294
  // Public — called from wrapResolve (free function, crosses class boundary).
@@ -281,6 +314,7 @@ class GraphQLExecutePlugin extends TracingPlugin {
281
314
  type: 'graphql',
282
315
  startTime,
283
316
  meta: {
317
+ 'graphql.field.coordinates': `${field.parentTypeName}.${fieldName}`,
284
318
  'graphql.field.name': fieldName,
285
319
  'graphql.field.path': collapsedKey,
286
320
  'graphql.field.type': baseTypeName,
@@ -291,7 +325,7 @@ class GraphQLExecutePlugin extends TracingPlugin {
291
325
  field.span = span
292
326
 
293
327
  if (fieldNode && this.config.variables && fieldNode.arguments) {
294
- const variables = this.config.variables(variableValues)
328
+ const variables = this.#filterVariables(rootCtx, variableValues)
295
329
  for (const arg of fieldNode.arguments) {
296
330
  if (arg.value?.name && arg.value.kind === 'Variable' && variables[arg.value.name.value]) {
297
331
  const name = arg.value.name.value
@@ -303,6 +337,27 @@ class GraphQLExecutePlugin extends TracingPlugin {
303
337
  return span
304
338
  }
305
339
 
340
+ // Memoize the user variables filter against the last-seen variableValues
341
+ // object. graphql hands every resolver in one execute the same coerced
342
+ // variableValues object, so all arg-bearing fields hit the identity fast
343
+ // path and the filter runs once per operation. A nested execute() sharing
344
+ // the same object contextValue reuses the outer rootCtx but carries its own
345
+ // variableValues; comparing by identity recomputes for it (and any later
346
+ // fields on that inner object reuse the slot), so each field's tags stay
347
+ // correct. A single slot beats a WeakMap here: no per-operation allocation,
348
+ // and the common single-object case is a bare `===` (see the microbenchmark
349
+ // numbers in the commit body).
350
+ #filterVariables (rootCtx, variableValues) {
351
+ if (rootCtx.filteredVariablesKey === variableValues) {
352
+ return rootCtx.filteredVariables
353
+ }
354
+
355
+ const filtered = this.config.variables(variableValues)
356
+ rootCtx.filteredVariablesKey = variableValues
357
+ rootCtx.filteredVariables = filtered
358
+ return filtered
359
+ }
360
+
306
361
  // Public — called from wrapResolve. endTime reflects when the resolver
307
362
  // actually completed, not when the field record was created.
308
363
  finishResolveSpan (span, field, error, result, endTime) {
@@ -376,20 +431,35 @@ function wrapResolve (resolve) {
376
431
  return resolve.apply(this, arguments)
377
432
  }
378
433
 
379
- const fieldKey = config.collapse ? buildCachedCollapsedPath(infoPath, rootCtx.collapsedPathCache) : infoPath
434
+ const fieldKey = config.collapse ? pathString : infoPath
435
+ const parentTypeName = info.parentType.name
380
436
  let field = rootCtx.fields.get(fieldKey)
437
+ const collapsedField = field
438
+ if (config.collapse && field !== undefined && field.parentTypeName !== parentTypeName) {
439
+ const parentTypeFields = field.parentTypeFields
440
+ if (parentTypeFields?.parentTypeName === undefined) {
441
+ field = parentTypeFields?.get(parentTypeName)
442
+ } else if (parentTypeFields.parentTypeName === parentTypeName) {
443
+ field = parentTypeFields
444
+ } else {
445
+ field = undefined
446
+ }
447
+ if (field && infoPath.typename === undefined) {
448
+ cacheFieldByPath(rootCtx, infoPath, field)
449
+ }
450
+ }
381
451
  const isFirst = !field
382
452
 
383
453
  if (isFirst) {
384
454
  field = {
385
455
  fieldNode: info.fieldNodes?.[0],
386
456
  fieldName: info.fieldName,
457
+ parentTypeName,
387
458
  returnType: info.returnType,
388
459
  baseTypeName: getBaseTypeName(info.returnType),
389
460
  variableValues: info.variableValues,
390
461
  args,
391
462
  infoPath,
392
- fieldKey,
393
463
  pathString,
394
464
  collapsedKey: collapsedKey ?? pathString,
395
465
  span: null,
@@ -398,7 +468,25 @@ function wrapResolve (resolve) {
398
468
  parentStore: null,
399
469
  currentStore: null,
400
470
  }
401
- rootCtx.fields.set(fieldKey, field)
471
+ if (config.collapse && collapsedField) {
472
+ const parentTypeFields = collapsedField.parentTypeFields
473
+ if (parentTypeFields === undefined) {
474
+ collapsedField.parentTypeFields = field
475
+ } else if (parentTypeFields.parentTypeName === undefined) {
476
+ parentTypeFields.set(parentTypeName, field)
477
+ } else {
478
+ const fieldsByParentType = new Map()
479
+ .set(collapsedField.parentTypeName, collapsedField)
480
+ .set(parentTypeFields.parentTypeName, parentTypeFields)
481
+ .set(parentTypeName, field)
482
+ collapsedField.parentTypeFields = fieldsByParentType
483
+ }
484
+ if (infoPath.typename === undefined) {
485
+ cacheFieldByPath(rootCtx, infoPath, field)
486
+ }
487
+ } else {
488
+ rootCtx.fields.set(fieldKey, field)
489
+ }
402
490
  }
403
491
 
404
492
  // Collapsed siblings still publish updateField (master's contract: one
@@ -432,15 +520,44 @@ function wrapResolve (resolve) {
432
520
  return resolveAsync
433
521
  }
434
522
 
435
- function wrapFields (type) {
436
- if (!type?._fields || patchedTypes.has(type)) return
523
+ function wrapFields (type, schema) {
524
+ if (!type || !markWalked(schema, type)) return
525
+
526
+ const tag = type[Symbol.toStringTag]
527
+
528
+ // Union types (e.g. Apollo Federation's `_Entity`) hold their members on
529
+ // `_types`, not `_fields`. Their member object types are reachable only here,
530
+ // so descend into each to wrap the entity resolvers a `_entities` query runs.
531
+ if (tag === 'GraphQLUnionType') {
532
+ for (const member of type.getTypes()) wrapFields(member, schema)
533
+ return
534
+ }
535
+
536
+ if (type._fields) {
537
+ for (const field of Object.values(type._fields)) {
538
+ wrapFieldResolve(field)
539
+ wrapFieldType(field, schema)
540
+ }
541
+ }
437
542
 
438
- patchedTypes.add(type)
543
+ // Interface implementations carry their own resolvers and are reachable only
544
+ // through `getPossibleTypes`; an interface return type alone never wraps them.
545
+ if (schema && tag === 'GraphQLInterfaceType') {
546
+ for (const impl of schema.getPossibleTypes(type)) wrapFields(impl, schema)
547
+ }
548
+ }
439
549
 
440
- for (const field of Object.values(type._fields)) {
441
- wrapFieldResolve(field)
442
- wrapFieldType(field)
550
+ // Marks the guard on entry so recursive types (a field looping back to its own
551
+ // type, an interface an implementation returns) terminate the walk.
552
+ function markWalked (schema, type) {
553
+ let walked = walkedTypes.get(schema)
554
+ if (walked === undefined) {
555
+ walked = new WeakSet()
556
+ walkedTypes.set(schema, walked)
443
557
  }
558
+ if (walked.has(type)) return false
559
+ walked.add(type)
560
+ return true
444
561
  }
445
562
 
446
563
  function wrapFieldResolve (field) {
@@ -448,13 +565,13 @@ function wrapFieldResolve (field) {
448
565
  field.resolve = wrapResolve(field.resolve)
449
566
  }
450
567
 
451
- function wrapFieldType (field) {
568
+ function wrapFieldType (field, schema) {
452
569
  if (!field?.type) return
453
570
 
454
571
  let unwrapped = field.type
455
572
  while (unwrapped.ofType) unwrapped = unwrapped.ofType
456
573
 
457
- wrapFields(unwrapped)
574
+ wrapFields(unwrapped, schema)
458
575
  }
459
576
 
460
577
  // Runs the resolver inside `store`, including any code after an internal
@@ -516,31 +633,18 @@ function buildCachedPathString (path, cache, collapse) {
516
633
  return pathString
517
634
  }
518
635
 
519
- function buildCachedCollapsedPath (path, cache) {
520
- if (!path) return
521
-
522
- const cached = cache.byPath.get(path)
523
- if (cached !== undefined) return cached
524
-
525
- const segment = typeof path.key === 'string' ? path.key : '*'
526
- const prev = path.prev === undefined
527
- ? undefined
528
- : buildCachedCollapsedPath(path.prev, cache)
529
-
530
- let siblings = cache.byParent.get(prev)
531
- if (siblings === undefined) {
532
- siblings = new Map()
533
- cache.byParent.set(prev, siblings)
534
- }
535
-
536
- let collapsedPath = siblings.get(segment)
537
- if (collapsedPath === undefined) {
538
- collapsedPath = { key: segment, prev }
539
- siblings.set(segment, collapsedPath)
540
- }
541
-
542
- cache.byPath.set(path, collapsedPath)
543
- return collapsedPath
636
+ /**
637
+ * @param {{ hasFieldsByPath?: boolean, fields: Map<string|object, object> }} rootCtx
638
+ * @param {object} path
639
+ * @param {object} field
640
+ */
641
+ function cacheFieldByPath (rootCtx, path, field) {
642
+ // Leaf fields cannot parent resolver spans, so their concrete paths are never read.
643
+ if (field.fieldNode?.selectionSet === undefined) return
644
+
645
+ // Concrete info path objects cannot collide with collapsed path string keys.
646
+ rootCtx.hasFieldsByPath = true
647
+ rootCtx.fields.set(path, field)
544
648
  }
545
649
 
546
650
  // Depth filtering directly on the linked-list node — no array allocation needed.
@@ -564,9 +668,23 @@ function shouldInstrumentNode (config, path) {
564
668
  }
565
669
 
566
670
  function getParentField (rootCtx, field) {
567
- for (let curr = field.fieldKey?.prev; curr; curr = curr.prev) {
568
- const innerField = rootCtx.fields.get(curr)
569
- if (innerField) return innerField
671
+ for (let curr = field.infoPath?.prev; curr; curr = curr.prev) {
672
+ const fieldKey = rootCtx.config.collapse ? rootCtx.pathCache.get(curr) : curr
673
+ const innerField = rootCtx.fields.get(fieldKey)
674
+ if (innerField) {
675
+ if (curr.typename === undefined) {
676
+ if (rootCtx.hasFieldsByPath) {
677
+ const fieldByPath = rootCtx.fields.get(curr)
678
+ if (fieldByPath) return fieldByPath
679
+ }
680
+ return innerField
681
+ }
682
+ if (innerField.parentTypeName === curr.typename) return innerField
683
+
684
+ const parentTypeFields = innerField.parentTypeFields
685
+ if (parentTypeFields.parentTypeName === undefined) return parentTypeFields.get(curr.typename)
686
+ return parentTypeFields
687
+ }
570
688
  }
571
689
 
572
690
  return null
@@ -1,9 +1,15 @@
1
1
  'use strict'
2
2
 
3
3
  const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
4
+ const { isApolloHealthCheckSource } = require('./utils')
4
5
 
5
6
  const documentSources = new WeakMap()
6
7
 
8
+ // Documents produced by parsing an Apollo Gateway health-check poll. Populated
9
+ // here (parse owns the document lifecycle, like documentSources) and read by the
10
+ // validate plugin. Execute independently verifies the operation for cached docs.
11
+ const healthCheckDocuments = new WeakSet()
12
+
7
13
  class GraphQLParsePlugin extends TracingPlugin {
8
14
  static id = 'graphql'
9
15
  static operation = 'parser'
@@ -12,6 +18,14 @@ class GraphQLParsePlugin extends TracingPlugin {
12
18
  bindStart (ctx) {
13
19
  const source = ctx.arguments?.[0]
14
20
 
21
+ // Apollo Gateway polls every subgraph with a fixed health-check query.
22
+ // Mark its document after parsing so validation can skip the same poll.
23
+ if (isApolloHealthCheckSource(source?.body ?? source)) {
24
+ ctx.ddHealthCheck = true
25
+ ctx.ddSkipped = true
26
+ return ctx.currentStore
27
+ }
28
+
15
29
  this.startSpan('graphql.parse', {
16
30
  service: this.config.service,
17
31
  type: 'graphql',
@@ -24,8 +38,14 @@ class GraphQLParsePlugin extends TracingPlugin {
24
38
  }
25
39
 
26
40
  end (ctx) {
27
- const source = ctx.ddSource
28
41
  const document = ctx.result
42
+
43
+ if (ctx.ddHealthCheck) {
44
+ if (document) healthCheckDocuments.add(document)
45
+ return ctx.parentStore
46
+ }
47
+
48
+ const source = ctx.ddSource
29
49
  const span = ctx?.currentStore?.span || this.activeSpan
30
50
 
31
51
  let docSource
@@ -51,5 +71,6 @@ class GraphQLParsePlugin extends TracingPlugin {
51
71
  }
52
72
 
53
73
  GraphQLParsePlugin.documentSources = documentSources
74
+ GraphQLParsePlugin.healthCheckDocuments = healthCheckDocuments
54
75
 
55
76
  module.exports = GraphQLParsePlugin
@@ -1,7 +1,24 @@
1
1
  'use strict'
2
2
 
3
3
  const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
4
- const { extractErrorIntoSpanEvent, getCachedRequestOperation } = require('./utils')
4
+ const { extractErrorIntoSpanEvent, getCachedRequestOperation, isApolloHealthCheckSource } = require('./utils')
5
+
6
+ /**
7
+ * @typedef {object} GraphQLRequestStore
8
+ * @property {import('../../dd-trace/src/opentracing/span')} [span]
9
+ * @property {import('../../dd-trace/src/opentracing/span')} [graphqlRequestSpan]
10
+ * @property {string} [graphqlRequestOperationName]
11
+ * @property {unknown} [graphqlRequestSource]
12
+ */
13
+
14
+ /**
15
+ * @typedef {object} GraphQLRequestContext
16
+ * @property {unknown[]} [arguments]
17
+ * @property {GraphQLRequestStore} [currentStore]
18
+ * @property {GraphQLRequestStore} [parentStore]
19
+ * @property {boolean} [ddSkipped]
20
+ * @property {{ errors?: import('graphql').GraphQLError[] }} [result]
21
+ */
5
22
 
6
23
  // Top-level GraphQL request span for drivers that funnel every operation
7
24
  // through a single entry point but parse/validate/execute internally (mercurius
@@ -25,9 +42,18 @@ class GraphQLRequestPlugin extends TracingPlugin {
25
42
  static kind = 'server'
26
43
  static prefix = 'tracing:orchestrion:mercurius:apm:graphql:request'
27
44
 
45
+ /**
46
+ * @param {GraphQLRequestContext} ctx
47
+ */
28
48
  bindStart (ctx) {
29
49
  // fastifyGraphQl(source, context, variables, operationName)
30
50
  const source = ctx.arguments?.[0]
51
+
52
+ if (isApolloHealthCheckSource(source)) {
53
+ ctx.ddSkipped = true
54
+ return ctx.currentStore
55
+ }
56
+
31
57
  const operationName = ctx.arguments?.[3]
32
58
 
33
59
  // `source` is the request text on the common path, but mercurius also
@@ -72,7 +98,12 @@ class GraphQLRequestPlugin extends TracingPlugin {
72
98
  return ctx.currentStore
73
99
  }
74
100
 
101
+ /**
102
+ * @param {GraphQLRequestContext} ctx
103
+ */
75
104
  asyncEnd (ctx) {
105
+ if (ctx.ddSkipped) return ctx.parentStore
106
+
76
107
  /* istanbul ignore next: currentStore is populated for the request lifecycle; activeSpan is base-plugin fallback. */
77
108
  const span = ctx?.currentStore?.span || this.activeSpan
78
109
  /* istanbul ignore if: startSpan always populates currentStore for the request lifecycle. */
@@ -228,6 +228,46 @@ function extractErrorIntoSpanEvent (config, span, exc) {
228
228
  span.addEvent('dd.graphql.query.error', attributes, Date.now())
229
229
  }
230
230
 
231
+ // Apollo Gateway's fixed subgraph health-check query, sent verbatim on every
232
+ // poll interval. See https://github.com/apollographql/federation
233
+ // `HEALTH_CHECK_QUERY`.
234
+ const HEALTH_CHECK_QUERY = 'query __ApolloServiceHealthCheck__ { __typename }'
235
+
236
+ /**
237
+ * Matches the raw query string before it is parsed (the only input parse has).
238
+ *
239
+ * @param {unknown} source Raw query string or a graphql `Source` body.
240
+ * @returns {boolean}
241
+ */
242
+ function isApolloHealthCheckSource (source) {
243
+ return source === HEALTH_CHECK_QUERY
244
+ }
245
+
246
+ /**
247
+ * Matches Apollo's parsed health-check operation exactly for cached documents.
248
+ *
249
+ * @param {import('graphql').OperationDefinitionNode | undefined} operation
250
+ * @returns {boolean}
251
+ */
252
+ function isApolloHealthCheck (operation) {
253
+ const selections = operation?.selectionSet?.selections
254
+ if (operation?.operation !== 'query' ||
255
+ operation.name?.value !== '__ApolloServiceHealthCheck__' ||
256
+ operation.variableDefinitions?.length ||
257
+ operation.directives?.length ||
258
+ selections?.length !== 1) {
259
+ return false
260
+ }
261
+
262
+ const selection = selections[0]
263
+ return selection.kind === 'Field' &&
264
+ selection.name?.value === '__typename' &&
265
+ selection.alias === undefined &&
266
+ selection.selectionSet === undefined &&
267
+ selection.arguments?.length === 0 &&
268
+ selection.directives?.length === 0
269
+ }
270
+
231
271
  let tools
232
272
 
233
273
  function getSignature (document, operationName, operationType, calculate) {
@@ -261,5 +301,7 @@ module.exports = {
261
301
  getCachedRequestOperation,
262
302
  getOperation,
263
303
  getSignature,
304
+ isApolloHealthCheck,
305
+ isApolloHealthCheckSource,
264
306
  refineRequestSpan,
265
307
  }
@@ -3,7 +3,7 @@
3
3
  const { storage } = require('../../datadog-core')
4
4
  const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
5
5
  const GraphQLParsePlugin = require('./parse')
6
- const { extractErrorIntoSpanEvent, refineRequestSpan } = require('./utils')
6
+ const { extractErrorIntoSpanEvent, isApolloHealthCheck, refineRequestSpan } = require('./utils')
7
7
 
8
8
  const legacyStorage = storage('legacy')
9
9
 
@@ -15,6 +15,16 @@ class GraphQLValidatePlugin extends TracingPlugin {
15
15
  bindStart (ctx) {
16
16
  // validate(schema, documentAST, rules, options, typeInfo)
17
17
  const document = ctx.arguments?.[1]
18
+
19
+ // Verify the marked document in case the caller transformed its AST after parsing.
20
+ if (document &&
21
+ GraphQLParsePlugin.healthCheckDocuments.has(document) &&
22
+ document.definitions?.length === 1 &&
23
+ isApolloHealthCheck(document.definitions[0])) {
24
+ ctx.ddSkipped = true
25
+ return ctx.currentStore
26
+ }
27
+
18
28
  const docSource = document ? GraphQLParsePlugin.documentSources.get(document) : undefined
19
29
  const source = this.config.source && document && docSource
20
30
 
@@ -52,12 +62,12 @@ class GraphQLValidatePlugin extends TracingPlugin {
52
62
  }
53
63
 
54
64
  end (ctx) {
65
+ if (ctx.ddSkipped) return ctx.parentStore
66
+
55
67
  const document = ctx.ddDocument
56
68
  const errors = ctx.result
57
69
  const span = ctx?.currentStore?.span || this.activeSpan
58
70
 
59
- this.config.hooks.validate(span, document, errors)
60
-
61
71
  if (errors?.length) {
62
72
  span.setTag('error', errors[0])
63
73
  for (const err of errors) {
@@ -65,6 +75,8 @@ class GraphQLValidatePlugin extends TracingPlugin {
65
75
  }
66
76
  }
67
77
 
78
+ this.config.hooks.validate(span, document, errors)
79
+
68
80
  span.finish()
69
81
 
70
82
  return ctx.parentStore
@@ -32,7 +32,7 @@ class Http2ClientPlugin extends ClientPlugin {
32
32
  const { authority, options, headers = {} } = message
33
33
  const sessionDetails = extractSessionDetails(authority, options)
34
34
  const path = headers[HTTP2_HEADER_PATH] || '/'
35
- const pathname = path.split(/[?#]/)[0]
35
+ const pathname = path.split(/[?#]/, 1)[0]
36
36
  const method = headers[HTTP2_HEADER_METHOD] || HTTP2_METHOD_GET
37
37
  const base = `${sessionDetails.protocol}//${sessionDetails.host}:${sessionDetails.port}`
38
38
  const uri = `${base}${pathname}`
@@ -1,7 +1,5 @@
1
1
  'use strict'
2
2
 
3
- // Plugin temporarily disabled. See https://github.com/DataDog/dd-trace-js/issues/312
4
-
5
3
  const ServerPlugin = require('../../dd-trace/src/plugins/server')
6
4
  const web = require('../../dd-trace/src/plugins/util/web')
7
5
  const { COMPONENT, SVC_SRC_KEY } = require('../../dd-trace/src/constants')
@@ -10,6 +8,7 @@ class Http2ServerPlugin extends ServerPlugin {
10
8
  constructor (tracer, config) {
11
9
  super(tracer, config)
12
10
  this.addBind('apm:http2:server:response:emit', this.bindEmit)
11
+ this.addSub('apm:http2:server:request:adopt', this.adopt)
13
12
  }
14
13
 
15
14
  static id = 'http2'
@@ -47,15 +46,35 @@ class Http2ServerPlugin extends ServerPlugin {
47
46
 
48
47
  const context = web.getContext(req)
49
48
 
50
- if (!context.instrumented) {
51
- context.res.writeHead = web.wrapWriteHead(context)
52
- context.instrumented = true
53
- }
49
+ // A mixed server adopts the real request off this stream later; key the
50
+ // context on the stream now so that lookup resolves. Skipped for the common
51
+ // single-listener request, which never adopts.
52
+ if (ctx.adoptable) web.linkContextToStream(req.stream, context)
53
+
54
+ instrumentWriteHead(context)
54
55
 
55
56
  return ctx.currentStore
56
57
  }
57
58
 
59
+ // A mixed server (raw-stream + 'request' listeners) creates the span from the
60
+ // 'stream' event with a throwaway adapter. When the compatibility layer then
61
+ // synthesizes the real request/response off the same stream, point the shared
62
+ // context at them so `web.setFramework`/`web.setRoute` from the user's
63
+ // 'request' handler resolve to this span and the finish `hooks.request`
64
+ // receives the real objects instead of the adapter.
65
+ adopt (ctx) {
66
+ const context = web.patch(ctx.req)
67
+ context.req = ctx.req
68
+ context.res = ctx.res
69
+ instrumentWriteHead(context)
70
+ }
71
+
58
72
  bindEmit (ctx) {
73
+ // Both the compatibility response and the core-API stream emit 'close'
74
+ // exactly once, so the span is finished from a single source. `web.js`
75
+ // bypasses its `finished` idempotency guard for stream-backed requests
76
+ // (`!req.stream`); that bypass is harmless here only because of this
77
+ // single-finish property.
59
78
  if (ctx.eventName !== 'close') return ctx.currentStore
60
79
 
61
80
  const { req } = ctx
@@ -78,4 +97,17 @@ class Http2ServerPlugin extends ServerPlugin {
78
97
  }
79
98
  }
80
99
 
100
+ // The core stream API has no `res.writeHead`; CORS preflight tagging only
101
+ // applies to the compatibility response that exposes it. Runs once per context:
102
+ // the mixed path calls it again from `adopt` once the real response is in place.
103
+ /**
104
+ * @param {{ res: { writeHead?: Function }, instrumented?: boolean }} context
105
+ */
106
+ function instrumentWriteHead (context) {
107
+ if (!context.instrumented && typeof context.res.writeHead === 'function') {
108
+ context.res.writeHead = web.wrapWriteHead(context)
109
+ context.instrumented = true
110
+ }
111
+ }
112
+
81
113
  module.exports = Http2ServerPlugin