dd-trace 5.110.0 → 5.112.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 (164) hide show
  1. package/LICENSE-3rdparty.csv +1 -2
  2. package/README.md +13 -8
  3. package/ci/init.js +21 -2
  4. package/ci/vitest-no-worker-init-setup.mjs +430 -0
  5. package/ext/tags.js +2 -0
  6. package/index.d.ts +34 -1
  7. package/initialize.mjs +5 -6
  8. package/loader-hook.mjs +76 -55
  9. package/package.json +37 -34
  10. package/packages/datadog-instrumentations/src/ai.js +45 -0
  11. package/packages/datadog-instrumentations/src/apollo-server.js +5 -13
  12. package/packages/datadog-instrumentations/src/child_process.js +3 -3
  13. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +587 -0
  14. package/packages/datadog-instrumentations/src/cucumber.js +102 -43
  15. package/packages/datadog-instrumentations/src/cypress-config.js +11 -3
  16. package/packages/datadog-instrumentations/src/fastify.js +27 -8
  17. package/packages/datadog-instrumentations/src/fs.js +8 -6
  18. package/packages/datadog-instrumentations/src/graphql.js +26 -495
  19. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  20. package/packages/datadog-instrumentations/src/helpers/instrument.js +7 -0
  21. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +6 -6
  22. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/claude-agent-sdk.js +17 -0
  23. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql.js +201 -0
  24. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -0
  25. package/packages/datadog-instrumentations/src/jest.js +713 -63
  26. package/packages/datadog-instrumentations/src/mocha/main.js +24 -3
  27. package/packages/datadog-instrumentations/src/mocha/utils.js +128 -22
  28. package/packages/datadog-instrumentations/src/mocha/worker.js +13 -0
  29. package/packages/datadog-instrumentations/src/mongodb.js +74 -0
  30. package/packages/datadog-instrumentations/src/mongoose.js +13 -2
  31. package/packages/datadog-instrumentations/src/playwright.js +13 -9
  32. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +1297 -0
  33. package/packages/datadog-instrumentations/src/vitest-main.js +1594 -0
  34. package/packages/datadog-instrumentations/src/vitest-util.js +254 -0
  35. package/packages/datadog-instrumentations/src/vitest-worker.js +823 -0
  36. package/packages/datadog-instrumentations/src/vitest.js +11 -1855
  37. package/packages/datadog-plugin-ai/src/index.js +1 -1
  38. package/packages/datadog-plugin-ai/src/tracing.js +66 -3
  39. package/packages/datadog-plugin-ai/src/utils.js +17 -4
  40. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/client.js +2 -2
  41. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +19 -10
  42. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +1 -0
  43. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +47 -11
  44. package/packages/datadog-plugin-child_process/src/index.js +13 -2
  45. package/packages/datadog-plugin-claude-agent-sdk/src/index.js +31 -0
  46. package/packages/datadog-plugin-claude-agent-sdk/src/tracing.js +107 -0
  47. package/packages/datadog-plugin-claude-agent-sdk/src/util.js +15 -0
  48. package/packages/datadog-plugin-cucumber/src/index.js +15 -24
  49. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +360 -14
  50. package/packages/datadog-plugin-cypress/src/index.js +47 -2
  51. package/packages/datadog-plugin-cypress/src/plugin.js +1 -0
  52. package/packages/datadog-plugin-cypress/src/support.js +45 -1
  53. package/packages/datadog-plugin-express/src/code_origin.js +1 -1
  54. package/packages/datadog-plugin-fastify/src/code_origin.js +1 -1
  55. package/packages/datadog-plugin-graphql/src/execute.js +639 -12
  56. package/packages/datadog-plugin-graphql/src/index.js +37 -9
  57. package/packages/datadog-plugin-graphql/src/parse.js +24 -4
  58. package/packages/datadog-plugin-graphql/src/utils.js +9 -2
  59. package/packages/datadog-plugin-graphql/src/validate.js +17 -6
  60. package/packages/datadog-plugin-http/src/index.js +6 -8
  61. package/packages/datadog-plugin-jest/src/index.js +31 -15
  62. package/packages/datadog-plugin-mocha/src/index.js +40 -15
  63. package/packages/datadog-plugin-mongodb-core/src/bulk-write.js +43 -0
  64. package/packages/datadog-plugin-mongodb-core/src/index.js +8 -443
  65. package/packages/datadog-plugin-mongodb-core/src/query.js +452 -0
  66. package/packages/datadog-plugin-openai/src/services.js +2 -2
  67. package/packages/datadog-plugin-playwright/src/index.js +4 -3
  68. package/packages/datadog-plugin-vitest/src/index.js +120 -72
  69. package/packages/datadog-shimmer/src/shimmer.js +37 -16
  70. package/packages/dd-trace/src/aiguard/index.js +9 -14
  71. package/packages/dd-trace/src/aiguard/integrations/index.js +34 -0
  72. package/packages/dd-trace/src/aiguard/integrations/openai.js +47 -33
  73. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +42 -28
  74. package/packages/dd-trace/src/aiguard/sdk.js +2 -2
  75. package/packages/dd-trace/src/appsec/api_security/sampler.js +3 -3
  76. package/packages/dd-trace/src/appsec/channels.js +3 -1
  77. package/packages/dd-trace/src/appsec/downstream_requests.js +4 -4
  78. package/packages/dd-trace/src/appsec/graphql.js +14 -11
  79. package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +23 -23
  80. package/packages/dd-trace/src/appsec/iast/security-controls/index.js +2 -2
  81. package/packages/dd-trace/src/appsec/iast/telemetry/index.js +2 -1
  82. package/packages/dd-trace/src/appsec/index.js +10 -1
  83. package/packages/dd-trace/src/appsec/lambda.js +135 -0
  84. package/packages/dd-trace/src/appsec/reporter.js +49 -10
  85. package/packages/dd-trace/src/appsec/telemetry/index.js +1 -1
  86. package/packages/dd-trace/src/appsec/waf/index.js +16 -4
  87. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +4 -4
  88. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +26 -1
  89. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +115 -24
  90. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +21 -6
  91. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +5 -2
  92. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +1 -1
  93. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +1 -1
  94. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +6 -2
  95. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +1 -1
  96. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +130 -20
  97. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +2 -2
  98. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +17 -0
  99. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +47 -26
  100. package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +2 -2
  101. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +64 -53
  102. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +3 -3
  103. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +156 -0
  104. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +23 -5
  105. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +128 -0
  106. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +287 -0
  107. package/packages/dd-trace/src/config/defaults.js +3 -2
  108. package/packages/dd-trace/src/config/generated-config-types.d.ts +71 -57
  109. package/packages/dd-trace/src/config/helper.js +1 -0
  110. package/packages/dd-trace/src/config/index.js +35 -22
  111. package/packages/dd-trace/src/config/major-overrides.js +4 -2
  112. package/packages/dd-trace/src/config/remote_config.js +1 -1
  113. package/packages/dd-trace/src/config/supported-configurations.json +127 -56
  114. package/packages/dd-trace/src/constants.js +7 -0
  115. package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -2
  116. package/packages/dd-trace/src/datastreams/processor.js +11 -3
  117. package/packages/dd-trace/src/exporters/agent/index.js +1 -1
  118. package/packages/dd-trace/src/exporters/agentless/writer.js +4 -4
  119. package/packages/dd-trace/src/llmobs/index.js +1 -1
  120. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +403 -0
  121. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +8 -395
  122. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +27 -0
  123. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +363 -0
  124. package/packages/dd-trace/src/llmobs/plugins/base.js +4 -4
  125. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +270 -0
  126. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/utils.js +10 -0
  127. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  128. package/packages/dd-trace/src/llmobs/sdk.js +3 -3
  129. package/packages/dd-trace/src/llmobs/span_processor.js +1 -1
  130. package/packages/dd-trace/src/llmobs/tagger.js +3 -3
  131. package/packages/dd-trace/src/llmobs/writers/base.js +1 -1
  132. package/packages/dd-trace/src/opentelemetry/metrics/index.js +54 -5
  133. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +40 -0
  134. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +169 -0
  135. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +38 -39
  136. package/packages/dd-trace/src/opentelemetry/span.js +5 -0
  137. package/packages/dd-trace/src/opentelemetry/trace/index.js +4 -0
  138. package/packages/dd-trace/src/opentelemetry/tracer_provider.js +1 -1
  139. package/packages/dd-trace/src/opentracing/tracer.js +6 -1
  140. package/packages/dd-trace/src/plugin_manager.js +23 -7
  141. package/packages/dd-trace/src/plugins/ci_plugin.js +159 -10
  142. package/packages/dd-trace/src/plugins/index.js +2 -0
  143. package/packages/dd-trace/src/plugins/util/llm.js +6 -1
  144. package/packages/dd-trace/src/plugins/util/test.js +15 -9
  145. package/packages/dd-trace/src/profiling/exporter_cli.js +2 -2
  146. package/packages/dd-trace/src/profiling/exporters/agent.js +28 -3
  147. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +3 -3
  148. package/packages/dd-trace/src/profiling/profilers/wall.js +1 -1
  149. package/packages/dd-trace/src/proxy.js +8 -8
  150. package/packages/dd-trace/src/span_processor.js +5 -6
  151. package/packages/dd-trace/src/span_stats.js +96 -78
  152. package/packages/dd-trace/src/startup-log.js +2 -1
  153. package/packages/dd-trace/src/telemetry/dependencies.js +1 -1
  154. package/packages/dd-trace/src/telemetry/endpoints.js +6 -4
  155. package/packages/dd-trace/src/telemetry/index.js +2 -2
  156. package/packages/dd-trace/src/telemetry/logs/index.js +1 -1
  157. package/packages/dd-trace/src/telemetry/send-data.js +8 -8
  158. package/packages/dd-trace/src/telemetry/session-propagation.js +1 -1
  159. package/packages/dd-trace/src/telemetry/telemetry.js +9 -9
  160. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  161. package/vendor/dist/protobufjs/index.js +1 -1
  162. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  163. package/vendor/dist/shell-quote/index.js +1 -1
  164. package/packages/datadog-plugin-graphql/src/resolve.js +0 -170
