Skip to content

Commit

Permalink
feat: Add ANR detection for main process (#753)
Browse files Browse the repository at this point in the history
  • Loading branch information
timfish committed Oct 5, 2023
1 parent 3ba8684 commit 871d57f
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 0 deletions.
36 changes: 36 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export {
trace,
} from '@sentry/core';

import type { enableAnrDetection as enableNodeAnrDetection } from '@sentry/node';

export const Integrations = getIntegrations();

// eslint-disable-next-line @typescript-eslint/no-empty-interface
Expand All @@ -62,6 +64,7 @@ interface ProcessEntryPoint {
init: (options: Partial<ElectronOptions>) => void;
close?: (timeout?: number) => Promise<boolean>;
flush?: (timeout?: number) => Promise<boolean>;
enableAnrDetection?(options: Parameters<typeof enableNodeAnrDetection>[0]): Promise<void>;
}

/** Fetches the SDK entry point for the current process */
Expand Down Expand Up @@ -165,3 +168,36 @@ export async function flush(timeout?: number): Promise<boolean> {

throw new Error('The Electron SDK should be flushed from the main 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/electron';
*
* init({ dsn: "__DSN__" });
*
* // with ESM + Electron v28+
* await enableAnrDetection({ captureStackTrace: true });
* runApp();
*
* // with CJS
* enableAnrDetection({ captureStackTrace: true }).then(() => {
* runApp();
* });
* ```
*/
export async function enableAnrDetection(options: Parameters<typeof enableNodeAnrDetection>[0]): Promise<void> {
const entryPoint = getEntryPoint();

if (entryPoint.enableAnrDetection) {
return entryPoint.enableAnrDetection(options);
}

throw new Error('ANR detection should be started in the main process');
}
59 changes: 59 additions & 0 deletions src/main/anr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { enableAnrDetection as enableNodeAnrDetection } from '@sentry/node';
import { app } from 'electron';

import { ELECTRON_MAJOR_VERSION } from './electron-normalize';

type MainProcessOptions = Parameters<typeof enableNodeAnrDetection>[0];

interface Options {
/**
* Main process ANR options.
*
* Set to false to disable ANR detection in the main process.
*/
mainProcess?: MainProcessOptions | false;
}

function enableAnrMainProcess(options: MainProcessOptions): Promise<void> {
if (ELECTRON_MAJOR_VERSION < 4) {
throw new Error('Main process ANR detection is only supported on Electron v4+');
}

const mainOptions = {
entryScript: app.getAppPath(),
...options,
};

return enableNodeAnrDetection(mainOptions);
}

/**
* **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/electron';
*
* init({ dsn: "__DSN__" });
*
* // with ESM + Electron v28+
* await enableAnrDetection({ mainProcess: { captureStackTrace: true }});
* runApp();
*
* // with CJS
* enableAnrDetection({ mainProcess: { captureStackTrace: true }}).then(() => {
* runApp();
* });
* ```
*/
export async function enableAnrDetection(options: Options = {}): Promise<void> {
if (options.mainProcess !== false) {
return enableAnrMainProcess(options.mainProcess || {});
}

return Promise.resolve();
}
1 change: 1 addition & 0 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,4 @@ export const Integrations = { ...ElectronMainIntegrations, ...NodeIntegrations }
export type { ElectronMainOptions } from './sdk';
export { init, defaultIntegrations } from './sdk';
export { IPCMode } from '../common';
export { enableAnrDetection } from './anr';
5 changes: 5 additions & 0 deletions src/main/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ export function init(userOptions: ElectronMainOptions): void {
const options: ElectronMainOptionsInternal = Object.assign(defaultOptions, userOptions);
const defaults = defaultIntegrations;

if (process.env.SENTRY_ANR_CHILD_PROCESS) {
options.autoSessionTracking = false;
options.tracesSampleRate = 0;
}

// If we don't set a release, @sentry/node will automatically fetch from environment variables
if (options.release === undefined) {
options.release = getDefaultReleaseName();
Expand Down
94 changes: 94 additions & 0 deletions test/e2e/test-apps/anr/anr-main/event.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
{
"method": "envelope",
"sentryKey": "37f8a2ee37c0409d8970bc7559c7c7e4",
"appId": "277345",
"data": {
"sdk": {
"name": "sentry.javascript.electron",
"packages": [
{
"name": "npm:@sentry/electron",
"version": "{{version}}"
}
],
"version": "{{version}}"
},
"contexts": {
"app": {
"app_name": "anr-main",
"app_version": "1.0.0",
"app_start_time": "{{time}}"
},
"browser": {
"name": "Chrome"
},
"chrome": {
"name": "Chrome",
"type": "runtime",
"version": "{{version}}"
},
"device": {
"arch": "{{arch}}",
"family": "Desktop",
"memory_size": 0,
"free_memory": 0,
"processor_count": 0,
"processor_frequency": 0,
"cpu_description": "{{cpu}}",
"screen_resolution": "{{screen}}",
"screen_density": 1,
"language": "{{language}}"
},
"node": {
"name": "Node",
"type": "runtime",
"version": "{{version}}"
},
"os": {
"name": "{{platform}}",
"version": "{{version}}"
},
"runtime": {
"name": "Electron",
"version": "{{version}}"
}
},
"release": "anr-main@1.0.0",
"environment": "development",
"user": {
"ip_address": "{{auto}}"
},
"exception": {
"values": [
{
"type": "ApplicationNotResponding",
"value": "Application Not Responding for at least 1000 ms",
"mechanism": { "type": "ANR" },
"stacktrace": {
"frames": [
{
"colno": 0,
"function": "{{function}}",
"in_app": false,
"lineno": 0,
"module": "pbkdf2"
}
]
}
}
]
},
"level": "error",
"event_id": "{{id}}",
"platform": "node",
"timestamp": 0,
"breadcrumbs": [],
"tags": {
"event.environment": "javascript",
"event.origin": "electron",
"event.process": "browser",
"event_type": "javascript",
"process.name": "ANR"
}
}
}
8 changes: 8 additions & 0 deletions test/e2e/test-apps/anr/anr-main/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "anr-main",
"version": "1.0.0",
"main": "src/main.js",
"dependencies": {
"@sentry/electron": "3.0.0"
}
}
4 changes: 4 additions & 0 deletions test/e2e/test-apps/anr/anr-main/recipe.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
description: ANR Main Event
category: ANR
command: yarn
condition: version.major >= 4
28 changes: 28 additions & 0 deletions test/e2e/test-apps/anr/anr-main/src/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const crypto = require('crypto');

const { app } = require('electron');
const { init, enableAnrDetection } = require('@sentry/electron/main');

init({
dsn: '__DSN__',
debug: true,
autoSessionTracking: false,
onFatalError: () => {},
});

function longWork() {
for (let i = 0; i < 100; i++) {
const salt = crypto.randomBytes(128).toString('base64');
// eslint-disable-next-line no-unused-vars
const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512');
}
}

enableAnrDetection({ mainProcess: { debug: true, anrThreshold: 1000, captureStackTrace: true } }).then(() => {
console.log('main app code');
app.on('ready', () => {
setTimeout(() => {
longWork();
}, 1000);
});
});

0 comments on commit 871d57f

Please sign in to comment.