dd-trace 5.124.0 → 5.125.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 (201) hide show
  1. package/LICENSE-3rdparty.csv +3 -1
  2. package/ci/init.js +8 -3
  3. package/ci/test-optimization-validation/ci-package-scripts.js +2 -2
  4. package/ci/test-optimization-validation/ci-remediation.js +6 -6
  5. package/ci/test-optimization-validation/command-blocker.js +7 -5
  6. package/ci/test-optimization-validation/framework-adapters/cucumber.js +1 -1
  7. package/ci/test-optimization-validation/framework-adapters/vitest.js +3 -3
  8. package/ci/test-optimization-validation/package-check.js +1 -1
  9. package/ci/test-optimization-validation/report-writer.js +7 -5
  10. package/ci/vitest-no-worker-init-runner-setup.mjs +8 -0
  11. package/ci/vitest-no-worker-init-setup.mjs +13 -13
  12. package/ci/vitest-no-worker-init-v5-setup.mjs +4 -0
  13. package/package.json +13 -10
  14. package/packages/datadog-esbuild/index.js +3 -1
  15. package/packages/datadog-instrumentations/src/cypress-config.js +19 -17
  16. package/packages/datadog-instrumentations/src/fs.js +24 -20
  17. package/packages/datadog-instrumentations/src/google-cloud-vertexai.js +2 -2
  18. package/packages/datadog-instrumentations/src/graphql.js +16 -16
  19. package/packages/datadog-instrumentations/src/helpers/graphql-jit-runtime.js +504 -0
  20. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  21. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +265 -0
  22. package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +1 -2
  23. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +95 -30
  24. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +0 -25
  25. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql-jit.js +83 -0
  26. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql.js +85 -1
  27. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  28. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/mercurius.js +30 -12
  29. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +13 -0
  30. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +251 -0
  31. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +11 -1
  32. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +562 -27
  33. package/packages/datadog-instrumentations/src/jest.js +25 -20
  34. package/packages/datadog-instrumentations/src/knex.js +76 -0
  35. package/packages/datadog-instrumentations/src/mariadb.js +6 -0
  36. package/packages/datadog-instrumentations/src/mocha/common.js +59 -2
  37. package/packages/datadog-instrumentations/src/mocha/main.js +25 -15
  38. package/packages/datadog-instrumentations/src/mocha/utils.js +2 -1
  39. package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +98 -0
  40. package/packages/datadog-instrumentations/src/mocha/worker.js +12 -8
  41. package/packages/datadog-instrumentations/src/net.js +14 -17
  42. package/packages/datadog-instrumentations/src/next.js +212 -62
  43. package/packages/datadog-instrumentations/src/openai.js +42 -40
  44. package/packages/datadog-instrumentations/src/pg.js +15 -0
  45. package/packages/datadog-instrumentations/src/playwright.js +109 -13
  46. package/packages/datadog-instrumentations/src/rum-browser-scripts.js +40 -0
  47. package/packages/datadog-instrumentations/src/sequelize.js +94 -2
  48. package/packages/datadog-instrumentations/src/undici.js +53 -4
  49. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +37 -3
  50. package/packages/datadog-instrumentations/src/vitest-main.js +92 -9
  51. package/packages/datadog-instrumentations/src/vitest-worker.js +71 -14
  52. package/packages/datadog-instrumentations/src/webdriverio.js +965 -6
  53. package/packages/datadog-plugin-amqp10/src/consumer.js +3 -1
  54. package/packages/datadog-plugin-amqp10/src/producer.js +3 -1
  55. package/packages/datadog-plugin-amqp10/src/util.js +2 -1
  56. package/packages/datadog-plugin-avsc/src/schema_iterator.js +6 -1
  57. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +16 -5
  58. package/packages/datadog-plugin-cassandra-driver/src/index.js +8 -3
  59. package/packages/datadog-plugin-cucumber/src/index.js +0 -2
  60. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +262 -48
  61. package/packages/datadog-plugin-google-cloud-pubsub/src/client.js +4 -1
  62. package/packages/datadog-plugin-graphql/src/execute.js +998 -196
  63. package/packages/datadog-plugin-graphql/src/index.js +27 -7
  64. package/packages/datadog-plugin-graphql/src/jit.js +136 -0
  65. package/packages/datadog-plugin-graphql/src/request.js +18 -27
  66. package/packages/datadog-plugin-graphql/src/resolve-error.js +108 -0
  67. package/packages/datadog-plugin-graphql/src/tools/signature.js +1 -2
  68. package/packages/datadog-plugin-graphql/src/tools/transforms.js +45 -3
  69. package/packages/datadog-plugin-graphql/src/utils.js +208 -120
  70. package/packages/datadog-plugin-graphql/src/validate.js +10 -23
  71. package/packages/datadog-plugin-jest/src/index.js +0 -6
  72. package/packages/datadog-plugin-langchain/src/tracing.js +7 -0
  73. package/packages/datadog-plugin-mocha/src/index.js +403 -75
  74. package/packages/datadog-plugin-mongodb-core/src/query.js +6 -0
  75. package/packages/datadog-plugin-mysql/src/index.js +41 -15
  76. package/packages/datadog-plugin-net/src/tcp.js +3 -1
  77. package/packages/datadog-plugin-next/src/index.js +63 -29
  78. package/packages/datadog-plugin-playwright/src/index.js +167 -27
  79. package/packages/datadog-plugin-undici/src/index.js +27 -41
  80. package/packages/datadog-plugin-vitest/src/index.js +6 -12
  81. package/packages/dd-trace/src/aiguard/evaluation.js +2 -1
  82. package/packages/dd-trace/src/aiguard/messages/anthropic.js +8 -1
  83. package/packages/dd-trace/src/aiguard/messages/openai.js +8 -1
  84. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +20 -6
  85. package/packages/dd-trace/src/appsec/api_security/index.js +23 -6
  86. package/packages/dd-trace/src/appsec/api_security/sampler.js +99 -40
  87. package/packages/dd-trace/src/appsec/graphql.js +6 -2
  88. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +3 -3
  89. package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +2 -1
  90. package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +8 -2
  91. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/sql-sensitive-analyzer.js +11 -36
  92. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +1 -1
  93. package/packages/dd-trace/src/appsec/lambda.js +44 -26
  94. package/packages/dd-trace/src/ci-visibility/encode/json-encoder.js +32 -2
  95. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +8 -8
  96. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +6 -4
  97. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +20 -4
  98. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +5 -173
  99. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +7 -6
  100. package/packages/dd-trace/src/ci-visibility/exporters/agents.js +54 -0
  101. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +1 -2
  102. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +394 -225
  103. package/packages/dd-trace/src/ci-visibility/exporters/limits.js +9 -0
  104. package/packages/dd-trace/src/ci-visibility/exporters/request.js +323 -31
  105. package/packages/dd-trace/src/ci-visibility/exporters/settings-cache-key.js +42 -0
  106. package/packages/dd-trace/src/ci-visibility/final-flush.js +2 -1
  107. package/packages/dd-trace/src/ci-visibility/requests/fs-cache.js +57 -18
  108. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +0 -3
  109. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +164 -71
  110. package/packages/dd-trace/src/ci-visibility/requests/video-request.js +176 -0
  111. package/packages/dd-trace/src/ci-visibility/telemetry.js +10 -7
  112. package/packages/dd-trace/src/ci-visibility/test-video.js +61 -0
  113. package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -0
  114. package/packages/dd-trace/src/config/index.js +73 -13
  115. package/packages/dd-trace/src/config/parsers.js +11 -1
  116. package/packages/dd-trace/src/config/stable.js +3 -3
  117. package/packages/dd-trace/src/config/supported-configurations.json +20 -2
  118. package/packages/dd-trace/src/crashtracking/crashtracker.js +82 -18
  119. package/packages/dd-trace/src/debugger/config.js +12 -1
  120. package/packages/dd-trace/src/debugger/constants.js +1 -0
  121. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +2 -1
  122. package/packages/dd-trace/src/debugger/devtools_client/request-options.js +33 -0
  123. package/packages/dd-trace/src/debugger/devtools_client/send.js +7 -40
  124. package/packages/dd-trace/src/debugger/devtools_client/status.js +11 -6
  125. package/packages/dd-trace/src/debugger/devtools_client/tags.js +41 -0
  126. package/packages/dd-trace/src/debugger/index.js +23 -3
  127. package/packages/dd-trace/src/dogstatsd.js +510 -76
  128. package/packages/dd-trace/src/encode/0.4-cross-payload.js +170 -0
  129. package/packages/dd-trace/src/encode/0.4.js +5 -5
  130. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +9 -8
  131. package/packages/dd-trace/src/evp_proxy/direct.js +27 -23
  132. package/packages/dd-trace/src/exporters/agent/index.js +2 -1
  133. package/packages/dd-trace/src/exporters/agent/writer.js +24 -7
  134. package/packages/dd-trace/src/exporters/agentless/index.js +7 -9
  135. package/packages/dd-trace/src/exporters/agentless/writer.js +100 -118
  136. package/packages/dd-trace/src/exporters/common/agents.js +28 -6
  137. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +2 -2
  138. package/packages/dd-trace/src/exporters/common/final-flush-request-tracker.js +191 -0
  139. package/packages/dd-trace/src/exporters/common/request.js +31 -11
  140. package/packages/dd-trace/src/exporters/common/url.js +40 -1
  141. package/packages/dd-trace/src/exporters/common/writer.js +9 -7
  142. package/packages/dd-trace/src/flush.js +6 -3
  143. package/packages/dd-trace/src/id.js +14 -0
  144. package/packages/dd-trace/src/index.js +5 -2
  145. package/packages/dd-trace/src/llmobs/experiments/dataset.js +2 -1
  146. package/packages/dd-trace/src/llmobs/index.js +32 -32
  147. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +22 -71
  148. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +23 -13
  149. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +7 -0
  150. package/packages/dd-trace/src/llmobs/util.js +0 -33
  151. package/packages/dd-trace/src/llmobs/writers/base.js +49 -32
  152. package/packages/dd-trace/src/log/index.js +7 -1
  153. package/packages/dd-trace/src/{ci-visibility/log-submission → log-submission}/log-submission-plugin.js +48 -19
  154. package/packages/dd-trace/src/openfeature/writers/base.js +14 -12
  155. package/packages/dd-trace/src/openfeature/writers/util.js +1 -1
  156. package/packages/dd-trace/src/opentelemetry/logs/index.js +5 -24
  157. package/packages/dd-trace/src/opentelemetry/logs/otlp_http_log_exporter.js +14 -4
  158. package/packages/dd-trace/src/opentelemetry/metrics/index.js +15 -22
  159. package/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js +14 -4
  160. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +10 -0
  161. package/packages/dd-trace/src/opentelemetry/otlp/otlp_transformer_base.js +19 -4
  162. package/packages/dd-trace/src/opentelemetry/resource-attributes.js +55 -0
  163. package/packages/dd-trace/src/opentelemetry/trace/index.js +6 -1
  164. package/packages/dd-trace/src/opentelemetry/trace/otlp_http_trace_exporter.js +10 -0
  165. package/packages/dd-trace/src/opentracing/propagation/text_map.js +93 -18
  166. package/packages/dd-trace/src/payload-tagging/index.js +2 -1
  167. package/packages/dd-trace/src/plugins/ci_plugin.js +1 -6
  168. package/packages/dd-trace/src/plugins/tracing.js +12 -10
  169. package/packages/dd-trace/src/plugins/util/ci.js +3 -1
  170. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +16 -15
  171. package/packages/dd-trace/src/plugins/util/test.js +83 -20
  172. package/packages/dd-trace/src/plugins/util/url.js +2 -2
  173. package/packages/dd-trace/src/plugins/util/web.js +4 -9
  174. package/packages/dd-trace/src/profiling/config.js +1 -0
  175. package/packages/dd-trace/src/profiling/oom.js +7 -2
  176. package/packages/dd-trace/src/profiling/profiler.js +23 -13
  177. package/packages/dd-trace/src/profiling/profilers/space.js +38 -11
  178. package/packages/dd-trace/src/profiling/webspan-utils.js +7 -4
  179. package/packages/dd-trace/src/proxy.js +75 -5
  180. package/packages/dd-trace/src/remote_config/index.js +319 -101
  181. package/packages/dd-trace/src/remote_config/scheduler.js +37 -12
  182. package/packages/dd-trace/src/runtime_metrics/client.js +37 -8
  183. package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +5 -1
  184. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +5 -1
  185. package/packages/dd-trace/src/sampling_rule.js +36 -9
  186. package/packages/dd-trace/src/serverless/telemetry-delivery-tracker.js +4 -8
  187. package/packages/dd-trace/src/serverless/vercel.js +53 -20
  188. package/packages/dd-trace/src/serverless.js +10 -3
  189. package/packages/dd-trace/src/service-naming/schemas/util.js +1 -1
  190. package/packages/dd-trace/src/span_stats.js +24 -28
  191. package/packages/dd-trace/src/startup-log.js +3 -2
  192. package/packages/dd-trace/src/telemetry/agentless-url.js +28 -0
  193. package/packages/dd-trace/src/telemetry/send-data.js +18 -16
  194. package/packages/dd-trace/src/telemetry/session-propagation.js +3 -2
  195. package/packages/dd-trace/src/tracer.js +4 -5
  196. package/packages/dd-trace/src/tracer_metadata.js +11 -3
  197. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  198. package/vendor/dist/protobufjs/index.js +1 -1
  199. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  200. package/packages/datadog-instrumentations/src/playwright-browser-scripts.js +0 -27
  201. package/packages/dd-trace/src/encode/agentless-json.js +0 -210
