dd-trace 5.109.0 → 5.111.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/README.md +13 -8
  2. package/ci/init.js +25 -5
  3. package/ci/vitest-no-worker-init-setup.mjs +430 -0
  4. package/ext/tags.js +2 -0
  5. package/index.d.ts +79 -24
  6. package/initialize.mjs +5 -6
  7. package/loader-hook.mjs +154 -47
  8. package/package.json +20 -16
  9. package/packages/datadog-esbuild/index.js +26 -0
  10. package/packages/datadog-esbuild/src/utils.js +46 -3
  11. package/packages/datadog-instrumentations/src/connect.js +4 -3
  12. package/packages/datadog-instrumentations/src/fastify.js +4 -12
  13. package/packages/datadog-instrumentations/src/fs.js +8 -6
  14. package/packages/datadog-instrumentations/src/graphql.js +26 -484
  15. package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +81 -8
  16. package/packages/datadog-instrumentations/src/helpers/instrument.js +27 -2
  17. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +65 -0
  18. package/packages/datadog-instrumentations/src/helpers/register.js +6 -4
  19. package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +17 -0
  20. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql.js +201 -0
  21. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  22. package/packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs +18 -2
  23. package/packages/datadog-instrumentations/src/koa.js +3 -2
  24. package/packages/datadog-instrumentations/src/mariadb.js +4 -2
  25. package/packages/datadog-instrumentations/src/mocha/main.js +3 -3
  26. package/packages/datadog-instrumentations/src/mongoose.js +14 -2
  27. package/packages/datadog-instrumentations/src/next.js +10 -10
  28. package/packages/datadog-instrumentations/src/otel-sdk-trace.js +7 -6
  29. package/packages/datadog-instrumentations/src/playwright.js +1 -1
  30. package/packages/datadog-instrumentations/src/restify.js +4 -3
  31. package/packages/datadog-instrumentations/src/router.js +18 -5
  32. package/packages/datadog-instrumentations/src/selenium.js +1 -2
  33. package/packages/datadog-instrumentations/src/tedious.js +28 -0
  34. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +1293 -0
  35. package/packages/datadog-instrumentations/src/vitest-main.js +1567 -0
  36. package/packages/datadog-instrumentations/src/vitest-util.js +249 -0
  37. package/packages/datadog-instrumentations/src/vitest-worker.js +753 -0
  38. package/packages/datadog-instrumentations/src/vitest.js +11 -1598
  39. package/packages/datadog-plugin-avsc/src/schema_iterator.js +2 -2
  40. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/client.js +2 -2
  41. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +18 -10
  42. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +47 -11
  43. package/packages/datadog-plugin-aws-sdk/src/base.js +13 -9
  44. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +43 -6
  45. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +83 -91
  46. package/packages/datadog-plugin-cucumber/src/index.js +1 -1
  47. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +19 -5
  48. package/packages/datadog-plugin-cypress/src/support.js +45 -1
  49. package/packages/datadog-plugin-express/src/code_origin.js +1 -1
  50. package/packages/datadog-plugin-fastify/src/code_origin.js +1 -1
  51. package/packages/datadog-plugin-graphql/src/execute.js +627 -11
  52. package/packages/datadog-plugin-graphql/src/index.js +20 -8
  53. package/packages/datadog-plugin-graphql/src/parse.js +24 -4
  54. package/packages/datadog-plugin-graphql/src/validate.js +16 -5
  55. package/packages/datadog-plugin-http/src/client.js +8 -3
  56. package/packages/datadog-plugin-http/src/index.js +6 -8
  57. package/packages/datadog-plugin-http2/src/client.js +5 -2
  58. package/packages/datadog-plugin-jest/src/index.js +1 -1
  59. package/packages/datadog-plugin-mocha/src/index.js +1 -1
  60. package/packages/datadog-plugin-mysql/src/index.js +7 -2
  61. package/packages/datadog-plugin-openai/src/services.js +2 -2
  62. package/packages/datadog-plugin-playwright/src/index.js +1 -1
  63. package/packages/datadog-plugin-protobufjs/src/schema_iterator.js +2 -2
  64. package/packages/datadog-plugin-undici/src/index.js +5 -2
  65. package/packages/datadog-plugin-vitest/src/index.js +76 -64
  66. package/packages/datadog-shimmer/src/shimmer.js +37 -16
  67. package/packages/datadog-webpack/index.js +17 -1
  68. package/packages/datadog-webpack/src/optional-peer-loader.js +17 -0
  69. package/packages/dd-trace/src/aiguard/sdk.js +15 -3
  70. package/packages/dd-trace/src/appsec/api_security/index.js +55 -0
  71. package/packages/dd-trace/src/appsec/api_security/sampler.js +147 -0
  72. package/packages/dd-trace/src/appsec/channels.js +3 -1
  73. package/packages/dd-trace/src/appsec/downstream_requests.js +4 -4
  74. package/packages/dd-trace/src/appsec/graphql.js +5 -5
  75. package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +26 -19
  76. package/packages/dd-trace/src/appsec/iast/security-controls/index.js +2 -2
  77. package/packages/dd-trace/src/appsec/iast/telemetry/index.js +2 -1
  78. package/packages/dd-trace/src/appsec/index.js +20 -7
  79. package/packages/dd-trace/src/appsec/lambda.js +135 -0
  80. package/packages/dd-trace/src/appsec/reporter.js +50 -10
  81. package/packages/dd-trace/src/appsec/telemetry/api_security.js +34 -0
  82. package/packages/dd-trace/src/appsec/telemetry/index.js +27 -1
  83. package/packages/dd-trace/src/appsec/waf/index.js +16 -4
  84. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +4 -4
  85. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +21 -6
  86. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +2 -2
  87. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +1 -1
  88. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +1 -1
  89. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +3 -2
  90. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +1 -1
  91. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +55 -13
  92. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +2 -2
  93. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +17 -0
  94. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +47 -26
  95. package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +2 -2
  96. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +64 -53
  97. package/packages/dd-trace/src/ci-visibility/requests/request.js +1 -17
  98. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +3 -3
  99. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +23 -5
  100. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +128 -0
  101. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +287 -0
  102. package/packages/dd-trace/src/config/defaults.js +25 -5
  103. package/packages/dd-trace/src/config/generated-config-types.d.ts +596 -58
  104. package/packages/dd-trace/src/config/helper.js +134 -33
  105. package/packages/dd-trace/src/config/index.js +39 -23
  106. package/packages/dd-trace/src/config/major-overrides.js +4 -2
  107. package/packages/dd-trace/src/config/parsers.js +19 -1
  108. package/packages/dd-trace/src/config/remote_config.js +1 -1
  109. package/packages/dd-trace/src/config/supported-configurations.json +109 -57
  110. package/packages/dd-trace/src/constants.js +7 -0
  111. package/packages/dd-trace/src/datastreams/processor.js +11 -3
  112. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +47 -35
  113. package/packages/dd-trace/src/debugger/devtools_client/index.js +31 -66
  114. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +83 -0
  115. package/packages/dd-trace/src/debugger/devtools_client/state.js +1 -0
  116. package/packages/dd-trace/src/debugger/index.js +5 -0
  117. package/packages/dd-trace/src/debugger/probe_sampler.js +112 -0
  118. package/packages/dd-trace/src/debugger/probe_sampler_constants.js +19 -0
  119. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +77 -26
  120. package/packages/dd-trace/src/exporters/agent/index.js +1 -1
  121. package/packages/dd-trace/src/exporters/agentless/writer.js +4 -4
  122. package/packages/dd-trace/src/exporters/common/request.js +1 -20
  123. package/packages/dd-trace/src/exporters/common/url.js +34 -0
  124. package/packages/dd-trace/src/exporters/span-stats/writer.js +1 -3
  125. package/packages/dd-trace/src/guardrails/index.js +8 -5
  126. package/packages/dd-trace/src/index.js +6 -4
  127. package/packages/dd-trace/src/llmobs/constants/tags.js +6 -0
  128. package/packages/dd-trace/src/llmobs/index.js +17 -6
  129. package/packages/dd-trace/src/llmobs/plugins/base.js +4 -4
  130. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +3 -3
  131. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +5 -1
  132. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  133. package/packages/dd-trace/src/llmobs/sdk.js +8 -6
  134. package/packages/dd-trace/src/llmobs/span_processor.js +5 -1
  135. package/packages/dd-trace/src/llmobs/tagger.js +58 -3
  136. package/packages/dd-trace/src/llmobs/telemetry.js +1 -0
  137. package/packages/dd-trace/src/llmobs/writers/base.js +1 -1
  138. package/packages/dd-trace/src/log/index.js +18 -20
  139. package/packages/dd-trace/src/openfeature/flagging_provider.js +2 -7
  140. package/packages/dd-trace/src/opentelemetry/bridge-span-base.js +5 -4
  141. package/packages/dd-trace/src/opentelemetry/metrics/index.js +54 -5
  142. package/packages/dd-trace/src/opentelemetry/metrics/instruments.js +2 -1
  143. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +40 -0
  144. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +169 -0
  145. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +4 -3
  146. package/packages/dd-trace/src/opentelemetry/metrics/time.js +19 -0
  147. package/packages/dd-trace/src/opentelemetry/otlp/common.proto +2 -2
  148. package/packages/dd-trace/src/opentelemetry/otlp/logs.proto +1 -1
  149. package/packages/dd-trace/src/opentelemetry/otlp/logs_service.proto +1 -1
  150. package/packages/dd-trace/src/opentelemetry/otlp/metrics.proto +1 -1
  151. package/packages/dd-trace/src/opentelemetry/otlp/metrics_service.proto +1 -1
  152. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +44 -38
  153. package/packages/dd-trace/src/opentelemetry/span-helpers.js +29 -13
  154. package/packages/dd-trace/src/opentelemetry/span.js +14 -3
  155. package/packages/dd-trace/src/opentelemetry/span_context.js +1 -0
  156. package/packages/dd-trace/src/opentelemetry/trace/index.js +6 -1
  157. package/packages/dd-trace/src/opentelemetry/trace/otlp_http_trace_exporter.js +3 -2
  158. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +26 -13
  159. package/packages/dd-trace/src/opentelemetry/tracer_provider.js +1 -1
  160. package/packages/dd-trace/src/opentracing/propagation/text_map.js +3 -3
  161. package/packages/dd-trace/src/opentracing/span_context.js +17 -0
  162. package/packages/dd-trace/src/opentracing/tracer.js +6 -1
  163. package/packages/dd-trace/src/plugin_manager.js +14 -8
  164. package/packages/dd-trace/src/plugins/ci_plugin.js +21 -4
  165. package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +276 -0
  166. package/packages/dd-trace/src/plugins/util/llm.js +6 -1
  167. package/packages/dd-trace/src/plugins/util/test.js +91 -9
  168. package/packages/dd-trace/src/plugins/util/url.js +68 -1
  169. package/packages/dd-trace/src/plugins/util/web.js +19 -36
  170. package/packages/dd-trace/src/priority_sampler.js +1 -13
  171. package/packages/dd-trace/src/profiling/config.js +129 -198
  172. package/packages/dd-trace/src/profiling/constants.js +4 -7
  173. package/packages/dd-trace/src/profiling/exporter_cli.js +10 -10
  174. package/packages/dd-trace/src/profiling/exporters/agent.js +23 -16
  175. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +45 -14
  176. package/packages/dd-trace/src/profiling/exporters/file.js +16 -5
  177. package/packages/dd-trace/src/profiling/oom.js +71 -0
  178. package/packages/dd-trace/src/profiling/profiler.js +35 -23
  179. package/packages/dd-trace/src/profiling/profilers/events.js +14 -7
  180. package/packages/dd-trace/src/profiling/profilers/space.js +27 -18
  181. package/packages/dd-trace/src/profiling/profilers/wall.js +35 -25
  182. package/packages/dd-trace/src/proxy.js +8 -8
  183. package/packages/dd-trace/src/ritm.js +4 -2
  184. package/packages/dd-trace/src/serverless.js +1 -2
  185. package/packages/dd-trace/src/service-naming/schemas/util.js +36 -5
  186. package/packages/dd-trace/src/service-naming/schemas/v1/web.js +5 -3
  187. package/packages/dd-trace/src/span_format.js +2 -1
  188. package/packages/dd-trace/src/span_processor.js +11 -6
  189. package/packages/dd-trace/src/span_stats.js +96 -78
  190. package/packages/dd-trace/src/startup-log.js +2 -1
  191. package/packages/dd-trace/src/telemetry/dependencies.js +1 -1
  192. package/packages/dd-trace/src/telemetry/endpoints.js +6 -4
  193. package/packages/dd-trace/src/telemetry/index.js +2 -2
  194. package/packages/dd-trace/src/telemetry/logs/index.js +1 -1
  195. package/packages/dd-trace/src/telemetry/send-data.js +20 -18
  196. package/packages/dd-trace/src/telemetry/session-propagation.js +1 -1
  197. package/packages/dd-trace/src/telemetry/telemetry.js +9 -9
  198. package/packages/dd-trace/src/tracer.js +15 -1
  199. package/packages/dd-trace/src/util.js +14 -0
  200. package/register.js +58 -1
  201. package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
  202. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  203. package/vendor/dist/pprof-format/index.js +1 -1
  204. package/vendor/dist/protobufjs/index.js +1 -1
  205. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  206. package/vendor/dist/shell-quote/index.js +1 -1
  207. package/packages/datadog-plugin-graphql/src/resolve.js +0 -170
  208. package/packages/dd-trace/src/appsec/api_security_sampler.js +0 -117
