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
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,12 @@
"e2e": "xvfb-maybe vitest run --root=./test/e2e --silent=false --disable-console-intercept"
},
"dependencies": {
"@sentry/browser": "10.26.0",
"@sentry/core": "10.26.0",
"@sentry/node": "10.26.0"
"@sentry/browser": "10.27.0",
"@sentry/core": "10.27.0",
"@sentry/node": "10.27.0"
},
"peerDependencies": {
"@sentry/node-native": "10.26.0"
"@sentry/node-native": "10.27.0"
},
"peerDependenciesMeta": {
"@sentry/node-native": {
Expand All @@ -119,8 +119,8 @@
},
"devDependencies": {
"@rollup/plugin-typescript": "^12.1.3",
"@sentry/node-native": "10.26.0",
"@sentry-internal/eslint-config-sdk": "10.26.0",
"@sentry/node-native": "10.27.0",
"@sentry-internal/eslint-config-sdk": "10.27.0",
"@sentry-internal/typescript": "10.26.0",
"@types/busboy": "^1.5.4",
"@types/koa": "^2.0.52",
Expand Down
24 changes: 23 additions & 1 deletion src/common/envelope.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { Attachment, AttachmentItem, Envelope, Event, EventItem, forEachEnvelopeItem, Profile } from '@sentry/core';
import {
Attachment,
AttachmentItem,
Envelope,
Event,
EventItem,
forEachEnvelopeItem,
Profile,
ProfileChunk,
} from '@sentry/core';

/** Pulls an event and additional envelope items out of an envelope. Returns undefined if there was no event */
export function eventFromEnvelope(envelope: Envelope): [Event, Attachment[], Profile | undefined] | undefined {
Expand All @@ -25,3 +34,16 @@ export function eventFromEnvelope(envelope: Envelope): [Event, Attachment[], Pro

return event ? [event, attachments, profile] : undefined;
}

/** Extracts profile_chunk from an envelope if present */
export function profileChunkFromEnvelope(envelope: Envelope): ProfileChunk | undefined {
let profileChunk: ProfileChunk | undefined;

forEachEnvelopeItem(envelope, (item, type) => {
if (type === 'profile_chunk') {
profileChunk = item[1] as unknown as ProfileChunk;
}
});

return profileChunk;
}
17 changes: 12 additions & 5 deletions src/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import {
} from '@sentry/core';
import { captureEvent, getClient, getCurrentScope } from '@sentry/node';
import { app, ipcMain, protocol, WebContents, webContents } from 'electron';
import { eventFromEnvelope } from '../common/envelope.js';
import { eventFromEnvelope, profileChunkFromEnvelope } from '../common/envelope.js';
import { ipcChannelUtils, IPCMode, IpcUtils, RendererStatus } from '../common/ipc.js';
import { registerProtocol } from './electron-normalize.js';
import { createRendererEventLoopBlockStatusHandler } from './integrations/renderer-anr.js';
import { rendererProfileFromIpc } from './integrations/renderer-profiling.js';
import { getOsDeviceLogAttributes } from './log.js';
import { mergeEvents } from './merge.js';
import { normalizeReplayEnvelope } from './normalize.js';
import { normalizeProfileChunkEnvelope, normalizeReplayEnvelope } from './normalize.js';
import { ElectronMainOptionsInternal } from './sdk.js';
import { SDK_VERSION } from './version.js';

Expand Down Expand Up @@ -114,9 +114,16 @@ function handleEnvelope(

captureEventFromRenderer(options, event, dynamicSamplingContext, attachments, contents);
} else {
const normalizedEnvelope = normalizeReplayEnvelope(options, envelope, app.getAppPath());
// Pass other types of envelope straight to the transport
void getClient()?.getTransport()?.send(normalizedEnvelope);
// Check if this is a profile_chunk envelope (from UI profiling)
const profileChunk = profileChunkFromEnvelope(envelope);
if (profileChunk) {
const normalizedEnvelope = normalizeProfileChunkEnvelope(options, envelope, app.getAppPath());
void getClient()?.getTransport()?.send(normalizedEnvelope);
} else {
const normalizedEnvelope = normalizeReplayEnvelope(options, envelope, app.getAppPath());
// Pass other types of envelope straight to the transport
void getClient()?.getTransport()?.send(normalizedEnvelope);
}
}
}

Expand Down
69 changes: 69 additions & 0 deletions src/main/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import {
getCurrentScope,
normalizeUrlToBase,
Profile,
ProfileChunk,
ReplayEvent,
} from '@sentry/core';
import { createGetModuleFromFilename } from '@sentry/node';
import { app } from 'electron';
import { ElectronMainOptionsInternal } from './sdk.js';
import { SDK_VERSION } from './version.js';

const getModuleFromFilename = createGetModuleFromFilename(app.getAppPath());

Expand Down Expand Up @@ -120,3 +122,70 @@ export function normaliseProfile(profile: Profile, basePath: string): void {
}
}
}

