Skip to content

Commit 9d680ff

Browse files
authored
feat(node): Add scope to ANR events (v7) (#11267)
Backport of #11256 Scope wasn't working for the deprecated export (pre. integration) so I didn't spend any time trying to fix it and just don't test this scope feature for that.
1 parent 42b09c5 commit 9d680ff

File tree

9 files changed

+230
-18
lines changed

9 files changed

+230
-18
lines changed

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

+3
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

+3
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

+3
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

+3
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');
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

+3
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

+90-1
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: [
@@ -56,9 +65,59 @@ conditionalTest({ min: 16 })('should report ANR when event loop blocked', () =>
5665
cleanupChildProcesses();
5766
});
5867

68+
const EXPECTED_LEGACY_ANR_EVENT = {
69+
// Ensure we have context
70+
contexts: {
71+
trace: {
72+
span_id: expect.any(String),
73+
trace_id: expect.any(String),
74+
},
75+
device: {
76+
arch: expect.any(String),
77+
},
78+
app: {
79+
app_start_time: expect.any(String),
80+
},
81+
os: {
82+
name: expect.any(String),
83+
},
84+
culture: {
85+
timezone: expect.any(String),
86+
},
87+
},
88+
// and an exception that is our ANR
89+
exception: {
90+
values: [
91+
{
92+
type: 'ApplicationNotResponding',
93+
value: 'Application Not Responding for at least 100 ms',
94+
mechanism: { type: 'ANR' },
95+
stacktrace: {
96+
frames: expect.arrayContaining([
97+
{
98+
colno: expect.any(Number),
99+
lineno: expect.any(Number),
100+
filename: expect.any(String),
101+
function: '?',
102+
in_app: true,
103+
},
104+
{
105+
colno: expect.any(Number),
106+
lineno: expect.any(Number),
107+
filename: expect.any(String),
108+
function: 'longWork',
109+
in_app: true,
110+
},
111+
]),
112+
},
113+
},
114+
],
115+
},
116+
};
117+
59118
// TODO (v8): Remove this old API and this test
60119
test('Legacy API', done => {
61-
createRunner(__dirname, 'legacy.js').expect({ event: EXPECTED_ANR_EVENT }).start(done);
120+
createRunner(__dirname, 'legacy.js').expect({ event: EXPECTED_LEGACY_ANR_EVENT }).start(done);
62121
});
63122

64123
test('CJS', done => {
@@ -110,4 +169,34 @@ conditionalTest({ min: 16 })('should report ANR when event loop blocked', () =>
110169
test('worker can be stopped and restarted', done => {
111170
createRunner(__dirname, 'stop-and-start.js').expect({ event: EXPECTED_ANR_EVENT }).start(done);
112171
});
172+
173+
const EXPECTED_ISOLATED_EVENT = {
174+
user: {
175+
id: 5,
176+
},
177+
exception: {
178+
values: [
179+
{
180+
type: 'ApplicationNotResponding',
181+
value: 'Application Not Responding for at least 100 ms',
182+
mechanism: { type: 'ANR' },
183+
stacktrace: {
184+
frames: expect.arrayContaining([
185+
{
186+
colno: expect.any(Number),
187+
lineno: expect.any(Number),
188+
filename: expect.stringMatching(/isolated.mjs$/),
189+
function: 'longWork',
190+
in_app: true,
191+
},
192+
]),
193+
},
194+
},
195+
],
196+
},
197+
};
198+
199+
test('fetches correct isolated scope', done => {
200+
createRunner(__dirname, 'isolated.mjs').expect({ event: EXPECTED_ISOLATED_EVENT }).start(done);
201+
});
113202
});

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

+37-6
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,
@@ -10,8 +17,9 @@ import type {
1017
IntegrationClass,
1118
IntegrationFn,
1219
IntegrationFnResult,
20+
ScopeData,
1321
} from '@sentry/types';
14-
import { dynamicRequire, logger } from '@sentry/utils';
22+
import { GLOBAL_OBJ, dynamicRequire, logger } from '@sentry/utils';
1523
import type { Worker, WorkerOptions } from 'worker_threads';
1624
import type { NodeClient } from '../../client';
1725
import { NODE_VERSION } from '../../nodeVersion';
@@ -31,6 +39,24 @@ function log(message: string, ...args: unknown[]): void {
3139
logger.log(`[ANR] ${message}`, ...args);
3240
}
3341

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

6692
const _anrIntegration = ((options: Partial<AnrIntegrationOptions> = {}) => {
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+
6797
let worker: Promise<() => void> | undefined;
6898
let client: NodeClient | undefined;
6999

100+
// Hookup the scope fetch function to the global object so that it can be called from the worker thread via the
101+
// debugger when it pauses
102+
const gbl = globalWithScopeFetchFn();
103+
gbl.__SENTRY_GET_SCOPES__ = getScopeData;
104+
70105
return {
71106
name: INTEGRATION_NAME,
72107
// TODO v8: Remove this
@@ -90,10 +125,6 @@ const _anrIntegration = ((options: Partial<AnrIntegrationOptions> = {}) => {
90125
}
91126
},
92127
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-
97128
client = initClient;
98129

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

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

+35-11
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)