Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/main/actions/tracingPropogation.js
Original file line number Diff line number Diff line change
@@ -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);
};
17 changes: 12 additions & 5 deletions src/main/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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:",
};
}
});

Expand Down
59 changes: 59 additions & 0 deletions src/main/lib/tracingMainUtils.ts
Original file line number Diff line number Diff line change
@@ -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<any>) => {
return async (event: any, payload: any) => {
const { _traceContext, ...actualPayload } = payload || {};

if (!_traceContext || !_traceContext["sentry-trace"]) {
return handler(event, actualPayload);
}
Comment on lines +17 to +22
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Destructuring may corrupt primitive payloads.

If payload is a primitive value (string, number, boolean), the object destructuring at line 18 will produce unexpected results:

  • String payload: actualPayload becomes { 0: 'f', 1: 'o', 2: 'o' } (spreads characters)
  • Number/boolean: actualPayload becomes {}

Consider adding a type guard if primitive payloads are possible.

💡 Proposed fix
    return async (event: any, payload: any) => {
+     // Handle non-object payloads (primitives)
+     if (payload === null || payload === undefined || typeof payload !== "object") {
+       return handler(event, payload);
+     }
+
      const { _traceContext, ...actualPayload } = payload || {};

      if (!_traceContext || !_traceContext["sentry-trace"]) {
        return handler(event, actualPayload);
      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return async (event: any, payload: any) => {
const { _traceContext, ...actualPayload } = payload || {};
if (!_traceContext || !_traceContext["sentry-trace"]) {
return handler(event, actualPayload);
}
return async (event: any, payload: any) => {
// Handle non-object payloads (primitives)
if (payload === null || payload === undefined || typeof payload !== "object") {
return handler(event, payload);
}
const { _traceContext, ...actualPayload } = payload || {};
if (!_traceContext || !_traceContext["sentry-trace"]) {
return handler(event, actualPayload);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/lib/tracingMainUtils.ts` around lines 17 - 22, The destructuring of
payload in the wrapper returned by the function can corrupt primitive payloads;
add a type guard before destructuring so you only extract _traceContext when
payload is a non-null object (e.g. check typeof payload === 'object' && payload
!== null), otherwise set actualPayload = payload directly; update the logic
around the existing _traceContext check and the call to handler(event,
actualPayload) so primitives pass through unchanged and only object payloads are
split into _traceContext and 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;
}
}
);
}
);
};
};
}
63 changes: 37 additions & 26 deletions src/renderer/lib/RPCServiceOverIPC.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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}`;
}

Expand All @@ -29,38 +32,46 @@ export class RPCServiceOverIPC {
method: (..._args: any[]) => Promise<any>
) {
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,
Expand Down
75 changes: 75 additions & 0 deletions src/renderer/lib/tracingRendererUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
interface TraceContext {
"sentry-trace": string;
baggage?: string;
}

interface ExecuteWithTracingConfig {
traceContext: TraceContext | null;
spanName: string;
op: string;
attributes?: Record<string, any>;
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<T>(
config: ExecuteWithTracingConfig,
fn: () => Promise<T>
): Promise<T> {
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;
}
}
);
}
);
}