@@ -1,21 +1,117 @@
1
1
  'use strict'
2
2
 
3
+ const dc = require('dc-polyfill')
4
+
5
+ const { storage } = require('../../datadog-core')
3
6
  const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
7
+ const GraphQLParsePlugin = require('./parse')
4
8
  const { extractErrorIntoSpanEvent, getSignature } = require('./utils')
5
9
 
10
+ const legacyStorage = storage('legacy')
11
+
12
+ const types = new Set(['query', 'mutation', 'subscription'])
13
+
14
+ const iastResolveCh = dc.channel('apm:graphql:resolve:start')
15
+ const resolverStartCh = dc.channel('datadog:graphql:resolver:start')
16
+ const updateFieldCh = dc.channel('apm:graphql:resolve:updateField')
17
+
18
+ // AppSec/WAF abort gate. Published synchronously from bindStart with a
19
+ // payload carrying the abortController; subscribers that call
20
+ // `payload.abortController.abort()` signal a pre-execute abort. bindStart
21
+ // observes the aborted signal by replacing `ctx.arguments[0]` with a Proxy
22
+ // whose getters throw AbortError — the orchestrion-emitted wrapper's
23
+ // `try { __apm$traced() } catch { ...; throw err }` block then propagates
24
+ // the AbortError to the caller of graphql.execute.
25
+ const startExecuteCh = dc.channel('apm:graphql:execute:start')
26
+
27
+ const contexts = new WeakMap()
28
+ const instrumentedArgs = new WeakSet()
29
+
30
+ const patchedResolvers = new WeakSet()
31
+ const patchedTypes = new WeakSet()
32
+
33
+ // Module-level fast path: skip the resolver-side WeakMap lookup entirely
34
+ // when depth=0 disables resolver instrumentation.
35
+ let depthDisabled = false
36
+
37
+ class AbortError extends Error {
38
+ constructor (message) {
39
+ super(message)
40
+ this.name = 'AbortError'
41
+ }
42
+ }
43
+
6
44
  class GraphQLExecutePlugin extends TracingPlugin {
7
45
  static id = 'graphql'
8
46
  static operation = 'execute'
9
47
  static type = 'graphql'
10
48
  static kind = 'server'
49
+ static prefix = 'tracing:orchestrion:graphql:apm:graphql:execute'
50
+
51
+ // @graphql-tools/executor (used by graphql-yoga) emits on a different channel
52
+ // prefix because the module name differs. Subscribe to both so Yoga execution
53
+ // produces graphql.execute spans.
54
+ static extraPrefixes = [
55
+ 'tracing:orchestrion:@graphql-tools/executor:apm:graphql:execute',
56
+ ]
57
+
58
+ /**
59
+ * @param {{ depth?: number }} config
60
+ */
61
+ configure (config) {
62
+ super.configure(config)
63
+ depthDisabled = config.depth === 0
64
+ }
65
+
66
+ addTraceSubs () {
67
+ super.addTraceSubs()
68
+
69
+ for (const prefix of this.constructor.extraPrefixes) {
70
+ const events = ['start', 'end', 'asyncStart', 'asyncEnd', 'error', 'finish']
71
+
72
+ for (const event of events) {
73
+ const bindName = `bind${event.charAt(0).toUpperCase()}${event.slice(1)}`
74
+
75
+ if (this[event]) {
76
+ this.addSub(`${prefix}:${event}`, message => {
77
+ this[event](message)
78
+ })
79
+ }
80
+
81
+ if (this[bindName]) {
82
+ this.addBind(`${prefix}:${event}`, message => this[bindName](message))
83
+ }
84
+ }
85
+ }
86
+ }
11
87
 
12
88
  bindStart (ctx) {
13
- const { operation, args, docSource } = ctx
89
+ const rawArgs = ctx.arguments
90
+ const objectForm = isObjectForm(rawArgs)
91
+ const args = readArgs(rawArgs, objectForm)
92
+
93
+ // Re-entrant execute() short-circuit (yoga's normalizedExecutor calls
94
+ // execute internally with the same arguments object — without this we'd
95
+ // double-span). The contextValue check catches object contexts; the args
96
+ // check also catches primitive contexts.
97
+ if (instrumentedArgs.has(rawArgs?.[0])) {
98
+ ctx.ddSkipped = true
99
+ return ctx.currentStore
100
+ }
101
+
102
+ const { contextValue } = args
103
+ if (contextValue && typeof contextValue === 'object' && contexts.has(contextValue)) {
104
+ ctx.ddSkipped = true
105
+ return ctx.currentStore
106
+ }
14
107
 
15
- const type = operation && operation.operation
16
- const name = operation && operation.name && operation.name.value
17
108
  const document = args.document
18
- const source = this.config.source && document && docSource
109
+ const docSource = document ? GraphQLParsePlugin.documentSources.get(document) : undefined
110
+ const operation = getOperation(document, args.operationName)
111
+
112
+ const type = operation?.operation
113
+ const name = operation?.name?.value
114
+ const source = this.config.source && docSource
19
115
 
20
116
  ctx.collapse = this.config.collapse
21
117
 
@@ -33,25 +129,556 @@ class GraphQLExecutePlugin extends TracingPlugin {
33
129
 
34
130
  addVariableTags(this.config, span, args.variableValues)
35
131
 
132
+ const abortController = new AbortController()
133
+
134
+ // AppSec/WAF synchronous-abort gate. Publish before any resolver-wrapping
135
+ // work — if a subscriber aborts, none of that work matters because we'll
136
+ // make execute's body throw AbortError before it reaches any resolvers.
137
+ // bindStart runs as a bindStore transform on
138
+ // tracing:orchestrion:graphql:apm:graphql:execute:start, which fires
139
+ // BEFORE the orchestrion-emitted wrapper publishes its own :start and
140
+ // BEFORE the wrapped fn runs. The subscriber gets the abortController
141
+ // synchronously; on return we observe `signal.aborted` and act.
142
+ if (startExecuteCh.hasSubscribers) {
143
+ startExecuteCh.publish({ abortController, args })
144
+ if (abortController.signal.aborted) {
145
+ // Replace ctx.arguments[0] with a Proxy that throws AbortError on any
146
+ // property access. The orchestrion wrapper calls
147
+ // `__apm$wrapped.apply(this, ctx.arguments)`; graphql.execute's body
148
+ // begins with `const { schema, document, ... } = args` so the
149
+ // destructure triggers the trap immediately. The wrapper catches and
150
+ // rethrows, propagating AbortError to graphql.execute's caller.
151
+ ctx.arguments[0] = new Proxy({}, {
152
+ get () { throw new AbortError('Aborted') },
153
+ has () { throw new AbortError('Aborted') },
154
+ })
155
+ ctx.ddAborted = true
156
+ return ctx.currentStore
157
+ }
158
+ }
159
+
160
+ ctx.ddArgs = setWrappedFieldResolver(rawArgs, args, objectForm, defaultFieldResolver)
161
+ if (ctx.ddArgs && typeof ctx.ddArgs === 'object') {
162
+ instrumentedArgs.add(ctx.ddArgs)
163
+ ctx.ddInstrumentedArgs = ctx.ddArgs
164
+ }
165
+
166
+ const schema = args.schema
167
+ if (schema) {
168
+ wrapFields(schema._queryType)
169
+ wrapFields(schema._mutationType)
170
+ wrapFields(schema._subscriptionType)
171
+ }
172
+
173
+ const rootCtx = {
174
+ source: docSource,
175
+ config: this.config,
176
+ fields: new Map(),
177
+ pathCache: new Map(),
178
+ collapsedPathCache: this.config.collapse ? { byPath: new Map(), byParent: new Map() } : undefined,
179
+ abortController,
180
+ executeSpan: span,
181
+ plugin: this,
182
+ }
183
+ ctx.ddRootCtx = rootCtx
184
+ if (isWeakMapKey(contextValue)) {
185
+ contexts.set(contextValue, rootCtx)
186
+ ctx.ddContextValue = contextValue
187
+ } else {
188
+ // Primitive / non-keyable contextValue: stash rootCtx on the
189
+ // orchestrion-scoped store that runStores enters for execute. Wrapped
190
+ // resolvers read it via legacyStorage.getStore() and it unwinds with the
191
+ // frame — no enterWith store that leaks past execute.
192
+ ctx.currentStore.graphqlRootCtx = rootCtx
193
+ }
194
+
36
195
  return ctx.currentStore
37
196
  }
38
197
 
39
- finish (ctx) {
40
- const { res, args } = ctx
198
+ end (ctx) {
199
+ if (ctx.ddSkipped) return ctx.parentStore
200
+
201
+ const span = ctx?.currentStore?.span || this.activeSpan
202
+ if (!span) return
203
+
204
+ // Synchronous execute() throw (e.g. execute(null, doc)) — error handler
205
+ // already tagged the span, just finish it.
206
+ if (ctx.error) {
207
+ this.#drain(ctx, span)
208
+ return ctx.parentStore
209
+ }
210
+
211
+ const result = ctx.result
212
+
213
+ if (typeof result?.then === 'function') {
214
+ result.then(
215
+ (res) => this.#finishSpan(ctx, span, res),
216
+ (err) => {
217
+ span.setTag('error', err)
218
+ this.#drain(ctx, span)
219
+ }
220
+ )
221
+ } else {
222
+ this.#finishSpan(ctx, span, result)
223
+ }
224
+
225
+ return ctx.parentStore
226
+ }
227
+
228
+ error (ctx) {
229
+ // Pre-execute WAF abort isn't an error condition — opSpan.error must
230
+ // stay 0 per master's contract.
231
+ if (ctx.ddAborted) return
41
232
  const span = ctx?.currentStore?.span || this.activeSpan
42
- this.config.hooks.execute(span, args, res)
43
- if (res?.errors) {
233
+ if (span && ctx?.error) {
234
+ span.setTag('error', ctx.error)
235
+ }
236
+ }
237
+
238
+ #finishSpan (ctx, span, res) {
239
+ this.config.hooks.execute(span, ctx.ddArgs, res)
240
+
241
+ if (res?.errors?.length) {
242
+ span.setTag('error', res.errors[0])
44
243
  for (const err of res.errors) {
45
- extractErrorIntoSpanEvent(this._tracerConfig, span, err)
244
+ extractErrorIntoSpanEvent(this.config, span, err)
46
245
  }
47
246
  }
48
- super.finish(ctx)
49
247
 
50
- return ctx.parentStore
248
+ this.#drain(ctx, span)
249
+ }
250
+
251
+ #drain (ctx, span) {
252
+ span.finish()
253
+ if (ctx.ddContextValue) {
254
+ contexts.delete(ctx.ddContextValue)
255
+ }
256
+ if (ctx.ddInstrumentedArgs) {
257
+ instrumentedArgs.delete(ctx.ddInstrumentedArgs)
258
+ }
259
+ }
260
+
261
+ // Public — called from wrapResolve (free function, crosses class boundary).
262
+ // Resolve-span creation is inline at first-encounter; deferring to a batch
263
+ // produces a bursty encoder stall when many spans finish together.
264
+ startResolveSpan (field, rootCtx, executeSpan, startTime) {
265
+ const { fieldNode, fieldName, returnType, baseTypeName, variableValues, collapsedKey } = field
266
+
267
+ const parent = getParentField(rootCtx, field)
268
+ const childOf = parent?.span || executeSpan
269
+
270
+ const document = rootCtx.source
271
+ const loc = this.config.source && document && fieldNode && fieldNode.loc
272
+ const source = loc && document.slice(loc.start, loc.end)
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.
277
+ const span = this.startSpan('graphql.resolve', {
278
+ service: this.config.service,
279
+ resource: `${fieldName}:${returnType}`,
280
+ childOf,
281
+ type: 'graphql',
282
+ startTime,
283
+ meta: {
284
+ 'graphql.field.name': fieldName,
285
+ 'graphql.field.path': collapsedKey,
286
+ 'graphql.field.type': baseTypeName,
287
+ 'graphql.source': source,
288
+ },
289
+ }, field)
290
+
291
+ field.span = span
292
+
293
+ if (fieldNode && this.config.variables && fieldNode.arguments) {
294
+ const variables = this.config.variables(variableValues)
295
+ for (const arg of fieldNode.arguments) {
296
+ if (arg.value?.name && arg.value.kind === 'Variable' && variables[arg.value.name.value]) {
297
+ const name = arg.value.name.value
298
+ span.setTag(`graphql.variables.${name}`, variables[name])
299
+ }
300
+ }
301
+ }
302
+
303
+ return span
304
+ }
305
+
306
+ // Public — called from wrapResolve. endTime reflects when the resolver
307
+ // actually completed, not when the field record was created.
308
+ finishResolveSpan (span, field, error, result, endTime) {
309
+ if (error) span.setTag('error', error)
310
+
311
+ if (this.config.hooks.resolve) {
312
+ this.config.hooks.resolve(span, {
313
+ fieldName: field.fieldName,
314
+ path: field.pathString,
315
+ error: error || null,
316
+ // Any thenable from any realm — keep undefined so the hook doesn't
317
+ // accidentally see the unresolved promise.
318
+ result: typeof result?.then === 'function' ? undefined : result,
319
+ })
320
+ }
321
+
322
+ span.finish(endTime)
323
+ }
324
+ }
325
+
326
+ // --- resolver wrapping --------------------------------------------------------
327
+
328
+ function wrapResolve (resolve) {
329
+ if (typeof resolve !== 'function' || patchedResolvers.has(resolve)) return resolve
330
+
331
+ function resolveAsync (source, args, contextValue, info) {
332
+ const hasIastSub = iastResolveCh.hasSubscribers
333
+ const hasResolverSub = resolverStartCh.hasSubscribers
334
+
335
+ // Combined fast-path: depth=0 AND no IAST/AppSec subscriber means nothing
336
+ // to do — skip rootCtx lookup, path walk, publish gates.
337
+ if (depthDisabled && !hasIastSub && !hasResolverSub) {
338
+ return resolve.apply(this, arguments)
339
+ }
340
+
341
+ const rootCtx = contexts.get(contextValue) ?? legacyStorage.getStore()?.graphqlRootCtx
342
+ if (!rootCtx) return resolve.apply(this, arguments)
343
+
344
+ const infoPath = info?.path
345
+ const config = rootCtx.config
346
+
347
+ // pathString built incrementally off the parent's cached value
348
+ // (rootCtx.pathCache, keyed by path node) — avoids re-walking the whole
349
+ // path linked-list on every resolver call, which is O(depth) per call for
350
+ // deeply nested resolvers. Shared between the IAST publish and the field
351
+ // record. Collapse-aware: list-index segments become '*'.
352
+ let pathString
353
+ let collapsedKey
354
+ if (infoPath) {
355
+ pathString = buildCachedPathString(infoPath, rootCtx.pathCache, config.collapse)
356
+ if (config.collapse) collapsedKey = pathString
357
+ }
358
+
359
+ // IAST and AppSec subscribers see EVERY resolver call, regardless of
360
+ // depth or collapse. The depth knob caps span creation only.
361
+ if (hasIastSub) {
362
+ iastResolveCh.publish({ rootCtx, args, info, path: pathToArray(infoPath), pathString })
363
+ }
364
+ if (hasResolverSub) {
365
+ resolverStartCh.publish({
366
+ abortController: rootCtx.abortController,
367
+ resolverInfo: getResolverInfo(info, args),
368
+ })
369
+ }
370
+
371
+ if (depthDisabled || !shouldInstrumentNode(config, infoPath)) {
372
+ if (rootCtx.abortController?.signal.aborted) {
373
+ throw new AbortError('Aborted')
374
+ }
375
+
376
+ return resolve.apply(this, arguments)
377
+ }
378
+
379
+ const fieldKey = config.collapse ? buildCachedCollapsedPath(infoPath, rootCtx.collapsedPathCache) : infoPath
380
+ let field = rootCtx.fields.get(fieldKey)
381
+ const isFirst = !field
382
+
383
+ if (isFirst) {
384
+ field = {
385
+ fieldNode: info.fieldNodes?.[0],
386
+ fieldName: info.fieldName,
387
+ returnType: info.returnType,
388
+ baseTypeName: getBaseTypeName(info.returnType),
389
+ variableValues: info.variableValues,
390
+ args,
391
+ infoPath,
392
+ fieldKey,
393
+ pathString,
394
+ collapsedKey: collapsedKey ?? pathString,
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,
400
+ }
401
+ rootCtx.fields.set(fieldKey, field)
402
+ }
403
+
404
+ // Collapsed siblings still publish updateField (master's contract: one
405
+ // publish per resolver call, even when the span is collapsed) and route
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.
410
+ if (!isFirst) {
411
+ return callInAsyncScope(resolve, this, arguments, rootCtx.abortController, field.parentStore, (err) => {
412
+ if (updateFieldCh.hasSubscribers) {
413
+ updateFieldCh.publish({ rootCtx, field, error: err, pathString: field.pathString })
414
+ }
415
+ })
416
+ }
417
+
418
+ const executeSpan = rootCtx.executeSpan
419
+ const startTime = executeSpan._getTime()
420
+ const span = rootCtx.plugin.startResolveSpan(field, rootCtx, executeSpan, startTime)
421
+
422
+ return callInAsyncScope(resolve, this, arguments, rootCtx.abortController, field.currentStore, (err, res) => {
423
+ const endTime = executeSpan._getTime()
424
+ rootCtx.plugin.finishResolveSpan(span, field, err, res, endTime || startTime)
425
+ if (updateFieldCh.hasSubscribers) {
426
+ updateFieldCh.publish({ rootCtx, field, error: err, pathString: field.pathString })
427
+ }
428
+ })
429
+ }
430
+
431
+ patchedResolvers.add(resolveAsync)
432
+ return resolveAsync
433
+ }
434
+
435
+ function wrapFields (type) {
436
+ if (!type?._fields || patchedTypes.has(type)) return
437
+
438
+ patchedTypes.add(type)
439
+
440
+ for (const field of Object.values(type._fields)) {
441
+ wrapFieldResolve(field)
442
+ wrapFieldType(field)
443
+ }
444
+ }
445
+
446
+ function wrapFieldResolve (field) {
447
+ if (!field?.resolve) return
448
+ field.resolve = wrapResolve(field.resolve)
449
+ }
450
+
451
+ function wrapFieldType (field) {
452
+ if (!field?.type) return
453
+
454
+ let unwrapped = field.type
455
+ while (unwrapped.ofType) unwrapped = unwrapped.ofType
456
+
457
+ wrapFields(unwrapped)
458
+ }
459
+
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) {
463
+ if (abortController?.signal.aborted) {
464
+ cb(null, null)
465
+ throw new AbortError('Aborted')
466
+ }
467
+
468
+ try {
469
+ const result = legacyStorage.run(store, () => fn.apply(thisArg, args))
470
+ if (typeof result?.then === 'function') {
471
+ return result.then(
472
+ res => { cb(null, res); return res },
473
+ err => { cb(err); throw err }
474
+ )
475
+ }
476
+ cb(null, result)
477
+ return result
478
+ } catch (err) {
479
+ cb(err)
480
+ throw err
481
+ }
482
+ }
483
+
484
+ function pathToArray (path) {
485
+ let length = 0
486
+ for (let curr = path; curr; curr = curr.prev) {
487
+ length += 1
488
+ }
489
+
490
+ const flattened = new Array(length)
491
+ let index = length
492
+ for (let curr = path; curr; curr = curr.prev) {
493
+ flattened[--index] = curr.key
494
+ }
495
+ return flattened
496
+ }
497
+
498
+ // Build the dotted pathString for a resolver's path node, caching per node on
499
+ // rootCtx.pathCache so each call reuses the parent's already-built string
500
+ // instead of re-walking the whole path linked-list (O(1) amortized per call).
501
+ // Collapse-aware: numeric (list-index) segments become '*'. The recursion
502
+ // handles the cold path where a parent node never hit a resolver (graphql
503
+ // inserts a synthetic array-index node between a list field and its items).
504
+ function buildCachedPathString (path, cache, collapse) {
505
+ const cached = cache.get(path)
506
+ if (cached !== undefined) return cached
507
+
508
+ const key = path.key
509
+ const segment = collapse && typeof key !== 'string' ? '*' : key
510
+ const prev = path.prev
511
+
512
+ const pathString = prev === undefined
513
+ ? String(segment)
514
+ : `${buildCachedPathString(prev, cache, collapse)}.${segment}`
515
+ cache.set(path, pathString)
516
+ return pathString
517
+ }
518
+
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)
51
540
  }
