Skip to content

Commit

Permalink
feat(node): Add scope to ANR events (v7) (#11267)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
timfish committed Mar 28, 2024
1 parent 42b09c5 commit 9d680ff
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ Sentry.init({
integrations: [new Sentry.Integrations.Anr({ captureStackTrace: true, anrThreshold: 100 })],
});

Sentry.setUser({ email: 'person@home.com' });
Sentry.addBreadcrumb({ message: 'important message!' });

function longWork() {
for (let i = 0; i < 20; i++) {
const salt = crypto.randomBytes(128).toString('base64');
Expand Down
3 changes: 3 additions & 0 deletions dev-packages/node-integration-tests/suites/anr/basic.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ Sentry.init({
integrations: [new Sentry.Integrations.Anr({ captureStackTrace: true, anrThreshold: 100 })],
});

Sentry.setUser({ email: 'person@home.com' });
Sentry.addBreadcrumb({ message: 'important message!' });

function longWork() {
for (let i = 0; i < 20; i++) {
const salt = crypto.randomBytes(128).toString('base64');
Expand Down
3 changes: 3 additions & 0 deletions dev-packages/node-integration-tests/suites/anr/basic.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ Sentry.init({
integrations: [new Sentry.Integrations.Anr({ captureStackTrace: true, anrThreshold: 100 })],
});

Sentry.setUser({ email: 'person@home.com' });
Sentry.addBreadcrumb({ message: 'important message!' });

function longWork() {
for (let i = 0; i < 20; i++) {
const salt = crypto.randomBytes(128).toString('base64');
Expand Down
3 changes: 3 additions & 0 deletions dev-packages/node-integration-tests/suites/anr/forked.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ Sentry.init({
integrations: [new Sentry.Integrations.Anr({ captureStackTrace: true, anrThreshold: 100 })],
});

Sentry.setUser({ email: 'person@home.com' });
Sentry.addBreadcrumb({ message: 'important message!' });

function longWork() {
for (let i = 0; i < 20; i++) {
const salt = crypto.randomBytes(128).toString('base64');
Expand Down
53 changes: 53 additions & 0 deletions dev-packages/node-integration-tests/suites/anr/isolated.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as assert from 'assert';
import * as crypto from 'crypto';

import * as Sentry from '@sentry/node';

setTimeout(() => {
process.exit();
}, 10000);

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
debug: true,
autoSessionTracking: false,
integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })],
});

async function longWork() {
await new Promise(resolve => setTimeout(resolve, 1000));

for (let i = 0; i < 20; i++) {
const salt = crypto.randomBytes(128).toString('base64');
const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512');
assert.ok(hash);
}
}

function neverResolve() {
return new Promise(() => {
//
});
}

const fns = [
neverResolve,
neverResolve,
neverResolve,
neverResolve,
neverResolve,
longWork, // [5]
neverResolve,
neverResolve,
neverResolve,
neverResolve,
];

for (let id = 0; id < 10; id++) {
Sentry.withIsolationScope(async () => {
Sentry.setUser({ id });

await fns[id]();
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ Sentry.init({
integrations: [anr],
});

Sentry.setUser({ email: 'person@home.com' });
Sentry.addBreadcrumb({ message: 'important message!' });

function longWorkIgnored() {
for (let i = 0; i < 20; i++) {
const salt = crypto.randomBytes(128).toString('base64');
Expand Down
91 changes: 90 additions & 1 deletion dev-packages/node-integration-tests/suites/anr/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ const EXPECTED_ANR_EVENT = {
timezone: expect.any(String),
},
},
user: {
email: 'person@home.com',
},
breadcrumbs: [
{
timestamp: expect.any(Number),
message: 'important message!',
},
],
// and an exception that is our ANR
exception: {
values: [
Expand Down Expand Up @@ -56,9 +65,59 @@ conditionalTest({ min: 16 })('should report ANR when event loop blocked', () =>
cleanupChildProcesses();
});

const EXPECTED_LEGACY_ANR_EVENT = {
// Ensure we have context
contexts: {
trace: {
span_id: expect.any(String),
trace_id: expect.any(String),
},
device: {
arch: expect.any(String),
},
app: {
app_start_time: expect.any(String),
},
os: {
name: expect.any(String),
},
culture: {
timezone: expect.any(String),
},
},
// and an exception that is our ANR
exception: {
values: [
{
type: 'ApplicationNotResponding',
value: 'Application Not Responding for at least 100 ms',
mechanism: { type: 'ANR' },
stacktrace: {
frames: expect.arrayContaining([
{
colno: expect.any(Number),
lineno: expect.any(Number),
filename: expect.any(String),
function: '?',
in_app: true,
},
{
colno: expect.any(Number),
lineno: expect.any(Number),
filename: expect.any(String),
function: 'longWork',
in_app: true,
},
]),
},
},
],
},
};

// TODO (v8): Remove this old API and this test
test('Legacy API', done => {
createRunner(__dirname, 'legacy.js').expect({ event: EXPECTED_ANR_EVENT }).start(done);
createRunner(__dirname, 'legacy.js').expect({ event: EXPECTED_LEGACY_ANR_EVENT }).start(done);
});

test('CJS', done => {
Expand Down Expand Up @@ -110,4 +169,34 @@ conditionalTest({ min: 16 })('should report ANR when event loop blocked', () =>
test('worker can be stopped and restarted', done => {
createRunner(__dirname, 'stop-and-start.js').expect({ event: EXPECTED_ANR_EVENT }).start(done);
});

const EXPECTED_ISOLATED_EVENT = {
user: {
id: 5,
},
exception: {
values: [
{
type: 'ApplicationNotResponding',
value: 'Application Not Responding for at least 100 ms',
mechanism: { type: 'ANR' },
stacktrace: {
frames: expect.arrayContaining([
{
colno: expect.any(Number),
lineno: expect.any(Number),
filename: expect.stringMatching(/isolated.mjs$/),
function: 'longWork',
in_app: true,
},
]),
},
},
],
},
};

test('fetches correct isolated scope', done => {
createRunner(__dirname, 'isolated.mjs').expect({ event: EXPECTED_ISOLATED_EVENT }).start(done);
});
});
43 changes: 37 additions & 6 deletions packages/node/src/integrations/anr/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
// TODO (v8): This import can be removed once we only support Node with global URL
import { URL } from 'url';
import { convertIntegrationFnToClass, defineIntegration, getCurrentScope } from '@sentry/core';
import {
convertIntegrationFnToClass,
defineIntegration,
getCurrentScope,
getGlobalScope,
getIsolationScope,
mergeScopeData,
} from '@sentry/core';
import type {
Client,
Contexts,
Expand All @@ -10,8 +17,9 @@ import type {
IntegrationClass,
IntegrationFn,
IntegrationFnResult,
ScopeData,
} from '@sentry/types';
import { dynamicRequire, logger } from '@sentry/utils';
import { GLOBAL_OBJ, dynamicRequire, logger } from '@sentry/utils';
import type { Worker, WorkerOptions } from 'worker_threads';
import type { NodeClient } from '../../client';
import { NODE_VERSION } from '../../nodeVersion';
Expand All @@ -31,6 +39,24 @@ function log(message: string, ...args: unknown[]): void {
logger.log(`[ANR] ${message}`, ...args);
}

function globalWithScopeFetchFn(): typeof GLOBAL_OBJ & { __SENTRY_GET_SCOPES__?: () => ScopeData } {
return GLOBAL_OBJ;
}

/** Fetches merged scope data */
function getScopeData(): ScopeData {
const scope = getGlobalScope().getScopeData();
mergeScopeData(scope, getIsolationScope().getScopeData());
mergeScopeData(scope, getCurrentScope().getScopeData());

// We remove attachments because they likely won't serialize well as json
scope.attachments = [];
// We can't serialize event processor functions
scope.eventProcessors = [];

return scope;
}

/**
* We need to use dynamicRequire because worker_threads is not available in node < v12 and webpack error will when
* targeting those versions
Expand Down Expand Up @@ -64,9 +90,18 @@ const INTEGRATION_NAME = 'Anr';
type AnrInternal = { startWorker: () => void; stopWorker: () => void };

const _anrIntegration = ((options: Partial<AnrIntegrationOptions> = {}) => {
if (NODE_VERSION.major < 16 || (NODE_VERSION.major === 16 && NODE_VERSION.minor < 17)) {
throw new Error('ANR detection requires Node 16.17.0 or later');
}

let worker: Promise<() => void> | undefined;
let client: NodeClient | undefined;

// Hookup the scope fetch function to the global object so that it can be called from the worker thread via the
// debugger when it pauses
const gbl = globalWithScopeFetchFn();
gbl.__SENTRY_GET_SCOPES__ = getScopeData;

return {
name: INTEGRATION_NAME,
// TODO v8: Remove this
Expand All @@ -90,10 +125,6 @@ const _anrIntegration = ((options: Partial<AnrIntegrationOptions> = {}) => {
}
},
setup(initClient: NodeClient) {
if (NODE_VERSION.major < 16 || (NODE_VERSION.major === 16 && NODE_VERSION.minor < 17)) {
throw new Error('ANR detection requires Node 16.17.0 or later');
}

client = initClient;

// setImmediate is used to ensure that all other integrations have had their setup called first.
Expand Down
46 changes: 35 additions & 11 deletions packages/node/src/integrations/anr/worker.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import {
applyScopeDataToEvent,
createEventEnvelope,
createSessionEnvelope,
getEnvelopeEndpointWithUrlEncodedAuth,
makeSession,
updateSession,
} from '@sentry/core';
import type { Event, Session, StackFrame, TraceContext } from '@sentry/types';
import type { Event, ScopeData, Session, StackFrame } from '@sentry/types';
import {
callFrameToStackFrame,
normalizeUrlToBase,
Expand Down Expand Up @@ -87,7 +88,23 @@ function prepareStackFrames(stackFrames: StackFrame[] | undefined): StackFrame[]
return strippedFrames;
}

async function sendAnrEvent(frames?: StackFrame[], traceContext?: TraceContext): Promise<void> {
function applyScopeToEvent(event: Event, scope: ScopeData): void {
applyScopeDataToEvent(event, scope);

if (!event.contexts?.trace) {
const { traceId, spanId, parentSpanId } = scope.propagationContext;
event.contexts = {
trace: {
trace_id: traceId,
span_id: spanId,
parent_span_id: parentSpanId,
},
...event.contexts,
};
}
}

async function sendAnrEvent(frames?: StackFrame[], scope?: ScopeData): Promise<void> {
if (hasSentAnrEvent) {
return;
}
Expand All @@ -100,7 +117,7 @@ async function sendAnrEvent(frames?: StackFrame[], traceContext?: TraceContext):

const event: Event = {
event_id: uuid4(),
contexts: { ...options.contexts, trace: traceContext },
contexts: options.contexts,
release: options.release,
environment: options.environment,
dist: options.dist,
Expand All @@ -120,8 +137,12 @@ async function sendAnrEvent(frames?: StackFrame[], traceContext?: TraceContext):
tags: options.staticTags,
};

if (scope) {
applyScopeToEvent(event, scope);
}

const envelope = createEventEnvelope(event, options.dsn, options.sdkMetadata);
// Log the envelope so to aid in testing
// Log the envelope to aid in testing
log(JSON.stringify(envelope));

await transport.send(envelope);
Expand Down Expand Up @@ -172,20 +193,23 @@ if (options.captureStackTrace) {
'Runtime.evaluate',
{
// Grab the trace context from the current scope
expression:
'var __sentry_ctx = __SENTRY__.hub.getScope().getPropagationContext(); __sentry_ctx.traceId + "-" + __sentry_ctx.spanId + "-" + __sentry_ctx.parentSpanId',
expression: 'global.__SENTRY_GET_SCOPES__();',
// Don't re-trigger the debugger if this causes an error
silent: true,
// Serialize the result to json otherwise only primitives are supported
returnByValue: true,
},
(_, param) => {
const traceId = param && param.result ? (param.result.value as string) : '--';
const [trace_id, span_id, parent_span_id] = traceId.split('-') as (string | undefined)[];
(err, param) => {
if (err) {
log(`Error executing script: '${err.message}'`);
}

const scopes = param && param.result ? (param.result.value as ScopeData) : undefined;

session.post('Debugger.resume');
session.post('Debugger.disable');

const context = trace_id?.length && span_id?.length ? { trace_id, span_id, parent_span_id } : undefined;
sendAnrEvent(stackFrames, context).then(null, () => {
sendAnrEvent(stackFrames, scopes).then(null, () => {
log('Sending ANR event failed.');
});
},
Expand Down

0 comments on commit 9d680ff

Please sign in to comment.