@@ -1,504 +1,46 @@
1
1
  'use strict'
2
2
 
3
- const { AsyncLocalStorage } = require('node:async_hooks')
3
+ const { addHook, getHooks } = require('./helpers/instrument')
4
4
 
5
- const shimmer = require('../../datadog-shimmer')
6
- const {
7
- addHook,
8
- channel,
9
- } = require('./helpers/instrument')
5
+ // Orchestrion rewriter handles wrapping of:
6
+ // - graphql: execute, parse, validate (CJS + ESM)
7
+ // - @graphql-tools/executor: execute, normalizedExecutor (CJS + ESM)
8
+ // See helpers/rewriter/instrumentations/graphql.js for the full config.
9
+ //
10
+ // The plugin (packages/datadog-plugin-graphql/src/execute.js) handles the
11
+ // `apm:graphql:execute:start` AppSec/WAF contract from inside its bindStart:
12
+ // publishing the channel synchronously runs subscribers, and an
13
+ // `abortController.abort()` from a subscriber is observed by replacing
14
+ // `ctx.arguments[0]` with an object whose getters throw AbortError. The
15
+ // orchestrion-emitted wrapper's `catch { ...; throw err }` block propagates
16
+ // that throw to the caller of graphql.execute. No outer wrap needed.
10
17
 
