dd-trace 5.109.0 → 5.110.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 (128) hide show
  1. package/ci/init.js +4 -3
  2. package/index.d.ts +66 -24
  3. package/loader-hook.mjs +88 -2
  4. package/package.json +18 -14
  5. package/packages/datadog-esbuild/index.js +26 -0
  6. package/packages/datadog-esbuild/src/utils.js +46 -3
  7. package/packages/datadog-instrumentations/src/connect.js +4 -3
  8. package/packages/datadog-instrumentations/src/fastify.js +4 -12
  9. package/packages/datadog-instrumentations/src/graphql.js +17 -6
  10. package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +81 -8
  11. package/packages/datadog-instrumentations/src/helpers/instrument.js +27 -2
  12. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +65 -0
  13. package/packages/datadog-instrumentations/src/helpers/register.js +6 -4
  14. package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +17 -0
  15. package/packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs +18 -2
  16. package/packages/datadog-instrumentations/src/koa.js +3 -2
  17. package/packages/datadog-instrumentations/src/mariadb.js +4 -2
  18. package/packages/datadog-instrumentations/src/mocha/main.js +3 -3
  19. package/packages/datadog-instrumentations/src/next.js +10 -10
  20. package/packages/datadog-instrumentations/src/otel-sdk-trace.js +7 -6
  21. package/packages/datadog-instrumentations/src/playwright.js +1 -1
  22. package/packages/datadog-instrumentations/src/restify.js +4 -3
  23. package/packages/datadog-instrumentations/src/router.js +18 -5
  24. package/packages/datadog-instrumentations/src/selenium.js +1 -2
  25. package/packages/datadog-instrumentations/src/tedious.js +28 -0
  26. package/packages/datadog-instrumentations/src/vitest.js +264 -7
  27. package/packages/datadog-plugin-avsc/src/schema_iterator.js +2 -2
  28. package/packages/datadog-plugin-aws-sdk/src/base.js +13 -9
  29. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +43 -6
  30. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +83 -91
  31. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +9 -1
  32. package/packages/datadog-plugin-http/src/client.js +8 -3
  33. package/packages/datadog-plugin-http2/src/client.js +5 -2
  34. package/packages/datadog-plugin-mysql/src/index.js +7 -2
  35. package/packages/datadog-plugin-protobufjs/src/schema_iterator.js +2 -2
  36. package/packages/datadog-plugin-undici/src/index.js +5 -2
  37. package/packages/datadog-plugin-vitest/src/index.js +7 -1
  38. package/packages/datadog-webpack/index.js +17 -1
  39. package/packages/datadog-webpack/src/optional-peer-loader.js +17 -0
  40. package/packages/dd-trace/src/aiguard/sdk.js +13 -1
  41. package/packages/dd-trace/src/appsec/api_security/index.js +55 -0
  42. package/packages/dd-trace/src/appsec/api_security/sampler.js +147 -0
  43. package/packages/dd-trace/src/appsec/index.js +10 -6
  44. package/packages/dd-trace/src/appsec/reporter.js +1 -0
  45. package/packages/dd-trace/src/appsec/telemetry/api_security.js +34 -0
  46. package/packages/dd-trace/src/appsec/telemetry/index.js +26 -0
  47. package/packages/dd-trace/src/ci-visibility/requests/request.js +1 -17
  48. package/packages/dd-trace/src/config/defaults.js +22 -3
  49. package/packages/dd-trace/src/config/generated-config-types.d.ts +535 -1
  50. package/packages/dd-trace/src/config/helper.js +133 -33
  51. package/packages/dd-trace/src/config/index.js +4 -1
  52. package/packages/dd-trace/src/config/parsers.js +19 -1
  53. package/packages/dd-trace/src/config/supported-configurations.json +21 -3
  54. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +47 -35
  55. package/packages/dd-trace/src/debugger/devtools_client/index.js +31 -66
  56. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +83 -0
  57. package/packages/dd-trace/src/debugger/devtools_client/state.js +1 -0
  58. package/packages/dd-trace/src/debugger/index.js +5 -0
  59. package/packages/dd-trace/src/debugger/probe_sampler.js +112 -0
  60. package/packages/dd-trace/src/debugger/probe_sampler_constants.js +19 -0
  61. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +77 -26
  62. package/packages/dd-trace/src/exporters/common/request.js +1 -20
  63. package/packages/dd-trace/src/exporters/common/url.js +34 -0
  64. package/packages/dd-trace/src/exporters/span-stats/writer.js +1 -3
  65. package/packages/dd-trace/src/guardrails/index.js +8 -5
  66. package/packages/dd-trace/src/index.js +6 -4
  67. package/packages/dd-trace/src/llmobs/constants/tags.js +6 -0
  68. package/packages/dd-trace/src/llmobs/index.js +16 -5
  69. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +3 -3
  70. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +5 -1
  71. package/packages/dd-trace/src/llmobs/sdk.js +5 -3
  72. package/packages/dd-trace/src/llmobs/span_processor.js +4 -0
  73. package/packages/dd-trace/src/llmobs/tagger.js +55 -0
  74. package/packages/dd-trace/src/llmobs/telemetry.js +1 -0
  75. package/packages/dd-trace/src/log/index.js +18 -20
  76. package/packages/dd-trace/src/openfeature/flagging_provider.js +2 -7
  77. package/packages/dd-trace/src/opentelemetry/bridge-span-base.js +5 -4
  78. package/packages/dd-trace/src/opentelemetry/metrics/instruments.js +2 -1
  79. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +4 -3
  80. package/packages/dd-trace/src/opentelemetry/metrics/time.js +19 -0
  81. package/packages/dd-trace/src/opentelemetry/otlp/common.proto +2 -2
  82. package/packages/dd-trace/src/opentelemetry/otlp/logs.proto +1 -1
  83. package/packages/dd-trace/src/opentelemetry/otlp/logs_service.proto +1 -1
  84. package/packages/dd-trace/src/opentelemetry/otlp/metrics.proto +1 -1
  85. package/packages/dd-trace/src/opentelemetry/otlp/metrics_service.proto +1 -1
  86. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +11 -4
  87. package/packages/dd-trace/src/opentelemetry/span-helpers.js +29 -13
  88. package/packages/dd-trace/src/opentelemetry/span.js +14 -3
  89. package/packages/dd-trace/src/opentelemetry/span_context.js +1 -0
  90. package/packages/dd-trace/src/opentelemetry/trace/index.js +2 -1
  91. package/packages/dd-trace/src/opentelemetry/trace/otlp_http_trace_exporter.js +3 -2
  92. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +26 -13
  93. package/packages/dd-trace/src/opentracing/propagation/text_map.js +3 -3
  94. package/packages/dd-trace/src/opentracing/span_context.js +17 -0
  95. package/packages/dd-trace/src/plugin_manager.js +7 -1
  96. package/packages/dd-trace/src/plugins/ci_plugin.js +9 -1
  97. package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +276 -0
  98. package/packages/dd-trace/src/plugins/util/test.js +80 -0
  99. package/packages/dd-trace/src/plugins/util/url.js +68 -1
  100. package/packages/dd-trace/src/plugins/util/web.js +19 -36
  101. package/packages/dd-trace/src/priority_sampler.js +1 -13
  102. package/packages/dd-trace/src/profiling/config.js +129 -198
  103. package/packages/dd-trace/src/profiling/constants.js +4 -7
  104. package/packages/dd-trace/src/profiling/exporter_cli.js +10 -10
  105. package/packages/dd-trace/src/profiling/exporters/agent.js +23 -16
  106. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +45 -14
  107. package/packages/dd-trace/src/profiling/exporters/file.js +16 -5
  108. package/packages/dd-trace/src/profiling/oom.js +71 -0
  109. package/packages/dd-trace/src/profiling/profiler.js +35 -23
  110. package/packages/dd-trace/src/profiling/profilers/events.js +14 -7
  111. package/packages/dd-trace/src/profiling/profilers/space.js +27 -18
  112. package/packages/dd-trace/src/profiling/profilers/wall.js +35 -25
  113. package/packages/dd-trace/src/ritm.js +4 -2
  114. package/packages/dd-trace/src/serverless.js +1 -2
  115. package/packages/dd-trace/src/service-naming/schemas/util.js +36 -5
  116. package/packages/dd-trace/src/service-naming/schemas/v1/web.js +5 -3
  117. package/packages/dd-trace/src/span_format.js +2 -1
  118. package/packages/dd-trace/src/span_processor.js +6 -0
  119. package/packages/dd-trace/src/telemetry/send-data.js +12 -10
  120. package/packages/dd-trace/src/tracer.js +15 -1
  121. package/packages/dd-trace/src/util.js +14 -0
  122. package/register.js +58 -1
  123. package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
  124. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  125. package/vendor/dist/pprof-format/index.js +1 -1
  126. package/vendor/dist/protobufjs/index.js +1 -1
  127. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  128. package/packages/dd-trace/src/appsec/api_security_sampler.js +0 -117
