Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Determine Electron process from minidump metadata #1049

Merged
merged 8 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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": "[email protected]",
"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
Loading