Skip to content

Commit c91f6cb

Browse files
authored
feat: Add support for UI profiling (#1267)
1 parent 23796c6 commit c91f6cb

18 files changed

Lines changed: 701 additions & 483 deletions

File tree

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,12 @@
105105
"e2e": "xvfb-maybe vitest run --root=./test/e2e --silent=false --disable-console-intercept"
106106
},
107107
"dependencies": {
108-
"@sentry/browser": "10.26.0",
109-
"@sentry/core": "10.26.0",
110-
"@sentry/node": "10.26.0"
108+
"@sentry/browser": "10.27.0",
109+
"@sentry/core": "10.27.0",
110+
"@sentry/node": "10.27.0"
111111
},
112112
"peerDependencies": {
113-
"@sentry/node-native": "10.26.0"
113+
"@sentry/node-native": "10.27.0"
114114
},
115115
"peerDependenciesMeta": {
116116
"@sentry/node-native": {
@@ -119,8 +119,8 @@
119119
},
120120
"devDependencies": {
121121
"@rollup/plugin-typescript": "^12.1.3",
122-
"@sentry/node-native": "10.26.0",
123-
"@sentry-internal/eslint-config-sdk": "10.26.0",
122+
"@sentry/node-native": "10.27.0",
123+
"@sentry-internal/eslint-config-sdk": "10.27.0",
124124
"@sentry-internal/typescript": "10.26.0",
125125
"@types/busboy": "^1.5.4",
126126
"@types/koa": "^2.0.52",

src/common/envelope.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
import { Attachment, AttachmentItem, Envelope, Event, EventItem, forEachEnvelopeItem, Profile } from '@sentry/core';
1+
import {
2+
Attachment,
3+
AttachmentItem,
4+
Envelope,
5+
Event,
6+
EventItem,
7+
forEachEnvelopeItem,
8+
Profile,
9+
ProfileChunk,
10+
} from '@sentry/core';
211

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

2635
return event ? [event, attachments, profile] : undefined;
2736
}
37+
38+
/** Extracts profile_chunk from an envelope if present */
39+
export function profileChunkFromEnvelope(envelope: Envelope): ProfileChunk | undefined {
40+
let profileChunk: ProfileChunk | undefined;
41+
42+
forEachEnvelopeItem(envelope, (item, type) => {
43+
if (type === 'profile_chunk') {
44+
profileChunk = item[1] as unknown as ProfileChunk;
45+
}
46+
});
47+
48+
return profileChunk;
49+
}

src/main/ipc.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ import {
1111
} from '@sentry/core';
1212
import { captureEvent, getClient, getCurrentScope } from '@sentry/node';
1313
import { app, ipcMain, protocol, WebContents, webContents } from 'electron';
14-
import { eventFromEnvelope } from '../common/envelope.js';
14+
import { eventFromEnvelope, profileChunkFromEnvelope } from '../common/envelope.js';
1515
import { ipcChannelUtils, IPCMode, IpcUtils, RendererStatus } from '../common/ipc.js';
1616
import { registerProtocol } from './electron-normalize.js';
1717
import { createRendererEventLoopBlockStatusHandler } from './integrations/renderer-anr.js';
1818
import { rendererProfileFromIpc } from './integrations/renderer-profiling.js';
1919
import { getOsDeviceLogAttributes } from './log.js';
2020
import { mergeEvents } from './merge.js';
21-
import { normalizeReplayEnvelope } from './normalize.js';
21+
import { normalizeProfileChunkEnvelope, normalizeReplayEnvelope } from './normalize.js';
2222
import { ElectronMainOptionsInternal } from './sdk.js';
2323
import { SDK_VERSION } from './version.js';
2424

@@ -114,9 +114,16 @@ function handleEnvelope(
114114

115115
captureEventFromRenderer(options, event, dynamicSamplingContext, attachments, contents);
116116
} else {
117-
const normalizedEnvelope = normalizeReplayEnvelope(options, envelope, app.getAppPath());
118-
// Pass other types of envelope straight to the transport
119-
void getClient()?.getTransport()?.send(normalizedEnvelope);
117+
// Check if this is a profile_chunk envelope (from UI profiling)
118+
const profileChunk = profileChunkFromEnvelope(envelope);
119+
if (profileChunk) {
120+
const normalizedEnvelope = normalizeProfileChunkEnvelope(options, envelope, app.getAppPath());
121+
void getClient()?.getTransport()?.send(normalizedEnvelope);
122+
} else {
123+
const normalizedEnvelope = normalizeReplayEnvelope(options, envelope, app.getAppPath());
124+
// Pass other types of envelope straight to the transport
125+
void getClient()?.getTransport()?.send(normalizedEnvelope);
126+
}
120127
}
121128
}
122129

src/main/normalize.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import {
77
getCurrentScope,
88
normalizeUrlToBase,
99
Profile,
10+
ProfileChunk,
1011
ReplayEvent,
1112
} from '@sentry/core';
1213
import { createGetModuleFromFilename } from '@sentry/node';
1314
import { app } from 'electron';
1415
import { ElectronMainOptionsInternal } from './sdk.js';
16+
import { SDK_VERSION } from './version.js';
1517

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

