Skip to content

Commit 9ac67e5

Browse files
authored
feat: Support Electron utility process (#991)
1 parent c18f888 commit 9ac67e5

File tree

18 files changed

+600
-30
lines changed

18 files changed

+600
-30
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ stats.json
1616
/renderer
1717
/main
1818
/common
19+
/utility
1920
/index.*
2021
/integrations.*
2122
/ipc.*

.npmignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@
66
!/main/**/*
77
!/renderer/**/*
88
!/common/**/*
9+
!/utility/**/*
910
!/index.*
1011
!/integrations.*

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
"./preload": {
2525
"require": "./preload/index.js",
2626
"import": "./esm/preload/index.js"
27+
},
28+
"./utility": {
29+
"require": "./utility/index.js",
30+
"import": "./esm/utility/index.js"
2731
}
2832
},
2933
"repository": "https://github.com/getsentry/sentry-electron.git",

rollup.config.mjs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,12 @@ function bundlePreload(format, input, output) {
7171
}
7272

7373
export default [
74-
transpileFiles('cjs', ['src/index.ts', 'src/main/index.ts', 'src/renderer/index.ts'], '.'),
75-
transpileFiles('esm', ['src/index.ts', 'src/main/index.ts', 'src/renderer/index.ts'], './esm'),
74+
transpileFiles('cjs', ['src/index.ts', 'src/main/index.ts', 'src/renderer/index.ts', 'src/utility/index.ts'], '.'),
75+
transpileFiles(
76+
'esm',
77+
['src/index.ts', 'src/main/index.ts', 'src/renderer/index.ts', 'src/utility/index.ts'],
78+
'./esm',
79+
),
7680
bundlePreload('cjs', 'src/preload/index.ts', './preload/index.js'),
7781
bundlePreload('esm', 'src/preload/index.ts', './esm/preload/index.js'),
7882
];

scripts/check-exports.mjs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as browser from '@sentry/browser';
22
import * as renderer from '../esm/renderer/index.js';
3+
import * as utility from '../esm/utility/index.js';
34

45
import * as node from '@sentry/node';
56

@@ -13,6 +14,7 @@ const browserExports = Object.keys(browser);
1314
const rendererExports = Object.keys(renderer);
1415
const nodeExports = Object.keys(node);
1516
const mainExports = Object.keys(main);
17+
const utilityExports = Object.keys(utility);
1618

