@sentry/node
Version:
Official Sentry SDK for Node.js
274 lines (232 loc) • 7.98 kB
JavaScript
var {
_optionalChain
} = require('@sentry/utils');
Object.defineProperty(exports, '__esModule', { value: true });
const child_process = require('child_process');
const core = require('@sentry/core');
const utils = require('@sentry/utils');
const _debugger = require('./debugger.js');
const DEFAULT_INTERVAL = 50;
const DEFAULT_HANG_THRESHOLD = 5000;
function createAnrEvent(blockedMs, frames) {
return {
level: 'error',
exception: {
values: [
{
type: 'ApplicationNotResponding',
value: `Application Not Responding for at least ${blockedMs} ms`,
stacktrace: { frames },
mechanism: {
// This ensures the UI doesn't say 'Crashed in' for the stack trace
type: 'ANR',
},
},
],
},
};
}
/**
* Starts the node debugger and returns the inspector url.
*
* When inspector.url() returns undefined, it means the port is already in use so we try the next port.
*/
function startInspector(startPort = 9229) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const inspector = require('inspector');
let inspectorUrl = undefined;
let port = startPort;
while (inspectorUrl === undefined && port < startPort + 100) {
inspector.open(port);
inspectorUrl = inspector.url();
port++;
}
return inspectorUrl;
}
function startChildProcess(options) {
function log(message, ...args) {
utils.logger.log(`[ANR] ${message}`, ...args);
}
const hub = core.getCurrentHub();
try {
const env = { ...process.env };
env.SENTRY_ANR_CHILD_PROCESS = 'true';
if (options.captureStackTrace) {
env.SENTRY_INSPECT_URL = startInspector();
}
log(`Spawning child process with execPath:'${process.execPath}' and entryScript:'${options.entryScript}'`);
const child = child_process.spawn(process.execPath, [options.entryScript], {
env,
stdio: utils.logger.isEnabled() ? ['inherit', 'inherit', 'inherit', 'ipc'] : ['ignore', 'ignore', 'ignore', 'ipc'],
});
// The child process should not keep the main process alive
child.unref();
const timer = setInterval(() => {
try {
const currentSession = _optionalChain([hub, 'access', _2 => _2.getScope, 'call', _3 => _3(), 'optionalAccess', _4 => _4.getSession, 'call', _5 => _5()]);
// We need to copy the session object and remove the toJSON method so it can be sent to the child process
// serialized without making it a SerializedSession
const session = currentSession ? { ...currentSession, toJSON: undefined } : undefined;
// message the child process to tell it the main event loop is still running
child.send({ session });
} catch (_) {
//
}
}, options.pollInterval);
child.on('message', (msg) => {
if (msg === 'session-ended') {
log('ANR event sent from child process. Clearing session in this process.');
_optionalChain([hub, 'access', _6 => _6.getScope, 'call', _7 => _7(), 'optionalAccess', _8 => _8.setSession, 'call', _9 => _9(undefined)]);
}
});
const end = (type) => {
return (...args) => {
clearInterval(timer);
log(`Child process ${type}`, ...args);
};
};
child.on('error', end('error'));
child.on('disconnect', end('disconnect'));
child.on('exit', end('exit'));
} catch (e) {
log('Failed to start child process', e);
}
}
function createHrTimer() {
let lastPoll = process.hrtime();
return {
getTimeMs: () => {
const [seconds, nanoSeconds] = process.hrtime(lastPoll);
return Math.floor(seconds * 1e3 + nanoSeconds / 1e6);
},
reset: () => {
lastPoll = process.hrtime();
},
};
}
function handleChildProcess(options) {
process.title = 'sentry-anr';
function log(message) {
utils.logger.log(`[ANR child process] ${message}`);
}
log('Started');
let session;
function sendAnrEvent(frames) {
if (session) {
log('Sending abnormal session');
core.updateSession(session, { status: 'abnormal', abnormal_mechanism: 'anr_foreground' });
_optionalChain([core.getClient, 'call', _10 => _10(), 'optionalAccess', _11 => _11.sendSession, 'call', _12 => _12(session)]);
try {
// Notify the main process that the session has ended so the session can be cleared from the scope
_optionalChain([process, 'access', _13 => _13.send, 'optionalCall', _14 => _14('session-ended')]);
} catch (_) {
// ignore
}
}
core.captureEvent(createAnrEvent(options.anrThreshold, frames));
void core.flush(3000).then(() => {
// We only capture one event to avoid spamming users with errors
process.exit();
});
}
core.addEventProcessor(event => {
// Strip sdkProcessingMetadata from all child process events to remove trace info
delete event.sdkProcessingMetadata;
event.tags = {
...event.tags,
'process.name': 'ANR',
};
return event;
});
let debuggerPause;
// if attachStackTrace is enabled, we'll have a debugger url to connect to
if (process.env.SENTRY_INSPECT_URL) {
log('Connecting to debugger');
debuggerPause = _debugger.captureStackTrace(process.env.SENTRY_INSPECT_URL, frames => {
log('Capturing event with stack frames');
sendAnrEvent(frames);
});
}
async function watchdogTimeout() {
log('Watchdog timeout');
try {
const pauseAndCapture = await debuggerPause;
if (pauseAndCapture) {
log('Pausing debugger to capture stack trace');
pauseAndCapture();
return;
}
} catch (_) {
// ignore
}
log('Capturing event');
sendAnrEvent();
}
const { poll } = utils.watchdogTimer(createHrTimer, options.pollInterval, options.anrThreshold, watchdogTimeout);
process.on('message', (msg) => {
if (msg.session) {
session = core.makeSession(msg.session);
}
poll();
});
process.on('disconnect', () => {
// Parent process has exited.
process.exit();
});
}
/**
* Returns true if the current process is an ANR child process.
*/
function isAnrChildProcess() {
return !!process.send && !!process.env.SENTRY_ANR_CHILD_PROCESS;
}
/**
* **Note** This feature is still in beta so there may be breaking changes in future releases.
*
* Starts a child process that detects Application Not Responding (ANR) errors.
*
* It's important to await on the returned promise before your app code to ensure this code does not run in the ANR
* child process.
*
* ```js
* import { init, enableAnrDetection } from '@sentry/node';
*
* init({ dsn: "__DSN__" });
*
* // with ESM + Node 14+
* await enableAnrDetection({ captureStackTrace: true });
* runApp();
*
* // with CJS or Node 10+
* enableAnrDetection({ captureStackTrace: true }).then(() => {
* runApp();
* });
* ```
*/
function enableAnrDetection(options) {
// When pm2 runs the script in cluster mode, process.argv[1] is the pm2 script and process.env.pm_exec_path is the
// path to the entry script
const entryScript = options.entryScript || process.env.pm_exec_path || process.argv[1];
const anrOptions = {
entryScript,
pollInterval: options.pollInterval || DEFAULT_INTERVAL,
anrThreshold: options.anrThreshold || DEFAULT_HANG_THRESHOLD,
captureStackTrace: !!options.captureStackTrace,
// eslint-disable-next-line deprecation/deprecation
debug: !!options.debug,
};
if (isAnrChildProcess()) {
handleChildProcess(anrOptions);
// In the child process, the promise never resolves which stops the app code from running
return new Promise(() => {
// Never resolve
});
} else {
startChildProcess(anrOptions);
// In the main process, the promise resolves immediately
return Promise.resolve();
}
}
exports.enableAnrDetection = enableAnrDetection;
exports.isAnrChildProcess = isAnrChildProcess;
//# sourceMappingURL=index.js.map