11
- const ddGlobal = globalThis[Symbol.for('dd-trace')]
12
-
13
- /** cached objects */
14
-
15
- // `contexts` is the fast resolver-side lookup; `executeCtx` is the fallback
16
- // when `contextValue` is a primitive and cannot key a WeakMap.
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()
22
- const documentSources = new WeakMap()
23
- const patchedResolvers = new WeakSet()
24
- const patchedTypes = new WeakSet()
25
-
26
- /** CHANNELS */
27
-
28
- // execute channels
29
- const startExecuteCh = channel('apm:graphql:execute:start')
30
- const finishExecuteCh = channel('apm:graphql:execute:finish')
31
- const executeErrorCh = channel('apm:graphql:execute:error')
32
-
33
- // resolve channels
34
- const startResolveCh = channel('apm:graphql:resolve:start')
35
- const finishResolveCh = channel('apm:graphql:resolve:finish')
36
- const updateFieldCh = channel('apm:graphql:resolve:updateField')
37
- const resolveErrorCh = channel('apm:graphql:resolve:error')
38
-
39
- // parse channels
40
- const parseStartCh = channel('apm:graphql:parser:start')
41
- const parseFinishCh = channel('apm:graphql:parser:finish')
42
- const parseErrorCh = channel('apm:graphql:parser:error')
43
-
44
- // validate channels
45
- const validateStartCh = channel('apm:graphql:validate:start')
46
- const validateFinishCh = channel('apm:graphql:validate:finish')
47
- const validateErrorCh = channel('apm:graphql:validate:error')
48
-
49
- class AbortError extends Error {
50
- constructor (message) {
51
- super(message)
52
- this.name = 'AbortError'
53
- }
54
- }
55
-
56
- const types = new Set(['query', 'mutation', 'subscription'])
57
-
58
- function getOperation (document, operationName) {
59
- if (!document || !Array.isArray(document.definitions)) {
60
- return
61
- }
62
-
63
- for (const definition of document.definitions) {
64
- if (definition && types.has(definition.operation) && (!operationName || definition.name?.value === operationName)) {
65
- return definition
66
- }
67
- }
68
- }
69
-
70
- function normalizeArgs (args, defaultFieldResolver) {
71
- if (args.length !== 1) return normalizePositional(args, defaultFieldResolver)
72
-
73
- const original = args[0]
74
- const normalized = {
75
- ...original,
76
- fieldResolver: wrapResolve(original.fieldResolver || defaultFieldResolver),
77
- }
78
-
79
- args[0] = normalized
80
- return normalized
81
- }
82
-
83
- function normalizePositional (args, defaultFieldResolver) {
84
- args[6] = wrapResolve(args[6] || defaultFieldResolver) // fieldResolver
85
- args.length = Math.max(args.length, 7)
86
-
87
- return {
88
- schema: args[0],
89
- document: args[1],
90
- rootValue: args[2],
91
- contextValue: args[3],
92
- variableValues: args[4],
93
- operationName: args[5],
94
- fieldResolver: args[6],
95
- }
96
- }
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
-
104
- function wrapParse (parse) {
105
- return function (source) {
106
- if (!parseStartCh.hasSubscribers) {
107
- return parse.apply(this, arguments)
108
- }
109
-
110
- const ctx = { source }
111
- return parseStartCh.runStores(ctx, () => {
112
- try {
113
- ctx.document = parse.apply(this, arguments)
114
- const operation = getOperation(ctx.document)
115
-
116
- if (!operation) return ctx.document
117
-
118
- if (source) {
119
- documentSources.set(ctx.document, source.body || source)
120
- }
121
- ctx.docSource = documentSources.get(ctx.document)
122
-
123
- return ctx.document
124
- } catch (err) {
125
- void err.stack
126
- ctx.error = err
127
- parseErrorCh.publish(ctx)
128
-
129
- throw err
130
- } finally {
131
- parseFinishCh.publish(ctx)
132
- }
133
- })
134
- }
18
+ for (const hook of getHooks('graphql')) {
19
+ addHook(hook, exports => exports)
135
20
  }
136
21
 
137
- function wrapValidate (validate) {
138
- return function (_schema, document, _rules, _typeInfo) {
139
- if (!validateStartCh.hasSubscribers) {
140
- return validate.apply(this, arguments)
141
- }
142
-
143
- const ctx = { docSource: documentSources.get(document), document }
144
- return validateStartCh.runStores(ctx, () => {
145
- let errors
146
- try {
147
- errors = validate.apply(this, arguments)
148
- if (errors && errors[0]) {
149
- ctx.error = errors[0]
150
- validateErrorCh.publish(ctx)
151
- }
152
- return errors
153
- } catch (err) {
154
- void err.stack
155
- ctx.error = err
156
- validateErrorCh.publish(ctx)
157
-
158
- throw err
159
- } finally {
160
- ctx.errors = errors
161
- validateFinishCh.publish(ctx)
162
- }
163
- })
164
- }
22
+ for (const hook of getHooks('@graphql-tools/executor')) {
23
+ addHook(hook, exports => exports)
165
24
  }
166
25
 
167
- function wrapExecute (execute) {
168
- return function (exe) {
169
- const defaultFieldResolver = execute.defaultFieldResolver
170
- return function () {
171
- if (!startExecuteCh.hasSubscribers) {
172
- return exe.apply(this, arguments)
173
- }
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
-
181
- const args = normalizeArgs(arguments, defaultFieldResolver)
182
- const schema = args.schema
183
- const document = args.document
184
- const source = documentSources.get(document)
185
- const contextValue = args.contextValue
186
- const keyable = isWeakMapKey(contextValue)
187
- const operation = getOperation(document, args.operationName)
188
-
189
- if (keyable && contexts.has(contextValue)) {
190
- return exe.apply(this, arguments)
191
- }
192
-
193
- const ctx = {
194
- operation,
195
- args,
196
- docSource: source,
197
- source,
198
- fields: new Map(),
199
- abortController: new AbortController(),
200
- }
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
-
206
- return startExecuteCh.runStores(ctx, () => {
207
- if (schema) {
208
- wrapFields(schema._queryType)
209
- wrapFields(schema._mutationType)
210
- }
211
-
212
- if (keyable) contexts.set(contextValue, ctx)
213
-
214
- const finish = (err, res) => {
215
- if (finishResolveCh.hasSubscribers) finishResolvers(ctx)
216
-
217
- const error = err || (res && res.errors && res.errors[0])
218
-
219
- if (error) {
220
- ctx.error = error
221
- executeErrorCh.publish(ctx)
222
- }
223
-
224
- ctx.res = res
225
- if (keyable) contexts.delete(contextValue)
226
- instrumentedArgs.delete(args)
227
- finishExecuteCh.publish(ctx)
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))
235
- })
236
- }
237
- }
238
- }
239
-
240
- function wrapResolve (resolve) {
241
- if (typeof resolve !== 'function' || patchedResolvers.has(resolve)) return resolve
242
-
243
- function resolveAsync (source, args, contextValue, info) {
244
- if (!startResolveCh.hasSubscribers) return resolve.apply(this, arguments)
245
-
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()
249
-
250
- /* istanbul ignore if: resolver invoked outside execute(), so no per-execute ctx was registered */
251
- if (!ctx) return resolve.apply(this, arguments)
252
-
253
- const field = assertField(ctx, info, args)
254
-
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
- }
280
- }
281
-
282
- patchedResolvers.add(resolveAsync)
283
-
284
- return resolveAsync
285
- }
286
-
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
- }
297
-
298
- function callInAsyncScope (fn, thisArg, args, abortController, cb) {
299
- if (abortController.signal.aborted) {
300
- cb(null, null)
301
- throw new AbortError('Aborted')
302
- }
303
-
304
- try {
305
- const result = fn.apply(thisArg, args)
306
- if (result !== null && typeof result?.then === 'function') {
307
- return result.then(
308
- res => {
309
- cb(null, res)
310
- return res
311
- },
312
- /* istanbul ignore next: graphql.execute() rejects only via custom executors (graphql-yoga / graphql-tools) */
313
- error => {
314
- cb(error)
315
- throw error
316
- }
317
- )
318
- }
319
- cb(null, result)
320
- return result
321
- } catch (error) {
322
- cb(error)
323
- throw error
324
- }
325
- }
326
-
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
- */
343
- function assertField (rootCtx, info, args) {
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
380
- }
381
-
382
- const field = { error: null, ctx: fieldCtx }
383
- rootCtx.fields.set(path, field)
384
- if (collapsedFields !== undefined) collapsedFields.set(pathString, field)
385
- return field
386
- }
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
-
411
- function wrapFields (type) {
412
- if (!type || !type._fields || patchedTypes.has(type)) {
413
- return
414
- }
415
-
416
- patchedTypes.add(type)
417
-
418
- for (const field of Object.values(type._fields)) {
419
- wrapFieldResolve(field)
420
- wrapFieldType(field)
421
- }
422
- }
423
-
424
- function wrapFieldResolve (field) {
425
- if (!field || !field.resolve) return
426
- field.resolve = wrapResolve(field.resolve)
427
- }
428
-
429
- function wrapFieldType (field) {
430
- if (!field || !field.type) return
431
-
432
- let unwrappedType = field.type
433
-
434
- while (unwrappedType.ofType) {
435
- unwrappedType = unwrappedType.ofType
436
- }
437
-
438
- wrapFields(unwrappedType)
439
- }
440
-
441
- function finishResolvers ({ fields }) {
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
450
- if (field.error) {
451
- fieldCtx.error = field.error
452
- resolveErrorCh.publish(fieldCtx)
453
- }
454
- finishResolveCh.publish(fieldCtx)
455
- }
456
- }
457
-
458
- addHook({ name: '@graphql-tools/executor', versions: ['>=0.0.14'] }, executor => {
459
- // graphql-yoga uses the normalizedExecutor function, so we need to wrap both. There is no risk in wrapping both
460
- // since the functions are closely related, and our wrappedExecute function prevents double calls with the
461
- // contexts.has(contextValue) check.
462
- shimmer.wrap(executor, 'execute', wrapExecute(executor))
463
- shimmer.wrap(executor, 'normalizedExecutor', wrapExecute(executor))
464
- return executor
465
- })
466
-
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.
472
- addHook({ name: 'graphql', file: 'execution/execute.js', versions: ['>=0.10'] }, execute => {
473
- shimmer.wrap(execute, 'execute', wrapExecute(execute))
474
- return execute
475
- })
476
-
477
- addHook({ name: 'graphql', file: 'language/parser.js', versions: ['>=0.10'] }, parser => {
478
- shimmer.wrap(parser, 'parse', wrapParse)
479
- return parser
480
- })
481
-
482
- addHook({ name: 'graphql', file: 'validation/validate.js', versions: ['>=0.10'] }, validate => {
483
- shimmer.wrap(validate, 'validate', wrapValidate)
484
-
485
- return validate
486
- })
26
+ // Module-load hooks: capture references on ddGlobal for cross-plugin access
27
+ // (read lazily inside each callback so agent.load() between mocha suites can
28
+ // rebind globalThis[dd-trace] without us stashing a stale reference).
487
29
 
