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
3 changes: 3 additions & 0 deletions src/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { rendererProfileFromIpc } from './integrations/renderer-profiling.js';
import { getOsDeviceLogAttributes } from './log.js';
import { mergeEvents } from './merge.js';
import { normalizeProfileChunkEnvelope, normalizeReplayEnvelope, normalizeSpanStreamingEnvelope } from './normalize.js';
import { markRendererEvent } from './renderer-event.js';
import type { ElectronMainOptionsInternal } from './sdk.js';
import { SDK_VERSION } from './version.js';

Expand Down Expand Up @@ -107,6 +108,8 @@ function prepareRendererEvent(
event.sdkProcessingMetadata = { ...event.sdkProcessingMetadata, dynamicSamplingContext };
}

markRendererEvent(event);

return mergeEvents(event, { tags: { 'event.process': process } });
}

Expand Down
59 changes: 59 additions & 0 deletions src/main/renderer-event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { Event, Integration } from '@sentry/core';
import { isAbsolute, relative, resolve } from 'path';

const RENDERER_EVENT_KEY = '__sentry_electron_renderer_event__';
Comment thread
timfish marked this conversation as resolved.

/** Marks the event as sent from a renderer process */
export function markRendererEvent(event: Event): void {
event.sdkProcessingMetadata = { ...event.sdkProcessingMetadata, [RENDERER_EVENT_KEY]: true };
}

/** Was the event sent from a renderer process */
export function isRendererEvent(event: Event): boolean {
return event.sdkProcessingMetadata?.[RENDERER_EVENT_KEY] === true;
}

function isPathInDirectory(path: string | undefined, directory: string): boolean {
if (!path || !isAbsolute(path)) {
return false;
}

const rel = relative(resolve(directory), resolve(path));
return !!rel && !rel.startsWith('..') && !isAbsolute(rel);
}

/**
* Renderer stack frames can only get main process context lines from files
* in the app directory, such as preload scripts.
*/
export function limitContextLinesForRendererEvents(integration: Integration, getAppPath: () => string): Integration {
const processEvent = integration.processEvent?.bind(integration);

if (integration.name !== 'ContextLines' || !processEvent) {
return integration;
}

return {
...integration,
processEvent: async (event, hint, client) => {
if (!isRendererEvent(event)) {
return processEvent(event, hint, client);
}

const appPath = getAppPath();
// ContextLines adds context to the frame objects so the frames in the
// original event get it too
const values = (event.exception?.values || []).map((exception) => ({
...exception,
stacktrace: exception.stacktrace && {
...exception.stacktrace,
frames: exception.stacktrace.frames?.filter((frame) => isPathInDirectory(frame.filename, appPath)),
},
}));

await processEvent({ ...event, exception: { values } }, hint, client);

return event;
},
};
}
4 changes: 3 additions & 1 deletion src/main/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
onUnhandledRejectionIntegration,
} from '@sentry/node';
import type { Session, WebContents } from 'electron';
import { session } from 'electron';
import { app, session } from 'electron';
import { IPCMode } from '../common/ipc.js';
import { getDefaultEnvironment, getDefaultReleaseName, getSdkInfo } from './context.js';
import { additionalContextIntegration } from './integrations/additional-context.js';
Expand All @@ -40,6 +40,7 @@ import { screenshotsIntegration } from './integrations/screenshots.js';
import { sentryMinidumpIntegration } from './integrations/sentry-minidump/index.js';
import { configureIPC } from './ipc.js';
import { getOsDeviceLogAttributes } from './log.js';
import { limitContextLinesForRendererEvents } from './renderer-event.js';
import { defaultStackParser } from './stack-parse.js';
import type { ElectronOfflineTransportOptions } from './transports/electron-offline-net.js';
import { makeElectronOfflineTransport } from './transports/electron-offline-net.js';
Expand Down Expand Up @@ -191,6 +192,7 @@ export function init(userOptions: ElectronMainOptions): void {
}

removeRedundantIntegrations(options);
options.integrations = options.integrations.map((i) => limitContextLinesForRendererEvents(i, () => app.getAppPath()));
configureUtilityProcessIPC();

const scope = getCurrentScope();
Expand Down
68 changes: 68 additions & 0 deletions test/unit/renderer-event.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { Event, Integration, StackFrame } from '@sentry/core';
import { join } from 'path';
import { describe, expect, test, vi } from 'vitest';
import { limitContextLinesForRendererEvents, markRendererEvent } from '../../src/main/renderer-event';

const APP_PATH = join(__dirname, 'app');

function createIntegration(name: string): { integration: Integration; processEvent: ReturnType<typeof vi.fn> } {
const processEvent = vi.fn((event: Event) => {
for (const frame of event.exception?.values?.[0]?.stacktrace?.frames || []) {
frame.context_line = 'context';
}
return event;
});
return { integration: { name, processEvent }, processEvent };
}

function createEvent(filenames: string[]): Event {
return {
exception: {
values: [{ type: 'Error', stacktrace: { frames: filenames.map((filename) => ({ filename, lineno: 1 })) } }],
},
};
}

function frames(event: Event | null | PromiseLike<Event | null>): StackFrame[] {
return (event as Event).exception?.values?.[0]?.stacktrace?.frames || [];
}

describe('limitContextLinesForRendererEvents', () => {
test('Only adds context lines to renderer frames in the app directory', async () => {
const { integration } = createIntegration('ContextLines');
const wrapped = limitContextLinesForRendererEvents(integration, () => APP_PATH);

const inApp = join(APP_PATH, 'src', 'preload.js');
const outsideApp = join(__dirname, 'other', 'file.js');
const event = createEvent([
inApp,
outsideApp,
join(APP_PATH, '..', 'app-other', 'file.js'),
'app:///src/index.html',
]);
markRendererEvent(event);

const result = await wrapped.processEvent?.(event, {}, {} as never);

expect(result).toBe(event);
expect(frames(event).map((frame) => frame.context_line)).toEqual(['context', undefined, undefined, undefined]);
});

test('Adds context lines to all frames of other events', async () => {
const { integration, processEvent } = createIntegration('ContextLines');
const wrapped = limitContextLinesForRendererEvents(integration, () => APP_PATH);

const event = createEvent([join(__dirname, 'other', 'file.js')]);

const result = await wrapped.processEvent?.(event, {}, {} as never);

expect(processEvent).toHaveBeenCalledTimes(1);
expect(frames(result as Event).map((frame) => frame.context_line)).toEqual(['context']);
});

test('Does not change other integrations', () => {
const { integration } = createIntegration('NormalizePaths');

expect(limitContextLinesForRendererEvents(integration, () => APP_PATH)).toBe(integration);
Comment thread
timfish marked this conversation as resolved.
});
});
Loading