@@ -1,31 +1,52 @@
1
1
  'use strict'
2
2
 
3
+ const { AsyncResource } = require('node:async_hooks')
4
+
3
5
  const dc = require('dc-polyfill')
4
6
 
5
7
  const { storage } = require('../../datadog-core')
6
8
  const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
7
9
  const GraphQLParsePlugin = require('./parse')
8
- const { extractErrorIntoSpanEvent, getOperation, getSignature, isApolloHealthCheck } = require('./utils')
10
+ const { recordResolveError } = require('./resolve-error')
11
+ const {
12
+ extractErrorIntoSpanEvent,
13
+ getBaseTypeName,
14
+ getOperation,
15
+ getSignature,
16
+ isApolloHealthCheck,
17
+ refineRequestSpanMetadata,
18
+ } = require('./utils')
19
+
20
+ /**
21
+ * @typedef {import('../../dd-trace/src/opentracing/span')} DatadogSpan
22
+ * @typedef {import('../../datadog-instrumentations/src/helpers/graphql-jit-runtime').JitFieldDescriptor} JitDescriptor
23
+ * @typedef {import('graphql').GraphQLFieldResolver<unknown, unknown>} GraphQLFieldResolver
24
+ * @typedef {Parameters<GraphQLFieldResolver> & { [index: number]: unknown, length: number }} ArgumentsList
25
+ * @typedef {{
26
+ * schema?: import('graphql').GraphQLSchema,
27
+ * document?: import('graphql').DocumentNode,
28
+ * rootValue?: unknown,
29
+ * contextValue?: unknown,
30
+ * variableValues?: Record<string, unknown>,
31
+ * operationName?: string | null,
32
+ * fieldResolver?: GraphQLFieldResolver
33
+ * }} ExecutionArguments
34
+ * @typedef {(value: unknown) => unknown} ThenableCallback
35
+ * @typedef {{ then: (onFulfilled?: ThenableCallback, onRejected?: ThenableCallback) => unknown }} Thenable
36
+ * @typedef {import('../../datadog-instrumentations/src/helpers/graphql-jit-runtime').ArgumentFactory} ArgumentFactory
37
+ */
9
38
 
10
39
  const legacyStorage = storage('legacy')
11
40
 
12
41
  const iastResolveCh = dc.channel('apm:graphql:resolve:start')
13
42
  const resolverStartCh = dc.channel('datadog:graphql:resolver:start')
14
- const updateFieldCh = dc.channel('apm:graphql:resolve:updateField')
15
-
16
- // AppSec/WAF abort gate. Published synchronously from bindStart with a
17
- // payload carrying the abortController; subscribers that call
18
- // `payload.abortController.abort()` signal a pre-execute abort. bindStart
19
- // observes the aborted signal by replacing `ctx.arguments[0]` with a Proxy
20
- // whose getters throw AbortError — the orchestrion-emitted wrapper's
21
- // `try { __apm$traced() } catch { ...; throw err }` block then propagates
22
- // the AbortError to the caller of graphql.execute.
23
- const startExecuteCh = dc.channel('apm:graphql:execute:start')
24
43
 
25
44
  const contexts = new WeakMap()
26
45
  const instrumentedArgs = new WeakSet()
27
46
 
28
47
  const patchedResolvers = new WeakSet()
48
+ const jitResolvers = new WeakMap()
49
+ const originalResolvers = new WeakMap()
29
50
 
30
51
  // Visited types per caller-owned schema. The walk reaches union members and
31
52
  // interface implementations through the schema (`getTypes`/`getPossibleTypes`),
@@ -44,6 +65,18 @@ let depthDisabled = false
44
65
  // operation's variableValues is undefined.
45
66
  const NO_VARIABLES_CACHED = Symbol('noVariablesCached')
46
67
 
