Skip to content

Commit

Permalink
feat: Identify webContents of renderers via custom protocol (#762)
Browse files Browse the repository at this point in the history
  • Loading branch information
timfish authored Oct 2, 2023
1 parent e52864a commit a6bf684
Show file tree
Hide file tree
Showing 11 changed files with 273 additions and 36 deletions.
6 changes: 5 additions & 1 deletion src/common/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export const PROTOCOL_SCHEME = 'sentry-ipc';

export enum IPCChannel {
/** IPC to check main process is listening */
PING = 'sentry-electron.ping',
RENDERER_START = 'sentry-electron.renderer-start',
/** IPC to send a captured event to Sentry. */
EVENT = 'sentry-electron.event',
/** IPC to pass scope changes to main process. */
Expand All @@ -12,17 +12,21 @@ export enum IPCChannel {
}

export interface IPCInterface {
sendRendererStart: () => void;
sendScope: (scope: string) => void;
sendEvent: (event: string) => void;
sendEnvelope: (evn: Uint8Array | string) => void;
}

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

/**
* We store the IPC interface on window so it's the same for both regular and isolated contexts
*/
declare global {
interface Window {
__SENTRY_IPC__?: IPCInterface;
__SENTRY__RENDERER_INIT__?: boolean;
__SENTRY_RENDERER_ID__?: string;
}
}
4 changes: 4 additions & 0 deletions src/main/electron-normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { parseSemver } from '@sentry/utils';
import { app, BrowserWindow, crashReporter, NativeImage, WebContents } from 'electron';
import { basename } from 'path';

import { RENDERER_ID_HEADER } from '../common/ipc';
import { Optional } from '../common/types';

const parsed = parseSemver(process.versions.electron);
Expand Down Expand Up @@ -219,6 +220,7 @@ function supportsProtocolHandle(): boolean {
}

interface InternalRequest {
windowId?: string;
url: string;
body?: Buffer;
}
Expand All @@ -236,6 +238,7 @@ export function registerProtocol(
if (supportsProtocolHandle()) {
protocol.handle(scheme, async (request) => {
callback({
windowId: request.headers.get(RENDERER_ID_HEADER) || undefined,
url: request.url,
body: Buffer.from(await request.arrayBuffer()),
});
Expand All @@ -246,6 +249,7 @@ export function registerProtocol(
// eslint-disable-next-line deprecation/deprecation
protocol.registerStringProtocol(scheme, (request, complete) => {
callback({
windowId: request.headers[RENDERER_ID_HEADER],
url: request.url,
body: request.uploadData?.[0]?.bytes,
});
Expand Down
63 changes: 58 additions & 5 deletions src/main/ipc.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,46 @@
import { captureEvent, configureScope, getCurrentHub, Scope } from '@sentry/core';
import { Attachment, AttachmentItem, Envelope, Event, EventItem } from '@sentry/types';
import { forEachEnvelopeItem, logger, parseEnvelope, SentryError } from '@sentry/utils';
import { app, ipcMain, protocol, WebContents } from 'electron';
import { app, ipcMain, protocol, WebContents, webContents } from 'electron';
import { TextDecoder, TextEncoder } from 'util';

import { IPCChannel, IPCMode, mergeEvents, normalizeUrlsInReplayEnvelope, PROTOCOL_SCHEME } from '../common';
import { registerProtocol, supportsFullProtocol, whenAppReady } from './electron-normalize';
import { ElectronMainOptionsInternal } from './sdk';

let KNOWN_RENDERERS: Set<number> | undefined;
let WINDOW_ID_TO_WEB_CONTENTS: Map<string, number> | undefined;

async function newProtocolRenderer(): Promise<void> {
KNOWN_RENDERERS = KNOWN_RENDERERS || new Set();
WINDOW_ID_TO_WEB_CONTENTS = WINDOW_ID_TO_WEB_CONTENTS || new Map();

for (const wc of webContents.getAllWebContents()) {
const wcId = wc.id;
if (KNOWN_RENDERERS.has(wcId)) {
continue;
}

if (!wc.isDestroyed()) {
try {
const windowId: string | undefined = await wc.executeJavaScript('window.__SENTRY_RENDERER_ID__');

if (windowId) {
KNOWN_RENDERERS.add(wcId);
WINDOW_ID_TO_WEB_CONTENTS.set(windowId, wcId);

wc.once('destroyed', () => {
KNOWN_RENDERERS?.delete(wcId);
WINDOW_ID_TO_WEB_CONTENTS?.delete(windowId);
});
}
} catch (_) {
// ignore
}
}
}
}

function captureEventFromRenderer(
options: ElectronMainOptionsInternal,
event: Event,
Expand Down Expand Up @@ -139,14 +172,20 @@ function configureProtocol(options: ElectronMainOptionsInternal): void {
.then(() => {
for (const sesh of options.getSessions()) {
registerProtocol(sesh.protocol, PROTOCOL_SCHEME, (request) => {
const data = request.body;
const getWebContents = (): WebContents | undefined => {
const webContentsId = request.windowId ? WINDOW_ID_TO_WEB_CONTENTS?.get(request.windowId) : undefined;
return webContentsId ? webContents.fromId(webContentsId) : undefined;
};

if (request.url.startsWith(`${PROTOCOL_SCHEME}://${IPCChannel.EVENT}`) && data) {
handleEvent(options, data.toString());
const data = request.body;
if (request.url.startsWith(`${PROTOCOL_SCHEME}://${IPCChannel.RENDERER_START}`)) {
void newProtocolRenderer();
} else if (request.url.startsWith(`${PROTOCOL_SCHEME}://${IPCChannel.EVENT}`) && data) {
handleEvent(options, data.toString(), getWebContents());
} else if (request.url.startsWith(`${PROTOCOL_SCHEME}://${IPCChannel.SCOPE}`) && data) {
handleScope(options, data.toString());
} else if (request.url.startsWith(`${PROTOCOL_SCHEME}://${IPCChannel.ENVELOPE}`) && data) {
handleEnvelope(options, data);
handleEnvelope(options, data, getWebContents());
}
});
}
Expand All @@ -158,6 +197,20 @@ function configureProtocol(options: ElectronMainOptionsInternal): void {
* Hooks IPC for communication with the renderer processes
*/
function configureClassic(options: ElectronMainOptionsInternal): void {
ipcMain.on(IPCChannel.RENDERER_START, ({ sender }) => {
const id = sender.id;

// In older Electron, sender can be destroyed before this callback is called
if (!sender.isDestroyed()) {
// Keep track of renderers that are using IPC
KNOWN_RENDERERS = KNOWN_RENDERERS || new Set();
KNOWN_RENDERERS.add(id);

sender.once('destroyed', () => {
KNOWN_RENDERERS?.delete(id);
});
}
});
ipcMain.on(IPCChannel.EVENT, ({ sender }, jsonEvent: string) => handleEvent(options, jsonEvent, sender));
ipcMain.on(IPCChannel.SCOPE, (_, jsonScope: string) => handleScope(options, jsonScope));
ipcMain.on(IPCChannel.ENVELOPE, ({ sender }, env: Uint8Array | string) => handleEnvelope(options, env, sender));
Expand Down
1 change: 1 addition & 0 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ if (window.__SENTRY_IPC__) {
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),
sendEvent: (eventJson: string) => ipcRenderer.send(IPCChannel.EVENT, eventJson),
sendEnvelope: (envelope: Uint8Array | string) => ipcRenderer.send(IPCChannel.ENVELOPE, envelope),
Expand Down
1 change: 1 addition & 0 deletions src/preload/legacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ if (window.__SENTRY_IPC__) {
});

const ipcObject = {
sendRendererStart: () => ipcRenderer.send(IPCChannel.RENDERER_START),
sendScope: (scopeJson: string) => ipcRenderer.send(IPCChannel.SCOPE, scopeJson),
sendEvent: (eventJson: string) => ipcRenderer.send(IPCChannel.EVENT, eventJson),
sendEnvelope: (envelope: Uint8Array | string) => ipcRenderer.send(IPCChannel.ENVELOPE, envelope),
Expand Down
71 changes: 41 additions & 30 deletions src/renderer/ipc.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,54 @@
/* eslint-disable no-restricted-globals */
/* eslint-disable no-console */
import { logger } from '@sentry/utils';
import { logger, uuid4 } from '@sentry/utils';

import { IPCChannel, IPCInterface, PROTOCOL_SCHEME } from '../common';
import { IPCChannel, IPCInterface, PROTOCOL_SCHEME, RENDERER_ID_HEADER } from '../common/ipc';

function buildUrl(channel: IPCChannel): string {
// We include sentry_key in the URL so these don't end up in fetch breadcrumbs
// https://github.com/getsentry/sentry-javascript/blob/a3f70d8869121183bec037571a3ee78efaf26b0b/packages/browser/src/integrations/breadcrumbs.ts#L240
return `${PROTOCOL_SCHEME}://${channel}/sentry_key`;
}

/** Gets the available IPC implementation */
function getImplementation(): IPCInterface {
// Favour IPC if it's been exposed by a preload script
if (window.__SENTRY_IPC__) {
return window.__SENTRY_IPC__;
}
} else {
logger.log('IPC was not configured in preload script, falling back to custom protocol and fetch');

logger.log('IPC was not configured in preload script, falling back to custom protocol and fetch');
// A unique ID used to identify this renderer and is send in the headers of every request
// Because it added as a global, this can be fetched from the main process via executeJavaScript
const id = (window.__SENTRY_RENDERER_ID__ = uuid4());
const headers: Record<string, string> = { [RENDERER_ID_HEADER]: id };

fetch(`${PROTOCOL_SCHEME}://${IPCChannel.PING}/sentry_key`, { method: 'POST', body: '' }).catch(() =>
console.error(`Sentry SDK failed to establish connection with the Electron main process.
- Ensure you have initialized the SDK in the main process
- If your renderers use custom sessions, be sure to set 'getSessions' in the main process options
- If you are bundling your main process code and using Electron < v5, you'll need to manually configure a preload script`),
);

// We include sentry_key in the URL so these dont end up in fetch breadcrumbs
// https://github.com/getsentry/sentry-javascript/blob/a3f70d8869121183bec037571a3ee78efaf26b0b/packages/browser/src/integrations/breadcrumbs.ts#L240
return {
sendScope: (body) => {
fetch(`${PROTOCOL_SCHEME}://${IPCChannel.SCOPE}/sentry_key`, { method: 'POST', body }).catch(() => {
// ignore
});
},
sendEvent: (body) => {
fetch(`${PROTOCOL_SCHEME}://${IPCChannel.EVENT}/sentry_key`, { method: 'POST', body }).catch(() => {
// ignore
});
},
sendEnvelope: (body) => {
fetch(`${PROTOCOL_SCHEME}://${IPCChannel.ENVELOPE}/sentry_key`, { method: 'POST', body }).catch(() => {
// ignore
});
},
};
return {
sendRendererStart: () => {
fetch(buildUrl(IPCChannel.RENDERER_START), { method: 'POST', body: '', headers }).catch(() => {
console.error(`Sentry SDK failed to establish connection with the Electron main process.
- Ensure you have initialized the SDK in the main process
- If your renderers use custom sessions, be sure to set 'getSessions' in the main process options
- If you are bundling your main process code and using Electron < v5, you'll need to manually configure a preload script`);
});
},
sendScope: (body: string) => {
fetch(buildUrl(IPCChannel.SCOPE), { method: 'POST', body, headers }).catch(() => {
// ignore
});
},
sendEvent: (body: string) => {
fetch(buildUrl(IPCChannel.EVENT), { method: 'POST', body, headers }).catch(() => {
// ignore
});
},
sendEnvelope: (body: string | Uint8Array) => {
fetch(buildUrl(IPCChannel.ENVELOPE), { method: 'POST', body, headers }).catch(() => {
// ignore
});
},
};
}
}

let cachedInterface: IPCInterface | undefined;
Expand All @@ -52,6 +62,7 @@ let cachedInterface: IPCInterface | undefined;
export function getIPC(): IPCInterface {
if (!cachedInterface) {
cachedInterface = getImplementation();
cachedInterface.sendRendererStart();
}

return cachedInterface;
Expand Down
106 changes: 106 additions & 0 deletions test/e2e/test-apps/other/custom-renderer-name-protocol/event.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
{
"method": "envelope",
"sentryKey": "37f8a2ee37c0409d8970bc7559c7c7e4",
"appId": "277345",
"data": {
"sdk": {
"name": "sentry.javascript.electron",
"packages": [
{
"name": "npm:@sentry/electron",
"version": "{{version}}"
}
],
"version": "{{version}}"
},
"contexts": {
"app": {
"app_name": "custom-renderer-name-protocol",
"app_version": "1.0.0",
"app_start_time": "{{time}}"
},
"browser": {
"name": "Chrome"
},
"chrome": {
"name": "Chrome",
"type": "runtime",
"version": "{{version}}"
},
"device": {
"arch": "{{arch}}",
"family": "Desktop",
"memory_size": 0,
"free_memory": 0,
"processor_count": 0,
"processor_frequency": 0,
"cpu_description": "{{cpu}}",
"screen_resolution":"{{screen}}",
"screen_density": 1,
"language": "{{language}}"
},
"node": {
"name": "Node",
"type": "runtime",
"version": "{{version}}"
},
"os": {
"name": "{{platform}}",
"version": "{{version}}"
},
"runtime": {
"name": "Electron",
"version": "{{version}}"
}
},
"release": "custom-renderer-name-protocol@1.0.0",
"environment": "development",
"user": {
"ip_address": "{{auto}}"
},
"exception": {
"values": [
{
"type": "Error",
"value": "Some renderer error",
"stacktrace": {
"frames": [
{
"colno": 0,
"filename": "app:///src/index.html",
"function": "{{function}}",
"in_app": true,
"lineno": 0
}
]
},
"mechanism": {
"handled": false,
"type": "instrument"
}
}
]
},
"level": "error",
"event_id": "{{id}}",
"platform": "javascript",
"timestamp": 0,
"breadcrumbs": [
{
"timestamp": 0,
"category": "electron",
"message": "SomeWindow.dom-ready",
"type": "ui"
}
],
"request": {
"url": "app:///src/index.html"
},
"tags": {
"event.environment": "javascript",
"event.origin": "electron",
"event.process": "SomeWindow",
"event_type": "javascript"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "custom-renderer-name-protocol",
"version": "1.0.0",
"main": "src/main.js",
"dependencies": {
"@sentry/electron": "3.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
description: Custom Renderer Name via Protocol
command: yarn
condition: version.major >= 5
Loading

0 comments on commit a6bf684

Please sign in to comment.