488
30
  addHook({ name: 'graphql', file: 'language/printer.js', versions: ['>=0.10'] }, printer => {
489
- // HACK: It's possible `graphql` is loaded before `@apollo/gateway` so we
490
- // can't use a channel as the latter plugin would load after the publish
491
- // happened. Not sure how to handle this so for now use a global.
492
- ddGlobal.graphql_printer = printer
31
+ const ddGlobal = globalThis[Symbol.for('dd-trace')]
32
+ if (ddGlobal) ddGlobal.graphql_printer = printer
493
33
  return printer
494
34
  })
495
35
 
496
36
  addHook({ name: 'graphql', file: 'language/visitor.js', versions: ['>=0.10'] }, visitor => {
497
- ddGlobal.graphql_visitor = visitor
37
+ const ddGlobal = globalThis[Symbol.for('dd-trace')]
38
+ if (ddGlobal) ddGlobal.graphql_visitor = visitor
498
39
  return visitor
499
40
  })
500
41
 
501
42
  addHook({ name: 'graphql', file: 'utilities/index.js', versions: ['>=0.10'] }, utilities => {
502
- ddGlobal.graphql_utilities = utilities
43
+ const ddGlobal = globalThis[Symbol.for('dd-trace')]
44
+ if (ddGlobal) ddGlobal.graphql_utilities = utilities
503
45
  return utilities
504
46
  })
