Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Identify webContents of renderers via custom protocol #762

Merged
merged 6 commits into from
Oct 2, 2023
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
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 @@ -215,6 +216,7 @@ function supportsProtocolHandle(): boolean {
}

interface InternalRequest {
windowId?: string;
url: string;
body?: Buffer;
}
Expand All @@ -232,6 +234,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 @@ -242,6 +245,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 @@ -14,6 +14,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 @@ -25,6 +25,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
Loading