From ea4333b4eeb6fbb635b6d19b9028829606f1e8ca Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 3 Nov 2021 18:25:28 +0000 Subject: [PATCH 1/7] Fallback to protocol --- examples/electron-forge-webpack/event.json | 5 +- examples/electron-forge-webpack/package.json | 5 +- examples/electron-forge-webpack/src/main.js | 2 - .../README.md | 7 + .../event.json | 139 ++++++++++++++++++ .../package.json | 18 +++ .../src/main.js | 33 +++++ .../src/renderer.js | 14 ++ .../webpack.config.js | 33 +++++ examples/webpack-context-isolation/event.json | 5 +- .../webpack-context-isolation/src/main.js | 1 - .../webpack.config.js | 9 -- package.json | 2 +- src/common/index.ts | 1 + src/common/ipc.ts | 14 +- src/common/mode.ts | 14 ++ src/index.ts | 2 + src/integrations.ts | 4 +- src/main/electron-normalize.ts | 23 +++ src/main/hook-ipc.ts | 97 ------------ src/main/index.ts | 1 + src/main/integrations/preload-injection.ts | 33 +++-- src/main/ipc/classic.ts | 13 ++ src/main/ipc/common.ts | 71 +++++++++ src/main/ipc/index.ts | 23 +++ src/main/ipc/protocol.ts | 39 +++++ src/main/sdk.ts | 31 +++- src/main/transports/electron-net.ts | 19 +-- src/preload/index.ts | 36 ++--- src/preload/legacy.ts | 56 +++---- src/renderer/integrations/event-to-main.ts | 6 +- src/renderer/integrations/index.ts | 1 - src/renderer/integrations/renderer-context.ts | 43 ------ src/renderer/integrations/scope-to-main.ts | 4 +- src/renderer/ipc.ts | 45 ++++++ src/renderer/sdk.ts | 17 +-- test/e2e/context.ts | 2 +- test/e2e/recipe/index.ts | 16 +- test/e2e/recipe/parser.ts | 4 +- .../renderer-error-browser-sdk/README.md | 6 - .../renderer-error-browser-sdk/package.json | 9 -- .../renderer-error-browser-sdk/src/index.html | 23 --- .../renderer-error-protocol/README.md | 7 + .../event.json | 18 +-- .../renderer-error-protocol/package.json | 8 + .../renderer-error-protocol/src/index.html | 19 +++ .../src/main.js | 3 +- .../other/error-after-ready/README.md | 7 + .../other/error-after-ready/package.json | 8 + .../other/error-after-ready/src/main.js | 18 +++ .../test-apps/other/error-no-main/README.md | 6 + .../other/error-no-main/package.json | 8 + .../other/error-no-main/src/index.html | 15 ++ .../test-apps/other/error-no-main/src/main.js | 19 +++ test/e2e/utils.ts | 15 +- yarn.lock | 2 +- 56 files changed, 744 insertions(+), 335 deletions(-) create mode 100644 examples/webpack-context-isolation-preload/README.md create mode 100644 examples/webpack-context-isolation-preload/event.json create mode 100644 examples/webpack-context-isolation-preload/package.json create mode 100644 examples/webpack-context-isolation-preload/src/main.js create mode 100644 examples/webpack-context-isolation-preload/src/renderer.js create mode 100644 examples/webpack-context-isolation-preload/webpack.config.js create mode 100644 src/common/mode.ts delete mode 100644 src/main/hook-ipc.ts create mode 100644 src/main/ipc/classic.ts create mode 100644 src/main/ipc/common.ts create mode 100644 src/main/ipc/index.ts create mode 100644 src/main/ipc/protocol.ts delete mode 100644 src/renderer/integrations/renderer-context.ts create mode 100644 src/renderer/ipc.ts delete mode 100644 test/e2e/test-apps/javascript/renderer-error-browser-sdk/README.md delete mode 100644 test/e2e/test-apps/javascript/renderer-error-browser-sdk/package.json delete mode 100644 test/e2e/test-apps/javascript/renderer-error-browser-sdk/src/index.html create mode 100644 test/e2e/test-apps/javascript/renderer-error-protocol/README.md rename test/e2e/test-apps/javascript/{renderer-error-browser-sdk => renderer-error-protocol}/event.json (82%) create mode 100644 test/e2e/test-apps/javascript/renderer-error-protocol/package.json create mode 100644 test/e2e/test-apps/javascript/renderer-error-protocol/src/index.html rename test/e2e/test-apps/javascript/{renderer-error-browser-sdk => renderer-error-protocol}/src/main.js (83%) create mode 100644 test/e2e/test-apps/other/error-after-ready/README.md create mode 100644 test/e2e/test-apps/other/error-after-ready/package.json create mode 100644 test/e2e/test-apps/other/error-after-ready/src/main.js create mode 100644 test/e2e/test-apps/other/error-no-main/README.md create mode 100644 test/e2e/test-apps/other/error-no-main/package.json create mode 100644 test/e2e/test-apps/other/error-no-main/src/index.html create mode 100644 test/e2e/test-apps/other/error-no-main/src/main.js diff --git a/examples/electron-forge-webpack/event.json b/examples/electron-forge-webpack/event.json index 8c4545a4..c5915a2f 100644 --- a/examples/electron-forge-webpack/event.json +++ b/examples/electron-forge-webpack/event.json @@ -45,8 +45,7 @@ "version": "{{version}}" }, "electron": { - "crashed_process": "WebContents[1]", - "crashed_url": "app:///.webpack/renderer/main_window/index.html" + "crashed_process": "renderer" } }, "release": "electron-forge-webpack@1.0.0", @@ -135,4 +134,4 @@ "event_type": "javascript" } } -} \ No newline at end of file +} diff --git a/examples/electron-forge-webpack/package.json b/examples/electron-forge-webpack/package.json index 2e5ce3cb..4b33fc3c 100644 --- a/examples/electron-forge-webpack/package.json +++ b/examples/electron-forge-webpack/package.json @@ -18,10 +18,7 @@ { "html": "./src/index.html", "js": "./src/renderer.js", - "name": "main_window", - "preload": { - "js": "@sentry/electron/preload" - } + "name": "main_window" } ] } diff --git a/examples/electron-forge-webpack/src/main.js b/examples/electron-forge-webpack/src/main.js index 747fa63c..bbd161d3 100644 --- a/examples/electron-forge-webpack/src/main.js +++ b/examples/electron-forge-webpack/src/main.js @@ -17,8 +17,6 @@ const createWindow = () => { const mainWindow = new BrowserWindow({ show: false, webPreferences: { - // eslint-disable-next-line no-undef - preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY, nodeIntegration: false, contextIsolation: true, sandbox: true, diff --git a/examples/webpack-context-isolation-preload/README.md b/examples/webpack-context-isolation-preload/README.md new file mode 100644 index 00000000..0467fb97 --- /dev/null +++ b/examples/webpack-context-isolation-preload/README.md @@ -0,0 +1,7 @@ +# Webpack 5 app with contextIsolation and sandbox with preload + +| Setting | Value | +| ------------- | ------------------------ | +| Build Command | yarn && yarn build | +| Run Condition | supportsContextIsolation | +| Timeout | 120s | diff --git a/examples/webpack-context-isolation-preload/event.json b/examples/webpack-context-isolation-preload/event.json new file mode 100644 index 00000000..9a53cb1e --- /dev/null +++ b/examples/webpack-context-isolation-preload/event.json @@ -0,0 +1,139 @@ +{ + "method": "envelope", + "sentryKey": "37f8a2ee37c0409d8970bc7559c7c7e4", + "appId": "277345", + "dumpFile": false, + "data": { + "sdk": { + "name": "sentry.javascript.electron", + "packages": [ + { + "name": "npm:@sentry/electron", + "version": "{{version}}" + } + ], + "version": "{{version}}" + }, + "contexts": { + "app": { + "app_name": "webpack-context-isolation-preload", + "app_version": "1.0.0" + }, + "browser": { + "name": "Chrome" + }, + "chrome": { + "name": "Chrome", + "type": "runtime", + "version": "{{version}}" + }, + "device": { + "arch": "{{arch}}", + "family": "Desktop" + }, + "node": { + "name": "Node", + "type": "runtime", + "version": "{{version}}" + }, + "os": { + "name": "{{platform}}", + "version": "{{version}}" + }, + "runtime": { + "name": "Electron", + "version": "{{version}}" + }, + "electron": { + "crashed_process": "WebContents[1]", + "crashed_url": "app:///dist/index.html" + } + }, + "release": "webpack-context-isolation-preload@1.0.0", + "environment": "production", + "user": { + "ip_address": "{{auto}}", + "id": "abc-123" + }, + "exception": { + "values": [ + { + "type": "Error", + "value": "Some renderer error", + "stacktrace": { + "frames": [ + { + "colno": 0, + "filename": "app:///dist/renderer.js", + "function": "{{function}}", + "in_app": true, + "lineno": 0 + }, + { + "colno": 0, + "filename": "app:///dist/renderer.js", + "function": "{{function}}", + "in_app": true, + "lineno": 0 + } + ] + }, + "mechanism": { + "handled": true, + "type": "generic" + } + } + ] + }, + "level": "error", + "event_id": "{{id}}", + "platform": "javascript", + "timestamp": 0, + "breadcrumbs": [ + { + "timestamp": 0, + "category": "electron", + "message": "app.will-finish-launching", + "type": "ui" + }, + { + "timestamp": 0, + "category": "electron", + "message": "app.ready", + "type": "ui" + }, + { + "timestamp": 0, + "category": "electron", + "message": "app.session-created", + "type": "ui" + }, + { + "timestamp": 0, + "category": "electron", + "message": "app.web-contents-created", + "type": "ui" + }, + { + "timestamp": 0, + "category": "electron", + "message": "app.browser-window-created", + "type": "ui" + }, + { + "timestamp": 0, + "category": "electron", + "message": "WebContents[1].dom-ready", + "type": "ui" + } + ], + "request": { + "url": "app:///dist/index.html" + }, + "tags": { + "event.environment": "javascript", + "event.origin": "electron", + "event_type": "javascript" + } + } +} diff --git a/examples/webpack-context-isolation-preload/package.json b/examples/webpack-context-isolation-preload/package.json new file mode 100644 index 00000000..5837b7d2 --- /dev/null +++ b/examples/webpack-context-isolation-preload/package.json @@ -0,0 +1,18 @@ +{ + "name": "webpack-context-isolation-preload", + "version": "1.0.0", + "scripts": { + "start": "electron .", + "build": "webpack" + }, + "main": "dist/main.js", + "devDependencies": { + "html-webpack-plugin": "^5.3.2", + "webpack": "^5.48.0", + "webpack-cli": "^4.7.2", + "warnings-to-errors-webpack-plugin": "^2.0.1" + }, + "dependencies": { + "@sentry/electron": "3.0.0" + } +} diff --git a/examples/webpack-context-isolation-preload/src/main.js b/examples/webpack-context-isolation-preload/src/main.js new file mode 100644 index 00000000..a0f50d4f --- /dev/null +++ b/examples/webpack-context-isolation-preload/src/main.js @@ -0,0 +1,33 @@ +import * as path from 'path'; +import * as url from 'url'; + +import { app, BrowserWindow } from 'electron'; +// eslint-disable-next-line import/no-unresolved +import { init } from '@sentry/electron'; + +init({ + dsn: '__DSN__', + debug: true, + autoSessionTracking: false, + onFatalError: () => {}, +}); + +app.on('ready', () => { + const window = new BrowserWindow({ + show: false, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + nodeIntegration: false, + contextIsolation: true, + sandbox: true, + }, + }); + + window.loadURL( + url.format({ + pathname: path.join(__dirname, 'index.html'), + protocol: 'file:', + slashes: true, + }), + ); +}); diff --git a/examples/webpack-context-isolation-preload/src/renderer.js b/examples/webpack-context-isolation-preload/src/renderer.js new file mode 100644 index 00000000..71f56898 --- /dev/null +++ b/examples/webpack-context-isolation-preload/src/renderer.js @@ -0,0 +1,14 @@ +// eslint-disable-next-line import/no-unresolved +import { init, configureScope } from '@sentry/electron'; + +init({ + debug: true, +}); + +configureScope((scope) => { + scope.setUser({ id: 'abc-123' }); +}); + +setTimeout(() => { + throw new Error('Some renderer error'); +}, 500); diff --git a/examples/webpack-context-isolation-preload/webpack.config.js b/examples/webpack-context-isolation-preload/webpack.config.js new file mode 100644 index 00000000..c9f420ea --- /dev/null +++ b/examples/webpack-context-isolation-preload/webpack.config.js @@ -0,0 +1,33 @@ +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const WarningsToErrorsPlugin = require('warnings-to-errors-webpack-plugin'); + +module.exports = [ + { + mode: 'production', + entry: './src/main.js', + target: 'electron-main', + output: { + libraryTarget: 'commonjs2', + filename: 'main.js', + }, + plugins: [new WarningsToErrorsPlugin()], + }, + { + mode: 'production', + entry: '@sentry/electron/preload', + target: 'electron-preload', + output: { + filename: 'preload.js', + }, + plugins: [new WarningsToErrorsPlugin()], + }, + { + mode: 'production', + entry: './src/renderer.js', + target: 'web', + output: { + filename: 'renderer.js', + }, + plugins: [new HtmlWebpackPlugin(), new WarningsToErrorsPlugin()], + }, +]; diff --git a/examples/webpack-context-isolation/event.json b/examples/webpack-context-isolation/event.json index 84b5f340..9e80c4f8 100644 --- a/examples/webpack-context-isolation/event.json +++ b/examples/webpack-context-isolation/event.json @@ -45,8 +45,7 @@ "version": "{{version}}" }, "electron": { - "crashed_process": "WebContents[1]", - "crashed_url": "app:///dist/index.html" + "crashed_process": "renderer" } }, "release": "webpack-context-isolation@1.0.0", @@ -136,4 +135,4 @@ "event_type": "javascript" } } -} \ No newline at end of file +} diff --git a/examples/webpack-context-isolation/src/main.js b/examples/webpack-context-isolation/src/main.js index a0f50d4f..3d4960bd 100644 --- a/examples/webpack-context-isolation/src/main.js +++ b/examples/webpack-context-isolation/src/main.js @@ -16,7 +16,6 @@ app.on('ready', () => { const window = new BrowserWindow({ show: false, webPreferences: { - preload: path.join(__dirname, 'preload.js'), nodeIntegration: false, contextIsolation: true, sandbox: true, diff --git a/examples/webpack-context-isolation/webpack.config.js b/examples/webpack-context-isolation/webpack.config.js index c9f420ea..7ead98b1 100644 --- a/examples/webpack-context-isolation/webpack.config.js +++ b/examples/webpack-context-isolation/webpack.config.js @@ -12,15 +12,6 @@ module.exports = [ }, plugins: [new WarningsToErrorsPlugin()], }, - { - mode: 'production', - entry: '@sentry/electron/preload', - target: 'electron-preload', - output: { - filename: 'preload.js', - }, - plugins: [new WarningsToErrorsPlugin()], - }, { mode: 'production', entry: './src/renderer.js', diff --git a/package.json b/package.json index 0f707898..cb7dd512 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "@types/koa-bodyparser": "^4.3.0", "@types/mocha": "^9.0.0", "busboy": "^0.3.1", - "chai": "^4.1.2", + "chai": "^4.3.4", "chai-as-promised": "^7.1.1", "chai-subset": "^1.6.0", "cross-env": "^7.0.3", diff --git a/src/common/index.ts b/src/common/index.ts index f4a6e7f0..62f75142 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -2,3 +2,4 @@ export * from './ipc'; export * from './normalize'; export * from './walk'; export * from './merge'; +export * from './mode'; diff --git a/src/common/ipc.ts b/src/common/ipc.ts index 8a1791a5..c90a5f4f 100644 --- a/src/common/ipc.ts +++ b/src/common/ipc.ts @@ -1,23 +1,17 @@ -import { Event } from '@sentry/types'; +export const PROTOCOL_SCHEME = 'sentry-ipc'; -export enum IPC { +export enum IPCChannel { + /** IPC to check main process is listening */ + PING = 'sentry-electron.ping', /** IPC to send a captured event to Sentry. */ EVENT = 'sentry-electron.event', /** IPC to capture scope globally. */ SCOPE = 'sentry-electron.scope', - /** IPC to get Electron scope in renderer */ - CONTEXT = 'sentry-electron.context', -} - -export interface AppContext { - eventDefaults: Event; - appBasePath: string; } export interface IPCInterface { sendScope: (scope: string) => void; sendEvent: (event: string) => void; - getContext: (callback: (context: string) => void) => void; } /** diff --git a/src/common/mode.ts b/src/common/mode.ts new file mode 100644 index 00000000..369d19dd --- /dev/null +++ b/src/common/mode.ts @@ -0,0 +1,14 @@ +/** Ways to communicate between the renderer and main process */ +export enum IPCMode { + /** Configures Electron IPC to receive messages from renderers */ + Classic = 1, + /** Configures Electron protocol module to receive messages from renderers */ + Protocol = 2, + /** + * Configures both methods for best compatibility. + * + * Renderers favour IPC but fall back to protocol if IPC has not + * been configured in a preload script + */ + Both = 3, +} diff --git a/src/index.ts b/src/index.ts index 8d2379cd..71d957e9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -50,6 +50,8 @@ export interface ElectronOptions extends ElectronMainOptions, BrowserOptions { // } +export { IPCMode } from './common'; + /** * The Sentry Electron SDK Client. * diff --git a/src/integrations.ts b/src/integrations.ts index cf396e22..3df52772 100644 --- a/src/integrations.ts +++ b/src/integrations.ts @@ -11,7 +11,7 @@ import { SentryMinidump, MainProcessSession, } from './main/integrations'; -import { EventToMain, RendererContext, ScopeToMain } from './renderer/integrations'; +import { EventToMain, ScopeToMain } from './renderer/integrations'; /** Convenience interface used to expose Integrations */ export interface Integrations { @@ -26,7 +26,6 @@ export interface Integrations { // For renderer process ScopeToMain: ScopeToMain; EventToMain: EventToMain; - RendererContext: RendererContext; } /** Return all Electron integrations and add EmptyIntegrations for integrations missing in this process. */ @@ -37,7 +36,6 @@ export function getIntegrations(): Integrations { ...dynamicRequire(module, './main').Integrations, ScopeToMain: EmptyIntegration, EventToMain: EmptyIntegration, - RendererContext: EmptyIntegration, } : { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access diff --git a/src/main/electron-normalize.ts b/src/main/electron-normalize.ts index d57403dd..3a09a45f 100644 --- a/src/main/electron-normalize.ts +++ b/src/main/electron-normalize.ts @@ -4,6 +4,22 @@ import { app, crashReporter, RenderProcessGoneDetails, WebContents } from 'elect const parsed = parseSemver(process.versions.electron); const version = { major: parsed.major || 0, minor: parsed.minor || 0, patch: parsed.patch || 0 }; +/** + * Returns a promise that resolves when app is ready. + */ +async function appIsReady(): Promise { + return app.isReady() + ? Promise.resolve() + : new Promise((resolve) => { + app.once('ready', () => { + resolve(); + }); + }); +} + +/** A promise that is resolved when the app is ready */ +export const whenAppReady: Promise = appIsReady(); + /** * Electron >=8.4 | >=9.1 | >=10 * Use `render-process-gone` rather than `crashed` @@ -14,6 +30,13 @@ function supportsRenderProcessGone(): boolean { ); } +/** + * Electron >= 5 support full protocol API + */ +export function supportsFullProtocol(): boolean { + return version.major >= 5; +} + /** * Implements 'render-process-gone' event across Electron versions */ diff --git a/src/main/hook-ipc.ts b/src/main/hook-ipc.ts deleted file mode 100644 index 6f020886..00000000 --- a/src/main/hook-ipc.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { captureEvent, configureScope, Scope } from '@sentry/core'; -import { Event } from '@sentry/types'; -import { logger } from '@sentry/utils'; -import { app, ipcMain, WebContents } from 'electron'; - -import { AppContext, IPC, normalizeUrl, walk } from '../common'; -import { getEventDefaults } from './context'; -import { ElectronMainOptions } from './sdk'; - -/** - * Handle events from the renderer processes - */ -function handleEvent(jsonEvent: string, contents: WebContents, options?: ElectronMainOptions): void { - let event: Event; - try { - event = JSON.parse(jsonEvent) as Event; - } catch { - logger.warn('sentry-electron received an invalid IPC_EVENT message'); - return; - } - - if (event.exception) { - event.contexts = { - electron: { - crashed_process: options?.getRendererName?.(contents) || `WebContents[${contents.id}]`, - crashed_url: normalizeUrl(contents.getURL(), app.getAppPath()), - }, - ...event.contexts, - }; - } - - captureEvent(event); -} - -/** Is object defined and has keys */ -function hasKeys(obj: any): boolean { - return obj != undefined && Object.keys(obj).length > 0; -} - -/** - * Handle scope updates from renderer processes - */ -function handleScope(jsonScope: string, options?: ElectronMainOptions): void { - let rendererScope: Scope; - try { - rendererScope = JSON.parse(jsonScope) as Scope; - } catch { - logger.warn('sentry-electron received an invalid IPC_SCOPE message'); - return; - } - - const sentScope = Scope.clone(rendererScope) as any; - /* eslint-disable @typescript-eslint/no-unsafe-member-access */ - configureScope((scope) => { - if (hasKeys(sentScope._user)) { - scope.setUser(sentScope._user); - } - - if (hasKeys(sentScope._tags)) { - scope.setTags(sentScope._tags); - } - - if (hasKeys(sentScope._extra)) { - scope.setExtras(sentScope._extra); - } - - scope.addBreadcrumb(sentScope._breadcrumbs.pop(), options?.maxBreadcrumbs || 100); - }); - /* eslint-enable @typescript-eslint/no-unsafe-member-access */ -} - -/** - * Gets the Electron context and passes it to the renderer - * - * This allows the browser SDK to be used in isolation and still include Electron context and normalise paths - */ -async function handleContext(contents: WebContents, options?: ElectronMainOptions): Promise { - const eventDefaults = await getEventDefaults(options?.release); - - const crashed_process = options?.getRendererName?.(contents) || `WebContents[${contents.id}]`; - eventDefaults.contexts = { - ...eventDefaults.contexts, - electron: { ...eventDefaults.contexts?.electron, crashed_process }, - }; - - const context: AppContext = { eventDefaults, appBasePath: app.getAppPath() }; - contents.send(IPC.CONTEXT, JSON.stringify(context, walk)); -} - -/** - * Hooks IPC for communication with the renderer processes - */ -export function hookIPC(options: ElectronMainOptions): void { - ipcMain.on(IPC.EVENT, (event, jsonEvent: string) => handleEvent(jsonEvent, event.sender, options)); - ipcMain.on(IPC.SCOPE, (_, jsonScope: string) => handleScope(jsonScope, options)); - ipcMain.on(IPC.CONTEXT, (event) => handleContext(event.sender, options)); -} diff --git a/src/main/index.ts b/src/main/index.ts index 13776939..ddd1a994 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -46,3 +46,4 @@ export { ElectronNetTransport } from './transports/electron-net'; export const Integrations = { ...ElectronMainIntegrations, ...NodeIntegrations }; export { init, ElectronMainOptions, defaultIntegrations } from './sdk'; +export { IPCMode } from '../common'; diff --git a/src/main/integrations/preload-injection.ts b/src/main/integrations/preload-injection.ts index c70b998e..18106399 100644 --- a/src/main/integrations/preload-injection.ts +++ b/src/main/integrations/preload-injection.ts @@ -1,14 +1,13 @@ +import { getCurrentHub } from '@sentry/core'; +import { NodeClient } from '@sentry/node'; import { Integration } from '@sentry/types'; import { logger } from '@sentry/utils'; -import { app, Session, session } from 'electron'; +import { app } from 'electron'; import { existsSync } from 'fs'; +import { IPCMode } from '../../common'; import { rendererRequiresCrashReporterStart } from '../electron-normalize'; - -interface PreloadInjectionOptions { - /** Function that fetches the sessions that should have preloads injected */ - sessions?: () => Session[]; -} +import { ElectronMainOptions } from '../sdk'; /** * Injects the preload script into the provided sessions. @@ -22,21 +21,25 @@ export class PreloadInjection implements Integration { /** @inheritDoc */ public name: string = PreloadInjection.id; - public constructor( - private readonly _options: PreloadInjectionOptions = { sessions: () => [session.defaultSession] }, - ) {} - /** @inheritDoc */ public setupOnce(): void { + const options = getCurrentHub().getClient()?.getOptions() as ElectronMainOptions; + + // If classic IPC mode is disabled, we shouldn't attempt to inject preload scripts + // eslint-disable-next-line no-bitwise + if ((options.ipcMode & IPCMode.Classic) == 0) { + return; + } + app.once('ready', () => { - this._addPreloadToSessions(); + this._addPreloadToSessions(options); }); } /** * Attempts to add the preload script the the provided sessions */ - private _addPreloadToSessions(): void { + private _addPreloadToSessions(options: ElectronMainOptions): void { let path = undefined; try { path = rendererRequiresCrashReporterStart() @@ -46,14 +49,14 @@ export class PreloadInjection implements Integration { // } - if (this._options.sessions && path && typeof path === 'string' && existsSync(path)) { - for (const sesh of this._options.sessions()) { + if (path && typeof path === 'string' && existsSync(path)) { + for (const sesh of options.getSessions()) { // Fetch any existing preloads so we don't overwrite them const existing = sesh.getPreloads(); sesh.setPreloads([path, ...existing]); } } else { - logger.warn( + logger.log( 'The preload script could not be injected automatically. This is most likely caused by bundling of the main process', ); } diff --git a/src/main/ipc/classic.ts b/src/main/ipc/classic.ts new file mode 100644 index 00000000..25f6fa37 --- /dev/null +++ b/src/main/ipc/classic.ts @@ -0,0 +1,13 @@ +import { ipcMain } from 'electron'; + +import { IPCChannel } from '../../common'; +import { ElectronMainOptions } from '../sdk'; +import { handleEvent, handleScope } from './common'; + +/** + * Hooks IPC for communication with the renderer processes + */ +export function configure(options: ElectronMainOptions): void { + ipcMain.on(IPCChannel.EVENT, ({ sender }, jsonEvent: string) => handleEvent(options, jsonEvent, sender)); + ipcMain.on(IPCChannel.SCOPE, (_, jsonScope: string) => handleScope(options, jsonScope)); +} diff --git a/src/main/ipc/common.ts b/src/main/ipc/common.ts new file mode 100644 index 00000000..4c132e77 --- /dev/null +++ b/src/main/ipc/common.ts @@ -0,0 +1,71 @@ +import { captureEvent, configureScope, Scope } from '@sentry/core'; +import { Event } from '@sentry/types'; +import { logger } from '@sentry/utils'; +import { app, WebContents } from 'electron'; + +import { normalizeUrl } from '../../common'; +import { ElectronMainOptions } from '../sdk'; + +/** + * Handle events from the renderer processes + */ +export function handleEvent(options: ElectronMainOptions, jsonEvent: string, contents?: WebContents): void { + let event: Event; + try { + event = JSON.parse(jsonEvent) as Event; + } catch { + logger.warn('sentry-electron received an invalid event message'); + return; + } + + if (event.exception) { + event.contexts = { + ...event.contexts, + electron: contents + ? { + crashed_process: options?.getRendererName?.(contents) || `WebContents[${contents.id}]`, + crashed_url: normalizeUrl(contents.getURL(), app.getAppPath()), + } + : { crashed_process: 'renderer' }, + }; + } + + captureEvent(event); +} + +/** Is object defined and has keys */ +function hasKeys(obj: any): boolean { + return obj != undefined && Object.keys(obj).length > 0; +} + +/** + * Handle scope updates from renderer processes + */ +export function handleScope(options: ElectronMainOptions, jsonScope: string): void { + let rendererScope: Scope; + try { + rendererScope = JSON.parse(jsonScope) as Scope; + } catch { + logger.warn('sentry-electron received an invalid scope message'); + return; + } + + const sentScope = Scope.clone(rendererScope) as any; + /* eslint-disable @typescript-eslint/no-unsafe-member-access */ + configureScope((scope) => { + if (hasKeys(sentScope._user)) { + scope.setUser(sentScope._user); + } + + if (hasKeys(sentScope._tags)) { + scope.setTags(sentScope._tags); + } + + if (hasKeys(sentScope._extra)) { + scope.setExtras(sentScope._extra); + } + + scope.addBreadcrumb(sentScope._breadcrumbs.pop(), options?.maxBreadcrumbs || 100); + }); + /* eslint-enable @typescript-eslint/no-unsafe-member-access */ +} diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts new file mode 100644 index 00000000..c966026f --- /dev/null +++ b/src/main/ipc/index.ts @@ -0,0 +1,23 @@ +import { SentryError } from '@sentry/utils'; +import { IPCMode } from '../../common'; +import { supportsFullProtocol } from '../electron-normalize'; +import { ElectronMainOptions } from '../sdk'; +import { configure as configureClassic } from './classic'; +import { configure as configureProtocol } from './protocol'; + +/** Sets up communication channels with the renderer */ +export function configureIPC(options: ElectronMainOptions): void { + if (!supportsFullProtocol() && options.ipcMode === IPCMode.Protocol) { + throw new SentryError('IPCMode.Protocol is only supported in Electron >= v5'); + } + + // eslint-disable-next-line no-bitwise + if (supportsFullProtocol() && (options.ipcMode & IPCMode.Protocol) > 0) { + configureProtocol(options); + } + + // eslint-disable-next-line no-bitwise + if ((options.ipcMode & IPCMode.Classic) > 0) { + configureClassic(options); + } +} diff --git a/src/main/ipc/protocol.ts b/src/main/ipc/protocol.ts new file mode 100644 index 00000000..a6f52138 --- /dev/null +++ b/src/main/ipc/protocol.ts @@ -0,0 +1,39 @@ +import { forget, SentryError } from '@sentry/utils'; +import { app, protocol } from 'electron'; + +import { IPCChannel, PROTOCOL_SCHEME } from '../../common'; +import { whenAppReady } from '../electron-normalize'; +import { ElectronMainOptions } from '../sdk'; +import { handleEvent, handleScope } from './common'; + +/** Enables Electron protocol handling */ +export function configure(options: ElectronMainOptions): void { + if (app.isReady()) { + throw new SentryError("Sentry SDK should be initialized before the Electron app 'ready' event is fired"); + } + + protocol.registerSchemesAsPrivileged([ + { + scheme: PROTOCOL_SCHEME, + privileges: { bypassCSP: true, supportFetchAPI: true }, + }, + ]); + + forget( + whenAppReady.then(() => { + for (const sesh of options.getSessions()) { + sesh.protocol.registerStringProtocol(PROTOCOL_SCHEME, (request, callback) => { + const data = request.uploadData?.[0]?.bytes.toString(); + + if (request.url.startsWith(`${PROTOCOL_SCHEME}://${IPCChannel.EVENT}`) && data) { + handleEvent(options, data); + } else if (request.url.startsWith(`${PROTOCOL_SCHEME}://${IPCChannel.SCOPE}`) && data) { + handleScope(options, data); + } + + callback(''); + }); + } + }), + ); +} diff --git a/src/main/sdk.ts b/src/main/sdk.ts index a3b1373e..edda6f28 100644 --- a/src/main/sdk.ts +++ b/src/main/sdk.ts @@ -1,9 +1,9 @@ import { defaultIntegrations as defaultNodeIntegrations, init as nodeInit, NodeOptions } from '@sentry/node'; import { Integration } from '@sentry/types'; -import { WebContents } from 'electron'; +import { Session, session, WebContents } from 'electron'; +import { IPCMode } from '../common'; import { getDefaultReleaseName } from './context'; -import { hookIPC } from './hook-ipc'; import { ElectronEvents, MainContext, @@ -12,6 +12,7 @@ import { PreloadInjection, SentryMinidump, } from './integrations'; +import { configureIPC } from './ipc'; import { ElectronNetTransport } from './transports/electron-net'; export const defaultIntegrations: Integration[] = [ @@ -25,17 +26,37 @@ export const defaultIntegrations: Integration[] = [ export interface ElectronMainOptions extends NodeOptions { /** - * Callback to allow custom naming of renderer processes + * Inter-process communication mode + */ + ipcMode: IPCMode; + /** + * Callback to allow custom naming of renderer processes. + * * If the callback is not set, or it returns `undefined`, the default naming * scheme is used. */ getRendererName?: (contents: WebContents) => string | undefined; + /** + * A function that returns an array of Electron session objects + * + * These sessions are used to configure communication between the Electron + * main and renderer processes. + * + * Defaults to () => [session.defaultSession] + */ + getSessions: () => Session[]; } +const defaultOptions: ElectronMainOptions = { + ipcMode: IPCMode.Both, + getSessions: () => [session.defaultSession], +}; + /** * Initialize Sentry in the Electron main process */ -export function init(options: ElectronMainOptions): void { +export function init(partialOptions: Partial): void { + const options: ElectronMainOptions = Object.assign(defaultOptions, partialOptions); const defaults = defaultIntegrations; // If we don't set a release, @sentry/node will automatically fetch from environment variables @@ -57,7 +78,7 @@ export function init(options: ElectronMainOptions): void { options.transport = ElectronNetTransport; } - hookIPC(options); + configureIPC(options); nodeInit(options); } diff --git a/src/main/transports/electron-net.ts b/src/main/transports/electron-net.ts index 9cd148a9..0fcd96b0 100644 --- a/src/main/transports/electron-net.ts +++ b/src/main/transports/electron-net.ts @@ -10,26 +10,13 @@ import { TransportOptions, } from '@sentry/types'; import { logger, PromiseBuffer, SentryError } from '@sentry/utils'; -import { app, net } from 'electron'; +import { net } from 'electron'; import { Readable, Writable } from 'stream'; import * as url from 'url'; import { createGzip } from 'zlib'; import { getSdkInfo } from '../context'; - -/** - * Returns a promise that resolves when app is ready. - */ -async function isAppReady(): Promise { - return ( - app.isReady() || - new Promise((resolve) => { - app.once('ready', () => { - resolve(true); - }); - }) - ); -} +import { whenAppReady } from '../electron-normalize'; // Estimated maximum size for reasonable standalone event const GZIP_THRESHOLD = 1024 * 32; @@ -130,7 +117,7 @@ export class ElectronNetTransport extends Transports.BaseTransport { return Promise.reject(new SentryError('Not adding Promise due to buffer limit reached.')); } - await isAppReady(); + await whenAppReady; const options = this._getRequestOptions(new url.URL(request.url)); options.headers = { diff --git a/src/preload/index.ts b/src/preload/index.ts index 8b92129d..08bf91d0 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -6,25 +6,27 @@ import { contextBridge, ipcRenderer } from 'electron'; -import { IPC } from '../common/ipc'; +import { IPCChannel } from '../common/ipc'; -const ipcObject = { - sendScope: (scopeJson: string) => ipcRenderer.send(IPC.SCOPE, scopeJson), - sendEvent: (eventJson: string) => ipcRenderer.send(IPC.EVENT, eventJson), - getContext: (callback: (eventJson: string) => void) => { - ipcRenderer.once(IPC.CONTEXT, (_, json) => callback(json)); - ipcRenderer.send(IPC.CONTEXT); - }, -}; +// 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 = { + sendScope: (scopeJson: string) => ipcRenderer.send(IPCChannel.SCOPE, scopeJson), + sendEvent: (eventJson: string) => ipcRenderer.send(IPCChannel.EVENT, eventJson), + }; -window.__SENTRY_IPC__ = ipcObject; + window.__SENTRY_IPC__ = 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('__SENTRY_IPC__', ipcObject); + } catch (e) { + // + } } } diff --git a/src/preload/legacy.ts b/src/preload/legacy.ts index 01d7e7e4..5a572aa9 100644 --- a/src/preload/legacy.ts +++ b/src/preload/legacy.ts @@ -7,36 +7,38 @@ import { contextBridge, crashReporter, ipcRenderer } from 'electron'; import * as electron from 'electron'; -import { IPC } from '../common/ipc'; +import { IPCChannel } from '../common/ipc'; -crashReporter.start({ - companyName: '', - ignoreSystemCrashHandler: true, - // This script is only ever used for Electron < v9 where the remote module is available - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - productName: (electron as any).remote.app.name || (electron as any).remote.app.getName(), - submitURL: '', - uploadToServer: false, -}); +// 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 { + crashReporter.start({ + companyName: '', + ignoreSystemCrashHandler: true, + // This script is only ever used for Electron < v9 where the remote module is available + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + productName: (electron as any).remote.app.name || (electron as any).remote.app.getName(), + submitURL: '', + uploadToServer: false, + }); -const ipcObject = { - sendScope: (scopeJson: string) => ipcRenderer.send(IPC.SCOPE, scopeJson), - sendEvent: (eventJson: string) => ipcRenderer.send(IPC.EVENT, eventJson), - getContext: (callback: (eventJson: string) => void) => { - ipcRenderer.once(IPC.CONTEXT, (_, json) => callback(json)); - ipcRenderer.send(IPC.CONTEXT); - }, -}; + const ipcObject = { + sendScope: (scopeJson: string) => ipcRenderer.send(IPCChannel.SCOPE, scopeJson), + sendEvent: (eventJson: string) => ipcRenderer.send(IPCChannel.EVENT, eventJson), + }; -// eslint-disable-next-line no-restricted-globals -window.__SENTRY_IPC__ = ipcObject; + // eslint-disable-next-line no-restricted-globals + window.__SENTRY_IPC__ = 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('__SENTRY_IPC__', ipcObject); + } catch (e) { + // + } } } diff --git a/src/renderer/integrations/event-to-main.ts b/src/renderer/integrations/event-to-main.ts index 3dace554..46e289b7 100644 --- a/src/renderer/integrations/event-to-main.ts +++ b/src/renderer/integrations/event-to-main.ts @@ -1,6 +1,7 @@ import { Event, EventProcessor, Integration } from '@sentry/types'; import { walk } from '../../common'; +import { IPC } from '../ipc'; /** * Passes events to the main process. @@ -14,11 +15,12 @@ export class EventToMain implements Integration { /** @inheritDoc */ public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void { - addGlobalEventProcessor((event: Event) => { + addGlobalEventProcessor(async (event: Event) => { // Ensure breadcrumbs is not `undefined` as `walk` translates it into a string event.breadcrumbs = event.breadcrumbs || []; + // eslint-disable-next-line no-restricted-globals - window.__SENTRY_IPC__?.sendEvent(JSON.stringify(event, walk)); + await IPC.sendEvent(JSON.stringify(event, walk)); // Events are handled and sent from the main process so we return null here so nothing is sent from the renderer return null; }); diff --git a/src/renderer/integrations/index.ts b/src/renderer/integrations/index.ts index ffe8d972..4b5b55f7 100644 --- a/src/renderer/integrations/index.ts +++ b/src/renderer/integrations/index.ts @@ -1,3 +1,2 @@ export { ScopeToMain } from './scope-to-main'; export { EventToMain } from './event-to-main'; -export { RendererContext } from './renderer-context'; diff --git a/src/renderer/integrations/renderer-context.ts b/src/renderer/integrations/renderer-context.ts deleted file mode 100644 index ba212024..00000000 --- a/src/renderer/integrations/renderer-context.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Event, EventProcessor, Integration } from '@sentry/types'; - -import { AppContext, mergeEvents, normalizeEvent } from '../../common'; - -/** - * Fetches context from the main process so the browser SDK can be used in isolation - */ -export class RendererContext implements Integration { - /** @inheritDoc */ - public static id: string = 'RendererContext'; - - /** @inheritDoc */ - public name: string = RendererContext.id; - - /** Caches Electron context */ - private _appContext?: Promise; - - /** @inheritDoc */ - public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void { - addGlobalEventProcessor((event: Event) => { - return this._addContextToEvent(event); - }); - } - - /** Adds Electron context to an event */ - private async _addContextToEvent(event: Event): Promise { - if (!this._appContext) { - this._appContext = this._getContextFromMain(); - } - - const appContext = await this._appContext; - - return mergeEvents(appContext.eventDefaults, normalizeEvent(event, appContext.appBasePath)); - } - - /** Asynchronously fetches context from the main process */ - private async _getContextFromMain(): Promise { - return new Promise((resolve) => { - // eslint-disable-next-line no-restricted-globals - window.__SENTRY_IPC__?.getContext((event) => resolve(JSON.parse(event))); - }); - } -} diff --git a/src/renderer/integrations/scope-to-main.ts b/src/renderer/integrations/scope-to-main.ts index 6ce6e6cf..588566f3 100644 --- a/src/renderer/integrations/scope-to-main.ts +++ b/src/renderer/integrations/scope-to-main.ts @@ -2,6 +2,7 @@ import { getCurrentHub } from '@sentry/core'; import { Integration } from '@sentry/types'; import { walk } from '../../common'; +import { IPC } from '../ipc'; /** * Passes scope changes to the main process. @@ -25,8 +26,7 @@ export class ScopeToMain implements Integration { const scope = getCurrentHub().getScope(); if (scope) { scope.addScopeListener((updatedScope) => { - // eslint-disable-next-line no-restricted-globals - window.__SENTRY_IPC__?.sendScope(JSON.stringify(updatedScope, walk)); + IPC.sendScope(JSON.stringify(updatedScope, walk)); scope.clearBreadcrumbs(); }); } diff --git a/src/renderer/ipc.ts b/src/renderer/ipc.ts new file mode 100644 index 00000000..103d32c2 --- /dev/null +++ b/src/renderer/ipc.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-restricted-globals */ +/* eslint-disable no-console */ +import { logger } from '@sentry/utils'; + +import { IPCChannel, IPCInterface, PROTOCOL_SCHEME } from '../common'; + +/** 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__; + } + + logger.log('IPC was not configured in preload script, falling back to custom protocol and fetch'); + + fetch(`${PROTOCOL_SCHEME}://${IPCChannel.PING}`).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: (scope) => { + fetch(`${PROTOCOL_SCHEME}://${IPCChannel.SCOPE}/sentry_key`, { method: 'POST', body: scope }).catch(() => { + // ignore + }); + }, + sendEvent: (event) => { + fetch(`${PROTOCOL_SCHEME}://${IPCChannel.EVENT}/sentry_key`, { method: 'POST', body: event }).catch(() => { + // ignore + }); + }, + }; +} + +/** + * Renderer IPC interface + * + * Favours IPC if its been exposed via a preload script but will + * fallback to custom protocol and fetch is IPC is not available + */ +export const IPC: IPCInterface = getImplementation(); diff --git a/src/renderer/sdk.ts b/src/renderer/sdk.ts index 86e07603..bb071761 100644 --- a/src/renderer/sdk.ts +++ b/src/renderer/sdk.ts @@ -4,7 +4,7 @@ import { defaultIntegrations as defaultBrowserIntegrations, init as browserInit, } from '@sentry/browser'; -import { logger, SentryError } from '@sentry/utils'; +import { logger } from '@sentry/utils'; import { EventToMain, ScopeToMain } from './integrations'; @@ -23,21 +23,6 @@ If init has been called in the preload and contextIsolation is disabled, is not window.__SENTRY__RENDERER_INIT__ = true; - if (window.__SENTRY_IPC__ === undefined) { - throw new SentryError(`Communication with the Electron main process could not be established. - -This is likely because the preload script was not run. -Preload scripts are usually injected automatically but this can fail if you are bundling the Electron main process code. - -The required preload code can be imported via: - require('@sentry/electron/preload'); -or - import '@sentry/electron/preload'; - -Check out the Webpack example for how to configure this: -https://github.com/getsentry/sentry-electron/blob/master/examples/webpack-context-isolation.md`); - } - // We don't want browser session tracking enabled by default because we already have Electron // specific session tracking if (options.autoSessionTracking === undefined) { diff --git a/test/e2e/context.ts b/test/e2e/context.ts index c0134926..4e1dfc7f 100644 --- a/test/e2e/context.ts +++ b/test/e2e/context.ts @@ -55,7 +55,7 @@ export class TestContext { const env: Record = { ...process.env, - ELECTRON_ENABLE_LOGGING: process.env.DEBUG, + ELECTRON_ENABLE_LOGGING: true, ELECTRON_DISABLE_SECURITY_WARNINGS: true, }; diff --git a/test/e2e/recipe/index.ts b/test/e2e/recipe/index.ts index 5147a44c..0ce27a50 100644 --- a/test/e2e/recipe/index.ts +++ b/test/e2e/recipe/index.ts @@ -5,7 +5,7 @@ import { expect } from 'chai'; import { SDK_VERSION } from '../../../src/main/version'; import { TestServer } from '../server'; -import { createLogger, walkSync } from '../utils'; +import { createLogger, getTestLog, walkSync } from '../utils'; import { normalize } from './normalize'; import { parseRecipe, TestRecipe } from './parser'; import { TestContext } from '../context'; @@ -36,7 +36,7 @@ export function getCategorisedTestRecipes(electronVersion: string): Record { - const cat = cur.category || 'Other Features'; + const cat = cur.category || 'Other'; if (obj[cat]) { obj[cat].push(cur); } else { @@ -159,13 +159,23 @@ export class RecipeRunner { throw new Error(`Expected ${expectedEvents.length} events but server has ${testServer.events.length} events`); } + // Checks the app output for an expected error string + if (this._recipe.metadata.expectedError) { + // if there are no expected events, at least wait until the app closes + if (expectedEvents.length === 0) { + await context.waitForAppClose(); + } + + const log = getTestLog().join(' '); + expect(log).to.include(this._recipe.metadata.expectedError); + } + for (const event of testServer.events) { event.data = normalize(event.data); } for (const [i, expectedEvent] of expectedEvents.entries()) { delete expectedEvent.condition; - log(`Comparing event ${i + 1} of ${expectedEvents.length}`); expect(testServer.events).to.containSubset([expectedEvent]); } diff --git a/test/e2e/recipe/parser.ts b/test/e2e/recipe/parser.ts index ce2beb79..215eb6dd 100644 --- a/test/e2e/recipe/parser.ts +++ b/test/e2e/recipe/parser.ts @@ -14,6 +14,7 @@ export interface TestMetadata { command?: string; timeout?: number; runTwice?: boolean; + expectedError?: string; } export interface TestRecipe { @@ -46,10 +47,11 @@ function parseMetadata(doc: string): TestMetadata { const condition = getTableValue(doc, 'run condition'); const command = getTableValue(doc, 'build command'); const timeoutStr = getTableValue(doc, 'timeout'); + const expectedError = getTableValue(doc, 'expected error'); const timeout = timeoutStr ? parseInt(timeoutStr.replace('s', '000')) : undefined; const runTwice = !!getTableValue(doc, 'run twice'); - return { description, category, command, condition, timeout, runTwice }; + return { description, category, command, condition, timeout, runTwice, expectedError }; } function isEventOrSession(path: string): boolean { diff --git a/test/e2e/test-apps/javascript/renderer-error-browser-sdk/README.md b/test/e2e/test-apps/javascript/renderer-error-browser-sdk/README.md deleted file mode 100644 index ce1305d7..00000000 --- a/test/e2e/test-apps/javascript/renderer-error-browser-sdk/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# JavaScript Renderer Error Browser SDK - -| Setting | Value | -| ------------- | ---------- | -| Category | JavaScript | -| Build Command | yarn | diff --git a/test/e2e/test-apps/javascript/renderer-error-browser-sdk/package.json b/test/e2e/test-apps/javascript/renderer-error-browser-sdk/package.json deleted file mode 100644 index 362d1396..00000000 --- a/test/e2e/test-apps/javascript/renderer-error-browser-sdk/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "javascript-renderer-browser-sdk", - "version": "1.0.0", - "main": "src/main.js", - "dependencies": { - "@sentry/electron": "3.0.0", - "@sentry/browser": "6.13.2" - } -} diff --git a/test/e2e/test-apps/javascript/renderer-error-browser-sdk/src/index.html b/test/e2e/test-apps/javascript/renderer-error-browser-sdk/src/index.html deleted file mode 100644 index 1054affc..00000000 --- a/test/e2e/test-apps/javascript/renderer-error-browser-sdk/src/index.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - diff --git a/test/e2e/test-apps/javascript/renderer-error-protocol/README.md b/test/e2e/test-apps/javascript/renderer-error-protocol/README.md new file mode 100644 index 00000000..17ec2952 --- /dev/null +++ b/test/e2e/test-apps/javascript/renderer-error-protocol/README.md @@ -0,0 +1,7 @@ +# JavaScript Renderer Error via Protocol + +| Setting | Value | +| ------------- | ------------------ | +| Category | JavaScript | +| Build Command | yarn | +| Run Condition | version.major >= 5 | diff --git a/test/e2e/test-apps/javascript/renderer-error-browser-sdk/event.json b/test/e2e/test-apps/javascript/renderer-error-protocol/event.json similarity index 82% rename from test/e2e/test-apps/javascript/renderer-error-browser-sdk/event.json rename to test/e2e/test-apps/javascript/renderer-error-protocol/event.json index 1b336336..f3864db2 100644 --- a/test/e2e/test-apps/javascript/renderer-error-browser-sdk/event.json +++ b/test/e2e/test-apps/javascript/renderer-error-protocol/event.json @@ -1,5 +1,5 @@ { - "method": "store", + "method": "envelope", "sentryKey": "37f8a2ee37c0409d8970bc7559c7c7e4", "appId": "277345", "dumpFile": false, @@ -10,17 +10,13 @@ { "name": "npm:@sentry/electron", "version": "{{version}}" - }, - { - "name": "npm:@sentry/browser", - "version": "{{version}}" } ], "version": "{{version}}" }, "contexts": { "app": { - "app_name": "javascript-renderer-browser-sdk", + "app_name": "javascript-renderer-protocol", "app_version": "1.0.0" }, "browser": { @@ -49,10 +45,10 @@ "version": "{{version}}" }, "electron": { - "crashed_process": "WebContents[1]" + "crashed_process": "renderer" } }, - "release": "javascript-renderer-browser-sdk@1.0.0", + "release": "javascript-renderer-protocol@1.0.0", "environment": "production", "user": { "ip_address": "{{auto}}" @@ -74,11 +70,8 @@ ] }, "mechanism": { - "data": { - "function": "setTimeout" - }, "handled": true, - "type": "instrument" + "type": "generic" } } ] @@ -87,6 +80,7 @@ "event_id": "{{id}}", "platform": "javascript", "timestamp": 0, + "breadcrumbs": [], "request": { "url": "app:///src/index.html" }, diff --git a/test/e2e/test-apps/javascript/renderer-error-protocol/package.json b/test/e2e/test-apps/javascript/renderer-error-protocol/package.json new file mode 100644 index 00000000..89be62dd --- /dev/null +++ b/test/e2e/test-apps/javascript/renderer-error-protocol/package.json @@ -0,0 +1,8 @@ +{ + "name": "javascript-renderer-protocol", + "version": "1.0.0", + "main": "src/main.js", + "dependencies": { + "@sentry/electron": "3.0.0" + } +} diff --git a/test/e2e/test-apps/javascript/renderer-error-protocol/src/index.html b/test/e2e/test-apps/javascript/renderer-error-protocol/src/index.html new file mode 100644 index 00000000..dd1d6fc5 --- /dev/null +++ b/test/e2e/test-apps/javascript/renderer-error-protocol/src/index.html @@ -0,0 +1,19 @@ + + + + + + + + + \ No newline at end of file diff --git a/test/e2e/test-apps/javascript/renderer-error-browser-sdk/src/main.js b/test/e2e/test-apps/javascript/renderer-error-protocol/src/main.js similarity index 83% rename from test/e2e/test-apps/javascript/renderer-error-browser-sdk/src/main.js rename to test/e2e/test-apps/javascript/renderer-error-protocol/src/main.js index bede8faa..72ca4f53 100644 --- a/test/e2e/test-apps/javascript/renderer-error-browser-sdk/src/main.js +++ b/test/e2e/test-apps/javascript/renderer-error-protocol/src/main.js @@ -1,11 +1,12 @@ const path = require('path'); const { app, BrowserWindow } = require('electron'); -const { init } = require('@sentry/electron'); +const { init, IPCMode } = require('@sentry/electron'); init({ dsn: '__DSN__', debug: true, + ipcMode: IPCMode.Protocol, autoSessionTracking: false, onFatalError: () => {}, }); diff --git a/test/e2e/test-apps/other/error-after-ready/README.md b/test/e2e/test-apps/other/error-after-ready/README.md new file mode 100644 index 00000000..56637aa5 --- /dev/null +++ b/test/e2e/test-apps/other/error-after-ready/README.md @@ -0,0 +1,7 @@ +# Error thrown if init after app ready + +| Setting | Value | +| -------------- | ----------------------------------------------------- | +| Build Command | yarn | +| Expected Error | should be initialized before the Electron app 'ready' | +| Run Condition | version.major >= 5 | diff --git a/test/e2e/test-apps/other/error-after-ready/package.json b/test/e2e/test-apps/other/error-after-ready/package.json new file mode 100644 index 00000000..f53be642 --- /dev/null +++ b/test/e2e/test-apps/other/error-after-ready/package.json @@ -0,0 +1,8 @@ +{ + "name": "error-after-ready", + "version": "1.0.0", + "main": "src/main.js", + "dependencies": { + "@sentry/electron": "3.0.0" + } +} diff --git a/test/e2e/test-apps/other/error-after-ready/src/main.js b/test/e2e/test-apps/other/error-after-ready/src/main.js new file mode 100644 index 00000000..12a06fae --- /dev/null +++ b/test/e2e/test-apps/other/error-after-ready/src/main.js @@ -0,0 +1,18 @@ +const { app } = require('electron'); +const { init } = require('@sentry/electron'); + +// Log errors rather than displaying dialog so we can compare the text in the test +process.on('uncaughtException', (e) => console.error(e)); + +app.on('ready', () => { + init({ + dsn: '__DSN__', + debug: true, + autoSessionTracking: false, + onFatalError: () => {}, + }); +}); + +setTimeout(() => { + process.exit(); +}, 2000); diff --git a/test/e2e/test-apps/other/error-no-main/README.md b/test/e2e/test-apps/other/error-no-main/README.md new file mode 100644 index 00000000..21f0cfb6 --- /dev/null +++ b/test/e2e/test-apps/other/error-no-main/README.md @@ -0,0 +1,6 @@ +# Error logged with no SDK in main process + +| Setting | Value | +| -------------- | ------------------------------------------------------------- | +| Build Command | yarn | +| Expected Error | failed to establish connection with the Electron main process | diff --git a/test/e2e/test-apps/other/error-no-main/package.json b/test/e2e/test-apps/other/error-no-main/package.json new file mode 100644 index 00000000..931a0300 --- /dev/null +++ b/test/e2e/test-apps/other/error-no-main/package.json @@ -0,0 +1,8 @@ +{ + "name": "error-no-main", + "version": "1.0.0", + "main": "src/main.js", + "dependencies": { + "@sentry/electron": "3.0.0" + } +} diff --git a/test/e2e/test-apps/other/error-no-main/src/index.html b/test/e2e/test-apps/other/error-no-main/src/index.html new file mode 100644 index 00000000..b43f77af --- /dev/null +++ b/test/e2e/test-apps/other/error-no-main/src/index.html @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/test/e2e/test-apps/other/error-no-main/src/main.js b/test/e2e/test-apps/other/error-no-main/src/main.js new file mode 100644 index 00000000..60f93f09 --- /dev/null +++ b/test/e2e/test-apps/other/error-no-main/src/main.js @@ -0,0 +1,19 @@ +const path = require('path'); + +const { app, BrowserWindow } = require('electron'); + +app.on('ready', () => { + const mainWindow = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + }, + }); + + mainWindow.loadFile(path.join(__dirname, 'index.html')); +}); + +setTimeout(() => { + process.exit(); +}, 2000); diff --git a/test/e2e/utils.ts b/test/e2e/utils.ts index 4d718812..db2ad280 100644 --- a/test/e2e/utils.ts +++ b/test/e2e/utils.ts @@ -9,6 +9,16 @@ export function clearTestLog(): void { TEST_LOG = []; } +export function getTestLog(): string[] { + const output = []; + + for (const args of TEST_LOG) { + output.push(args.map((a) => a.toString()).join(' ')); + } + + return output; +} + export function outputTestLog(): void { for (const args of TEST_LOG) { console.log(...args); @@ -17,7 +27,10 @@ export function outputTestLog(): void { export function createLogger(name: string): (...args: any[]) => void { if (process.env.DEBUG) { - return (...args: any[]) => console.log(`[${name}]`, ...args); + return (...args: any[]) => { + console.log(`[${name}]`, ...args); + TEST_LOG.push([`[${name}]`, ...args]); + }; } else { return (...args: any[]) => TEST_LOG.push([`[${name}]`, ...args]); } diff --git a/yarn.lock b/yarn.lock index 46e50048..024f1171 100644 --- a/yarn.lock +++ b/yarn.lock @@ -734,7 +734,7 @@ chai-subset@^1.6.0: resolved "https://registry.yarnpkg.com/chai-subset/-/chai-subset-1.6.0.tgz#a5d0ca14e329a79596ed70058b6646bd6988cfe9" integrity sha1-pdDKFOMpp5WW7XAFi2ZGvWmIz+k= -chai@^4.1.2: +chai@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.4.tgz#b55e655b31e1eac7099be4c08c21964fce2e6c49" integrity sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA== From d51c13436e08522e7c9eaf9af468ec7e9a029529 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 5 Nov 2021 02:18:36 +0000 Subject: [PATCH 2/7] Simplify --- src/main/electron-normalize.ts | 28 ++----- src/main/ipc.ts | 129 +++++++++++++++++++++++++++++++++ src/main/ipc/classic.ts | 13 ---- src/main/ipc/common.ts | 71 ------------------ src/main/ipc/index.ts | 23 ------ src/main/ipc/protocol.ts | 39 ---------- 6 files changed, 137 insertions(+), 166 deletions(-) create mode 100644 src/main/ipc.ts delete mode 100644 src/main/ipc/classic.ts delete mode 100644 src/main/ipc/common.ts delete mode 100644 src/main/ipc/index.ts delete mode 100644 src/main/ipc/protocol.ts diff --git a/src/main/electron-normalize.ts b/src/main/electron-normalize.ts index 3a09a45f..2f5d4267 100644 --- a/src/main/electron-normalize.ts +++ b/src/main/electron-normalize.ts @@ -4,10 +4,8 @@ import { app, crashReporter, RenderProcessGoneDetails, WebContents } from 'elect const parsed = parseSemver(process.versions.electron); const version = { major: parsed.major || 0, minor: parsed.minor || 0, patch: parsed.patch || 0 }; -/** - * Returns a promise that resolves when app is ready. - */ -async function appIsReady(): Promise { +/** A promise that is resolved when the app is ready */ +export const whenAppReady: Promise = (() => { return app.isReady() ? Promise.resolve() : new Promise((resolve) => { @@ -15,20 +13,7 @@ async function appIsReady(): Promise { resolve(); }); }); -} - -/** A promise that is resolved when the app is ready */ -export const whenAppReady: Promise = appIsReady(); - -/** - * Electron >=8.4 | >=9.1 | >=10 - * Use `render-process-gone` rather than `crashed` - */ -function supportsRenderProcessGone(): boolean { - return ( - version.major >= 10 || (version.major === 9 && version.minor >= 1) || (version.major === 8 && version.minor >= 4) - ); -} +})(); /** * Electron >= 5 support full protocol API @@ -43,8 +28,11 @@ export function supportsFullProtocol(): boolean { export function onRendererProcessGone( callback: (contents: WebContents, details?: RenderProcessGoneDetails) => void, ): void { + const supportsRenderProcessGone = + version.major >= 10 || (version.major === 9 && version.minor >= 1) || (version.major === 8 && version.minor >= 4); + app.on('web-contents-created', (_, contents) => { - if (supportsRenderProcessGone()) { + if (supportsRenderProcessGone) { contents.on('render-process-gone', async (__, details) => { const ignoredReasons = ['clean-exit', 'killed']; @@ -99,7 +87,7 @@ export function usesCrashpad(): boolean { } /** - * Electron >= 9 supports `app.getPath('crashDumps')` rather than + * Electron >= 9 uses `app.getPath('crashDumps')` rather than * `crashReporter.getCrashesDirectory()` */ export function getCrashesDirectory(): string { diff --git a/src/main/ipc.ts b/src/main/ipc.ts new file mode 100644 index 00000000..853d55e2 --- /dev/null +++ b/src/main/ipc.ts @@ -0,0 +1,129 @@ +import { captureEvent, configureScope, Scope } from '@sentry/core'; +import { Event } from '@sentry/types'; +import { forget, logger, SentryError } from '@sentry/utils'; +import { app, ipcMain, protocol, WebContents } from 'electron'; + +import { IPCChannel, IPCMode, normalizeUrl, PROTOCOL_SCHEME } from '../common'; +import { supportsFullProtocol, whenAppReady } from './electron-normalize'; +import { ElectronMainOptions } from './sdk'; + +/** + * Handle events from the renderer processes + */ +export function handleEvent(options: ElectronMainOptions, jsonEvent: string, contents?: WebContents): void { + let event: Event; + try { + event = JSON.parse(jsonEvent) as Event; + } catch { + logger.warn('sentry-electron received an invalid event message'); + return; + } + + if (event.exception) { + event.contexts = { + ...event.contexts, + electron: contents + ? { + crashed_process: options?.getRendererName?.(contents) || `WebContents[${contents.id}]`, + crashed_url: normalizeUrl(contents.getURL(), app.getAppPath()), + } + : { crashed_process: 'renderer' }, + }; + } + + captureEvent(event); +} + +/** Is object defined and has keys */ +function hasKeys(obj: any): boolean { + return obj != undefined && Object.keys(obj).length > 0; +} + +/** + * Handle scope updates from renderer processes + */ +export function handleScope(options: ElectronMainOptions, jsonScope: string): void { + let rendererScope: Scope; + try { + rendererScope = JSON.parse(jsonScope) as Scope; + } catch { + logger.warn('sentry-electron received an invalid scope message'); + return; + } + + const sentScope = Scope.clone(rendererScope) as any; + /* eslint-disable @typescript-eslint/no-unsafe-member-access */ + configureScope((scope) => { + if (hasKeys(sentScope._user)) { + scope.setUser(sentScope._user); + } + + if (hasKeys(sentScope._tags)) { + scope.setTags(sentScope._tags); + } + + if (hasKeys(sentScope._extra)) { + scope.setExtras(sentScope._extra); + } + + scope.addBreadcrumb(sentScope._breadcrumbs.pop(), options?.maxBreadcrumbs || 100); + }); + /* eslint-enable @typescript-eslint/no-unsafe-member-access */ +} + +/** Enables Electron protocol handling */ +function configureProtocol(options: ElectronMainOptions): void { + if (app.isReady()) { + throw new SentryError("Sentry SDK should be initialized before the Electron app 'ready' event is fired"); + } + + protocol.registerSchemesAsPrivileged([ + { + scheme: PROTOCOL_SCHEME, + privileges: { bypassCSP: true, supportFetchAPI: true }, + }, + ]); + + forget( + whenAppReady.then(() => { + for (const sesh of options.getSessions()) { + sesh.protocol.registerStringProtocol(PROTOCOL_SCHEME, (request, callback) => { + const data = request.uploadData?.[0]?.bytes.toString(); + + if (request.url.startsWith(`${PROTOCOL_SCHEME}://${IPCChannel.EVENT}`) && data) { + handleEvent(options, data); + } else if (request.url.startsWith(`${PROTOCOL_SCHEME}://${IPCChannel.SCOPE}`) && data) { + handleScope(options, data); + } + + callback(''); + }); + } + }), + ); +} + +/** + * Hooks IPC for communication with the renderer processes + */ +function configureClassic(options: ElectronMainOptions): void { + ipcMain.on(IPCChannel.EVENT, ({ sender }, jsonEvent: string) => handleEvent(options, jsonEvent, sender)); + ipcMain.on(IPCChannel.SCOPE, (_, jsonScope: string) => handleScope(options, jsonScope)); +} + +/** Sets up communication channels with the renderer */ +export function configureIPC(options: ElectronMainOptions): void { + if (!supportsFullProtocol() && options.ipcMode === IPCMode.Protocol) { + throw new SentryError('IPCMode.Protocol is only supported in Electron >= v5'); + } + + // eslint-disable-next-line no-bitwise + if (supportsFullProtocol() && (options.ipcMode & IPCMode.Protocol) > 0) { + configureProtocol(options); + } + + // eslint-disable-next-line no-bitwise + if ((options.ipcMode & IPCMode.Classic) > 0) { + configureClassic(options); + } +} diff --git a/src/main/ipc/classic.ts b/src/main/ipc/classic.ts deleted file mode 100644 index 25f6fa37..00000000 --- a/src/main/ipc/classic.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ipcMain } from 'electron'; - -import { IPCChannel } from '../../common'; -import { ElectronMainOptions } from '../sdk'; -import { handleEvent, handleScope } from './common'; - -/** - * Hooks IPC for communication with the renderer processes - */ -export function configure(options: ElectronMainOptions): void { - ipcMain.on(IPCChannel.EVENT, ({ sender }, jsonEvent: string) => handleEvent(options, jsonEvent, sender)); - ipcMain.on(IPCChannel.SCOPE, (_, jsonScope: string) => handleScope(options, jsonScope)); -} diff --git a/src/main/ipc/common.ts b/src/main/ipc/common.ts deleted file mode 100644 index 4c132e77..00000000 --- a/src/main/ipc/common.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { captureEvent, configureScope, Scope } from '@sentry/core'; -import { Event } from '@sentry/types'; -import { logger } from '@sentry/utils'; -import { app, WebContents } from 'electron'; - -import { normalizeUrl } from '../../common'; -import { ElectronMainOptions } from '../sdk'; - -/** - * Handle events from the renderer processes - */ -export function handleEvent(options: ElectronMainOptions, jsonEvent: string, contents?: WebContents): void { - let event: Event; - try { - event = JSON.parse(jsonEvent) as Event; - } catch { - logger.warn('sentry-electron received an invalid event message'); - return; - } - - if (event.exception) { - event.contexts = { - ...event.contexts, - electron: contents - ? { - crashed_process: options?.getRendererName?.(contents) || `WebContents[${contents.id}]`, - crashed_url: normalizeUrl(contents.getURL(), app.getAppPath()), - } - : { crashed_process: 'renderer' }, - }; - } - - captureEvent(event); -} - -/** Is object defined and has keys */ -function hasKeys(obj: any): boolean { - return obj != undefined && Object.keys(obj).length > 0; -} - -/** - * Handle scope updates from renderer processes - */ -export function handleScope(options: ElectronMainOptions, jsonScope: string): void { - let rendererScope: Scope; - try { - rendererScope = JSON.parse(jsonScope) as Scope; - } catch { - logger.warn('sentry-electron received an invalid scope message'); - return; - } - - const sentScope = Scope.clone(rendererScope) as any; - /* eslint-disable @typescript-eslint/no-unsafe-member-access */ - configureScope((scope) => { - if (hasKeys(sentScope._user)) { - scope.setUser(sentScope._user); - } - - if (hasKeys(sentScope._tags)) { - scope.setTags(sentScope._tags); - } - - if (hasKeys(sentScope._extra)) { - scope.setExtras(sentScope._extra); - } - - scope.addBreadcrumb(sentScope._breadcrumbs.pop(), options?.maxBreadcrumbs || 100); - }); - /* eslint-enable @typescript-eslint/no-unsafe-member-access */ -} diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts deleted file mode 100644 index c966026f..00000000 --- a/src/main/ipc/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { SentryError } from '@sentry/utils'; -import { IPCMode } from '../../common'; -import { supportsFullProtocol } from '../electron-normalize'; -import { ElectronMainOptions } from '../sdk'; -import { configure as configureClassic } from './classic'; -import { configure as configureProtocol } from './protocol'; - -/** Sets up communication channels with the renderer */ -export function configureIPC(options: ElectronMainOptions): void { - if (!supportsFullProtocol() && options.ipcMode === IPCMode.Protocol) { - throw new SentryError('IPCMode.Protocol is only supported in Electron >= v5'); - } - - // eslint-disable-next-line no-bitwise - if (supportsFullProtocol() && (options.ipcMode & IPCMode.Protocol) > 0) { - configureProtocol(options); - } - - // eslint-disable-next-line no-bitwise - if ((options.ipcMode & IPCMode.Classic) > 0) { - configureClassic(options); - } -} diff --git a/src/main/ipc/protocol.ts b/src/main/ipc/protocol.ts deleted file mode 100644 index a6f52138..00000000 --- a/src/main/ipc/protocol.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { forget, SentryError } from '@sentry/utils'; -import { app, protocol } from 'electron'; - -import { IPCChannel, PROTOCOL_SCHEME } from '../../common'; -import { whenAppReady } from '../electron-normalize'; -import { ElectronMainOptions } from '../sdk'; -import { handleEvent, handleScope } from './common'; - -/** Enables Electron protocol handling */ -export function configure(options: ElectronMainOptions): void { - if (app.isReady()) { - throw new SentryError("Sentry SDK should be initialized before the Electron app 'ready' event is fired"); - } - - protocol.registerSchemesAsPrivileged([ - { - scheme: PROTOCOL_SCHEME, - privileges: { bypassCSP: true, supportFetchAPI: true }, - }, - ]); - - forget( - whenAppReady.then(() => { - for (const sesh of options.getSessions()) { - sesh.protocol.registerStringProtocol(PROTOCOL_SCHEME, (request, callback) => { - const data = request.uploadData?.[0]?.bytes.toString(); - - if (request.url.startsWith(`${PROTOCOL_SCHEME}://${IPCChannel.EVENT}`) && data) { - handleEvent(options, data); - } else if (request.url.startsWith(`${PROTOCOL_SCHEME}://${IPCChannel.SCOPE}`) && data) { - handleScope(options, data); - } - - callback(''); - }); - } - }), - ); -} From ff03e95af6b721daf65cefc4323f672a8290a338 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 5 Nov 2021 14:35:31 +0000 Subject: [PATCH 3/7] Test events from iframe --- package.json | 2 +- test/e2e/recipe/index.ts | 2 + .../test-apps/other/error-iframe/README.md | 7 ++ .../test-apps/other/error-iframe/event.json | 101 ++++++++++++++++++ .../test-apps/other/error-iframe/package.json | 18 ++++ .../other/error-iframe/src/iframe.js | 14 +++ .../test-apps/other/error-iframe/src/main.js | 32 ++++++ .../other/error-iframe/src/renderer.js | 12 +++ .../other/error-iframe/webpack.config.js | 33 ++++++ 9 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 test/e2e/test-apps/other/error-iframe/README.md create mode 100644 test/e2e/test-apps/other/error-iframe/event.json create mode 100644 test/e2e/test-apps/other/error-iframe/package.json create mode 100644 test/e2e/test-apps/other/error-iframe/src/iframe.js create mode 100644 test/e2e/test-apps/other/error-iframe/src/main.js create mode 100644 test/e2e/test-apps/other/error-iframe/src/renderer.js create mode 100644 test/e2e/test-apps/other/error-iframe/webpack.config.js diff --git a/package.json b/package.json index cb7dd512..fc877d69 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "pretest": "yarn build", "test": "cross-env TS_NODE_PROJECT=tsconfig.json xvfb-maybe electron-mocha --require ts-node/register/transpile-only --timeout 120000 ./test/unit/**/*.ts", "pree2e": "rimraf test/e2e/dist/**/node_modules/@sentry/electron/** test/e2e/dist/**/yarn.lock && node scripts/clean-cache.js && yarn build && npm pack", - "e2e": "cross-env TS_NODE_PROJECT=tsconfig.json xvfb-maybe mocha --require ts-node/register/transpile-only --retries 3 ./test/e2e/*.ts" + "e2e": "cross-env TS_NODE_PROJECT=tsconfig.json xvfb-maybe mocha --require ts-node/register/transpile-only ./test/e2e/*.ts" }, "dependencies": { "@sentry/browser": "6.13.3", diff --git a/test/e2e/recipe/index.ts b/test/e2e/recipe/index.ts index 0ce27a50..e23adc90 100644 --- a/test/e2e/recipe/index.ts +++ b/test/e2e/recipe/index.ts @@ -88,6 +88,8 @@ export class RecipeRunner { public async prepare(context: Mocha.Context, testBasePath: string): Promise<[string, string]> { log(`Preparing recipe '${this.description}'`); + context.retries(process.env.CI ? 3 : 0); + const timeout = this._recipe.metadata.timeout || 30_000; // macOS runs quite slowly in GitHub actions context.timeout(process.platform === 'darwin' ? timeout * 2 : timeout); diff --git a/test/e2e/test-apps/other/error-iframe/README.md b/test/e2e/test-apps/other/error-iframe/README.md new file mode 100644 index 00000000..c91d4bf0 --- /dev/null +++ b/test/e2e/test-apps/other/error-iframe/README.md @@ -0,0 +1,7 @@ +# JavaScript Error from