dd-trace 5.85.0 → 5.87.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 (71) hide show
  1. package/index.d.ts +38 -4
  2. package/package.json +1 -1
  3. package/packages/datadog-core/src/storage.js +30 -12
  4. package/packages/datadog-instrumentations/src/cucumber.js +14 -0
  5. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  6. package/packages/datadog-instrumentations/src/http/client.js +119 -1
  7. package/packages/datadog-instrumentations/src/jest.js +135 -10
  8. package/packages/datadog-instrumentations/src/mocha/main.js +9 -0
  9. package/packages/datadog-instrumentations/src/mocha/utils.js +6 -0
  10. package/packages/datadog-instrumentations/src/mysql2.js +131 -64
  11. package/packages/datadog-instrumentations/src/playwright.js +8 -0
  12. package/packages/datadog-instrumentations/src/prisma.js +225 -30
  13. package/packages/datadog-instrumentations/src/stripe.js +92 -0
  14. package/packages/datadog-instrumentations/src/vitest.js +11 -0
  15. package/packages/datadog-instrumentations/src/ws.js +22 -0
  16. package/packages/datadog-plugin-azure-functions/src/index.js +53 -37
  17. package/packages/datadog-plugin-cucumber/src/index.js +4 -10
  18. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +12 -1
  19. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +2 -4
  20. package/packages/datadog-plugin-http/src/server.js +23 -8
  21. package/packages/datadog-plugin-jest/src/index.js +29 -10
  22. package/packages/datadog-plugin-jest/src/util.js +7 -1
  23. package/packages/datadog-plugin-mocha/src/index.js +5 -17
  24. package/packages/datadog-plugin-playwright/src/index.js +3 -0
  25. package/packages/datadog-plugin-prisma/src/datadog-tracing-helper.js +37 -14
  26. package/packages/datadog-plugin-prisma/src/index.js +8 -5
  27. package/packages/datadog-plugin-router/src/index.js +28 -19
  28. package/packages/datadog-plugin-vitest/src/index.js +6 -10
  29. package/packages/datadog-plugin-ws/src/server.js +8 -0
  30. package/packages/dd-trace/src/appsec/addresses.js +11 -0
  31. package/packages/dd-trace/src/appsec/channels.js +5 -1
  32. package/packages/dd-trace/src/appsec/downstream_requests.js +302 -0
  33. package/packages/dd-trace/src/appsec/iast/path-line.js +1 -0
  34. package/packages/dd-trace/src/appsec/index.js +103 -0
  35. package/packages/dd-trace/src/appsec/rasp/ssrf.js +66 -4
  36. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +14 -1
  37. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  38. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +19 -0
  39. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +15 -0
  40. package/packages/dd-trace/src/ci-visibility/telemetry.js +36 -0
  41. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +44 -1
  42. package/packages/dd-trace/src/config/defaults.js +2 -0
  43. package/packages/dd-trace/src/config/index.js +6 -0
  44. package/packages/dd-trace/src/config/supported-configurations.json +2 -0
  45. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +47 -2
  46. package/packages/dd-trace/src/debugger/devtools_client/index.js +75 -23
  47. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +23 -1
  48. package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +3 -3
  49. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +168 -36
  50. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +18 -0
  51. package/packages/dd-trace/src/exporters/common/agents.js +1 -1
  52. package/packages/dd-trace/src/exporters/common/request.js +35 -35
  53. package/packages/dd-trace/src/id.js +1 -1
  54. package/packages/dd-trace/src/lambda/context.js +27 -0
  55. package/packages/dd-trace/src/lambda/handler.js +5 -18
  56. package/packages/dd-trace/src/llmobs/constants/writers.js +1 -1
  57. package/packages/dd-trace/src/llmobs/sdk.js +34 -5
  58. package/packages/dd-trace/src/log/writer.js +1 -5
  59. package/packages/dd-trace/src/plugins/ci_plugin.js +63 -1
  60. package/packages/dd-trace/src/plugins/database.js +42 -43
  61. package/packages/dd-trace/src/plugins/outbound.js +27 -2
  62. package/packages/dd-trace/src/plugins/tracing.js +39 -4
  63. package/packages/dd-trace/src/plugins/util/git.js +27 -30
  64. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +7 -0
  65. package/packages/dd-trace/src/plugins/util/test.js +3 -1
  66. package/packages/dd-trace/src/plugins/util/web.js +9 -7
  67. package/packages/dd-trace/src/profiling/config.js +6 -14
  68. package/packages/dd-trace/src/profiling/exporters/agent.js +23 -24
  69. package/packages/dd-trace/src/profiling/profiler.js +2 -0
  70. package/packages/dd-trace/src/startup-log.js +3 -2
  71. package/packages/dd-trace/src/plugins/util/serverless.js +0 -8
