diff --git a/src/main/integrations/normalize-paths.ts b/src/main/integrations/normalize-paths.ts index 1ed3ed4d..464bdd31 100644 --- a/src/main/integrations/normalize-paths.ts +++ b/src/main/integrations/normalize-paths.ts @@ -1,7 +1,8 @@ -import type { Profile } from '@sentry/core'; +import type { Profile, ProfileChunk } from '@sentry/core'; import { defineIntegration, forEachEnvelopeItem, normalizeUrlToBase } from '@sentry/core'; import { app } from 'electron'; -import { normaliseProfile, normalizePaths } from '../normalize.js'; +import { normaliseProfile, normaliseProfileChunk, normalizePaths } from '../normalize.js'; +import type { ElectronMainOptionsInternal } from '../sdk.js'; export const normalizePathsIntegration = defineIntegration(() => { return { @@ -14,6 +15,14 @@ export const normalizePathsIntegration = defineIntegration(() => { forEachEnvelopeItem(envelope, (item, type) => { if (type === 'profile') { normaliseProfile(item[1] as Profile, app.getAppPath()); + } 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(), + client.getOptions() as ElectronMainOptionsInternal, + ); } }); }); diff --git a/src/main/integrations/startup-tracing.ts b/src/main/integrations/startup-tracing.ts index 30536ae5..97b9d1a3 100644 --- a/src/main/integrations/startup-tracing.ts +++ b/src/main/integrations/startup-tracing.ts @@ -1,13 +1,16 @@ import type { Event, SerializedStreamedSpan, Span, StartSpanOptions } from '@sentry/core'; import { defineIntegration, + getClient, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setMeasurement, startSpanManual, timestampInSeconds, } from '@sentry/core'; +import type { WebContents } from 'electron'; import { app } from 'electron'; -import { flushSpanEnvelopeBuffer, ipcMainHooks, startSpanEnvelopeBuffering } from '../ipc.js'; +import { flushSpanEnvelopeBuffer, getRendererProcessName, ipcMainHooks, startSpanEnvelopeBuffering } from '../ipc.js'; +import type { ElectronMainOptionsInternal } from '../sdk.js'; import { applyStreamedRendererSpans, parseStatus } from './streamed-renderer-spans.js'; export interface StartupTracingOptions { @@ -66,11 +69,14 @@ function zeroLengthSpan(options: StartSpanOptions): void { ); } -type RendererPageload = { event: Event } | { spans: SerializedStreamedSpan[] } | undefined; +type RendererPageload = + | { event: Event; contents: WebContents | undefined } + | { spans: SerializedStreamedSpan[] } + | undefined; function waitForRendererPageload(timeout: number): Promise { return new Promise((resolve) => { - const onTransaction = (event: Event): void => finish({ event }); + const onTransaction = (event: Event, contents: WebContents | undefined): void => finish({ event, contents }); const onSpans = (spans: SerializedStreamedSpan[]): void => finish({ spans }); const timer = setTimeout(() => finish(undefined), timeout); @@ -87,13 +93,23 @@ function waitForRendererPageload(timeout: number): Promise { }); } -function applyRendererSpansAndMeasurements(parentSpan: Span, event: Event | undefined, endTimestamp: number): number { +function applyRendererSpansAndMeasurements( + parentSpan: Span, + event: Event | undefined, + contents: WebContents | undefined, + endTimestamp: number, +): number { let lastEndTimestamp = endTimestamp; if (!event) { return lastEndTimestamp; } + // These spans are re-created in the main process so we keep the name of the renderer process + const rendererProcess = { + 'electron.process': getRendererProcessName(getClient()?.getOptions() as ElectronMainOptionsInternal, contents), + }; + const rendererStartTime = event.start_timestamp || event.timestamp; parentSpan.setAttribute('performance.timeOrigin', rendererStartTime); @@ -105,6 +121,7 @@ function applyRendererSpansAndMeasurements(parentSpan: Span, event: Event | unde parentSpan, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.electron.startup', + ...rendererProcess, }, }, (rendererSpan) => { @@ -122,7 +139,7 @@ function applyRendererSpansAndMeasurements(parentSpan: Span, event: Event | unde name: spanJson.description || 'electron.renderer', op: spanJson.op, startTime, - attributes: spanJson.data, + attributes: { ...spanJson.data, ...rendererProcess }, parentSpan: rendererSpan, }, (span) => { @@ -257,7 +274,12 @@ export const startupTracingIntegration = defineIntegration((options: StartupTrac if (pageload && 'spans' in pageload) { lastEndTimestamp = applyStreamedRendererSpans(parentSpan, pageload.spans, lastEndTimestamp); } else { - lastEndTimestamp = applyRendererSpansAndMeasurements(parentSpan, pageload?.event, lastEndTimestamp); + lastEndTimestamp = applyRendererSpansAndMeasurements( + parentSpan, + pageload?.event, + pageload?.contents, + lastEndTimestamp, + ); } parentSpan.end(lastEndTimestamp * 1000); diff --git a/src/main/integrations/streamed-renderer-spans.ts b/src/main/integrations/streamed-renderer-spans.ts index 79475179..12d3f9c4 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 @@ -134,6 +136,7 @@ export function applyStreamedRendererSpans( parentSpan, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.electron.startup', + [ELECTRON_PROCESS]: segment?.attributes?.[ELECTRON_PROCESS]?.value || 'renderer', }, }, (rendererSpan) => { diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 49a929c4..996b5929 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 { @@ -75,13 +76,21 @@ function newProtocolRenderer(): void { } } +/** Gets the process name used to tag data from a renderer */ +export function getRendererProcessName( + options: ElectronMainOptionsInternal | undefined, + contents: WebContents | undefined, +): string { + return contents ? options?.getRendererName?.(contents) || 'renderer' : 'renderer'; +} + function prepareRendererEvent( options: ElectronMainOptionsInternal, event: Event, dynamicSamplingContext: Partial | undefined, contents: WebContents | undefined, ): Event { - const process = contents ? options?.getRendererName?.(contents) || 'renderer' : 'renderer'; + const process = getRendererProcessName(options, contents); // Ensure breadcrumbs are empty as they sent via scope updates event.breadcrumbs = event.breadcrumbs || []; @@ -152,7 +161,7 @@ async function sendFeedbackFromRenderer( let cached_public_key: string | undefined; -function normalizeDynamicSamplingContext( +export function normalizeDynamicSamplingContext( client: Client, options: ElectronMainOptionsInternal, envelope: Envelope, @@ -295,6 +304,8 @@ function handleEnvelope( const spans = spanContainerFromEnvelope(envelope); if (spans) { + handleSpansFromRenderer(client, options, spans, contents); + const [normalizedSpanEnvelope, segmentOrigin] = normalizeSpanStreamingEnvelope( options, envelope, @@ -378,9 +389,24 @@ function handleAttributes( options: ElectronMainOptionsInternal, contents: WebContents | undefined, maybeAttributes?: SerializedLog['attributes'], + includeOsDevice: boolean = true, ): SerializedLog['attributes'] { - const process = contents ? options?.getRendererName?.(contents) || 'renderer' : 'renderer'; + const process = getRendererProcessName(options, contents); + return applyProcessAttributes(client, options, process, maybeAttributes, includeOsDevice); +} + +/** + * Applies the main process release, environment, SDK and context attributes to a span, log or + * metric from another process + */ +export function applyProcessAttributes( + client: Client, + options: ElectronMainOptionsInternal, + process: string, + maybeAttributes?: SerializedLog['attributes'], + includeOsDevice: boolean = true, +): SerializedLog['attributes'] { const attributes: SerializedLog['attributes'] = maybeAttributes || {}; if (options.release) { @@ -396,6 +422,10 @@ function handleAttributes( attributes['electron.process'] = { value: process, type: 'string' }; + if (!includeOsDevice) { + return attributes; + } + const osDeviceAttributes = getOsDeviceLogAttributes(client); if (osDeviceAttributes['os.name']) { @@ -417,6 +447,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 +625,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/src/main/normalize.ts b/src/main/normalize.ts index 69953a24..9b254971 100644 --- a/src/main/normalize.ts +++ b/src/main/normalize.ts @@ -236,6 +236,20 @@ export function normalizeSpanStreamingEnvelope( ); } + const urlFull = span.attributes?.['url.full']; + if (typeof urlFull?.value === 'string') { + urlFull.value = normalizeUrlToBase(urlFull.value, basePath); + } + + // `url.path` has no scheme so we only replace it when it is a path inside the app + const urlPath = span.attributes?.['url.path']; + if (typeof urlPath?.value === 'string') { + const normalizedPath = normalizeUrlToBase(urlPath.value, basePath); + if (normalizedPath !== urlPath.value && normalizedPath.startsWith('app://')) { + urlPath.value = normalizedPath.slice('app://'.length); + } + } + if (span.is_segment) { segmentOrigin = span.attributes?.['sentry.origin']?.value as string | undefined; } diff --git a/src/main/sdk.ts b/src/main/sdk.ts index c739a3c0..59c3a7b1 100644 --- a/src/main/sdk.ts +++ b/src/main/sdk.ts @@ -210,6 +210,11 @@ export function init(userOptions: ElectronMainOptions): void { }; }); + client.on('processSpan', (span) => { + // Spans re-created from renderer spans already have the renderer process name + span.attributes = { 'electron.process': 'browser', ...span.attributes }; + }); + scope.setClient(client); client.init(); diff --git a/src/main/utility-processes.ts b/src/main/utility-processes.ts index 2b3f9a71..e3cb9bc5 100644 --- a/src/main/utility-processes.ts +++ b/src/main/utility-processes.ts @@ -1,10 +1,22 @@ -import type { Attachment, Event } from '@sentry/core'; -import { debug, parseEnvelope } from '@sentry/core'; +import type { + Attachment, + Client, + Envelope, + Event, + SerializedLog, + SerializedMetric, + SerializedStreamedSpanContainer, +} from '@sentry/core'; +import { debug, forEachEnvelopeItem, parseEnvelope } from '@sentry/core'; import { captureEvent, getClient } from '@sentry/node'; import * as electron from 'electron'; import { eventFromEnvelope } from '../common/envelope.js'; import { getMagicMessage, isMagicMessage } from '../common/ipc.js'; +import { applyProcessAttributes, normalizeDynamicSamplingContext } from './ipc.js'; import { mergeEvents } from './merge.js'; +import { normalizeSpanStreamingEnvelope } from './normalize.js'; +import type { ElectronMainOptionsInternal } from './sdk.js'; +import { SDK_VERSION } from './version.js'; function log(message: string): void { debug.log(`[Utility Process] ${message}`); @@ -86,11 +98,44 @@ function handleEnvelopeFromUtility(env: Uint8Array | string): void { captureEventFromUtility(event, attachments); } else { - // Pass other types of envelope straight to the client - void getClient()?.sendEnvelope(envelope); + const client = getClient(); + + if (client) { + void client.sendEnvelope(prepareEnvelopeFromUtility(client, envelope)); + } } } +/** + * Spans, logs and metrics from utility processes are forwarded without passing through the main + * process client, so we apply the main process release, environment, SDK and context attributes. + */ +function prepareEnvelopeFromUtility(client: Client, envelope: Envelope): Envelope { + const options = client.getOptions() as ElectronMainOptionsInternal; + + normalizeDynamicSamplingContext(client, options, envelope); + + if (envelope[0].sdk) { + envelope[0].sdk = { name: 'sentry.javascript.electron', version: SDK_VERSION }; + } + + forEachEnvelopeItem(envelope, (item, type) => { + if (type === 'span') { + for (const span of (item[1] as SerializedStreamedSpanContainer).items) { + // Like main process spans, only segment spans get the OS and device attributes + span.attributes = applyProcessAttributes(client, options, 'utility', span.attributes, !!span.is_segment) || {}; + } + } else if (type === 'log' || type === 'trace_metric') { + for (const entry of (item[1] as { items: Array }).items) { + entry.attributes = applyProcessAttributes(client, options, 'utility', entry.attributes); + } + } + }); + + const [normalizedEnvelope] = normalizeSpanStreamingEnvelope(options, envelope, electron.app.getAppPath()); + return normalizedEnvelope; +} + function captureEventFromUtility(event: Event, attachments: Attachment[]): void { // Remove the environment as it defaults to 'production' and overwrites the main process environment delete event.environment; 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 5897a09f..a331942f 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 6de8505b..27f3c29b 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 cb8a7248..6b58aad5 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,29 @@ 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' }, + // URLs are normalized relative to the app path + 'url.full': { value: 'app:///src/index.html', type: 'string' }, + 'url.path': { value: '/src/index.html', 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/custom-tracing/test.ts b/test/e2e/test-apps/other/custom-tracing/test.ts index dcdcea36..7498fe5a 100644 --- a/test/e2e/test-apps/other/custom-tracing/test.ts +++ b/test/e2e/test-apps/other/custom-tracing/test.ts @@ -26,6 +26,7 @@ electronTestRunner( 'sentry.segment.id': { value: SHORT_UUID_MATCHER, type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.electron', type: 'string' }, 'sentry.trace_lifecycle': { value: 'stream', type: 'string' }, + 'electron.process': { value: 'browser', type: 'string' }, }), }, { @@ -45,6 +46,7 @@ electronTestRunner( 'sentry.sdk.name': { value: 'sentry.javascript.electron', type: 'string' }, 'sentry.sdk.integrations': { value: expect.any(Array), type: 'array' }, 'os.name': { value: expect.any(String), type: 'string' }, + 'electron.process': { value: 'browser', type: 'string' }, 'sentry.segment.name.source': { value: 'custom', type: 'string' }, }), }, 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 537391c9..61b08635 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 00000000..8080c195 --- /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 00000000..48bcce7f --- /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(); +}); diff --git a/test/e2e/test-apps/other/startup-tracing/test.ts b/test/e2e/test-apps/other/startup-tracing/test.ts index 3edae1f9..bb11826f 100644 --- a/test/e2e/test-apps/other/startup-tracing/test.ts +++ b/test/e2e/test-apps/other/startup-tracing/test.ts @@ -75,6 +75,19 @@ electronTestRunner( ]) { expect(ops).toContain(op); } + + // Spans keep the name of the process they were created in + const processFor = (op: string): unknown => + ( + spans?.find((s) => s.attributes?.['sentry.op']?.value === op)?.attributes as + | Record + | undefined + )?.['electron.process']?.value; + + 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(); diff --git a/test/e2e/test-apps/other/utility-process-tracing/package.json b/test/e2e/test-apps/other/utility-process-tracing/package.json new file mode 100644 index 00000000..a6a72f9a --- /dev/null +++ b/test/e2e/test-apps/other/utility-process-tracing/package.json @@ -0,0 +1,9 @@ +{ + "name": "utility-process-tracing", + "description": "Utility process spans, logs and metrics", + "version": "1.0.0", + "main": "src/main.js", + "dependencies": { + "@sentry/electron": "5.6.0" + } +} diff --git a/test/e2e/test-apps/other/utility-process-tracing/src/main.js b/test/e2e/test-apps/other/utility-process-tracing/src/main.js new file mode 100644 index 00000000..cf37709b --- /dev/null +++ b/test/e2e/test-apps/other/utility-process-tracing/src/main.js @@ -0,0 +1,15 @@ +const path = require('path'); + +const { app, utilityProcess } = require('electron'); +const { init } = require('@sentry/electron/main'); + +init({ + dsn: '__DSN__', + debug: true, + release: 'some-release', + onFatalError: () => {}, +}); + +app.on('ready', () => { + utilityProcess.fork(path.join(__dirname, 'utility.js')); +}); diff --git a/test/e2e/test-apps/other/utility-process-tracing/src/utility.js b/test/e2e/test-apps/other/utility-process-tracing/src/utility.js new file mode 100644 index 00000000..b61d8c2f --- /dev/null +++ b/test/e2e/test-apps/other/utility-process-tracing/src/utility.js @@ -0,0 +1,17 @@ +const { init, flush, logger, metrics, startSpan } = require('@sentry/electron/utility'); + +init({ + debug: true, + tracesSampleRate: 1, +}); + +setTimeout(() => { + startSpan({ name: 'utility-span' }, () => { + startSpan({ name: 'utility-child-span' }, () => {}); + }); + + logger.info('utility log'); + metrics.count('utility.metric', 1); + + flush(); +}, 1000); diff --git a/test/e2e/test-apps/other/utility-process-tracing/test.ts b/test/e2e/test-apps/other/utility-process-tracing/test.ts new file mode 100644 index 00000000..386d7c13 --- /dev/null +++ b/test/e2e/test-apps/other/utility-process-tracing/test.ts @@ -0,0 +1,63 @@ +import type { Envelope } from '@sentry/core'; +import { expect } from 'vitest'; +import { electronTestRunner, SDK_VERSION } from '../../..'; + +type Item = { name?: string; body?: string; is_segment?: boolean; attributes?: Record }; + +function itemsOfType(envelope: Envelope, type: string): Item[] | undefined { + const item = envelope[1].find(([headers]) => (headers.type as string) === type); + return (item?.[1] as { items?: Item[] } | undefined)?.items; +} + +// Spans, logs and metrics from utility processes get the main process release, environment and SDK +const MAIN_PROCESS_ATTRIBUTES = { + '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: 'utility', type: 'string' }, +}; + +const OS_ATTRIBUTES = { + 'os.name': { value: expect.any(String), type: 'string' }, +}; + +electronTestRunner(__dirname, { skipEsmAutoTransform: true }, async (ctx) => { + await ctx + .ignoreExpectationOrder() + .expect({ + envelope: (envelope) => { + const spans = itemsOfType(envelope, 'span'); + expect(spans).toHaveLength(2); + + const [header] = envelope; + expect(header.sdk).toEqual({ name: 'sentry.javascript.electron', version: SDK_VERSION }); + expect(header.trace).toMatchObject({ release: 'some-release', environment: 'development' }); + + const segment = spans?.find((span) => span.is_segment); + expect(segment?.name).toEqual('utility-span'); + expect(segment?.attributes).toMatchObject({ ...MAIN_PROCESS_ATTRIBUTES, ...OS_ATTRIBUTES }); + + const child = spans?.find((span) => !span.is_segment); + expect(child?.name).toEqual('utility-child-span'); + expect(child?.attributes).toMatchObject(MAIN_PROCESS_ATTRIBUTES); + }, + }) + .expect({ + envelope: (envelope) => { + const logs = itemsOfType(envelope, 'log'); + expect(logs).toHaveLength(1); + expect(logs?.[0]?.body).toEqual('utility log'); + expect(logs?.[0]?.attributes).toMatchObject({ ...MAIN_PROCESS_ATTRIBUTES, ...OS_ATTRIBUTES }); + }, + }) + .expect({ + envelope: (envelope) => { + const metrics = itemsOfType(envelope, 'trace_metric'); + expect(metrics).toHaveLength(1); + expect(metrics?.[0]?.name).toEqual('utility.metric'); + expect(metrics?.[0]?.attributes).toMatchObject({ ...MAIN_PROCESS_ATTRIBUTES, ...OS_ATTRIBUTES }); + }, + }) + .run(); +});