dd-trace 5.103.0 → 5.105.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 (213) hide show
  1. package/LICENSE-3rdparty.csv +90 -102
  2. package/index.d.ts +107 -6
  3. package/package.json +18 -17
  4. package/packages/datadog-core/src/storage.js +1 -1
  5. package/packages/datadog-instrumentations/src/aerospike.js +1 -1
  6. package/packages/datadog-instrumentations/src/ai.js +8 -7
  7. package/packages/datadog-instrumentations/src/aws-sdk.js +15 -2
  8. package/packages/datadog-instrumentations/src/azure-cosmos.js +7 -0
  9. package/packages/datadog-instrumentations/src/azure-functions.js +3 -0
  10. package/packages/datadog-instrumentations/src/cassandra-driver.js +5 -2
  11. package/packages/datadog-instrumentations/src/cucumber.js +181 -35
  12. package/packages/datadog-instrumentations/src/dns.js +54 -18
  13. package/packages/datadog-instrumentations/src/elasticsearch.js +4 -4
  14. package/packages/datadog-instrumentations/src/fastify.js +142 -82
  15. package/packages/datadog-instrumentations/src/graphql.js +188 -67
  16. package/packages/datadog-instrumentations/src/grpc/client.js +48 -32
  17. package/packages/datadog-instrumentations/src/helpers/ai-messages.js +322 -14
  18. package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +1 -1
  19. package/packages/datadog-instrumentations/src/helpers/hooks.js +4 -0
  20. package/packages/datadog-instrumentations/src/helpers/instrument.js +2 -1
  21. package/packages/datadog-instrumentations/src/helpers/kafka.js +17 -0
  22. package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +269 -0
  23. package/packages/datadog-instrumentations/src/helpers/promise-instrumentor.js +42 -0
  24. package/packages/datadog-instrumentations/src/helpers/register.js +1 -1
  25. package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +3 -2
  26. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +19 -6
  27. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/azure-cosmos.js +50 -0
  28. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -0
  29. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/langgraph.js +4 -2
  30. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +85 -0
  31. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +31 -229
  32. package/packages/datadog-instrumentations/src/hono.js +54 -3
  33. package/packages/datadog-instrumentations/src/http/client.js +2 -2
  34. package/packages/datadog-instrumentations/src/http/server.js +9 -4
  35. package/packages/datadog-instrumentations/src/ioredis.js +3 -3
  36. package/packages/datadog-instrumentations/src/jest/coverage-backfill.js +163 -0
  37. package/packages/datadog-instrumentations/src/jest.js +390 -183
  38. package/packages/datadog-instrumentations/src/kafkajs.js +140 -17
  39. package/packages/datadog-instrumentations/src/mariadb.js +1 -1
  40. package/packages/datadog-instrumentations/src/memcached.js +2 -1
  41. package/packages/datadog-instrumentations/src/mocha/main.js +399 -107
  42. package/packages/datadog-instrumentations/src/mocha/utils.js +48 -8
  43. package/packages/datadog-instrumentations/src/mongodb-core.js +1 -1
  44. package/packages/datadog-instrumentations/src/mongoose.js +10 -12
  45. package/packages/datadog-instrumentations/src/mysql.js +2 -2
  46. package/packages/datadog-instrumentations/src/mysql2.js +1 -1
  47. package/packages/datadog-instrumentations/src/nats.js +182 -0
  48. package/packages/datadog-instrumentations/src/nyc.js +38 -1
  49. package/packages/datadog-instrumentations/src/openai.js +33 -18
  50. package/packages/datadog-instrumentations/src/oracledb.js +6 -1
  51. package/packages/datadog-instrumentations/src/pg.js +1 -1
  52. package/packages/datadog-instrumentations/src/pino.js +17 -5
  53. package/packages/datadog-instrumentations/src/playwright.js +537 -297
  54. package/packages/datadog-instrumentations/src/router.js +80 -34
  55. package/packages/datadog-instrumentations/src/stripe.js +1 -1
  56. package/packages/datadog-instrumentations/src/vitest.js +246 -149
  57. package/packages/datadog-plugin-avsc/src/schema_iterator.js +1 -1
  58. package/packages/datadog-plugin-azure-cosmos/src/index.js +144 -0
  59. package/packages/datadog-plugin-azure-event-hubs/src/producer.js +1 -1
  60. package/packages/datadog-plugin-azure-functions/src/index.js +5 -2
  61. package/packages/datadog-plugin-azure-service-bus/src/producer.js +1 -1
  62. package/packages/datadog-plugin-bunyan/src/index.js +28 -0
  63. package/packages/datadog-plugin-cucumber/src/index.js +17 -3
  64. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +223 -45
  65. package/packages/datadog-plugin-cypress/src/support.js +69 -1
  66. package/packages/datadog-plugin-dns/src/lookup.js +8 -6
  67. package/packages/datadog-plugin-elasticsearch/src/index.js +28 -8
  68. package/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-push-subscription.js +1 -1
  69. package/packages/datadog-plugin-graphql/src/execute.js +2 -0
  70. package/packages/datadog-plugin-graphql/src/resolve.js +64 -67
  71. package/packages/datadog-plugin-graphql/src/utils.js +4 -1
  72. package/packages/datadog-plugin-http/src/server.js +40 -15
  73. package/packages/datadog-plugin-jest/src/index.js +11 -3
  74. package/packages/datadog-plugin-jest/src/util.js +15 -8
  75. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +1 -1
  76. package/packages/datadog-plugin-kafkajs/src/producer.js +35 -0
  77. package/packages/datadog-plugin-langgraph/src/stream.js +1 -1
  78. package/packages/datadog-plugin-mocha/src/index.js +19 -4
  79. package/packages/datadog-plugin-mongodb-core/src/index.js +311 -35
  80. package/packages/datadog-plugin-nats/src/consumer.js +43 -0
  81. package/packages/datadog-plugin-nats/src/index.js +20 -0
  82. package/packages/datadog-plugin-nats/src/producer.js +62 -0
  83. package/packages/datadog-plugin-nats/src/util.js +33 -0
  84. package/packages/datadog-plugin-next/src/index.js +5 -3
  85. package/packages/datadog-plugin-openai/src/tracing.js +15 -2
  86. package/packages/datadog-plugin-oracledb/src/index.js +13 -2
  87. package/packages/datadog-plugin-pino/src/index.js +42 -0
  88. package/packages/datadog-plugin-playwright/src/index.js +4 -4
  89. package/packages/datadog-plugin-protobufjs/src/schema_iterator.js +1 -1
  90. package/packages/datadog-plugin-redis/src/index.js +37 -2
  91. package/packages/datadog-plugin-rhea/src/producer.js +1 -1
  92. package/packages/datadog-plugin-router/src/index.js +33 -44
  93. package/packages/datadog-plugin-selenium/src/index.js +1 -1
  94. package/packages/datadog-plugin-undici/src/index.js +19 -0
  95. package/packages/datadog-plugin-vitest/src/index.js +24 -20
  96. package/packages/datadog-plugin-winston/src/index.js +30 -0
  97. package/packages/datadog-shimmer/src/shimmer.js +49 -21
  98. package/packages/dd-trace/src/aiguard/index.js +1 -1
  99. package/packages/dd-trace/src/aiguard/sdk.js +1 -1
  100. package/packages/dd-trace/src/appsec/api_security_sampler.js +1 -1
  101. package/packages/dd-trace/src/appsec/blocking.js +2 -2
  102. package/packages/dd-trace/src/appsec/index.js +11 -4
  103. package/packages/dd-trace/src/appsec/reporter.js +24 -11
  104. package/packages/dd-trace/src/appsec/sdk/user_blocking.js +1 -1
  105. package/packages/dd-trace/src/appsec/sdk/utils.js +1 -1
  106. package/packages/dd-trace/src/appsec/user_tracking.js +5 -4
  107. package/packages/dd-trace/src/baggage.js +7 -1
  108. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +0 -1
  109. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +25 -13
  110. package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -1
  111. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +5 -3
  112. package/packages/dd-trace/src/ci-visibility/test-optimization-cache.js +70 -6
  113. package/packages/dd-trace/src/config/generated-config-types.d.ts +7 -2
  114. package/packages/dd-trace/src/config/supported-configurations.json +36 -8
  115. package/packages/dd-trace/src/crashtracking/crashtracker.js +15 -3
  116. package/packages/dd-trace/src/datastreams/context.js +4 -2
  117. package/packages/dd-trace/src/datastreams/writer.js +2 -4
  118. package/packages/dd-trace/src/debugger/devtools_client/condition.js +5 -8
  119. package/packages/dd-trace/src/encode/0.4.js +124 -108
  120. package/packages/dd-trace/src/encode/0.5.js +114 -26
  121. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +57 -42
  122. package/packages/dd-trace/src/encode/agentless-json.js +4 -2
  123. package/packages/dd-trace/src/encode/coverage-ci-visibility.js +32 -13
  124. package/packages/dd-trace/src/encode/span-stats.js +16 -16
  125. package/packages/dd-trace/src/encode/tags-processors.js +16 -0
  126. package/packages/dd-trace/src/exporters/common/agents.js +3 -1
  127. package/packages/dd-trace/src/exporters/common/request.js +3 -1
  128. package/packages/dd-trace/src/id.js +17 -4
  129. package/packages/dd-trace/src/lambda/handler.js +2 -4
  130. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +1 -1
  131. package/packages/dd-trace/src/llmobs/plugins/genai/index.js +1 -1
  132. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +1 -1
  133. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +9 -7
  134. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  135. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +1 -1
  136. package/packages/dd-trace/src/llmobs/sdk.js +10 -16
  137. package/packages/dd-trace/src/llmobs/span_processor.js +3 -3
  138. package/packages/dd-trace/src/llmobs/tagger.js +9 -1
  139. package/packages/dd-trace/src/llmobs/telemetry.js +1 -1
  140. package/packages/dd-trace/src/llmobs/util.js +66 -3
  141. package/packages/dd-trace/src/log/index.js +1 -1
  142. package/packages/dd-trace/src/log/writer.js +3 -1
  143. package/packages/dd-trace/src/msgpack/chunk.js +394 -10
  144. package/packages/dd-trace/src/msgpack/index.js +96 -2
  145. package/packages/dd-trace/src/noop/span.js +3 -1
  146. package/packages/dd-trace/src/openfeature/encoding.js +70 -0
  147. package/packages/dd-trace/src/openfeature/flagging_provider.js +20 -0
  148. package/packages/dd-trace/src/openfeature/span-enrichment-hook.js +143 -0
  149. package/packages/dd-trace/src/openfeature/span-enrichment.js +149 -0
  150. package/packages/dd-trace/src/openfeature/writers/exposures.js +51 -20
  151. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +1 -1
  152. package/packages/dd-trace/src/opentelemetry/span-helpers.js +4 -3
  153. package/packages/dd-trace/src/opentelemetry/span.js +1 -1
  154. package/packages/dd-trace/src/opentracing/propagation/log.js +18 -7
  155. package/packages/dd-trace/src/opentracing/propagation/text_map.js +62 -67
  156. package/packages/dd-trace/src/opentracing/span.js +59 -19
  157. package/packages/dd-trace/src/opentracing/span_context.js +49 -0
  158. package/packages/dd-trace/src/plugins/apollo.js +3 -1
  159. package/packages/dd-trace/src/plugins/ci_plugin.js +23 -33
  160. package/packages/dd-trace/src/plugins/database.js +7 -6
  161. package/packages/dd-trace/src/plugins/index.js +4 -0
  162. package/packages/dd-trace/src/plugins/log_injection.js +56 -0
  163. package/packages/dd-trace/src/plugins/log_plugin.js +3 -46
  164. package/packages/dd-trace/src/plugins/outbound.js +1 -1
  165. package/packages/dd-trace/src/plugins/plugin.js +15 -17
  166. package/packages/dd-trace/src/plugins/tracing.js +48 -8
  167. package/packages/dd-trace/src/plugins/util/git.js +3 -1
  168. package/packages/dd-trace/src/plugins/util/test.js +318 -13
  169. package/packages/dd-trace/src/plugins/util/web.js +89 -64
  170. package/packages/dd-trace/src/priority_sampler.js +2 -2
  171. package/packages/dd-trace/src/profiling/profiler.js +2 -2
  172. package/packages/dd-trace/src/profiling/profilers/wall.js +10 -4
  173. package/packages/dd-trace/src/sampling_rule.js +7 -7
  174. package/packages/dd-trace/src/scope.js +7 -5
  175. package/packages/dd-trace/src/service-naming/extra-services.js +14 -0
  176. package/packages/dd-trace/src/service-naming/schemas/v0/messaging.js +10 -0
  177. package/packages/dd-trace/src/service-naming/schemas/v1/messaging.js +8 -0
  178. package/packages/dd-trace/src/service-naming/source-resolver.js +46 -0
  179. package/packages/dd-trace/src/span_format.js +190 -58
  180. package/packages/dd-trace/src/spanleak.js +1 -1
  181. package/packages/dd-trace/src/standalone/index.js +3 -3
  182. package/packages/dd-trace/src/tagger.js +0 -2
  183. package/vendor/dist/@apm-js-collab/code-transformer/index.js +70 -39
  184. package/vendor/dist/@datadog/sketches-js/LICENSE +10 -36
  185. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  186. package/vendor/dist/protobufjs/index.js +1 -1
  187. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  188. package/packages/dd-trace/src/msgpack/encoder.js +0 -308
  189. package/packages/dd-trace/src/plugins/structured_log_plugin.js +0 -9
  190. package/vendor/dist/opentracing/LICENSE +0 -201
  191. package/vendor/dist/opentracing/binary_carrier.d.ts +0 -11
  192. package/vendor/dist/opentracing/constants.d.ts +0 -61
  193. package/vendor/dist/opentracing/examples/demo/demo.d.ts +0 -2
  194. package/vendor/dist/opentracing/ext/tags.d.ts +0 -90
  195. package/vendor/dist/opentracing/functions.d.ts +0 -20
  196. package/vendor/dist/opentracing/global_tracer.d.ts +0 -14
  197. package/vendor/dist/opentracing/index.d.ts +0 -12
  198. package/vendor/dist/opentracing/index.js +0 -1
  199. package/vendor/dist/opentracing/mock_tracer/index.d.ts +0 -5
  200. package/vendor/dist/opentracing/mock_tracer/mock_context.d.ts +0 -13
  201. package/vendor/dist/opentracing/mock_tracer/mock_report.d.ts +0 -16
  202. package/vendor/dist/opentracing/mock_tracer/mock_span.d.ts +0 -50
  203. package/vendor/dist/opentracing/mock_tracer/mock_tracer.d.ts +0 -26
  204. package/vendor/dist/opentracing/noop.d.ts +0 -8
  205. package/vendor/dist/opentracing/reference.d.ts +0 -33
  206. package/vendor/dist/opentracing/span.d.ts +0 -147
  207. package/vendor/dist/opentracing/span_context.d.ts +0 -26
  208. package/vendor/dist/opentracing/test/api_compatibility.d.ts +0 -16
  209. package/vendor/dist/opentracing/test/mocktracer_implemenation.d.ts +0 -3
  210. package/vendor/dist/opentracing/test/noop_implementation.d.ts +0 -4
  211. package/vendor/dist/opentracing/test/opentracing_api.d.ts +0 -3
  212. package/vendor/dist/opentracing/test/unittest.d.ts +0 -2
  213. package/vendor/dist/opentracing/tracer.d.ts +0 -127
