Skip to content

Commit f2f88b5

Browse files
committed
feat: Add support for UI profiling
1 parent 3cf70b7 commit f2f88b5

11 files changed

Lines changed: 517 additions & 467 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: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
getCurrentScope,
88
normalizeUrlToBase,
99
Profile,
10+
ProfileChunk,
1011
ReplayEvent,
1112
} from '@sentry/core';
1213
import { createGetModuleFromFilename } from '@sentry/node';
@@ -120,3 +121,63 @@ export function normaliseProfile(profile: Profile, basePath: string): void {
120121
}
121122
}
122123
}
124+
125+
/**
126+
* Normalizes all URLs in a profile chunk and sets release/environment
127+
*/
128+
export function normaliseProfileChunk(
129+
profileChunk: ProfileChunk,
130+
basePath: string,
131+
options: ElectronMainOptionsInternal,
132+
): void {
133+
// Override release and environment with main process values
134+
if (options.release) {
135+
profileChunk.release = options.release;
136+
}
137+
if (options.environment) {
138+
profileChunk.environment = options.environment;
139+
}
140+
141+
// Normalize paths in profile frames
142+
const profile = profileChunk.profile as { frames?: Array<{ abs_path?: string; filename?: string; module?: string }> };
143+
if (profile?.frames) {
144+
for (const frame of profile.frames) {
145+
if (frame.abs_path) {
146+
frame.abs_path = normalizeUrlToBase(frame.abs_path, basePath);
147+
}
148+
149+
if (frame.filename) {
150+
frame.filename = normalizeUrlToBase(frame.filename, basePath);
151+
}
152+
153+
if (frame.module) {
154+
frame.module = getModuleFromFilename(frame.abs_path);
155+
}
156+
}
157+
}
158+
}
159+
160+
/**
161+
* Normalizes profile_chunk envelope items and returns the modified envelope
162+
*/
163+
export function normalizeProfileChunkEnvelope(
164+
options: ElectronMainOptionsInternal,
165+
envelope: Envelope,
166+
basePath: string,
167+
): Envelope {
168+
let modifiedEnvelope = createEnvelope(envelope[0]);
169+
let isProfileChunk = false;
170+
171+
forEachEnvelopeItem(envelope, (item, type) => {
172+
if (type === 'profile_chunk') {
173+
isProfileChunk = true;
174+
const [headers, chunk] = item as [{ type: 'profile_chunk' }, ProfileChunk];
175+
176+
normaliseProfileChunk(chunk, basePath, options);
177+
178+
modifiedEnvelope = addItemToEnvelope(modifiedEnvelope, [headers, chunk]);
179+
}
180+
});
181+
182+
return isProfileChunk ? modifiedEnvelope : envelope;
183+
}

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/src/index.html

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,14 @@
66
<body>
77
<script>
88
const crypto = require('crypto');
9-
const {
10-
init,
11-
browserProfilingIntegration,
12-
startSpan,
13-
} = require('@sentry/electron/renderer');
9+
const { init, browserProfilingIntegration, startSpan, uiProfiler } = require('@sentry/electron/renderer');
1410

1511
init({
1612
debug: true,
1713
integrations: [browserProfilingIntegration()],
1814
tracesSampleRate: 1,
19-
profilesSampleRate: 1,
15+
profileSessionSampleRate: 1,
16+
profileLifecycle: 'manual',
2017
});
2118

2219
function pbkdf2() {
@@ -34,10 +31,16 @@
3431
}
3532
}
3633

37-
setTimeout(() => {
38-
startSpan({ name: 'Long work' }, async () => {
34+
setTimeout(async () => {
35+
// Start UI profiling manually
36+
uiProfiler.startProfiler();
37+
38+
await startSpan({ name: 'Long work' }, async () => {
3939
await longWork();
4040
});
41+
42+
// Stop profiling - this will send a profile_chunk
43+
uiProfiler.stopProfiler();
4144
}, 500);
4245
</script>
4346
</body>

0 commit comments

Comments
 (0)