@@ -1,8 +1,9 @@
1
1
  'use strict'
2
2
 
3
- // This code runs before the tracer is configured and before a logger is ready
4
- // For that reason we queue up the messages now and decide what to do with them later
5
- const warnings = []
3
+ // Queued before the logger is ready; flushed once the tracer knows its config.
4
+ const warnings = [] // conflicts; flushed under DD_TRACE_DEBUG
5
+ const loadOrderWarnings = [] // "loaded before dd-trace"; flushed under startupLogs
6
+ const frameworkWarnings = [] // curated (e.g. Next.js); flushed unconditionally
6
7
 
7
8
  /**
8
9
  * Here we maintain a list of packages that an application
@@ -28,6 +29,24 @@ const potentialConflicts = new Set([
28
29
 
29
30
  const extractPackageAndModulePath = require('./extract-package-and-module-path')
30
31
 
32
+ /**
33
+ * Frameworks that load their own server code before any user code (and thus
34
+ * before a late `tracer.init()`) can run. When their server module is already
35
+ * in `require.cache` at init time, dd-trace was loaded too late to instrument
36
+ * them and the integration silently no-ops. Unlike the broad scan below, this
37
+ * set is high-signal enough to warn on by default. `file` is the module whose
38
+ * presence proves the server is already loaded; `guidance` is the bundler note
39
+ * appended to the warning.
40
+ */
41
+ const earlyLoadFrameworks = new Map([
42
+ ['next', {
43
+ // dist/server/next-server.js (>=11.1), dist/next-server/server/next-server.js (older)
44
+ file: 'next-server.js',
45
+ guidance: "add 'dd-trace' to `serverExternalPackages` (Next.js >=15) or " +
46
+ '`experimental.serverComponentsExternalPackages` (13-14) so it is not bundled',
47
+ }],
48
+ ])
49
+
31
50
  /**
32
51
  * The lowest hanging fruit to debug an app that isn't tracing
33
52
  * properly is to check that it is loaded before any modules
@@ -43,25 +62,53 @@ const extractPackageAndModulePath = require('./extract-package-and-module-path')
43
62
  * app loads a package we instrument but outside of an
44
63
  * unsupported version then a warning would still be displayed.
45
64
  * This is OK as the tracer should be loaded earlier anyway.
65
+ *
66
+ * Curated frameworks (see `earlyLoadFrameworks`) surface unconditionally; the
67
+ * broad list of packages loaded before dd-trace is queued for the startupLogs-
68
+ * gated flush (`flushLoadOrderWarnings`).
46
69
  */