1719
const ignoredBrowser = [
1820
'SDK_VERSION',
@@ -46,10 +48,13 @@ const ignoredNode = [
4648
'initWithoutDefaultIntegrations',
4749
];
4850

51+
const ignoredUtility = [...ignoredNode, 'anrIntegration'];
52+
4953
const missingRenderer = browserExports.filter((key) => !rendererExports.includes(key) && !ignoredBrowser.includes(key));
5054
const missingMain = nodeExports.filter((key) => !mainExports.includes(key) && !ignoredNode.includes(key));
55+
const missingUtility = nodeExports.filter((key) => !utilityExports.includes(key) && !ignoredUtility.includes(key));
5156

52-
if (missingRenderer.length || missingMain.length) {
57+
if (missingRenderer.length || missingMain.length || missingUtility.length) {
5358
if (missingRenderer.length) {
5459
console.error('Missing renderer exports:', missingRenderer);
5560
}
@@ -58,5 +63,9 @@ if (missingRenderer.length || missingMain.length) {
5863
console.error('Missing main exports:', missingMain);
5964
}
6065

66+
if (missingUtility.length) {
67+
console.error('Missing utility exports:', missingUtility);
68+
}
69+
6170
process.exit(1);
6271
}

src/common/envelope.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Attachment, AttachmentItem, Envelope, Event, EventItem, Profile } from '@sentry/types';
2+
import { forEachEnvelopeItem } from '@sentry/utils';
3+
4+
/** Pulls an event and additional envelope items out of an envelope. Returns undefined if there was no event */
5+
export function eventFromEnvelope(envelope: Envelope): [Event, Attachment[], Profile | undefined] | undefined {
6+
let event: Event | undefined;
7+
const attachments: Attachment[] = [];
8+
let profile: Profile | undefined;
9+
10+
forEachEnvelopeItem(envelope, (item, type) => {
11+
if (type === 'event' || type === 'transaction' || type === 'feedback') {
12+
event = Array.isArray(item) ? (item as EventItem)[1] : undefined;
13+
} else if (type === 'attachment') {
14+
const [headers, data] = item as AttachmentItem;
15+
16+
attachments.push({
17+
filename: headers.filename,
18+
attachmentType: headers.attachment_type,
19+
contentType: headers.content_type,
20+
data,
21+
});
22+
} else if (type === 'profile') {
23+
profile = item[1] as unknown as Profile;
24+
}
25+
});
26+
27+
return event ? [event, attachments, profile] : undefined;
28+
}

src/common/ipc.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,18 @@ export interface IPCInterface {
7878

7979
export const RENDERER_ID_HEADER = 'sentry-electron-renderer-id';
8080

81+
const UTILITY_PROCESS_MAGIC_MESSAGE_KEY = '__sentry_message_port_message__';
82+
83+
/** Does the message look like the magic message */
84+
export function isMagicMessage(msg: unknown): boolean {
85+
return !!(msg && typeof msg === 'object' && UTILITY_PROCESS_MAGIC_MESSAGE_KEY in msg);
86+
}
87+
88+
/** Get the magic message to send to the utility process */
89+
export function getMagicMessage(): unknown {
90+
return { [UTILITY_PROCESS_MAGIC_MESSAGE_KEY]: true };
91+
}
92+
8193
/**
8294
* We store the IPC interface on window so it's the same for both regular and isolated contexts
8395
*/

src/main/ipc.ts

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { captureEvent, getClient, getCurrentScope, metrics } from '@sentry/node';
2-
import { Attachment, AttachmentItem, Envelope, Event, EventItem, Profile, ScopeData } from '@sentry/types';
3-
import { forEachEnvelopeItem, logger, parseEnvelope, SentryError } from '@sentry/utils';
2+
import { Attachment, Event, ScopeData } from '@sentry/types';
3+
import { logger, parseEnvelope, SentryError } from '@sentry/utils';
44
import { app, ipcMain, protocol, WebContents, webContents } from 'electron';
55

6+
import { eventFromEnvelope } from '../common/envelope';
67
import { IPCChannel, IPCMode, MetricIPCMessage, PROTOCOL_SCHEME, RendererStatus } from '../common/ipc';
78
import { createRendererAnrStatusHandler } from './anr';
89
import { registerProtocol } from './electron-normalize';
@@ -79,31 +80,6 @@ function handleEvent(options: ElectronMainOptionsInternal, jsonEvent: string, co
7980
captureEventFromRenderer(options, event, [], contents);
8081
}
8182

82-
function eventFromEnvelope(envelope: Envelope): [Event, Attachment[], Profile | undefined] | undefined {
83-
let event: Event | undefined;
84-
const attachments: Attachment[] = [];
85-
let profile: Profile | undefined;
86-
87-
forEachEnvelopeItem(envelope, (item, type) => {
88-
if (type === 'event' || type === 'transaction' || type === 'feedback') {
89-
event = Array.isArray(item) ? (item as EventItem)[1] : undefined;
90-
} else if (type === 'attachment') {
91-
const [headers, data] = item as AttachmentItem;
92-
93-
attachments.push({
94-
filename: headers.filename,
95-
attachmentType: headers.attachment_type,
96-
contentType: headers.content_type,
97-
data,
98-
});
99-
} else if (type === 'profile') {
100-
profile = item[1] as unknown as Profile;
101-
}
102-
});
103-
104-
return event ? [event, attachments, profile] : undefined;
105-
}
106-
10783
function handleEnvelope(options: ElectronMainOptionsInternal, env: Uint8Array | string, contents?: WebContents): void {
10884
const envelope = parseEnvelope(env);
10985

src/main/sdk.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { sentryMinidumpIntegration } from './integrations/sentry-minidump';
3636
import { configureIPC } from './ipc';
3737
import { defaultStackParser } from './stack-parse';
3838
import { ElectronOfflineTransportOptions, makeElectronOfflineTransport } from './transports/electron-offline-net';
39+
import { configureUtilityProcessIPC } from './utility-processes';
3940

4041
/** Get the default integrations for the main process SDK. */
4142
export function getDefaultIntegrations(options: ElectronMainOptions): Integration[] {
@@ -159,6 +160,7 @@ export function init(userOptions: ElectronMainOptions): void {
159160

160161
removeRedundantIntegrations(options);
161162
configureIPC(options);
163+
configureUtilityProcessIPC();
162164

163165
setNodeAsyncContextStrategy();
164166

src/main/utility-processes.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { captureEvent, getClient } from '@sentry/node';
2+
import { Attachment, Event } from '@sentry/types';
3+
import { logger, parseEnvelope } from '@sentry/utils';
4+
import * as electron from 'electron';
5+
6+
import { eventFromEnvelope } from '../common/envelope';
7+
import { getMagicMessage, isMagicMessage } from '../common/ipc';
8+
import { mergeEvents } from './merge';
9+
10+
function log(message: string): void {
11+
logger.log(`[Utility Process] ${message}`);
12+
}
13+
14+
/**
15+
* We wrap `electron.utilityProcess.fork` so we can pass a messageport to any SDK running in the utility process
16+
*/
17+
export function configureUtilityProcessIPC(): void {
18+
if (!electron.utilityProcess?.fork) {
19+
return;
20+
}
21+
22+
// eslint-disable-next-line @typescript-eslint/unbound-method
23+
electron.utilityProcess.fork = new Proxy(electron.utilityProcess.fork, {
24+
apply: (target, thisArg, args: Parameters<typeof electron.utilityProcess.fork>) => {
25+
// Call the underlying function to get the child process
26+
const child: electron.UtilityProcess = target.apply(thisArg, args);
27+
28+
function getProcessName(): string {
29+
const [, , options] = args;
30+
return options?.serviceName || `pid:${child.pid}`;
31+
}
32+
33+
// We don't send any messages unless we've heard from the child SDK. At that point we know it's ready to receive
34+
// and will also filter out any messages we send so users don't see them
35+
child.on('message', (msg: unknown) => {
36+
if (isMagicMessage(msg)) {
37+
log(`SDK started in utility process '${getProcessName()}'`);
38+
39+
const { port1, port2 } = new electron.MessageChannelMain();
40+
41+
port2.on('message', (msg) => {
42+
if (msg.data instanceof Uint8Array || typeof msg.data === 'string') {
43+
handleEnvelopeFromUtility(msg.data);
44+
}
45+
});
46+
port2.start();
47+
48+
// Send one side of the message port to the child SDK
49+
child.postMessage(getMagicMessage(), [port1]);
50+
}
51+
});
52+
53+
// We proxy child.on so we can filter messages from the child SDK and ensure that users do not see them
54+
// eslint-disable-next-line @typescript-eslint/unbound-method
55+
child.on = new Proxy(child.on, {
56+
apply: (target, thisArg, [event, listener]) => {
57+
if (event === 'message') {
58+
return target.apply(thisArg, [
59+
'message',
60+
(msg: unknown) => {
61+
if (isMagicMessage(msg)) {
62+
return;
63+
}
64+
65+
return listener(msg);
66+
},
67+
]);
68+
}
69+
70+
return target.apply(thisArg, [event, listener]);
71+
},
72+
});
73+
74+
return child;
75+
},
76+
});
77+
}
78+
79+
function handleEnvelopeFromUtility(env: Uint8Array | string): void {
80+
const envelope = parseEnvelope(env);
81+
82+
const eventAndAttachments = eventFromEnvelope(envelope);
83+
if (eventAndAttachments) {
84+
const [event, attachments] = eventAndAttachments;
85+
86+
captureEventFromUtility(event, attachments);
87+
} else {
88+
// Pass other types of envelope straight to the transport
89+
void getClient()?.getTransport()?.send(envelope);
90+
}
91+
}
92+
93+
function captureEventFromUtility(event: Event, attachments: Attachment[]): void {
94+
// Remove the environment as it defaults to 'production' and overwrites the main process environment
95+
delete event.environment;
96+
delete event.release;
97+
98+
// Remove the SDK info as we want the Electron SDK to be the one reporting the event
99+
delete event.sdk?.name;
100+
delete event.sdk?.version;
101+
delete event.sdk?.packages;
102+
103+
captureEvent(mergeEvents(event, { tags: { 'event.process': 'utility' } }), { attachments });
104+
}

0 commit comments

Comments
 (0)