@@ -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
+ }
@@ -6,6 +6,24 @@ const { normalizeName, REDACTED_IDENTIFIERS } = require('./redaction')
6
6
 
7
7
  module.exports = {
8
8
  processRawState: processProperties,
9
+ processRemoteObject,
10
+ }
11
+
12
+ /**
13
+ * A RemoteObject with collected properties attached.
14
+ *
15
+ * @typedef {import('inspector').Runtime.RemoteObject & { properties?: object[] }} RemoteObjectWithProperties
16
+ */
17
+
18
+ /**
19
+ * Process a RemoteObject into the snapshot format.
20
+ *
21
+ * @param {RemoteObjectWithProperties} remoteObject
22
+ * @param {number} maxLength - Maximum string length
23
+ * @returns {object} The processed value in snapshot format
24
+ */
25
+ function processRemoteObject (remoteObject, maxLength) {
26
+ return getPropertyValueRaw({ value: remoteObject }, maxLength)
9
27
  }
10
28
 
11
29
  // Matches classes in source code, no matter how it's written:
@@ -38,5 +38,5 @@ const HttpsAgent = createAgentClass(https.Agent)
38
38
 
39
39
  module.exports = {
40
40
  httpAgent: new HttpAgent(),
41
- HttpsAgent: new HttpsAgent(),
41
+ httpsAgent: new HttpsAgent(),
42
42
  }
