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.
- package/index.d.ts +38 -4
- package/package.json +1 -1
- package/packages/datadog-core/src/storage.js +30 -12
- package/packages/datadog-instrumentations/src/cucumber.js +14 -0
- package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
- package/packages/datadog-instrumentations/src/http/client.js +119 -1
- package/packages/datadog-instrumentations/src/jest.js +135 -10
- package/packages/datadog-instrumentations/src/mocha/main.js +9 -0
- package/packages/datadog-instrumentations/src/mocha/utils.js +6 -0
- package/packages/datadog-instrumentations/src/mysql2.js +131 -64
- package/packages/datadog-instrumentations/src/playwright.js +8 -0
- package/packages/datadog-instrumentations/src/prisma.js +225 -30
- package/packages/datadog-instrumentations/src/stripe.js +92 -0
- package/packages/datadog-instrumentations/src/vitest.js +11 -0
- package/packages/datadog-instrumentations/src/ws.js +22 -0
- package/packages/datadog-plugin-azure-functions/src/index.js +53 -37
- package/packages/datadog-plugin-cucumber/src/index.js +4 -10
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +12 -1
- package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +2 -4
- package/packages/datadog-plugin-http/src/server.js +23 -8
- package/packages/datadog-plugin-jest/src/index.js +29 -10
- package/packages/datadog-plugin-jest/src/util.js +7 -1
- package/packages/datadog-plugin-mocha/src/index.js +5 -17
- package/packages/datadog-plugin-playwright/src/index.js +3 -0
- package/packages/datadog-plugin-prisma/src/datadog-tracing-helper.js +37 -14
- package/packages/datadog-plugin-prisma/src/index.js +8 -5
- package/packages/datadog-plugin-router/src/index.js +28 -19
- package/packages/datadog-plugin-vitest/src/index.js +6 -10
- package/packages/datadog-plugin-ws/src/server.js +8 -0
- package/packages/dd-trace/src/appsec/addresses.js +11 -0
- package/packages/dd-trace/src/appsec/channels.js +5 -1
- package/packages/dd-trace/src/appsec/downstream_requests.js +302 -0
- package/packages/dd-trace/src/appsec/iast/path-line.js +1 -0
- package/packages/dd-trace/src/appsec/index.js +103 -0
- package/packages/dd-trace/src/appsec/rasp/ssrf.js +66 -4
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +14 -1
- package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +19 -0
- package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +15 -0
- package/packages/dd-trace/src/ci-visibility/telemetry.js +36 -0
- package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +44 -1
- package/packages/dd-trace/src/config/defaults.js +2 -0
- package/packages/dd-trace/src/config/index.js +6 -0
- package/packages/dd-trace/src/config/supported-configurations.json +2 -0
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +47 -2
- package/packages/dd-trace/src/debugger/devtools_client/index.js +75 -23
- package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +23 -1
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +3 -3
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +168 -36
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +18 -0
- package/packages/dd-trace/src/exporters/common/agents.js +1 -1
- package/packages/dd-trace/src/exporters/common/request.js +35 -35
- package/packages/dd-trace/src/id.js +1 -1
- package/packages/dd-trace/src/lambda/context.js +27 -0
- package/packages/dd-trace/src/lambda/handler.js +5 -18
- package/packages/dd-trace/src/llmobs/constants/writers.js +1 -1
- package/packages/dd-trace/src/llmobs/sdk.js +34 -5
- package/packages/dd-trace/src/log/writer.js +1 -5
- package/packages/dd-trace/src/plugins/ci_plugin.js +63 -1
- package/packages/dd-trace/src/plugins/database.js +42 -43
- package/packages/dd-trace/src/plugins/outbound.js +27 -2
- package/packages/dd-trace/src/plugins/tracing.js +39 -4
- package/packages/dd-trace/src/plugins/util/git.js +27 -30
- package/packages/dd-trace/src/plugins/util/inferred_proxy.js +7 -0
- package/packages/dd-trace/src/plugins/util/test.js +3 -1
- package/packages/dd-trace/src/plugins/util/web.js +9 -7
- package/packages/dd-trace/src/profiling/config.js +6 -14
- package/packages/dd-trace/src/profiling/exporters/agent.js +23 -24
- package/packages/dd-trace/src/profiling/profiler.js +2 -0
- package/packages/dd-trace/src/startup-log.js +3 -2
- 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
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
184
|
+
let processLocalState
|
|
185
|
+
/** @type {Error[] | undefined} */
|
|
186
|
+
let fatalSnapshotErrors
|
|
177
187
|
if (numberOfProbesWithSnapshots !== 0) {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
maxCollectionSize,
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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 (
|
|
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 =
|
|
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 {
|
|
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.
|
|
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.
|
|
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}
|
|
20
|
-
* @property {number}
|
|
21
|
-
*
|
|
22
|
-
* @property {number}
|
|
23
|
-
*
|
|
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 {
|
|
37
|
-
* @
|
|
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
|
-
|
|
41
|
-
{
|
|
42
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
@@ -128,50 +128,50 @@ function request (data, options, callback) {
|
|
|
128
128
|
|
|
129
129
|
activeRequests++
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
139
|
+
const req = client.request(options, (res) => onResponse(res, finalize))
|
|
142
140
|
|
|
143
|
-
|
|
144
|
-
|
|
141
|
+
req.once('close', finalize)
|
|
142
|
+
req.once('timeout', finalize)
|
|
145
143
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
144
|
+
req.once('error', err => {
|
|
145
|
+
finalize()
|
|
146
|
+
onError(err)
|
|
147
|
+
})
|
|
150
148
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
}
|
|
159
|
-
|
|
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
|
-
//
|
|
174
|
-
//
|
|
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)))
|
|
@@ -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
|
-
|
|
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) => {
|