From d8c5730bb25a736539c8641f8f3a16ffbff84327 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 30 Sep 2026 12:54:53 +0100 Subject: [PATCH 1/5] fix: Apply main process attributes to streamed renderer spans --- .../integrations/streamed-renderer-spans.ts | 2 ++ src/main/ipc.ts | 34 +++++++++++++++++++ .../other/browser-profiling-manual/test.ts | 8 ++--- .../other/browser-profiling-trace/test.ts | 8 ++--- .../test-apps/other/browser-tracing/test.ts | 16 +++++++-- .../e2e/test-apps/other/no-pii/src/index.html | 5 ++- test/e2e/test-apps/other/no-pii/test.ts | 11 +++++- 7 files changed, 72 insertions(+), 12 deletions(-) diff --git a/src/main/integrations/streamed-renderer-spans.ts b/src/main/integrations/streamed-renderer-spans.ts index 794751790..8aa209ad7 100644 --- a/src/main/integrations/streamed-renderer-spans.ts +++ b/src/main/integrations/streamed-renderer-spans.ts @@ -27,6 +27,7 @@ import { // dynamically from the request/response lifecycle. const SENTRY_SEGMENT_NAME_SOURCE = 'sentry.segment.name.source'; const HTTP_REQUEST_HEADER_USER_AGENT = 'http.request.header.user_agent'; +const ELECTRON_PROCESS = 'electron.process'; /** Converts a serialized span status into a `SpanStatus` */ export function parseStatus(status: string): SpanStatus { @@ -66,6 +67,7 @@ const NON_INHERITED_SEGMENT_ATTRIBUTES = new Set([ SENTRY_ENVIRONMENT, URL_FULL, HTTP_REQUEST_HEADER_USER_AGENT, + ELECTRON_PROCESS, ]); // Attributes that pin a streamed child span to the original renderer segment or SDK. These are not diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 49a929c4e..b9f2432c9 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -9,6 +9,7 @@ import type { EventHint, ScopeData, SerializedStreamedSpan, + SerializedStreamedSpanContainer, TransportMakeRequestResponse, } from '@sentry/core'; import { @@ -295,6 +296,8 @@ function handleEnvelope( const spans = spanContainerFromEnvelope(envelope); if (spans) { + handleSpansFromRenderer(client, options, spans, contents); + const [normalizedSpanEnvelope, segmentOrigin] = normalizeSpanStreamingEnvelope( options, envelope, @@ -378,6 +381,7 @@ function handleAttributes( options: ElectronMainOptionsInternal, contents: WebContents | undefined, maybeAttributes?: SerializedLog['attributes'], + includeOsDevice: boolean = true, ): SerializedLog['attributes'] { const process = contents ? options?.getRendererName?.(contents) || 'renderer' : 'renderer'; @@ -396,6 +400,10 @@ function handleAttributes( attributes['electron.process'] = { value: process, type: 'string' }; + if (!includeOsDevice) { + return attributes; + } + const osDeviceAttributes = getOsDeviceLogAttributes(client); if (osDeviceAttributes['os.name']) { @@ -417,6 +425,28 @@ function handleAttributes( return attributes; } +/** + * Streamed spans from renderers are forwarded without passing through the main process client, so + * we apply the main process release, environment, SDK and context attributes here. + */ +function handleSpansFromRenderer( + client: Client, + options: ElectronMainOptionsInternal, + container: SerializedStreamedSpanContainer, + contents: WebContents | undefined, +): void { + // IP address inference is controlled from the main process, as it is for events + container.ingest_settings = { + ...container.ingest_settings, + infer_ip: client.getDataCollectionOptions().userInfo ? 'auto' : 'never', + }; + + for (const span of container.items) { + // Like main process spans, only segment spans get the OS and device attributes + span.attributes = handleAttributes(client, options, contents, span.attributes, !!span.is_segment) || {}; + } +} + function handleLogFromRenderer( client: Client, options: ElectronMainOptionsInternal, @@ -573,4 +603,8 @@ export function configureIPC(client: Client, options: ElectronMainOptionsInterna if ((options.ipcMode & IPCMode.Classic) > 0) { configureClassic(client, ipcUtil, options); } + + // The OS and device attributes resolve asynchronously so we fetch them before the first renderer + // spans arrive + void app.whenReady().then(() => getOsDeviceLogAttributes(client)); } diff --git a/test/e2e/test-apps/other/browser-profiling-manual/test.ts b/test/e2e/test-apps/other/browser-profiling-manual/test.ts index 5897a09fb..a331942fb 100644 --- a/test/e2e/test-apps/other/browser-profiling-manual/test.ts +++ b/test/e2e/test-apps/other/browser-profiling-manual/test.ts @@ -15,10 +15,10 @@ function pbkdf2Span(): StreamedSpanJSON { status: 'ok', attributes: expect.objectContaining({ 'sentry.origin': { value: 'manual', type: 'string' }, - 'sentry.environment': { value: 'production', type: 'string' }, + 'sentry.environment': { value: 'development', type: 'string' }, 'sentry.segment.name': { value: 'Long work', type: 'string' }, 'sentry.segment.id': { value: SHORT_UUID_MATCHER, type: 'string' }, - 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.electron', type: 'string' }, }), }; } @@ -53,10 +53,10 @@ electronTestRunner(__dirname, async (ctx) => { attributes: expect.objectContaining({ 'sentry.origin': { value: 'manual', type: 'string' }, 'sentry.sample_rate': { value: 1, type: 'integer' }, - 'sentry.environment': { value: 'production', type: 'string' }, + 'sentry.environment': { value: 'development', type: 'string' }, 'sentry.segment.name': { value: 'Long work', type: 'string' }, 'sentry.segment.id': { value: SHORT_UUID_MATCHER, type: 'string' }, - 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.electron', type: 'string' }, 'sentry.profiler_id': { value: UUID_MATCHER, type: 'string' }, 'sentry.segment.name.source': { value: 'custom', type: 'string' }, }), diff --git a/test/e2e/test-apps/other/browser-profiling-trace/test.ts b/test/e2e/test-apps/other/browser-profiling-trace/test.ts index 6de8505b2..27f3c29be 100644 --- a/test/e2e/test-apps/other/browser-profiling-trace/test.ts +++ b/test/e2e/test-apps/other/browser-profiling-trace/test.ts @@ -15,10 +15,10 @@ function pbkdf2Span(): StreamedSpanJSON { status: 'ok', attributes: expect.objectContaining({ 'sentry.origin': { value: 'manual', type: 'string' }, - 'sentry.environment': { value: 'production', type: 'string' }, + 'sentry.environment': { value: 'development', type: 'string' }, 'sentry.segment.name': { value: 'Long work', type: 'string' }, 'sentry.segment.id': { value: SHORT_UUID_MATCHER, type: 'string' }, - 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.electron', type: 'string' }, }), }; } @@ -53,10 +53,10 @@ electronTestRunner(__dirname, async (ctx) => { attributes: expect.objectContaining({ 'sentry.origin': { value: 'manual', type: 'string' }, 'sentry.sample_rate': { value: 1, type: 'integer' }, - 'sentry.environment': { value: 'production', type: 'string' }, + 'sentry.environment': { value: 'development', type: 'string' }, 'sentry.segment.name': { value: 'Long work', type: 'string' }, 'sentry.segment.id': { value: SHORT_UUID_MATCHER, type: 'string' }, - 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.electron', type: 'string' }, 'sentry.profiler_id': { value: UUID_MATCHER, type: 'string' }, 'sentry.segment.name.source': { value: 'custom', type: 'string' }, }), diff --git a/test/e2e/test-apps/other/browser-tracing/test.ts b/test/e2e/test-apps/other/browser-tracing/test.ts index cb8a72484..f74457a82 100644 --- a/test/e2e/test-apps/other/browser-tracing/test.ts +++ b/test/e2e/test-apps/other/browser-tracing/test.ts @@ -1,5 +1,5 @@ import { expect } from 'vitest'; -import { electronTestRunner, getSpansFromEnvelope } from '../../..'; +import { electronTestRunner, getSpansFromEnvelope, SDK_VERSION } from '../../..'; electronTestRunner( __dirname, @@ -32,14 +32,26 @@ electronTestRunner( 'sentry.origin': { value: 'auto.pageload.browser', type: 'string' }, 'sentry.segment.name.source': { value: 'url', type: 'string' }, 'sentry.sample_rate': { value: 1, type: 'integer' }, + 'os.name': { value: expect.any(String), type: 'string' }, }), }); - // All spans share the same trace + // All spans share the same trace and get the main process release, environment and SDK for (const span of spans ?? []) { expect(span.trace_id).toEqual(segment?.trace_id); + expect(span.attributes).toMatchObject({ + 'sentry.release': { value: 'some-release', type: 'string' }, + 'sentry.environment': { value: 'development', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.electron', type: 'string' }, + 'sentry.sdk.version': { value: SDK_VERSION, type: 'string' }, + 'electron.process': { value: 'renderer', type: 'string' }, + }); } + // IP address inference is controlled by the main process + const container = envelope[1][0]?.[1] as { ingest_settings?: { infer_ip?: string } }; + expect(container.ingest_settings?.infer_ip).toEqual('auto'); + // The key browser performance metric spans are present as children const ops = (spans ?? []).map( (s) => (s.attributes as Record | undefined)?.['sentry.op']?.value, diff --git a/test/e2e/test-apps/other/no-pii/src/index.html b/test/e2e/test-apps/other/no-pii/src/index.html index 537391c9b..61b086352 100644 --- a/test/e2e/test-apps/other/no-pii/src/index.html +++ b/test/e2e/test-apps/other/no-pii/src/index.html @@ -5,12 +5,15 @@ + + diff --git a/test/e2e/test-apps/other/startup-tracing-static-renderer/src/main.js b/test/e2e/test-apps/other/startup-tracing-static-renderer/src/main.js new file mode 100644 index 000000000..8080c195a --- /dev/null +++ b/test/e2e/test-apps/other/startup-tracing-static-renderer/src/main.js @@ -0,0 +1,24 @@ +const path = require('path'); + +const { app, BrowserWindow } = require('electron'); +const { init, startupTracingIntegration } = require('@sentry/electron/main'); + +init({ + dsn: '__DSN__', + debug: true, + tracesSampleRate: 1, + integrations: [startupTracingIntegration()], + onFatalError: () => {}, +}); + +app.on('ready', () => { + const mainWindow = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + }, + }); + + mainWindow.loadFile(path.join(__dirname, 'index.html')); +}); diff --git a/test/e2e/test-apps/other/startup-tracing-static-renderer/test.ts b/test/e2e/test-apps/other/startup-tracing-static-renderer/test.ts new file mode 100644 index 000000000..48bcce7f6 --- /dev/null +++ b/test/e2e/test-apps/other/startup-tracing-static-renderer/test.ts @@ -0,0 +1,31 @@ +import { expect } from 'vitest'; +import { electronTestRunner, getSpansFromEnvelope } from '../../..'; + +electronTestRunner(__dirname, async (ctx) => { + await ctx + .expect({ + // The renderer sends its pageload as a transaction and the main process re-creates its spans + // in the streamed `Startup` segment + envelope: (envelope) => { + const spans = getSpansFromEnvelope(envelope); + expect(spans).toBeDefined(); + + const segment = spans?.find((s) => s.is_segment); + expect(segment?.name).toEqual('Startup'); + + const processFor = (op: string): unknown => + ( + spans?.find((s) => s.attributes?.['sentry.op']?.value === op)?.attributes as + | Record + | undefined + )?.['electron.process']?.value; + + // Spans keep the name of the process they were created in + expect(segment?.attributes?.['electron.process']).toEqual({ value: 'browser', type: 'string' }); + expect(processFor('electron.ready')).toEqual('browser'); + expect(processFor('electron.renderer')).toEqual('renderer'); + expect(processFor('browser.request')).toEqual('renderer'); + }, + }) + .run(); +}); From d9628fa9016ac4c915c86673224a9864858a78c1 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 30 Sep 2026 14:46:35 +0100 Subject: [PATCH 5/5] ref: Normalize profile chunks in the beforeEnvelope hook --- src/main/integrations/normalize-paths.ts | 22 +++++-------------- .../test-apps/other/node-profiling/test.ts | 15 ++++++++----- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/src/main/integrations/normalize-paths.ts b/src/main/integrations/normalize-paths.ts index ff08472ee..464bdd31b 100644 --- a/src/main/integrations/normalize-paths.ts +++ b/src/main/integrations/normalize-paths.ts @@ -15,19 +15,9 @@ export const normalizePathsIntegration = defineIntegration(() => { forEachEnvelopeItem(envelope, (item, type) => { if (type === 'profile') { normaliseProfile(item[1] as Profile, app.getAppPath()); - } - }); - }); - }); - - // `@sentry/profiling-node` sends profile chunks straight to the transport so the - // `beforeEnvelope` hook is not called for them - const transport = client.getTransport(); - if (transport) { - const send = transport.send.bind(transport); - transport.send = (envelope) => { - forEachEnvelopeItem(envelope, (item, type) => { - if (type === 'profile_chunk') { + } else if (type === 'profile_chunk') { + // Chunks from `@sentry/profiling-node` only reach this hook once + // https://github.com/getsentry/sentry-javascript/pull/24896 is released normaliseProfileChunk( item[1] as ProfileChunk, app.getAppPath(), @@ -35,10 +25,8 @@ export const normalizePathsIntegration = defineIntegration(() => { ); } }); - - return send(envelope); - }; - } + }); + }); }, processEvent(event) { return normalizePaths(event, app.getAppPath()); diff --git a/test/e2e/test-apps/other/node-profiling/test.ts b/test/e2e/test-apps/other/node-profiling/test.ts index ce9b1654d..f00d281c2 100644 --- a/test/e2e/test-apps/other/node-profiling/test.ts +++ b/test/e2e/test-apps/other/node-profiling/test.ts @@ -69,12 +69,15 @@ electronTestRunner(__dirname, async (ctx) => { release: 'some-release', environment: 'development', measurements: expect.any(Object), - // Frame paths are normalized relative to the app path - profile: expect.objectContaining({ - frames: expect.arrayContaining([ - expect.objectContaining({ abs_path: expect.stringMatching(/^app:\/\/\/src\/main\.m?js$/) }), - ]), - }), + // TODO: Enable this once https://github.com/getsentry/sentry-javascript/pull/24896 is + // released. Until then, chunks from `@sentry/profiling-node` do not pass through the + // `beforeEnvelope` hook so their frame paths are not normalized. + // + // profile: expect.objectContaining({ + // frames: expect.arrayContaining([ + // expect.objectContaining({ abs_path: expect.stringMatching(/^app:\/\/\/src\/main\.m?js$/) }), + // ]), + // }), }, 'node', ),