Skip to content

Commit bd829c1

Browse files
watsonrochdev
authored andcommitted
[DI] Improve error handling during snapshot collection (#5719)
This changes what happens if an error occurs while gathering the local state for the snapshot. Before this change, an `ERROR` event would be emitted to the diagnostics endpoint, while still sending a probe result without any snapshot to the intake, followed by an `EMITTING` event. However, not being able to collect the snapshot does not warrant emitting an `ERROR` event. This commit changes that behaviour to instead report the snapshot with a `notCapturedReason` without emitting an `ERROR` event.
1 parent dfba0db commit bd829c1

File tree

5 files changed

+64
-41
lines changed

5 files changed

+64
-41
lines changed

packages/dd-trace/src/debugger/devtools_client/index.js

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const session = require('./session')
66
const { getLocalStateForCallFrame } = require('./snapshot')
77
const send = require('./send')
88
const { getStackFromCallFrames } = require('./state')
9-
const { ackEmitting, ackError } = require('./status')
9+
const { ackEmitting } = require('./status')
1010
const { parentThreadId } = require('./config')
1111
const { MAX_SNAPSHOTS_PER_SECOND_GLOBALLY } = require('./defaults')
1212
const log = require('../../log')
@@ -144,20 +144,11 @@ session.on('Debugger.paused', async ({ params }) => {
144144
evalResults = result?.value ?? []
145145
}
146146

147-
let processLocalState
148-
if (numberOfProbesWithSnapshots !== 0) {
149-
try {
150-
// TODO: Create unique states for each affected probe based on that probes unique `capture` settings (DEBUG-2863)
151-
processLocalState = await getLocalStateForCallFrame(
152-
params.callFrames[0],
153-
{ maxReferenceDepth, maxCollectionSize, maxFieldCount, maxLength }
154-
)
155-
} catch (err) {
156-
for (let i = 0; i < numberOfProbesWithSnapshots; i++) {
157-
ackError(err, probes[snapshotProbeIndex[i]]) // TODO: Ok to continue after sending ackError?
158-
}
159-
}
160-
}
147+
// TODO: Create unique states for each affected probe based on that probes unique `capture` settings (DEBUG-2863)
148+
const processLocalState = numberOfProbesWithSnapshots !== 0 && await getLocalStateForCallFrame(
149+
params.callFrames[0],
150+
{ maxReferenceDepth, maxCollectionSize, maxFieldCount, maxLength }
151+
)
161152

162153
await session.post('Debugger.resume')
163154
const diff = process.hrtime.bigint() - start // TODO: Recored as telemetry (DEBUG-2858)
@@ -197,7 +188,9 @@ session.on('Debugger.paused', async ({ params }) => {
197188

198189
if (probe.captureSnapshot) {
199190
const state = processLocalState()
200-
if (state) {
191+
if (state instanceof Error) {
192+
snapshot.captureError = state.message
193+
} else if (state) {
201194
snapshot.captures = {
202195
lines: { [probe.location.lines[0]]: { locals: state } }
203196
}

packages/dd-trace/src/debugger/devtools_client/snapshot/index.js

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const { getRuntimeObject } = require('./collector')
44
const { processRawState } = require('./processor')
5+
const log = require('../../../log')
56

67
const DEFAULT_MAX_REFERENCE_DEPTH = 3
78
const DEFAULT_MAX_COLLECTION_SIZE = 100
@@ -24,13 +25,20 @@ async function getLocalStateForCallFrame (
2425
const rawState = []
2526
let processedState = null
2627

27-
await Promise.all(callFrame.scopeChain.map(async (scope) => {
28-
if (scope.type === 'global') return // The global scope is too noisy
29-
rawState.push(...await getRuntimeObject(
30-
scope.object.objectId,
31-
{ maxReferenceDepth, maxCollectionSize, maxFieldCount }
32-
))
33-
}))
28+
try {
29+
await Promise.all(callFrame.scopeChain.map(async (scope) => {
30+
if (scope.type === 'global') return // The global scope is too noisy
31+
rawState.push(...await getRuntimeObject(
32+
scope.object.objectId,
33+
{ maxReferenceDepth, maxCollectionSize, maxFieldCount }
34+
))
35+
}))
36+
} catch (err) {
37+
// TODO: We might be able to get part of the scope chain.
38+
// Consider if we could set errors just for the part of the scope chain that throws during collection.
39+
log.error('[debugger:devtools_client] Error getting local state for call frame', err)
40+
return () => new Error('Error getting local state')
41+
}
3442

3543
// Deplay calling `processRawState` so the caller gets a chance to resume the main thread before processing `rawState`
3644
return () => {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use strict'
2+
3+
require('../../../setup/mocha')
4+
5+
const assert = require('node:assert')
6+
7+
require('./stub-session')
8+
const { getLocalStateForCallFrame } = require('../../../../src/debugger/devtools_client/snapshot')
9+
10+
describe('debugger -> devtools client -> snapshot.getLocalStateForCallFrame', function () {
11+
describe('error handling', function () {
12+
it('should generate a notCapturedReason if an error is thrown during inital collection', async function () {
13+
const invalidCallFrameThatTriggersAnException = {}
14+
const processLocalState = await getLocalStateForCallFrame(invalidCallFrameThatTriggersAnException)
15+
const result = processLocalState()
16+
assert.ok(result instanceof Error)
17+
assert.strictEqual(result.message, 'Error getting local state')
18+
})
19+
})
20+
})
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use strict'
2+
3+
const inspector = require('../../../../src/debugger/devtools_client/inspector_promises_polyfill')
4+
const session = module.exports = new inspector.Session()
5+
session.connect()
6+
7+
session['@noCallThru'] = true
8+
proxyquire('../src/debugger/devtools_client/snapshot/collector', {
9+
'../session': session
10+
})
11+
proxyquire('../src/debugger/devtools_client/snapshot/redaction', {
12+
'../config': {
13+
dynamicInstrumentation: {
14+
redactedIdentifiers: [],
15+
redactionExcludedIdentifiers: []
16+
},
17+
'@noCallThru': true
18+
}
19+
})

packages/dd-trace/test/debugger/devtools_client/snapshot/utils.js

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,7 @@
22

33
const { join, basename } = require('path')
44

5-
const inspector = require('../../../../src/debugger/devtools_client/inspector_promises_polyfill')
6-
const session = new inspector.Session()
7-
session.connect()
8-
9-
session['@noCallThru'] = true
10-
proxyquire('../src/debugger/devtools_client/snapshot/collector', {
11-
'../session': session
12-
})
13-
proxyquire('../src/debugger/devtools_client/snapshot/redaction', {
14-
'../config': {
15-
dynamicInstrumentation: {
16-
redactedIdentifiers: [],
17-
redactionExcludedIdentifiers: []
18-
},
19-
'@noCallThru': true
20-
}
21-
})
22-
5+
const session = require('./stub-session')
236
const { getLocalStateForCallFrame } = require('../../../../src/debugger/devtools_client/snapshot')
247

258
module.exports = {

0 commit comments

Comments
 (0)