Skip to content

Commit

Permalink
Redirect logs/uncaught errors from webview to the main extension logg…
Browse files Browse the repository at this point in the history
…er (#621)

This PR adds a mechanism for redirecting logs and unhandled error from
webview to the main extension.

The reason for this change is that we can see potential issues and logs
in a unified log output that we create for the extension context. This
way we provide better visibility of errors that happen in the webview.
Without this change, we'd always need to check both the extension log
output, and the vscode devtools panel. With this change, having
everything in a single output is specifically helpful when receiving
logs from the users, as we no longer need to ask them to check devtools
console and can focus on a single log file they need to check or send as
a part of the report.

### How Has This Been Tested: 
Add log statement and unhandled error to the main webview App.tsx. See
that this gets included in the main log output.
  • Loading branch information
kmagiera authored Oct 14, 2024
1 parent 2903512 commit b0e114f
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 32 deletions.
2 changes: 2 additions & 0 deletions packages/vscode-extension/src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ export interface UtilsInterface {
showDismissableError(errorMessage: string): Promise<void>;

openExternalUrl(uriString: string): Promise<void>;

log(type: "info" | "error" | "warn" | "log", message: string, ...args: any[]): Promise<void>;
}
60 changes: 31 additions & 29 deletions packages/vscode-extension/src/panels/WebviewController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { DependencyManager } from "../dependency/DependencyManager";
import { DeviceManager } from "../devices/DeviceManager";
import { Project } from "../project/project";
import { Logger } from "../Logger";
import { extensionContext } from "../utilities/extensionContext";
import { WorkspaceConfigController } from "./WorkspaceConfigController";
import { getTelemetryReporter } from "../utilities/telemetry";
import { Utils } from "../utilities/utils";
Expand All @@ -15,10 +14,10 @@ type CallArgs = {
method: string;
args: unknown[];
};
export type WebviewEvent =
| {
command: "call";
} & CallArgs;

export type WebviewEvent = {
command: "call";
} & CallArgs;

export class WebviewController implements Disposable {
private readonly dependencyManager: DependencyManager;
Expand Down Expand Up @@ -92,15 +91,13 @@ export class WebviewController implements Disposable {
private setWebviewMessageListener(webview: Webview) {
webview.onDidReceiveMessage(
(message: WebviewEvent) => {
const isTouchEvent = message.command === "call" && message.method === "dispatchTouches";
if (!isTouchEvent) {
// ignore dispatchTouches and log calls from being logged as "Message from webview"
if (message.method !== "dispatchTouches" && message.method !== "log") {
Logger.log("Message from webview", message);
}

switch (message.command) {
case "call":
this.handleRemoteCall(message);
return;
if (message.command === "call") {
this.handleRemoteCall(message);
}
},
undefined,
Expand All @@ -113,27 +110,32 @@ export class WebviewController implements Disposable {
const callableObject = this.callableObjects.get(object);
if (callableObject && method in callableObject) {
const argsWithCallbacks = args.map((arg: any) => {
if (typeof arg === "object" && arg !== null && "__callbackId" in arg) {
const callbackId = arg.__callbackId;
let callback = this.idToCallback.get(callbackId)?.deref();
if (!callback) {
callback = (...options: any[]) => {
this.webview.postMessage({
command: "callback",
callbackId,
args: options,
});
};
this.idToCallback.set(callbackId, new WeakRef(callback));
if (this.idToCallback.size > 200) {
Logger.warn("Too many callbacks in memory! Something is wrong!");
if (typeof arg === "object" && arg !== null) {
if ("__callbackId" in arg) {
const callbackId = arg.__callbackId;
let callback = this.idToCallback.get(callbackId)?.deref();
if (!callback) {
callback = (...options: any[]) => {
this.webview.postMessage({
command: "callback",
callbackId,
args: options,
});
};
this.idToCallback.set(callbackId, new WeakRef(callback));
if (this.idToCallback.size > 200) {
Logger.warn("Too many callbacks in memory! Something is wrong!");
}
this.idToCallbackFinalizationRegistry.register(callback, callbackId);
}
this.idToCallbackFinalizationRegistry.register(callback, callbackId);
return callback;
} else if ("__error" in arg) {
const error = new Error(arg.__error.message);
Object.assign(error, arg.__error);
return error;
}
return callback;
} else {
return arg;
}
return arg;
});
// @ts-ignore
const result = callableObject[method](...argsWithCallbacks);
Expand Down
4 changes: 4 additions & 0 deletions packages/vscode-extension/src/utilities/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,8 @@ export class Utils implements UtilsInterface {
public async openExternalUrl(uriString: string) {
env.openExternal(Uri.parse(uriString));
}

public async log(type: "info" | "error" | "warn" | "log", message: string, ...args: any[]) {
Logger[type]("[WEBVIEW LOG]", message, ...args);
}
}
4 changes: 3 additions & 1 deletion packages/vscode-extension/src/webview/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import AlertProvider from "./providers/AlertProvider";
import WorkspaceConfigProvider from "./providers/WorkspaceConfigProvider";

import "./styles/theme.css";
import UtilsProvider from "./providers/UtilsProvider";
import { UtilsProvider, installLogOverrides } from "./providers/UtilsProvider";
import LaunchConfigProvider from "./providers/LaunchConfigProvider";

installLogOverrides();

const container = document.getElementById("root");
const root = createRoot(container);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const utils = makeProxy<Utils>("Utils");

const UtilsContext = createContext<UtilsInterface>(utils);

export default function UtilsProvider({ children }: PropsWithChildren) {
export function UtilsProvider({ children }: PropsWithChildren) {
return <UtilsContext.Provider value={utils}>{children}</UtilsContext.Provider>;
}

Expand All @@ -33,3 +33,29 @@ export function useUtils() {
}
return context;
}

export function installLogOverrides() {
function wrapConsole(methodName: "log" | "info" | "warn" | "error") {
const consoleMethod = console[methodName];
console[methodName] = (message: string, ...args: any[]) => {
utils.log(methodName, message, ...args);
consoleMethod(message, ...args);
};
}

(["log", "info", "warn", "error"] as const).forEach(wrapConsole);

// install uncaught exception handler
window.addEventListener("error", (event) => {
utils.log("error", "Uncaught exception", event.error.stack);
// rethrow the error to be caught by the global error handler
throw event.error;
});

// install uncaught promise rejection handler
window.addEventListener("unhandledrejection", (event) => {
utils.log("error", "Uncaught promise rejection", event.reason);
// rethrow the error to be caught by the global error handler
throw event.reason;
});
}
10 changes: 9 additions & 1 deletion packages/vscode-extension/src/webview/utilities/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,15 @@ export function makeProxy<T extends object>(objectName: string) {
return (...args: any[]) => {
const currentCallId = `${instanceToken}:${globalCallCounter++}`;
let argsWithCallbacks = args.map((arg) => {
if (typeof arg === "function") {
if (arg instanceof Error) {
return {
__error: {
name: arg.name,
message: arg.message,
stack: arg.stack,
},
};
} else if (typeof arg === "function") {
maybeInitializeCallbackMessageListener();
const callbackId =
callbackToID.get(arg) || `${instanceToken}:${globalCallbackCounter++}`;
Expand Down

0 comments on commit b0e114f

Please sign in to comment.