From 39504671b4dd9a5d6d95fc6e36558606efb6dff4 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 15 Oct 2026 11:18:44 +0000 Subject: [PATCH 1/2] fix: Only add main process context lines to renderer frames in the app directory Renderer stack frames mostly hold renderer URLs. Preload script frames are files in the app directory and keep their main process context lines. Renderer frames outside the app directory get none. --- src/main/ipc.ts | 3 ++ src/main/renderer-event.ts | 59 +++++++++++++++++++++++++++ src/main/sdk.ts | 4 +- test/unit/renderer-event.test.ts | 68 ++++++++++++++++++++++++++++++++ 4 files changed, 133 insertions(+), 1 deletion(-) 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 293fbd32..6bdc6116 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -36,6 +36,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'; @@ -98,6 +99,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..216497b1 --- /dev/null +++ b/src/main/renderer-event.ts @@ -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__'; + +/** 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; + }, + }; +} diff --git a/src/main/sdk.ts b/src/main/sdk.ts index 44c8ad17..5e2763c2 100644 --- a/src/main/sdk.ts +++ b/src/main/sdk.ts @@ -22,7 +22,7 @@ import { setNodeAsyncContextStrategy, } 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'; @@ -41,6 +41,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'; @@ -198,6 +199,7 @@ export function init(userOptions: ElectronMainOptions): void { } removeRedundantIntegrations(options); + options.integrations = options.integrations.map((i) => limitContextLinesForRendererEvents(i, () => app.getAppPath())); configureUtilityProcessIPC(); setNodeAsyncContextStrategy(); diff --git a/test/unit/renderer-event.test.ts b/test/unit/renderer-event.test.ts new file mode 100644 index 00000000..75eb405e --- /dev/null +++ b/test/unit/renderer-event.test.ts @@ -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 } { + 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): 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); + }); +}); From b66fc1001c73d4095580318feb2b0d04a852530a Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 15 Oct 2026 12:03:06 +0000 Subject: [PATCH 2/2] ref: Use a Symbol for the renderer event marker Also check that the ContextLines integration gets wrapped. --- src/main/renderer-event.ts | 4 ++-- test/unit/renderer-event.test.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/renderer-event.ts b/src/main/renderer-event.ts index 216497b1..bf6cd422 100644 --- a/src/main/renderer-event.ts +++ b/src/main/renderer-event.ts @@ -1,7 +1,7 @@ import type { Event, Integration } from '@sentry/core'; import { isAbsolute, relative, resolve } from 'path'; -const RENDERER_EVENT_KEY = '__sentry_electron_renderer_event__'; +const RENDERER_EVENT_KEY = Symbol('sentry-electron-renderer-event'); /** Marks the event as sent from a renderer process */ export function markRendererEvent(event: Event): void { @@ -10,7 +10,7 @@ export function markRendererEvent(event: Event): void { /** Was the event sent from a renderer process */ export function isRendererEvent(event: Event): boolean { - return event.sdkProcessingMetadata?.[RENDERER_EVENT_KEY] === true; + return (event.sdkProcessingMetadata as Record | undefined)?.[RENDERER_EVENT_KEY] === true; } function isPathInDirectory(path: string | undefined, directory: string): boolean { diff --git a/test/unit/renderer-event.test.ts b/test/unit/renderer-event.test.ts index 75eb405e..0ee36818 100644 --- a/test/unit/renderer-event.test.ts +++ b/test/unit/renderer-event.test.ts @@ -44,6 +44,7 @@ describe('limitContextLinesForRendererEvents', () => { const result = await wrapped.processEvent?.(event, {}, {} as never); + expect(wrapped).not.toBe(integration); expect(result).toBe(event); expect(frames(event).map((frame) => frame.context_line)).toEqual(['context', undefined, undefined, undefined]); });