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
@@ -5,6 +5,7 @@ const { getGeneratedPosition } = require('./source-maps')
5
5
  const session = require('./session')
6
6
  const { compile, compileSegments, templateRequiresEvaluation } = require('./condition')
7
7
  const { MAX_SNAPSHOTS_PER_SECOND_PER_PROBE, MAX_NON_SNAPSHOTS_PER_SECOND_PER_PROBE } = require('./defaults')
8
+ const { compileBreakpointCondition, getRemoveProbeExpression } = require('./probe_sampler')
8
9
  const {
9
10
  DEFAULT_MAX_REFERENCE_DEPTH,
10
11
  DEFAULT_MAX_COLLECTION_SIZE,
@@ -17,6 +18,7 @@ const {
17
18
  locationToBreakpoint,
18
19
  breakpointToProbes,
19
20
  probeToLocation,
21
+ samplingIndexToProbe,
20
22
  } = require('./state')
21
23
  const log = require('./log')
22
24
 
@@ -26,6 +28,7 @@ const log = require('./log')
26
28
 
27
29
  let sessionStarted = false
28
30
  const probes = new Map()
31
+ let nextSamplingIndex = 0
29
32
  let scriptLoadingStabilizedResolve
30
33
  const scriptLoadingStabilized = new Promise((resolve) => { scriptLoadingStabilizedResolve = resolve })
31
34
 
@@ -55,6 +58,8 @@ async function addBreakpoint (probe) {
55
58
  if (!sessionStarted) await start()
56
59
 
57
60
  probes.set(probe.id, probe)
61
+ probe.samplingIndex = nextSamplingIndex++
62
+ samplingIndexToProbe.set(probe.samplingIndex, probe)
58
63
 
59
64
  const file = probe.where.sourceFile
60
65
  let lineNumber = Number(probe.where.lines[0]) // Tracer doesn't support multiple-line breakpoints
@@ -75,8 +80,6 @@ async function addBreakpoint (probe) {
75
80
  ? MAX_SNAPSHOTS_PER_SECOND_PER_PROBE
76
81
  : MAX_NON_SNAPSHOTS_PER_SECOND_PER_PROBE)
77
82
  probe.nsBetweenSampling = BigInt(Math.trunc(1 / snapshotsPerSecond * 1e9))
78
- // Initialize to a large negative value to ensure first probe hit is always captured
79
- probe.lastCaptureNs = BigInt(Number.MIN_SAFE_INTEGER)
80
83
 
81
84
  // Warning: The code below relies on undocumented behavior of the inspector!
82
85
  // It expects that `await session.post('Debugger.enable')` will wait for all loaded scripts to be emitted as
@@ -174,7 +177,7 @@ async function addBreakpoint (probe) {
174
177
  try {
175
178
  result = /** @type {SetBreakpointResponse} */ (await session.post('Debugger.setBreakpoint', {
176
179
  location,
177
- condition: probe.condition,
180
+ condition: compileBreakpointCondition([probe]),
178
181
  }))
179
182
  } catch (err) {
180
183
  throw new Error(`Error setting breakpoint for probe ${probe.id} (version: ${probe.version})`, { cause: err })
@@ -195,11 +198,14 @@ async function removeBreakpoint ({ id }) {
195
198
  }
196
199
 
197
200
  probes.delete(id)
201
+ await removeProbeFromSampler(id)
198
202
 
199
203
  const locationKey = probeToLocation.get(id)
200
204
  const breakpoint = locationToBreakpoint.get(locationKey)
201
205
  const probesAtLocation = breakpointToProbes.get(breakpoint.id)
206
+ const probe = probesAtLocation.get(id)
202
207
 
208
+ samplingIndexToProbe.delete(probe.samplingIndex)
203
209
  probesAtLocation.delete(id)
204
210
  probeToLocation.delete(id)
205
211
 
@@ -229,35 +235,37 @@ async function modifyBreakpoint (probe) {
229
235
 
230
236
  async function updateBreakpointInternal (breakpoint, probe) {
231
237
  const probesAtLocation = breakpointToProbes.get(breakpoint.id)
232
- const conditionBeforeNewProbe = compileCompoundCondition([...probesAtLocation.values()])
233
238
 
234
- // If a probe is provided, add it to the breakpoint. If not, it's because we're removing a probe, but potentially
235
- // need to update the condition of the breakpoint.
239
+ // If a probe is provided, add it to the breakpoint. If not, it's because we're removing a probe. In both cases the
240
+ // breakpoint condition must be rebuilt to match the remaining probes at the location.
236
241
  if (probe) {
237
242
  probesAtLocation.set(probe.id, probe)
238
243
  probeToLocation.set(probe.id, breakpoint.locationKey)
239
244
  }
240
245
 
241
- const condition = compileCompoundCondition([...probesAtLocation.values()])
242
-
243
- if (condition || conditionBeforeNewProbe !== condition) {
244
- try {
245
- await session.post('Debugger.removeBreakpoint', { breakpointId: breakpoint.id })
246
- } catch (err) {
247
- throw new Error(`Error removing breakpoint for probe ${probe.id}`, { cause: err })
248
- }
249
- breakpointToProbes.delete(breakpoint.id)
250
- let result
251
- try {
252
- result = /** @type {SetBreakpointResponse} */ (await session.post('Debugger.setBreakpoint', {
253
- location: breakpoint.location,
254
- condition,
255
- }))
256
- } catch (err) {
257
- throw new Error(`Error setting breakpoint for probe ${probe.id} (version: ${probe.version})`, { cause: err })
258
- }
259
- breakpointToProbes.set(result.breakpointId, probesAtLocation)
246
+ try {
247
+ await session.post('Debugger.removeBreakpoint', { breakpointId: breakpoint.id })
248
+ } catch (err) {
249
+ const message = probe
250
+ ? `Error replacing breakpoint while adding probe ${probe.id} (version: ${probe.version})`
251
+ : `Error replacing breakpoint after removing probe from ${breakpoint.locationKey}`
252
+ throw new Error(message, { cause: err })
253
+ }
254
+ breakpointToProbes.delete(breakpoint.id)
255
+ let result
256
+ try {
257
+ result = /** @type {SetBreakpointResponse} */ (await session.post('Debugger.setBreakpoint', {
258
+ location: breakpoint.location,
259
+ condition: compileBreakpointCondition([...probesAtLocation.values()]),
260
+ }))
261
+ } catch (err) {
262
+ const message = probe
263
+ ? `Error setting breakpoint while adding probe ${probe.id} (version: ${probe.version})`
264
+ : `Error setting breakpoint after removing probe from ${breakpoint.locationKey}`
265
+ throw new Error(message, { cause: err })
260
266
  }
267
+ breakpoint.id = result.breakpointId
268
+ breakpointToProbes.set(result.breakpointId, probesAtLocation)
261
269
  }
262
270
 
263
271
  async function reEvaluateProbe (probe) {
@@ -305,16 +313,20 @@ function lock (fn) {
305
313
  }
306
314
  }
307
315
 
308
- // Only if all probes have a condition can we use a compound condition.
309
- // Otherwise, we need to evaluate each probe individually once the breakpoint is hit.
310
- function compileCompoundCondition (probes) {
311
- if (probes.length === 1) return probes[0].condition
312
-
313
- return probes.every(p => p.condition)
314
- ? probes
315
- .map((p) => `(() => { try { return ${p.condition} } catch { return false } })()`)
316
- .join(' || ')
317
- : undefined
316
+ /**
317
+ * Remove cached sampling state for a probe from the runtime sampler.
318
+ *
319
+ * @param {string} id - The probe id.
320
+ * @returns {Promise<void>}
321
+ */
322
+ async function removeProbeFromSampler (id) {
323
+ try {
324
+ await session.post('Runtime.evaluate', {
325
+ expression: getRemoveProbeExpression(id),
326
+ })
327
+ } catch (err) {
328
+ log.error('[debugger:devtools_client] Error removing probe %s from sampler', id, err)
329
+ }
318
330
  }
319
331
 
320
332
  function generateLocationKey (scriptId, lineNumber, columnNumber) {
@@ -1,17 +1,22 @@
1
1
  'use strict'
2
2
 
3
3
  const { randomUUID } = require('crypto')
4
+ const { workerData: { probeSamplerBuffer } } = require('worker_threads')
4
5
  const { version } = require('../../../../../package.json')
5
- const { NODE_MAJOR } = require('../../../../../version')
6
6
  const processTags = require('../../process-tags')
7
- const { breakpointToProbes } = require('./state')
7
+ const {
8
+ MAX_SAMPLED_PROBES_PER_PAUSE,
9
+ SAMPLED_PROBE_COUNT_INDEX,
10
+ SAMPLED_PROBE_INDEXES_START,
11
+ SAMPLED_PROBE_OVERFLOW_INDEX,
12
+ } = require('../probe_sampler_constants')
13
+ const { breakpointToProbes, samplingIndexToProbe } = require('./state')
8
14
  const session = require('./session')
9
15
  const { getLocalStateForCallFrame, evaluateCaptureExpressions } = require('./snapshot')
10
16
  const send = require('./send')
11
17
  const { getStackFromCallFrames } = require('./state')
12
18
  const { ackEmitting } = require('./status')
13
19
  const config = require('./config')
14
- const { MAX_SNAPSHOTS_PER_SECOND_GLOBALLY } = require('./defaults')
15
20
  const log = require('./log')
16
21
 
17
22
  require('./remote_config')
@@ -38,25 +43,7 @@ const getDDTagsExpression = `(() => {
38
43
  // something that should be useful to a Node.js developer.
39
44
  const threadId = config.parentThreadId === 0 ? `pid:${process.pid}` : `pid:${process.pid};tid:${config.parentThreadId}`
40
45
  const threadName = config.parentThreadId === 0 ? 'MainThread' : `WorkerThread:${config.parentThreadId}`
41
-
42
- const SUPPORT_ARRAY_BUFFER_RESIZE = NODE_MAJOR >= 20
43
- const oneSecondNs = 1_000_000_000n
44
- let globalSnapshotSamplingRateWindowStart = 0n
45
- let snapshotsSampledWithinTheLastSecond = 0
46
-
47
- // TODO: Change to const once we drop support for Node.js 18
48
- let snapshotProbeIndexBuffer, snapshotProbeIndex
49
-
50
- if (SUPPORT_ARRAY_BUFFER_RESIZE) {
51
- // TODO: Is a limit of 256 snapshots ever going to be a problem?
52
- // @ts-ignore - ArrayBuffer constructor with maxByteLength is available in Node.js 20+ but not in @types/node@18
53
- // eslint-disable-next-line n/no-unsupported-features/es-syntax
54
- snapshotProbeIndexBuffer = new ArrayBuffer(1, { maxByteLength: 256 })
55
- // TODO: Is a limit of 256 probes ever going to be a problem?
56
- snapshotProbeIndex = new Uint8Array(snapshotProbeIndexBuffer)
57
- } else {
58
- snapshotProbeIndex = new Uint8Array(1)
59
- }
46
+ const sampledProbeIndexes = new Int32Array(probeSamplerBuffer)
60
47
 
61
48
  // WARNING: The code above the line `await session.post('Debugger.resume')` is highly optimized. Please edit with care!
62
49
  session.on('Debugger.paused', async ({ params }) => {
@@ -71,7 +58,6 @@ session.on('Debugger.paused', async ({ params }) => {
71
58
  let maxCollectionSize = 0
72
59
  let maxFieldCount = 0
73
60
  let maxLength = 0
74
- let sampled = false
75
61
  let numberOfProbesWithSnapshots = 0
76
62
  let probesWithCaptureExpressions = false
77
63
  const probes = []
@@ -80,7 +66,15 @@ session.on('Debugger.paused', async ({ params }) => {
80
66
  // V8 doesn't allow setting more than one breakpoint at a specific location, however, it's possible to set two
81
67
  // breakpoints just next to each other that will "snap" to the same logical location, which in turn will be hit at the
82
68
  // same time. E.g. index.js:1:1 and index.js:1:2.
83
- let numberOfProbesOnBreakpoint = params.hitBreakpoints.length
69
+ const numberOfSampledProbeIndexes = Math.min(
70
+ Atomics.exchange(sampledProbeIndexes, SAMPLED_PROBE_COUNT_INDEX, 0),
71
+ MAX_SAMPLED_PROBES_PER_PAUSE
72
+ )
73
+ if (Atomics.exchange(sampledProbeIndexes, SAMPLED_PROBE_OVERFLOW_INDEX, 0) === 1) {
74
+ log.error(
75
+ '[debugger:devtools_client] Too many probes sampled at the same breakpoint location; skipping excess probes'
76
+ )
77
+ }
84
78
 
85
79
  // TODO: Investigate if it will improve performance to create a fast-path for when there's only a single breakpoint
86
80
  for (let i = 0; i < params.hitBreakpoints.length; i++) {
@@ -92,36 +86,23 @@ session.on('Debugger.paused', async ({ params }) => {
92
86
  continue
93
87
  }
94
88
 
95
- if (probesAtLocation.size !== 1) {
96
- numberOfProbesOnBreakpoint = numberOfProbesOnBreakpoint + probesAtLocation.size - 1
97
- if (numberOfProbesOnBreakpoint > snapshotProbeIndex.length) {
98
- if (SUPPORT_ARRAY_BUFFER_RESIZE) {
99
- snapshotProbeIndexBuffer.resize(numberOfProbesOnBreakpoint)
100
- } else {
101
- snapshotProbeIndex = new Uint8Array(numberOfProbesOnBreakpoint)
102
- }
103
- }
104
- }
89
+ for (let j = 0; j < numberOfSampledProbeIndexes; j++) {
90
+ const samplingIndex = Atomics.load(sampledProbeIndexes, SAMPLED_PROBE_INDEXES_START + j)
91
+ const probe = samplingIndexToProbe.get(samplingIndex)
105
92
 
106
- for (const probe of probesAtLocation.values()) {
107
- if (start - probe.lastCaptureNs < probe.nsBetweenSampling) {
93
+ if (probe === undefined) {
94
+ log.error('[debugger:devtools_client] No probe found for sampled probe index %d', samplingIndex)
95
+ continue
96
+ }
97
+ if (!probesAtLocation.has(probe.id)) {
98
+ log.error('[debugger:devtools_client] Sampled probe %s was not found at breakpoint %s',
99
+ probe.id, params.hitBreakpoints[i])
108
100
  continue
109
101
  }
110
102
 
111
103
  if (probe.captureSnapshot === true || probe.compiledCaptureExpressions !== undefined) {
112
- // This algorithm to calculate number of sampled snapshots within the last second is not perfect, as it's not a
113
- // sliding window. But it's quick and easy :)
114
- if (i === 0 && start - globalSnapshotSamplingRateWindowStart > oneSecondNs) {
115
- snapshotsSampledWithinTheLastSecond = 1
116
- globalSnapshotSamplingRateWindowStart = start
117
- } else if (snapshotsSampledWithinTheLastSecond >= MAX_SNAPSHOTS_PER_SECOND_GLOBALLY) {
118
- continue
119
- } else {
120
- snapshotsSampledWithinTheLastSecond++
121
- }
122
-
123
104
  if (probe.captureSnapshot === true) {
124
- snapshotProbeIndex[numberOfProbesWithSnapshots++] = probes.length
105
+ numberOfProbesWithSnapshots++
125
106
  maxReferenceDepth = Math.max(probe.capture.maxReferenceDepth, maxReferenceDepth)
126
107
  maxCollectionSize = Math.max(probe.capture.maxCollectionSize, maxCollectionSize)
127
108
  maxFieldCount = Math.max(probe.capture.maxFieldCount, maxFieldCount)
@@ -131,23 +112,6 @@ session.on('Debugger.paused', async ({ params }) => {
131
112
  }
132
113
  }
133
114
 
134
- if (probe.condition !== undefined) {
135
- // TODO: Bundle all conditions and evaluate them in a single call
136
- // TODO: Handle errors
137
- const { result } = /** @type {EvaluateOnCallFrameResult} */ (
138
- // eslint-disable-next-line no-await-in-loop
139
- await session.post('Debugger.evaluateOnCallFrame', {
140
- callFrameId: params.callFrames[0].callFrameId,
141
- expression: probe.condition,
142
- returnByValue: true,
143
- })
144
- )
145
- if (result.value !== true) continue
146
- }
147
-
148
- sampled = true
149
- probe.lastCaptureNs = start
150
-
151
115
  if (probe.templateRequiresEvaluation) {
152
116
  templateExpressions += `,${probe.template}`
153
117
  }
@@ -156,7 +120,8 @@ session.on('Debugger.paused', async ({ params }) => {
156
120
  }
157
121
  }
158
122
 
159
- if (sampled === false) {
123
+ // This can happen if sampled probe indexes are inconsistent with the worker state. Those cases are logged above.
124
+ if (probes.length === 0) {
160
125
  return session.post('Debugger.resume')
161
126
  }
162
127
 
@@ -0,0 +1,83 @@
1
+ 'use strict'
2
+
3
+ const { DD_TRACE_SYMBOL, PROBE_SAMPLER_SYMBOL } = require('../probe_sampler_constants')
4
+
5
+ const SAMPLER_EXPRESSION = `globalThis[Symbol.for(${JSON.stringify(DD_TRACE_SYMBOL)})]?.` +
6
+ `[Symbol.for(${JSON.stringify(PROBE_SAMPLER_SYMBOL)})]`
7
+
8
+ module.exports = {
9
+ compileBreakpointCondition,
10
+ getRemoveProbeExpression,
11
+ }
12
+
13
+ /**
14
+ * Build the expression that removes a probe from runtime sampler state. Called by the devtools worker and evaluated on
15
+ * the debuggee.
16
+ *
17
+ * @param {string} id - The probe id.
18
+ * @returns {string}
19
+ */
20
+ function getRemoveProbeExpression (id) {
21
+ return `${SAMPLER_EXPRESSION}?.remove(${JSON.stringify(id)})`
22
+ }
23
+
24
+ /**
25
+ * Build a Chrome DevTools breakpoint condition that samples all matching probes at a location. Called by the devtools
26
+ * worker.
27
+ *
28
+ * @param {{
29
+ * id: string,
30
+ * samplingIndex: number,
31
+ * nsBetweenSampling: bigint,
32
+ * condition?: string,
33
+ * captureSnapshot?: boolean,
34
+ * compiledCaptureExpressions?: object[]
35
+ * }[]} probes - The probes at the breakpoint location.
36
+ * @returns {string}
37
+ */
38
+ function compileBreakpointCondition (probes) {
39
+ const probeConditions = []
40
+ for (const probe of probes) {
41
+ probeConditions.push(compileProbeCondition(probe))
42
+ }
43
+
44
+ // NOTE: $dd_sampler is read from the realm-local `globalThis` where it was installed (the main
45
+ // realm). A probe whose code runs in a different V8 realm (e.g. a `vm.createContext` script with a
46
+ // file-path filename) won't see it and will silently never fire. Known limitation: a breakpoint
47
+ // condition has no realm-independent handle to reach, so we degrade rather than crash.
48
+ return `(() => {
49
+ const $dd_sampler = ${SAMPLER_EXPRESSION}
50
+ if ($dd_sampler === undefined) return false
51
+ let $dd_sampled = false
52
+ ${probeConditions.join('\n ')}
53
+ return $dd_sampled
54
+ })()`
55
+ }
56
+
57
+ /**
58
+ * Build the condition fragment for a single probe. Called by the devtools worker while building breakpoint conditions.
59
+ *
60
+ * @param {{
61
+ * id: string,
62
+ * samplingIndex: number,
63
+ * nsBetweenSampling: bigint,
64
+ * condition?: string,
65
+ * captureSnapshot?: boolean,
66
+ * compiledCaptureExpressions?: object[]
67
+ * }} probe - The probe to sample.
68
+ * @returns {string}
69
+ */
70
+ function compileProbeCondition (probe) {
71
+ const sample = `$dd_sampler.makeSampleDecision(${probe.samplingIndex}, ${JSON.stringify(probe.id)}, ` +
72
+ `${probe.nsBetweenSampling}n, ${probe.captureSnapshot === true || probe.compiledCaptureExpressions !== undefined})`
73
+
74
+ if (probe.condition === undefined) {
75
+ return `$dd_sampled = ${sample} || $dd_sampled`
76
+ }
77
+
78
+ return `try {
79
+ if ((${probe.condition}) === true) {
80
+ $dd_sampled = ${sample} || $dd_sampled
81
+ }
82
+ } catch {}`
83
+ }
@@ -33,6 +33,7 @@ module.exports = {
33
33
  locationToBreakpoint: new Map(),
34
34
  breakpointToProbes: new Map(),
35
35
  probeToLocation: new Map(),
36
+ samplingIndexToProbe: new Map(),
36
37
 
37
38
  _loadedScripts: loadedScripts, // Only exposed for testing
38
39
  _scriptUrls: scriptUrls, // Only exposed for testing
@@ -8,6 +8,7 @@ const log = require('../log')
8
8
  const { fetchAgentInfo } = require('../agent/info')
9
9
  const getDebuggerConfig = require('./config')
10
10
  const { DEBUGGER_DIAGNOSTICS_V1, DEBUGGER_INPUT_V2 } = require('./constants')
11
+ const { installProbeSampler, uninstallProbeSampler } = require('./probe_sampler')
11
12
 
12
13
  /**
13
14
  * @typedef {ReturnType<import('../config')>} Config
@@ -65,6 +66,8 @@ function start (config, rcInstance) {
65
66
 
66
67
  globalThis[Symbol.for('dd-trace')].utilTypes = types
67
68
 
69
+ const probeSamplerBuffer = installProbeSampler()
70
+
68
71
  readProbeFile(config.dynamicInstrumentation.probeFile, (probes) => {
69
72
  const action = 'apply'
70
73
  for (const probe of probes) {
@@ -110,6 +113,7 @@ function start (config, rcInstance) {
110
113
  probePort: probeChannel.port1,
111
114
  logPort: logChannel.port1,
112
115
  configPort: configChannel.port1,
116
+ probeSamplerBuffer,
113
117
  },
114
118
  transferList: [probeChannel.port1, logChannel.port1, configChannel.port1],
115
119
  }
@@ -187,6 +191,7 @@ function cleanup (error) {
187
191
  worker.removeAllListeners()
188
192
  worker = null
189
193
  }
194
+ uninstallProbeSampler()
190
195
  configChannel = null
191
196
  inputPath = null
192
197
 
@@ -0,0 +1,112 @@
1
+ 'use strict'
2
+
3
+ const { MAX_SNAPSHOTS_PER_SECOND_GLOBALLY } = require('./devtools_client/defaults')
4
+ const {
5
+ DD_TRACE_SYMBOL,
6
+ MAX_SAMPLED_PROBES_PER_PAUSE,
7
+ PROBE_SAMPLER_SYMBOL,
8
+ SAMPLED_PROBE_COUNT_INDEX,
9
+ SAMPLED_PROBE_INDEXES_START,
10
+ SAMPLED_PROBE_OVERFLOW_INDEX,
11
+ } = require('./probe_sampler_constants')
12
+
13
+ const ddTraceGlobal = /** @type {Record<symbol, SharedArrayBuffer | object | undefined>} */ (
14
+ /** @type {Record<symbol, unknown>} */ (globalThis)[Symbol.for(DD_TRACE_SYMBOL)]
15
+ )
16
+
17
+ module.exports = {
18
+ installProbeSampler,
19
+ uninstallProbeSampler,
20
+ }
21
+
22
+ /**
23
+ * Install the runtime sampler in the debuggee context.
24
+ *
25
+ * @returns {SharedArrayBuffer} The shared sampler buffer to pass to the debugger worker.
26
+ */
27
+ function installProbeSampler () {
28
+ const buffer = createProbeSamplerBuffer()
29
+
30
+ const lastCaptureNsByProbeId = new Map()
31
+ const sampledProbeIndexes = new Int32Array(buffer)
32
+ Atomics.store(sampledProbeIndexes, SAMPLED_PROBE_COUNT_INDEX, 0)
33
+ Atomics.store(sampledProbeIndexes, SAMPLED_PROBE_OVERFLOW_INDEX, 0)
34
+
35
+ const oneSecondNs = 1_000_000_000n
36
+ let globalSnapshotSamplingRateWindowStart = 0n
37
+ let snapshotsSampledWithinTheLastSecond = 0
38
+
39
+ ddTraceGlobal[Symbol.for(PROBE_SAMPLER_SYMBOL)] = {
40
+ /**
41
+ * Decide if a probe should be sampled and store sampled probe indexes for the debugger worker.
42
+ *
43
+ * @param {number} probeIndex - The worker-side probe sampling index.
44
+ * @param {string} probeId - The probe id.
45
+ * @param {bigint} nsBetweenSampling - Minimum nanoseconds between samples for this probe.
46
+ * @param {boolean} isSnapshotProducingProbe - Whether this probe counts toward the global snapshot sample limit.
47
+ * @returns {boolean} Whether this probe should make the breakpoint condition pause.
48
+ */
49
+ makeSampleDecision (probeIndex, probeId, nsBetweenSampling, isSnapshotProducingProbe) {
50
+ const now = process.hrtime.bigint()
51
+ const lastCaptureNs = lastCaptureNsByProbeId.get(probeId)
52
+ if (lastCaptureNs !== undefined && now - lastCaptureNs < nsBetweenSampling) return false
53
+
54
+ let shouldResetGlobalSnapshotRateWindow = false
55
+ if (isSnapshotProducingProbe === true) {
56
+ if (now - globalSnapshotSamplingRateWindowStart > oneSecondNs) {
57
+ shouldResetGlobalSnapshotRateWindow = true
58
+ } else if (snapshotsSampledWithinTheLastSecond >= MAX_SNAPSHOTS_PER_SECOND_GLOBALLY) {
59
+ return false
60
+ }
61
+ }
62
+
63
+ const sampledProbeCount = Atomics.add(sampledProbeIndexes, SAMPLED_PROBE_COUNT_INDEX, 1)
64
+ if (sampledProbeCount >= MAX_SAMPLED_PROBES_PER_PAUSE) {
65
+ Atomics.store(sampledProbeIndexes, SAMPLED_PROBE_OVERFLOW_INDEX, 1)
66
+ return false
67
+ }
68
+
69
+ if (isSnapshotProducingProbe === true) {
70
+ if (shouldResetGlobalSnapshotRateWindow === true) {
71
+ snapshotsSampledWithinTheLastSecond = 1
72
+ globalSnapshotSamplingRateWindowStart = now
73
+ } else {
74
+ snapshotsSampledWithinTheLastSecond++
75
+ }
76
+ }
77
+
78
+ lastCaptureNsByProbeId.set(probeId, now)
79
+ Atomics.store(sampledProbeIndexes, SAMPLED_PROBE_INDEXES_START + sampledProbeCount, probeIndex)
80
+ return true
81
+ },
82
+
83
+ /**
84
+ * Remove cached sampling state for a probe.
85
+ *
86
+ * @param {string} probeId - The probe id.
87
+ */
88
+ remove (probeId) {
89
+ lastCaptureNsByProbeId.delete(probeId)
90
+ },
91
+ }
92
+
93
+ return buffer
94
+ }
95
+
96
+ /**
97
+ * Remove the runtime sampler from the debuggee context.
98
+ */
99
+ function uninstallProbeSampler () {
100
+ delete ddTraceGlobal[Symbol.for(PROBE_SAMPLER_SYMBOL)]
101
+ }
102
+
103
+ /**
104
+ * Create the shared buffer used to hand sampled probe indexes from breakpoint conditions to the debugger worker.
105
+ *
106
+ * @returns {SharedArrayBuffer}
107
+ */
108
+ function createProbeSamplerBuffer () {
109
+ return new SharedArrayBuffer(
110
+ (SAMPLED_PROBE_INDEXES_START + MAX_SAMPLED_PROBES_PER_PAUSE) * Int32Array.BYTES_PER_ELEMENT
111
+ )
112
+ }
@@ -0,0 +1,19 @@
1
+ 'use strict'
2
+
3
+ const DD_TRACE_SYMBOL = 'dd-trace'
4
+ const PROBE_SAMPLER_SYMBOL = 'dd-trace.debugger.probeSampler'
5
+
6
+ // Shared sampler contract used by the main debugger bootstrap and the devtools worker.
7
+ const MAX_SAMPLED_PROBES_PER_PAUSE = 256
8
+ const SAMPLED_PROBE_COUNT_INDEX = 0
9
+ const SAMPLED_PROBE_OVERFLOW_INDEX = 1
10
+ const SAMPLED_PROBE_INDEXES_START = 2
11
+
12
+ module.exports = {
13
+ DD_TRACE_SYMBOL,
14
+ MAX_SAMPLED_PROBES_PER_PAUSE,
15
+ PROBE_SAMPLER_SYMBOL,
16
+ SAMPLED_PROBE_COUNT_INDEX,
17
+ SAMPLED_PROBE_INDEXES_START,
18
+ SAMPLED_PROBE_OVERFLOW_INDEX,
19
+ }