Skip to content

Commit

Permalink
feat(node): Add scope to ANR events (getsentry#11256)
Browse files Browse the repository at this point in the history
Closes getsentry#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.
  • Loading branch information
timfish authored and cadesalaberry committed Apr 19, 2024
1 parent 9d5b60e commit 3493d11
Show file tree
Hide file tree
Showing 9 changed files with 173 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ Sentry.init({
autoSessionTracking: true,
});

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: [Sentry.anrIntegration({ 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: [Sentry.anrIntegration({ 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: [Sentry.anrIntegration({ 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
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: 'person@home.com',
},
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

0 comments on commit 3493d11

Please sign in to comment.