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.
- package/LICENSE-3rdparty.csv +60 -32
- package/ext/exporters.d.ts +1 -0
- package/ext/exporters.js +1 -0
- package/index.d.ts +243 -7
- package/package.json +9 -6
- package/packages/datadog-instrumentations/src/ai.js +54 -90
- package/packages/datadog-instrumentations/src/cucumber.js +14 -0
- package/packages/datadog-instrumentations/src/helpers/hook.js +17 -11
- package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +55 -14
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +15 -13
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +103 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.js +108 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -1
- package/packages/datadog-instrumentations/src/helpers/rewriter/transformer.js +21 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +138 -12
- package/packages/datadog-instrumentations/src/http/client.js +119 -1
- package/packages/datadog-instrumentations/src/jest.js +179 -15
- package/packages/datadog-instrumentations/src/kafkajs.js +20 -17
- 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 +9 -1
- package/packages/datadog-instrumentations/src/stripe.js +92 -0
- package/packages/datadog-instrumentations/src/vitest.js +11 -0
- package/packages/datadog-plugin-amqplib/src/consumer.js +14 -10
- package/packages/datadog-plugin-amqplib/src/producer.js +23 -19
- package/packages/datadog-plugin-azure-functions/src/index.js +53 -37
- package/packages/datadog-plugin-bullmq/src/consumer.js +33 -11
- package/packages/datadog-plugin-bullmq/src/producer.js +60 -31
- package/packages/datadog-plugin-cucumber/src/index.js +9 -6
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +33 -0
- package/packages/datadog-plugin-cypress/src/support.js +48 -8
- package/packages/datadog-plugin-jest/src/index.js +12 -2
- package/packages/datadog-plugin-jest/src/util.js +2 -1
- package/packages/datadog-plugin-kafkajs/src/consumer.js +22 -12
- package/packages/datadog-plugin-kafkajs/src/producer.js +33 -22
- package/packages/datadog-plugin-mocha/src/index.js +9 -6
- package/packages/datadog-plugin-playwright/src/index.js +10 -6
- package/packages/datadog-plugin-vitest/src/index.js +13 -8
- 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/analyzers/cookie-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/ssrf-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +4 -5
- package/packages/dd-trace/src/appsec/iast/path-line.js +36 -25
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +3 -4
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +3 -2
- 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/azure_metadata.js +0 -2
- 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/ci-visibility-exporter.js +2 -0
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +1 -1
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -1
- package/packages/dd-trace/src/ci-visibility/requests/request.js +236 -0
- package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
- package/packages/dd-trace/src/config/defaults.js +148 -195
- package/packages/dd-trace/src/config/helper.js +43 -1
- package/packages/dd-trace/src/config/index.js +42 -14
- package/packages/dd-trace/src/config/supported-configurations.json +4115 -510
- package/packages/dd-trace/src/constants.js +0 -2
- package/packages/dd-trace/src/crashtracking/crashtracker.js +10 -3
- package/packages/dd-trace/src/datastreams/pathway.js +22 -3
- package/packages/dd-trace/src/datastreams/processor.js +14 -1
- 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/encode/agentless-json.js +141 -0
- package/packages/dd-trace/src/exporter.js +2 -0
- package/packages/dd-trace/src/exporters/agent/writer.js +22 -8
- package/packages/dd-trace/src/exporters/agentless/index.js +89 -0
- package/packages/dd-trace/src/exporters/agentless/writer.js +184 -0
- package/packages/dd-trace/src/exporters/common/agents.js +1 -1
- package/packages/dd-trace/src/exporters/common/request.js +4 -4
- package/packages/dd-trace/src/llmobs/constants/writers.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/ai/index.js +5 -3
- package/packages/dd-trace/src/llmobs/sdk.js +34 -5
- package/packages/dd-trace/src/opentelemetry/context_manager.js +19 -46
- package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +3 -4
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +3 -5
- package/packages/dd-trace/src/opentracing/span.js +6 -4
- package/packages/dd-trace/src/plugins/ci_plugin.js +57 -5
- package/packages/dd-trace/src/plugins/database.js +57 -45
- 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/inferred_proxy.js +7 -0
- package/packages/dd-trace/src/plugins/util/test.js +48 -0
- package/packages/dd-trace/src/plugins/util/web.js +8 -7
- package/packages/dd-trace/src/profiling/exporter_cli.js +1 -0
- package/packages/dd-trace/src/propagation-hash/index.js +145 -0
- package/packages/dd-trace/src/proxy.js +4 -0
- package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +1 -1
- package/packages/dd-trace/src/startup-log.js +3 -3
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.json +0 -106
- package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js +0 -741
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +0 -11
- package/packages/dd-trace/src/plugins/util/serverless.js +0 -8
- 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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 &&
|
|
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
|
|
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
|
+
}
|