Skip to content

feat(node): Add scope to ANR events #11256

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ Sentry.init({
autoSessionTracking: true,
});

Sentry.setUser({ email: '[email protected]' });
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: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })],
});

Sentry.setUser({ email: '[email protected]' });
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: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })],
});

Sentry.setUser({ email: '[email protected]' });
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: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })],
});

Sentry.setUser({ email: '[email protected]' });
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://[email protected]/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: '[email protected]' });
Sentry.addBreadcrumb({ message: 'important message!' });

function longWorkIgnored() {
for (let i = 0; i < 20; i++) {
const salt = crypto.randomBytes(128).toString('base64');
Expand Down
39 changes: 39 additions & 0 deletions 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: '[email protected]',
},
breadcrumbs: [
{
timestamp: expect.any(Number),
message: 'important message!',
},
],
// and an exception that is our ANR
exception: {
values: [
Expand Down Expand Up @@ -105,4 +114,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);
});
});
40 changes: 32 additions & 8 deletions packages/node-experimental/src/integrations/anr/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { defineIntegration, getCurrentScope } from '@sentry/core';
import type { Contexts, Event, EventHint, Integration, IntegrationFn } from '@sentry/types';
import { logger } from '@sentry/utils';
import { defineIntegration, mergeScopeData } from '@sentry/core';
import type { Contexts, Event, EventHint, Integration, IntegrationFn, ScopeData } from '@sentry/types';
import { GLOBAL_OBJ, logger } from '@sentry/utils';
import * as inspector from 'inspector';
import { Worker } from 'worker_threads';
import { getCurrentScope, getGlobalScope, getIsolationScope } from '../..';
import { NODE_VERSION } from '../../nodeVersion';
import type { NodeClient } from '../../sdk/client';
import type { AnrIntegrationOptions, WorkerStartData } from './common';
Expand All @@ -15,8 +16,26 @@ 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;
}

/**
* Gets contexts by calling all event processors. This relies on being called after all integrations are setup
* Gets contexts by calling all event processors. This shouldn't be called until all integrations are setup
*/
async function getContexts(client: NodeClient): Promise<Contexts> {
let event: Event | null = { message: 'ANR' };
Expand All @@ -35,9 +54,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,
startWorker: () => {
Expand All @@ -59,10 +87,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
44 changes: 34 additions & 10 deletions packages/node-experimental/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 @@ -86,7 +87,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 @@ -99,7 +116,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 @@ -119,6 +136,10 @@ async function sendAnrEvent(frames?: StackFrame[], traceContext?: TraceContext):
tags: options.staticTags,
};

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

const envelope = createEventEnvelope(event, options.dsn, options.sdkMetadata, options.tunnel);
// Log the envelope to aid in testing
log(JSON.stringify(envelope));
Expand Down Expand Up @@ -171,20 +192,23 @@ if (options.captureStackTrace) {
'Runtime.evaluate',
{
// Grab the trace context from the current scope
expression:
'var __sentry_ctx = __SENTRY__.acs?.getCurrentScope().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