diff --git a/src/main/actions/tracingPropogation.js b/src/main/actions/tracingPropogation.js new file mode 100644 index 0000000..ce8325e --- /dev/null +++ b/src/main/actions/tracingPropogation.js @@ -0,0 +1,17 @@ +const Sentry = require("@sentry/electron/main"); +import { createTracedHandler } from "../lib/tracingMainUtils"; + +/** + * Wraps IPC handlers in the main process with distributed tracing + * @param {string} operationName - Name of the IPC operation + * @param {Function} handler - The actual handler function + * @returns {Function} Traced handler + */ +export const withTracing = (operationName, handler) => { + return createTracedHandler({ + operationName, + op: "Electron-ipc.main.handle", + processName: "main", + Sentry, + })(handler); +}; diff --git a/src/main/events.js b/src/main/events.js index c2b5510..abceda0 100644 --- a/src/main/events.js +++ b/src/main/events.js @@ -11,6 +11,7 @@ import logNetworkRequestV2 from "./actions/logNetworkRequestV2"; import getCurrentNetworkLogs from "./actions/getCurrentNetworkLogs"; import * as PrimaryStorageService from "./actions/initPrimaryStorage"; import makeApiClientRequest from "./actions/makeApiClientRequest"; +import { withTracing } from "./actions/tracingPropogation"; import storageService from "../lib/storage"; import { deleteNetworkRecording, @@ -190,9 +191,13 @@ export const registerMainProcessEventsForWebAppWindow = (webAppWindow) => { } }); - ipcMain.handle("get-api-response", async (event, payload) => { - return makeApiClientRequest(payload); - }); + ipcMain.handle( + "get-api-response", + withTracing("get-api-response", async (event, payload) => { + // throw new Error("Intentional Tracing Error"); + return makeApiClientRequest(payload); + }) + ); /* HACKY: Forces regeneration by deleting old cert and closes app */ ipcMain.handle("renew-ssl-certificates", async () => { @@ -215,10 +220,12 @@ export const registerMainProcessEventsForWebAppWindow = (webAppWindow) => { return { success: false, error: "Invalid URL provided" }; } await loadWebAppUrl(url); - return { success: true }; } catch (error) { - return { success: false, error: error?.message ?? "Error changing webapp URL:" }; + return { + success: false, + error: error?.message ?? "Error changing webapp URL:", + }; } }); diff --git a/src/main/lib/tracingMainUtils.ts b/src/main/lib/tracingMainUtils.ts new file mode 100644 index 0000000..4615245 --- /dev/null +++ b/src/main/lib/tracingMainUtils.ts @@ -0,0 +1,59 @@ +interface TraceContext { + "sentry-trace": string; + baggage?: string; +} + +interface TracedHandlerConfig { + operationName: string; + op: string; + processName: string; + Sentry: any; +} + +export function createTracedHandler(config: TracedHandlerConfig) { + const { operationName, op, processName, Sentry } = config; + + return (handler: (event: any, payload: any) => Promise) => { + return async (event: any, payload: any) => { + const { _traceContext, ...actualPayload } = payload || {}; + + if (!_traceContext || !_traceContext["sentry-trace"]) { + return handler(event, actualPayload); + } + + return await Sentry.continueTrace( + { + sentryTrace: _traceContext["sentry-trace"], + baggage: _traceContext.baggage, + }, + async () => { + return await Sentry.startSpan( + { + name: operationName, + op: op, + attributes: { + "ipc.event": operationName, + "ipc.process": processName, + }, + }, + async () => { + try { + return await handler(event, actualPayload); + } catch (error) { + Sentry.captureException(error, { + tags: { + operation: operationName, + process: processName, + component: "ipc-handler", + traced: true, + }, + }); + throw error; + } + } + ); + } + ); + }; + }; +} diff --git a/src/renderer/lib/RPCServiceOverIPC.ts b/src/renderer/lib/RPCServiceOverIPC.ts index 985c163..9a5854b 100644 --- a/src/renderer/lib/RPCServiceOverIPC.ts +++ b/src/renderer/lib/RPCServiceOverIPC.ts @@ -1,5 +1,9 @@ -import { captureException } from "@sentry/browser"; +import * as Sentry from "@sentry/browser"; import { ipcRenderer } from "electron"; +import { + extractTraceContextFromArgs, + executeWithTracing, +} from "./tracingRendererUtils"; /** * Used to create a RPC like service in the Background process. @@ -20,7 +24,6 @@ export class RPCServiceOverIPC { } generateChannelNameForMethod(method: Function) { - console.log("DBG-1: method name", method.name); return `${this.RPC_CHANNEL_PREFIX}${method.name}`; } @@ -29,38 +32,46 @@ export class RPCServiceOverIPC { method: (..._args: any[]) => Promise ) { const channelName = `${this.RPC_CHANNEL_PREFIX}${exposedMethodName}`; - // console.log("DBG-1: exposing channel", channelName, Date.now()); + ipcRenderer.on(channelName, async (_event, args) => { - // console.log( - // "DBG-1: received event on channel", - // channelName, - // _event, - // args, - // Date.now() - // ); + const { traceContext, cleanArgs } = extractTraceContextFromArgs(args); + try { - const result = await method(...args); + const result = await executeWithTracing( + { + traceContext, + spanName: channelName, + op: "Electron-background.rpc", + attributes: { + "rpc.method": exposedMethodName, + "ipc.process": "background", + }, + Sentry, + }, + async () => method(...cleanArgs) + ); - // console.log( - // "DBG-2: result in method", - // result, - // channelName, - // _event, - // args, - // exposedMethodName, - // Date.now() - // ); ipcRenderer.send(`reply-${channelName}`, { success: true, data: result, }); } catch (error: any) { - // console.log( - // `DBG-2: reply-${channelName} error in method`, - // error, - // Date.now() - // ); - captureException(error); + // Capture exception in Sentry with context + Sentry.captureException(error, { + tags: { + process: "electron-background", + component: "rpc-handler", + rpc_method: exposedMethodName, + }, + contexts: { + rpc: { + channel: channelName, + method: exposedMethodName, + args: cleanArgs, + }, + }, + }); + ipcRenderer.send(`reply-${channelName}`, { success: false, data: error.message, diff --git a/src/renderer/lib/tracingRendererUtils.ts b/src/renderer/lib/tracingRendererUtils.ts new file mode 100644 index 0000000..10dde7e --- /dev/null +++ b/src/renderer/lib/tracingRendererUtils.ts @@ -0,0 +1,75 @@ +interface TraceContext { + "sentry-trace": string; + baggage?: string; +} + +interface ExecuteWithTracingConfig { + traceContext: TraceContext | null; + spanName: string; + op: string; + attributes?: Record; + Sentry: any; +} + +export function extractTraceContextFromArgs(args: any[]): { + traceContext: TraceContext | null; + cleanArgs: any[]; +} { + if (!Array.isArray(args) || args.length === 0) { + return { traceContext: null, cleanArgs: args }; + } + + const lastArg = args[args.length - 1]; + + // Check if last argument contains trace context + if (lastArg && typeof lastArg === "object" && lastArg._traceContext) { + return { + traceContext: lastArg._traceContext, + cleanArgs: args.slice(0, -1), + }; + } + + return { traceContext: null, cleanArgs: args }; +} + +export async function executeWithTracing( + config: ExecuteWithTracingConfig, + fn: () => Promise +): Promise { + const { traceContext, spanName, op, attributes = {}, Sentry } = config; + + // If no trace context, execute normally + if (!traceContext || !traceContext["sentry-trace"]) { + return fn(); + } + + return await Sentry.continueTrace( + { + sentryTrace: traceContext["sentry-trace"], + baggage: traceContext.baggage, + }, + async () => { + return await Sentry.startSpan( + { + name: spanName, + op: op, + attributes, + }, + async () => { + try { + return await fn(); + } catch (error) { + Sentry.captureException(error, { + tags: { + operation: spanName, + traced: true, + ...attributes, + }, + }); + throw error; + } + } + ); + } + ); +}