dd-trace 5.110.0 → 5.111.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 (115) hide show
  1. package/README.md +13 -8
  2. package/ci/init.js +21 -2
  3. package/ci/vitest-no-worker-init-setup.mjs +430 -0
  4. package/ext/tags.js +2 -0
  5. package/index.d.ts +13 -0
  6. package/initialize.mjs +5 -6
  7. package/loader-hook.mjs +76 -55
  8. package/package.json +9 -9
  9. package/packages/datadog-instrumentations/src/fs.js +8 -6
  10. package/packages/datadog-instrumentations/src/graphql.js +26 -495
  11. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql.js +201 -0
  12. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  13. package/packages/datadog-instrumentations/src/mongoose.js +14 -2
  14. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +1293 -0
  15. package/packages/datadog-instrumentations/src/vitest-main.js +1567 -0
  16. package/packages/datadog-instrumentations/src/vitest-util.js +249 -0
  17. package/packages/datadog-instrumentations/src/vitest-worker.js +753 -0
  18. package/packages/datadog-instrumentations/src/vitest.js +11 -1855
  19. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/client.js +2 -2
  20. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +18 -10
  21. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +47 -11
  22. package/packages/datadog-plugin-cucumber/src/index.js +1 -1
  23. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +10 -4
  24. package/packages/datadog-plugin-cypress/src/support.js +45 -1
  25. package/packages/datadog-plugin-express/src/code_origin.js +1 -1
  26. package/packages/datadog-plugin-fastify/src/code_origin.js +1 -1
  27. package/packages/datadog-plugin-graphql/src/execute.js +627 -11
  28. package/packages/datadog-plugin-graphql/src/index.js +20 -8
  29. package/packages/datadog-plugin-graphql/src/parse.js +24 -4
  30. package/packages/datadog-plugin-graphql/src/validate.js +16 -5
  31. package/packages/datadog-plugin-http/src/index.js +6 -8
  32. package/packages/datadog-plugin-jest/src/index.js +1 -1
  33. package/packages/datadog-plugin-mocha/src/index.js +1 -1
  34. package/packages/datadog-plugin-openai/src/services.js +2 -2
  35. package/packages/datadog-plugin-playwright/src/index.js +1 -1
  36. package/packages/datadog-plugin-vitest/src/index.js +69 -63
  37. package/packages/datadog-shimmer/src/shimmer.js +37 -16
  38. package/packages/dd-trace/src/aiguard/sdk.js +2 -2
  39. package/packages/dd-trace/src/appsec/api_security/sampler.js +3 -3
  40. package/packages/dd-trace/src/appsec/channels.js +3 -1
  41. package/packages/dd-trace/src/appsec/downstream_requests.js +4 -4
  42. package/packages/dd-trace/src/appsec/graphql.js +5 -5
  43. package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +26 -19
  44. package/packages/dd-trace/src/appsec/iast/security-controls/index.js +2 -2
  45. package/packages/dd-trace/src/appsec/iast/telemetry/index.js +2 -1
  46. package/packages/dd-trace/src/appsec/index.js +10 -1
  47. package/packages/dd-trace/src/appsec/lambda.js +135 -0
  48. package/packages/dd-trace/src/appsec/reporter.js +49 -10
  49. package/packages/dd-trace/src/appsec/telemetry/index.js +1 -1
  50. package/packages/dd-trace/src/appsec/waf/index.js +16 -4
  51. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +4 -4
  52. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +21 -6
  53. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +2 -2
  54. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +1 -1
  55. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +1 -1
  56. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +3 -2
  57. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +1 -1
  58. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +55 -13
  59. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +2 -2
  60. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +17 -0
  61. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +47 -26
  62. package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +2 -2
  63. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +64 -53
  64. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +3 -3
  65. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +23 -5
  66. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +128 -0
  67. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +287 -0
  68. package/packages/dd-trace/src/config/defaults.js +3 -2
  69. package/packages/dd-trace/src/config/generated-config-types.d.ts +61 -57
  70. package/packages/dd-trace/src/config/helper.js +1 -0
  71. package/packages/dd-trace/src/config/index.js +35 -22
  72. package/packages/dd-trace/src/config/major-overrides.js +4 -2
  73. package/packages/dd-trace/src/config/remote_config.js +1 -1
  74. package/packages/dd-trace/src/config/supported-configurations.json +90 -56
  75. package/packages/dd-trace/src/constants.js +7 -0
  76. package/packages/dd-trace/src/datastreams/processor.js +11 -3
  77. package/packages/dd-trace/src/exporters/agent/index.js +1 -1
  78. package/packages/dd-trace/src/exporters/agentless/writer.js +4 -4
  79. package/packages/dd-trace/src/llmobs/index.js +1 -1
  80. package/packages/dd-trace/src/llmobs/plugins/base.js +4 -4
  81. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  82. package/packages/dd-trace/src/llmobs/sdk.js +3 -3
  83. package/packages/dd-trace/src/llmobs/span_processor.js +1 -1
  84. package/packages/dd-trace/src/llmobs/tagger.js +3 -3
  85. package/packages/dd-trace/src/llmobs/writers/base.js +1 -1
  86. package/packages/dd-trace/src/opentelemetry/metrics/index.js +54 -5
  87. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +40 -0
  88. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +169 -0
  89. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +38 -39
  90. package/packages/dd-trace/src/opentelemetry/trace/index.js +4 -0
  91. package/packages/dd-trace/src/opentelemetry/tracer_provider.js +1 -1
  92. package/packages/dd-trace/src/opentracing/tracer.js +6 -1
  93. package/packages/dd-trace/src/plugin_manager.js +7 -7
  94. package/packages/dd-trace/src/plugins/ci_plugin.js +12 -3
  95. package/packages/dd-trace/src/plugins/util/llm.js +6 -1
  96. package/packages/dd-trace/src/plugins/util/test.js +11 -9
  97. package/packages/dd-trace/src/profiling/exporter_cli.js +2 -2
  98. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +3 -3
  99. package/packages/dd-trace/src/profiling/profilers/wall.js +1 -1
  100. package/packages/dd-trace/src/proxy.js +8 -8
  101. package/packages/dd-trace/src/span_processor.js +5 -6
  102. package/packages/dd-trace/src/span_stats.js +96 -78
  103. package/packages/dd-trace/src/startup-log.js +2 -1
  104. package/packages/dd-trace/src/telemetry/dependencies.js +1 -1
  105. package/packages/dd-trace/src/telemetry/endpoints.js +6 -4
  106. package/packages/dd-trace/src/telemetry/index.js +2 -2
  107. package/packages/dd-trace/src/telemetry/logs/index.js +1 -1
  108. package/packages/dd-trace/src/telemetry/send-data.js +8 -8
  109. package/packages/dd-trace/src/telemetry/session-propagation.js +1 -1
  110. package/packages/dd-trace/src/telemetry/telemetry.js +9 -9
  111. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  112. package/vendor/dist/protobufjs/index.js +1 -1
  113. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  114. package/vendor/dist/shell-quote/index.js +1 -1
  115. 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,545 @@ 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