@@ -149,7 +149,140 @@ function validateAccess (name) {
149
149
  }
150
150
  }
151
151
 
152
+ let configurationsTable
153
+ let configDefaults
154
+
155
+ // Lazy require to keep the `config -> helper` import direction acyclic: helper
156
+ // must not pull `config/defaults` at module load. `config/defaults` defers its
157
+ // own instrumented `dns` require until after it exports, so by the time any
158
+ // caller resolves a value the table is fully populated.
159
+ function loadConfigurationsTable () {
160
+ if (configurationsTable === undefined) {
161
+ const defaultsModule = require('./defaults')
162
+ configurationsTable = defaultsModule.configurationsTable
163
+ configDefaults = defaultsModule.defaults
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Parses and transforms a raw environment value with the same parser and
169
+ * transformer Config applies when it reads environment sources, so callers
170
+ * receive the typed value instead of the raw string.
171
+ *
172
+ * @param {string} name Canonical or alias environment variable name.
173
+ * @param {string} value Raw value read from an environment source.
174
+ * @param {'env_var'|'fleet_stable_config'|'local_stable_config'} source Source the value was read from.
175
+ * @returns {string | number | boolean | object | undefined}
176
+ */
177
+ function parseConfigurationValue (name, value, source) {
178
+ loadConfigurationsTable()
179
+ const canonical = aliasToCanonical[name] ?? name
180
+ const entry = configurationsTable[canonical]
181
+ const parsed = entry.parser(value, canonical, source)
182
+ return parsed !== undefined && entry.transformer ? entry.transformer(parsed, canonical, source) : parsed
183
+ }
184
+
185
+ /**
186
+ * Returns the registered default for a configuration — the same value Config
187
+ * resolves when no environment source sets it.
188
+ *
189
+ * @param {string} name Canonical or alias environment variable name.
190
+ * @returns {string | number | boolean | object | undefined}
191
+ */
192
+ function getRegisteredDefault (name) {
193
+ loadConfigurationsTable()
194
+ const canonical = aliasToCanonical[name] ?? name
195
+ const entry = configurationsTable[canonical]
196
+ return configDefaults[entry.property ?? canonical]
197
+ }
198
+
199
+ /** @typedef {import('./generated-config-types').GeneratedEnvVarConfig} GeneratedEnvVarConfig */
200
+
201
+ /**
202
+ * Returns the value stored at the given name, assumed to be in environment variable format,
203
+ * from the supported env sources (process.env, local stable config, fleet stable config).
204
+ * Falls back to aliases if the canonical name is not set.
205
+ *
206
+ * The raw value is parsed and transformed exactly as Config does when it reads environment
207
+ * sources, so the returned value is typed (boolean, number, array, map, ...) rather than the
208
+ * raw string. A literal supported name resolves to its specific configured type.
209
+ *
210
+ * When the name is not set in any source, the registered default is returned, so callers can
211
+ * use the value directly. Callers that must distinguish an explicitly configured value from the
212
+ * default (e.g. an OTel fallback that only applies when the option is unset) pass `skipDefault`
213
+ * to receive `undefined` for an unset value instead. All sources (process.env, local and fleet
214
+ * stable config) are read in both modes.
215
+ *
216
+ * @template {keyof GeneratedEnvVarConfig} TName
217
+ * @overload
218
+ * @param {TName} name Environment variable name
219
+ * @returns {GeneratedEnvVarConfig[TName]}
220
+ */
221
+ /**
222
+ * @template {keyof GeneratedEnvVarConfig} TName
223
+ * @overload
224
+ * @param {TName} name Environment variable name
225
+ * @param {boolean} skipDefault Return `undefined` instead of the registered default when unset.
226
+ * @returns {GeneratedEnvVarConfig[TName] | undefined}
227
+ */
228
+ /**
229
+ * @overload
230
+ * @param {string} name Environment variable name
231
+ * @param {boolean} [skipDefault] Return `undefined` instead of the registered default when unset.
232
+ * @returns {string | number | boolean | object | undefined}
233
+ */
234
+ /**
235
+ * @param {string} name
236
+ * @param {boolean} [skipDefault]
237
+ * @throws {Error} if the configuration is not supported
238
+ */
239
+ function getValueFromEnvSources (name, skipDefault) {
240
+ validateAccess(name)
241
+
242
+ if (!stableConfigLoaded) {
243
+ loadStableConfig()
244
+ }
245
+
246
+ // Sources are read in priority order (fleet > env > local). A value the parser
247
+ // or transformer rejects resolves to `undefined` and is ignored, falling
248
+ // through to the next source and finally the registered default — mirroring
249
+ // how Config applies env sources (`setAndTrack` skips such values). This keeps
250
+ // a malformed higher-priority value (e.g. a fleet-stable `DD_TRACE_DEBUG=maybe`)
251
+ // from masking a valid `DD_TRACE_DEBUG=true`.
252
+ if (fleetStableConfig !== undefined) {
253
+ const fromFleet = getValueFromSource(name, fleetStableConfig)
254
+ if (fromFleet !== undefined) {
255
+ const parsed = parseConfigurationValue(name, fromFleet, 'fleet_stable_config')
256
+ if (parsed !== undefined) {
257
+ return parsed
258
+ }
259
+ }
260
+ }
261
+
262
+ const fromEnv = getValueFromSource(name, process.env)
263
+ if (fromEnv !== undefined) {
264
+ const parsed = parseConfigurationValue(name, fromEnv, 'env_var')
265
+ if (parsed !== undefined) {
266
+ return parsed
267
+ }
268
+ }
269
+
270
+ if (localStableConfig !== undefined) {
271
+ const fromLocal = getValueFromSource(name, localStableConfig)
272
+ if (fromLocal !== undefined) {
273
+ const parsed = parseConfigurationValue(name, fromLocal, 'local_stable_config')
274
+ if (parsed !== undefined) {
275
+ return parsed
276
+ }
277
+ }
278
+ }
279
+
280
+ return skipDefault ? undefined : getRegisteredDefault(name)
281
+ }
282
+
152
283
  module.exports = {
284
+ getValueFromEnvSources,
285
+
153
286
  /**
154
287
  * Expose raw stable config maps and warnings for consumers that need
155
288
  * per-source access (e.g. telemetry in Config).
@@ -209,39 +342,6 @@ module.exports = {
209
342
  return getValueFromSource(name, process.env)
210
343
  },
211
344
 
212
- /**
213
- * Returns the value stored at the given name, assumed to be in environment variable format,
214
- * from the supported env sources (process.env, local stable config, fleet stable config).
215
- * Falls back to aliases if the canonical name is not set.
216
- *
217
- * @param {string} name Environment variable name
218
- * @returns {string|undefined}
219
- * @throws {Error} if the configuration is not supported
220
- */
221
- getValueFromEnvSources (name) {
222
- validateAccess(name)
223
-
224
- if (!stableConfigLoaded) {
225
- loadStableConfig()
226
- }
227
-
228
- if (fleetStableConfig !== undefined) {
229
- const fromFleet = getValueFromSource(name, fleetStableConfig)
230
- if (fromFleet !== undefined) {
231
- return fromFleet
232
- }
233
- }
234
-
235
- const fromEnv = getValueFromSource(name, process.env)
236
- if (fromEnv !== undefined) {
237
- return fromEnv
238
- }
239
-
240
- if (localStableConfig !== undefined) {
241
- return getValueFromSource(name, localStableConfig)
242
- }
243
- },
244
-
245
345
  /**
246
346
  * Returns the actual environment variable name used for a supported configuration
247
347
  * from a specific environment-based source.
@@ -7,7 +7,7 @@ const { URL, format } = require('node:url')
7
7
  const rfdc = require('../../../../vendor/dist/rfdc')({ proto: false, circles: false })
8
8
  const uuid = require('../../../../vendor/dist/crypto-randomuuid') // we need to keep the old uuid dep because of cypress
9
9
  const set = require('../../../datadog-core/src/utils/src/set')
10
- const { DD_MAJOR } = require('../../../../version')
10
+ const { DD_MAJOR, NODE_MAJOR } = require('../../../../version')
11
11
  const log = require('../log')
12
12
  const pkg = require('../pkg')
13
13
  const { isTrue } = require('../util')
@@ -361,6 +361,9 @@ class Config extends ConfigBase {
361
361
  if (this.telemetry.heartbeatInterval) {
362
362
  setAndTrack(this, 'telemetry.heartbeatInterval', Math.floor(this.telemetry.heartbeatInterval * 1000))
363
363
  }
364
+
365
+ // Allocation profiling needs a sampling hook only available on Node.js 26+.
366
+ setAndTrack(this, 'DD_PROFILING_ALLOCATION_ENABLED', NODE_MAJOR >= 26 && this.DD_PROFILING_ALLOCATION_ENABLED)
364
367
  if (this.telemetry.extendedHeartbeatInterval) {
365
368
  setAndTrack(this, 'telemetry.extendedHeartbeatInterval',
366
369
  Math.floor(this.telemetry.extendedHeartbeatInterval * 1000))
@@ -2,7 +2,7 @@
2
2
 
3
3
  const fs = require('fs')
4
4
 
5
- const { DD_MAJOR } = require('../../../../version')
5
+ const { DD_MAJOR, NODE_MAJOR } = require('../../../../version')
6
6
  const tagger = require('../tagger')
7
7
 
8
8
  let warnInvalidValue
@@ -100,6 +100,24 @@ const transformers = {
100
100
  }
101
101
  return configValue
102
102
  },
103
+ /**
104
+ * Parses DD_PROFILING_DEBUG_UPLOAD_COMPRESSION ('on' | 'off' | 'gzip[-1..9]' | 'zstd[-1..22]')
105
+ * into the codec and level the profiler uploads with. The value's shape is already range-checked
106
+ * by the `allowed` pattern, so no validation is needed here.
107
+ *
108
+ * @param {string} value
109
+ * @returns {{ method: string, level: number | undefined }}
110
+ */
111
+ normalizeProfilingUploadCompression (value) {
112
+ let [method, level] = value.toLowerCase().split('-')
113
+ // Default "on" to zstd on Node.js 24+ and gzip earlier. zstd ships everywhere via a Rust
114
+ // compressor, but 24+ has a built-in that runs on libuv worker threads just as gzip does, so it
115
+ // is the least disruptive default.
116
+ if (method === 'on') {
117
+ method = NODE_MAJOR >= 24 ? 'zstd' : 'gzip'
118
+ }
119
+ return { method, level: level ? Number.parseInt(level, 10) : undefined }
120
+ },
103
121
  sampleRate (value, optionName, source) {
104
122
  const number = Number(value)
105
123
  if (Number.isNaN(number) || value === null) {
@@ -1238,6 +1238,16 @@
1238
1238
  "default": null
1239
1239
  }
1240
1240
  ],
1241
+ "DD_LLMOBS_SAMPLE_RATE": [
1242
+ {
1243
+ "implementation": "A",
1244
+ "type": "decimal",
1245
+ "configurationNames": [
1246
+ "llmobs.sampleRate"
1247
+ ],
1248
+ "default": "1.0"
1249
+ }
1250
+ ],
1241
1251
  "DD_LOGS_INJECTION": [
1242
1252
  {
1243
1253
  "implementation": "B",
@@ -1398,8 +1408,8 @@
1398
1408
  "implementation": "B",
1399
1409
  "type": "string",
1400
1410
  "default": "on",
1401
- "allowed": "on|off|(gzip|zstd)(-[1-9][0-9]?)?",
1402
- "transform": "toLowerCase"
1411
+ "allowed": "on|off|gzip(-[1-9])?|zstd(-([1-9]|1[0-9]|2[0-2]))?",
1412
+ "transform": "normalizeProfilingUploadCompression"
1403
1413
  }
1404
1414
  ],
1405
1415
  "DD_PROFILING_ENABLED": [
@@ -1486,7 +1496,8 @@
1486
1496
  {
1487
1497
  "implementation": "A",
1488
1498
  "type": "int",
1489
- "default": "524288"
1499
+ "default": "524288",
1500
+ "allowed": "[1-9]\\d*"
1490
1501
  }
1491
1502
  ],
1492
1503
  "DD_PROFILING_PPROF_PREFIX": [
@@ -3320,6 +3331,13 @@
3320
3331
  "default": "true"
3321
3332
  }
3322
3333
  ],
3334
+ "DD_TRACE_OTEL_SEMANTICS_ENABLED": [
3335
+ {
3336
+ "implementation": "A",
3337
+ "type": "boolean",
3338
+ "default": "false"
3339
+ }
3340
+ ],
3323
3341
  "DD_TRACE_OTEL_ENABLED": [
3324
3342
  {
3325
3343
  "implementation": "A",
@@ -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