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
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@
}
},
"./preload": {
"require": {
"types": "./preload/default.d.ts",
"default": "./preload/default.js"
},
"import": {
"types": "./esm/preload/default.d.ts",
"default": "./esm/preload/default.js"
}
},
"./preload-namespaced": {
"require": {
"types": "./preload/index.d.ts",
"default": "./preload/index.js"
Expand Down
5 changes: 3 additions & 2 deletions rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,12 @@ const entryPoints = [
'src/renderer/index.ts',
'src/utility/index.ts',
'src/native/index.ts',
'src/preload/index.ts',
];

export default [
transpileFiles('cjs', entryPoints, '.'),
transpileFiles('esm', entryPoints, './esm'),
bundlePreload('cjs', 'src/preload/index.ts', './preload/index.js'),
bundlePreload('esm', 'src/preload/index.ts', './esm/preload/index.js'),
bundlePreload('cjs', 'src/preload/default.ts', './preload/default.js'),
bundlePreload('esm', 'src/preload/default.ts', './esm/preload/default.js'),
];
41 changes: 32 additions & 9 deletions src/common/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,42 @@ export enum IPCMode {
Both = 3,
}

export const PROTOCOL_SCHEME = 'sentry-ipc';

export enum IPCChannel {
export type Channel =
/** IPC to check main process is listening */
RENDERER_START = 'sentry-electron.renderer-start',
| 'start'
/** IPC to pass scope changes to main process. */
SCOPE = 'sentry-electron.scope',
| 'scope'
/** IPC to pass envelopes to the main process. */
ENVELOPE = 'sentry-electron.envelope',
| 'envelope'
/** IPC to pass renderer status updates */
STATUS = 'sentry-electron.status',
| 'status'
/** IPC to pass structured log messages */
STRUCTURED_LOG = 'sentry-electron.structured-log',
| 'structured-log';

export interface IpcUtils {
createUrl: (channel: Channel) => string;
urlMatches: (url: string, channel: Channel) => boolean;
createKey: (channel: Channel) => string;
readonly namespace: string;
}

/**
* Utility for creating namespaced IPC channels and protocol routes
*/
export function ipcChannelUtils(namespace: string): IpcUtils {
return {
createUrl: (channel: Channel) => {
// sentry_key in the url stops these messages from being picked up by our HTTP instrumentations
return `${namespace}://${channel}/sentry_key`;
},
urlMatches: function (url: string, channel: Channel): boolean {
return url.startsWith(this.createUrl(channel));
},
createKey: (channel: Channel) => {
return `${namespace}.${channel}`;
},
namespace,
};
}

export interface RendererProcessAnrOptions {
Expand Down Expand Up @@ -83,7 +106,7 @@ export function getMagicMessage(): unknown {
*/
declare global {
interface Window {
__SENTRY_IPC__?: IPCInterface;
__SENTRY_IPC__?: Record<string, IPCInterface>;
__SENTRY__RENDERER_INIT__?: boolean;
__SENTRY_RENDERER_ID__?: string;
}
Expand Down
56 changes: 29 additions & 27 deletions src/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import { captureEvent, getClient, getCurrentScope } from '@sentry/node';
import { app, ipcMain, protocol, WebContents, webContents } from 'electron';
import { eventFromEnvelope } from '../common/envelope.js';
import { IPCChannel, IPCMode, PROTOCOL_SCHEME, RendererStatus } from '../common/ipc.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';
Expand All @@ -24,11 +24,6 @@ import { SDK_VERSION } from './version.js';
let KNOWN_RENDERERS: Set<number> | undefined;
let WINDOW_ID_TO_WEB_CONTENTS: Map<string, number> | undefined;

const SENTRY_CUSTOM_SCHEME = {
scheme: PROTOCOL_SCHEME,
privileges: { bypassCSP: true, corsEnabled: true, supportFetchAPI: true, secure: true },
};

function newProtocolRenderer(): void {
KNOWN_RENDERERS = KNOWN_RENDERERS || new Set();
WINDOW_ID_TO_WEB_CONTENTS = WINDOW_ID_TO_WEB_CONTENTS || new Map();
Expand Down Expand Up @@ -183,18 +178,23 @@ function handleLogFromRenderer(client: Client, options: ElectronMainOptionsInter
}

/** Enables Electron protocol handling */
function configureProtocol(client: Client, options: ElectronMainOptionsInternal): void {
function configureProtocol(client: Client, ipcUtil: IpcUtils, options: ElectronMainOptionsInternal): void {
if (app.isReady()) {
throw new Error("Sentry SDK should be initialized before the Electron app 'ready' event is fired");
}

protocol.registerSchemesAsPrivileged([SENTRY_CUSTOM_SCHEME]);
const scheme = {
scheme: ipcUtil.namespace,
privileges: { bypassCSP: true, corsEnabled: true, supportFetchAPI: true, secure: true },
};

protocol.registerSchemesAsPrivileged([scheme]);

// We Proxy this function so that later user calls to registerSchemesAsPrivileged don't overwrite our custom scheme
// eslint-disable-next-line @typescript-eslint/unbound-method
protocol.registerSchemesAsPrivileged = new Proxy(protocol.registerSchemesAsPrivileged, {
apply: (target, __, args: Parameters<typeof protocol.registerSchemesAsPrivileged>) => {
target([...args[0], SENTRY_CUSTOM_SCHEME]);
target([...args[0], scheme]);
},
});

Expand All @@ -204,26 +204,22 @@ function configureProtocol(client: Client, options: ElectronMainOptionsInternal)
.whenReady()
.then(() => {
for (const sesh of options.getSessions()) {
registerProtocol(sesh.protocol, PROTOCOL_SCHEME, (request) => {
registerProtocol(sesh.protocol, ipcUtil.namespace, (request) => {
const getWebContents = (): WebContents | undefined => {
const webContentsId = request.windowId ? WINDOW_ID_TO_WEB_CONTENTS?.get(request.windowId) : undefined;
return webContentsId ? webContents.fromId(webContentsId) : undefined;
};

const data = request.body;
if (request.url.startsWith(`${PROTOCOL_SCHEME}://${IPCChannel.RENDERER_START}`)) {
if (ipcUtil.urlMatches(request.url, 'start')) {
newProtocolRenderer();
} else if (request.url.startsWith(`${PROTOCOL_SCHEME}://${IPCChannel.SCOPE}`) && data) {
} else if (ipcUtil.urlMatches(request.url, 'scope') && data) {
handleScope(options, data.toString());
} else if (request.url.startsWith(`${PROTOCOL_SCHEME}://${IPCChannel.ENVELOPE}`) && data) {
} else if (ipcUtil.urlMatches(request.url, 'envelope') && data) {
handleEnvelope(client, options, data, getWebContents());
} else if (request.url.startsWith(`${PROTOCOL_SCHEME}://${IPCChannel.STRUCTURED_LOG}`) && data) {
} else if (ipcUtil.urlMatches(request.url, 'structured-log') && data) {
handleLogFromRenderer(client, options, JSON.parse(data.toString()));
} else if (
rendererStatusChanged &&
request.url.startsWith(`${PROTOCOL_SCHEME}://${IPCChannel.STATUS}`) &&
data
) {
} else if (rendererStatusChanged && ipcUtil.urlMatches(request.url, 'status') && data) {
const contents = getWebContents();
if (contents) {
const status = (JSON.parse(data.toString()) as { status: RendererStatus }).status;
Expand All @@ -239,8 +235,8 @@ function configureProtocol(client: Client, options: ElectronMainOptionsInternal)
/**
* Hooks IPC for communication with the renderer processes
*/
function configureClassic(client: Client, options: ElectronMainOptionsInternal): void {
ipcMain.on(IPCChannel.RENDERER_START, ({ sender }) => {
function configureClassic(client: Client, ipcUtil: IpcUtils, options: ElectronMainOptionsInternal): void {
ipcMain.on(ipcUtil.createKey('start'), ({ sender }) => {
const id = sender.id;
// Keep track of renderers that are using IPC
KNOWN_RENDERERS = KNOWN_RENDERERS || new Set();
Expand All @@ -258,27 +254,33 @@ function configureClassic(client: Client, options: ElectronMainOptionsInternal):
});
}
});
ipcMain.on(IPCChannel.SCOPE, (_, jsonScope: string) => handleScope(options, jsonScope));
ipcMain.on(IPCChannel.ENVELOPE, ({ sender }, env: Uint8Array | string) =>
ipcMain.on(ipcUtil.createKey('scope'), (_, jsonScope: string) => handleScope(options, jsonScope));
ipcMain.on(ipcUtil.createKey('envelope'), ({ sender }, env: Uint8Array | string) =>
handleEnvelope(client, options, env, sender),
);
ipcMain.on(IPCChannel.STRUCTURED_LOG, (_, log: SerializedLog) => handleLogFromRenderer(client, options, log));
ipcMain.on(ipcUtil.createKey('structured-log'), (_, log: SerializedLog) =>
handleLogFromRenderer(client, options, log),
);

const rendererStatusChanged = createRendererEventLoopBlockStatusHandler(client);
if (rendererStatusChanged) {
ipcMain.on(IPCChannel.STATUS, ({ sender }, status: RendererStatus) => rendererStatusChanged(status, sender));
ipcMain.on(ipcUtil.createKey('status'), ({ sender }, status: RendererStatus) =>
rendererStatusChanged(status, sender),
);
}
}

/** Sets up communication channels with the renderer */
export function configureIPC(client: Client, options: ElectronMainOptionsInternal): void {
const ipcUtil = ipcChannelUtils(options.ipcNamespace);

// eslint-disable-next-line no-bitwise
if ((options.ipcMode & IPCMode.Protocol) > 0) {
configureProtocol(client, options);
configureProtocol(client, ipcUtil, options);
}

// eslint-disable-next-line no-bitwise
if ((options.ipcMode & IPCMode.Classic) > 0) {
configureClassic(client, options);
configureClassic(client, ipcUtil, options);
}
}
18 changes: 16 additions & 2 deletions src/main/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,16 @@ export interface ElectronMainOptionsInternal
*/
ipcMode: IPCMode;

/**
* Custom namespace for IPC channels and protocol routes.
*
* Valid characters are a-z, 0-9, hyphen (-).
* Should match `ipcNamespace` passed in the renderer processes.
*
* @default "sentry-ipc"
*/
ipcNamespace: string;

/**
* A function that returns an array of Electron session objects
*
Expand Down Expand Up @@ -137,8 +147,11 @@ export interface ElectronMainOptionsInternal
}

// getSessions and ipcMode properties are optional because they have defaults
export type ElectronMainOptions = Pick<Partial<ElectronMainOptionsInternal>, 'getSessions' | 'ipcMode'> &
Omit<ElectronMainOptionsInternal, 'getSessions' | 'ipcMode'> &
export type ElectronMainOptions = Pick<
Partial<ElectronMainOptionsInternal>,
'getSessions' | 'ipcMode' | 'ipcNamespace'
> &
Omit<ElectronMainOptionsInternal, 'getSessions' | 'ipcMode' | 'ipcNamespace'> &
NodeOptions;

/**
Expand All @@ -154,6 +167,7 @@ export function init(userOptions: ElectronMainOptions): void {
const optionsWithDefaults = {
_metadata: { sdk: getSdkInfo(!!userOptions.sendDefaultPii) },
ipcMode: IPCMode.Both,
ipcNamespace: 'sentry-ipc',
release: getDefaultReleaseName(),
environment: getDefaultEnvironment(),
defaultIntegrations: getDefaultIntegrations(userOptions),
Expand Down
3 changes: 3 additions & 0 deletions src/preload/default.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { hookupIpc } from './index.js';

hookupIpc();
54 changes: 33 additions & 21 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,43 @@

import { SerializedLog } from '@sentry/core';
import { contextBridge, ipcRenderer } from 'electron';
import { IPCChannel, RendererStatus } from '../common/ipc.js';
import { ipcChannelUtils, RendererStatus } from '../common/ipc.js';

// eslint-disable-next-line no-restricted-globals
if (window.__SENTRY_IPC__) {
// eslint-disable-next-line no-console
console.log('Sentry Electron preload has already been run');
} else {
const ipcObject = {
sendRendererStart: () => ipcRenderer.send(IPCChannel.RENDERER_START),
sendScope: (scopeJson: string) => ipcRenderer.send(IPCChannel.SCOPE, scopeJson),
sendEnvelope: (envelope: Uint8Array | string) => ipcRenderer.send(IPCChannel.ENVELOPE, envelope),
sendStatus: (status: RendererStatus) => ipcRenderer.send(IPCChannel.STATUS, status),
sendStructuredLog: (log: SerializedLog) => ipcRenderer.send(IPCChannel.STRUCTURED_LOG, log),
};
/**
* Hook up IPC to the window object and uses contextBridge if available.
*
* @param namespace An optional namespace to use for the IPC channels
*/
export function hookupIpc(namespace: string = 'sentry-ipc'): void {
const ipcUtil = ipcChannelUtils(namespace);

// eslint-disable-next-line no-restricted-globals
window.__SENTRY_IPC__ = ipcObject;
window.__SENTRY_IPC__ = window.__SENTRY_IPC__ || {};

// eslint-disable-next-line no-restricted-globals
if (window.__SENTRY_IPC__[ipcUtil.namespace]) {
// eslint-disable-next-line no-console
console.log('Sentry Electron preload has already been run');
} else {
const ipcObject = {
sendRendererStart: () => ipcRenderer.send(ipcUtil.createKey('start')),
sendScope: (scopeJson: string) => ipcRenderer.send(ipcUtil.createKey('scope'), scopeJson),
sendEnvelope: (envelope: Uint8Array | string) => ipcRenderer.send(ipcUtil.createKey('envelope'), envelope),
sendStatus: (status: RendererStatus) => ipcRenderer.send(ipcUtil.createKey('status'), status),
sendStructuredLog: (log: SerializedLog) => ipcRenderer.send(ipcUtil.createKey('structured-log'), log),
};

// eslint-disable-next-line no-restricted-globals
window.__SENTRY_IPC__[ipcUtil.namespace] = ipcObject;

// We attempt to use contextBridge if it's available
if (contextBridge) {
// This will fail if contextIsolation is not enabled
try {
contextBridge.exposeInMainWorld('__SENTRY_IPC__', ipcObject);
} catch (e) {
//
// We attempt to use contextBridge if it's available
if (contextBridge) {
// This will fail if contextIsolation is not enabled
try {
contextBridge.exposeInMainWorld(ipcUtil.namespace, ipcObject);
} catch (e) {
//
}
}
}
}
4 changes: 2 additions & 2 deletions src/renderer/integrations/event-loop-block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ export const eventLoopBlockIntegration = defineIntegration((options?: Options) =

return {
name: 'EventLoopBlockRenderer',
setup() {
setup(client) {
const config: RendererProcessAnrOptions = {
pollInterval,
anrThreshold,
captureStackTrace: true,
...options,
};

const ipc = getIPC();
const ipc = getIPC(client);

// eslint-disable-next-line no-restricted-globals
ipc.sendStatus({ status: document.visibilityState, config });
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/integrations/scope-to-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import { getIPC } from '../ipc.js';
export const scopeToMainIntegration = defineIntegration(() => {
return {
name: 'ScopeToMain',
setup() {
const ipc = getIPC();
setup(client) {
const ipc = getIPC(client);

addScopeListener((merged, changed) => {
ipc.sendScope(JSON.stringify(normalize(merged, 20, 2_000)));
Expand Down
Loading
Loading