dd-trace 5.86.0 → 5.88.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 (105) hide show
  1. package/LICENSE-3rdparty.csv +60 -32
  2. package/ext/exporters.d.ts +1 -0
  3. package/ext/exporters.js +1 -0
  4. package/index.d.ts +243 -7
  5. package/package.json +9 -6
  6. package/packages/datadog-instrumentations/src/ai.js +54 -90
  7. package/packages/datadog-instrumentations/src/cucumber.js +14 -0
  8. package/packages/datadog-instrumentations/src/helpers/hook.js +17 -11
  9. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  10. package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +55 -14
  11. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +15 -13
  12. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +103 -0
  13. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.js +108 -0
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -1
  15. package/packages/datadog-instrumentations/src/helpers/rewriter/transformer.js +21 -0
  16. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +138 -12
  17. package/packages/datadog-instrumentations/src/http/client.js +119 -1
  18. package/packages/datadog-instrumentations/src/jest.js +179 -15
  19. package/packages/datadog-instrumentations/src/kafkajs.js +20 -17
  20. package/packages/datadog-instrumentations/src/mocha/utils.js +6 -0
  21. package/packages/datadog-instrumentations/src/mysql2.js +131 -64
  22. package/packages/datadog-instrumentations/src/playwright.js +9 -1
  23. package/packages/datadog-instrumentations/src/stripe.js +92 -0
  24. package/packages/datadog-instrumentations/src/vitest.js +11 -0
  25. package/packages/datadog-plugin-amqplib/src/consumer.js +14 -10
  26. package/packages/datadog-plugin-amqplib/src/producer.js +23 -19
  27. package/packages/datadog-plugin-azure-functions/src/index.js +53 -37
  28. package/packages/datadog-plugin-bullmq/src/consumer.js +33 -11
  29. package/packages/datadog-plugin-bullmq/src/producer.js +60 -31
  30. package/packages/datadog-plugin-cucumber/src/index.js +9 -6
  31. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +33 -0
  32. package/packages/datadog-plugin-cypress/src/support.js +48 -8
  33. package/packages/datadog-plugin-jest/src/index.js +12 -2
  34. package/packages/datadog-plugin-jest/src/util.js +2 -1
  35. package/packages/datadog-plugin-kafkajs/src/consumer.js +22 -12
  36. package/packages/datadog-plugin-kafkajs/src/producer.js +33 -22
  37. package/packages/datadog-plugin-mocha/src/index.js +9 -6
  38. package/packages/datadog-plugin-playwright/src/index.js +10 -6
  39. package/packages/datadog-plugin-vitest/src/index.js +13 -8
  40. package/packages/dd-trace/src/appsec/addresses.js +11 -0
  41. package/packages/dd-trace/src/appsec/channels.js +5 -1
  42. package/packages/dd-trace/src/appsec/downstream_requests.js +302 -0
  43. package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +1 -1
  44. package/packages/dd-trace/src/appsec/iast/analyzers/ssrf-analyzer.js +1 -1
  45. package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +1 -1
  46. package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +4 -5
  47. package/packages/dd-trace/src/appsec/iast/path-line.js +36 -25
  48. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +1 -1
  49. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +3 -4
  50. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +3 -2
  51. package/packages/dd-trace/src/appsec/index.js +103 -0
  52. package/packages/dd-trace/src/appsec/rasp/ssrf.js +66 -4
  53. package/packages/dd-trace/src/azure_metadata.js +0 -2
  54. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +14 -1
  55. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  56. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +2 -0
  57. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +1 -1
  58. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -1
  59. package/packages/dd-trace/src/ci-visibility/requests/request.js +236 -0
  60. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  61. package/packages/dd-trace/src/config/defaults.js +148 -195
  62. package/packages/dd-trace/src/config/helper.js +43 -1
  63. package/packages/dd-trace/src/config/index.js +42 -14
  64. package/packages/dd-trace/src/config/supported-configurations.json +4115 -510
  65. package/packages/dd-trace/src/constants.js +0 -2
  66. package/packages/dd-trace/src/crashtracking/crashtracker.js +10 -3
  67. package/packages/dd-trace/src/datastreams/pathway.js +22 -3
  68. package/packages/dd-trace/src/datastreams/processor.js +14 -1
  69. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +47 -2
  70. package/packages/dd-trace/src/debugger/devtools_client/index.js +75 -23
  71. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +23 -1
  72. package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +3 -3
  73. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +168 -36
  74. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +18 -0
  75. package/packages/dd-trace/src/encode/agentless-json.js +141 -0
  76. package/packages/dd-trace/src/exporter.js +2 -0
  77. package/packages/dd-trace/src/exporters/agent/writer.js +22 -8
  78. package/packages/dd-trace/src/exporters/agentless/index.js +89 -0
  79. package/packages/dd-trace/src/exporters/agentless/writer.js +184 -0
  80. package/packages/dd-trace/src/exporters/common/agents.js +1 -1
  81. package/packages/dd-trace/src/exporters/common/request.js +4 -4
  82. package/packages/dd-trace/src/llmobs/constants/writers.js +1 -1
  83. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +5 -3
  84. package/packages/dd-trace/src/llmobs/sdk.js +34 -5
  85. package/packages/dd-trace/src/opentelemetry/context_manager.js +19 -46
  86. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +3 -4
  87. package/packages/dd-trace/src/opentracing/propagation/text_map.js +3 -5
  88. package/packages/dd-trace/src/opentracing/span.js +6 -4
  89. package/packages/dd-trace/src/plugins/ci_plugin.js +57 -5
  90. package/packages/dd-trace/src/plugins/database.js +57 -45
  91. package/packages/dd-trace/src/plugins/outbound.js +27 -2
  92. package/packages/dd-trace/src/plugins/tracing.js +39 -4
  93. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +7 -0
  94. package/packages/dd-trace/src/plugins/util/test.js +48 -0
  95. package/packages/dd-trace/src/plugins/util/web.js +8 -7
  96. package/packages/dd-trace/src/profiling/exporter_cli.js +1 -0
  97. package/packages/dd-trace/src/propagation-hash/index.js +145 -0
  98. package/packages/dd-trace/src/proxy.js +4 -0
  99. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +1 -1
  100. package/packages/dd-trace/src/startup-log.js +3 -3
  101. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.json +0 -106
  102. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js +0 -741
  103. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +0 -11
  104. package/packages/dd-trace/src/plugins/util/serverless.js +0 -8
  105. package/packages/dd-trace/src/scope/noop/scope.js +0 -21
