Skip to content
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
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,16 @@
"e2e": "xvfb-maybe vitest run --root=./test/e2e --silent=false --disable-console-intercept"
},
"dependencies": {
"@sentry/browser": "8.50.0",
"@sentry/core": "8.50.0",
"@sentry/node": "8.50.0",
"@sentry/browser": "8.51.0",
"@sentry/core": "8.51.0",
"@sentry/node": "8.51.0",
"deepmerge": "4.3.1"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-typescript": "^11.1.6",
"@sentry-internal/eslint-config-sdk": "8.50.0",
"@sentry-internal/typescript": "8.50.0",
"@sentry-internal/eslint-config-sdk": "8.51.0",
"@sentry-internal/typescript": "8.51.0",
"@types/busboy": "^1.5.4",
"@types/form-data": "^2.5.0",
"@types/koa": "^2.0.52",
Expand Down
95 changes: 44 additions & 51 deletions src/main/integrations/sentry-minidump/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Scope,
ScopeData,
SentryError,
Session,
} from '@sentry/core';
import { NodeClient } from '@sentry/node';
import { app, crashReporter } from 'electron';
Expand All @@ -16,7 +17,7 @@ import { getEventDefaults } from '../../context';
import { EXIT_REASONS, getSentryCachePath, usesCrashpad } from '../../electron-normalize';
import { getRendererProperties, trackRendererProperties } from '../../renderers';
import { ElectronMainOptions } from '../../sdk';
import { checkPreviousSession, sessionCrashed } from '../../sessions';
import { previousSessionWasAbnormal, restorePreviousSession, setPreviousSessionAsCurrent } from '../../sessions';
import { BufferedWriteStore } from '../../store';
import { getMinidumpLoader, MinidumpLoader } from './minidump-loader';

