UNPKG

@sentry/node

Version:
274 lines (232 loc) 7.98 kB
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