@@ -128,50 +128,50 @@ function request (data, options, callback) {
128
128
 
129
129
  activeRequests++
130
130
 
131
- const store = storage('legacy').getStore()
132
- storage('legacy').enterWith({ noop: true })
133
-
134
- let finished = false
135
- const finalize = () => {
136
- if (finished) return
137
- finished = true
138
- activeRequests--
139
- }
131
+ storage('legacy').run({ noop: true }, () => {
132
+ let finished = false
133
+ const finalize = () => {
134
+ if (finished) return
135
+ finished = true
136
+ activeRequests--
137
+ }
140
138
 
141
- const req = client.request(options, (res) => onResponse(res, finalize))
139
+ const req = client.request(options, (res) => onResponse(res, finalize))
142
140
 
143
- req.once('close', finalize)
144
- req.once('timeout', finalize)
141
+ req.once('close', finalize)
142
+ req.once('timeout', finalize)
145
143
 
146
- req.once('error', err => {
147
- finalize()
148
- onError(err)
149
- })
144
+ req.once('error', err => {
145
+ finalize()
146
+ onError(err)
147
+ })
150
148
 
151
- req.setTimeout(timeout, () => {
152
- try {
153
- if (typeof req.abort === 'function') {
154
- req.abort()
155
- } else {
156
- req.destroy()
149
+ req.setTimeout(timeout, () => {
150
+ try {
151
+ if (typeof req.abort === 'function') {
152
+ req.abort()
153
+ } else {
154
+ req.destroy()
155
+ }
156
+ } catch {
157
+ // ignore
157
158
  }
158
- } catch {
159
- // ignore
159
+ })
160
+
161
+ if (isReadable) {
162
+ data.pipe(req) // TODO: Validate whether this is actually retriable.
163
+ } else {
164
+ for (const buffer of dataArray) req.write(buffer)
165
+ req.end()
160
166
  }
161
167
  })
162
-
163
- if (isReadable) {
164
- data.pipe(req) // TODO: Validate whether this is actually retriable.
165
- } else {
166
- for (const buffer of dataArray) req.write(buffer)
167
- req.end()
168
- }
169
-
170
- storage('legacy').enterWith(store)
171
168
  }
172
169
 
173
- // TODO: Figure out why setTimeout is needed to avoid losing the async context
174
- // in the retry request before socket.connect() is called.
170
+ // The setTimeout is needed to avoid losing the async context in the retry
171
+ // request before socket.connect() is called. This is a workaround for the
172
+ // issue that the AsyncLocalStorage.run() method does not call the
173
+ // AsyncLocalStorage.enterWith() method when not using AsyncContextFrame.
174
+ //
175
175
  // TODO: Test that this doesn't trace itself on retry when the diagnostics
176
176
  // channel events are available in the agent exporter.
177
177
  makeRequest(() => setTimeout(() => makeRequest(callback)))
@@ -243,5 +243,5 @@ function writeUInt32BE (buffer, value, offset) {
243
243
  * @returns {Identifier}
244
244
  */
245
245
  module.exports = function createIdentifier (value, radix) {
246
- return new Identifier(value, radix)
246
+ return new Identifier(value ?? '', radix)
247
247
  }
@@ -0,0 +1,27 @@
1
+ 'use strict'
2
+
3
+ const log = require('../log')
4
+
5
+ /**
6
+ * Extracts the context from the given Lambda handler arguments.
7
+ *
8
+ * It is possible for users to define a lambda function without specifying a
9
+ * context arg. In these cases, this function returns null instead of throwing
10
+ * an error.
11
+ *
12
+ * @param {unknown[]} args any amount of arguments
13
+ * @returns {object | null}
14
+ */
15
+ exports.extractContext = function extractContext (args) {
16
+ let context = null
17
+ for (let i = 0; i < args.length && i < 3; i++) {
18
+ if (args[i] && typeof args[i].getRemainingTimeInMillis === 'function') {
19
+ context = args[i]
20
+ break
21
+ }
22
+ }
23
+ if (!context) {
24
+ log.debug('Unable to extract context object from Lambda handler arguments')
25
+ }
26
+ return context
27
+ }
@@ -5,6 +5,7 @@ const { channel } = require('../../../datadog-instrumentations/src/helpers/instr
5
5
  const { ERROR_MESSAGE, ERROR_TYPE } = require('../constants')
6
6
  const { getValueFromEnvSources } = require('../config/helper')
7
7
  const { ImpendingTimeout } = require('./runtime/errors')
8
+ const { extractContext } = require('./context')
8
9
 
9
10
  const globalTracer = global._ddtrace
10
11
  const tracer = globalTracer._tracer
@@ -60,23 +61,6 @@ function crashFlush () {
60
61
  }
61
62
  }
62
63
 
63
- /**
64
- * Extracts the context from the given Lambda handler arguments.
65
- *
66
- * @param {unknown[]} args any amount of arguments
67
- * @returns the context, if extraction was succesful.
68
- */
69
- function extractContext (args) {
70
- let context = args.length > 1 ? args[1] : undefined
71
- if (context === undefined || context.getRemainingTimeInMillis === undefined) {
72
- context = args.length > 2 ? args[2] : undefined
73
- if (context === undefined || context.getRemainingTimeInMillis === undefined) {
74
- throw new Error('Could not extract context')
75
- }
76
- }
77
- return context
78
- }
79
-
80
64
  /**
81
65
  * Patches your AWS Lambda handler function to add some tracing support.
82
66
  *
@@ -86,7 +70,10 @@ exports.datadog = function datadog (lambdaHandler) {
86
70
  return (...args) => {
87
71
  const context = extractContext(args)
88
72
 
89
- checkTimeout(context)
73
+ if (context) {
74
+ checkTimeout(context)
75
+ }
76
+
90
77
  const result = lambdaHandler.apply(this, args)
91
78
  if (result && typeof result.then === 'function') {
92
79
  return result.then((res) => {