Skip to content

Commit f765d07

Browse files
committed
feat(node): Add scope to ANR events
improve comments test isolated scope minor simplify test longer other contexts
1 parent 16dc092 commit f765d07

File tree

9 files changed

+181
-20
lines changed

9 files changed

+181
-20
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
@@ -14,6 +14,9 @@ Sentry.init({
1414
integrations: [new Sentry.Integrations.Anr({ captureStackTrace: true, anrThreshold: 100 })],
1515
});
1616

17+
Sentry.setUser({ email: '[email protected]' });
18+
Sentry.addBreadcrumb({ message: 'important message!' });
19+
1720
function longWork() {
1821
for (let i = 0; i < 20; i++) {
1922
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: [new Sentry.Integrations.Anr({ 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: [new Sentry.Integrations.Anr({ 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: [new Sentry.Integrations.Anr({ 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: [
@@ -110,4 +119,34 @@ conditionalTest({ min: 16 })('should report ANR when event loop blocked', () =>
110119
test('worker can be stopped and restarted', done => {
111120
createRunner(__dirname, 'stop-and-start.js').expect({ event: EXPECTED_ANR_EVENT }).start(done);
112121
});
122+
123+
const EXPECTED_ISOLATED_EVENT = {
124+
user: {
125+
id: 5,
126+
},
127+
exception: {
128+
values: [
129+
{
130+
type: 'ApplicationNotResponding',
131+
value: 'Application Not Responding for at least 100 ms',
132+
mechanism: { type: 'ANR' },
133+
stacktrace: {
134+
frames: expect.arrayContaining([
135+
{
136+
colno: expect.any(Number),
137+
lineno: expect.any(Number),
138+
filename: expect.stringMatching(/isolated.mjs$/),
139+
function: 'longWork',
140+
in_app: true,
141+
},
142+
]),
143+
},
144+
},
145+
],
146+
},
147+
};
148+
149+
test('fetches correct isolated scope', done => {
150+
createRunner(__dirname, 'isolated.mjs').expect({ event: EXPECTED_ISOLATED_EVENT }).start(done);
151+
});
113152
});

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

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
// TODO (v8): This import can be removed once we only support Node with global URL
22
import { URL } from 'url';
3-
import { convertIntegrationFnToClass, defineIntegration, getCurrentScope } from '@sentry/core';
3+
import {
4+
convertIntegrationFnToClass,
5+
defineIntegration,
6+
getCurrentScope,
7+
getGlobalScope,
8+
getIsolationScope,
9+
mergeScopeData,
10+
} from '@sentry/core';
411
import type {
512
Client,
613
Contexts,
@@ -9,9 +16,9 @@ import type {
916
Integration,
1017
IntegrationClass,
1118
IntegrationFn,
12-
IntegrationFnResult,
19+
ScopeData,
1320
} from '@sentry/types';
14-
import { dynamicRequire, logger } from '@sentry/utils';
21+
import { GLOBAL_OBJ, dynamicRequire, logger } from '@sentry/utils';
1522
import type { Worker, WorkerOptions } from 'worker_threads';
1623
import type { NodeClient } from '../../client';
1724
import { NODE_VERSION } from '../../nodeVersion';
@@ -31,6 +38,24 @@ function log(message: string, ...args: unknown[]): void {
3138
logger.log(`[ANR] ${message}`, ...args);
3239
}
3340

41+
function globalWithScopeFetchFn(): typeof GLOBAL_OBJ & { __SENTRY_GET_SCOPES__?: () => ScopeData } {
42+
return GLOBAL_OBJ;
43+
}
44+
45+
/** Fetches merged scope data */
46+
function getScopeData(): ScopeData {
47+
const scope = getGlobalScope().getScopeData();
48+
mergeScopeData(scope, getIsolationScope().getScopeData());
49+
mergeScopeData(scope, getCurrentScope().getScopeData());
50+
51+
// We remove attachments because they likely won't serialize well as json
52+
scope.attachments = [];
53+
// We can't serialize event processor functions
54+
scope.eventProcessors = [];
55+
56+
return scope;
57+
}
58+
3459
/**
3560
* We need to use dynamicRequire because worker_threads is not available in node < v12 and webpack error will when
3661
* targeting those versions
@@ -64,9 +89,18 @@ const INTEGRATION_NAME = 'Anr';
6489
type AnrInternal = { startWorker: () => void; stopWorker: () => void };
6590

6691
const _anrIntegration = ((options: Partial<AnrIntegrationOptions> = {}) => {
92+
if (NODE_VERSION.major < 16 || (NODE_VERSION.major === 16 && NODE_VERSION.minor < 17)) {
93+
throw new Error('ANR detection requires Node 16.17.0 or later');
94+
}
95+
6796
let worker: Promise<() => void> | undefined;
6897
let client: NodeClient | undefined;
6998

99+
// Hookup the scope fetch function to the global object so that it can be called from the worker thread via the
100+
// debugger when it pauses
101+
const gbl = globalWithScopeFetchFn();
102+
gbl.__SENTRY_GET_SCOPES__ = getScopeData;
103+
70104
return {
71105
name: INTEGRATION_NAME,
72106
// TODO v8: Remove this
@@ -90,20 +124,16 @@ const _anrIntegration = ((options: Partial<AnrIntegrationOptions> = {}) => {
90124
}
91125
},
92126
setup(initClient: NodeClient) {
93-
if (NODE_VERSION.major < 16 || (NODE_VERSION.major === 16 && NODE_VERSION.minor < 17)) {
94-
throw new Error('ANR detection requires Node 16.17.0 or later');
95-
}
96-
97127
client = initClient;
98128

99129
// setImmediate is used to ensure that all other integrations have had their setup called first.
100130
// This allows us to call into all integrations to fetch the full context
101131
setImmediate(() => this.startWorker());
102132
},
103-
} as IntegrationFnResult & AnrInternal;
133+
} as Integration & AnrInternal;
104134
}) satisfies IntegrationFn;
105135

106-
type AnrReturn = (options?: Partial<AnrIntegrationOptions>) => IntegrationFnResult & AnrInternal;
136+
type AnrReturn = (options?: Partial<AnrIntegrationOptions>) => Integration & AnrInternal;
107137

108138
export const anrIntegration = defineIntegration(_anrIntegration) as AnrReturn;
109139

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

Lines changed: 35 additions & 11 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,
@@ -87,7 +88,23 @@ function prepareStackFrames(stackFrames: StackFrame[] | undefined): StackFrame[]
8788
return strippedFrames;
8889
}
8990

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

101118
const event: Event = {
102119
event_id: uuid4(),
103-
contexts: { ...options.contexts, trace: traceContext },
120+
contexts: options.contexts,
104121
release: options.release,
105122
environment: options.environment,
106123
dist: options.dist,
@@ -120,8 +137,12 @@ async function sendAnrEvent(frames?: StackFrame[], traceContext?: TraceContext):
120137
tags: options.staticTags,
121138
};
122139

140+
if (scope) {
141+
applyScopeToEvent(event, scope);
142+
}
143+
123144
const envelope = createEventEnvelope(event, options.dsn, options.sdkMetadata);
124-
// Log the envelope so to aid in testing
145+
// Log the envelope to aid in testing
125146
log(JSON.stringify(envelope));
126147

127148
await transport.send(envelope);
@@ -172,20 +193,23 @@ if (options.captureStackTrace) {
172193
'Runtime.evaluate',
173194
{
174195
// Grab the trace context from the current scope
175-
expression:
176-
'var __sentry_ctx = __SENTRY__.hub.getScope().getPropagationContext(); __sentry_ctx.traceId + "-" + __sentry_ctx.spanId + "-" + __sentry_ctx.parentSpanId',
196+
expression: 'global.__SENTRY_GET_SCOPES__();',
177197
// Don't re-trigger the debugger if this causes an error
178198
silent: true,
199+
// Serialize the result to json otherwise only primitives are supported
200+
returnByValue: true,
179201
},
180-
(_, param) => {
181-
const traceId = param && param.result ? (param.result.value as string) : '--';
182-
const [trace_id, span_id, parent_span_id] = traceId.split('-') as (string | undefined)[];
202+
(err, param) => {
203+
if (err) {
204+
log(`Error executing script: '${err.message}'`);
205+
}
206+
207+
const scopes = param && param.result ? (param.result.value as ScopeData) : undefined;
183208

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

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

0 commit comments

Comments
 (0)