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
13 changes: 11 additions & 2 deletions src/main/integrations/normalize-paths.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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,
);
}
});
});
Expand Down
34 changes: 28 additions & 6 deletions src/main/integrations/startup-tracing.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<RendererPageload> {
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);
Expand All @@ -87,13 +93,23 @@ function waitForRendererPageload(timeout: number): Promise<RendererPageload> {
});
}

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);

Expand All @@ -105,6 +121,7 @@ function applyRendererSpansAndMeasurements(parentSpan: Span, event: Event | unde
parentSpan,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.electron.startup',
...rendererProcess,
},
},
(rendererSpan) => {
Expand All @@ -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) => {
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions src/main/integrations/streamed-renderer-spans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -66,6 +67,7 @@ const NON_INHERITED_SEGMENT_ATTRIBUTES = new Set<string>([
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
Expand Down Expand Up @@ -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) => {
Expand Down
62 changes: 59 additions & 3 deletions src/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
EventHint,
ScopeData,
SerializedStreamedSpan,
SerializedStreamedSpanContainer,
TransportMakeRequestResponse,
} from '@sentry/core';
import {
Expand Down Expand Up @@ -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<DynamicSamplingContext> | 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 || [];
Expand Down Expand Up @@ -152,7 +161,7 @@ async function sendFeedbackFromRenderer(

let cached_public_key: string | undefined;

function normalizeDynamicSamplingContext(
export function normalizeDynamicSamplingContext(
client: Client,
options: ElectronMainOptionsInternal,
envelope: Envelope,
Expand Down Expand Up @@ -295,6 +304,8 @@ function handleEnvelope(

const spans = spanContainerFromEnvelope(envelope);
if (spans) {
handleSpansFromRenderer(client, options, spans, contents);

const [normalizedSpanEnvelope, segmentOrigin] = normalizeSpanStreamingEnvelope(
options,
envelope,
Expand Down Expand Up @@ -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) {
Expand All @@ -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']) {
Expand All @@ -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,
Expand Down Expand Up @@ -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));
}
14 changes: 14 additions & 0 deletions src/main/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
5 changes: 5 additions & 0 deletions src/main/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
});
Comment thread
timfish marked this conversation as resolved.

scope.setClient(client);
client.init();

Expand Down
53 changes: 49 additions & 4 deletions src/main/utility-processes.ts
Original file line number Diff line number Diff line change
@@ -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}`);
Expand Down Expand Up @@ -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<SerializedLog | SerializedMetric> }).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;
Expand Down
8 changes: 4 additions & 4 deletions test/e2e/test-apps/other/browser-profiling-manual/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
}),
};
}
Expand Down Expand Up @@ -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' },
}),
Expand Down
Loading
Loading