Skip to content

Commit

Permalink
feat: Support Electron utility process (#991)
Browse files Browse the repository at this point in the history
  • Loading branch information
timfish authored Oct 4, 2024
1 parent c18f888 commit 9ac67e5
Show file tree
Hide file tree
Showing 18 changed files with 600 additions and 30 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ stats.json
/renderer
/main
/common
/utility
/index.*
/integrations.*
/ipc.*
Expand Down
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
!/main/**/*
!/renderer/**/*
!/common/**/*
!/utility/**/*
!/index.*
!/integrations.*
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
"./preload": {
"require": "./preload/index.js",
"import": "./esm/preload/index.js"
},
"./utility": {
"require": "./utility/index.js",
"import": "./esm/utility/index.js"
}
},
"repository": "https://github.com/getsentry/sentry-electron.git",
Expand Down
8 changes: 6 additions & 2 deletions rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,12 @@ function bundlePreload(format, input, output) {
}

export default [
transpileFiles('cjs', ['src/index.ts', 'src/main/index.ts', 'src/renderer/index.ts'], '.'),
transpileFiles('esm', ['src/index.ts', 'src/main/index.ts', 'src/renderer/index.ts'], './esm'),
transpileFiles('cjs', ['src/index.ts', 'src/main/index.ts', 'src/renderer/index.ts', 'src/utility/index.ts'], '.'),
transpileFiles(
'esm',
['src/index.ts', 'src/main/index.ts', 'src/renderer/index.ts', 'src/utility/index.ts'],
'./esm',
),
bundlePreload('cjs', 'src/preload/index.ts', './preload/index.js'),
bundlePreload('esm', 'src/preload/index.ts', './esm/preload/index.js'),
];
11 changes: 10 additions & 1 deletion scripts/check-exports.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as browser from '@sentry/browser';
import * as renderer from '../esm/renderer/index.js';
import * as utility from '../esm/utility/index.js';

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

Expand All @@ -13,6 +14,7 @@ const browserExports = Object.keys(browser);
const rendererExports = Object.keys(renderer);
const nodeExports = Object.keys(node);
const mainExports = Object.keys(main);
const utilityExports = Object.keys(utility);

