Skip to content

Commit 9f594dd

Browse files
authored
fix(node-native): Don't drop breadcrumbs from event loop block events (#22322)
1 parent da1645c commit 9f594dd

3 files changed

Lines changed: 38 additions & 4 deletions

File tree

dev-packages/node-integration-tests/suites/thread-blocked-native/basic.mjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ Sentry.init({
1414
integrations: [eventLoopBlockIntegration()],
1515
});
1616

17+
// Sentry.addBreadcrumb() writes to the isolation scope which is only captured via
18+
// AsyncLocalStorage, so we add to the current scope to test the poll state route
19+
Sentry.getCurrentScope().addBreadcrumb({
20+
category: 'test',
21+
message: 'blocking event loop soon',
22+
level: 'info',
23+
});
24+
1725
setTimeout(() => {
1826
longWork();
1927
}, 2000);

dev-packages/node-integration-tests/suites/thread-blocked-native/test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,20 @@ describe('Thread Blocked Native', { timeout: 30_000 }, () => {
105105
test('ESM', async () => {
106106
await createRunner(__dirname, 'basic.mjs')
107107
.withMockSentryServer()
108-
.expect({ event: ANR_EVENT_WITH_DEBUG_META('basic') })
108+
.expect({
109+
event: {
110+
...ANR_EVENT_WITH_DEBUG_META('basic'),
111+
// Ensures breadcrumbs make it through the poll state rather than via AsyncLocalStorage
112+
breadcrumbs: [
113+
{
114+
timestamp: expect.any(Number),
115+
category: 'test',
116+
message: 'blocking event loop soon',
117+
level: 'info',
118+
},
119+
],
120+
},
121+
})
109122
.start()
110123
.completed();
111124
});

packages/node-native/src/event-loop-block-watchdog.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,21 @@ function getExceptionAndThreads(
225225
};
226226
}
227227

228+
/**
229+
* Rehydrates a Scope from serialized ScopeData that has been passed over a JSON serialization boundary.
230+
*
231+
* `Scope.update()` only handles `ScopeContext` fields, so breadcrumbs have to be carried over separately.
232+
* Attachments are deliberately not carried over since their binary data does not survive JSON serialization.
233+
*/
234+
function hydrateScope(data: Partial<ScopeData> | undefined): Scope {
235+
const scope = new Scope();
236+
if (data) {
237+
scope.update(data);
238+
data.breadcrumbs?.forEach(breadcrumb => scope.addBreadcrumb(breadcrumb));
239+
}
240+
return scope;
241+
}
242+
228243
function applyScopeToEvent(event: Event, scope: ScopeData): void {
229244
applyScopeDataToEvent(event, scope);
230245

@@ -274,9 +289,7 @@ async function sendBlockEvent(crashedThreadId: string): Promise<void> {
274289
...getExceptionAndThreads(crashedThreadId, threads),
275290
};
276291

277-
const scope = crashedThread.pollState?.scope
278-
? new Scope().update(crashedThread.pollState.scope).getScopeData()
279-
: new Scope().getScopeData();
292+
const scope = hydrateScope(crashedThread.pollState?.scope).getScopeData();
280293

281294
if (crashedThread?.asyncState?.isolationScope) {
282295
// We need to rehydrate the scope from the serialized object with properties beginning with _user, etc

0 commit comments

Comments
 (0)