244
  extractErrorIntoSpanEvent(this._tracerConfig, 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
+ const span = this.startSpan('graphql.resolve', {
275
+ service: this.config.service,
276
+ resource: `${fieldName}:${returnType}`,
277
+ childOf,
278
+ type: 'graphql',
279
+ startTime,
280
+ meta: {
281
+ 'graphql.field.name': fieldName,
282
+ 'graphql.field.path': collapsedKey,
283
+ 'graphql.field.type': baseTypeName,
284
+ 'graphql.source': source,
285
+ },
286
+ }, false)
287
+
288
+ if (fieldNode && this.config.variables && fieldNode.arguments) {
289
+ const variables = this.config.variables(variableValues)
290
+ for (const arg of fieldNode.arguments) {
291
+ if (arg.value?.name && arg.value.kind === 'Variable' && variables[arg.value.name.value]) {
292
+ const name = arg.value.name.value
293
+ span.setTag(`graphql.variables.${name}`, variables[name])
294
+ }
295
+ }
296
+ }
297
+
298
+ return span
299
+ }
300
+
301
+ // Public — called from wrapResolve. endTime reflects when the resolver
302
+ // actually completed, not when the field record was created.
303
+ finishResolveSpan (span, field, error, result, endTime) {
304
+ if (error) span.setTag('error', error)
305
+
306
+ if (this.config.hooks.resolve) {
307
+ this.config.hooks.resolve(span, {
308
+ fieldName: field.fieldName,
309
+ path: field.pathString,
310
+ error: error || null,
311
+ // Any thenable from any realm — keep undefined so the hook doesn't
312
+ // accidentally see the unresolved promise.
313
+ result: typeof result?.then === 'function' ? undefined : result,
314
+ })
315
+ }
316
+
317
+ span.finish(endTime)
318
+ }
319
+ }
320
+
321
+ // --- resolver wrapping --------------------------------------------------------
322
+
323
+ function wrapResolve (resolve) {
324
+ if (typeof resolve !== 'function' || patchedResolvers.has(resolve)) return resolve
325
+
326
+ function resolveAsync (source, args, contextValue, info) {
327
+ const hasIastSub = iastResolveCh.hasSubscribers
328
+ const hasResolverSub = resolverStartCh.hasSubscribers
329
+
330
+ // Combined fast-path: depth=0 AND no IAST/AppSec subscriber means nothing
331
+ // to do — skip rootCtx lookup, path walk, publish gates.
332
+ if (depthDisabled && !hasIastSub && !hasResolverSub) {
333
+ return resolve.apply(this, arguments)
334
+ }
335
+
336
+ const rootCtx = contexts.get(contextValue) ?? legacyStorage.getStore()?.graphqlRootCtx
337
+ if (!rootCtx) return resolve.apply(this, arguments)
338
+
339
+ const infoPath = info?.path
340
+ const config = rootCtx.config
341
+
342
+ // pathString built incrementally off the parent's cached value
343
+ // (rootCtx.pathCache, keyed by path node) — avoids re-walking the whole
344
+ // path linked-list on every resolver call, which is O(depth) per call for
345
+ // deeply nested resolvers. Shared between the IAST publish and the field
346
+ // record. Collapse-aware: list-index segments become '*'.
347
+ let pathString
348
+ let collapsedKey
349
+ if (infoPath) {
350
+ pathString = buildCachedPathString(infoPath, rootCtx.pathCache, config.collapse)
351
+ if (config.collapse) collapsedKey = pathString
352
+ }
353
+
354
+ // IAST and AppSec subscribers see EVERY resolver call, regardless of
355
+ // depth or collapse. The depth knob caps span creation only.
356
+ if (hasIastSub) {
357
+ iastResolveCh.publish({ rootCtx, args, info, path: pathToArray(infoPath), pathString })
358
+ }
359
+ if (hasResolverSub) {
360
+ resolverStartCh.publish({
361
+ abortController: rootCtx.abortController,
362
+ resolverInfo: getResolverInfo(info, args),
363
+ })
364
+ }
365
+
366
+ if (depthDisabled || !shouldInstrumentNode(config, infoPath)) {
367
+ if (rootCtx.abortController?.signal.aborted) {
368
+ throw new AbortError('Aborted')
369
+ }
370
+
371
+ return resolve.apply(this, arguments)
372
+ }
373
+
374
+ const fieldKey = config.collapse ? buildCachedCollapsedPath(infoPath, rootCtx.collapsedPathCache) : infoPath
375
+ let field = rootCtx.fields.get(fieldKey)
376
+ const isFirst = !field
377
+
378
+ if (isFirst) {
379
+ field = {
380
+ fieldNode: info.fieldNodes?.[0],
381
+ fieldName: info.fieldName,
382
+ returnType: info.returnType,
383
+ baseTypeName: getBaseTypeName(info.returnType),
384
+ variableValues: info.variableValues,
385
+ args,
386
+ infoPath,
387
+ fieldKey,
388
+ pathString,
389
+ collapsedKey: collapsedKey ?? pathString,
390
+ span: null,
391
+ }
392
+ rootCtx.fields.set(fieldKey, field)
393
+ }
394
+
395
+ // Collapsed siblings still publish updateField (master's contract: one
396
+ // publish per resolver call, even when the span is collapsed) and route
397
+ // through callInAsyncScope so the abort signal stops them mid-flight.
398
+ if (!isFirst) {
399
+ return callInAsyncScope(resolve, this, arguments, rootCtx.abortController, (err) => {
400
+ if (updateFieldCh.hasSubscribers) {
401
+ updateFieldCh.publish({ rootCtx, field, error: err, pathString: field.pathString })
402
+ }
403
+ })
404
+ }
405
+
406
+ const executeSpan = rootCtx.executeSpan
407
+ const startTime = executeSpan._getTime()
408
+ const span = rootCtx.plugin.startResolveSpan(field, rootCtx, executeSpan, startTime)
409
+ field.span = span
410
+
411
+ return callInAsyncScope(resolve, this, arguments, rootCtx.abortController, (err, res) => {
412
+ const endTime = executeSpan._getTime()
413
+ rootCtx.plugin.finishResolveSpan(span, field, err, res, endTime || startTime)
414
+ if (updateFieldCh.hasSubscribers) {
415
+ updateFieldCh.publish({ rootCtx, field, error: err, pathString: field.pathString })
416
+ }
417
+ })
418
+ }
419
+
420
+ patchedResolvers.add(resolveAsync)
421
+ return resolveAsync
422
+ }
423
+
424
+ function wrapFields (type) {
425
+ if (!type?._fields || patchedTypes.has(type)) return
426
+
427
+ patchedTypes.add(type)
428
+
429
+ for (const field of Object.values(type._fields)) {
430
+ wrapFieldResolve(field)
431
+ wrapFieldType(field)
432
+ }
433
+ }
434
+
435
+ function wrapFieldResolve (field) {
436
+ if (!field?.resolve) return
437
+ field.resolve = wrapResolve(field.resolve)
438
+ }
439
+
440
+ function wrapFieldType (field) {
441
+ if (!field?.type) return
442
+
443
+ let unwrapped = field.type
444
+ while (unwrapped.ofType) unwrapped = unwrapped.ofType
445
+
446
+ wrapFields(unwrapped)
447
+ }
448
+
449
+ function callInAsyncScope (fn, thisArg, args, abortController, cb) {
450
+ cb ??= () => {}
451
+
452
+ if (abortController?.signal.aborted) {
453
+ cb(null, null)
454
+ throw new AbortError('Aborted')
455
+ }
456
+
457
+ try {
458
+ const result = fn.apply(thisArg, args)
459
+ if (typeof result?.then === 'function') {
460
+ return result.then(
461
+ res => { cb(null, res); return res },
462
+ err => { cb(err); throw err }
463
+ )
464
+ }
465
+ cb(null, result)
466
+ return result
467
+ } catch (err) {
468
+ cb(err)
469
+ throw err
470
+ }
471
+ }
472
+
473
+ function pathToArray (path) {
474
+ let length = 0
475
+ for (let curr = path; curr; curr = curr.prev) {
476
+ length += 1
477
+ }
478
+
479
+ const flattened = new Array(length)
480
+ let index = length
481
+ for (let curr = path; curr; curr = curr.prev) {
482
+ flattened[--index] = curr.key
483
+ }
484
+ return flattened
485
+ }
486
+
487
+ // Build the dotted pathString for a resolver's path node, caching per node on
488
+ // rootCtx.pathCache so each call reuses the parent's already-built string
489
+ // instead of re-walking the whole path linked-list (O(1) amortized per call).
490
+ // Collapse-aware: numeric (list-index) segments become '*'. The recursion
491
+ // handles the cold path where a parent node never hit a resolver (graphql
492
+ // inserts a synthetic array-index node between a list field and its items).
493
+ function buildCachedPathString (path, cache, collapse) {
494
+ const cached = cache.get(path)
495
+ if (cached !== undefined) return cached
496
+
497
+ const key = path.key
498
+ const segment = collapse && typeof key !== 'string' ? '*' : key
499
+ const prev = path.prev
500
+
501
+ const pathString = prev === undefined
502
+ ? String(segment)
503
+ : `${buildCachedPathString(prev, cache, collapse)}.${segment}`
504
+ cache.set(path, pathString)
505
+ return pathString
506
+ }
507
+
508
+ function buildCachedCollapsedPath (path, cache) {
509
+ if (!path) return
510
+
511
+ const cached = cache.byPath.get(path)
512
+ if (cached !== undefined) return cached
513
+
514
+ const segment = typeof path.key === 'string' ? path.key : '*'
515
+ const prev = path.prev === undefined
516
+ ? undefined
517
+ : buildCachedCollapsedPath(path.prev, cache)
518
+
519
+ let siblings = cache.byParent.get(prev)
520
+ if (siblings === undefined) {
521
+ siblings = new Map()
522
+ cache.byParent.set(prev, siblings)
523
+ }
524
+
525
+ let collapsedPath = siblings.get(segment)
526
+ if (collapsedPath === undefined) {
527
+ collapsedPath = { key: segment, prev }
528
+ siblings.set(segment, collapsedPath)
51
529
  }
530
+
531
+ cache.byPath.set(path, collapsedPath)
532
+ return collapsedPath
533
+ }
534
+
535
+ // Depth filtering directly on the linked-list node — no array allocation needed.
536
+ // config.depth < 0 means no limit. Only selection-set segments (string keys)
537
+ // count toward depth; list indices are execution artifacts and are transparent.
538
+ // On the v5 line `countListIndices` keeps the legacy behaviour of counting every
539
+ // node when collapsing folds the numeric indices into '*'.
540
+ function shouldInstrumentNode (config, path) {
541
+ if (config.depth < 0) return true
542
+
543
+ let depth = 0
544
+ if (config.countListIndices) {
545
+ for (let curr = path; curr; curr = curr.prev) depth++
546
+ } else {
547
+ for (let curr = path; curr; curr = curr.prev) {
548
+ if (typeof curr.key === 'string') depth++
549
+ }
550
+ }
551
+
552
+ return config.depth >= depth
553
+ }
554
+
555
+ function getParentField (rootCtx, field) {
556
+ for (let curr = field.fieldKey?.prev; curr; curr = curr.prev) {
557
+ const innerField = rootCtx.fields.get(curr)
558
+ if (innerField) return innerField
559
+ }
560
+
561
+ return null
562
+ }
563
+
564
+ // Build the resolverInfo payload that AppSec's datadog:graphql:resolver:start
565
+ // subscriber expects: { [fieldName]: { ...args, ...directives } }.
566
+ function getResolverInfo (info, args) {
567
+ let resolverVars = args ? { ...args } : undefined
568
+
569
+ const directives = info.fieldNodes?.[0]?.directives
570
+ if (Array.isArray(directives)) {
571
+ for (const directive of directives) {
572
+ if (directive.arguments.length === 0) continue
573
+
574
+ const argList = {}
575
+ for (const argument of directive.arguments) {
576
+ argList[argument.name.value] = argument.value.value
577
+ }
578
+
579
+ resolverVars ??= {}
580
+ resolverVars[directive.name.value] = argList
581
+ }
582
+ }
583
+
584
+ return resolverVars === undefined ? null : { [info.fieldName]: resolverVars }
585
+ }
586
+
587
+ // --- arg / context normalization ---------------------------------------------
588
+
589
+ // graphql.execute accepts either a single args object or positional arguments;
590
+ // the object form is a lone non-array object in slot 0.
591
+ function isObjectForm (args) {
592
+ return args?.length === 1 && args[0] && typeof args[0] === 'object' && !Array.isArray(args[0])
52
593
  }