@@ -120,3 +122,70 @@ export function normaliseProfile(profile: Profile, basePath: string): void {
120122
}
121123
}
122124
}
125+
126+
/**
127+
* Normalizes all URLs in a profile chunk and sets release/environment
128+
*/
129+
export function normaliseProfileChunk(
130+
profileChunk: ProfileChunk,
131+
basePath: string,
132+
options: ElectronMainOptionsInternal,
133+
): void {
134+
// Override release and environment with main process values
135+
if (options.release) {
136+
profileChunk.release = options.release;
137+
}
138+
if (options.environment) {
139+
profileChunk.environment = options.environment;
140+
}
141+
142+
// Normalize paths in profile frames
143+
const profile = profileChunk.profile as { frames?: Array<{ abs_path?: string; filename?: string; module?: string }> };
144+
if (profile?.frames) {
145+
for (const frame of profile.frames) {
146+
if (frame.abs_path) {
147+
frame.abs_path = normalizeUrlToBase(frame.abs_path, basePath);
148+
}
149+
150+
if (frame.filename) {
151+
frame.filename = normalizeUrlToBase(frame.filename, basePath);
152+
}
153+
154+
if (frame.module) {
155+
frame.module = getModuleFromFilename(frame.abs_path);
156+
}
157+
}
158+
}
159+
}
160+
161+
/**
162+
* Normalizes profile_chunk envelope items and returns the modified envelope
163+
*/
164+
export function normalizeProfileChunkEnvelope(
165+
options: ElectronMainOptionsInternal,
166+
envelope: Envelope,
167+
basePath: string,
168+
): Envelope {
169+
// Override the SDK info in the envelope header to use Electron SDK
170+
const [originalHeader] = envelope;
171+
const modifiedHeader = {
172+
...originalHeader,
173+
sdk: { name: 'sentry.javascript.electron', version: SDK_VERSION },
174+
};
175+
176+
let modifiedEnvelope = createEnvelope(modifiedHeader);
177+
let isProfileChunk = false;
178+
179+
forEachEnvelopeItem(envelope, (item, type) => {
180+
if (type === 'profile_chunk') {
181+
isProfileChunk = true;
182+
const [headers, chunk] = item as [{ type: 'profile_chunk' }, ProfileChunk];
183+
184+
normaliseProfileChunk(chunk, basePath, options);
185+
186+
modifiedEnvelope = addItemToEnvelope(modifiedEnvelope, [headers, chunk]);
187+
}
188+
});
189+
190+
return isProfileChunk ? modifiedEnvelope : envelope;
191+
}

src/renderer/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ export {
129129
supabaseIntegration,
130130
suppressTracing,
131131
thirdPartyErrorFilterIntegration,
132+
uiProfiler,
132133
unleashIntegration,
133134
updateSpanName,
134135
webWorkerIntegration,

src/renderer/sdk.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ interface ElectronRendererOptions extends Partial<ElectronRendererOptionsInterna
5151
export function init<O extends ElectronRendererOptions>(
5252
options: ElectronRendererOptions & O = {} as ElectronRendererOptions & O,
5353
// This parameter name ensures that TypeScript error messages contain a hint for fixing SDK version mismatches
54-
originalInit: (if_you_get_a_typescript_error_ensure_sdks_use_version_v10_26_0: O) => void = browserInit,
54+
originalInit: (if_you_get_a_typescript_error_ensure_sdks_use_version_v10_27_0: O) => void = browserInit,
5555
): void {
5656
// Ensure the browser SDK is only init'ed once.
5757
if (window?.__SENTRY__RENDERER_INIT__) {

test/e2e/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export {
1111
feedbackEnvelope,
1212
getEventFromEnvelope,
1313
getSessionFromEnvelope,
14+
profileChunkEnvelope,
1415
sessionEnvelope,
1516
transactionEnvelope,
1617
} from './utils';

test/e2e/test-apps/other/browser-profiling/package.json renamed to test/e2e/test-apps/other/browser-profiling-manual/package.json

File renamed without changes.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="UTF-8" />
5+
</head>
6+
<body>
7+
<script>
8+
const crypto = require('crypto');
9+
const { init, browserProfilingIntegration, startSpan, uiProfiler } = require('@sentry/electron/renderer');
10+
11+
init({
12+
debug: true,
13+
integrations: [browserProfilingIntegration()],
14+
tracesSampleRate: 1,
15+
profileSessionSampleRate: 1,
16+
profileLifecycle: 'manual',
17+
});
18+
19+
function pbkdf2() {
20+
return new Promise((resolve) => {
21+
const salt = crypto.randomBytes(128).toString('base64');
22+
crypto.pbkdf2('myPassword', salt, 10000, 512, 'sha512', resolve);
23+
});
24+
}
25+
26+
async function longWork() {
27+
for (let i = 0; i < 10; i++) {
28+
await startSpan({ name: 'PBKDF2' }, async () => {
29+
await pbkdf2();
30+
});
31+
}
32+
}
33+
34+
setTimeout(async () => {
35+
// Start UI profiling manually
36+
uiProfiler.startProfiler();
37+
38+
await startSpan({ name: 'Long work' }, async () => {
39+
await longWork();
40+
});
41+
42+
// Stop profiling - this will send a profile_chunk
43+
uiProfiler.stopProfiler();
44+
}, 500);
45+
</script>
46+
</body>
47+
</html>

test/e2e/test-apps/other/browser-profiling/src/main.js renamed to test/e2e/test-apps/other/browser-profiling-manual/src/main.js

File renamed without changes.

0 commit comments

Comments
 (0)