541
+
542
+ cache.byPath.set(path, collapsedPath)
543
+ return collapsedPath
544
+ }
545
+
546
+ // Depth filtering directly on the linked-list node — no array allocation needed.
547
+ // config.depth < 0 means no limit. Only selection-set segments (string keys)
548
+ // count toward depth; list indices are execution artifacts and are transparent.
549
+ // On the v5 line `countListIndices` keeps the legacy behaviour of counting every
550
+ // node when collapsing folds the numeric indices into '*'.
551
+ function shouldInstrumentNode (config, path) {
552
+ if (config.depth < 0) return true
553
+
554
+ let depth = 0
555
+ if (config.countListIndices) {
556
+ for (let curr = path; curr; curr = curr.prev) depth++
557
+ } else {
558
+ for (let curr = path; curr; curr = curr.prev) {
559
+ if (typeof curr.key === 'string') depth++
560
+ }
561
+ }
562
+
563
+ return config.depth >= depth
564
+ }
565
+
566
+ 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
570
+ }
571
+
572
+ return null
573
+ }
574
+
575
+ // Build the resolverInfo payload that AppSec's datadog:graphql:resolver:start
576
+ // subscriber expects: { [fieldName]: { ...args, ...directives } }.
577
+ function getResolverInfo (info, args) {
578
+ let resolverVars = args ? { ...args } : undefined
579
+
580
+ const directives = info.fieldNodes?.[0]?.directives
581
+ if (Array.isArray(directives)) {
582
+ for (const directive of directives) {
583
+ if (directive.arguments.length === 0) continue
584
+
585
+ const argList = {}
586
+ for (const argument of directive.arguments) {
587
+ argList[argument.name.value] = argument.value.value
588
+ }
589
+
590
+ resolverVars ??= {}
591
+ resolverVars[directive.name.value] = argList
592
+ }
593
+ }
594
+
595
+ return resolverVars === undefined ? null : { [info.fieldName]: resolverVars }
596
+ }
597
+
598
+ // --- arg / context normalization ---------------------------------------------
599
+
600
+ // graphql.execute accepts either a single args object or positional arguments;
601
+ // the object form is a lone non-array object in slot 0.
602
+ function isObjectForm (args) {
603
+ return args?.length === 1 && args[0] && typeof args[0] === 'object' && !Array.isArray(args[0])
52
604
  }
