dd-trace 6.0.0 → 6.2.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 (98) hide show
  1. package/LICENSE-3rdparty.csv +1 -2
  2. package/index.d.ts +28 -1
  3. package/initialize.mjs +4 -2
  4. package/package.json +34 -30
  5. package/packages/datadog-instrumentations/src/ai.js +45 -0
  6. package/packages/datadog-instrumentations/src/apollo-server.js +5 -13
  7. package/packages/datadog-instrumentations/src/child_process.js +3 -3
  8. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +587 -0
  9. package/packages/datadog-instrumentations/src/cucumber.js +102 -43
  10. package/packages/datadog-instrumentations/src/cypress-config.js +11 -3
  11. package/packages/datadog-instrumentations/src/fastify.js +27 -8
  12. package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -0
  13. package/packages/datadog-instrumentations/src/helpers/instrument.js +7 -0
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +5 -1
  15. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +6 -6
  16. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/claude-agent-sdk.js +17 -0
  17. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -0
  18. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/mercurius.js +31 -0
  19. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +19 -5
  20. package/packages/datadog-instrumentations/src/jest.js +713 -63
  21. package/packages/datadog-instrumentations/src/mercurius.js +11 -0
  22. package/packages/datadog-instrumentations/src/mocha/main.js +24 -3
  23. package/packages/datadog-instrumentations/src/mocha/utils.js +128 -22
  24. package/packages/datadog-instrumentations/src/mocha/worker.js +13 -0
  25. package/packages/datadog-instrumentations/src/mongodb.js +74 -0
  26. package/packages/datadog-instrumentations/src/mongoose.js +4 -5
  27. package/packages/datadog-instrumentations/src/otel-sdk-trace.js +15 -0
  28. package/packages/datadog-instrumentations/src/playwright.js +13 -9
  29. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +4 -0
  30. package/packages/datadog-instrumentations/src/vitest-main.js +26 -1
  31. package/packages/datadog-instrumentations/src/vitest-util.js +2 -0
  32. package/packages/datadog-instrumentations/src/vitest-worker.js +103 -34
  33. package/packages/datadog-plugin-ai/src/index.js +1 -1
  34. package/packages/datadog-plugin-ai/src/tracing.js +66 -3
  35. package/packages/datadog-plugin-ai/src/utils.js +17 -4
  36. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +1 -0
  37. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +1 -0
  38. package/packages/datadog-plugin-child_process/src/index.js +13 -2
  39. package/packages/datadog-plugin-claude-agent-sdk/src/index.js +31 -0
  40. package/packages/datadog-plugin-claude-agent-sdk/src/tracing.js +107 -0
  41. package/packages/datadog-plugin-claude-agent-sdk/src/util.js +15 -0
  42. package/packages/datadog-plugin-cucumber/src/index.js +14 -23
  43. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +351 -11
  44. package/packages/datadog-plugin-cypress/src/index.js +47 -2
  45. package/packages/datadog-plugin-cypress/src/plugin.js +1 -0
  46. package/packages/datadog-plugin-graphql/src/execute.js +25 -25
  47. package/packages/datadog-plugin-graphql/src/index.js +23 -1
  48. package/packages/datadog-plugin-graphql/src/request.js +104 -0
  49. package/packages/datadog-plugin-graphql/src/utils.js +186 -2
  50. package/packages/datadog-plugin-graphql/src/validate.js +25 -2
  51. package/packages/datadog-plugin-jest/src/index.js +30 -14
  52. package/packages/datadog-plugin-mocha/src/index.js +39 -14
  53. package/packages/datadog-plugin-mongodb-core/src/bulk-write.js +43 -0
  54. package/packages/datadog-plugin-mongodb-core/src/index.js +8 -443
  55. package/packages/datadog-plugin-mongodb-core/src/query.js +452 -0
  56. package/packages/datadog-plugin-playwright/src/index.js +3 -2
  57. package/packages/datadog-plugin-vitest/src/index.js +43 -9
  58. package/packages/dd-trace/src/aiguard/index.js +9 -14
  59. package/packages/dd-trace/src/aiguard/integrations/index.js +34 -0
  60. package/packages/dd-trace/src/aiguard/integrations/openai.js +47 -33
  61. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +42 -28
  62. package/packages/dd-trace/src/appsec/graphql.js +9 -6
  63. package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +3 -10
  64. package/packages/dd-trace/src/appsec/lambda.js +8 -8
  65. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +26 -1
  66. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +115 -24
  67. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +3 -0
  68. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +3 -0
  69. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +75 -7
  70. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +156 -0
  71. package/packages/dd-trace/src/config/generated-config-types.d.ts +10 -0
  72. package/packages/dd-trace/src/config/supported-configurations.json +37 -0
  73. package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -2
  74. package/packages/dd-trace/src/datastreams/writer.js +14 -2
  75. package/packages/dd-trace/src/encode/0.4.js +23 -2
  76. package/packages/dd-trace/src/encode/0.5.js +12 -1
  77. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +11 -1
  78. package/packages/dd-trace/src/encode/coverage-ci-visibility.js +20 -2
  79. package/packages/dd-trace/src/exporters/common/writer.js +17 -1
  80. package/packages/dd-trace/src/guardrails/index.js +3 -1
  81. package/packages/dd-trace/src/guardrails/telemetry.js +40 -3
  82. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +403 -0
  83. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +8 -395
  84. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +27 -0
  85. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +363 -0
  86. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +270 -0
  87. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/utils.js +10 -0
  88. package/packages/dd-trace/src/msgpack/chunk.js +33 -1
  89. package/packages/dd-trace/src/msgpack/index.js +6 -1
  90. package/packages/dd-trace/src/opentelemetry/span.js +5 -0
  91. package/packages/dd-trace/src/plugin_manager.js +16 -0
  92. package/packages/dd-trace/src/plugins/ci_plugin.js +147 -7
  93. package/packages/dd-trace/src/plugins/index.js +5 -0
  94. package/packages/dd-trace/src/plugins/util/test.js +4 -0
  95. package/packages/dd-trace/src/profiling/exporters/agent.js +28 -3
  96. package/packages/dd-trace/src/service-naming/schemas/v0/graphql.js +6 -0
  97. package/packages/dd-trace/src/service-naming/schemas/v1/graphql.js +8 -0
  98. package/vendor/dist/@apm-js-collab/code-transformer/index.js +5 -6
