Skip to content

Commit 693f6ca

Browse files
authored
feat(node): Add scope to ANR events (#11256)
Closes #10668 Rather than inject large unchecked JavaScript strings to run via `Runtime.evaluate`, when the ANR integration is enabled, we add a function to `global.__SENTRY_GET_SCOPES__` which can then be called via the debugger when the event loop is suspended.
1 parent 0000055 commit 693f6ca

File tree

9 files changed

+173
-18
lines changed

9 files changed

+173
-18
lines changed

dev-packages/node-integration-tests/suites/anr/basic-session.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ Sentry.init({
1515
autoSessionTracking: true,
1616
});
1717

18+
Sentry.setUser({ email: '[email protected]' });
19+
Sentry.addBreadcrumb({ message: 'important message!' });
20+
1821
function longWork() {
1922
for (let i = 0; i < 20; i++) {
2023
const salt = crypto.randomBytes(128).toString('base64');

dev-packages/node-integration-tests/suites/anr/basic.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ Sentry.init({
1515
integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })],
1616
});
1717

18+
Sentry.setUser({ email: '[email protected]' });
19+
Sentry.addBreadcrumb({ message: 'important message!' });
20+
1821
function longWork() {
1922
for (let i = 0; i < 20; i++) {
2023
const salt = crypto.randomBytes(128).toString('base64');

dev-packages/node-integration-tests/suites/anr/basic.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ Sentry.init({
1515
integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })],
1616
});
1717

18+
Sentry.setUser({ email: '[email protected]' });
19+
Sentry.addBreadcrumb({ message: 'important message!' });
20+
1821
function longWork() {
1922
for (let i = 0; i < 20; i++) {
2023
const salt = crypto.randomBytes(128).toString('base64');

dev-packages/node-integration-tests/suites/anr/forked.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ Sentry.init({
1515
integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })],
1616
});
1717

18+
Sentry.setUser({ email: '[email protected]' });
19+
Sentry.addBreadcrumb({ message: 'important message!' });
20+
1821
function longWork() {
1922
for (let i = 0; i < 20; i++) {
2023
const salt = crypto.randomBytes(128).toString('base64');
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import * as assert from 'assert';
2+
import * as crypto from 'crypto';
3+
4+
import * as Sentry from '@sentry/node';
5+
6+
setTimeout(() => {
7+
process.exit();
8+
}, 10000);
9+
10+
Sentry.init({
11+
dsn: 'https://[email protected]/1337',
12+
release: '1.0',
13+
debug: true,
14+
autoSessionTracking: false,
15+
integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })],
16+
});
17+
18+
async function longWork() {
19+
await new Promise(resolve => setTimeout(resolve, 1000));
20+
21+
for (let i = 0; i < 20; i++) {
22+
const salt = crypto.randomBytes(128).toString('base64');
23+
const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512');
24+
assert.ok(hash);
25+
}
26+
}
27+
28+
function neverResolve() {
29+
return new Promise(() => {
30+
//
31+
});
32+
}
33+
34+
const fns = [
35+
neverResolve,
36+
neverResolve,
37+
neverResolve,
38+
neverResolve,
39+
neverResolve,
40+
longWork, // [5]
41+
neverResolve,
42+
neverResolve,
43+
neverResolve,
44+
neverResolve,
45+
];
46+
47+
for (let id = 0; id < 10; id++) {
48+
Sentry.withIsolationScope(async () => {
49+
Sentry.setUser({ id });
50+
51+
await fns[id]();
52+
});
53+
}

dev-packages/node-integration-tests/suites/anr/stop-and-start.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ Sentry.init({
1717
integrations: [anr],
1818
});
1919

