From 62c02376d815e4a07efde7f4f7d47db931960279 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 15 Oct 2026 10:03:46 +0000 Subject: [PATCH 1/2] fix: Do not add main process context lines to renderer events Renderer stack frames hold renderer URLs, not paths in the main process filesystem. The main process ContextLines integration now skips events that come from renderers. --- src/main/ipc.ts | 3 +++ src/main/renderer-event.ts | 30 ++++++++++++++++++++++++++ src/main/sdk.ts | 2 ++ test/unit/renderer-event.test.ts | 37 ++++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+) create mode 100644 src/main/renderer-event.ts create mode 100644 test/unit/renderer-event.test.ts diff --git a/src/main/ipc.ts b/src/main/ipc.ts index e0e20662..669d1733 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -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'; @@ -107,6 +108,8 @@ function prepareRendererEvent( event.sdkProcessingMetadata = { ...event.sdkProcessingMetadata, dynamicSamplingContext }; } + markRendererEvent(event); + return mergeEvents(event, { tags: { 'event.process': process } }); } diff --git a/src/main/renderer-event.ts b/src/main/renderer-event.ts new file mode 100644 index 00000000..fa6a5713 --- /dev/null +++ b/src/main/renderer-event.ts @@ -0,0 +1,30 @@ +import type { Event, Integration } from '@sentry/core'; + +const RENDERER_EVENT_KEY = '__sentry_electron_renderer_event__'; + +/** 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; +} + +/** + * Renderer stack frames are URLs from the renderer, not paths in the main + * process filesystem, so main process context lines do not apply to them. + */ +export function skipContextLinesForRendererEvents(integration: Integration): Integration { + const processEvent = integration.processEvent?.bind(integration); + + if (integration.name !== 'ContextLines' || !processEvent) { + return integration; + } + + return { + ...integration, + processEvent: (event, hint, client) => (isRendererEvent(event) ? event : processEvent(event, hint, client)), + }; +} diff --git a/src/main/sdk.ts b/src/main/sdk.ts index 59c3a7b1..fd84d6a4 100644 --- a/src/main/sdk.ts +++ b/src/main/sdk.ts @@ -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 { skipContextLinesForRendererEvents } 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'; @@ -191,6 +192,7 @@ export function init(userOptions: ElectronMainOptions): void { } removeRedundantIntegrations(options); + options.integrations = options.integrations.map(skipContextLinesForRendererEvents); configureUtilityProcessIPC(); const scope = getCurrentScope(); diff --git a/test/unit/renderer-event.test.ts b/test/unit/renderer-event.test.ts new file mode 100644 index 00000000..42925070 --- /dev/null +++ b/test/unit/renderer-event.test.ts @@ -0,0 +1,37 @@ +import type { Event, Integration } from '@sentry/core'; +import { describe, expect, test, vi } from 'vitest'; +import { markRendererEvent, skipContextLinesForRendererEvents } from '../../src/main/renderer-event'; + +function createIntegration(name: string): { integration: Integration; processEvent: ReturnType } { + const processEvent = vi.fn((event: Event) => ({ ...event, extra: { processed: true } })); + return { integration: { name, processEvent }, processEvent }; +} + +describe('skipContextLinesForRendererEvents', () => { + test('Skips ContextLines for renderer events', () => { + const { integration, processEvent } = createIntegration('ContextLines'); + const wrapped = skipContextLinesForRendererEvents(integration); + + const event: Event = { message: 'renderer' }; + markRendererEvent(event); + + expect(wrapped.processEvent?.(event, {}, {} as never)).toBe(event); + expect(processEvent).not.toHaveBeenCalled(); + }); + + test('Runs ContextLines for other events', () => { + const { integration, processEvent } = createIntegration('ContextLines'); + const wrapped = skipContextLinesForRendererEvents(integration); + + const event: Event = { message: 'main' }; + + expect(wrapped.processEvent?.(event, {}, {} as never)).toEqual({ message: 'main', extra: { processed: true } }); + expect(processEvent).toHaveBeenCalledTimes(1); + }); + + test('Does not change other integrations', () => { + const { integration } = createIntegration('NormalizePaths'); + + expect(skipContextLinesForRendererEvents(integration)).toBe(integration); + }); +}); From 8db27da7fb268e0de1e03766e640eb842fc0b0ca Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 15 Oct 2026 11:18:12 +0000 Subject: [PATCH 2/2] fix: Keep context lines for renderer frames in the app directory Preload script frames are files in the app directory and need main process context lines. Renderer frames outside the app directory still get none. --- src/main/renderer-event.ts | 37 ++++++++++++++++--- src/main/sdk.ts | 6 ++-- test/unit/renderer-event.test.ts | 61 ++++++++++++++++++++++++-------- 3 files changed, 82 insertions(+), 22 deletions(-) diff --git a/src/main/renderer-event.ts b/src/main/renderer-event.ts index fa6a5713..216497b1 100644 --- a/src/main/renderer-event.ts +++ b/src/main/renderer-event.ts @@ -1,4 +1,5 @@ import type { Event, Integration } from '@sentry/core'; +import { isAbsolute, relative, resolve } from 'path'; const RENDERER_EVENT_KEY = '__sentry_electron_renderer_event__'; @@ -12,11 +13,20 @@ 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 are URLs from the renderer, not paths in the main - * process filesystem, so main process context lines do not apply to them. + * Renderer stack frames can only get main process context lines from files + * in the app directory, such as preload scripts. */ -export function skipContextLinesForRendererEvents(integration: Integration): Integration { +export function limitContextLinesForRendererEvents(integration: Integration, getAppPath: () => string): Integration { const processEvent = integration.processEvent?.bind(integration); if (integration.name !== 'ContextLines' || !processEvent) { @@ -25,6 +35,25 @@ export function skipContextLinesForRendererEvents(integration: Integration): Int return { ...integration, - processEvent: (event, hint, client) => (isRendererEvent(event) ? event : processEvent(event, hint, client)), + 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; + }, }; } diff --git a/src/main/sdk.ts b/src/main/sdk.ts index fd84d6a4..0c1dbd3e 100644 --- a/src/main/sdk.ts +++ b/src/main/sdk.ts @@ -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'; @@ -40,7 +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 { skipContextLinesForRendererEvents } from './renderer-event.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'; @@ -192,7 +192,7 @@ export function init(userOptions: ElectronMainOptions): void { } removeRedundantIntegrations(options); - options.integrations = options.integrations.map(skipContextLinesForRendererEvents); + options.integrations = options.integrations.map((i) => limitContextLinesForRendererEvents(i, () => app.getAppPath())); configureUtilityProcessIPC(); const scope = getCurrentScope(); diff --git a/test/unit/renderer-event.test.ts b/test/unit/renderer-event.test.ts index 42925070..75eb405e 100644 --- a/test/unit/renderer-event.test.ts +++ b/test/unit/renderer-event.test.ts @@ -1,37 +1,68 @@ -import type { Event, Integration } from '@sentry/core'; +import type { Event, Integration, StackFrame } from '@sentry/core'; +import { join } from 'path'; import { describe, expect, test, vi } from 'vitest'; -import { markRendererEvent, skipContextLinesForRendererEvents } from '../../src/main/renderer-event'; +import { limitContextLinesForRendererEvents, markRendererEvent } from '../../src/main/renderer-event'; + +const APP_PATH = join(__dirname, 'app'); function createIntegration(name: string): { integration: Integration; processEvent: ReturnType } { - const processEvent = vi.fn((event: Event) => ({ ...event, extra: { processed: true } })); + 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 }; } -describe('skipContextLinesForRendererEvents', () => { - test('Skips ContextLines for renderer events', () => { - const { integration, processEvent } = createIntegration('ContextLines'); - const wrapped = skipContextLinesForRendererEvents(integration); +function createEvent(filenames: string[]): Event { + return { + exception: { + values: [{ type: 'Error', stacktrace: { frames: filenames.map((filename) => ({ filename, lineno: 1 })) } }], + }, + }; +} - const event: Event = { message: 'renderer' }; +function frames(event: Event | null | PromiseLike): 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); - expect(wrapped.processEvent?.(event, {}, {} as never)).toBe(event); - expect(processEvent).not.toHaveBeenCalled(); + 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('Runs ContextLines for other events', () => { + test('Adds context lines to all frames of other events', async () => { const { integration, processEvent } = createIntegration('ContextLines'); - const wrapped = skipContextLinesForRendererEvents(integration); + const wrapped = limitContextLinesForRendererEvents(integration, () => APP_PATH); + + const event = createEvent([join(__dirname, 'other', 'file.js')]); - const event: Event = { message: 'main' }; + const result = await wrapped.processEvent?.(event, {}, {} as never); - expect(wrapped.processEvent?.(event, {}, {} as never)).toEqual({ message: 'main', extra: { processed: true } }); 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(skipContextLinesForRendererEvents(integration)).toBe(integration); + expect(limitContextLinesForRendererEvents(integration, () => APP_PATH)).toBe(integration); }); });