47
70
  module.exports.checkForRequiredModules = function () {
48
71
  const packages = require('./hooks')
49
72
  const naughties = new Set()
73
+ const frameworksSeen = new Set()
50
74
  let didWarn = false
51
75
 
52
76
  for (const pathToModule of Object.keys(require.cache)) {
53
- const { pkg } = extractPackageAndModulePath(pathToModule)
77
+ // require.cache keys use the platform separator; normalize so the
78
+ // `node_modules/<pkg>` parsing works on Windows (backslash paths).
79
+ const { pkg, path } = extractPackageAndModulePath(pathToModule.replaceAll('\\', '/'))
54
80
 
55
- if (naughties.has(pkg)) continue
56
- if (!(pkg in packages)) continue
81
+ if (pkg === null) continue
82
+
83
+ // A curated framework loads its own server before user code, so its server
84
+ // module being cached means dd-trace was too late to instrument it. These
85
+ // surface unconditionally (see flushFrameworkWarnings) with an actionable
86
+ // message, so they never fall through to the broad load-order list below.
87
+ const framework = earlyLoadFrameworks.get(pkg)
88
+ if (framework !== undefined) {
89
+ if (!frameworksSeen.has(pkg) && path?.endsWith(framework.file)) {
90
+ frameworksSeen.add(pkg)
91
+ frameworkWarnings.push(
92
+ `'${pkg}' was loaded before dd-trace, so the ${pkg} integration will not be applied. ` +
93
+ 'Initialize dd-trace before your application starts — ' +
94
+ "NODE_OPTIONS='--require dd-trace/init' (CommonJS) or '--import dd-trace/initialize.mjs' (ESM) — " +
95
+ `and ${framework.guidance}.`
96
+ )
97
+ }
98
+ continue
99
+ }
57
100
 
58
- warnings.push(() => `Warning: Package '${pkg}' was loaded before dd-trace! This may break instrumentation.`)
101
+ if (naughties.has(pkg) || !(pkg in packages)) continue
102
+
103
+ loadOrderWarnings.push(
104
+ () => `Warning: Package '${pkg}' was loaded before dd-trace! This may break instrumentation.`
105
+ )
59
106
 
60
107
  naughties.add(pkg)
61
108
  didWarn = true
62
109
  }
63
110
 
64
- if (didWarn) warnings.push('Warning: Please ensure dd-trace is loaded before other modules.')
111
+ if (didWarn) loadOrderWarnings.push('Warning: Please ensure dd-trace is loaded before other modules.')
65
112
  }
