Skip to content

Commit

Permalink
feat: Determine Electron process from minidump metadata (#1049)
Browse files Browse the repository at this point in the history
  • Loading branch information
timfish authored Jan 9, 2025
1 parent 69333d2 commit dabd6da
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 49 deletions.
100 changes: 55 additions & 45 deletions src/main/integrations/sentry-minidump/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { app, crashReporter } from 'electron';

import { addScopeListener, getScopeData } from '../../../common/scope';
import { getEventDefaults } from '../../context';
import { EXIT_REASONS, getSentryCachePath } from '../../electron-normalize';
import { EXIT_REASONS, getSentryCachePath, usesCrashpad } from '../../electron-normalize';
import { getRendererProperties, trackRendererProperties } from '../../renderers';
import { ElectronMainOptions } from '../../sdk';
import { checkPreviousSession, sessionCrashed } from '../../sessions';
Expand Down Expand Up @@ -80,7 +80,10 @@ export const sentryMinidumpIntegration = defineIntegration((options: Options = {
scopeChanged(getScopeData());
}

async function sendNativeCrashes(client: NodeClient, eventIn: Event): Promise<boolean> {
async function sendNativeCrashes(
client: NodeClient,
getEvent: (minidumpProcess: string | undefined) => Event,
): Promise<boolean> {
// Whenever we are called, assume that the crashes we are going to load down
// below have occurred recently. This means, we can use the same event data
// for all minidumps that we load now. There are two conditions:
Expand All @@ -93,26 +96,6 @@ export const sentryMinidumpIntegration = defineIntegration((options: Options = {
// about it. Just use the breadcrumbs and context information we have
// right now and hope that the delay was not too long.

const event = eventIn;

// If this is a native main process crash, we need to apply the scope and context from the previous run
if (event.tags?.['event.process'] === 'browser') {
const previousRun = await scopeLastRun;
if (previousRun) {
if (previousRun.scope) {
applyScopeDataToEvent(event, previousRun.scope);
}

event.release = previousRun.event?.release || event.release;
event.environment = previousRun.event?.environment || event.environment;
event.contexts = previousRun.event?.contexts || event.contexts;
}
}

if (!event) {
return false;
}

if (minidumpsRemaining <= 0) {
logger.log('Not sending minidumps because the limit has been reached');
}
Expand All @@ -121,9 +104,30 @@ export const sentryMinidumpIntegration = defineIntegration((options: Options = {
const deleteAll = client.getOptions().enabled === false || minidumpsRemaining <= 0;

let minidumpFound = false;
await minidumpLoader?.(deleteAll, (attachment) => {

await minidumpLoader?.(deleteAll, async (minidumpProcess, attachment) => {
minidumpFound = true;

const event = getEvent(minidumpProcess);

// If this is a native main process crash, we need to apply the scope and context from the previous run
if (event.tags?.['event.process'] === 'browser') {
const previousRun = await scopeLastRun;
if (previousRun) {
if (previousRun.scope) {
applyScopeDataToEvent(event, previousRun.scope);
}

event.release = previousRun.event?.release || event.release;
event.environment = previousRun.event?.environment || event.environment;
event.contexts = previousRun.event?.contexts || event.contexts;
}
}

if (!event) {
return;
}

if (minidumpsRemaining > 0) {
minidumpsRemaining -= 1;
captureEvent(event as Event, { attachments: [attachment] });
Expand All @@ -140,25 +144,31 @@ export const sentryMinidumpIntegration = defineIntegration((options: Options = {
details: Partial<Electron.RenderProcessGoneDetails>,
): Promise<void> {
const { getRendererName } = options;
const crashedProcess = getRendererName?.(contents) || 'renderer';

logger.log(`'${crashedProcess}' process '${details.reason}'`);
const found = await sendNativeCrashes(client, (minidumpProcess) => {
// We only call 'getRendererName' if this was in fact a renderer crash
const crashedProcess =
(minidumpProcess === 'renderer' && getRendererName ? getRendererName(contents) : minidumpProcess) ||
(usesCrashpad() ? 'unknown' : 'renderer');

const found = await sendNativeCrashes(client, {
contexts: {
electron: {
crashed_url: getRendererProperties(contents.id)?.url || 'unknown',
details,
logger.log(`'${crashedProcess}' process '${details.reason}'`);

return {
contexts: {
electron: {
crashed_url: getRendererProperties(contents.id)?.url || 'unknown',
details,
},
},
},
level: 'fatal',
// The default is javascript
platform: 'native',
tags: {
'event.environment': 'native',
'event.process': crashedProcess,
'exit.reason': details.reason,
},
level: 'fatal',
// The default is javascript
platform: 'native',
tags: {
'event.environment': 'native',
'event.process': crashedProcess,
'exit.reason': details.reason,
},
};
});

if (found) {
Expand All @@ -173,7 +183,7 @@ export const sentryMinidumpIntegration = defineIntegration((options: Options = {
): Promise<void> {
logger.log(`${details.type} process has ${details.reason}`);

const found = await sendNativeCrashes(client, {
const found = await sendNativeCrashes(client, (minidumpProcess) => ({
contexts: {
electron: { details },
},
Expand All @@ -182,11 +192,11 @@ export const sentryMinidumpIntegration = defineIntegration((options: Options = {
platform: 'native',
tags: {
'event.environment': 'native',
'event.process': details.type,
'event.process': minidumpProcess || details.type,
'exit.reason': details.reason,
event_type: 'native',
},
});
}));

if (found) {
sessionCrashed();
Expand Down Expand Up @@ -234,14 +244,14 @@ export const sentryMinidumpIntegration = defineIntegration((options: Options = {

// Start to submit recent minidump crashes. This will load breadcrumbs and
// context information that was cached on disk in the previous app run, prior to the crash.
sendNativeCrashes(client, {
sendNativeCrashes(client, (minidumpProcess) => ({
level: 'fatal',
platform: 'native',
tags: {
'event.environment': 'native',
'event.process': 'browser',
'event.process': minidumpProcess || (usesCrashpad() ? 'unknown' : 'browser'),
},
})
}))
.then((minidumpsFound) =>
// Check for previous uncompleted session. If a previous session exists
// and no minidumps were found, its likely an abnormal exit
Expand Down
58 changes: 56 additions & 2 deletions src/main/integrations/sentry-minidump/minidump-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ function delay(ms: number): Promise<void> {
* @param deleteAll Whether to just delete all minidumps
* @param callback A callback to call with the attachment ready to send
*/
export type MinidumpLoader = (deleteAll: boolean, callback: (attachment: Attachment) => void) => Promise<void>;
export type MinidumpLoader = (
deleteAll: boolean,
callback: (processType: string | undefined, attachment: Attachment) => Promise<void>,
) => Promise<void>;

/**
* Creates a minidump loader
Expand Down Expand Up @@ -75,9 +78,11 @@ export function createMinidumpLoader(
break;
}

const minidumpProcess = getMinidumpProcessType(data);

logger.log('Sending minidump');

callback({
await callback(minidumpProcess, {
attachmentType: 'event.minidump',
filename: basename(path),
data,
Expand Down Expand Up @@ -214,3 +219,52 @@ function breakpadMinidumpLoader(): MinidumpLoader {
export function getMinidumpLoader(): MinidumpLoader {
return usesCrashpad() ? crashpadMinidumpLoader() : breakpadMinidumpLoader();
}

/**
* Crashpad includes it's own custom stream in the minidump file that can include metadata. Electron uses this to
* include details about the app and process that caused the crash.
*
* Rather than parse the minidump by reading the header and parsing through all the streams, we can just look for the
* 'process_type' key and then pick the string that comes after that.
*/
function getMinidumpProcessType(buffer: Buffer): string | undefined {
const index = buffer.indexOf('process_type');

if (index < 0) {
return;
}

// start after 'process_type'
let start = index + 12;

// Move start to the first ascii character
while ((buffer[start] || 0) < 32) {
start++;

// If we can't find the start in the first 20 bytes, we assume it's not there
if (start - index > 20) {
return;
}
}

let end = start;

// Move the end of the ascii
while ((buffer[end] || -1) >= 32) {
end++;

// If we can't find the end in the first 20 bytes, we assume it's not there
if (end - start > 20) {
return;
}
}

const processType = buffer.subarray(start, end).toString().replace('-process', '');

// For backwards compatibility
if (processType === 'gpu') {
return 'GPU';
}

return processType;
}
84 changes: 84 additions & 0 deletions test/e2e/test-apps/native-sentry/unknown/event.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
{
"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": "native-sentry-unknown",
"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
},
"culture": {
"locale": "{{locale}}",
"timezone": "{{timezone}}"
},
"node": {
"name": "Node",
"type": "runtime",
"version": "{{version}}"
},
"os": {
"name": "{{platform}}",
"version": "{{version}}"
},
"runtime": {
"name": "Electron",
"version": "{{version}}"
}
},
"release": "native-sentry-unknown@1.0.0",
"environment": "development",
"event_id": "{{id}}",
"timestamp": 0,
"breadcrumbs": [
{
"category": "console",
"level": "log",
"message": "main process breadcrumb from second run"
}
],
"tags": {
"event.environment": "native",
"event.origin": "electron",
"event.process": "unknown",
"app-run": "second"
}
},
"attachments": [
{
"length": 12004,
"filename": "0dc9e285-df8d-47b7-8147-85308b54065a.dmp",
"attachment_type": "event.minidump"
}
]
}
8 changes: 8 additions & 0 deletions test/e2e/test-apps/native-sentry/unknown/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "native-sentry-unknown",
"version": "1.0.0",
"main": "src/main.js",
"dependencies": {
"@sentry/electron": "5.6.0"
}
}
5 changes: 5 additions & 0 deletions test/e2e/test-apps/native-sentry/unknown/recipe.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
description: Native Unknown Crash
category: Native (Sentry Uploader)
command: yarn
condition: usesCrashpad
runTwice: true
15 changes: 15 additions & 0 deletions test/e2e/test-apps/native-sentry/unknown/src/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
</head>
<body>
<script>
const { init } = require('@sentry/electron/renderer');

init({
debug: true,
});
</script>
</body>
</html>
Loading

0 comments on commit dabd6da

Please sign in to comment.