/**
* Normalizes all URLs in a profile chunk and sets release/environment
*/
export function normaliseProfileChunk(
profileChunk: ProfileChunk,
basePath: string,
options: ElectronMainOptionsInternal,
): void {
// Override release and environment with main process values
if (options.release) {
profileChunk.release = options.release;
}
if (options.environment) {
profileChunk.environment = options.environment;
}

// Normalize paths in profile frames
const profile = profileChunk.profile as { frames?: Array<{ abs_path?: string; filename?: string; module?: string }> };
if (profile?.frames) {
for (const frame of profile.frames) {
if (frame.abs_path) {
frame.abs_path = normalizeUrlToBase(frame.abs_path, basePath);
}

if (frame.filename) {
frame.filename = normalizeUrlToBase(frame.filename, basePath);
}

if (frame.module) {
frame.module = getModuleFromFilename(frame.abs_path);
}
}
}
}

/**
* Normalizes profile_chunk envelope items and returns the modified envelope
*/
export function normalizeProfileChunkEnvelope(
options: ElectronMainOptionsInternal,
envelope: Envelope,
basePath: string,
): Envelope {
// Override the SDK info in the envelope header to use Electron SDK
const [originalHeader] = envelope;
const modifiedHeader = {
...originalHeader,
sdk: { name: 'sentry.javascript.electron', version: SDK_VERSION },
};

let modifiedEnvelope = createEnvelope(modifiedHeader);
let isProfileChunk = false;

forEachEnvelopeItem(envelope, (item, type) => {
if (type === 'profile_chunk') {
isProfileChunk = true;
const [headers, chunk] = item as [{ type: 'profile_chunk' }, ProfileChunk];

normaliseProfileChunk(chunk, basePath, options);

modifiedEnvelope = addItemToEnvelope(modifiedEnvelope, [headers, chunk]);
}
});

return isProfileChunk ? modifiedEnvelope : envelope;
}
1 change: 1 addition & 0 deletions src/renderer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ export {
supabaseIntegration,
suppressTracing,
thirdPartyErrorFilterIntegration,
uiProfiler,
unleashIntegration,
updateSpanName,
webWorkerIntegration,
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ interface ElectronRendererOptions extends Partial<ElectronRendererOptionsInterna
export function init<O extends ElectronRendererOptions>(
options: ElectronRendererOptions & O = {} as ElectronRendererOptions & O,
// This parameter name ensures that TypeScript error messages contain a hint for fixing SDK version mismatches
originalInit: (if_you_get_a_typescript_error_ensure_sdks_use_version_v10_26_0: O) => void = browserInit,
originalInit: (if_you_get_a_typescript_error_ensure_sdks_use_version_v10_27_0: O) => void = browserInit,
): void {
// Ensure the browser SDK is only init'ed once.
if (window?.__SENTRY__RENDERER_INIT__) {
Expand Down
1 change: 1 addition & 0 deletions test/e2e/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export {
feedbackEnvelope,
getEventFromEnvelope,
getSessionFromEnvelope,
profileChunkEnvelope,
sessionEnvelope,
transactionEnvelope,
} from './utils';
Expand Down
47 changes: 47 additions & 0 deletions test/e2e/test-apps/other/browser-profiling-manual/src/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
</head>
<body>
<script>
const crypto = require('crypto');
const { init, browserProfilingIntegration, startSpan, uiProfiler } = require('@sentry/electron/renderer');

init({
debug: true,
integrations: [browserProfilingIntegration()],
tracesSampleRate: 1,
profileSessionSampleRate: 1,
profileLifecycle: 'manual',
});

function pbkdf2() {
return new Promise((resolve) => {
const salt = crypto.randomBytes(128).toString('base64');
crypto.pbkdf2('myPassword', salt, 10000, 512, 'sha512', resolve);
});
}

async function longWork() {
for (let i = 0; i < 10; i++) {
await startSpan({ name: 'PBKDF2' }, async () => {
await pbkdf2();
});
}
}

setTimeout(async () => {
// Start UI profiling manually
uiProfiler.startProfiler();

await startSpan({ name: 'Long work' }, async () => {
await longWork();
});

// Stop profiling - this will send a profile_chunk
uiProfiler.stopProfiler();
}, 500);
</script>
</body>
</html>
84 changes: 84 additions & 0 deletions test/e2e/test-apps/other/browser-profiling-manual/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { expect } from 'vitest';
import {
electronTestRunner,
profileChunkEnvelope,
SHORT_UUID_MATCHER,
transactionEnvelope,
UUID_MATCHER,
} from '../../..';

electronTestRunner(__dirname, async (ctx) => {
await ctx
// Expect the transaction (without attached profile since we're using UI profiling)
.expect({
envelope: transactionEnvelope({
platform: 'javascript',
type: 'transaction',
release: 'some-release',
transaction: 'Long work',
transaction_info: { source: 'custom' },
start_timestamp: expect.any(Number),
contexts: {
trace: expect.objectContaining({
trace_id: UUID_MATCHER,
span_id: SHORT_UUID_MATCHER,
data: expect.objectContaining({
'sentry.origin': 'manual',
'sentry.sample_rate': 1,
'sentry.source': 'custom',
}),
origin: 'manual',
}),
// UI profiling adds profile context with profiler_id
profile: {
profiler_id: UUID_MATCHER,
},
},
spans: expect.arrayContaining([
expect.objectContaining({
description: 'PBKDF2',
origin: 'manual',
parent_span_id: SHORT_UUID_MATCHER,
span_id: SHORT_UUID_MATCHER,
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: UUID_MATCHER,
}),
]),
tags: {
'event.environment': 'javascript',
'event.origin': 'electron',
'event.process': 'renderer',
},
request: {
headers: {},
url: 'app:///src/index.html',
},
}),
})
// Expect the profile_chunk envelope from UI profiling
.expect({
envelope: profileChunkEnvelope({
release: 'some-release',
environment: 'development',
profile: expect.objectContaining({
samples: expect.arrayContaining([
expect.objectContaining({
stack_id: expect.any(Number),
thread_id: expect.any(String),
timestamp: expect.any(Number),
}),
]),
stacks: expect.any(Array),
frames: expect.arrayContaining([
expect.objectContaining({
function: expect.any(String),
abs_path: expect.stringContaining('app:///'),
}),
]),
thread_metadata: expect.any(Object),
}),
}),
})
.run();
});
10 changes: 10 additions & 0 deletions test/e2e/test-apps/other/browser-profiling-trace/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "browser-profiling-trace",
"description": "Browser Profiling with Trace Lifecycle",
"version": "1.0.0",
"main": "src/main.js",
"dependencies": {
"@sentry/electron": "5.6.0"
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,14 @@
<body>
<script>
const crypto = require('crypto');
const {
init,
browserProfilingIntegration,
startSpan,
} = require('@sentry/electron/renderer');
const { init, browserProfilingIntegration, startSpan } = require('@sentry/electron/renderer');

init({
debug: true,
integrations: [browserProfilingIntegration()],
tracesSampleRate: 1,
profilesSampleRate: 1,
profileSessionSampleRate: 1,
profileLifecycle: 'trace',
});

function pbkdf2() {
Expand All @@ -34,8 +31,9 @@
}
}

setTimeout(() => {
startSpan({ name: 'Long work' }, async () => {
// In trace mode, profiling automatically starts/stops with spans
setTimeout(async () => {
await startSpan({ name: 'Long work' }, async () => {
await longWork();
});
}, 500);
Expand Down
Loading
Loading