53
605
 
54
- // span-related
606
+ function readArgs (args, objectForm) {
607
+ if (!args || args.length === 0) return {}
608
+
609
+ if (objectForm) {
610
+ return args[0]
611
+ }
612
+
613
+ return {
614
+ schema: args[0],
615
+ document: args[1],
616
+ rootValue: args[2],
617
+ contextValue: args[3],
618
+ variableValues: args[4],
619
+ operationName: args[5],
620
+ fieldResolver: args[6],
621
+ }
622
+ }
623
+
624
+ // No user input may be modified. Object-form clones rawArgs[0]; positional
625
+ // form rewrites its own arguments slots (no caller-observable mutation).
626
+ // Returns the readArgs-shaped view of the (possibly cloned) args so the caller
627
+ // doesn't have to re-readArgs after the swap.
628
+ function setWrappedFieldResolver (rawArgs, args, objectForm, defaultFieldResolver) {
629
+ if (!rawArgs || rawArgs.length === 0) return args
630
+
631
+ if (objectForm) {
632
+ const clone = {
633
+ ...args,
634
+ fieldResolver: wrapResolve(args.fieldResolver || defaultFieldResolver),
635
+ }
636
+ rawArgs[0] = clone
637
+ return clone
638
+ }
639
+
640
+ rawArgs[6] = wrapResolve(args.fieldResolver || defaultFieldResolver)
641
+ if (rawArgs.length < 7) rawArgs.length = 7
642
+ args.fieldResolver = rawArgs[6]
643
+ return args
644
+ }
645
+
646
+ function isWeakMapKey (value) {
647
+ return value !== null && (typeof value === 'object' || typeof value === 'function')
648
+ }
649
+
650
+ // Unwrap GraphQL List/NonNull wrappers to get the underlying named type's name.
651
+ // e.g. [Human] → 'Human', [Pet!] → 'Pet', String → 'String'
652
+ function getBaseTypeName (type) {
653
+ let cursor = type
654
+ while (cursor && cursor.ofType) cursor = cursor.ofType
655
+ return cursor?.name
656
+ }
657
+
658
+ // Fallback resolver used when graphql.execute() is called without an explicit
659
+ // fieldResolver and the schema field has no .resolve. Mirrors graphql's own
660
+ // defaultFieldResolver: property access on source, calling it if it's a function.
661
+ // Defined locally so it survives dd-trace plugin-manager reloads (agent.load()
662
+ // recreates globalThis[Symbol.for('dd-trace')], so capturing defaultFieldResolver
663
+ // via ddGlobal at IITM hook time would lose the reference across test suites).
664
+ function defaultFieldResolver (source, args, contextValue, info) {
665
+ if ((typeof source === 'object' && source !== null) || typeof source === 'function') {
666
+ const property = source[info.fieldName]
667
+ if (typeof property === 'function') return source[info.fieldName](args, contextValue, info)
668
+ return property
669
+ }
670
+ }
671
+
672
+ function getOperation (document, operationName) {
673
+ if (!document || !Array.isArray(document.definitions)) return
674
+
675
+ for (const definition of document.definitions) {
676
+ if (definition && types.has(definition.operation) &&
677
+ (!operationName || definition.name?.value === operationName)) {
678
+ return definition
679
+ }
680
+ }
681
+ }
55
682
 
56
683
  function addVariableTags (config, span, variableValues) {
57
684
  if (!variableValues || !config.variables) return