@@ -46,8 +46,6 @@ module.exports = {
46
46
  SCHEMA_TOPIC: 'schema.topic',
47
47
  SCHEMA_OPERATION: 'schema.operation',
48
48
  SCHEMA_NAME: 'schema.name',
49
- GRPC_CLIENT_ERROR_STATUSES: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
50
- GRPC_SERVER_ERROR_STATUSES: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
51
49
  DYNAMODB_PTR_KIND: 'aws.dynamodb.item',
52
50
  S3_PTR_KIND: 'aws.s3.object',
53
51
  WEBSOCKET_PTR_KIND: 'websocket',
@@ -52,6 +52,12 @@ class Crashtracker {
52
52
  #getConfig (config) {
53
53
  const url = getAgentUrl(config)
54
54
 
55
+ // Out-of-process symbolication currently (crashtracker 27.0.0) works on
56
+ // Linux only, does not work on Mac.
57
+ const resolveMode = require('os').platform === 'linux'
58
+ ? 'EnabledWithSymbolsInReceiver'
59
+ : 'EnabledWithInprocessSymbols'
60
+
55
61
  return {
56
62
  additional_files: [],
57
63
  create_alt_stack: true,
@@ -67,9 +73,10 @@ class Crashtracker {
67
73
  },
68
74
  timeout_ms: 3000,
69
75
  },
70
- timeout_ms: 5000,
71
- // TODO: Use `EnabledWithSymbolsInReceiver` instead for Linux when fixed.
72
- resolve_frames: 'EnabledWithInprocessSymbols',
76
+ timeout: { secs: 5, nanos: 0 },
77
+ demangle_names: false,
78
+ signals: [],
79
+ resolve_frames: resolveMode,
73
80
  }
74
81
  }
75
82
 
