From f99c57107c195d4a6de6b8a5990f73c42b95f08f Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 27 Sep 2024 15:25:45 +0100 Subject: [PATCH 1/6] Add support --- .gitignore | 1 + .npmignore | 1 + package.json | 4 + rollup.config.mjs | 8 +- src/common/envelope.ts | 28 +++++++ src/common/ipc.ts | 12 +++ src/main/ipc.ts | 30 +------ src/main/sdk.ts | 2 + src/main/utility-processes.ts | 101 ++++++++++++++++++++++++ src/utility/index.ts | 142 ++++++++++++++++++++++++++++++++++ src/utility/sdk.ts | 81 +++++++++++++++++++ src/utility/transport.ts | 60 ++++++++++++++ 12 files changed, 441 insertions(+), 29 deletions(-) create mode 100644 src/common/envelope.ts create mode 100644 src/main/utility-processes.ts create mode 100644 src/utility/index.ts create mode 100644 src/utility/sdk.ts create mode 100644 src/utility/transport.ts diff --git a/.gitignore b/.gitignore index 6dcbd423..4d3d1b78 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ stats.json /renderer /main /common +/utility /index.* /integrations.* /ipc.* diff --git a/.npmignore b/.npmignore index ad275b43..9218e615 100644 --- a/.npmignore +++ b/.npmignore @@ -6,5 +6,6 @@ !/main/**/* !/renderer/**/* !/common/**/* +!/utility/**/* !/index.* !/integrations.* diff --git a/package.json b/package.json index 1c2f6609..c25ded77 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,10 @@ "./preload": { "require": "./preload/index.js", "import": "./esm/preload/index.js" + }, + "./utility": { + "require": "./utility/index.js", + "import": "./esm/utility/index.js" } }, "repository": "https://github.com/getsentry/sentry-electron.git", diff --git a/rollup.config.mjs b/rollup.config.mjs index 585310c9..140af64e 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -71,8 +71,12 @@ function bundlePreload(format, input, output) { } export default [ - transpileFiles('cjs', ['src/index.ts', 'src/main/index.ts', 'src/renderer/index.ts'], '.'), - transpileFiles('esm', ['src/index.ts', 'src/main/index.ts', 'src/renderer/index.ts'], './esm'), + transpileFiles('cjs', ['src/index.ts', 'src/main/index.ts', 'src/renderer/index.ts', 'src/utility/index.ts'], '.'), + transpileFiles( + 'esm', + ['src/index.ts', 'src/main/index.ts', 'src/renderer/index.ts', 'src/utility/index.ts'], + './esm', + ), bundlePreload('cjs', 'src/preload/index.ts', './preload/index.js'), bundlePreload('esm', 'src/preload/index.ts', './esm/preload/index.js'), ]; diff --git a/src/common/envelope.ts b/src/common/envelope.ts new file mode 100644 index 00000000..8a686db9 --- /dev/null +++ b/src/common/envelope.ts @@ -0,0 +1,28 @@ +import { Attachment, AttachmentItem, Envelope, Event, EventItem, Profile } from '@sentry/types'; +import { forEachEnvelopeItem } from '@sentry/utils'; + +/** Pulls an event and additional envelope items out of an envelope. Returns undefined if there was no event */ +export function eventFromEnvelope(envelope: Envelope): [Event, Attachment[], Profile | undefined] | undefined { + let event: Event | undefined; + const attachments: Attachment[] = []; + let profile: Profile | undefined; + + forEachEnvelopeItem(envelope, (item, type) => { + if (type === 'event' || type === 'transaction' || type === 'feedback') { + event = Array.isArray(item) ? (item as EventItem)[1] : undefined; + } else if (type === 'attachment') { + const [headers, data] = item as AttachmentItem; + + attachments.push({ + filename: headers.filename, + attachmentType: headers.attachment_type, + contentType: headers.content_type, + data, + }); + } else if (type === 'profile') { + profile = item[1] as unknown as Profile; + } + }); + + return event ? [event, attachments, profile] : undefined; +} diff --git a/src/common/ipc.ts b/src/common/ipc.ts index dfb4bc18..0d746563 100644 --- a/src/common/ipc.ts +++ b/src/common/ipc.ts @@ -78,6 +78,18 @@ export interface IPCInterface { export const RENDERER_ID_HEADER = 'sentry-electron-renderer-id'; +const UTILITY_PROCESS_MAGIC_MESSAGE_KEY = '__sentry_message_port_message__'; + +/** Does the message look like the magic message */ +export function isMagicMessage(msg: unknown): boolean { + return !!(msg && typeof msg === 'object' && UTILITY_PROCESS_MAGIC_MESSAGE_KEY in msg); +} + +/** Get the magic message to send to the utility process */ +export function getMagicMessage(): unknown { + return { [UTILITY_PROCESS_MAGIC_MESSAGE_KEY]: true }; +} + /** * We store the IPC interface on window so it's the same for both regular and isolated contexts */ diff --git a/src/main/ipc.ts b/src/main/ipc.ts index a658562e..89664ef7 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -1,8 +1,9 @@ import { captureEvent, getClient, getCurrentScope, metrics } from '@sentry/node'; -import { Attachment, AttachmentItem, Envelope, Event, EventItem, Profile, ScopeData } from '@sentry/types'; -import { forEachEnvelopeItem, logger, parseEnvelope, SentryError } from '@sentry/utils'; +import { Attachment, Event, ScopeData } from '@sentry/types'; +import { logger, parseEnvelope, SentryError } from '@sentry/utils'; import { app, ipcMain, protocol, WebContents, webContents } from 'electron'; +import { eventFromEnvelope } from '../common/envelope'; import { IPCChannel, IPCMode, MetricIPCMessage, PROTOCOL_SCHEME, RendererStatus } from '../common/ipc'; import { createRendererAnrStatusHandler } from './anr'; import { registerProtocol } from './electron-normalize'; @@ -79,31 +80,6 @@ function handleEvent(options: ElectronMainOptionsInternal, jsonEvent: string, co captureEventFromRenderer(options, event, [], contents); } -function eventFromEnvelope(envelope: Envelope): [Event, Attachment[], Profile | undefined] | undefined { - let event: Event | undefined; - const attachments: Attachment[] = []; - let profile: Profile | undefined; - - forEachEnvelopeItem(envelope, (item, type) => { - if (type === 'event' || type === 'transaction' || type === 'feedback') { - event = Array.isArray(item) ? (item as EventItem)[1] : undefined; - } else if (type === 'attachment') { - const [headers, data] = item as AttachmentItem; - - attachments.push({ - filename: headers.filename, - attachmentType: headers.attachment_type, - contentType: headers.content_type, - data, - }); - } else if (type === 'profile') { - profile = item[1] as unknown as Profile; - } - }); - - return event ? [event, attachments, profile] : undefined; -} - function handleEnvelope(options: ElectronMainOptionsInternal, env: Uint8Array | string, contents?: WebContents): void { const envelope = parseEnvelope(env); diff --git a/src/main/sdk.ts b/src/main/sdk.ts index 8af5d878..f9605d1d 100644 --- a/src/main/sdk.ts +++ b/src/main/sdk.ts @@ -36,6 +36,7 @@ import { sentryMinidumpIntegration } from './integrations/sentry-minidump'; import { configureIPC } from './ipc'; import { defaultStackParser } from './stack-parse'; import { ElectronOfflineTransportOptions, makeElectronOfflineTransport } from './transports/electron-offline-net'; +import { configureUtilityProcessIPC } from './utility-processes'; /** Get the default integrations for the main process SDK. */ export function getDefaultIntegrations(options: ElectronMainOptions): Integration[] { @@ -155,6 +156,7 @@ export function init(userOptions: ElectronMainOptions): void { removeRedundantIntegrations(options); configureIPC(options); + configureUtilityProcessIPC(); setNodeAsyncContextStrategy(); diff --git a/src/main/utility-processes.ts b/src/main/utility-processes.ts new file mode 100644 index 00000000..fd859ad3 --- /dev/null +++ b/src/main/utility-processes.ts @@ -0,0 +1,101 @@ +import { captureEvent, getClient } from '@sentry/node'; +import { Attachment, Event } from '@sentry/types'; +import { logger, parseEnvelope } from '@sentry/utils'; +import * as electron from 'electron'; + +import { eventFromEnvelope } from '../common/envelope'; +import { getMagicMessage, isMagicMessage } from '../common/ipc'; +import { mergeEvents } from './merge'; + +function log(message: string): void { + logger.log(`[Utility Process] ${message}`); +} + +/** + * We wrap `electron.utilityProcess.fork` so we can pass a messageport to any SDK running in the utility process + */ +export function configureUtilityProcessIPC(): void { + if (!electron.utilityProcess?.fork) { + return; + } + + // eslint-disable-next-line @typescript-eslint/unbound-method + electron.utilityProcess.fork = new Proxy(electron.utilityProcess.fork, { + apply: (target, thisArg, args: Parameters) => { + // Call the underlying function to get the child process + const child: electron.UtilityProcess = target.apply(thisArg, args); + + const [, , options] = args || {}; + const childName = options?.serviceName || `pid:${child.pid}`; + + // We don't send any messages unless we've heard from the child SDK. At that point we know it's ready to receive + // and will also filter out any messages we send so users don't see them + child.on('message', (msg: unknown) => { + if (isMagicMessage(msg)) { + log(`SDK started in utility process '${childName}'`); + + const { port1, port2 } = new electron.MessageChannelMain(); + + port2.on('message', (msg) => { + if (msg.data instanceof Uint8Array || typeof msg.data === 'string') { + handleEnvelopeFromUtility(msg.data); + } + }); + + // Send one side of the message port to the child SDK + child.postMessage(getMagicMessage(), [port1]); + } + }); + + // We proxy child.on so we can filter messages from the child SDK and ensure that users do not see them + // eslint-disable-next-line @typescript-eslint/unbound-method + child.on = new Proxy(child.on, { + apply: (target, thisArg, [event, listener]) => { + if (event === 'message') { + return target.apply(thisArg, [ + 'message', + (msg: unknown) => { + if (isMagicMessage(msg)) { + return; + } + + return listener(msg); + }, + ]); + } + + return target.apply(thisArg, [event, listener]); + }, + }); + + return child; + }, + }); +} + +function handleEnvelopeFromUtility(env: Uint8Array | string): void { + const envelope = parseEnvelope(env); + + const eventAndAttachments = eventFromEnvelope(envelope); + if (eventAndAttachments) { + const [event, attachments] = eventAndAttachments; + + captureEventFromUtility(event, attachments); + } else { + // Pass other types of envelope straight to the transport + void getClient()?.getTransport()?.send(envelope); + } +} + +function captureEventFromUtility(event: Event, attachments: Attachment[]): void { + // Remove the environment as it defaults to 'production' and overwrites the main process environment + delete event.environment; + delete event.release; + + // Remove the SDK info as we want the Electron SDK to be the one reporting the event + delete event.sdk?.name; + delete event.sdk?.version; + delete event.sdk?.packages; + + captureEvent(mergeEvents(event, { tags: { 'event.process': 'utility' } }), { attachments }); +} diff --git a/src/utility/index.ts b/src/utility/index.ts new file mode 100644 index 00000000..12b80ff7 --- /dev/null +++ b/src/utility/index.ts @@ -0,0 +1,142 @@ +export type { + Breadcrumb, + BreadcrumbHint, + PolymorphicRequest, + Request, + SdkInfo, + Event, + EventHint, + Exception, + Session, + SeverityLevel, + StackFrame, + Stacktrace, + Thread, + User, + Span, +} from '@sentry/types'; + +export { + addBreadcrumb, + addEventProcessor, + addIntegration, + addOpenTelemetryInstrumentation, + addRequestDataToEvent, + captureCheckIn, + captureConsoleIntegration, + captureEvent, + captureException, + captureFeedback, + captureMessage, + captureSession, + close, + connectIntegration, + consoleIntegration, + contextLinesIntegration, + continueTrace, + createGetModuleFromFilename, + createTransport, + cron, + dataloaderIntegration, + debugIntegration, + dedupeIntegration, + DEFAULT_USER_INCLUDES, + endSession, + expressErrorHandler, + expressIntegration, + extractRequestData, + extraErrorDataIntegration, + fastifyIntegration, + flush, + fsIntegration, + functionToStringIntegration, + generateInstrumentOnce, + genericPoolIntegration, + getActiveSpan, + getAutoPerformanceIntegrations, + getClient, + // eslint-disable-next-line deprecation/deprecation + getCurrentHub, + getCurrentScope, + getGlobalScope, + getIsolationScope, + getRootSpan, + getSpanDescendants, + getSpanStatusFromHttpCode, + getTraceData, + getTraceMetaTags, + graphqlIntegration, + hapiIntegration, + httpIntegration, + inboundFiltersIntegration, + initOpenTelemetry, + isInitialized, + kafkaIntegration, + koaIntegration, + lastEventId, + linkedErrorsIntegration, + localVariablesIntegration, + metrics, + modulesIntegration, + mongoIntegration, + mongooseIntegration, + mysql2Integration, + mysqlIntegration, + nativeNodeFetchIntegration, + nestIntegration, + NodeClient, + nodeContextIntegration, + onUnhandledRejectionIntegration, + parameterize, + postgresIntegration, + prismaIntegration, + profiler, + redisIntegration, + requestDataIntegration, + rewriteFramesIntegration, + Scope, + SentryContextManager, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + sessionTimingIntegration, + setContext, + setCurrentClient, + setExtra, + setExtras, + setHttpStatus, + setMeasurement, + setNodeAsyncContextStrategy, + setTag, + setTags, + setupConnectErrorHandler, + setupExpressErrorHandler, + setupFastifyErrorHandler, + setupHapiErrorHandler, + setupKoaErrorHandler, + setupNestErrorHandler, + setUser, + spanToBaggageHeader, + spanToJSON, + spanToTraceHeader, + spotlightIntegration, + startInactiveSpan, + startNewTrace, + startSession, + startSpan, + startSpanManual, + trpcMiddleware, + validateOpenTelemetrySetup, + withActiveSpan, + withIsolationScope, + withMonitor, + withScope, + zodErrorsIntegration, +} from '@sentry/node'; + +export type { NodeOptions } from '@sentry/node'; + +export { makeUtilityProcessTransport } from './transport'; + +export { init, getDefaultIntegrations, defaultStackParser } from './sdk'; diff --git a/src/utility/sdk.ts b/src/utility/sdk.ts new file mode 100644 index 00000000..9bf2cc32 --- /dev/null +++ b/src/utility/sdk.ts @@ -0,0 +1,81 @@ +import { getIntegrationsToSetup } from '@sentry/core'; +import { + consoleIntegration, + contextLinesIntegration, + createGetModuleFromFilename, + functionToStringIntegration, + getCurrentScope, + inboundFiltersIntegration, + initOpenTelemetry, + linkedErrorsIntegration, + localVariablesIntegration, + nativeNodeFetchIntegration, + NodeClient, + nodeContextIntegration, + NodeOptions, + onUncaughtExceptionIntegration, + onUnhandledRejectionIntegration, + setNodeAsyncContextStrategy, +} from '@sentry/node'; +import { Integration, StackParser } from '@sentry/types'; +import { createStackParser, nodeStackLineParser, stackParserFromStackParserOptions } from '@sentry/utils'; + +import { makeUtilityProcessTransport } from './transport'; + +export const defaultStackParser: StackParser = createStackParser(nodeStackLineParser(createGetModuleFromFilename())); + +/** Get the default integrations for the main process SDK. */ +export function getDefaultIntegrations(): Integration[] { + const integrations = [ + // Node integrations + inboundFiltersIntegration(), + functionToStringIntegration(), + linkedErrorsIntegration(), + consoleIntegration(), + nativeNodeFetchIntegration(), + onUncaughtExceptionIntegration(), + onUnhandledRejectionIntegration(), + contextLinesIntegration(), + localVariablesIntegration(), + nodeContextIntegration({ cloudResource: false }), + ]; + + return integrations; +} + +/** + * Initialize Sentry in the Electron main process + */ +export function init(userOptions: NodeOptions = {}): void { + const optionsWithDefaults = { + defaultIntegrations: getDefaultIntegrations(), + transport: makeUtilityProcessTransport(), + // We track sessions in the main process + autoSessionTracking: false, + sendClientReports: false, + ...userOptions, + stackParser: stackParserFromStackParserOptions(userOptions.stackParser || defaultStackParser), + // Events are sent via the main process but the Node SDK wont start without dsn + dsn: 'https://12345@dummy.dsn/12345', + }; + + const options = { + ...optionsWithDefaults, + integrations: getIntegrationsToSetup(optionsWithDefaults), + }; + + setNodeAsyncContextStrategy(); + + const scope = getCurrentScope(); + scope.update(options.initialScope); + + const client = new NodeClient(options); + scope.setClient(client); + client.init(); + + // If users opt-out of this, they _have_ to set up OpenTelemetry themselves + // There is no way to use this SDK without OpenTelemetry! + if (!options.skipOpenTelemetrySetup) { + initOpenTelemetry(client); + } +} diff --git a/src/utility/transport.ts b/src/utility/transport.ts new file mode 100644 index 00000000..e0a5b17e --- /dev/null +++ b/src/utility/transport.ts @@ -0,0 +1,60 @@ +import { createTransport } from '@sentry/core'; +import { BaseTransportOptions, Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types'; + +import { getMagicMessage, isMagicMessage } from '../common/ipc'; + +/** + * Creates a Transport that passes envelopes to the Electron main process. + */ +export function makeUtilityProcessTransport(): (options: BaseTransportOptions) => Transport { + let mainMessagePort: Electron.MessagePortMain | undefined; + + async function sendEnvelope(envelope: string | Uint8Array): Promise { + if (mainMessagePort) { + mainMessagePort.postMessage(envelope); + } + } + + // Receive the messageport from the main process + process.parentPort.on('message', (msg) => { + // eslint-disable-next-line no-console + console.log('process.parentPort.on', JSON.stringify(msg)); + + if (isMagicMessage(msg.data)) { + const [port] = msg.ports; + mainMessagePort = port; + } + }); + + // We proxy `process.parentPort.on` so we can filter messages from the main SDK and ensure that users do not see them + // eslint-disable-next-line @typescript-eslint/unbound-method + process.parentPort.on = new Proxy(process.parentPort.on, { + apply: (target, thisArg, [event, listener]) => { + if (event === 'message') { + return target.apply(thisArg, [ + 'message', + (msg: MessageEvent) => { + if (isMagicMessage(msg.data)) { + return; + } + + return listener(msg); + }, + ]); + } + + return target.apply(thisArg, [event, listener]); + }, + }); + + // Notify the main process that this utility process has started with an SDK configured + process.parentPort.postMessage(getMagicMessage()); + + return (options) => { + return createTransport(options, async (request: TransportRequest): Promise => { + await sendEnvelope(request.body); + // Since the main process handles sending of envelopes and rate limiting, we always return 200 OK + return { statusCode: 200 }; + }); + }; +} From 919632f806423bb3858281e9bbf44f71e82f09a4 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 30 Sep 2024 13:02:56 +0200 Subject: [PATCH 2/6] changes --- src/main/utility-processes.ts | 8 +- src/utility/sdk.ts | 12 +- src/utility/transport.ts | 4 +- .../other/utility-process/event.json | 104 ++++++++++++++++++ .../other/utility-process/package.json | 8 ++ .../other/utility-process/recipe.only.yml | 2 + .../other/utility-process/src/main.js | 20 ++++ .../other/utility-process/src/utility.js | 13 +++ 8 files changed, 158 insertions(+), 13 deletions(-) create mode 100644 test/e2e/test-apps/other/utility-process/event.json create mode 100644 test/e2e/test-apps/other/utility-process/package.json create mode 100644 test/e2e/test-apps/other/utility-process/recipe.only.yml create mode 100644 test/e2e/test-apps/other/utility-process/src/main.js create mode 100644 test/e2e/test-apps/other/utility-process/src/utility.js diff --git a/src/main/utility-processes.ts b/src/main/utility-processes.ts index fd859ad3..26b696e6 100644 --- a/src/main/utility-processes.ts +++ b/src/main/utility-processes.ts @@ -25,14 +25,16 @@ export function configureUtilityProcessIPC(): void { // Call the underlying function to get the child process const child: electron.UtilityProcess = target.apply(thisArg, args); - const [, , options] = args || {}; - const childName = options?.serviceName || `pid:${child.pid}`; + function getProcessName(): string { + const [, , options] = args; + return options?.serviceName || `pid:${child.pid}`; + } // We don't send any messages unless we've heard from the child SDK. At that point we know it's ready to receive // and will also filter out any messages we send so users don't see them child.on('message', (msg: unknown) => { if (isMagicMessage(msg)) { - log(`SDK started in utility process '${childName}'`); + log(`SDK started in utility process '${getProcessName()}'`); const { port1, port2 } = new electron.MessageChannelMain(); diff --git a/src/utility/sdk.ts b/src/utility/sdk.ts index 9bf2cc32..cb54e6c7 100644 --- a/src/utility/sdk.ts +++ b/src/utility/sdk.ts @@ -1,24 +1,21 @@ import { getIntegrationsToSetup } from '@sentry/core'; import { consoleIntegration, - contextLinesIntegration, createGetModuleFromFilename, functionToStringIntegration, getCurrentScope, inboundFiltersIntegration, initOpenTelemetry, linkedErrorsIntegration, - localVariablesIntegration, nativeNodeFetchIntegration, NodeClient, - nodeContextIntegration, NodeOptions, onUncaughtExceptionIntegration, onUnhandledRejectionIntegration, setNodeAsyncContextStrategy, } from '@sentry/node'; import { Integration, StackParser } from '@sentry/types'; -import { createStackParser, nodeStackLineParser, stackParserFromStackParserOptions } from '@sentry/utils'; +import { createStackParser, logger, nodeStackLineParser, stackParserFromStackParserOptions } from '@sentry/utils'; import { makeUtilityProcessTransport } from './transport'; @@ -35,9 +32,6 @@ export function getDefaultIntegrations(): Integration[] { nativeNodeFetchIntegration(), onUncaughtExceptionIntegration(), onUnhandledRejectionIntegration(), - contextLinesIntegration(), - localVariablesIntegration(), - nodeContextIntegration({ cloudResource: false }), ]; return integrations; @@ -64,6 +58,10 @@ export function init(userOptions: NodeOptions = {}): void { integrations: getIntegrationsToSetup(optionsWithDefaults), }; + if (options.debug) { + logger.enable(); + } + setNodeAsyncContextStrategy(); const scope = getCurrentScope(); diff --git a/src/utility/transport.ts b/src/utility/transport.ts index e0a5b17e..d7f92f54 100644 --- a/src/utility/transport.ts +++ b/src/utility/transport.ts @@ -17,12 +17,10 @@ export function makeUtilityProcessTransport(): (options: BaseTransportOptions) = // Receive the messageport from the main process process.parentPort.on('message', (msg) => { - // eslint-disable-next-line no-console - console.log('process.parentPort.on', JSON.stringify(msg)); - if (isMagicMessage(msg.data)) { const [port] = msg.ports; mainMessagePort = port; + mainMessagePort?.start(); } }); diff --git a/test/e2e/test-apps/other/utility-process/event.json b/test/e2e/test-apps/other/utility-process/event.json new file mode 100644 index 00000000..2281f6b9 --- /dev/null +++ b/test/e2e/test-apps/other/utility-process/event.json @@ -0,0 +1,104 @@ +{ + "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": "javascript-screenshots", + "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 + }, + "culture": { + "locale": "{{locale}}", + "timezone": "{{timezone}}" + }, + "node": { + "name": "Node", + "type": "runtime", + "version": "{{version}}" + }, + "os": { + "name": "{{platform}}", + "version": "{{version}}" + }, + "runtime": { + "name": "Electron", + "version": "{{version}}" + } + }, + "release": "javascript-screenshots@1.0.0", + "environment": "development", + "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": [], + "request": { + "url": "app:///src/index.html" + }, + "tags": { + "event.environment": "javascript", + "event.origin": "electron", + "event.process": "renderer" + } + }, + "attachments": [ + { + "filename": "screenshot.png", + "content_type": "image/png" + } + ] +} diff --git a/test/e2e/test-apps/other/utility-process/package.json b/test/e2e/test-apps/other/utility-process/package.json new file mode 100644 index 00000000..79d44df3 --- /dev/null +++ b/test/e2e/test-apps/other/utility-process/package.json @@ -0,0 +1,8 @@ +{ + "name": "utility-process", + "version": "1.0.0", + "main": "src/main.js", + "dependencies": { + "@sentry/electron": "3.0.0" + } +} diff --git a/test/e2e/test-apps/other/utility-process/recipe.only.yml b/test/e2e/test-apps/other/utility-process/recipe.only.yml new file mode 100644 index 00000000..e9f60e4b --- /dev/null +++ b/test/e2e/test-apps/other/utility-process/recipe.only.yml @@ -0,0 +1,2 @@ +description: Utility Process +command: yarn diff --git a/test/e2e/test-apps/other/utility-process/src/main.js b/test/e2e/test-apps/other/utility-process/src/main.js new file mode 100644 index 00000000..b3a4a0c8 --- /dev/null +++ b/test/e2e/test-apps/other/utility-process/src/main.js @@ -0,0 +1,20 @@ +const path = require('path'); + +const { app, utilityProcess } = require('electron'); +const { init } = require('@sentry/electron/main'); + +init({ + dsn: '__DSN__', + debug: true, + autoSessionTracking: false, + attachScreenshot: true, + onFatalError: () => {}, +}); + +app.on('ready', () => { + const child = utilityProcess.fork(path.join(__dirname, 'utility.js')); + + child.on('message', () => { + throw new Error('should not get any messages'); + }); +}); diff --git a/test/e2e/test-apps/other/utility-process/src/utility.js b/test/e2e/test-apps/other/utility-process/src/utility.js new file mode 100644 index 00000000..fd2772dc --- /dev/null +++ b/test/e2e/test-apps/other/utility-process/src/utility.js @@ -0,0 +1,13 @@ +const { init } = require('@sentry/electron/utility'); + +init({ + debug: true, +}); + +process.parentPort.on('message', () => { + throw new Error('should get any massages'); +}); + +setTimeout(() => { + throw new Error('utility fail!'); +}, 1000); From 93a5b6d547fba52541c8f956ed95533112b62fad Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 1 Oct 2024 12:23:59 +0200 Subject: [PATCH 3/6] Get utility test passing --- src/main/sdk.ts | 6 ++++- src/main/utility-processes.ts | 1 + .../other/utility-process/event.json | 27 +++++++------------ .../{recipe.only.yml => recipe.yml} | 0 .../other/utility-process/src/main.js | 1 - .../other/utility-process/src/utility.js | 2 +- 6 files changed, 16 insertions(+), 21 deletions(-) rename test/e2e/test-apps/other/utility-process/{recipe.only.yml => recipe.yml} (100%) diff --git a/src/main/sdk.ts b/src/main/sdk.ts index f9605d1d..7d7b11bc 100644 --- a/src/main/sdk.ts +++ b/src/main/sdk.ts @@ -16,7 +16,7 @@ import { setNodeAsyncContextStrategy, } from '@sentry/node'; import { Integration, Options } from '@sentry/types'; -import { stackParserFromStackParserOptions } from '@sentry/utils'; +import { logger, stackParserFromStackParserOptions } from '@sentry/utils'; import { Session, session, WebContents } from 'electron'; import { IPCMode } from '../common/ipc'; @@ -154,6 +154,10 @@ export function init(userOptions: ElectronMainOptions): void { integrations: getIntegrationsToSetup(optionsWithDefaults), }; + if (options.debug) { + logger.enable(); + } + removeRedundantIntegrations(options); configureIPC(options); configureUtilityProcessIPC(); diff --git a/src/main/utility-processes.ts b/src/main/utility-processes.ts index 26b696e6..367fee88 100644 --- a/src/main/utility-processes.ts +++ b/src/main/utility-processes.ts @@ -43,6 +43,7 @@ export function configureUtilityProcessIPC(): void { handleEnvelopeFromUtility(msg.data); } }); + port2.start(); // Send one side of the message port to the child SDK child.postMessage(getMagicMessage(), [port1]); diff --git a/test/e2e/test-apps/other/utility-process/event.json b/test/e2e/test-apps/other/utility-process/event.json index 2281f6b9..31dcca74 100644 --- a/test/e2e/test-apps/other/utility-process/event.json +++ b/test/e2e/test-apps/other/utility-process/event.json @@ -15,7 +15,7 @@ }, "contexts": { "app": { - "app_name": "javascript-screenshots", + "app_name": "utility-process", "app_version": "1.0.0", "app_start_time": "{{time}}" }, @@ -56,18 +56,18 @@ "version": "{{version}}" } }, - "release": "javascript-screenshots@1.0.0", + "release": "utility-process@1.0.0", "environment": "development", "exception": { "values": [ { "type": "Error", - "value": "Some renderer error", + "value": "Some utility error", "stacktrace": { "frames": [ { "colno": 0, - "filename": "app:///src/index.html", + "filename": "app:///src/utility.js", "function": "{{function}}", "in_app": true, "lineno": 0 @@ -76,29 +76,20 @@ }, "mechanism": { "handled": false, - "type": "instrument" + "type": "onuncaughtexception" } } ] }, - "level": "error", + "level": "fatal", "event_id": "{{id}}", - "platform": "javascript", + "platform": "node", "timestamp": 0, "breadcrumbs": [], - "request": { - "url": "app:///src/index.html" - }, "tags": { "event.environment": "javascript", "event.origin": "electron", - "event.process": "renderer" - } - }, - "attachments": [ - { - "filename": "screenshot.png", - "content_type": "image/png" + "event.process": "utility" } - ] + } } diff --git a/test/e2e/test-apps/other/utility-process/recipe.only.yml b/test/e2e/test-apps/other/utility-process/recipe.yml similarity index 100% rename from test/e2e/test-apps/other/utility-process/recipe.only.yml rename to test/e2e/test-apps/other/utility-process/recipe.yml diff --git a/test/e2e/test-apps/other/utility-process/src/main.js b/test/e2e/test-apps/other/utility-process/src/main.js index b3a4a0c8..3cca68b0 100644 --- a/test/e2e/test-apps/other/utility-process/src/main.js +++ b/test/e2e/test-apps/other/utility-process/src/main.js @@ -7,7 +7,6 @@ init({ dsn: '__DSN__', debug: true, autoSessionTracking: false, - attachScreenshot: true, onFatalError: () => {}, }); diff --git a/test/e2e/test-apps/other/utility-process/src/utility.js b/test/e2e/test-apps/other/utility-process/src/utility.js index fd2772dc..272e77fa 100644 --- a/test/e2e/test-apps/other/utility-process/src/utility.js +++ b/test/e2e/test-apps/other/utility-process/src/utility.js @@ -9,5 +9,5 @@ process.parentPort.on('message', () => { }); setTimeout(() => { - throw new Error('utility fail!'); + throw new Error('Some utility error'); }, 1000); From cefac331c0a6d9d575db0b43990d5772f1b5c923 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 1 Oct 2024 12:30:05 +0200 Subject: [PATCH 4/6] Also check exports for utility process code --- scripts/check-exports.mjs | 11 ++++++++++- src/utility/index.ts | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/scripts/check-exports.mjs b/scripts/check-exports.mjs index 9fe9f0ec..a0af2f3e 100644 --- a/scripts/check-exports.mjs +++ b/scripts/check-exports.mjs @@ -1,5 +1,6 @@ import * as browser from '@sentry/browser'; import * as renderer from '../esm/renderer/index.js'; +import * as utility from '../esm/utility/index.js'; import * as node from '@sentry/node'; @@ -13,6 +14,7 @@ const browserExports = Object.keys(browser); const rendererExports = Object.keys(renderer); const nodeExports = Object.keys(node); const mainExports = Object.keys(main); +const utilityExports = Object.keys(utility); const ignoredBrowser = [ 'SDK_VERSION', @@ -46,10 +48,13 @@ const ignoredNode = [ 'initWithoutDefaultIntegrations', ]; +const ignoredUtility = [...ignoredNode, 'anrIntegration']; + const missingRenderer = browserExports.filter((key) => !rendererExports.includes(key) && !ignoredBrowser.includes(key)); const missingMain = nodeExports.filter((key) => !mainExports.includes(key) && !ignoredNode.includes(key)); +const missingUtility = nodeExports.filter((key) => !utilityExports.includes(key) && !ignoredUtility.includes(key)); -if (missingRenderer.length || missingMain.length) { +if (missingRenderer.length || missingMain.length || missingUtility.length) { if (missingRenderer.length) { console.error('Missing renderer exports:', missingRenderer); } @@ -58,5 +63,9 @@ if (missingRenderer.length || missingMain.length) { console.error('Missing main exports:', missingMain); } + if (missingUtility.length) { + console.error('Missing utility exports:', missingUtility); + } + process.exit(1); } diff --git a/src/utility/index.ts b/src/utility/index.ts index 12b80ff7..c99bf5df 100644 --- a/src/utility/index.ts +++ b/src/utility/index.ts @@ -86,6 +86,7 @@ export { nestIntegration, NodeClient, nodeContextIntegration, + onUncaughtExceptionIntegration, onUnhandledRejectionIntegration, parameterize, postgresIntegration, From 95a0f13b077083a5763f3e637a96a6b269b4ac1b Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 1 Oct 2024 13:15:00 +0200 Subject: [PATCH 5/6] Wait for message port to be sent --- src/utility/transport.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/utility/transport.ts b/src/utility/transport.ts index d7f92f54..edb41cd1 100644 --- a/src/utility/transport.ts +++ b/src/utility/transport.ts @@ -10,9 +10,20 @@ export function makeUtilityProcessTransport(): (options: BaseTransportOptions) = let mainMessagePort: Electron.MessagePortMain | undefined; async function sendEnvelope(envelope: string | Uint8Array): Promise { - if (mainMessagePort) { - mainMessagePort.postMessage(envelope); + let count = 0; + + // mainMessagePort is undefined until the main process sends us the message port + while (mainMessagePort === undefined) { + await new Promise((resolve) => setTimeout(resolve, 100)); + count += 1; + + // After 5 seconds, we give up waiting for the main process to send us the message port + if (count >= 50) { + throw new Error('Timeout waiting for message port to send event to main process'); + } } + + mainMessagePort.postMessage(envelope); } // Receive the messageport from the main process From 0dbb6008e9646e517aafaeff1d736d65091c8ab2 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 1 Oct 2024 17:37:59 +0200 Subject: [PATCH 6/6] only test on Electron versions that support the utility process --- test/e2e/test-apps/other/utility-process/recipe.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/test/e2e/test-apps/other/utility-process/recipe.yml b/test/e2e/test-apps/other/utility-process/recipe.yml index e9f60e4b..b74b4e94 100644 --- a/test/e2e/test-apps/other/utility-process/recipe.yml +++ b/test/e2e/test-apps/other/utility-process/recipe.yml @@ -1,2 +1,3 @@ description: Utility Process command: yarn +condition: version.major >= 22