@@ -5,12 +5,10 @@ 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, getSignature } = require('./utils')
8
+ const { extractErrorIntoSpanEvent, getOperation, getSignature } = require('./utils')
9
9
 
10
10
  const legacyStorage = storage('legacy')
11
11
 
12
- const types = new Set(['query', 'mutation', 'subscription'])
13
-
14
12
  const iastResolveCh = dc.channel('apm:graphql:resolve:start')
15
13
  const resolverStartCh = dc.channel('datadog:graphql:resolver:start')
16
14
  const updateFieldCh = dc.channel('apm:graphql:resolve:updateField')
@@ -115,9 +113,11 @@ class GraphQLExecutePlugin extends TracingPlugin {
115
113
 
116
114
  ctx.collapse = this.config.collapse
117
115
 
116
+ const signature = getSignature(document, name, type, this.config.signature)
117
+
118
118
  const span = this.startSpan(this.operationName(), {
119
119
  service: this.config.service || this.serviceName(),
120
- resource: getSignature(document, name, type, this.config.signature),
120
+ resource: signature,
121
121
  kind: this.constructor.kind,
122
122
  type: this.constructor.type,
123
123
  meta: {
@@ -241,7 +241,7 @@ class GraphQLExecutePlugin extends TracingPlugin {
241
241
  if (res?.errors?.length) {
242
242
  span.setTag('error', res.errors[0])
243
243
  for (const err of res.errors) {
244
- extractErrorIntoSpanEvent(this._tracerConfig, span, err)
244
+ extractErrorIntoSpanEvent(this.config, span, err)
245
245
  }
246
246
  }
247
247
 
@@ -271,6 +271,9 @@ class GraphQLExecutePlugin extends TracingPlugin {
271
271
  const loc = this.config.source && document && fieldNode && fieldNode.loc
272
272
  const source = loc && document.slice(loc.start, loc.end)
273
273
 
274
+ // ctx form: startSpan sets field.currentStore = { ...activeStore, span }
275
+ // without entering it. Only the field's first resolver call runs in that
276
+ // store (isFirst check in wrapResolve); siblings use field.parentStore.
274
277
  const span = this.startSpan('graphql.resolve', {
275
278
  service: this.config.service,
276
279
  resource: `${fieldName}:${returnType}`,
@@ -283,7 +286,9 @@ class GraphQLExecutePlugin extends TracingPlugin {
283
286
  'graphql.field.type': baseTypeName,
284
287
  'graphql.source': source,
285
288
  },
286
- }, false)
289
+ }, field)
290
+
291
+ field.span = span
287
292
 
288
293
  if (fieldNode && this.config.variables && fieldNode.arguments) {
289
294
  const variables = this.config.variables(variableValues)
@@ -388,15 +393,22 @@ function wrapResolve (resolve) {
388
393
  pathString,
389
394
  collapsedKey: collapsedKey ?? pathString,
390
395
  span: null,
396
+ // Set by startResolveSpan; currentStore is used by the first resolver
397
+ // call only, siblings use parentStore (see the isFirst check below).
398
+ parentStore: null,
399
+ currentStore: null,
391
400
  }
392
401
  rootCtx.fields.set(fieldKey, field)
393
402
  }
394
403
 
395
404
  // Collapsed siblings still publish updateField (master's contract: one
396
405
  // publish per resolver call, even when the span is collapsed) and route
397
- // through callInAsyncScope so the abort signal stops them mid-flight.
406
+ // through callInAsyncScope so the abort signal stops them mid-flight. They
407
+ // run in the parent store, not field.currentStore: the first sibling's
408
+ // synchronous resolver already finished the shared graphql.resolve span, so
409
+ // re-entering its store would parent user spans to a closed span.
398
410
  if (!isFirst) {
399
- return callInAsyncScope(resolve, this, arguments, rootCtx.abortController, (err) => {
411
+ return callInAsyncScope(resolve, this, arguments, rootCtx.abortController, field.parentStore, (err) => {
400
412
  if (updateFieldCh.hasSubscribers) {
401
413
  updateFieldCh.publish({ rootCtx, field, error: err, pathString: field.pathString })
402
414
  }
@@ -406,9 +418,8 @@ function wrapResolve (resolve) {
406
418
  const executeSpan = rootCtx.executeSpan
407
419
  const startTime = executeSpan._getTime()
408
420
  const span = rootCtx.plugin.startResolveSpan(field, rootCtx, executeSpan, startTime)
409
- field.span = span
410
421
 
411
- return callInAsyncScope(resolve, this, arguments, rootCtx.abortController, (err, res) => {
422
+ return callInAsyncScope(resolve, this, arguments, rootCtx.abortController, field.currentStore, (err, res) => {
412
423
  const endTime = executeSpan._getTime()
413
424
  rootCtx.plugin.finishResolveSpan(span, field, err, res, endTime || startTime)
414
425
  if (updateFieldCh.hasSubscribers) {
@@ -446,16 +457,16 @@ function wrapFieldType (field) {
446
457
  wrapFields(unwrapped)
447
458
  }
448
459
 
449
- function callInAsyncScope (fn, thisArg, args, abortController, cb) {
450
- cb ??= () => {}
451
-
460
+ // Runs the resolver inside `store`, including any code after an internal
461
+ // `await`. A `.then()` the caller attaches afterward runs outside `store`.
462
+ function callInAsyncScope (fn, thisArg, args, abortController, store, cb) {
452
463
  if (abortController?.signal.aborted) {
453
464
  cb(null, null)
454
465
  throw new AbortError('Aborted')
455
466
  }
456
467
 
457
468
  try {
458
- const result = fn.apply(thisArg, args)
469
+ const result = legacyStorage.run(store, () => fn.apply(thisArg, args))
459
470
  if (typeof result?.then === 'function') {
460
471
  return result.then(
461
472
  res => { cb(null, res); return res },
@@ -658,17 +669,6 @@ function defaultFieldResolver (source, args, contextValue, info) {
658
669
  }
659
670
  }
660
671
 
661
- function getOperation (document, operationName) {
662
- if (!document || !Array.isArray(document.definitions)) return
663
-
664
- for (const definition of document.definitions) {
665
- if (definition && types.has(definition.operation) &&
666
- (!operationName || definition.name?.value === operationName)) {
667
- return definition
668
- }
669
- }
670
- }
671
-
672
672
  function addVariableTags (config, span, variableValues) {
673
673
  if (!variableValues || !config.variables) return
674
674
 
@@ -6,6 +6,7 @@ const CompositePlugin = require('../../dd-trace/src/plugins/composite')
6
6
  const log = require('../../dd-trace/src/log')
7
7
  const GraphQLExecutePlugin = require('./execute')
8
8
  const GraphQLParsePlugin = require('./parse')
9
+ const GraphQLRequestPlugin = require('./request')
9
10
  const GraphQLValidatePlugin = require('./validate')
10
11
 
11
12
  class GraphQLPlugin extends CompositePlugin {
@@ -14,6 +15,11 @@ class GraphQLPlugin extends CompositePlugin {
14
15
  return {
15
16
  execute: GraphQLExecutePlugin,
16
17
  parse: GraphQLParsePlugin,
18
+ // Top-level request span for drivers (mercurius) that funnel through a
19
+ // single entry point and parse/execute internally. graphql-js, apollo,
20
+ // and yoga produce no such channel, so the plugin simply never fires for
21
+ // them.
22
+ request: GraphQLRequestPlugin,
17
23
  validate: GraphQLValidatePlugin,
18
24
  // resolve plugin is absorbed into execute: per-field data is recorded
19
25
  // synchronously in wrapResolve, and all graphql.resolve spans are
@@ -30,6 +36,13 @@ class GraphQLPlugin extends CompositePlugin {
30
36
  }
31
37
 
32
38
  // config validator helpers
39
+ //
40
+ // `collapse`, `depth`, `variables`, and `errorExtensions` arrive pre-merged on
41
+ // `config`: plugin_manager seeds the `DD_TRACE_GRAPHQL_*` env values (already
42
+ // parsed to their declared type) as the base and a programmatic
43
+ // `tracer.use('graphql', …)` value overrides them. So these helpers only shape
44
+ // the merged value — coerce, validate, and turn `variables` into a filter — and
45
+ // never read the environment themselves.
33
46
 
34
47
  function validateConfig (config) {
35
48
  const collapse = config.collapse === undefined || !!config.collapse
@@ -41,6 +54,7 @@ function validateConfig (config) {
41
54
  // v5 counted collapsed list indices toward `depth`, so the same query reached a
42
55
  // different depth depending on `collapse`. v6 counts selection-set depth only.
43
56
  countListIndices: DD_MAJOR < 6 && collapse,
57
+ errorExtensions: getErrorExtensions(config),
44
58
  hooks: getHooks(config),
45
59
  }
46
60
  }
@@ -58,13 +72,21 @@ function getVariablesFilter (config) {
58
72
  if (typeof config.variables === 'function') {
59
73
  return config.variables
60
74
  } else if (Array.isArray(config.variables)) {
61
- return variables => pick(variables, config.variables)
75
+ return config.variables.length > 0 ? variables => pick(variables, config.variables) : null
62
76
  } else if (config.hasOwnProperty('variables')) {
63
77
  log.error('Expected `variables` to be an array or function.')
64
78
  }
65
79
  return null
66
80
  }
67
81
 
82
+ function getErrorExtensions (config) {
83
+ if (Array.isArray(config.errorExtensions)) {
84
+ return config.errorExtensions.length > 0 ? config.errorExtensions : undefined
85
+ } else if (config.hasOwnProperty('errorExtensions')) {
86
+ log.error('Expected `errorExtensions` to be an array.')
87
+ }
88
+ }
89
+
68
90
  const noop = () => {}
69
91
  const noopHooks = { execute: noop, parse: noop, validate: noop, resolve: undefined }
70
92
 
@@ -0,0 +1,104 @@
1
+ 'use strict'
2
+
3
+ const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
4
+ const { extractErrorIntoSpanEvent, getCachedRequestOperation } = require('./utils')
5
+
6
+ // Top-level GraphQL request span for drivers that funnel every operation
7
+ // through a single entry point but parse/validate/execute internally (mercurius
8
+ // today). It parents the `graphql.parse`/`graphql.validate`/`graphql.execute`
9
+ // (or JIT) sub-spans and carries the request text + operation name/type, which
10
+ // otherwise have no home when the query is JIT-compiled and `graphql.execute`
11
+ // never fires.
12
+ //
13
+ // The entry boundary only hands us the raw `source` (string or pre-parsed AST)
14
+ // and `operationName`; the parsed document — and therefore the precise
15
+ // operation signature — is only known once mercurius parses internally. On the
16
+ // cold path the `validate` sub-plugin refines the resource/operation tags onto
17
+ // this span via `ctx.currentStore.graphqlRequestSpan` once the document is
18
+ // available, so we never re-parse on the hot path. On the JIT warm path no
19
+ // sub-span fires, so we recover the same tags from the cache the cold path
20
+ // populated, keyed by source + operationName.
21
+ class GraphQLRequestPlugin extends TracingPlugin {
22
+ static id = 'graphql'
23
+ static operation = 'request'
24
+ static type = 'graphql'
25
+ static kind = 'server'
26
+ static prefix = 'tracing:orchestrion:mercurius:apm:graphql:request'
27
+
28
+ bindStart (ctx) {
29
+ // fastifyGraphQl(source, context, variables, operationName)
30
+ const source = ctx.arguments?.[0]
31
+ const operationName = ctx.arguments?.[3]
32
+
33
+ // `source` is the request text on the common path, but mercurius also
34
+ // accepts a pre-parsed document AST; only a string is the query text, and
35
+ // `graphql.source` carries only the text form.
36
+ const docSource = typeof source === 'string' ? source : undefined
37
+
38
+ // Warm (JIT-compiled) path: execute never fires, so recover the operation
39
+ // signature/type the cold path cached, keyed by source + operationName —
40
+ // by query text for a string, by document identity for a pre-parsed AST.
41
+ // Empty on the cold path — validate hasn't refined yet — where the request
42
+ // span is refined from the parsed document instead.
43
+ const cached = getCachedRequestOperation(source, operationName)
44
+
45
+ const span = this.startSpan(this.operationName({ id: 'request' }), {
46
+ service: this.config.service || this.serviceName(),
47
+ // The cached signature is the precise resource; otherwise provisional and
48
+ // refined by the validate sub-plugin once the document is parsed.
49
+ // `operationName` is the best name at the boundary; falls back to the
50
+ // operation signature once validate sees the document.
51
+ resource: cached?.signature || operationName || undefined,
52
+ kind: this.constructor.kind,
53
+ type: this.constructor.type,
54
+ meta: {
55
+ 'graphql.operation.type': cached?.type,
56
+ 'graphql.operation.name': cached?.name || operationName,
57
+ 'graphql.source': this.config.source ? docSource : undefined,
58
+ },
59
+ }, ctx)
60
+
61
+ // Hand the span, the requested operation name, and the raw source to the
62
+ // validate sub-plugin running inside this store so it can refine the
63
+ // resource + operation tags from the parsed document (validate is the first
64
+ // boundary that has it) and cache them keyed by the source the request
65
+ // boundary saw. The raw source is the cache key — validate sees mercurius's
66
+ // internally parsed document, not the caller's source, and for a pre-parsed
67
+ // AST the two are different objects.
68
+ ctx.currentStore.graphqlRequestSpan = span
69
+ ctx.currentStore.graphqlRequestOperationName = operationName
70
+ ctx.currentStore.graphqlRequestSource = source
71
+
72
+ return ctx.currentStore
73
+ }
74
+
75
+ asyncEnd (ctx) {
76
+ /* istanbul ignore next: currentStore is populated for the request lifecycle; activeSpan is base-plugin fallback. */
77
+ const span = ctx?.currentStore?.span || this.activeSpan
78
+ /* istanbul ignore if: startSpan always populates currentStore for the request lifecycle. */
79
+ if (!span) return ctx.parentStore
80
+
81
+ const result = ctx.result
82
+ if (result?.errors?.length) {
83
+ span.setTag('error', result.errors[0])
84
+ for (const error of result.errors) {
85
+ extractErrorIntoSpanEvent(this._tracerConfig, span, error)
86
+ }
87
+ }
88
+
89
+ span.finish()
90
+
91
+ return ctx.parentStore
92
+ }
93
+
94
+ error (ctx) {
95
+ /* istanbul ignore next: currentStore is populated for request errors; activeSpan is base-plugin fallback. */
96
+ const span = ctx?.currentStore?.span || this.activeSpan
97
+ /* istanbul ignore else: errors are only routed after the request span has started. */
98
+ if (span && ctx?.error) {
99
+ span.setTag('error', ctx.error)
100
+ }
101
+ }
102
+ }
103
+
104
+ module.exports = GraphQLRequestPlugin
@@ -1,5 +1,186 @@
1
1
  'use strict'
2
2
 
3
+ const { LRUCache } = require('../../../vendor/dist/lru-cache')
4
+
5
+ /**
6
+ * @typedef {{ signature?: string, type?: string, name?: string }} RequestOperation
7
+ */
8
+
9
+ const operationTypes = new Set(['query', 'mutation', 'subscription'])
10
+
11
+ // Mercurius funnels every operation through `fastifyGraphQl`, but the parsed
12
+ // document — and therefore the operation signature/type/name — is only known
13
+ // once mercurius parses internally. The top-level request span opens before
14
+ // that, and on the JIT warm path neither parse/validate nor execute fires, so
15
+ // the span would otherwise be left with only the provisional resource. The cold
16
+ // path caches the computed metadata; the request boundary reads it back on the
17
+ // warm path. Bounded so a flood of distinct queries can't grow it without limit.
18
+ //
19
+ // The key is the operation name plus the raw query text, not the source alone:
20
+ // mercurius keys its document LRU by source but compiles the JIT for a single
21
+ // `operationName`, and the compiled query then serves that operation for every
22
+ // later request that shares the source — regardless of the `operationName` those
23
+ // requests ask for. A source-only key would hand a warm request for operation B
24
+ // the metadata of whichever operation was cached last for that source (A),
25
+ // mislabeling the span. Operation names cannot contain a newline, so it is a
26
+ // safe separator that keeps the two parts from colliding.
27
+ const requestOperationCache = new LRUCache({ max: 500 })
28
+
29
+ // Mercurius also accepts a pre-parsed document AST as the source, which reaches
30
+ // the request boundary as an object rather than query text — so there is no
31
+ // string to key the LRU by. Mercurius keys its own document LRU by that source
32
+ // object's identity, and the same object reaches the boundary on the warm path,
33
+ // so a WeakMap keyed by the caller-owned document recovers the metadata without
34
+ // mutating the document and releases with it. The value carries the requested
35
+ // operation name so a JIT-only sibling selection is not handed another
36
+ // operation's metadata (same reason the string cache keys by operation name).
37
+ /** @type {WeakMap<object, Map<string | undefined, RequestOperation>>} */
38
+ const documentOperationCache = new WeakMap()
39
+
40
+ /**
41
+ * @param {string} source - The raw query text; the same key mercurius uses.
42
+ * @param {string | undefined} operationName - The requested operation name.
43
+ * @returns {string}
44
+ */
45
+ function requestOperationKey (source, operationName) {
46
+ return `${operationName ?? ''}\n${source}`
47
+ }
48
+
49
+ /**
50
+ * @param {unknown} source - Query text on the common path; a pre-parsed
51
+ * document AST otherwise. Any other shape (mercurius rejects it before
52
+ * execute) has no cache entry and yields undefined.
53
+ * @param {string | undefined} operationName - The requested operation name.
54
+ * @returns {RequestOperation | undefined}
55
+ */
56
+ function getCachedRequestOperation (source, operationName) {
57
+ if (typeof source === 'string') {
58
+ return requestOperationCache.get(requestOperationKey(source, operationName))
59
+ }
60
+ if (source === null || typeof source !== 'object') return
61
+ return documentOperationCache.get(source)?.get(operationName)
62
+ }
63
+
64
+ /**
65
+ * A string source keys the text LRU; a document AST keys the WeakMap. Any other
66
+ * shape has no usable key — mercurius rejects it before execute, so the warm
67
+ * path never reaches the request span for it either.
68
+ *
69
+ * @param {unknown} source
70
+ * @returns {source is string | object}
71
+ */
72
+ function isCacheableSource (source) {
73
+ return typeof source === 'string' || (source !== null && typeof source === 'object')
74
+ }
75
+
76
+ /**
77
+ * Select the operation definition matching `operationName`, or the first one
78
+ * when no name is given (graphql/mercurius default selection).
79
+ *
80
+ * @param {import('graphql').DocumentNode | undefined} document
81
+ * @param {string | undefined} operationName
82
+ * @returns {import('graphql').OperationDefinitionNode | undefined}
83
+ */
84
+ function getOperation (document, operationName) {
85
+ /* istanbul ignore if: validate/execute only call this with a parsed GraphQL document. */
86
+ if (!document || !Array.isArray(document.definitions)) return
87
+
88
+ for (const definition of document.definitions) {
89
+ if (operationTypes.has(definition?.operation) &&
90
+ (!operationName || definition.name?.value === operationName)) {
91
+ return definition
92
+ }
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Refine the top-level graphql.request span (mercurius) from the parsed
98
+ * document and cache the metadata so the JIT warm path — where no sub-span
99
+ * fires — can recover the same tags at the request boundary.
100
+ *
101
+ * This runs at the first boundary that has the document (validate on the cold
102
+ * path, which also precedes a pre-execute validation failure). It is idempotent
103
+ * across the later execute boundary via the `ddRequestRefined` flag, and a
104
+ * no-op for graphql-js/apollo/yoga, which never open a request span.
105
+ *
106
+ * Every named operation in the document is cached, not just the selected one:
107
+ * a multi-operation document parses once, and a later request may select a
108
+ * sibling operation that mercurius then serves exclusively through its JIT path
109
+ * (no execute span), so its metadata has to be ready before that happens.
110
+ *
111
+ * @param {import('../../dd-trace/src/opentracing/span') | undefined} requestSpan
112
+ * @param {import('graphql').DocumentNode | undefined} document
113
+ * @param {unknown} requestSource - The raw source the request boundary saw:
114
+ * query text on the common path, a pre-parsed document AST otherwise. The
115
+ * cache is keyed by it, not by the parsed document, so the request boundary
116
+ * recovers the metadata on the warm path from the same value mercurius keys
117
+ * its own document LRU by. Any other shape has no usable key and is not
118
+ * cached (the warm path never reaches this span for it either).
119
+ * @param {string | undefined} operationName - The requested operation name.
120
+ * @param {boolean} calculateSignature - The graphql plugin's `signature` config.
121
+ */
122
+ function refineRequestSpan (requestSpan, document, requestSource, operationName, calculateSignature) {
123
+ /* istanbul ignore if: validate only refines after the request span and parsed document exist. */
124
+ if (!requestSpan || requestSpan.ddRequestRefined || !document) return
125
+ requestSpan.ddRequestRefined = true
126
+
127
+ const operation = getOperation(document, operationName)
128
+ const type = operation?.operation
129
+ const name = operation?.name?.value
130
+ const signature = getSignature(document, name, type, calculateSignature)
131
+
132
+ if (signature) requestSpan.setTag('resource.name', signature)
133
+ if (type) requestSpan.setTag('graphql.operation.type', type)
134
+ if (name) requestSpan.setTag('graphql.operation.name', name)
135
+
136
+ if (!isCacheableSource(requestSource)) return
137
+
138
+ // Cache the selected operation under the requested name (undefined selects
139
+ // the document's first operation, so it shares the entry with that name).
140
+ cacheRequestOperation(requestSource, operationName, { signature, type, name })
141
+
142
+ // Cache every named operation so a JIT-only sibling selection is labeled from
143
+ // this single parse instead of falling back to a bare operation name.
144
+ for (const definition of document.definitions) {
145
+ const definitionName = definition?.name?.value
146
+ if (definitionName === undefined || !operationTypes.has(definition.operation)) continue
147
+ if (definitionName === operationName) continue
148
+
149
+ cacheRequestOperation(requestSource, definitionName, {
150
+ signature: getSignature(document, definitionName, definition.operation, calculateSignature),
151
+ type: definition.operation,
152
+ name: definitionName,
153
+ })
154
+ }
155
+ }
156
+
157
+ /**
158
+ * @param {string | import('graphql').DocumentNode} source - Query text keys the
159
+ * text LRU; a caller-owned document AST keys the WeakMap (never mutated).
160
+ * @param {string | undefined} operationName - The requested operation name.
161
+ * @param {RequestOperation} operation
162
+ */
163
+ function cacheRequestOperation (source, operationName, operation) {
164
+ if (typeof source === 'string') {
165
+ requestOperationCache.set(requestOperationKey(source, operationName), operation)
166
+ return
167
+ }
168
+
169
+ let operations = documentOperationCache.get(source)
170
+ if (operations === undefined) {
171
+ operations = new Map()
172
+ documentOperationCache.set(source, operations)
173
+ }
174
+ operations.set(operationName, operation)
175
+ }
176
+
177
+ /**
178
+ * @param {{ errorExtensions?: string[] }} config Resolved plugin config; `errorExtensions` lists the
179
+ * GraphQL error `extensions` keys to copy onto the span event.
180
+ * @param {import('../../dd-trace/src/opentracing/span')} span
181
+ * @param {{ name?: string, message?: string, stack?: string, locations?: Array<{ line: number, column: number }>,
182
+ * path?: Array<string|number>, extensions?: Record<string, unknown> }} exc
183
+ */
3
184
  function extractErrorIntoSpanEvent (config, span, exc) {
4
185
  const attributes = {}
5
186
 
@@ -29,8 +210,8 @@ function extractErrorIntoSpanEvent (config, span, exc) {
29
210
  attributes.message = exc.message
30
211
  }
31
212
 
32
- if (config.DD_TRACE_GRAPHQL_ERROR_EXTENSIONS) {
33
- for (const ext of config.DD_TRACE_GRAPHQL_ERROR_EXTENSIONS) {
213
+ if (config.errorExtensions) {
214
+ for (const ext of config.errorExtensions) {
34
215
  if (exc.extensions?.[ext]) {
35
216
  const value = exc.extensions[ext]
36
217
 
@@ -77,5 +258,8 @@ function getSignature (document, operationName, operationType, calculate) {
77
258
 
78
259
  module.exports = {
79
260
  extractErrorIntoSpanEvent,
261
+ getCachedRequestOperation,
262
+ getOperation,
80
263
  getSignature,
264
+ refineRequestSpan,
81
265
  }
@@ -1,8 +1,11 @@
1
1
  'use strict'
2
2
 
3
+ const { storage } = require('../../datadog-core')
3
4
  const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
4
5
  const GraphQLParsePlugin = require('./parse')
5
- const { extractErrorIntoSpanEvent } = require('./utils')
6
+ const { extractErrorIntoSpanEvent, refineRequestSpan } = require('./utils')
7
+
8
+ const legacyStorage = storage('legacy')
6
9
 
7
10
  class GraphQLValidatePlugin extends TracingPlugin {
8
11
  static id = 'graphql'
@@ -15,6 +18,26 @@ class GraphQLValidatePlugin extends TracingPlugin {
15
18
  const docSource = document ? GraphQLParsePlugin.documentSources.get(document) : undefined
16
19
  const source = this.config.source && document && docSource
17
20
 
21
+ // Refine the top-level graphql.request span (mercurius) from the parsed
22
+ // document. validate is the first boundary that has it and precedes both
23
+ // execute and any pre-execute rejection (unknown field, GET mutation), so a
24
+ // failing request still ends up with a resource and operation tags. The
25
+ // request span, its operation name, and the raw source ride the active
26
+ // store the request boundary entered (validate's own `ctx.currentStore` is
27
+ // not populated yet). The cache is keyed by that raw source, not the parsed
28
+ // document — for a pre-parsed AST mercurius validates a structuredClone, so
29
+ // the document here is a different object from the one the boundary saw and
30
+ // recovers on the warm path. No-op for graphql-js/apollo/yoga, which never
31
+ // open a request span.
32
+ const requestStore = legacyStorage.getStore()
33
+ refineRequestSpan(
34
+ requestStore?.graphqlRequestSpan,
35
+ document,
36
+ requestStore?.graphqlRequestSource,
37
+ requestStore?.graphqlRequestOperationName,
38
+ this.config.signature
39
+ )
40
+
18
41
  this.startSpan('graphql.validate', {
19
42
  service: this.config.service,
20
43
  type: 'graphql',
@@ -38,7 +61,7 @@ class GraphQLValidatePlugin extends TracingPlugin {
38
61
  if (errors?.length) {
39
62
  span.setTag('error', errors[0])
40
63
  for (const err of errors) {
41
- extractErrorIntoSpanEvent(this._tracerConfig, span, err)
64
+ extractErrorIntoSpanEvent(this.config, span, err)
42
65
  }
43
66
  }
44
67
 
@@ -61,15 +61,6 @@ const isJestWorker = !!getEnvironmentVariable('JEST_WORKER_ID')
61
61
  // https://github.com/facebook/jest/blob/d6ad15b0f88a05816c2fe034dd6900d28315d570/packages/jest-worker/src/types.ts#L38
62
62
  const CHILD_MESSAGE_END = 2
63
63
 
64
- function withTimeout (promise, timeoutMs) {
65
- return new Promise(resolve => {
66
- realSetTimeout(resolve, timeoutMs)
67
-
68
- // Also resolve if the original promise resolves
69
- promise.then(resolve)
70
- })
71
- }
72
-
73
64
  class JestPlugin extends CiPlugin {
74
65
  static id = 'jest'
75
66
 
@@ -451,6 +442,7 @@ class JestPlugin extends CiPlugin {
451
442
  isAtrRetry,
452
443
  finalStatus,
453
444
  earlyFlakeAbortReason,
445
+ promises,
454
446
  }) => {
455
447
  span.setTag(TEST_STATUS, status)
456
448
  if (finalStatus) {
@@ -481,12 +473,33 @@ class JestPlugin extends CiPlugin {
481
473
  this.getTestTelemetryTags(span)
482
474
  )
483
475
 
484
- span.finish()
485
- finishAllTraceSpans(span)
486
- this.activeTestSpan = null
476
+ const finish = () => {
477
+ span.finish()
478
+ finishAllTraceSpans(span)
479
+ this.activeTestSpan = null
480
+ }
481
+
482
+ if (finalStatus) {
483
+ if (promises?.hitBreakpointPromise) {
484
+ finish()
485
+ this.cancelDiBreakpointHitWait()
486
+ return
487
+ }
488
+ if (promises && this.diBreakpointHitPromise) {
489
+ promises.hitBreakpointPromise = this.waitForPreparedDiBreakpointHit().then(finish)
490
+ return
491
+ }
492
+ finish()
493
+ this.cancelDiBreakpointHitWait()
494
+ return
495
+ }
496
+ finish()
497
+ if (status === 'fail' && promises?.hitBreakpointPromise) {
498
+ this.prepareDiBreakpointHitWait()
499
+ }
487
500
  })
488
501
 
489
- this.addSub('ci:jest:test:err', ({ span, error, shouldSetProbe, promises }) => {
502
+ this.addSub('ci:jest:test:err', ({ span, error, shouldSetProbe, shouldWaitForHitProbe, promises }) => {
490
503
  if (error && span) {
491
504
  span.setTag(TEST_STATUS, 'fail')
492
505
  span.setTag('error', getFormattedError(error, this.repositoryRoot))
@@ -494,8 +507,11 @@ class JestPlugin extends CiPlugin {
494
507
  const probeInformation = this.addDiProbe(error)
495
508
  if (probeInformation) {
496
509
  const { setProbePromise } = probeInformation
497
- promises.isProbeReady = withTimeout(setProbePromise, 2000)
510
+ this.prepareDiBreakpointHitWait()
511
+ promises.isProbeReady = this.waitForDiOperation(setProbePromise)
498
512
  }
513
+ } else if (shouldWaitForHitProbe && this.di) {
514
+ promises.hitBreakpointPromise = this.waitForDiBreakpointHits()
499
515
  }
500
516
  }
501
517
  })