20+
Sentry.setUser({ email: '[email protected]' });
21+
Sentry.addBreadcrumb({ message: 'important message!' });
22+
2023
function longWorkIgnored() {
2124
for (let i = 0; i < 20; i++) {
2225
const salt = crypto.randomBytes(128).toString('base64');

dev-packages/node-integration-tests/suites/anr/test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ const EXPECTED_ANR_EVENT = {
2121
timezone: expect.any(String),
2222
},
2323
},
24+
user: {
25+
26+
},
27+
breadcrumbs: [
28+
{
29+
timestamp: expect.any(Number),
30+
message: 'important message!',
31+
},
32+
],
2433
// and an exception that is our ANR
2534
exception: {
2635
values: [
@@ -105,4 +114,34 @@ conditionalTest({ min: 16 })('should report ANR when event loop blocked', () =>
105114
test('worker can be stopped and restarted', done => {
106115
createRunner(__dirname, 'stop-and-start.js').expect({ event: EXPECTED_ANR_EVENT }).start(done);
107116
});
117+
118+
const EXPECTED_ISOLATED_EVENT = {
119+
user: {
120+
id: 5,
121+
},
122+
exception: {
123+
values: [
124+
{
125+
type: 'ApplicationNotResponding',
126+
value: 'Application Not Responding for at least 100 ms',
127+
mechanism: { type: 'ANR' },
128+
stacktrace: {
129+
frames: expect.arrayContaining([
130+
{
131+
colno: expect.any(Number),
132+
lineno: expect.any(Number),
133+
filename: expect.stringMatching(/isolated.mjs$/),
134+
function: 'longWork',
135+
in_app: true,
136+
},
137+
]),
138+
},
139+
},
140+
],
141+
},
142+
};
143+
144+
test('fetches correct isolated scope', done => {
145+
createRunner(__dirname, 'isolated.mjs').expect({ event: EXPECTED_ISOLATED_EVENT }).start(done);
146+
});
108147
});

packages/node-experimental/src/integrations/anr/index.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { defineIntegration, getCurrentScope } from '@sentry/core';
2-
import type { Contexts, Event, EventHint, Integration, IntegrationFn } from '@sentry/types';
3-
import { logger } from '@sentry/utils';
1+
import { defineIntegration, mergeScopeData } from '@sentry/core';
2+
import type { Contexts, Event, EventHint, Integration, IntegrationFn, ScopeData } from '@sentry/types';
3+
import { GLOBAL_OBJ, logger } from '@sentry/utils';
44
import * as inspector from 'inspector';
55
import { Worker } from 'worker_threads';
6+
import { getCurrentScope, getGlobalScope, getIsolationScope } from '../..';
67
import { NODE_VERSION } from '../../nodeVersion';
78
import type { NodeClient } from '../../sdk/client';
89
import type { AnrIntegrationOptions, WorkerStartData } from './common';
@@ -15,8 +16,26 @@ function log(message: string, ...args: unknown[]): void {
1516
logger.log(`[ANR] ${message}`, ...args);
1617
}
1718

19+
function globalWithScopeFetchFn(): typeof GLOBAL_OBJ & { __SENTRY_GET_SCOPES__?: () => ScopeData } {
20+
return GLOBAL_OBJ;
21+
}
22+
23+
/** Fetches merged scope data */
24+
function getScopeData(): ScopeData {
25+
const scope = getGlobalScope().getScopeData();
26+
mergeScopeData(scope, getIsolationScope().getScopeData());
27+
mergeScopeData(scope, getCurrentScope().getScopeData());
28+
29+
// We remove attachments because they likely won't serialize well as json
30+
scope.attachments = [];
31+
// We can't serialize event processor functions
32+
scope.eventProcessors = [];
33+
34+
return scope;
35+
}
36+
1837
/**
19-
* Gets contexts by calling all event processors. This relies on being called after all integrations are setup
38+
* Gets contexts by calling all event processors. This shouldn't be called until all integrations are setup
2039
*/
2140
async function getContexts(client: NodeClient): Promise<Contexts> {
2241
let event: Event | null = { message: 'ANR' };
@@ -35,9 +54,18 @@ const INTEGRATION_NAME = 'Anr';
3554
type AnrInternal = { startWorker: () => void; stopWorker: () => void };
3655

3756
const _anrIntegration = ((options: Partial<AnrIntegrationOptions> = {}) => {
57+
if (NODE_VERSION.major < 16 || (NODE_VERSION.major === 16 && NODE_VERSION.minor < 17)) {
58+
throw new Error('ANR detection requires Node 16.17.0 or later');
59+
}
60+
3861
let worker: Promise<() => void> | undefined;
3962
let client: NodeClient | undefined;
4063

64+
// Hookup the scope fetch function to the global object so that it can be called from the worker thread via the
65+
// debugger when it pauses
66+
const gbl = globalWithScopeFetchFn();
67+
gbl.__SENTRY_GET_SCOPES__ = getScopeData;
68+
4169
return {
4270
name: INTEGRATION_NAME,
4371
startWorker: () => {
@@ -59,10 +87,6 @@ const _anrIntegration = ((options: Partial<AnrIntegrationOptions> = {}) => {
5987
}
6088
},
6189
setup(initClient: NodeClient) {
62-
if (NODE_VERSION.major < 16 || (NODE_VERSION.major === 16 && NODE_VERSION.minor < 17)) {
63-
throw new Error('ANR detection requires Node 16.17.0 or later');
64-
}
65-
6690
client = initClient;
6791

6892
// setImmediate is used to ensure that all other integrations have had their setup called first.

packages/node-experimental/src/integrations/anr/worker.ts

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import {
2+
applyScopeDataToEvent,
23
createEventEnvelope,
34
createSessionEnvelope,
45
getEnvelopeEndpointWithUrlEncodedAuth,
56
makeSession,
67
updateSession,
78
} from '@sentry/core';
8-
import type { Event, Session, StackFrame, TraceContext } from '@sentry/types';
9+
import type { Event, ScopeData, Session, StackFrame } from '@sentry/types';
910
import {
1011
callFrameToStackFrame,
1112
normalizeUrlToBase,
@@ -86,7 +87,23 @@ function prepareStackFrames(stackFrames: StackFrame[] | undefined): StackFrame[]
8687
return strippedFrames;
8788
}
8889

89-
async function sendAnrEvent(frames?: StackFrame[], traceContext?: TraceContext): Promise<void> {
90+
function applyScopeToEvent(event: Event, scope: ScopeData): void {
91+
applyScopeDataToEvent(event, scope);
92+
93+
if (!event.contexts?.trace) {
94+
const { traceId, spanId, parentSpanId } = scope.propagationContext;
95+
event.contexts = {
96+
trace: {
97+
trace_id: traceId,
98+
span_id: spanId,
99+
parent_span_id: parentSpanId,
100+
},
101+
...event.contexts,
102+
};
103+
}
104+
}
105+
106+
async function sendAnrEvent(frames?: StackFrame[], scope?: ScopeData): Promise<void> {
90107
if (hasSentAnrEvent) {
91108
return;
92109
}
@@ -99,7 +116,7 @@ async function sendAnrEvent(frames?: StackFrame[], traceContext?: TraceContext):
99116

100117
const event: Event = {
101118
event_id: uuid4(),
102-
contexts: { ...options.contexts, trace: traceContext },
119+
contexts: options.contexts,
103120
release: options.release,
104121
environment: options.environment,
105122
dist: options.dist,
@@ -119,6 +136,10 @@ async function sendAnrEvent(frames?: StackFrame[], traceContext?: TraceContext):
119136
tags: options.staticTags,
120137
};
121138

139+
if (scope) {
140+
applyScopeToEvent(event, scope);
141+
}
142+
122143
const envelope = createEventEnvelope(event, options.dsn, options.sdkMetadata, options.tunnel);
123144
// Log the envelope to aid in testing
124145
log(JSON.stringify(envelope));
@@ -171,20 +192,23 @@ if (options.captureStackTrace) {
171192
'Runtime.evaluate',
172193
{
173194
// Grab the trace context from the current scope
174-
expression:
175-
'var __sentry_ctx = __SENTRY__.acs?.getCurrentScope().getPropagationContext() || {}; __sentry_ctx.traceId + "-" + __sentry_ctx.spanId + "-" + __sentry_ctx.parentSpanId',
195+
expression: 'global.__SENTRY_GET_SCOPES__();',
176196
// Don't re-trigger the debugger if this causes an error
177197
silent: true,
198+
// Serialize the result to json otherwise only primitives are supported
199+
returnByValue: true,
178200
},
179-
(_, param) => {
180-
const traceId = param && param.result ? (param.result.value as string) : '--';
181-
const [trace_id, span_id, parent_span_id] = traceId.split('-') as (string | undefined)[];
201+
(err, param) => {
202+
if (err) {
203+
log(`Error executing script: '${err.message}'`);
204+
}
205+
206+
const scopes = param && param.result ? (param.result.value as ScopeData) : undefined;
182207

183208
session.post('Debugger.resume');
184209
session.post('Debugger.disable');
185210

186-
const context = trace_id?.length && span_id?.length ? { trace_id, span_id, parent_span_id } : undefined;
187-
sendAnrEvent(stackFrames, context).then(null, () => {
211+
sendAnrEvent(stackFrames, scopes).then(null, () => {
188212
log('Sending ANR event failed.');
189213
});
190214
},

0 commit comments

Comments
 (0)