66
113
 
67
114
  /**
@@ -104,3 +151,29 @@ module.exports.flushStartupLogs = function (log) {
104
151
  log.warn(typeof entry === 'function' ? entry() : entry)
105
152
  }
106
153
  }
154
+
155
+ /**
156
+ * Drains the broad "loaded before dd-trace" warnings collected by
157
+ * `checkForRequiredModules`. The tracer gates this on startupLogs (these are
158
+ * startup diagnostics), unlike the unconditional `flushFrameworkWarnings`.
159
+ * @param {(message: string) => void} warn
160
+ */
161
+ module.exports.flushLoadOrderWarnings = function (warn) {
162
+ while (loadOrderWarnings.length) {
163
+ const entry = loadOrderWarnings.shift()
164
+ warn(typeof entry === 'function' ? entry() : entry)
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Drains the framework warnings collected by `checkForRequiredModules`. The
170
+ * tracer surfaces these unconditionally (not gated on startupLogs or
171
+ * DD_TRACE_DEBUG), unlike the DD_TRACE_DEBUG-only `flushStartupLogs` queue,
172
+ * because the affected users run with neither enabled (#5430 / #5432).
173
+ * @param {(message: string) => void} warn
174
+ */
175
+ module.exports.flushFrameworkWarnings = function (warn) {
176
+ while (frameworkWarnings.length) {
177
+ warn(frameworkWarnings.shift())
178
+ }
179
+ }
@@ -42,6 +42,31 @@ exports.tracingChannel = function (name) {
42
42
  return tc
43
43
  }
44
44
 
45
+ /**
46
+ * Build a guarded publisher for a public error channel. A subscriber that
47
+ * re-enters the same wrapped dispatch while handling the error would otherwise
48
+ * republish here and recurse until the stack overflows. Each framework binds
49
+ * its own publisher, so the in-flight flag stays private to one channel: a
50
+ * genuinely nested error on a different framework's channel (a Koa app mounted
51
+ * inside Express) still reaches its subscribers instead of being dropped, and
52
+ * the guard costs a closure read rather than a per-publish channel lookup.
53
+ *
54
+ * @param {Channel} errorChannel
55
+ */
56
+ exports.createErrorPublisher = function createErrorPublisher (errorChannel) {
57
+ let publishing = false
58
+ /** @param {object} message */
59
+ return function publishError (message) {
60
+ if (publishing) return
61
+ publishing = true
62
+ try {
63
+ errorChannel.publish(message)
64
+ } finally {
65
+ publishing = false
66
+ }
67
+ }
68
+ }
69
+
45
70
  exports.getHooks = function getHooks (names) {
46
71
  names = [names].flat()
47
72
 
@@ -55,9 +80,9 @@ exports.getHooks = function getHooks (names) {
55
80
  * @param {object} args
56
81
  * @param {string} args.name module name
57
82
  * @param {string[]} [args.versions] array of semver range strings
58
- * @param {string} [args.file] path to file within package to instrument. Defaults to 'index.js'.
83
+ * @param {string} [args.file] path to file within package to instrument. Defaults to 'index.js'.
59
84
  * @param {string} [args.filePattern] pattern to match files within package to instrument
60
- * @param {boolean} [args.patchDefault] whether to patch the default export. Defaults to true.
85
+ * @param {boolean} [args.patchDefault] whether to patch the default export. Defaults to true.
61
86
  * @param {(moduleExports: unknown, version: string, isIitm?: boolean, hookMeta?: object) => unknown} [hook]
62
87
  * Patches module exports
63
88
  */