68
+ // `field.endTime` stays numeric: 0 is pending, -1 means a collapsed span was reused,
69
+ // and positive values are exact completion times. Pending and reused spans finish at the execution boundary.
70
+ const PENDING_FIELD_END_TIME = 0
71
+ const REUSED_FIELD_END_TIME = -1
72
+
73
+ let asyncDisposeSymbol = Symbol.asyncDispose
74
+ // @graphql-tools/executor uses this fallback before Node.js exposes Symbol.asyncDispose.
75
+ /* istanbul ignore if */
76
+ if (asyncDisposeSymbol === undefined) {
77
+ asyncDisposeSymbol = Symbol.for('asyncDispose')
78
+ }
79
+
47
80
  class AbortError extends Error {
48
81
  constructor (message) {
49
82
  super(message)
@@ -51,6 +84,58 @@ class AbortError extends Error {
51
84
  }
52
85
  }
53
86
 
87
+ /**
88
+ * @param {GraphQLExecutePlugin} plugin
89
+ * @param {DatadogSpan} executeSpan
90
+ * @param {string | undefined} source
91
+ * @param {AbortController | undefined} abortController
92
+ * @param {{ fields: object[] } | undefined} jitPlan
93
+ * @param {Record<string, unknown> | undefined} variableValues
94
+ * @returns {object}
95
+ */
96
+ function createRootContext (plugin, executeSpan, source, abortController, jitPlan, variableValues) {
97
+ const hasIastSub = iastResolveCh.hasSubscribers
98
+ const hasResolverSub = abortController !== undefined
99
+ const traceResolvers = plugin.config.depth !== 0
100
+
101
+ // Read by the generated gate in every inlined default field.
102
+ const traceAll = hasIastSub || hasResolverSub ||
103
+ (traceResolvers && !plugin.config.collapse)
104
+
105
+ const rootCtx = {
106
+ source,
107
+ config: plugin.config,
108
+ abortController,
109
+ executeSpan,
110
+ plugin,
111
+ filteredVariablesKey: NO_VARIABLES_CACHED,
112
+ filteredVariables: undefined,
113
+ depthDisabled: !traceResolvers,
114
+ hasIastSub,
115
+ hasResolverSub,
116
+ jitTraceAll: traceAll,
117
+ jitTraceFirst: !traceAll && traceResolvers && plugin.config.collapse,
118
+ variableValues,
119
+ }
120
+
121
+ if (jitPlan) {
122
+ rootCtx.jitPlan = jitPlan
123
+ if (traceResolvers) {
124
+ if (plugin.config.collapse) {
125
+ rootCtx.jitFields = new Array(jitPlan.fields.length)
126
+ } else {
127
+ rootCtx.jitFieldsByPath = new Map()
128
+ }
129
+ }
130
+ }
131
+ if (traceResolvers && (!jitPlan || !plugin.config.collapse)) {
132
+ rootCtx.fields = new Map()
133
+ rootCtx.pathCache = new Map()
134
+ }
135
+
136
+ return rootCtx
137
+ }
138
+
54
139
  class GraphQLExecutePlugin extends TracingPlugin {
55
140
  static id = 'graphql'
56
141
  static operation = 'execute'
@@ -95,10 +180,13 @@ class GraphQLExecutePlugin extends TracingPlugin {
95
180
  }
96
181
  }
97
182
 
98
- bindStart (ctx) {
183
+ /**
184
+ * @param {object} ctx
185
+ * @returns {ExecutionArguments | undefined}
186
+ */
187
+ readExecutionArgs (ctx) {
99
188
  const rawArgs = ctx.arguments
100
- const objectForm = isObjectForm(rawArgs)
101
- const args = readArgs(rawArgs, objectForm)
189
+ const args = readArgs(rawArgs, isObjectForm(rawArgs))
102
190
 
103
191
  // Re-entrant execute() short-circuit (yoga's normalizedExecutor calls
104
192
  // execute internally with the same arguments object — without this we'd
@@ -106,36 +194,93 @@ class GraphQLExecutePlugin extends TracingPlugin {
106
194
  // check also catches primitive contexts.
107
195
  if (instrumentedArgs.has(rawArgs?.[0])) {
108
196
  ctx.ddSkipped = true
109
- return ctx.currentStore
197
+ ctx.currentStore = legacyStorage.getStore()
198
+ return
110
199
  }
111
200
 
112
201
  const { contextValue } = args
113
202
  if (contextValue && typeof contextValue === 'object' && contexts.has(contextValue)) {
114
203
  ctx.ddSkipped = true
115
- return ctx.currentStore
204
+ return
116
205
  }
117
206
 
207
+ return args
208
+ }
209
+
210
+ /**
211
+ * @param {object} ctx
212
+ * @param {ExecutionArguments} args
213
+ */
214
+ wrapExecutionResolvers (ctx, args) {
215
+ const rawArgs = ctx.arguments
216
+ ctx.ddArgs = setWrappedFieldResolver(rawArgs, args, isObjectForm(rawArgs), defaultFieldResolver)
217
+ if (ctx.ddArgs && typeof ctx.ddArgs === 'object') {
218
+ instrumentedArgs.add(ctx.ddArgs)
219
+ ctx.ddInstrumentedArgs = ctx.ddArgs
220
+ }
221
+
222
+ const { schema } = args
223
+ if (schema) {
224
+ wrapFields(schema._queryType, schema)
225
+ wrapFields(schema._mutationType, schema)
226
+ wrapFields(schema._subscriptionType, schema)
227
+ }
228
+ }
229
+
230
+ /**
231
+ * @param {object} ctx
232
+ * @param {unknown} contextValue
233
+ * @param {object} rootCtx
234
+ */
235
+ storeRootContext (ctx, contextValue, rootCtx) {
236
+ ctx.currentStore.graphqlRootCtx = rootCtx
237
+ if (isWeakMapKey(contextValue)) {
238
+ contexts.set(contextValue, rootCtx)
239
+ ctx.ddContextValue = contextValue
240
+ }
241
+ }
242
+
243
+ /**
244
+ * @param {object} ctx
245
+ */
246
+ bindStart (ctx) {
247
+ const args = this.readExecutionArgs(ctx)
248
+ if (!args) return ctx.currentStore
249
+
250
+ const { contextValue } = args
118
251
  const document = args.document
119
252
  const docSource = document ? GraphQLParsePlugin.documentSources.get(document) : undefined
120
253
  const operation = getOperation(document, args.operationName)
121
254
 
122
255
  const type = operation?.operation
123
- const name = operation?.name?.value
256
+ const name = operation?.name?.value ?? args.operationName ?? undefined
257
+ let signature = name ?? ''
124
258
  const source = this.config.source && docSource
125
259
 
126
260
  // Apollo Server may execute a cached document without parsing it first.
127
261
  // Match the full gateway operation here so caller-owned AST transformations
128
262
  // cannot suppress execute/resolver AppSec and IAST channels.
129
263
  if (name === '__ApolloServiceHealthCheck__' &&
130
- document.definitions.length === 1 &&
264
+ document?.definitions?.length === 1 &&
131
265
  isApolloHealthCheck(operation)) {
132
266
  ctx.ddSkipped = true
133
267
  return ctx.currentStore
134
268
  }
135
269
 
136
- ctx.collapse = this.config.collapse
270
+ const requestStore =
271
+ /** @type {{ graphqlRequestSpan?: DatadogSpan } | undefined} */ (legacyStorage.getStore())
272
+ if (type !== undefined) {
273
+ signature = getSignature(
274
+ /** @type {import('graphql').DocumentNode} */ (document),
275
+ name,
276
+ type,
277
+ this.config.signature
278
+ )
279
+ refineRequestSpanMetadata(requestStore?.graphqlRequestSpan, signature, type, name)
280
+ }
137
281
 
138
- const signature = getSignature(document, name, type, this.config.signature)
282
+ ctx.collapse = this.config.collapse
283
+ ctx.ddOperationType = type
139
284
 
140
285
  const span = this.startSpan(this.operationName(), {
141
286
  service: this.config.service || this.serviceName(),
@@ -151,71 +296,13 @@ class GraphQLExecutePlugin extends TracingPlugin {
151
296
 
152
297
  addVariableTags(this.config, span, args.variableValues)
153
298
 
154
- const abortController = new AbortController()
155
-
156
- // AppSec/WAF synchronous-abort gate. Publish before any resolver-wrapping
157
- // work — if a subscriber aborts, none of that work matters because we'll
158
- // make execute's body throw AbortError before it reaches any resolvers.
159
- // bindStart runs as a bindStore transform on
160
- // tracing:orchestrion:graphql:apm:graphql:execute:start, which fires
161
- // BEFORE the orchestrion-emitted wrapper publishes its own :start and
162
- // BEFORE the wrapped fn runs. The subscriber gets the abortController
163
- // synchronously; on return we observe `signal.aborted` and act.
164
- if (startExecuteCh.hasSubscribers) {
165
- startExecuteCh.publish({ abortController, args })
166
- if (abortController.signal.aborted) {
167
- // Replace ctx.arguments[0] with a Proxy that throws AbortError on any
168
- // property access. The orchestrion wrapper calls
169
- // `__apm$wrapped.apply(this, ctx.arguments)`; graphql.execute's body
170
- // begins with `const { schema, document, ... } = args` so the
171
- // destructure triggers the trap immediately. The wrapper catches and
172
- // rethrows, propagating AbortError to graphql.execute's caller.
173
- ctx.arguments[0] = new Proxy({}, {
174
- get () { throw new AbortError('Aborted') },
175
- has () { throw new AbortError('Aborted') },
176
- })
177
- ctx.ddAborted = true
178
- return ctx.currentStore
179
- }
180
- }
181
-
182
- ctx.ddArgs = setWrappedFieldResolver(rawArgs, args, objectForm, defaultFieldResolver)
183
- if (ctx.ddArgs && typeof ctx.ddArgs === 'object') {
184
- instrumentedArgs.add(ctx.ddArgs)
185
- ctx.ddInstrumentedArgs = ctx.ddArgs
186
- }
299
+ const abortController = resolverStartCh.hasSubscribers ? new AbortController() : undefined
187
300
 
188
- const schema = args.schema
189
- if (schema) {
190
- wrapFields(schema._queryType, schema)
191
- wrapFields(schema._mutationType, schema)
192
- wrapFields(schema._subscriptionType, schema)
193
- }
301
+ this.wrapExecutionResolvers(ctx, args)
194
302
 
195
- const rootCtx = {
196
- source: docSource,
197
- config: this.config,
198
- fields: new Map(),
199
- pathCache: new Map(),
200
- abortController,
201
- executeSpan: span,
202
- plugin: this,
203
- // graphql.resolve variable tags: memoize the filtered result for the
204
- // last-seen variableValues object (see #filterVariables).
205
- filteredVariablesKey: NO_VARIABLES_CACHED,
206
- filteredVariables: undefined,
207
- }
303
+ const rootCtx = createRootContext(this, span, docSource, abortController, ctx.ddPlan, args.variableValues)
208
304
  ctx.ddRootCtx = rootCtx
209
- if (isWeakMapKey(contextValue)) {
210
- contexts.set(contextValue, rootCtx)
211
- ctx.ddContextValue = contextValue
212
- } else {
213
- // Primitive / non-keyable contextValue: stash rootCtx on the
214
- // orchestrion-scoped store that runStores enters for execute. Wrapped
215
- // resolvers read it via legacyStorage.getStore() and it unwinds with the
216
- // frame — no enterWith store that leaks past execute.
217
- ctx.currentStore.graphqlRootCtx = rootCtx
218
- }
305
+ this.storeRootContext(ctx, contextValue, rootCtx)
219
306
 
220
307
  return ctx.currentStore
221
308
  }
@@ -226,14 +313,14 @@ class GraphQLExecutePlugin extends TracingPlugin {
226
313
  const span = ctx?.currentStore?.span || this.activeSpan
227
314
  if (!span) return
228
315
 
316
+ if (ctx.ddRootCtx && legacyStorage.getStore() !== ctx.currentStore) {
317
+ legacyStorage.enterWith(ctx.currentStore)
318
+ }
319
+
229
320
  // Synchronous execute() throw (e.g. execute(null, doc)) — error handler
230
321
  // already tagged the span.
231
322
  if (ctx.error) {
232
- if (ctx.ddAborted) {
233
- span.finish()
234
- } else {
235
- this.#finishSpan(ctx, span)
236
- }
323
+ this.#finishSpan(ctx, span)
237
324
  return ctx.parentStore
238
325
  }
239
326
 
@@ -242,7 +329,7 @@ class GraphQLExecutePlugin extends TracingPlugin {
242
329
  if (typeof result?.then === 'function') {
243
330
  result.then(
244
331
  (res) => this.#finishSpan(ctx, span, res),
245
- (err) => this.#finishSpan(ctx, span, undefined, err)
332
+ (error) => this.#finishSpan(ctx, span, undefined, error)
246
333
  )
247
334
  } else {
248
335
  this.#finishSpan(ctx, span, result)
@@ -252,9 +339,6 @@ class GraphQLExecutePlugin extends TracingPlugin {
252
339
  }
253
340
 
254
341
  error (ctx) {
255
- // Pre-execute WAF abort isn't an error condition — opSpan.error must
256
- // stay 0 per master's contract.
257
- if (ctx.ddAborted) return
258
342
  const span = ctx?.currentStore?.span || this.activeSpan
259
343
  if (span && ctx?.error) {
260
344
  span.setTag('error', ctx.error)
@@ -266,17 +350,30 @@ class GraphQLExecutePlugin extends TracingPlugin {
266
350
  * @param {import('../../dd-trace/src/opentracing/span')} span
267
351
  * @param {import('graphql').ExecutionResult} [res]
268
352
  * @param {unknown} [error]
353
+ * @param {boolean} [incrementalComplete]
354
+ * @param {boolean} [finishPendingFields]
269
355
  */
270
- #finishSpan (ctx, span, res, error) {
356
+ #finishSpan (ctx, span, res, error, incrementalComplete = false, finishPendingFields = false) {
357
+ if (!incrementalComplete && error === undefined && ctx.ddOperationType !== 'subscription') {
358
+ const iterator = getIncrementalIterator(res)
359
+ if (iterator) {
360
+ tagIncrementalResult(this.config, span, res?.initialResult)
361
+ observeIncrementalIterator(iterator, (result, iteratorError, iteratorCancelled, complete) => {
362
+ tagIncrementalResult(this.config, span, result)
363
+ if (complete) {
364
+ this.#finishSpan(ctx, span, res, iteratorError, true, iteratorCancelled)
365
+ }
366
+ })
367
+ return
368
+ }
369
+ }
370
+
271
371
  if (error !== undefined) {
272
372
  span.setTag('error', error)
273
373
  }
274
374
 
275
375
  if (res?.errors?.length) {
276
- span.setTag('error', res.errors[0])
277
- for (const err of res.errors) {
278
- extractErrorIntoSpanEvent(this.config, span, err)
279
- }
376
+ tagExecutionErrors(this.config, span, res.errors)
280
377
  }
281
378
 
282
379
  if (ctx.ddContextValue) {
@@ -286,18 +383,33 @@ class GraphQLExecutePlugin extends TracingPlugin {
286
383
  instrumentedArgs.delete(ctx.ddInstrumentedArgs)
287
384
  }
288
385
 
386
+ const rootCtx = ctx.ddRootCtx
387
+ const releaseBeforeExecuteHook = rootCtx?.resolveHooksPending === true
388
+ if (releaseBeforeExecuteHook) {
389
+ releaseRootContext(rootCtx, finishPendingFields)
390
+ } else if (rootCtx?.jitResolveHooksPending === true) {
391
+ runResolveHooks(rootCtx)
392
+ }
289
393
  this.config.hooks.execute(span, ctx.ddArgs, res)
290
-
394
+ if (!releaseBeforeExecuteHook) releaseRootContext(rootCtx, finishPendingFields)
291
395
  span.finish()
292
396
  }
293
397
 
294
398
  // Public — called from wrapResolve (free function, crosses class boundary).
295
- // Resolve-span creation is inline at first-encounter; deferring to a batch
296
- // produces a bursty encoder stall when many spans finish together.
297
- startResolveSpan (field, rootCtx, executeSpan, startTime) {
298
- const { fieldNode, fieldName, returnType, baseTypeName, variableValues, collapsedKey } = field
399
+ // Resolve-span creation stays inline so child fields can parent immediately.
400
+ /**
401
+ * @param {object} field
402
+ * @param {object} rootCtx
403
+ * @param {DatadogSpan} executeSpan
404
+ * @param {number} startTime
405
+ * @param {Record<string, unknown> | undefined} variableValues
406
+ * @param {object | null} [parentField]
407
+ * @returns {DatadogSpan}
408
+ */
409
+ startResolveSpan (field, rootCtx, executeSpan, startTime, variableValues, parentField) {
410
+ const { fieldNode, fieldName, returnType, baseTypeName, collapsedKey } = field
299
411
 
300
- const parent = getParentField(rootCtx, field)
412
+ const parent = parentField === undefined ? getParentField(rootCtx, field) : parentField
301
413
  const childOf = parent?.span || executeSpan
302
414
 
303
415
  const document = rootCtx.source
@@ -305,24 +417,29 @@ class GraphQLExecutePlugin extends TracingPlugin {
305
417
  const source = loc && document.slice(loc.start, loc.end)
306
418
 
307
419
  // ctx form: startSpan sets field.currentStore = { ...activeStore, span }
308
- // without entering it. Only the field's first resolver call runs in that
309
- // store (isFirst check in wrapResolve); siblings use field.parentStore.
420
+ // without entering it. Collapsed siblings reuse that store until execute
421
+ // completes and finishes the shared span.
422
+ const meta = field.tags ?? {
423
+ 'graphql.field.coordinates': `${field.parentTypeName}.${fieldName}`,
424
+ 'graphql.field.name': fieldName,
425
+ 'graphql.field.path': collapsedKey,
426
+ 'graphql.field.type': baseTypeName,
427
+ 'graphql.source': source,
428
+ }
310
429
  const span = this.startSpan('graphql.resolve', {
311
430
  service: this.config.service,
312
- resource: `${fieldName}:${returnType}`,
431
+ resource: field.resource ?? `${fieldName}:${returnType}`,
313
432
  childOf,
314
433
  type: 'graphql',
315
434
  startTime,
316
- meta: {
317
- 'graphql.field.coordinates': `${field.parentTypeName}.${fieldName}`,
318
- 'graphql.field.name': fieldName,
319
- 'graphql.field.path': collapsedKey,
320
- 'graphql.field.type': baseTypeName,
321
- 'graphql.source': source,
322
- },
435
+ meta,
323
436
  }, field)
324
437
 
325
438
  field.span = span
439
+ if (rootCtx.config.collapse) {
440
+ field.nextResolveField = rootCtx.resolveFields
441
+ rootCtx.resolveFields = field
442
+ }
326
443
 
327
444
  if (fieldNode && this.config.variables && fieldNode.arguments) {
328
445
  const variables = this.#filterVariables(rootCtx, variableValues)
@@ -358,30 +475,83 @@ class GraphQLExecutePlugin extends TracingPlugin {
358
475
  return filtered
359
476
  }
360
477
 
361
- // Public — called from wrapResolve. endTime reflects when the resolver
362
- // actually completed, not when the field record was created.
363
- finishResolveSpan (span, field, error, result, endTime) {
478
+ /**
479
+ * @param {object} rootCtx
480
+ * @param {object} field
481
+ * @param {unknown} error
482
+ * @param {unknown} result
483
+ * @param {boolean} [failed]
484
+ */
485
+ completeResolveSpan (rootCtx, field, error, result, failed = false) {
486
+ if (rootCtx.resolveFields !== undefined) {
487
+ if (field.endTime !== REUSED_FIELD_END_TIME) {
488
+ field.endTime = rootCtx.executeSpan._getTime()
489
+ }
490
+ if (field.jitPathKey === false) {
491
+ if (failed) recordResolveError(field, error)
492
+ if (this.config.hooks.resolve) {
493
+ field.resolveHookContext = createResolveHookContext(field, field.error, result)
494
+ rootCtx.resolveHooksPending = true
495
+ }
496
+ return
497
+ }
498
+ if (field.isSharedJitField) {
499
+ if (error) recordResolveError(field, error)
500
+ if (this.config.hooks.resolve) {
501
+ field.resolveHookContext = createResolveHookContext(field, field.error, result)
502
+ rootCtx.jitResolveHooksPending = true
503
+ }
504
+ return
505
+ }
506
+ this.#completeResolveSpan(field, error, result)
507
+ return
508
+ }
509
+
510
+ this.#finishResolveSpan(field, error, result, rootCtx.executeSpan._getTime())
511
+ }
512
+
513
+ /**
514
+ * @param {object} field
515
+ * @param {unknown} error
516
+ * @param {unknown} result
517
+ * @param {number} endTime
518
+ */
519
+ #finishResolveSpan (field, error, result, endTime) {
520
+ this.#completeResolveSpan(field, error, result)
521
+ field.span.finish(endTime)
522
+ }
523
+
524
+ /**
525
+ * @param {object} field
526
+ * @param {unknown} error
527
+ * @param {unknown} result
528
+ */
529
+ #completeResolveSpan (field, error, result) {
530
+ const { span } = field
364
531
  if (error) span.setTag('error', error)
365
532
 
366
533
  if (this.config.hooks.resolve) {
367
- this.config.hooks.resolve(span, {
368
- fieldName: field.fieldName,
369
- path: field.pathString,
370
- error: error || null,
371
- // Any thenable from any realm — keep undefined so the hook doesn't
372
- // accidentally see the unresolved promise.
373
- result: typeof result?.then === 'function' ? undefined : result,
374
- })
534
+ this.config.hooks.resolve(span, createResolveHookContext(field, error, result))
375
535
  }
376
-
377
- span.finish(endTime)
378
536
  }
379
537
  }
380
538
 
381
539
  // --- resolver wrapping --------------------------------------------------------
382
540
 
383
- function wrapResolve (resolve) {
384
- if (typeof resolve !== 'function' || patchedResolvers.has(resolve)) return resolve
541
+ /**
542
+ * @param {GraphQLFieldResolver} resolve
543
+ * @param {boolean} [isJit]
544
+ */
545
+ function wrapResolve (resolve, isJit = false) {
546
+ /* istanbul ignore next: future GraphQL versions may expose a non-callable resolver entry. */
547
+ if (typeof resolve !== 'function' || (!isJit && patchedResolvers.has(resolve))) return resolve
548
+
549
+ // Replace a schema wrapper with the execution-local JIT variant instead of nesting both.
550
+ resolve = unwrapResolve(resolve)
551
+ if (isJit) {
552
+ const jitResolver = jitResolvers.get(resolve)
553
+ if (jitResolver !== undefined) return jitResolver
554
+ }
385
555
 
386
556
  function resolveAsync (source, args, contextValue, info) {
387
557
  const hasIastSub = iastResolveCh.hasSubscribers
@@ -390,48 +560,57 @@ function wrapResolve (resolve) {
390
560
  // Combined fast-path: depth=0 AND no IAST/AppSec subscriber means nothing
391
561
  // to do — skip rootCtx lookup, path walk, publish gates.
392
562
  if (depthDisabled && !hasIastSub && !hasResolverSub) {
393
- return resolve.apply(this, arguments)
563
+ return invokeResolver(resolve, this, arguments, isJit)
394
564
  }
395
565
 
396
- const rootCtx = contexts.get(contextValue) ?? legacyStorage.getStore()?.graphqlRootCtx
397
- if (!rootCtx) return resolve.apply(this, arguments)
566
+ const rootCtx = isJit
567
+ ? legacyStorage.getStore()?.graphqlRootCtx
568
+ : contexts.get(contextValue) ?? legacyStorage.getStore()?.graphqlRootCtx
569
+ if (!rootCtx) return invokeResolver(resolve, this, arguments, isJit)
398
570
 
399
571
  const infoPath = info?.path
400
572
  const config = rootCtx.config
573
+ const traceResolver = !depthDisabled && shouldInstrumentNode(config, infoPath)
401
574
 
402
575
  // pathString built incrementally off the parent's cached value
403
576
  // (rootCtx.pathCache, keyed by path node) — avoids re-walking the whole
404
577
  // path linked-list on every resolver call, which is O(depth) per call for
405
- // deeply nested resolvers. Shared between the IAST publish and the field
406
- // record. Collapse-aware: list-index segments become '*'.
578
+ // deeply nested resolvers. Collapse-aware: list-index segments become '*'.
407
579
  let pathString
408
580
  let collapsedKey
409
- if (infoPath) {
410
- pathString = buildCachedPathString(infoPath, rootCtx.pathCache, config.collapse)
581
+ if (infoPath && traceResolver) {
582
+ const pathCache = rootCtx.pathCache ??= new Map()
583
+ pathString = buildCachedPathString(infoPath, pathCache, config.collapse)
411
584
  if (config.collapse) collapsedKey = pathString
412
585
  }
413
586
 
414
587
  // IAST and AppSec subscribers see EVERY resolver call, regardless of
415
588
  // depth or collapse. The depth knob caps span creation only.
416
589
  if (hasIastSub) {
417
- iastResolveCh.publish({ rootCtx, args, info, path: pathToArray(infoPath), pathString })
590
+ iastResolveCh.publish({ rootCtx, args })
418
591
  }
419
592
  if (hasResolverSub) {
420
593
  resolverStartCh.publish({
421
594
  abortController: rootCtx.abortController,
422
- resolverInfo: getResolverInfo(info, args),
595
+ createResolverInfo,
596
+ info,
597
+ args,
423
598
  })
424
599
  }
425
600
 
426
- if (depthDisabled || !shouldInstrumentNode(config, infoPath)) {
427
- if (rootCtx.abortController?.signal.aborted) {
428
- throw new AbortError('Aborted')
429
- }
601
+ if (rootCtx.abortController?.signal.aborted) {
602
+ throw new AbortError('Aborted')
603
+ }
430
604
 
431
- return resolve.apply(this, arguments)
605
+ if (!traceResolver) {
606
+ return invokeResolver(resolve, this, arguments, isJit)
432
607
  }
433
608
 
434
- const fieldKey = config.collapse ? pathString : infoPath
609
+ // Compilations performed while tracing is disabled have no static JIT
610
+ // descriptor. Their resolver-info path nodes are recreated for each
611
+ // resolver, so use the stable path string to preserve span parenting.
612
+ const useStringKey = config.collapse || isJit
613
+ const fieldKey = useStringKey ? pathString : infoPath
435
614
  const parentTypeName = info.parentType.name
436
615
  let field = rootCtx.fields.get(fieldKey)
437
616
  const collapsedField = field
@@ -457,14 +636,13 @@ function wrapResolve (resolve) {
457
636
  parentTypeName,
458
637
  returnType: info.returnType,
459
638
  baseTypeName: getBaseTypeName(info.returnType),
460
- variableValues: info.variableValues,
461
- args,
639
+ endTime: PENDING_FIELD_END_TIME,
462
640
  infoPath,
641
+ jitPathKey: isJit,
463
642
  pathString,
464
643
  collapsedKey: collapsedKey ?? pathString,
465
644
  span: null,
466
- // Set by startResolveSpan; currentStore is used by the first resolver
467
- // call only, siblings use parentStore (see the isFirst check below).
645
+ // Set by startResolveSpan; collapsed siblings reuse currentStore.
468
646
  parentStore: null,
469
647
  currentStore: null,
470
648
  }
@@ -487,38 +665,355 @@ function wrapResolve (resolve) {
487
665
  } else {
488
666
  rootCtx.fields.set(fieldKey, field)
489
667
  }
490
- }
491
-
492
- // Collapsed siblings still publish updateField (master's contract: one
493
- // publish per resolver call, even when the span is collapsed) and route
494
- // through callInAsyncScope so the abort signal stops them mid-flight. They
495
- // run in the parent store, not field.currentStore: the first sibling's
496
- // synchronous resolver already finished the shared graphql.resolve span, so
497
- // re-entering its store would parent user spans to a closed span.
498
- // eslint-disable-next-line unicorn/prefer-else-if -- Keep sibling early return outside first-field setup.
499
- if (!isFirst) {
500
- return callInAsyncScope(resolve, this, arguments, rootCtx.abortController, field.parentStore, (err) => {
501
- if (updateFieldCh.hasSubscribers) {
502
- updateFieldCh.publish({ rootCtx, field, error: err, pathString: field.pathString })
503
- }
504
- })
668
+ } else {
669
+ field.endTime = REUSED_FIELD_END_TIME
670
+ return callInCollapsedScope(
671
+ resolve,
672
+ this,
673
+ arguments,
674
+ field,
675
+ isJit
676
+ )
505
677
  }
506
678
 
507
679
  const executeSpan = rootCtx.executeSpan
508
680
  const startTime = executeSpan._getTime()
509
- const span = rootCtx.plugin.startResolveSpan(field, rootCtx, executeSpan, startTime)
681
+ rootCtx.plugin.startResolveSpan(field, rootCtx, executeSpan, startTime, info.variableValues)
682
+
683
+ /**
684
+ * @param {unknown} error
685
+ * @param {unknown} res
686
+ * @param {boolean} failed
687
+ */
688
+ const finishField = (error, res, failed) => {
689
+ rootCtx.plugin?.completeResolveSpan(rootCtx, field, error, res, failed)
690
+ }
691
+ return callInAsyncScope(resolve, this, arguments, field.currentStore, finishField, isJit)
692
+ }
510
693
 
511
- return callInAsyncScope(resolve, this, arguments, rootCtx.abortController, field.currentStore, (err, res) => {
512
- const endTime = executeSpan._getTime()
513
- rootCtx.plugin.finishResolveSpan(span, field, err, res, endTime || startTime)
514
- if (updateFieldCh.hasSubscribers) {
515
- updateFieldCh.publish({ rootCtx, field, error: err, pathString: field.pathString })
516
- }
694
+ if (isJit) {
695
+ jitResolvers.set(resolve, resolveAsync)
696
+ } else {
697
+ patchedResolvers.add(resolveAsync)
698
+ }
699
+ originalResolvers.set(resolveAsync, resolve)
700
+ return resolveAsync
701
+ }
702
+
703
+ /**
704
+ * @param {GraphQLFieldResolver} resolve
705
+ * @returns {GraphQLFieldResolver}
706
+ */
707
+ function unwrapResolve (resolve) {
708
+ return originalResolvers.get(resolve) ?? resolve
709
+ }
710
+
711
+ /**
712
+ * @param {object} rootCtx
713
+ * @param {number} descriptorId
714
+ * @param {GraphQLFieldResolver} resolve
715
+ * @param {unknown} self
716
+ * @param {unknown} source
717
+ * @param {Record<string, unknown>} args
718
+ * @param {unknown} contextValue
719
+ * @param {import('graphql').GraphQLResolveInfo} info
720
+ * @returns {unknown}
721
+ */
722
+ function resolveCompiledJitField (rootCtx, descriptorId, resolve, self, source, args, contextValue, info) {
723
+ const descriptor = rootCtx.jitPlan.fields[descriptorId]
724
+ const config = rootCtx.config
725
+
726
+ if (rootCtx.hasIastSub) {
727
+ iastResolveCh.publish({ rootCtx, args })
728
+ }
729
+ if (rootCtx.hasResolverSub) {
730
+ resolverStartCh.publish({
731
+ abortController: rootCtx.abortController,
732
+ createResolverInfo,
733
+ info,
734
+ args,
517
735
  })
518
736
  }
519
737
 
520
- patchedResolvers.add(resolveAsync)
521
- return resolveAsync
738
+ if (rootCtx.abortController?.signal.aborted) {
739
+ throw new AbortError('Aborted')
740
+ }
741
+
742
+ const depth = config.countListIndices ? descriptor.pathDepth : descriptor.selectionDepth
743
+ if (rootCtx.depthDisabled || (config.depth >= 0 && config.depth < depth)) {
744
+ return resolve.call(self, source, args, contextValue, info)
745
+ }
746
+
747
+ const path = config.collapse ? undefined : pathToArray(info.path)
748
+ const pathString = path ? path.join('.') : descriptor.collapsedPath
749
+ let field
750
+ if (config.collapse) {
751
+ field = rootCtx.jitFields[descriptor.id]
752
+ } else {
753
+ const fieldKey = `${descriptor.id}:${pathString}`
754
+ field = rootCtx.jitFieldsByPath.get(fieldKey)
755
+ }
756
+ if (field) {
757
+ field.endTime = REUSED_FIELD_END_TIME
758
+ if (legacyStorage.getStore() !== field.currentStore) {
759
+ legacyStorage.enterWith(field.currentStore)
760
+ }
761
+ return resolve.call(self, source, args, contextValue, info)
762
+ }
763
+
764
+ field = startJitField(
765
+ rootCtx,
766
+ descriptor,
767
+ pathString,
768
+ path ?? descriptor.collapsedPathSegments,
769
+ info.variableValues,
770
+ info.path
771
+ )
772
+
773
+ /**
774
+ * @param {unknown} error
775
+ * @param {unknown} result
776
+ */
777
+ const finishField = (error, result) => {
778
+ rootCtx.plugin?.completeResolveSpan(rootCtx, field, error, result)
779
+ }
780
+ return callCompiledJitInAsyncScope(
781
+ resolve,
782
+ self,
783
+ source,
784
+ args,
785
+ contextValue,
786
+ info,
787
+ field.currentStore,
788
+ finishField
789
+ )
790
+ }
791
+
792
+ /**
793
+ * @param {object} rootCtx
794
+ * @param {JitDescriptor} descriptor
795
+ * @param {string} pathString
796
+ * @param {(string | number)[]} path
797
+ * @param {Record<string, unknown> | undefined} variableValues
798
+ * @param {object | undefined} [infoPath]
799
+ * @returns {object}
800
+ */
801
+ function startJitField (rootCtx, descriptor, pathString, path, variableValues, infoPath) {
802
+ const field = {
803
+ fieldNode: descriptor.fieldNode,
804
+ fieldName: descriptor.fieldName,
805
+ parentTypeName: descriptor.parentTypeName,
806
+ returnType: descriptor.returnType,
807
+ baseTypeName: descriptor.baseTypeName,
808
+ endTime: PENDING_FIELD_END_TIME,
809
+ infoPath,
810
+ pathString,
811
+ collapsedKey: pathString,
812
+ resource: descriptor.resource,
813
+ span: null,
814
+ parentStore: null,
815
+ currentStore: null,
816
+ isSharedJitField: rootCtx.config.collapse && descriptor.pathDepth !== descriptor.selectionDepth,
817
+ tags: rootCtx.config.collapse && !rootCtx.config.source ? descriptor.tags : undefined,
818
+ }
819
+ if (rootCtx.config.collapse) {
820
+ rootCtx.jitFields[descriptor.id] = field
821
+ } else {
822
+ rootCtx.jitFieldsByPath.set(`${descriptor.id}:${pathString}`, field)
823
+ }
824
+
825
+ let parentField = null
826
+ if (descriptor.parentId !== undefined) {
827
+ if (rootCtx.config.collapse) {
828
+ parentField = rootCtx.jitFields[descriptor.parentId]
829
+ } else {
830
+ const parentPath = path.slice(0, -1)
831
+ while (typeof parentPath.at(-1) === 'number') parentPath.pop()
832
+ parentField = rootCtx.jitFieldsByPath.get(`${descriptor.parentId}:${parentPath.join('.')}`)
833
+ }
834
+ }
835
+ const executeSpan = rootCtx.executeSpan
836
+ rootCtx.plugin.startResolveSpan(field, rootCtx, executeSpan, executeSpan._getTime(), variableValues, parentField)
837
+ return field
838
+ }
839
+
840
+ /**
841
+ * @param {object} rootCtx
842
+ * @param {number} descriptorId
843
+ * @param {unknown} error
844
+ */
845
+ function recordJitResolverError (rootCtx, descriptorId, error) {
846
+ const field = rootCtx?.jitFields?.[descriptorId]
847
+ if (!field?.isSharedJitField) return
848
+
849
+ recordResolveError(field, error)
850
+ }
851
+
852
+ /**
853
+ * @param {object} rootCtx
854
+ * @param {number} descriptorId
855
+ * @param {Record<string, unknown>} source
856
+ * @param {(string | number)[] | undefined} path
857
+ * @returns {unknown}
858
+ */
859
+ function resolveJitDefault (rootCtx, descriptorId, source, path) {
860
+ const descriptor = rootCtx.jitPlan.fields[descriptorId]
861
+ const pathString = path ? path.join('.') : descriptor.collapsedPath
862
+ const field = startJitField(
863
+ rootCtx,
864
+ descriptor,
865
+ pathString,
866
+ path ?? descriptor.collapsedPathSegments,
867
+ rootCtx.variableValues
868
+ )
869
+ if (rootCtx.config.collapse && descriptor.pathDepth !== descriptor.selectionDepth) {
870
+ field.endTime = REUSED_FIELD_END_TIME
871
+ }
872
+
873
+ let result
874
+ try {
875
+ result = legacyStorage.run(field.currentStore, () => source[descriptor.fieldName])
876
+ } catch (error) {
877
+ rootCtx.plugin.completeResolveSpan(rootCtx, field, error)
878
+ throw error
879
+ }
880
+
881
+ rootCtx.plugin.completeResolveSpan(rootCtx, field, undefined, result)
882
+ return result
883
+ }
884
+
885
+ /**
886
+ * @param {object} rootCtx
887
+ * @param {number} descriptorId
888
+ * @param {Record<string, unknown>} source
889
+ * @param {(string | number)[] | undefined} path
890
+ * @returns {unknown}
891
+ */
892
+ function readJitDefaultInScope (rootCtx, descriptorId, source, path) {
893
+ const descriptor = rootCtx.jitPlan.fields[descriptorId]
894
+ const field = rootCtx.config.collapse
895
+ ? rootCtx.jitFields?.[descriptorId]
896
+ : rootCtx.jitFieldsByPath?.get(`${descriptorId}:${path.join('.')}`)
897
+ if (!field) return source[descriptor.fieldName]
898
+
899
+ if (legacyStorage.getStore() !== field.currentStore) {
900
+ legacyStorage.enterWith(field.currentStore)
901
+ }
902
+ return source[descriptor.fieldName]
903
+ }
904
+
905
+ /**
906
+ * @param {object} rootCtx
907
+ * @param {number} descriptorId
908
+ * @param {Record<string, unknown>} source
909
+ * @param {(string | number)[] | undefined} path
910
+ * @param {ArgumentFactory | undefined} argumentFactory
911
+ * @returns {unknown}
912
+ */
913
+ function resolveJitDefaultInvocation (rootCtx, descriptorId, source, path, argumentFactory) {
914
+ const descriptor = rootCtx.jitPlan.fields[descriptorId]
915
+ if (rootCtx.hasIastSub || rootCtx.hasResolverSub) {
916
+ if (rootCtx.hasIastSub) {
917
+ iastResolveCh.publish({
918
+ rootCtx,
919
+ args: getJitDefaultArguments(rootCtx, descriptor, argumentFactory, true),
920
+ })
921
+ }
922
+ if (rootCtx.hasResolverSub) {
923
+ resolverStartCh.publish({
924
+ abortController: rootCtx.abortController,
925
+ createResolverInfo: createJitDefaultResolverInfo,
926
+ rootCtx,
927
+ descriptor,
928
+ argumentFactory,
929
+ })
930
+ }
931
+ }
932
+ if (rootCtx.abortController?.signal.aborted) {
933
+ throw new AbortError('Aborted')
934
+ }
935
+
936
+ const depth = rootCtx.config.countListIndices ? descriptor.pathDepth : descriptor.selectionDepth
937
+ if (rootCtx.depthDisabled || (rootCtx.config.depth >= 0 && rootCtx.config.depth < depth)) {
938
+ if (!rootCtx.depthDisabled && rootCtx.config.collapse) rootCtx.jitFields[descriptorId] = false
939
+ return source[descriptor.fieldName]
940
+ }
941
+
942
+ const pathString = path ? path.join('.') : descriptor.collapsedPath
943
+ const field = rootCtx.config.collapse
944
+ ? rootCtx.jitFields[descriptorId]
945
+ : rootCtx.jitFieldsByPath.get(`${descriptorId}:${pathString}`)
946
+ if (field === undefined) {
947
+ return resolveJitDefault(rootCtx, descriptorId, source, path)
948
+ }
949
+ return readJitDefaultInScope(rootCtx, descriptorId, source, path)
950
+ }
951
+
952
+ /**
953
+ * @param {object} rootCtx
954
+ * @param {JitDescriptor} descriptor
955
+ * @param {ArgumentFactory | undefined} argumentFactory
956
+ * @param {boolean} mutable
957
+ * @returns {Record<string, unknown> | undefined}
958
+ */
959
+ function getJitDefaultArguments (rootCtx, descriptor, argumentFactory, mutable) {
960
+ if (mutable) {
961
+ return argumentFactory === undefined
962
+ ? {}
963
+ : argumentFactory(rootCtx.variableValues, cloneArgumentValue)
964
+ }
965
+ if (descriptor.hasArgumentVariables) {
966
+ return argumentFactory(rootCtx.variableValues)
967
+ }
968
+ return descriptor.staticArguments
969
+ }
970
+
971
+ /**
972
+ * @param {unknown} value
973
+ * @param {WeakMap<object, object>} [clones]
974
+ * @returns {unknown}
975
+ */
976
+ function cloneArgumentValue (value, clones) {
977
+ if (value === null || typeof value !== 'object') return value
978
+
979
+ // Lists get their own branch: Object.keys materializes the index strings and
980
+ // the clone is then filled through them instead of by index.
981
+ if (Array.isArray(value)) {
982
+ const { length } = value
983
+ if (length === 0) return value
984
+
985
+ clones ??= new WeakMap()
986
+ const cached = clones.get(value)
987
+ if (cached !== undefined) return cached
988
+
989
+ const clone = new Array(length)
990
+ clones.set(value, clone)
991
+ for (let index = 0; index < length; index++) {
992
+ clone[index] = cloneArgumentValue(value[index], clones)
993
+ }
994
+ return clone
995
+ }
996
+
997
+ const keys = Object.keys(value)
998
+ if (keys.length === 0) return value
999
+
1000
+ clones ??= new WeakMap()
1001
+ const cached = clones.get(value)
1002
+ if (cached !== undefined) return cached
1003
+
1004
+ const clone = {}
1005
+ clones.set(value, clone)
1006
+ for (const name of keys) {
1007
+ clone[name] = cloneArgumentValue(value[name], clones)
1008
+ }
1009
+ return clone
1010
+ }
1011
+
1012
+ /**
1013
+ * @param {GraphQLFieldResolver} resolve
1014
+ */
1015
+ function wrapJitResolve (resolve) {
1016
+ return wrapResolve(resolve, true)
522
1017
  }
523
1018
 
524
1019
  function wrapFields (type, schema) {
@@ -575,27 +1070,161 @@ function wrapFieldType (field, schema) {
575
1070
  wrapFields(unwrapped, schema)
576
1071
  }
577
1072
 
578
- // Runs the resolver inside `store`, including any code after an internal
579
- // `await`. A `.then()` the caller attaches afterward runs outside `store`.
580
- function callInAsyncScope (fn, thisArg, args, abortController, store, cb) {
581
- if (abortController?.signal.aborted) {
582
- cb(null, null)
583
- throw new AbortError('Aborted')
1073
+ /**
1074
+ * @param {GraphQLFieldResolver} resolve
1075
+ * @param {unknown} self
1076
+ * @param {ArgumentsList} args
1077
+ * @param {boolean} isJit
1078
+ * @returns {unknown}
1079
+ */
1080
+ function invokeResolver (resolve, self, args, isJit) {
1081
+ return isJit
1082
+ ? resolve.call(self, args[0], args[1], args[2], args[3])
1083
+ : resolve.apply(self, args)
1084
+ }
1085
+
1086
+ /**
1087
+ * @param {GraphQLFieldResolver} fn
1088
+ * @param {unknown} thisArg
1089
+ * @param {ArgumentsList} args
1090
+ * @param {object} field
1091
+ * @param {boolean} isJit
1092
+ * @returns {unknown}
1093
+ */
1094
+ function callInCollapsedScope (fn, thisArg, args, field, isJit) {
1095
+ if (!isJit) {
1096
+ field.asyncResource ??= legacyStorage.run(field.currentStore, createResolverAsyncResource)
1097
+ return field.asyncResource.runInAsyncScope(invokeResolver, undefined, fn, thisArg, args, false)
584
1098
  }
585
1099
 
1100
+ if (legacyStorage.getStore() !== field.currentStore) {
1101
+ legacyStorage.enterWith(field.currentStore)
1102
+ }
1103
+ return invokeResolver(fn, thisArg, args, isJit)
1104
+ }
1105
+
1106
+ function createResolverAsyncResource () {
1107
+ return new AsyncResource('dd-trace:graphql:resolve', { requireManualDestroy: true })
1108
+ }
1109
+
1110
+ /**
1111
+ * @param {GraphQLFieldResolver} resolve
1112
+ * @param {unknown} self
1113
+ * @param {unknown} source
1114
+ * @param {Record<string, unknown>} args
1115
+ * @param {unknown} contextValue
1116
+ * @param {import('graphql').GraphQLResolveInfo} info
1117
+ * @returns {unknown}
1118
+ */
1119
+ function invokeCompiledJitResolver (resolve, self, source, args, contextValue, info) {
1120
+ return resolve.call(self, source, args, contextValue, info)
1121
+ }
1122
+
1123
+ /**
1124
+ * @param {GraphQLFieldResolver} resolve
1125
+ * @param {unknown} self
1126
+ * @param {unknown} source
1127
+ * @param {Record<string, unknown>} args
1128
+ * @param {unknown} contextValue
1129
+ * @param {import('graphql').GraphQLResolveInfo} info
1130
+ * @param {object} store
1131
+ * @param {(error: unknown, result?: unknown) => void} callback
1132
+ * @returns {unknown}
1133
+ */
1134
+ function callCompiledJitInAsyncScope (resolve, self, source, args, contextValue, info, store, callback) {
586
1135
  try {
587
- const result = legacyStorage.run(store, () => fn.apply(thisArg, args))
588
- if (typeof result?.then === 'function') {
589
- return result.then(
590
- res => { cb(null, res); return res },
591
- err => { cb(err); throw err }
592
- )
1136
+ const result = legacyStorage.run(
1137
+ store,
1138
+ invokeCompiledJitResolver,
1139
+ resolve,
1140
+ self,
1141
+ source,
1142
+ args,
1143
+ contextValue,
1144
+ info
1145
+ )
1146
+ if (result !== null && typeof result === 'object' && typeof result.then === 'function') {
1147
+ return observeThenable(result, callback)
1148
+ }
1149
+ callback(null, result)
1150
+ return result
1151
+ } catch (error) {
1152
+ callback(error)
1153
+ throw error
1154
+ }
1155
+ }
1156
+
1157
+ /**
1158
+ * Runs the resolver inside `store`, including any code after an internal
1159
+ * `await`. A `.then()` the caller attaches afterward runs outside `store`.
1160
+ *
1161
+ * @param {GraphQLFieldResolver} fn
1162
+ * @param {unknown} thisArg
1163
+ * @param {ArgumentsList} args
1164
+ * @param {object} store
1165
+ * @param {(error: unknown, result?: unknown, failed?: boolean) => void} callback
1166
+ * @param {boolean} isJit
1167
+ * @returns {unknown}
1168
+ */
1169
+ function callInAsyncScope (fn, thisArg, args, store, callback, isJit) {
1170
+ try {
1171
+ const result = legacyStorage.run(store, invokeResolver, fn, thisArg, args, isJit)
1172
+ if (typeof result?.then === 'function' && (!isJit || (result !== null && typeof result === 'object'))) {
1173
+ return observeThenable(result, callback)
593
1174
  }
594
- cb(null, result)
1175
+ callback(null, result)
595
1176
  return result
596
- } catch (err) {
597
- cb(err)
598
- throw err
1177
+ } catch (error) {
1178
+ callback(error, undefined, true)
1179
+ throw error
1180
+ }
1181
+ }
1182
+
1183
+ /**
1184
+ * Observes settlement without standing between the consumer and the resolver's
1185
+ * own `then`. Three constraints hold at once, each with its own regression test:
1186
+ * the foreign `then` runs exactly once (`should not re-execute thenables from
1187
+ * resolvers` — a Mongoose `Query` throws on the second call), the return value
1188
+ * stays then-able, and the consumer receives whatever the foreign `then` returned.
1189
+ *
1190
+ * `return thenable.then(onSettled)` satisfies the first two and breaks the third:
1191
+ * only `Promise.prototype.then` is guaranteed to return a promise, and
1192
+ * `await`/`Promise.resolve` discard that return value, so a thenable returning
1193
+ * `this` or a plain value is legal and invisible everywhere except here — see
1194
+ * `CustomThenableSuccess` in jit.spec.js, which resolves with 'actual' but returns
1195
+ * 'wrong'. Forwarding `then` lets the consumer make that single call itself and
1196
+ * get its real return value, while the settlement callbacks pass through us.
1197
+ *
1198
+ * @param {Thenable} thenable
1199
+ * @param {(error: unknown, result?: unknown, failed?: boolean) => void} callback
1200
+ * @returns {Thenable}
1201
+ */
1202
+ function observeThenable (thenable, callback) {
1203
+ return {
1204
+ /**
1205
+ * @param {ThenableCallback} [onFulfilled]
1206
+ * @param {ThenableCallback} [onRejected]
1207
+ */
1208
+ // eslint-disable-next-line unicorn/no-thenable -- graphql-jit intentionally consumes this adapter as a thenable.
1209
+ then (onFulfilled, onRejected) {
1210
+ return thenable.then(
1211
+ /**
1212
+ * @param {unknown} result
1213
+ */
1214
+ result => {
1215
+ callback(null, result)
1216
+ return onFulfilled ? onFulfilled(result) : result
1217
+ },
1218
+ /**
1219
+ * @param {unknown} error
1220
+ */
1221
+ error => {
1222
+ callback(error, undefined, true)
1223
+ if (onRejected) return onRejected(error)
1224
+ throw error
1225
+ }
1226
+ )
1227
+ },
599
1228
  }
600
1229
  }
601
1230
 
@@ -670,7 +1299,7 @@ function shouldInstrumentNode (config, path) {
670
1299
 
671
1300
  function getParentField (rootCtx, field) {
672
1301
  for (let curr = field.infoPath?.prev; curr; curr = curr.prev) {
673
- const fieldKey = rootCtx.config.collapse ? rootCtx.pathCache.get(curr) : curr
1302
+ const fieldKey = rootCtx.config.collapse || field.jitPathKey ? rootCtx.pathCache.get(curr) : curr
674
1303
  const innerField = rootCtx.fields.get(fieldKey)
675
1304
  if (innerField) {
676
1305
  if (curr.typename === undefined) {
@@ -691,15 +1320,40 @@ function getParentField (rootCtx, field) {
691
1320
  return null
692
1321
  }
693
1322
 
694
- // Build the resolverInfo payload that AppSec's datadog:graphql:resolver:start
695
- // subscriber expects: { [fieldName]: { ...args, ...directives } }.
1323
+ /**
1324
+ * @param {{ info: import('graphql').GraphQLResolveInfo, args: Record<string, unknown> }} data
1325
+ * @returns {Record<string, Record<string, unknown>> | null}
1326
+ */
1327
+ function createResolverInfo ({ info, args }) {
1328
+ return getResolverInfo(info, args)
1329
+ }
1330
+
1331
+ /**
1332
+ * @param {{
1333
+ * rootCtx: object,
1334
+ * descriptor: JitDescriptor,
1335
+ * argumentFactory: ArgumentFactory | undefined
1336
+ * }} data
1337
+ * @returns {Record<string, Record<string, unknown>> | null}
1338
+ */
1339
+ function createJitDefaultResolverInfo ({ rootCtx, descriptor, argumentFactory }) {
1340
+ const args = getJitDefaultArguments(rootCtx, descriptor, argumentFactory, false)
1341
+
1342
+ return getResolverInfo(descriptor, args)
1343
+ }
1344
+
1345
+ /**
1346
+ * @param {{ fieldName: string, fieldNodes?: readonly import('graphql').FieldNode[] }} info
1347
+ * @param {Record<string, unknown> | undefined} args
1348
+ * @returns {Record<string, Record<string, unknown>> | null}
1349
+ */
696
1350
  function getResolverInfo (info, args) {
697
1351
  let resolverVars = args ? { ...args } : undefined
698
1352
 
699
1353
  const directives = info.fieldNodes?.[0]?.directives
700
1354
  if (Array.isArray(directives)) {
701
1355
  for (const directive of directives) {
702
- if (directive.arguments.length === 0) continue
1356
+ if (!directive.arguments?.length) continue
703
1357
 
704
1358
  const argList = {}
705
1359
  for (const argument of directive.arguments) {
@@ -766,12 +1420,154 @@ function isWeakMapKey (value) {
766
1420
  return value !== null && (typeof value === 'object' || typeof value === 'function')
767
1421
  }
768
1422
 
769
- // Unwrap GraphQL List/NonNull wrappers to get the underlying named type's name.
770
- // e.g. [Human] 'Human', [Pet!] → 'Pet', String → 'String'
771
- function getBaseTypeName (type) {
772
- let cursor = type
773
- while (cursor && cursor.ofType) cursor = cursor.ofType
774
- return cursor?.name
1423
+ /**
1424
+ * @param {unknown} result
1425
+ * @returns {object | undefined}
1426
+ */
1427
+ function getIncrementalIterator (result) {
1428
+ if (result === null || typeof result !== 'object') return
1429
+
1430
+ if ('initialResult' in result && typeof result.subsequentResults?.next === 'function') {
1431
+ return result.subsequentResults
1432
+ }
1433
+ if (typeof result.next === 'function' && typeof result[Symbol.asyncIterator] === 'function') {
1434
+ return result
1435
+ }
1436
+ }
1437
+
1438
+ /**
1439
+ * @param {object} iterator
1440
+ * @param {(result?: unknown, error?: unknown, cancelled?: boolean, complete?: boolean) => void} observe
1441
+ */
1442
+ function observeIncrementalIterator (iterator, observe) {
1443
+ let activeObserver = observe
1444
+
1445
+ /**
1446
+ * @param {unknown} [result]
1447
+ * @param {unknown} [error]
1448
+ * @param {boolean} [cancelled]
1449
+ * @param {boolean} [complete]
1450
+ */
1451
+ function notify (result, error, cancelled, complete) {
1452
+ if (!activeObserver) return
1453
+
1454
+ const callback = activeObserver
1455
+ if (complete) activeObserver = undefined
1456
+ callback(result, error, cancelled, complete)
1457
+ }
1458
+
1459
+ // Orchestrion cannot reach Promise-resolved or nested iterators. Shimmer adds measurable work to each execution.
1460
+ wrapIteratorMethod(iterator, 'next', false, notify)
1461
+ wrapIteratorMethod(iterator, 'return', true, notify)
1462
+ wrapIteratorMethod(iterator, 'throw', true, notify)
1463
+ wrapIteratorMethod(iterator, asyncDisposeSymbol, true, notify)
1464
+ }
1465
+
1466
+ /**
1467
+ * @param {object} iterator
1468
+ * @param {string | symbol} name
1469
+ * @param {boolean} terminal
1470
+ * @param {(result?: unknown, error?: unknown, cancelled?: boolean, complete?: boolean) => void} notify
1471
+ */
1472
+ function wrapIteratorMethod (iterator, name, terminal, notify) {
1473
+ const method = iterator[name]
1474
+ if (typeof method !== 'function') return
1475
+
1476
+ iterator[name] = function () {
1477
+ const result = method.apply(this, arguments)
1478
+ result.then((value) => {
1479
+ const complete = terminal || value?.done || value?.value?.hasNext === false
1480
+ notify(value?.value, undefined, terminal, complete)
1481
+ }, error => notify(undefined, error, true, true))
1482
+ return result
1483
+ }
1484
+ }
1485
+
1486
+ /**
1487
+ * @param {object} config
1488
+ * @param {DatadogSpan} span
1489
+ * @param {unknown} result
1490
+ */
1491
+ function tagIncrementalResult (config, span, result) {
1492
+ if (result === null || typeof result !== 'object') return
1493
+
1494
+ tagExecutionErrors(config, span, result.errors)
1495
+ if (result.incremental) {
1496
+ for (const entry of result.incremental) {
1497
+ tagExecutionErrors(config, span, entry.errors)
1498
+ }
1499
+ }
1500
+ }
1501
+
1502
+ /**
1503
+ * @param {object} config
1504
+ * @param {DatadogSpan} span
1505
+ * @param {unknown[] | undefined} errors
1506
+ */
1507
+ function tagExecutionErrors (config, span, errors) {
1508
+ if (!errors?.length) return
1509
+
1510
+ if (!span.context().getTag('error')) span.setTag('error', errors[0])
1511
+ for (const error of errors) {
1512
+ extractErrorIntoSpanEvent(config, span, error)
1513
+ }
1514
+ }
1515
+
1516
+ /**
1517
+ * @param {object} rootCtx
1518
+ * @param {boolean} [finishPendingFields]
1519
+ */
1520
+ function releaseRootContext (rootCtx, finishPendingFields) {
1521
+ const endTime = rootCtx.executeSpan._getTime()
1522
+ if (rootCtx.resolveFields !== undefined) {
1523
+ if (rootCtx.resolveHooksPending || rootCtx.jitResolveHooksPending) {
1524
+ runResolveHooks(rootCtx)
1525
+ }
1526
+ for (let field = rootCtx.resolveFields; field; field = field.nextResolveField) {
1527
+ field.asyncResource?.emitDestroy()
1528
+ field.span.finish(field.endTime > PENDING_FIELD_END_TIME ? field.endTime : endTime)
1529
+ }
1530
+ } else if (finishPendingFields && rootCtx.fields !== undefined) {
1531
+ for (const field of rootCtx.fields.values()) {
1532
+ field.span.finish(endTime)
1533
+ }
1534
+ }
1535
+
1536
+ // Resolver-created async resources retain copied stores that all share this owner.
1537
+ for (const key of Object.keys(rootCtx)) {
1538
+ rootCtx[key] = undefined
1539
+ }
1540
+ }
1541
+
1542
+ /**
1543
+ * @param {object} rootCtx
1544
+ */
1545
+ function runResolveHooks (rootCtx) {
1546
+ rootCtx.resolveHooksPending = undefined
1547
+ rootCtx.jitResolveHooksPending = undefined
1548
+
1549
+ for (let field = rootCtx.resolveFields; field; field = field.nextResolveField) {
1550
+ if (field.resolveHookContext === undefined) continue
1551
+
1552
+ rootCtx.config.hooks.resolve(field.span, field.resolveHookContext)
1553
+ field.resolveHookContext = undefined
1554
+ }
1555
+ }
1556
+
1557
+ /**
1558
+ * @param {object} field
1559
+ * @param {unknown} error
1560
+ * @param {unknown} result
1561
+ * @returns {{ fieldName: string, path: string, error: unknown, result: unknown }}
1562
+ */
1563
+ function createResolveHookContext (field, error, result) {
1564
+ return {
1565
+ fieldName: field.fieldName,
1566
+ path: field.pathString,
1567
+ error: error || null,
1568
+ // Any thenable from any realm must stay undefined so the hook does not see an unresolved promise.
1569
+ result: error || typeof result?.then === 'function' ? undefined : result,
1570
+ }
775
1571
  }
776
1572
 
777
1573
  // Fallback resolver used when graphql.execute() is called without an explicit
@@ -801,3 +1597,9 @@ function addVariableTags (config, span, variableValues) {
801
1597
  }
802
1598
 
803
1599
  module.exports = GraphQLExecutePlugin
1600
+ module.exports.readJitDefaultInScope = readJitDefaultInScope
1601
+ module.exports.recordJitResolverError = recordJitResolverError
1602
+ module.exports.resolveCompiledJitField = resolveCompiledJitField
1603
+ module.exports.resolveJitDefaultInvocation = resolveJitDefaultInvocation
1604
+ module.exports.unwrapJitResolve = unwrapResolve
1605
+ module.exports.wrapJitResolve = wrapJitResolve