@@ -54,65 +54,119 @@ function wrapAddHook (addHook) {
54
54
 
55
55
  if (typeof fn !== 'function') return addHook.apply(this, arguments)
56
56
 
57
- arguments[arguments.length - 1] = shimmer.wrapFunction(fn, fn => function (request, reply, done) {
58
- const req = getReq(request)
59
- const ctx = { req }
60
-
61
- try {
62
- // done callback is always the last argument
63
- const doneCallback = arguments[arguments.length - 1]
64
-
65
- if (typeof doneCallback === 'function') {
66
- arguments[arguments.length - 1] = function (err) {
67
- ctx.error = err
68
- publishError(ctx)
69
-
70
- const hasCookies = request.cookies && Object.keys(request.cookies).length > 0
71
-
72
- if (cookieParserReadCh.hasSubscribers && hasCookies && !cookiesPublished.has(req)) {
73
- ctx.res = getRes(reply)
74
- ctx.abortController = new AbortController()
75
- ctx.cookies = request.cookies
76
-
77
- cookieParserReadCh.publish(ctx)
78
- cookiesPublished.add(req)
79
-
80
- if (ctx.abortController.signal.aborted) return
81
- }
82
-
83
- if (name === 'onRequest' || name === 'preParsing') {
84
- parsingContexts.set(req, ctx)
85
-
86
- return callbackFinishCh.runStores(ctx, () => {
87
- return doneCallback.apply(this, arguments)
88
- })
89
- }
90
- return doneCallback.apply(this, arguments)
91
- }
92
-
93
- return fn.apply(this, arguments)
94
- }
95
-
96
- const promise = fn.apply(this, arguments)
97
-
98
- if (promise && typeof promise.catch === 'function') {
99
- return promise.catch(err => {
100
- ctx.error = err
101
- return publishError(ctx)
102
- })
103
- }
104
-
105
- return promise
106
- } catch (e) {
107
- ctx.error = e
108
- throw publishError(ctx)
57
+ arguments[arguments.length - 1] = shimmer.wrapFunction(fn, fn => function wrappedHook () {
58
+ // Fast path: every fastify request invokes each addHook'd handler, so the wrap
59
+ // runs in the user's hot path. The only side effects this wrapper carries are
60
+ // the three channels below; when none of them have a subscriber (the default
61
+ // plugin config, and the steady state once appsec / cookie subscribers detach),
62
+ // the wrap has nothing to do, and a `fn.apply(this, arguments)` forward keeps
63
+ // V8's CallApplyArguments fast path intact.
64
+ //
65
+ // The previous shape mutated `arguments[arguments.length - 1]` to swap `done`.
66
+ // That mutation materialises the magical arguments object and disables V8
67
+ // inlining of the enclosing function. The slow path below builds a fresh args
68
+ // array instead so the hot fast path keeps a clean forward.
69
+ if (errorChannel.hasSubscribers || cookieParserReadCh.hasSubscribers || callbackFinishCh.hasSubscribers) {
70
+ return invokeHookWithContext(name, fn, this, arguments)
109
71
  }
72
+ return fn.apply(this, arguments)
110
73
  })
111
74
 
112
75
  return addHook.apply(this, arguments)
113
76
  })
114
77
  }
115
78
 
79
+ /**
80
+ * Slow path of {@link wrapAddHook}; entered only when at least one wrap-fed
81
+ * channel has a subscriber. Allocates the per-request context, rewraps `done`,
82
+ * and forwards to the user-supplied hook.
83
+ *
84
+ * @param {string} name Lifecycle phase the hook was registered against.
85
+ * @param {Function} fn User-supplied hook.
86
+ * @param {unknown} thisArg `this` Fastify passes to the hook.
87
+ * @param {ArrayLike<unknown>} args Fastify's positional args; the dispatcher always
88
+ * places `done` as the trailing positional (see fastify/lib/hooks.js hookIterator,
89
+ * onSendHookRunner, preParsingHookRunner, onRequestAbortHookRunner).
90
+ */
91
+ function invokeHookWithContext (name, fn, thisArg, args) {
92
+ const request = args[0]
93
+ const reply = args[1]
94
+ const req = getReq(request)
95
+ const ctx = { req }
96
+
97
+ try {
98
+ const lastArg = args[args.length - 1]
99
+
100
+ if (typeof lastArg === 'function') {
101
+ // Copy the args so we can swap the trailing `done` without touching the
102
+ // caller's magical arguments object. Fastify hook arities are 2 to 4
103
+ // across lifecycle phases, but `done` is always last.
104
+ const callArgs = [...args]
105
+ callArgs[callArgs.length - 1] = wrapHookDone(ctx, request, reply, req, name, lastArg)
106
+ return fn.apply(thisArg, callArgs)
107
+ }
108
+
109
+ const promise = fn.apply(thisArg, args)
110
+
111
+ if (promise && typeof promise.catch === 'function') {
112
+ return promise.catch(error => {
113
+ ctx.error = error
114
+ return publishError(ctx)
115
+ })
116
+ }
117
+
118
+ return promise
119
+ } catch (error) {
120
+ ctx.error = error
121
+ throw publishError(ctx)
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Per-request closure invoked when fastify resolves the user hook's `done`.
127
+ * Captures `ctx` plus the dispatcher-level fields needed to publish on the
128
+ * cookie / callback channels. The closure cannot be hoisted: fastify invokes
129
+ * `done` with a single `(err)` arg, so request / reply / req / name / doneCallback
130
+ * must close over rather than ride the call signature.
131
+ *
132
+ * @param {{ req: unknown, [key: string]: unknown }} ctx
133
+ * @param {{ cookies?: Record<string, unknown>, [key: string]: unknown }} request
134
+ * @param {object} reply
135
+ * @param {unknown} req
136
+ * @param {string} name
137
+ * @param {Function} doneCallback
138
+ */
139
+ function wrapHookDone (ctx, request, reply, req, name, doneCallback) {
140
+ return function wrappedDone (error) {
141
+ ctx.error = error
142
+ publishError(ctx)
143
+
144
+ const hasCookies = request.cookies && Object.keys(request.cookies).length > 0
145
+
146
+ if (cookieParserReadCh.hasSubscribers && hasCookies && !cookiesPublished.has(req)) {
147
+ ctx.res = getRes(reply)
148
+ ctx.abortController = new AbortController()
149
+ ctx.cookies = request.cookies
150
+
151
+ cookieParserReadCh.publish(ctx)
152
+ cookiesPublished.add(req)
153
+
154
+ if (ctx.abortController.signal.aborted) return
155
+ }
156
+
157
+ if (name === 'onRequest' || name === 'preParsing') {
158
+ parsingContexts.set(req, ctx)
159
+
160
+ if (callbackFinishCh.hasSubscribers) {
161
+ const self = this
162
+ const allArgs = arguments
163
+ return callbackFinishCh.runStores(ctx, () => doneCallback.apply(self, allArgs))
164
+ }
165
+ }
166
+ return doneCallback.apply(this, arguments)
167
+ }
168
+ }
169
+
116
170
  function onRequest (request, reply, done) {
117
171
  if (typeof done !== 'function') return
118
172
 
@@ -157,45 +211,51 @@ function preValidation (request, reply, done) {
157
211
  const ctx = parsingContexts.get(req)
158
212
  ctx.res = res
159
213
 
160
- const processInContext = () => {
161
- let abortController
214
+ if (!ctx) return processInContext(request, ctx, done, req)
162
215
 
163
- if (queryParamsReadCh.hasSubscribers && request.query) {
164
- abortController ??= new AbortController()
165
- ctx.abortController = abortController
166
- ctx.query = request.query
167
- queryParamsReadCh.publish(ctx)
168
-
169
- if (abortController.signal.aborted) return
170
- }
216
+ preValidationCh.runStores(ctx, processInContext, undefined, request, ctx, done, req)
217
+ }
171
218
 
172
- // Analyze body before schema validation
173
- if (bodyParserReadCh.hasSubscribers && request.body && !bodyPublished.has(req)) {
174
- abortController ??= new AbortController()
175
- ctx.abortController = abortController
176
- ctx.body = request.body
177
- bodyParserReadCh.publish(ctx)
219
+ /**
220
+ * @param {{ query?: object, body?: object, params?: object, [key: string]: unknown }} request
221
+ * @param {{ res?: object, abortController?: AbortController, [key: string]: unknown }} ctx
222
+ * @param {Function} done
223
+ * @param {unknown} req
224
+ */
225
+ function processInContext (request, ctx, done, req) {
226
+ let abortController
227
+
228
+ if (queryParamsReadCh.hasSubscribers && request.query) {
229
+ abortController ??= new AbortController()
230
+ ctx.abortController = abortController
231
+ ctx.query = request.query
232
+ queryParamsReadCh.publish(ctx)
233
+
234
+ if (abortController.signal.aborted) return
235
+ }
178
236
 
179
- bodyPublished.add(req)
237
+ // Analyze body before schema validation
238
+ if (bodyParserReadCh.hasSubscribers && request.body && !bodyPublished.has(req)) {
239
+ abortController ??= new AbortController()
240
+ ctx.abortController = abortController
241
+ ctx.body = request.body
242
+ bodyParserReadCh.publish(ctx)
180
243
 
181
- if (abortController.signal.aborted) return
182
- }
244
+ bodyPublished.add(req)
183
245
 
184
- if (pathParamsReadCh.hasSubscribers && request.params) {
185
- abortController ??= new AbortController()
186
- ctx.abortController = abortController
187
- ctx.params = request.params
188
- pathParamsReadCh.publish(ctx)
246
+ if (abortController.signal.aborted) return
247
+ }
189
248
 
190
- if (abortController.signal.aborted) return
191
- }
249
+ if (pathParamsReadCh.hasSubscribers && request.params) {
250
+ abortController ??= new AbortController()
251
+ ctx.abortController = abortController
252
+ ctx.params = request.params
253
+ pathParamsReadCh.publish(ctx)
192
254
 
193
- done()
255
+ if (abortController.signal.aborted) return
194
256
  }
195
257
 
196
- if (!ctx) return processInContext()
197
-
198
- preValidationCh.runStores(ctx, processInContext)
258
+ done()
199
259
  }
200
260
 
201
261
  function preParsing (request, reply, payload, done) {
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const { AsyncLocalStorage } = require('node:async_hooks')
4
+
3
5
  const shimmer = require('../../datadog-shimmer')
4
6
  const {
5
7
  addHook,
@@ -10,7 +12,13 @@ const ddGlobal = globalThis[Symbol.for('dd-trace')]
10
12
 
11
13
  /** cached objects */
12
14
 
15
+ // `contexts` is the fast resolver-side lookup; `executeCtx` is the fallback
16
+ // when `contextValue` is a primitive and cannot key a WeakMap.
13
17
  const contexts = new WeakMap()
18
+ const executeCtx = new AsyncLocalStorage()
19
+ // Tracks normalized args already instrumented in an outer wrap so graphql-yoga
20
+ // (which stacks `execute` + `normalizedExecutor`) only emits one span per call.
21
+ const instrumentedArgs = new WeakSet()
14
22
  const documentSources = new WeakMap()
15
23
  const patchedResolvers = new WeakSet()
16
24
  const patchedTypes = new WeakSet()
@@ -62,14 +70,17 @@ function getOperation (document, operationName) {
62
70
  function normalizeArgs (args, defaultFieldResolver) {
63
71
  if (args.length !== 1) return normalizePositional(args, defaultFieldResolver)
64
72
 
65
- args[0].contextValue ||= {}
66
- args[0].fieldResolver = wrapResolve(args[0].fieldResolver || defaultFieldResolver)
73
+ const original = args[0]
74
+ const normalized = {
75
+ ...original,
76
+ fieldResolver: wrapResolve(original.fieldResolver || defaultFieldResolver),
77
+ }
67
78
 
68
- return args[0]
79
+ args[0] = normalized
80
+ return normalized
69
81
  }
70
82
 
71
83
  function normalizePositional (args, defaultFieldResolver) {
72
- args[3] = args[3] || {} // contextValue
73
84
  args[6] = wrapResolve(args[6] || defaultFieldResolver) // fieldResolver
74
85
  args.length = Math.max(args.length, 7)
75
86
 
@@ -84,6 +95,12 @@ function normalizePositional (args, defaultFieldResolver) {
84
95
  }
85
96
  }
86
97
 
98
+ // `WeakMap.set` throws `TypeError` on a non-object key; `get`/`has`/`delete`
99
+ // silently miss. Skip the WeakMap entirely for non-keyable `contextValue`.
100
+ function isWeakMapKey (value) {
101
+ return value !== null && typeof value === 'object'
102
+ }
103
+
87
104
  function wrapParse (parse) {
88
105
  return function (source) {
89
106
  if (!parseStartCh.hasSubscribers) {
@@ -155,14 +172,21 @@ function wrapExecute (execute) {
155
172
  return exe.apply(this, arguments)
156
173
  }
157
174
 
175
+ // The outer wrap leaves its normalized args object in `arguments[0]`; on
176
+ // graphql-yoga's inner wrap that reference is already known here.
177
+ if (instrumentedArgs.has(arguments[0])) {
178
+ return exe.apply(this, arguments)
179
+ }
180
+
158
181
  const args = normalizeArgs(arguments, defaultFieldResolver)
159
182
  const schema = args.schema
160
183
  const document = args.document
161
184
  const source = documentSources.get(document)
162
185
  const contextValue = args.contextValue
186
+ const keyable = isWeakMapKey(contextValue)
163
187
  const operation = getOperation(document, args.operationName)
164
188
 
165
- if (contexts.has(contextValue)) {
189
+ if (keyable && contexts.has(contextValue)) {
166
190
  return exe.apply(this, arguments)
167
191
  }
168
192
 
@@ -171,19 +195,23 @@ function wrapExecute (execute) {
171
195
  args,
172
196
  docSource: source,
173
197
  source,
174
- fields: Object.create(null),
198
+ fields: new Map(),
175
199
  abortController: new AbortController(),
176
200
  }
177
201
 
202
+ // Only the object form leaves a stable single-object handle in
203
+ // `arguments[0]` for the inner wrap to see.
204
+ if (args === arguments[0]) instrumentedArgs.add(args)
205
+
178
206
  return startExecuteCh.runStores(ctx, () => {
179
207
  if (schema) {
180
208
  wrapFields(schema._queryType)
181
209
  wrapFields(schema._mutationType)
182
210
  }
183
211
 
184
- contexts.set(contextValue, ctx)
212
+ if (keyable) contexts.set(contextValue, ctx)
185
213
 
186
- return callInAsyncScope(exe, this, arguments, ctx.abortController, (err, res) => {
214
+ const finish = (err, res) => {
187
215
  if (finishResolveCh.hasSubscribers) finishResolvers(ctx)
188
216
 
189
217
  const error = err || (res && res.errors && res.errors[0])
@@ -194,8 +222,16 @@ function wrapExecute (execute) {
194
222
  }
195
223
 
196
224
  ctx.res = res
225
+ if (keyable) contexts.delete(contextValue)
226
+ instrumentedArgs.delete(args)
197
227
  finishExecuteCh.publish(ctx)
198
- })
228
+ }
229
+
230
+ // Skip the ALS entry on the common object-`contextValue` path; the
231
+ // resolver reaches `ctx` via the WeakMap there.
232
+ return keyable
233
+ ? callInAsyncScope(exe, this, arguments, ctx.abortController, finish)
234
+ : executeCtx.run(ctx, () => callInAsyncScope(exe, this, arguments, ctx.abortController, finish))
199
235
  })
200
236
  }
201
237
  }
@@ -207,18 +243,40 @@ function wrapResolve (resolve) {
207
243
  function resolveAsync (source, args, contextValue, info) {
208
244
  if (!startResolveCh.hasSubscribers) return resolve.apply(this, arguments)
209
245
 
210
- const ctx = contexts.get(contextValue)
246
+ // `WeakMap.get(primitive)` returns `undefined`, so the fallback covers
247
+ // executes that ran with a primitive `contextValue`.
248
+ const ctx = contexts.get(contextValue) ?? executeCtx.getStore()
211
249
 
250
+ /* istanbul ignore if: resolver invoked outside execute(), so no per-execute ctx was registered */
212
251
  if (!ctx) return resolve.apply(this, arguments)
213
252
 
214
253
  const field = assertField(ctx, info, args)
215
254
 
216
- return callInAsyncScope(resolve, this, arguments, ctx.abortController, (err) => {
217
- field.ctx.error = err
218
- field.ctx.info = info
219
- field.ctx.field = field
220
- updateFieldCh.publish(field.ctx)
221
- })
255
+ if (ctx.abortController.signal.aborted) {
256
+ publishResolverFinish(field, null)
257
+ throw new AbortError('Aborted')
258
+ }
259
+
260
+ try {
261
+ const result = resolve.call(this, source, args, contextValue, info)
262
+ if (result !== null && typeof result?.then === 'function') {
263
+ return result.then(
264
+ res => {
265
+ publishResolverFinish(field, null)
266
+ return res
267
+ },
268
+ error => {
269
+ publishResolverFinish(field, error)
270
+ throw error
271
+ }
272
+ )
273
+ }
274
+ publishResolverFinish(field, null)
275
+ return result
276
+ } catch (error) {
277
+ publishResolverFinish(field, error)
278
+ throw error
279
+ }
222
280
  }
223
281
 
224
282
  patchedResolvers.add(resolveAsync)
@@ -226,72 +284,130 @@ function wrapResolve (resolve) {
226
284
  return resolveAsync
227
285
  }
228
286
 
229
- function callInAsyncScope (fn, thisArg, args, abortController, cb) {
230
- cb = cb || (() => {})
287
+ /**
288
+ * @param {{ ctx: object, error: unknown }} field
289
+ * @param {unknown} error
290
+ */
291
+ function publishResolverFinish (field, error) {
292
+ const fieldCtx = field.ctx
293
+ fieldCtx.error = error
294
+ fieldCtx.field = field
295
+ updateFieldCh.publish(fieldCtx)
296
+ }
231
297
 
232
- if (abortController?.signal.aborted) {
298
+ function callInAsyncScope (fn, thisArg, args, abortController, cb) {
299
+ if (abortController.signal.aborted) {
233
300
  cb(null, null)
234
301
  throw new AbortError('Aborted')
235
302
  }
236
303
 
237
304
  try {
238
305
  const result = fn.apply(thisArg, args)
239
- if (result && typeof result.then === 'function') {
306
+ if (result !== null && typeof result?.then === 'function') {
240
307
  return result.then(
241
308
  res => {
242
309
  cb(null, res)
243
310
  return res
244
311
  },
245
- err => {
246
- cb(err)
247
- throw err
312
+ /* istanbul ignore next: graphql.execute() rejects only via custom executors (graphql-yoga / graphql-tools) */
313
+ error => {
314
+ cb(error)
315
+ throw error
248
316
  }
249
317
  )
250
318
  }
251
319
  cb(null, result)
252
320
  return result
253
- } catch (err) {
254
- cb(err)
255
- throw err
321
+ } catch (error) {
322
+ cb(error)
323
+ throw error
256
324
  }
257
325
  }
258
326
 
259
- function pathToArray (path) {
260
- let length = 0
261
- for (let curr = path; curr; curr = curr.prev) {
262
- length += 1
263
- }
264
-
265
- const flattened = new Array(length)
266
- let index = length
267
- for (let curr = path; curr; curr = curr.prev) {
268
- flattened[--index] = curr.key
269
- }
270
- return flattened
271
- }
272
-
327
+ /**
328
+ * @typedef {{ prev: PathNode | undefined, key: string | number }} PathNode
329
+ *
330
+ * @typedef {{ error: unknown, ctx: object }} TrackedField
331
+ */
332
+
333
+ /**
334
+ * @param {{
335
+ * fields: Map<object, TrackedField>,
336
+ * collapse: boolean,
337
+ * collapsedFields?: Map<string, TrackedField>,
338
+ * pathCache?: Map<PathNode, string>,
339
+ * }} rootCtx
340
+ * @param {import('graphql').GraphQLResolveInfo} info
341
+ * @param {Record<string, unknown>} args
342
+ */
273
343
  function assertField (rootCtx, info, args) {
274
- const pathInfo = info && info.path
275
-
276
- const path = pathToArray(pathInfo)
277
-
278
- const pathString = path.join('.')
279
- const fields = rootCtx.fields
280
-
281
- let field = fields[pathString]
282
-
283
- if (!field) {
284
- const fieldCtx = { info, rootCtx, args, path, pathString }
285
- startResolveCh.publish(fieldCtx)
286
- field = fields[pathString] = {
287
- error: null,
288
- ctx: fieldCtx,
289
- }
344
+ const path = info.path
345
+ const collapse = rootCtx.collapse
346
+
347
+ const cache = rootCtx.pathCache ??= new Map()
348
+ const prev = path.prev
349
+ const key = path.key
350
+ const segment = collapse && typeof key !== 'string' ? '*' : key
351
+
352
+ const pathString = prev === undefined
353
+ ? String(segment)
354
+ : (cache.get(prev) ?? buildCachedPathString(prev, cache, collapse)) + '.' + segment
355
+ cache.set(path, pathString)
356
+
357
+ const fieldCtx = {
358
+ rootCtx,
359
+ args,
360
+ path,
361
+ pathString,
362
+ fieldName: info.fieldName,
363
+ returnType: info.returnType,
364
+ fieldNode: info.fieldNodes[0],
365
+ variableValues: info.variableValues,
366
+ }
367
+ // Publish per resolver call, before the collapse / depth dedupe below.
368
+ // IAST mutates each call's own args object; if siblings 2..N skip the
369
+ // publish, those args objects never get tainted.
370
+ startResolveCh.publish(fieldCtx)
371
+
372
+ let collapsedFields
373
+ if (collapse) {
374
+ collapsedFields = rootCtx.collapsedFields ??= new Map()
375
+ const existing = collapsedFields.get(pathString)
376
+ // Subsequent siblings of a collapsed list share the first sibling's field
377
+ // so updateFieldCh fires for every call and the span's finishTime tracks
378
+ // the last sibling's completion, not the first.
379
+ if (existing !== undefined) return existing
290
380
  }
291
381
 
382
+ const field = { error: null, ctx: fieldCtx }
383
+ rootCtx.fields.set(path, field)
384
+ if (collapsedFields !== undefined) collapsedFields.set(pathString, field)
292
385
  return field
293
386
  }
294
387
 
388
+ /**
389
+ * Cold path for assertField. graphql-js inserts a synthetic array-index
390
+ * node between a list field and its items, and that node never reaches a
391
+ * resolver — so assertField has no chance to cache it. The first child of
392
+ * the list item that hits the path cache lands here to walk and populate
393
+ * back to a cached ancestor.
394
+ *
395
+ * @param {PathNode} path
396
+ * @param {Map<PathNode, string>} cache
397
+ * @param {boolean} collapse
398
+ */
399
+ function buildCachedPathString (path, cache, collapse) {
400
+ const key = path.key
401
+ const segment = collapse && typeof key !== 'string' ? '*' : key
402
+ const prev = path.prev
403
+
404
+ const pathString = prev === undefined
405
+ ? String(segment)
406
+ : (cache.get(prev) ?? buildCachedPathString(prev, cache, collapse)) + '.' + segment
407
+ cache.set(path, pathString)
408
+ return pathString
409
+ }
410
+
295
411
  function wrapFields (type) {
296
412
  if (!type || !type._fields || patchedTypes.has(type)) {
297
413
  return
@@ -323,14 +439,19 @@ function wrapFieldType (field) {
323
439
  }
324
440
 
325
441
  function finishResolvers ({ fields }) {
326
- for (const field of Object.values(fields)) {
327
- field.ctx.finishTime = field.finishTime
328
- field.ctx.field = field
442
+ for (const field of fields.values()) {
443
+ const fieldCtx = field.ctx
444
+ // A depth-gated field publishes startResolveCh for IAST/AppSec but the
445
+ // resolve plugin's start short-circuits before creating a span, so there
446
+ // is no span here to finish.
447
+ if (fieldCtx.currentStore === undefined) continue
448
+ fieldCtx.finishTime = field.finishTime
449
+ fieldCtx.field = field
329
450
  if (field.error) {
330
- field.ctx.error = field.error
331
- resolveErrorCh.publish(field.ctx)
451
+ fieldCtx.error = field.error
452
+ resolveErrorCh.publish(fieldCtx)
332
453
  }
333
- finishResolveCh.publish(field.ctx)
454
+ finishResolveCh.publish(fieldCtx)
334
455
  }
335
456
  }
336
457
 
@@ -343,11 +464,11 @@ addHook({ name: '@graphql-tools/executor', versions: ['>=0.0.14'] }, executor =>
343
464
  return executor
344
465
  })
345
466
 
346
- addHook({ name: '@graphql-tools/executor', file: 'cjs/execution/execute.js', versions: ['>=0.0.14'] }, execute => {
347
- shimmer.wrap(execute, 'execute', wrapExecute(execute))
348
- return execute
349
- })
350
-
467
+ // TODO(BridgeAR): graphql >=17.0.0-alpha.9 routes execute() through
468
+ // experimentalExecuteIncrementally(), bypassing this hook. The same
469
+ // function returns { initialResult, subsequentResults } for @defer /
470
+ // @stream which callInAsyncScope does not handle — execute finishes
471
+ // before the streamed payloads land.
351
472
  addHook({ name: 'graphql', file: 'execution/execute.js', versions: ['>=0.10'] }, execute => {
352
473
  shimmer.wrap(execute, 'execute', wrapExecute(execute))
353
474
  return execute