Skip to content

Commit df08e8f

Browse files
authored
feat(node): Add abnormal session support for ANR (#9268)
1 parent 441f702 commit df08e8f

File tree

9 files changed

+168
-50
lines changed

9 files changed

+168
-50
lines changed

packages/core/src/session.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ export function updateSession(session: Session, context: SessionContext = {}): v
5757

5858
session.timestamp = context.timestamp || timestampInSeconds();
5959

60+
if (context.abnormal_mechanism) {
61+
session.abnormal_mechanism = context.abnormal_mechanism;
62+
}
63+
6064
if (context.ignoreDuration) {
6165
session.ignoreDuration = context.ignoreDuration;
6266
}
@@ -143,6 +147,7 @@ function sessionToJSON(session: Session): SerializedSession {
143147
errors: session.errors,
144148
did: typeof session.did === 'number' || typeof session.did === 'string' ? `${session.did}` : undefined,
145149
duration: session.duration,
150+
abnormal_mechanism: session.abnormal_mechanism,
146151
attrs: {
147152
release: session.release,
148153
environment: session.environment,
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const crypto = require('crypto');
2+
3+
const Sentry = require('@sentry/node');
4+
5+
const { transport } = require('./test-transport.js');
6+
7+
// close both processes after 5 seconds
8+
setTimeout(() => {
9+
process.exit();
10+
}, 5000);
11+
12+
Sentry.init({
13+
dsn: 'https://[email protected]/1337',
14+
release: '1.0',
15+
debug: true,
16+
transport,
17+
});
18+
19+
Sentry.enableAnrDetection({ captureStackTrace: true, anrThreshold: 200 }).then(() => {
20+
function longWork() {
21+
for (let i = 0; i < 100; i++) {
22+
const salt = crypto.randomBytes(128).toString('base64');
23+
// eslint-disable-next-line no-unused-vars
24+
const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512');
25+
}
26+
}
27+
28+
setTimeout(() => {
29+
longWork();
30+
}, 1000);
31+
});

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ const crypto = require('crypto');
22

33
const Sentry = require('@sentry/node');
44

5+
const { transport } = require('./test-transport.js');
6+
57
// close both processes after 5 seconds
68
setTimeout(() => {
79
process.exit();
@@ -11,10 +13,8 @@ Sentry.init({
1113
dsn: 'https://[email protected]/1337',
1214
release: '1.0',
1315
debug: true,
14-
beforeSend: event => {
15-
// eslint-disable-next-line no-console
16-
console.log(JSON.stringify(event));
17-
},
16+
autoSessionTracking: false,
17+
transport,
1818
});
1919

2020
Sentry.enableAnrDetection({ captureStackTrace: true, anrThreshold: 200 }).then(() => {

packages/node-integration-tests/suites/anr/basic.mjs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import * as crypto from 'crypto';
22

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

5+
const { transport } = await import('./test-transport.js');
6+
57
// close both processes after 5 seconds
68
setTimeout(() => {
79
process.exit();
@@ -11,10 +13,8 @@ Sentry.init({
1113
dsn: 'https://[email protected]/1337',
1214
release: '1.0',
1315
debug: true,
14-
beforeSend: event => {
15-
// eslint-disable-next-line no-console
16-
console.log(JSON.stringify(event));
17-
},
16+
autoSessionTracking: false,
17+
transport,
1818
});
1919

2020
await Sentry.enableAnrDetection({ captureStackTrace: true, anrThreshold: 200 });

packages/node-integration-tests/suites/anr/forked.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ const crypto = require('crypto');
22

33
const Sentry = require('@sentry/node');
44

5+
const { transport } = require('./test-transport.js');
6+
57
// close both processes after 5 seconds
68
setTimeout(() => {
79
process.exit();
@@ -11,10 +13,8 @@ Sentry.init({
1113
dsn: 'https://[email protected]/1337',
1214
release: '1.0',
1315
debug: true,
14-
beforeSend: event => {
15-
// eslint-disable-next-line no-console
16-
console.log(JSON.stringify(event));
17-
},
16+
autoSessionTracking: false,
17+
transport,
1818
});
1919

2020
Sentry.enableAnrDetection({ captureStackTrace: true, anrThreshold: 200 }).then(() => {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const { TextEncoder, TextDecoder } = require('util');
2+
3+
const { createTransport } = require('@sentry/core');
4+
const { parseEnvelope } = require('@sentry/utils');
5+
6+
const textEncoder = new TextEncoder();
7+
const textDecoder = new TextDecoder();
8+
9+
// A transport that just logs the envelope payloads to console for checking in tests
10+
exports.transport = () => {
11+
return createTransport({ recordDroppedEvent: () => {}, textEncoder }, async request => {
12+
const env = parseEnvelope(request.body, textEncoder, textDecoder);
13+
// eslint-disable-next-line no-console
14+
console.log(JSON.stringify(env[1][0][1]));
15+
return { statusCode: 200 };
16+
});
17+
};

packages/node-integration-tests/suites/anr/test.ts

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,40 @@
11
import type { Event } from '@sentry/node';
2+
import type { SerializedSession } from '@sentry/types';
23
import { parseSemver } from '@sentry/utils';
34
import * as childProcess from 'child_process';
45
import * as path from 'path';
56

67
const NODE_VERSION = parseSemver(process.versions.node).major || 0;
78

89
/** The output will contain logging so we need to find the line that parses as JSON */
9-
function parseJsonLine<T>(input: string): T {
10-
return (
11-
input
12-
.split('\n')
13-
.map(line => {
14-
try {
15-
return JSON.parse(line) as T;
16-
} catch {
17-
return undefined;
18-
}
19-
})
20-
.filter(a => a) as T[]
21-
)[0];
10+
function parseJsonLines<T extends unknown[]>(input: string, expected: number): T {
11+
const results = input
12+
.split('\n')
13+
.map(line => {
14+
try {
15+
return JSON.parse(line) as T;
16+
} catch {
17+
return undefined;
18+
}
19+
})
20+
.filter(a => a) as T;
21+
22+
expect(results.length).toEqual(expected);
23+
24+
return results;
2225
}
2326

2427
describe('should report ANR when event loop blocked', () => {
2528
test('CJS', done => {
2629
// The stack trace is different when node < 12
2730
const testFramesDetails = NODE_VERSION >= 12;
2831

29-
expect.assertions(testFramesDetails ? 6 : 4);
32+
expect.assertions(testFramesDetails ? 7 : 5);
3033

3134
const testScriptPath = path.resolve(__dirname, 'basic.js');
3235

3336
childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => {
34-
const event = parseJsonLine<Event>(stdout);
37+
const [event] = parseJsonLines<[Event]>(stdout, 1);
3538

3639
expect(event.exception?.values?.[0].mechanism).toEqual({ type: 'ANR' });
3740
expect(event.exception?.values?.[0].type).toEqual('ApplicationNotResponding');
@@ -53,12 +56,12 @@ describe('should report ANR when event loop blocked', () => {
5356
return;
5457
}
5558

56-
expect.assertions(6);
59+
expect.assertions(7);
5760

5861
const testScriptPath = path.resolve(__dirname, 'basic.mjs');
5962

6063
childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => {
61-
const event = parseJsonLine<Event>(stdout);
64+
const [event] = parseJsonLines<[Event]>(stdout, 1);
6265

6366
expect(event.exception?.values?.[0].mechanism).toEqual({ type: 'ANR' });
6467
expect(event.exception?.values?.[0].type).toEqual('ApplicationNotResponding');
@@ -71,16 +74,44 @@ describe('should report ANR when event loop blocked', () => {
7174
});
7275
});
7376

77+
test('With session', done => {
78+
// The stack trace is different when node < 12
79+
const testFramesDetails = NODE_VERSION >= 12;
80+
81+
expect.assertions(testFramesDetails ? 9 : 7);
82+
83+
const testScriptPath = path.resolve(__dirname, 'basic-session.js');
84+
85+
childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => {
86+
const [session, event] = parseJsonLines<[SerializedSession, Event]>(stdout, 2);
87+
88+
expect(event.exception?.values?.[0].mechanism).toEqual({ type: 'ANR' });
89+
expect(event.exception?.values?.[0].type).toEqual('ApplicationNotResponding');
90+
expect(event.exception?.values?.[0].value).toEqual('Application Not Responding for at least 200 ms');
91+
expect(event.exception?.values?.[0].stacktrace?.frames?.length).toBeGreaterThan(4);
92+
93+
if (testFramesDetails) {
94+
expect(event.exception?.values?.[0].stacktrace?.frames?.[2].function).toEqual('?');
95+
expect(event.exception?.values?.[0].stacktrace?.frames?.[3].function).toEqual('longWork');
96+
}
97+
98+
expect(session.status).toEqual('abnormal');
99+
expect(session.abnormal_mechanism).toEqual('anr_foreground');
100+
101+
done();
102+
});
103+
});
104+
74105
test('from forked process', done => {
75106
// The stack trace is different when node < 12
76107
const testFramesDetails = NODE_VERSION >= 12;
77108

78-
expect.assertions(testFramesDetails ? 6 : 4);
109+
expect.assertions(testFramesDetails ? 7 : 5);
79110

80111
const testScriptPath = path.resolve(__dirname, 'forker.js');
81112

82113
childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => {
83-
const event = parseJsonLine<Event>(stdout);
114+
const [event] = parseJsonLines<[Event]>(stdout, 1);
84115

85116
expect(event.exception?.values?.[0].mechanism).toEqual({ type: 'ANR' });
86117
expect(event.exception?.values?.[0].type).toEqual('ApplicationNotResponding');

0 commit comments

Comments
 (0)