const ignoredBrowser = [
'SDK_VERSION',
Expand Down Expand Up @@ -46,10 +48,13 @@ const ignoredNode = [
'initWithoutDefaultIntegrations',
];

const ignoredUtility = [...ignoredNode, 'anrIntegration'];

const missingRenderer = browserExports.filter((key) => !rendererExports.includes(key) && !ignoredBrowser.includes(key));
const missingMain = nodeExports.filter((key) => !mainExports.includes(key) && !ignoredNode.includes(key));
const missingUtility = nodeExports.filter((key) => !utilityExports.includes(key) && !ignoredUtility.includes(key));

if (missingRenderer.length || missingMain.length) {
if (missingRenderer.length || missingMain.length || missingUtility.length) {
if (missingRenderer.length) {
console.error('Missing renderer exports:', missingRenderer);
}
Expand All @@ -58,5 +63,9 @@ if (missingRenderer.length || missingMain.length) {
console.error('Missing main exports:', missingMain);
}

if (missingUtility.length) {
console.error('Missing utility exports:', missingUtility);
}

process.exit(1);
}
28 changes: 28 additions & 0 deletions src/common/envelope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Attachment, AttachmentItem, Envelope, Event, EventItem, Profile } from '@sentry/types';
import { forEachEnvelopeItem } from '@sentry/utils';

/** 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 {
let event: Event | undefined;
const attachments: Attachment[] = [];
let profile: Profile | undefined;

forEachEnvelopeItem(envelope, (item, type) => {
if (type === 'event' || type === 'transaction' || type === 'feedback') {
event = Array.isArray(item) ? (item as EventItem)[1] : undefined;
} else if (type === 'attachment') {
const [headers, data] = item as AttachmentItem;

attachments.push({
filename: headers.filename,
attachmentType: headers.attachment_type,
contentType: headers.content_type,
data,
});
} else if (type === 'profile') {
profile = item[1] as unknown as Profile;
}
});

return event ? [event, attachments, profile] : undefined;
}
12 changes: 12 additions & 0 deletions src/common/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,18 @@ export interface IPCInterface {

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

const UTILITY_PROCESS_MAGIC_MESSAGE_KEY = '__sentry_message_port_message__';

/** Does the message look like the magic message */
export function isMagicMessage(msg: unknown): boolean {
return !!(msg && typeof msg === 'object' && UTILITY_PROCESS_MAGIC_MESSAGE_KEY in msg);
}

/** Get the magic message to send to the utility process */
export function getMagicMessage(): unknown {
return { [UTILITY_PROCESS_MAGIC_MESSAGE_KEY]: true };
}

/**
* We store the IPC interface on window so it's the same for both regular and isolated contexts
*/
Expand Down
30 changes: 3 additions & 27 deletions src/main/ipc.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { captureEvent, getClient, getCurrentScope, metrics } from '@sentry/node';
import { Attachment, AttachmentItem, Envelope, Event, EventItem, Profile, ScopeData } from '@sentry/types';
import { forEachEnvelopeItem, logger, parseEnvelope, SentryError } from '@sentry/utils';
import { Attachment, Event, ScopeData } from '@sentry/types';
import { logger, parseEnvelope, SentryError } from '@sentry/utils';
import { app, ipcMain, protocol, WebContents, webContents } from 'electron';

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

function eventFromEnvelope(envelope: Envelope): [Event, Attachment[], Profile | undefined] | undefined {
let event: Event | undefined;
const attachments: Attachment[] = [];
let profile: Profile | undefined;

forEachEnvelopeItem(envelope, (item, type) => {
if (type === 'event' || type === 'transaction' || type === 'feedback') {
event = Array.isArray(item) ? (item as EventItem)[1] : undefined;
} else if (type === 'attachment') {
const [headers, data] = item as AttachmentItem;

attachments.push({
filename: headers.filename,
attachmentType: headers.attachment_type,
contentType: headers.content_type,
data,
});
} else if (type === 'profile') {
profile = item[1] as unknown as Profile;
}
});

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

function handleEnvelope(options: ElectronMainOptionsInternal, env: Uint8Array | string, contents?: WebContents): void {
const envelope = parseEnvelope(env);

Expand Down
2 changes: 2 additions & 0 deletions src/main/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { sentryMinidumpIntegration } from './integrations/sentry-minidump';
import { configureIPC } from './ipc';
import { defaultStackParser } from './stack-parse';
import { ElectronOfflineTransportOptions, makeElectronOfflineTransport } from './transports/electron-offline-net';
import { configureUtilityProcessIPC } from './utility-processes';

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

removeRedundantIntegrations(options);
configureIPC(options);
configureUtilityProcessIPC();

setNodeAsyncContextStrategy();

Expand Down
104 changes: 104 additions & 0 deletions src/main/utility-processes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { captureEvent, getClient } from '@sentry/node';
import { Attachment, Event } from '@sentry/types';
import { logger, parseEnvelope } from '@sentry/utils';
import * as electron from 'electron';

import { eventFromEnvelope } from '../common/envelope';
import { getMagicMessage, isMagicMessage } from '../common/ipc';
import { mergeEvents } from './merge';

function log(message: string): void {
logger.log(`[Utility Process] ${message}`);
}

/**
* We wrap `electron.utilityProcess.fork` so we can pass a messageport to any SDK running in the utility process
*/
export function configureUtilityProcessIPC(): void {
if (!electron.utilityProcess?.fork) {
return;
}

// eslint-disable-next-line @typescript-eslint/unbound-method
electron.utilityProcess.fork = new Proxy(electron.utilityProcess.fork, {
apply: (target, thisArg, args: Parameters<typeof electron.utilityProcess.fork>) => {
// Call the underlying function to get the child process
const child: electron.UtilityProcess = target.apply(thisArg, args);

function getProcessName(): string {
const [, , options] = args;
return options?.serviceName || `pid:${child.pid}`;
}

// We don't send any messages unless we've heard from the child SDK. At that point we know it's ready to receive
// and will also filter out any messages we send so users don't see them
child.on('message', (msg: unknown) => {
if (isMagicMessage(msg)) {
log(`SDK started in utility process '${getProcessName()}'`);

const { port1, port2 } = new electron.MessageChannelMain();

port2.on('message', (msg) => {
if (msg.data instanceof Uint8Array || typeof msg.data === 'string') {
handleEnvelopeFromUtility(msg.data);
}
});
port2.start();

// Send one side of the message port to the child SDK
child.postMessage(getMagicMessage(), [port1]);
}
});

// We proxy child.on so we can filter messages from the child SDK and ensure that users do not see them
// eslint-disable-next-line @typescript-eslint/unbound-method
child.on = new Proxy(child.on, {
apply: (target, thisArg, [event, listener]) => {
if (event === 'message') {
return target.apply(thisArg, [
'message',
(msg: unknown) => {
if (isMagicMessage(msg)) {
return;
}

return listener(msg);
},
]);
}

return target.apply(thisArg, [event, listener]);
},
});

return child;
},
});
}

function handleEnvelopeFromUtility(env: Uint8Array | string): void {
const envelope = parseEnvelope(env);

const eventAndAttachments = eventFromEnvelope(envelope);
if (eventAndAttachments) {
const [event, attachments] = eventAndAttachments;

captureEventFromUtility(event, attachments);
} else {
// Pass other types of envelope straight to the transport
void getClient()?.getTransport()?.send(envelope);
}
}

function captureEventFromUtility(event: Event, attachments: Attachment[]): void {
// Remove the environment as it defaults to 'production' and overwrites the main process environment
delete event.environment;
delete event.release;

// Remove the SDK info as we want the Electron SDK to be the one reporting the event
delete event.sdk?.name;
delete event.sdk?.version;
delete event.sdk?.packages;

captureEvent(mergeEvents(event, { tags: { 'event.process': 'utility' } }), { attachments });
}
Loading

0 comments on commit 9ac67e5

Please sign in to comment.