53
594
 
54
- // span-related
595
+ function readArgs (args, objectForm) {
596
+ if (!args || args.length === 0) return {}
597
+
598
+ if (objectForm) {
599
+ return args[0]
600
+ }
601
+
602
+ return {
603
+ schema: args[0],
604
+ document: args[1],
605
+ rootValue: args[2],
606
+ contextValue: args[3],
607
+ variableValues: args[4],
608
+ operationName: args[5],
609
+ fieldResolver: args[6],
610
+ }
611
+ }
612
+
613
+ // No user input may be modified. Object-form clones rawArgs[0]; positional
614
+ // form rewrites its own arguments slots (no caller-observable mutation).
615
+ // Returns the readArgs-shaped view of the (possibly cloned) args so the caller
616
+ // doesn't have to re-readArgs after the swap.
617
+ function setWrappedFieldResolver (rawArgs, args, objectForm, defaultFieldResolver) {
618
+ if (!rawArgs || rawArgs.length === 0) return args
619
+
620
+ if (objectForm) {
621
+ const clone = {
622
+ ...args,
623
+ fieldResolver: wrapResolve(args.fieldResolver || defaultFieldResolver),
624
+ }
625
+ rawArgs[0] = clone
626
+ return clone
627
+ }
628
+
629
+ rawArgs[6] = wrapResolve(args.fieldResolver || defaultFieldResolver)
630
+ if (rawArgs.length < 7) rawArgs.length = 7
631
+ args.fieldResolver = rawArgs[6]
632
+ return args
633
+ }
634
+
635
+ function isWeakMapKey (value) {
636
+ return value !== null && (typeof value === 'object' || typeof value === 'function')
637
+ }
638
+
639
+ // Unwrap GraphQL List/NonNull wrappers to get the underlying named type's name.
640
+ // e.g. [Human] → 'Human', [Pet!] → 'Pet', String → 'String'
641
+ function getBaseTypeName (type) {
642
+ let cursor = type
643
+ while (cursor && cursor.ofType) cursor = cursor.ofType
644
+ return cursor?.name
645
+ }
646
+
647
+ // Fallback resolver used when graphql.execute() is called without an explicit
648
+ // fieldResolver and the schema field has no .resolve. Mirrors graphql's own
649
+ // defaultFieldResolver: property access on source, calling it if it's a function.
650
+ // Defined locally so it survives dd-trace plugin-manager reloads (agent.load()
651
+ // recreates globalThis[Symbol.for('dd-trace')], so capturing defaultFieldResolver
652
+ // via ddGlobal at IITM hook time would lose the reference across test suites).
653
+ function defaultFieldResolver (source, args, contextValue, info) {
654
+ if ((typeof source === 'object' && source !== null) || typeof source === 'function') {
655
+ const property = source[info.fieldName]
656
+ if (typeof property === 'function') return source[info.fieldName](args, contextValue, info)
657
+ return property
658
+ }
659
+ }
660
+
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
+ }
55
671
 
56
672
  function addVariableTags (config, span, variableValues) {
57
673
  if (!variableValues || !config.variables) return