@@ -26,17 +26,36 @@ function shaHash (checkpointString) {
26
26
  * @param {string} env
27
27
  * @param {string[]} edgeTags
28
28
  * @param {Buffer} parentHash
29
+ * @param {bigint | null} propagationHashBigInt - Optional propagation hash for process/container tags
29
30
  */
30
- function computeHash (service, env, edgeTags, parentHash) {
31
+ function computeHash (service, env, edgeTags, parentHash, propagationHashBigInt = null) {
31
32
  edgeTags.sort()
32
33
  const hashableEdgeTags = edgeTags.filter(item => item !== 'manual_checkpoint:true')
33
34
 
34
- const key = `${service}${env}${hashableEdgeTags.join('')}${parentHash}`
35
+ // Cache key includes parentHash to handle fan-in/fan-out scenarios where the same
36
+ // service+env+tags+propagationHash can have different parents. This ensures we cache
37
+ // the complete pathway context, not just the current node's identity.
38
+ const propagationPart = propagationHashBigInt ? `:${propagationHashBigInt.toString(16)}` : ''
39
+ const key = `${service}${env}${hashableEdgeTags.join('')}${parentHash}${propagationPart}`
40
+
35
41
  let value = cache.get(key)
36
42
  if (value) {
37
43
  return value
38
44
  }
39
- const currentHash = shaHash(`${service}${env}` + hashableEdgeTags.join(''))
45
+
46
+ // Key vs hashInput distinction:
47
+ // - 'key' (above) is used for caching and includes parentHash to differentiate pathways
48
+ // with the same node but different parents (e.g., multiple queues feeding one consumer)
49
+ // - 'hashInput' (below) excludes parentHash to compute only the current node's identity hash,
50
+ // which is then XORed with parentHash (line 54) to build the complete pathway hash
51
+ // This two-step approach (hash current node independently, then combine with parent) is
52
+ // required for proper pathway construction in the DSM protocol.
53
+ const baseString = `${service}${env}` + hashableEdgeTags.join('')
54
+ const hashInput = propagationHashBigInt
55
+ ? `${baseString}:${propagationHashBigInt.toString(16)}`
56
+ : baseString
57
+
58
+ const currentHash = shaHash(hashInput)
40
59
  const buf = Buffer.concat([currentHash, parentHash], 16)
41
60
  value = shaHash(buf.toString())
42
61
  cache.set(key, value)
@@ -6,6 +6,8 @@ const pkg = require('../../../../package.json')
6
6
  const { LogCollapsingLowestDenseDDSketch } = require('../../../../vendor/dist/@datadog/sketches-js')
7
7
  const { PATHWAY_HASH } = require('../../../../ext/tags')
8
8
  const log = require('../log')
9
+ const processTags = require('../process-tags')
10
+ const propagationHash = require('../propagation-hash')
9
11
  const { DsmPathwayCodec } = require('./pathway')
10
12
  const { DataStreamsWriter } = require('./writer')
11
13
  const { computePathwayHash } = require('./pathway')
@@ -162,6 +164,7 @@ class DataStreamsProcessor {
162
164
  onInterval () {
163
165
  const { Stats } = this._serializeBuckets()
164
166
  if (Stats.length === 0) return
167
+
165
168
  const payload = {
166
169
  Env: this.env,
167
170
  Service: this.service,
@@ -171,6 +174,12 @@ class DataStreamsProcessor {
171
174
  Lang: 'javascript',
172
175
  Tags: Object.entries(this.tags).map(([key, value]) => `${key}:${value}`),
173
176
  }
177
+
178
+ // Add ProcessTags only if feature is enabled and process tags exist
179
+ if (propagationHash.isEnabled() && processTags.serialized) {
180
+ payload.ProcessTags = processTags.serialized.split(',')
181
+ }
182
+
174
183
  this.writer.flush(payload)
175
184
  }
176
185
 
@@ -234,7 +243,11 @@ class DataStreamsProcessor {
234
243
  edgeTags
235
244
  )
236
245
  }
237
- const hash = computePathwayHash(this.service, this.env, edgeTags, parentHash)
246
+
247
+ // Get propagation hash if enabled
248
+ const propagationHashValue = propagationHash.isEnabled() ? propagationHash.getHash() : null
249
+
250
+ const hash = computePathwayHash(this.service, this.env, edgeTags, parentHash, propagationHashValue)
238
251
  const edgeLatencyNs = nowNs - edgeStartNs
239
252
  const pathwayLatencyNs = nowNs - pathwayStartNs
240
253
  const dataStreamsContext = {
@@ -3,8 +3,14 @@
3
3
  const mutex = require('../../../../../vendor/dist/mutexify/promise')()
4
4
  const { getGeneratedPosition } = require('./source-maps')
5
5
  const session = require('./session')
6
- const { compile: compileCondition, compileSegments, templateRequiresEvaluation } = require('./condition')
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 {
9
+ DEFAULT_MAX_REFERENCE_DEPTH,
10
+ DEFAULT_MAX_COLLECTION_SIZE,
11
+ DEFAULT_MAX_FIELD_COUNT,
12
+ DEFAULT_MAX_LENGTH,
13
+ } = require('./snapshot/constants')
8
14
  const {
9
15
  findScriptFromPartialPath,
10
16
  clearState,
@@ -99,7 +105,7 @@ async function addBreakpoint (probe) {
99
105
  }
100
106
 
101
107
  try {
102
- probe.condition = probe.when?.json && compileCondition(probe.when.json)
108
+ probe.condition = probe.when?.json && compile(probe.when.json)
103
109
  } catch (err) {
104
110
  throw new Error(
105
111
  `Cannot compile expression: ${probe.when.dsl} (probe: ${probe.id}, version: ${probe.version})`,
@@ -107,6 +113,45 @@ async function addBreakpoint (probe) {
107
113
  )
108
114
  }
109
115
 
116
+ if (probe.captureSnapshot) {
117
+ probe.capture = {
118
+ maxReferenceDepth: probe.capture?.maxReferenceDepth ?? DEFAULT_MAX_REFERENCE_DEPTH,
119
+ maxCollectionSize: probe.capture?.maxCollectionSize ?? DEFAULT_MAX_COLLECTION_SIZE,
120
+ maxFieldCount: probe.capture?.maxFieldCount ?? DEFAULT_MAX_FIELD_COUNT,
121
+ maxLength: probe.capture?.maxLength ?? DEFAULT_MAX_LENGTH,
122
+ }
123
+ }
124
+
125
+ if (probe.captureExpressions?.length > 0) {
126
+ probe.compiledCaptureExpressions = []
127
+ for (const captureExpr of probe.captureExpressions) {
128
+ let expression
129
+ try {
130
+ expression = compile(captureExpr.expr.json)
131
+ } catch (err) {
132
+ throw new Error(
133
+ `Cannot compile capture expression: ${captureExpr.name} (probe: ${probe.id}, version: ${probe.version})`,
134
+ { cause: err }
135
+ )
136
+ }
137
+
138
+ probe.compiledCaptureExpressions.push({
139
+ name: captureExpr.name,
140
+ expression,
141
+ limits: {
142
+ maxReferenceDepth: captureExpr.capture?.maxReferenceDepth ??
143
+ probe.capture?.maxReferenceDepth ?? DEFAULT_MAX_REFERENCE_DEPTH,
144
+ maxCollectionSize: captureExpr.capture?.maxCollectionSize ??
145
+ probe.capture?.maxCollectionSize ?? DEFAULT_MAX_COLLECTION_SIZE,
146
+ maxFieldCount: captureExpr.capture?.maxFieldCount ??
147
+ probe.capture?.maxFieldCount ?? DEFAULT_MAX_FIELD_COUNT,
148
+ maxLength: captureExpr.capture?.maxLength ??
149
+ probe.capture?.maxLength ?? DEFAULT_MAX_LENGTH,
150
+ },
151
+ })
152
+ }
153
+ }
154
+
110
155
  const locationKey = generateLocationKey(scriptId, lineNumber, columnNumber)
111
156
  const breakpoint = locationToBreakpoint.get(locationKey)
112
157
 
@@ -6,7 +6,7 @@ const { NODE_MAJOR } = require('../../../../../version')
6
6
  const processTags = require('../../process-tags')
7
7
  const { breakpointToProbes } = require('./state')
8
8
  const session = require('./session')
9
- const { getLocalStateForCallFrame } = require('./snapshot')
9
+ const { getLocalStateForCallFrame, evaluateCaptureExpressions } = require('./snapshot')
10
10
  const send = require('./send')
11
11
  const { getStackFromCallFrames } = require('./state')
12
12
  const { ackEmitting } = require('./status')
@@ -67,9 +67,13 @@ session.on('Debugger.paused', async ({ params }) => {
67
67
  throw new Error(`Unexpected Debugger.paused reason: ${params.reason}`)
68
68
  }
69
69
 
70
- let maxReferenceDepth, maxCollectionSize, maxFieldCount, maxLength
70
+ let maxReferenceDepth = 0
71
+ let maxCollectionSize = 0
72
+ let maxFieldCount = 0
73
+ let maxLength = 0
71
74
  let sampled = false
72
75
  let numberOfProbesWithSnapshots = 0
76
+ let probesWithCaptureExpressions = false
73
77
  const probes = []
74
78
  let templateExpressions = ''
75
79
 
@@ -104,7 +108,7 @@ session.on('Debugger.paused', async ({ params }) => {
104
108
  continue
105
109
  }
106
110
 
107
- if (probe.captureSnapshot === true) {
111
+ if (probe.captureSnapshot === true || probe.compiledCaptureExpressions !== undefined) {
108
112
  // This algorithm to calculate number of sampled snapshots within the last second is not perfect, as it's not a
109
113
  // sliding window. But it's quick and easy :)
110
114
  if (i === 0 && start - globalSnapshotSamplingRateWindowStart > oneSecondNs) {
@@ -116,11 +120,15 @@ session.on('Debugger.paused', async ({ params }) => {
116
120
  snapshotsSampledWithinTheLastSecond++
117
121
  }
118
122
 
119
- snapshotProbeIndex[numberOfProbesWithSnapshots++] = probes.length
120
- maxReferenceDepth = highestOrUndefined(probe.capture?.maxReferenceDepth, maxReferenceDepth)
121
- maxCollectionSize = highestOrUndefined(probe.capture?.maxCollectionSize, maxCollectionSize)
122
- maxFieldCount = highestOrUndefined(probe.capture?.maxFieldCount, maxFieldCount)
123
- maxLength = highestOrUndefined(probe.capture?.maxLength, maxLength)
123
+ if (probe.captureSnapshot === true) {
124
+ snapshotProbeIndex[numberOfProbesWithSnapshots++] = probes.length
125
+ maxReferenceDepth = Math.max(probe.capture.maxReferenceDepth, maxReferenceDepth)
126
+ maxCollectionSize = Math.max(probe.capture.maxCollectionSize, maxCollectionSize)
127
+ maxFieldCount = Math.max(probe.capture.maxFieldCount, maxFieldCount)
128
+ maxLength = Math.max(probe.capture.maxLength, maxLength)
129
+ } else {
130
+ probesWithCaptureExpressions = true
131
+ }
124
132
  }
125
133
 
126
134
  if (probe.condition !== undefined) {
@@ -173,16 +181,32 @@ session.on('Debugger.paused', async ({ params }) => {
173
181
  }
174
182
 
175
183
  // TODO: Create unique states for each affected probe based on that probes unique `capture` settings (DEBUG-2863)
176
- let processLocalState, captureErrors
184
+ let processLocalState
185
+ /** @type {Error[] | undefined} */
186
+ let fatalSnapshotErrors
177
187
  if (numberOfProbesWithSnapshots !== 0) {
178
- const opts = {
179
- maxReferenceDepth,
180
- maxCollectionSize,
181
- maxFieldCount,
182
- maxLength,
183
- deadlineNs: start + config.dynamicInstrumentation.captureTimeoutNs,
188
+ const result = await getLocalStateForCallFrame(
189
+ params.callFrames[0],
190
+ { maxReferenceDepth, maxCollectionSize, maxFieldCount, maxLength },
191
+ start + config.dynamicInstrumentation.captureTimeoutNs
192
+ )
193
+ processLocalState = result.processLocalState
194
+ fatalSnapshotErrors = result.fatalErrors
195
+ }
196
+
197
+ // Evaluate capture expressions for probes that have them
198
+ let captureExpressionResults = null
199
+ if (probesWithCaptureExpressions === true) {
200
+ captureExpressionResults = new Map()
201
+ for (const probe of probes) {
202
+ if (probe.compiledCaptureExpressions === undefined) continue
203
+ // eslint-disable-next-line no-await-in-loop
204
+ captureExpressionResults.set(probe.id, await evaluateCaptureExpressions(
205
+ params.callFrames[0],
206
+ probe.compiledCaptureExpressions,
207
+ start + config.dynamicInstrumentation.captureTimeoutNs
208
+ ))
184
209
  }
185
- ;({ processLocalState, captureErrors } = await getLocalStateForCallFrame(params.callFrames[0], opts))
186
210
  }
187
211
 
188
212
  await session.post('Debugger.resume')
@@ -228,16 +252,48 @@ session.on('Debugger.paused', async ({ params }) => {
228
252
  }
229
253
 
230
254
  if (probe.captureSnapshot) {
231
- if (captureErrors?.length > 0) {
255
+ if (fatalSnapshotErrors && fatalSnapshotErrors.length > 0) {
232
256
  // There was an error collecting the snapshot for this probe, let's not try again
233
257
  probe.captureSnapshot = false
234
- probe.permanentEvaluationErrors = captureErrors.map(error => ({
258
+ probe.permanentEvaluationErrors = fatalSnapshotErrors.map(error => ({
235
259
  expr: '',
236
260
  message: error.message,
237
261
  }))
238
262
  }
239
263
  snapshot.captures = {
240
- lines: { [probe.location.lines[0]]: { locals: processLocalState() } },
264
+ lines: { [probe.location.lines[0]]: { locals: /** @type {Function} */ (processLocalState)() } },
265
+ }
266
+ } else if (probe.compiledCaptureExpressions !== undefined) {
267
+ const expressionResult = /** @type {Map} */ (captureExpressionResults).get(probe.id)
268
+ if (expressionResult) {
269
+ // Handle fatal capture errors - disable capture expressions for this probe permanently
270
+ if (expressionResult.fatalErrors?.length > 0) {
271
+ probe.compiledCaptureExpressions = undefined
272
+ probe.permanentEvaluationErrors = expressionResult.fatalErrors.map(error => ({
273
+ expr: '',
274
+ message: error.message,
275
+ }))
276
+ }
277
+
278
+ snapshot.captures = {
279
+ lines: { [probe.location.lines[0]]: { captureExpressions: expressionResult.processCaptureExpressions() } },
280
+ }
281
+
282
+ // Handle transient evaluation errors - include in snapshot for this capture
283
+ if (expressionResult.evaluationErrors?.length > 0) {
284
+ if (snapshot.evaluationErrors === undefined) {
285
+ snapshot.evaluationErrors = expressionResult.evaluationErrors
286
+ } else {
287
+ snapshot.evaluationErrors.push(...expressionResult.evaluationErrors)
288
+ }
289
+ }
290
+ } else {
291
+ log.error('[debugger:devtools_client] Missing capture expression results for probe %s (version: %s)',
292
+ probe.id, probe.version)
293
+ snapshot.evaluationErrors = [{
294
+ expr: '',
295
+ message: 'Internal error: capture expression results not found',
296
+ }]
241
297
  }
242
298
  }
243
299
 
@@ -275,10 +331,6 @@ session.on('Debugger.paused', async ({ params }) => {
275
331
  }
276
332
  })
277
333
 
278
- function highestOrUndefined (num, max) {
279
- return num === undefined ? max : Math.max(num, max ?? 0)
280
- }
281
-
282
334
  function processDD (result) {
283
335
  return result?.trace_id === undefined ? undefined : result
284
336
  }
@@ -5,7 +5,7 @@ const { addBreakpoint, removeBreakpoint, modifyBreakpoint } = require('./breakpo
5
5
  const { ackReceived, ackInstalled, ackError } = require('./status')
6
6
  const log = require('./log')
7
7
 
8
- // Example log line probe (simplified):
8
+ // Example log line probe with captureSnapshot (simplified):
9
9
  // {
10
10
  // id: '100c9a5c-45ad-49dc-818b-c570d31e11d1',
11
11
  // version: 0,
@@ -19,6 +19,23 @@ const log = require('./log')
19
19
  // evaluateAt: 'EXIT' // only used for method probes
20
20
  // }
21
21
  //
22
+ // Example log line probe with captureExpressions (simplified):
23
+ // Note: captureSnapshot and captureExpressions are mutually exclusive
24
+ // {
25
+ // id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
26
+ // version: 0,
27
+ // type: 'LOG_PROBE',
28
+ // where: { sourceFile: 'index.js', lines: ['25'] },
29
+ // template: 'Captured expressions',
30
+ // segments: [{ str: 'Captured expressions' }],
31
+ // captureExpressions: [
32
+ // { name: 'myVar', expr: { dsl: 'myVar', json: { ref: 'myVar' } }, capture: { maxReferenceDepth: 2 } },
33
+ // { name: 'obj.foo', expr: { dsl: 'obj.foo', json: { getmember: [{ ref: 'obj' }, 'foo'] } } }
34
+ // ],
35
+ // capture: { maxReferenceDepth: 3 }, // default limits for expressions without explicit capture
36
+ // sampling: { snapshotsPerSecond: 1 }
37
+ // }
38
+ //
22
39
  // Example log method probe (simplified):
23
40
  // {
24
41
  // id: 'd692ee6d-5734-4df7-9d86-e3bc6449cc8c',
@@ -63,6 +80,11 @@ async function processMsg (action, probe) {
63
80
  `Unsupported probe insertion point! Only line-based probes are supported (id: ${probe.id}, version: ${probe.version})`
64
81
  )
65
82
  }
83
+ if (probe.captureSnapshot && probe.captureExpressions?.length > 0) {
84
+ throw new Error(
85
+ `Cannot set both captureSnapshot and captureExpressions (probe: ${probe.id}, version: ${probe.version})`
86
+ )
87
+ }
66
88
 
67
89
  switch (action) {
68
90
  case 'unapply':
@@ -56,13 +56,13 @@ module.exports = {
56
56
 
57
57
  /**
58
58
  * @typedef {object} GetObjectOptions
59
- * @property {object} maxReferenceDepth - The maximum depth of the object to traverse
59
+ * @property {number} maxReferenceDepth - The maximum depth of the object to traverse
60
60
  * @property {number} maxCollectionSize - The maximum size of a collection to include in the snapshot
61
61
  * @property {number} maxFieldCount - The maximum number of properties on an object to include in the snapshot
62
62
  * @property {bigint} deadlineNs - The deadline in nanoseconds compared to `process.hrtime.bigint()`
63
63
  * @property {object} ctx - A context object to track the state/progress of the snapshot collection.
64
64
  * @property {boolean} ctx.deadlineReached - Will be set to `true` if the deadline has been reached.
65
- * @property {Error[]} ctx.captureErrors - An array on which errors can be pushed if an issue is detected while
65
+ * @property {Error[]} ctx.fatalErrors - An array on which errors can be pushed if an issue is detected while
66
66
  * collecting the snapshot.
67
67
  */
68
68
 
@@ -99,7 +99,7 @@ async function collectObjectProperties (objectId, opts, depth = 0, collection =
99
99
  // Trim the number of properties on the object if there's too many.
100
100
  const size = result.length
101
101
  if (size > LARGE_OBJECT_SKIP_THRESHOLD) {
102
- opts.ctx.captureErrors.push(new Error(
102
+ opts.ctx.fatalErrors.push(new Error(
103
103
  `An object with ${size} properties was detected while collecting a snapshot. ` +
104
104
  `This exceeds the maximum number of allowed properties of ${LARGE_OBJECT_SKIP_THRESHOLD}. ` +
105
105
  'Future snapshots for existing probes in this location will be skipped until the Node.js process is restarted'
@@ -1,55 +1,41 @@
1
1
  'use strict'
2
2
 
3
- const {
4
- DEFAULT_MAX_REFERENCE_DEPTH,
5
- DEFAULT_MAX_COLLECTION_SIZE,
6
- DEFAULT_MAX_FIELD_COUNT,
7
- DEFAULT_MAX_LENGTH,
8
- } = require('./constants')
3
+ const session = require('../session')
9
4
  const { collectObjectProperties } = require('./collector')
10
- const { processRawState } = require('./processor')
5
+ const { processRawState, processRemoteObject } = require('./processor')
11
6
 
12
7
  const BIGINT_MAX = (1n << 256n) - 1n
13
8
 
14
9
  module.exports = {
15
10
  getLocalStateForCallFrame,
11
+ evaluateCaptureExpressions,
16
12
  }
17
13
 
18
14
  /**
19
- * @typedef {object} GetLocalStateForCallFrameOptions
20
- * @property {number} [maxReferenceDepth] - The maximum depth of the object to traverse. Defaults to
21
- * {@link DEFAULT_MAX_REFERENCE_DEPTH}.
22
- * @property {number} [maxCollectionSize] - The maximum size of a collection to include in the snapshot. Defaults to
23
- * {@link DEFAULT_MAX_COLLECTION_SIZE}.
24
- * @property {number} [maxFieldCount] - The maximum number of properties on an object to include in the snapshot.
25
- * Defaults to {@link DEFAULT_MAX_FIELD_COUNT}.
26
- * @property {number} [maxLength] - The maximum length of a string to include in the snapshot. Defaults to
27
- * {@link DEFAULT_MAX_LENGTH}.
28
- * @property {bigint} [deadlineNs] - The deadline in nanoseconds compared to `process.hrtime.bigint()`. Defaults to
29
- * {@link BIGINT_MAX}. If the deadline is reached, the snapshot will be truncated.
15
+ * @typedef {object} CaptureLimits - Fully resolved capture limits (all fallbacks already applied)
16
+ * @property {number} maxReferenceDepth - The maximum depth of the object to traverse
17
+ * @property {number} maxCollectionSize - The maximum size of a collection to include in the snapshot
18
+ * @property {number} maxFieldCount - The maximum number of properties on an object to include in the snapshot
19
+ * @property {number} maxLength - The maximum length of a string to include in the snapshot
30
20
  */
31
21
 
32
22
  /**
33
23
  * Get the local state for a call frame.
34
24
  *
35
25
  * @param {import('inspector').Debugger.CallFrame} callFrame - The call frame to get the local state for
36
- * @param {GetLocalStateForCallFrameOptions} [opts] - The options for the snapshot
37
- * @returns {Promise<object>} The local state for the call frame
26
+ * @param {CaptureLimits} limits - The capture limits
27
+ * @param {bigint} [deadlineNs] - The deadline in nanoseconds compared to `process.hrtime.bigint()`. Defaults to
28
+ * {@link BIGINT_MAX}. If the deadline is reached, the snapshot will be truncated.
29
+ * @returns {Promise<{ processLocalState: () => ReturnType<typeof processRawState>, fatalErrors: Error[] }>} The local
30
+ * state for the call frame
38
31
  */
39
- async function getLocalStateForCallFrame (
40
- callFrame,
41
- {
42
- maxReferenceDepth = DEFAULT_MAX_REFERENCE_DEPTH,
43
- maxCollectionSize = DEFAULT_MAX_COLLECTION_SIZE,
44
- maxFieldCount = DEFAULT_MAX_FIELD_COUNT,
45
- maxLength = DEFAULT_MAX_LENGTH,
46
- deadlineNs = BIGINT_MAX,
47
- } = {}
48
- ) {
49
- /** @type {{ deadlineReached: boolean, captureErrors: Error[] }} */
50
- const ctx = { deadlineReached: false, captureErrors: [] }
32
+ async function getLocalStateForCallFrame (callFrame, limits, deadlineNs = BIGINT_MAX) {
33
+ const { maxReferenceDepth, maxCollectionSize, maxFieldCount, maxLength } = limits
34
+ /** @type {{ deadlineReached: boolean, fatalErrors: Error[] }} */
35
+ const ctx = { deadlineReached: false, fatalErrors: [] }
51
36
  const opts = { maxReferenceDepth, maxCollectionSize, maxFieldCount, deadlineNs, ctx }
52
37
  const rawState = []
38
+ /** @type {ReturnType<typeof processRawState> | null} */
53
39
  let processedState = null
54
40
 
55
41
  for (const scope of callFrame.scopeChain) {
@@ -63,21 +49,167 @@ async function getLocalStateForCallFrame (
63
49
  // eslint-disable-next-line no-await-in-loop
64
50
  rawState.push(...await collectObjectProperties(objectId, opts))
65
51
  } catch (err) {
66
- ctx.captureErrors.push(new Error(
52
+ ctx.fatalErrors.push(new Error(
67
53
  `Error getting local state for closure scope (type: ${scope.type}). ` +
68
- 'Future snapshots for existing probes in this location will be skipped until the Node.js process is restarted',
54
+ 'Future snapshots for existing probes in this location will be skipped until the probes are re-applied',
69
55
  { cause: err } // TODO: The cause is not used by the backend
70
56
  ))
71
57
  }
72
58
  if (ctx.deadlineReached === true) break // TODO: Bad UX; Variables in remaining scopes are silently dropped
73
59
  }
74
60
 
75
- // Delay calling `processRawState` so the caller gets a chance to resume the main thread before processing `rawState`
61
+ // Delay calling `processRawState` so caller can resume the main thread before processing `rawState`
76
62
  return {
77
63
  processLocalState () {
78
64
  processedState = processedState ?? processRawState(rawState, maxLength)
79
65
  return processedState
80
66
  },
81
- captureErrors: ctx.captureErrors,
67
+ fatalErrors: ctx.fatalErrors,
68
+ }
69
+ }
70
+
71
+ /**
72
+ * @typedef {object} CompiledCaptureExpression
73
+ * @property {string} name - The name of the expression (used as key in snapshot)
74
+ * @property {string} expression - The compiled expression string to evaluate
75
+ * @property {CaptureLimits} limits - Fully resolved capture limits (precomputed at probe setup)
76
+ */
77
+
78
+ /**
79
+ * @typedef {object} CaptureExpressionResult
80
+ * @property {() => Record<string, ReturnType<typeof processRemoteObject>>} processCaptureExpressions - Callback to
81
+ * process raw data into snapshot format
82
+ * @property {{ expr: string, message: string }[]} evaluationErrors - Transient errors from expression evaluation
83
+ * (safe to retry)
84
+ * @property {Error[]} fatalErrors - Fatal errors that should disable capture expressions for this probe permanently
85
+ */
86
+
87
+ /**
88
+ * @typedef {object} EvaluateOnCallFrameResult
89
+ * @property {import('./processor').RemoteObjectWithProperties} result - The result of the evaluation
90
+ * @property {import('inspector').Runtime.ExceptionDetails} [exceptionDetails] - Exception details if evaluation failed
91
+ */
92
+
93
+ /**
94
+ * Evaluate capture expressions for a call frame.
95
+ *
96
+ * Collects raw data while paused, returns a callback to process after resume.
97
+ *
98
+ * @param {import('inspector').Debugger.CallFrame} callFrame - The call frame to evaluate expressions on
99
+ * @param {CompiledCaptureExpression[]} expressions - The compiled expressions with precomputed capture limits
100
+ * @param {bigint} [deadlineNs] - The deadline in nanoseconds. Defaults to {@link BIGINT_MAX}. If the deadline is
101
+ * reached, the snapshot will be truncated.
102
+ * @returns {Promise<CaptureExpressionResult>} Raw results with deferred processing callback
103
+ */
104
+ async function evaluateCaptureExpressions (callFrame, expressions, deadlineNs = BIGINT_MAX) {
105
+ /** @type {{ name: string, remoteObject: object, maxLength: number }[]} */
106
+ const rawResults = []
107
+ /** @type {{ expr: string, message: string }[]} */
108
+ const evaluationErrors = []
109
+ /** @type {Error[]} */
110
+ const fatalErrors = []
111
+ /** @type {Record<string, ReturnType<typeof processRemoteObject>> | null} */
112
+ let processedResult = null
113
+
114
+ for (let i = 0; i < expressions.length; i++) {
115
+ const { name, expression, limits } = expressions[i]
116
+ const { maxReferenceDepth, maxCollectionSize, maxFieldCount, maxLength } = limits
117
+
118
+ try {
119
+ const { result, exceptionDetails } = /** @type {EvaluateOnCallFrameResult} */ (
120
+ // eslint-disable-next-line no-await-in-loop
121
+ await session.post('Debugger.evaluateOnCallFrame', {
122
+ callFrameId: callFrame.callFrameId,
123
+ expression,
124
+ })
125
+ )
126
+
127
+ // Handle evaluation exceptions (maybe transient - bad expression, undefined var, etc.)
128
+ if (exceptionDetails) {
129
+ evaluationErrors.push({ expr: name, message: extractErrorMessage(exceptionDetails) })
130
+ continue
131
+ }
132
+
133
+ // Collect raw properties for objects/functions while still paused
134
+ if ((result.type === 'object' || result.type === 'function') && result.objectId && maxReferenceDepth > 0) {
135
+ const ctx = { deadlineReached: false, fatalErrors: [] }
136
+ const isCollection = result.subtype === 'array' || result.subtype === 'typedarray'
137
+
138
+ // eslint-disable-next-line no-await-in-loop
139
+ result.properties = await collectObjectProperties(
140
+ result.objectId,
141
+ {
142
+ // The expression result itself is depth 0, so we subtract 1 when collecting its properties (depth 1+)
143
+ maxReferenceDepth: maxReferenceDepth - 1,
144
+ maxCollectionSize,
145
+ maxFieldCount,
146
+ deadlineNs,
147
+ ctx,
148
+ },
149
+ 0,
150
+ isCollection
151
+ )
152
+
153
+ // Propagate fatal errors from nested collection
154
+ if (ctx.fatalErrors.length > 0) {
155
+ fatalErrors.push(...ctx.fatalErrors)
156
+ }
157
+
158
+ if (ctx.deadlineReached === true) {
159
+ // Add the current expression (properties may be incomplete due to timeout)
160
+ rawResults.push({ name, remoteObject: result, maxLength })
161
+ // Add stub entries for remaining uncaptured expressions
162
+ for (let j = i + 1; j < expressions.length; j++) {
163
+ rawResults.push({
164
+ name: expressions[j].name,
165
+ remoteObject: { notCapturedReason: 'timeout' },
166
+ maxLength: 0,
167
+ })
168
+ }
169
+ break
170
+ }
171
+ }
172
+
173
+ rawResults.push({ name, remoteObject: result, maxLength })
174
+ } catch (err) {
175
+ fatalErrors.push(new Error(
176
+ `Error capturing expression "${name}". ` +
177
+ 'Capture expressions for this probe will be skipped until the probe is re-applied',
178
+ { cause: err } // TODO: The cause is not used by the backend
179
+ ))
180
+ }
181
+ }
182
+
183
+ // Delay calling `processRemoteObject` so caller can resume the main thread before processing `remoteObject`
184
+ return {
185
+ processCaptureExpressions () {
186
+ if (processedResult !== null) return processedResult
187
+
188
+ processedResult = {}
189
+ for (const { name, remoteObject, maxLength } of rawResults) {
190
+ // If the remote object has notCapturedReason (e.g., timeout), use it as-is without processing
191
+ processedResult[name] = remoteObject.notCapturedReason === undefined
192
+ ? processRemoteObject(remoteObject, maxLength)
193
+ : remoteObject
194
+ }
195
+
196
+ return processedResult
197
+ },
198
+ evaluationErrors,
199
+ fatalErrors,
82
200
  }
83
201
  }
202
+
203
+ /**
204
+ * Extract the error message from the exception details.
205
+ *
206
+ * @param {import('inspector').Runtime.ExceptionDetails} exceptionDetails - The exception details
207
+ * @returns {string} The error message
208
+ */
209
+ function extractErrorMessage (exceptionDetails) {
210
+ const description = exceptionDetails.exception?.description
211
+ if (!description) return 'Unknown evaluation error'
212
+ const startOfStackTraceIndex = description.indexOf('\n at ')
213
+ if (startOfStackTraceIndex === -1) return description
214
+ return description.slice(0, startOfStackTraceIndex)
215
+ }