Expand Down Expand Up @@ -82,7 +83,7 @@ export const sentryMinidumpIntegration = defineIntegration((options: Options = {

async function sendNativeCrashes(
client: NodeClient,
getEvent: (minidumpProcess: string | undefined) => Event,
getEvent: (minidumpProcess: string | undefined) => Event | Promise<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
Expand All @@ -108,29 +109,11 @@ export const sentryMinidumpIntegration = defineIntegration((options: Options = {
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;
}
const event = await getEvent(minidumpProcess);

if (minidumpsRemaining > 0) {
minidumpsRemaining -= 1;
captureEvent(event as Event, { attachments: [attachment] });
captureEvent(event, { attachments: [attachment] });
}
});

Expand All @@ -145,7 +128,7 @@ export const sentryMinidumpIntegration = defineIntegration((options: Options = {
): Promise<void> {
const { getRendererName } = options;

const found = await sendNativeCrashes(client, (minidumpProcess) => {
await sendNativeCrashes(client, (minidumpProcess) => {
// We only call 'getRendererName' if this was in fact a renderer crash
const crashedProcess =
(minidumpProcess === 'renderer' && getRendererName ? getRendererName(contents) : minidumpProcess) ||
Expand All @@ -170,20 +153,12 @@ export const sentryMinidumpIntegration = defineIntegration((options: Options = {
},
};
});

if (found) {
sessionCrashed();
}
}

async function sendChildProcessCrash(
client: NodeClient,
options: ElectronMainOptions,
details: Omit<Electron.Details, 'exitCode'>,
): Promise<void> {
async function sendChildProcessCrash(client: NodeClient, details: Omit<Electron.Details, 'exitCode'>): Promise<void> {
logger.log(`${details.type} process has ${details.reason}`);

const found = await sendNativeCrashes(client, (minidumpProcess) => ({
await sendNativeCrashes(client, (minidumpProcess) => ({
contexts: {
electron: { details },
},
Expand All @@ -197,10 +172,6 @@ export const sentryMinidumpIntegration = defineIntegration((options: Options = {
event_type: 'native',
},
}));

if (found) {
sessionCrashed();
}
}

return {
Expand Down Expand Up @@ -238,25 +209,47 @@ export const sentryMinidumpIntegration = defineIntegration((options: Options = {
});
app.on('child-process-gone', async (_, details) => {
if (EXIT_REASONS.includes(details.reason)) {
await sendChildProcessCrash(client, options, details);
await sendChildProcessCrash(client, details);
}
});

let sessionToRestore: Session | undefined;

// 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, (minidumpProcess) => ({
level: 'fatal',
platform: 'native',
tags: {
'event.environment': 'native',
'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
checkPreviousSession(minidumpsFound),
)
sendNativeCrashes(client, async (minidumpProcess) => {
const event: Event = {
level: 'fatal',
platform: 'native',
tags: {
'event.environment': 'native',
'event.process': minidumpProcess || (usesCrashpad() ? 'unknown' : 'browser'),
},
};

// This crash was found at startup, we need to apply the scope and context from the previous run
const previousRun = await scopeLastRun;
if (previousRun) {
if (previousRun.scope) {
applyScopeDataToEvent(event, previousRun.scope);
}

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

sessionToRestore = await setPreviousSessionAsCurrent();

return event;
})
.then(async (minidumpsFound) => {
if (!minidumpsFound) {
await previousSessionWasAbnormal();
} else if (sessionToRestore) {
restorePreviousSession(sessionToRestore);
}
})
.catch((error) => logger.error(error));
},
};
Expand Down
67 changes: 64 additions & 3 deletions src/main/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,20 @@ let previousSession: Promise<Partial<Session> | undefined> | undefined;
function getSessionStore(): Store<SessionContext | undefined> {
if (!sessionStore) {
sessionStore = new Store<SessionContext | undefined>(getSentryCachePath(), 'session', undefined);
previousSession = sessionStore.get();
previousSession = sessionStore.get().then((sesh) => (sesh ? makeSession(sesh) : sesh));
}

return sessionStore;
}

/** Copies a session and removes the toJSON function so it can be serialised without conversion */
function makeSessionSafeToSerialize(session: Session): Session {
const copy = { ...session };
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
delete (copy as any).toJSON;
return copy;
}

let persistTimer: ReturnType<typeof setInterval> | undefined;

/** Starts a session */
Expand All @@ -45,7 +53,7 @@ export function startSession(sendOnCreate: boolean): void {
}

getSessionStore()
.set(session)
.set(makeSessionSafeToSerialize(session))
.catch(() => {
// Does not throw
});
Expand All @@ -55,7 +63,7 @@ export function startSession(sendOnCreate: boolean): void {
const currentSession = getCurrentScope().getSession();
// Only bother saving if it hasn't already ended
if (currentSession && currentSession.status === 'ok') {
await getSessionStore().set(currentSession);
await getSessionStore().set(makeSessionSafeToSerialize(currentSession));
}
}, PERSIST_INTERVAL_MS);
}
Expand Down Expand Up @@ -110,6 +118,59 @@ export async function unreportedDuringLastSession(crashDate: Date | undefined):
return crashTime > lastPersist && crashTime < prevSessionEnd;
}

/** Sets the previous session as the current session and returns any existing session */
export async function setPreviousSessionAsCurrent(): Promise<Session | undefined> {
const previous = await previousSession;

const scope = getCurrentScope();
const currentSession = scope.getSession();

if (previous) {
previousSession = undefined;

if (previous.status === 'ok') {
scope.setSession(makeSession(previous));
}
}

return currentSession;
}

/** Restores a session */
export function restorePreviousSession(session: Session): void {
getCurrentScope().setSession(session);
}

/** Report the previous session as abnormal */
export async function previousSessionWasAbnormal(): Promise<void> {
const client = getClient<NodeClient>();

const previous = await previousSession;

if (previous && client) {
// Ignore if the previous session is already ended
if (previous.status !== 'ok') {
previousSession = undefined;
return;
}

logger.log(`Found previous abnormal session`);

const sesh = makeSession(previous);

updateSession(sesh, {
status: 'abnormal',
errors: (sesh.errors || 0) + 1,
release: (previous as unknown as SerializedSession).attrs?.release,
environment: (previous as unknown as SerializedSession).attrs?.environment,
});

await client.sendSession(sesh);

previousSession = undefined;
}
}

/** Checks if the previous session needs sending as crashed or abnormal */
export async function checkPreviousSession(crashed: boolean): Promise<void> {
const client = getClient<NodeClient>();
Expand Down
1 change: 1 addition & 0 deletions src/renderer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export {
featureFlagsIntegration,
launchDarklyIntegration,
openFeatureIntegration,
unleashIntegration,
} from '@sentry/browser';

export type { BrowserOptions, ReportDialogOptions } from '@sentry/browser';
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ interface ElectronRendererOptions extends Omit<BrowserOptions, 'dsn' | 'environm
export function init<O extends ElectronRendererOptions>(
options: ElectronRendererOptions & O = {} as ElectronRendererOptions & O,
// This parameter name ensures that TypeScript error messages contain a hint for fixing SDK version mismatches
originalInit: (if_you_get_a_typescript_error_ensure_sdks_use_version_v8_50_0: O) => void = browserInit,
originalInit: (if_you_get_a_typescript_error_ensure_sdks_use_version_v8_51_0: O) => void = browserInit,
): void {
// Ensure the browser SDK is only init'ed once.
if (window?.__SENTRY__RENDERER_INIT__) {
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/test-apps/native-sentry/unknown/event.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"event.environment": "native",
"event.origin": "electron",
"event.process": "unknown",
"app-run": "second"
"app-run": "first"
}
},
"attachments": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"init": false,
"started": 0,
"timestamp": 0,
"status": "ok",
"status": "exited",
"errors": 0,
"duration": 0,
"attrs": {
Expand Down
17 changes: 0 additions & 17 deletions test/e2e/test-apps/sessions/native-crash-main/session-4.json

This file was deleted.

This file was deleted.

This file was deleted.

Loading
Loading