From 83f57bdaffd7c2255eb033b7bfed38ff22a94486 Mon Sep 17 00:00:00 2001 From: Aarushsr12 Date: Thu, 26 Feb 2026 21:39:39 +0530 Subject: [PATCH 1/2] [desktop]distibuted-tracing --- src/main/actions/tracingHelpers.js | 51 ++++++++++++ src/main/events.js | 18 +++-- src/renderer/lib/RPCServiceOverIPC.ts | 107 +++++++++++++++++++------- 3 files changed, 145 insertions(+), 31 deletions(-) create mode 100644 src/main/actions/tracingHelpers.js diff --git a/src/main/actions/tracingHelpers.js b/src/main/actions/tracingHelpers.js new file mode 100644 index 0000000..70de819 --- /dev/null +++ b/src/main/actions/tracingHelpers.js @@ -0,0 +1,51 @@ +const Sentry = require("@sentry/electron/main"); + +export const withTracing = (operationName, handler) => { + return async (event, payload) => { + // Extract trace + const { _traceContext, ...actualPayload } = payload; + + if (!_traceContext || !_traceContext["sentry-trace"]) { + console.log( + `[Tracing] No trace context found for ${operationName}, running without trace` + ); + return handler(event, actualPayload); + } + + console.log(`[Tracing] Continuing trace for ${operationName}`); + console.log(`[Tracing] sentry-trace: ${_traceContext["sentry-trace"]}`); + + return await Sentry.continueTrace( + { + sentryTrace: _traceContext["sentry-trace"], + baggage: _traceContext.baggage, + }, + async () => { + return await Sentry.startSpan( + { + name: operationName, + op: "Electron-ipc.main.handle", + attributes: { + "ipc.event": operationName, + }, + }, + async () => { + try { + return await handler(event, actualPayload); + } catch (error) { + Sentry.captureException(error, { + tags: { + operation: operationName, + process: "main", + component: "ipc-handler", + traced: true, + }, + }); + throw error; + } + } + ); + } + ); + }; +}; diff --git a/src/main/events.js b/src/main/events.js index c2b5510..1bb023b 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/tracingHelpers"; import storageService from "../lib/storage"; import { deleteNetworkRecording, @@ -190,9 +191,14 @@ 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("Testing distributed tracing - intentional error"); + 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 +221,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/renderer/lib/RPCServiceOverIPC.ts b/src/renderer/lib/RPCServiceOverIPC.ts index 985c163..4fd8f5c 100644 --- a/src/renderer/lib/RPCServiceOverIPC.ts +++ b/src/renderer/lib/RPCServiceOverIPC.ts @@ -1,4 +1,4 @@ -import { captureException } from "@sentry/browser"; +import * as Sentry from "@sentry/browser"; import { ipcRenderer } from "electron"; /** @@ -20,7 +20,6 @@ export class RPCServiceOverIPC { } generateChannelNameForMethod(method: Function) { - console.log("DBG-1: method name", method.name); return `${this.RPC_CHANNEL_PREFIX}${method.name}`; } @@ -29,38 +28,94 @@ 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() - // ); + console.log(`[Background RPC] ${channelName} - Raw args:`, args); + console.log( + `[Background RPC] ${channelName} - Args is array?`, + Array.isArray(args) + ); + console.log( + `[Background RPC] ${channelName} - Args length:`, + args?.length + ); + + // Extract trace context from last argument (added by traceIPC.invokeEventInBG) + const lastArg = args[args.length - 1]; + console.log(`[Background RPC] ${channelName} - Last arg:`, lastArg); + + const hasTraceContext = + lastArg && typeof lastArg === "object" && lastArg._traceContext; + const traceContext = hasTraceContext ? lastArg._traceContext : null; + const cleanArgs = hasTraceContext ? args.slice(0, -1) : args; + + console.log( + `[Background RPC] ${channelName} - Trace context:`, + traceContext ? "present" : "missing", + traceContext + ); + try { - const result = await method(...args); + let result; + + if (traceContext) { + console.log( + `[Background RPC] ${channelName} - Starting traced execution` + ); + console.log( + `[Background RPC] sentry-trace:`, + traceContext["sentry-trace"] + ); + console.log(`[Background RPC] baggage:`, traceContext.baggage); + // Continue distributed trace from React + result = await Sentry.continueTrace( + { + sentryTrace: traceContext["sentry-trace"], + baggage: traceContext.baggage, + }, + async () => { + return await Sentry.startSpan( + { + name: channelName, + op: "Electron-background.rpc", + attributes: { + "rpc.method": exposedMethodName, + }, + }, + async () => { + return await method(...cleanArgs); + } + ); + } + ); + } else { + // No trace context, execute normally + result = await 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); + console.error(`[Background RPC] Error in ${channelName}:`, 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, From 165f193acc44127108e00463cb4ee5e36c45b8fe Mon Sep 17 00:00:00 2001 From: Aarushsr12 Date: Fri, 27 Feb 2026 03:13:12 +0530 Subject: [PATCH 2/2] fix: seperate main & renderer utils --- src/main/actions/tracingHelpers.js | 51 ---------------- src/main/actions/tracingPropogation.js | 17 ++++++ src/main/events.js | 5 +- src/main/lib/tracingMainUtils.ts | 59 ++++++++++++++++++ src/renderer/lib/RPCServiceOverIPC.ts | 78 ++++++------------------ src/renderer/lib/tracingRendererUtils.ts | 75 +++++++++++++++++++++++ 6 files changed, 170 insertions(+), 115 deletions(-) delete mode 100644 src/main/actions/tracingHelpers.js create mode 100644 src/main/actions/tracingPropogation.js create mode 100644 src/main/lib/tracingMainUtils.ts create mode 100644 src/renderer/lib/tracingRendererUtils.ts diff --git a/src/main/actions/tracingHelpers.js b/src/main/actions/tracingHelpers.js deleted file mode 100644 index 70de819..0000000 --- a/src/main/actions/tracingHelpers.js +++ /dev/null @@ -1,51 +0,0 @@ -const Sentry = require("@sentry/electron/main"); - -export const withTracing = (operationName, handler) => { - return async (event, payload) => { - // Extract trace - const { _traceContext, ...actualPayload } = payload; - - if (!_traceContext || !_traceContext["sentry-trace"]) { - console.log( - `[Tracing] No trace context found for ${operationName}, running without trace` - ); - return handler(event, actualPayload); - } - - console.log(`[Tracing] Continuing trace for ${operationName}`); - console.log(`[Tracing] sentry-trace: ${_traceContext["sentry-trace"]}`); - - return await Sentry.continueTrace( - { - sentryTrace: _traceContext["sentry-trace"], - baggage: _traceContext.baggage, - }, - async () => { - return await Sentry.startSpan( - { - name: operationName, - op: "Electron-ipc.main.handle", - attributes: { - "ipc.event": operationName, - }, - }, - async () => { - try { - return await handler(event, actualPayload); - } catch (error) { - Sentry.captureException(error, { - tags: { - operation: operationName, - process: "main", - component: "ipc-handler", - traced: true, - }, - }); - throw error; - } - } - ); - } - ); - }; -}; 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 1bb023b..abceda0 100644 --- a/src/main/events.js +++ b/src/main/events.js @@ -11,7 +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/tracingHelpers"; +import { withTracing } from "./actions/tracingPropogation"; import storageService from "../lib/storage"; import { deleteNetworkRecording, @@ -194,8 +194,7 @@ export const registerMainProcessEventsForWebAppWindow = (webAppWindow) => { ipcMain.handle( "get-api-response", withTracing("get-api-response", async (event, payload) => { - // throw new Error("Testing distributed tracing - intentional error"); - throw new Error("Intentional Tracing Error"); + // throw new Error("Intentional Tracing Error"); return makeApiClientRequest(payload); }) ); 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 4fd8f5c..9a5854b 100644 --- a/src/renderer/lib/RPCServiceOverIPC.ts +++ b/src/renderer/lib/RPCServiceOverIPC.ts @@ -1,5 +1,9 @@ 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. @@ -30,76 +34,28 @@ export class RPCServiceOverIPC { const channelName = `${this.RPC_CHANNEL_PREFIX}${exposedMethodName}`; ipcRenderer.on(channelName, async (_event, args) => { - console.log(`[Background RPC] ${channelName} - Raw args:`, args); - console.log( - `[Background RPC] ${channelName} - Args is array?`, - Array.isArray(args) - ); - console.log( - `[Background RPC] ${channelName} - Args length:`, - args?.length - ); - - // Extract trace context from last argument (added by traceIPC.invokeEventInBG) - const lastArg = args[args.length - 1]; - console.log(`[Background RPC] ${channelName} - Last arg:`, lastArg); - - const hasTraceContext = - lastArg && typeof lastArg === "object" && lastArg._traceContext; - const traceContext = hasTraceContext ? lastArg._traceContext : null; - const cleanArgs = hasTraceContext ? args.slice(0, -1) : args; - - console.log( - `[Background RPC] ${channelName} - Trace context:`, - traceContext ? "present" : "missing", - traceContext - ); + const { traceContext, cleanArgs } = extractTraceContextFromArgs(args); try { - let result; - - if (traceContext) { - console.log( - `[Background RPC] ${channelName} - Starting traced execution` - ); - console.log( - `[Background RPC] sentry-trace:`, - traceContext["sentry-trace"] - ); - console.log(`[Background RPC] baggage:`, traceContext.baggage); - // Continue distributed trace from React - result = await Sentry.continueTrace( - { - sentryTrace: traceContext["sentry-trace"], - baggage: traceContext.baggage, + const result = await executeWithTracing( + { + traceContext, + spanName: channelName, + op: "Electron-background.rpc", + attributes: { + "rpc.method": exposedMethodName, + "ipc.process": "background", }, - async () => { - return await Sentry.startSpan( - { - name: channelName, - op: "Electron-background.rpc", - attributes: { - "rpc.method": exposedMethodName, - }, - }, - async () => { - return await method(...cleanArgs); - } - ); - } - ); - } else { - // No trace context, execute normally - result = await method(...cleanArgs); - } + Sentry, + }, + async () => method(...cleanArgs) + ); ipcRenderer.send(`reply-${channelName}`, { success: true, data: result, }); } catch (error: any) { - console.error(`[Background RPC] Error in ${channelName}:`, error); - // Capture exception in Sentry with context Sentry.captureException(error, { tags: { 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; + } + } + ); + } + ); +}