Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#2517 improve tRPC error handling #2520

Merged
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
20 changes: 20 additions & 0 deletions src/webviews/api/configuration/appRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* This a minimal tRPC server
*/
import { callWithTelemetryAndErrorHandling } from '@microsoft/vscode-azext-utils';
import * as vscode from 'vscode';
import { z } from 'zod';
import { type API } from '../../../AzureDBExperiences';
import { collectionsViewRouter as collectionViewRouter } from '../../mongoClusters/collectionView/collectionViewRouter';
Expand Down Expand Up @@ -88,6 +89,25 @@ const commonRouter = router({
},
);
}),
displayErrorMessage: publicProcedure
.input(
z.object({
message: z.string(),
modal: z.boolean(),
cause: z.string(),
}),
)
.mutation(({ input }) => {
let message = input.message;
if (input.cause && !input.modal) {
message += ` (${input.cause})`;
}

void vscode.window.showErrorMessage(message, {
modal: input.modal,
detail: input.modal ? input.cause : undefined, // The content of the 'detail' field is only shown when modal is true
});
}),
hello: publicProcedure
// This is the input schema of your procedure, no parameters
.query(async () => {
Expand Down
41 changes: 40 additions & 1 deletion src/webviews/api/extension-server/WebviewController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { getTRPCErrorFromUnknown } from '@trpc/server';
import * as vscode from 'vscode';
import { type API } from '../../../AzureDBExperiences';
import { appRouter, type BaseRouterContext } from '../configuration/appRouter';
Expand Down Expand Up @@ -95,7 +96,8 @@ export class WebviewController<Configuration> extends WebviewBaseController<Conf

this._panel.webview.postMessage(response);
} catch (error) {
console.log(error);
const trpcErrorMessage = this.wrapInTrpcErrorMessage(error, message.id);
this._panel.webview.postMessage(trpcErrorMessage);
}

break;
Expand All @@ -104,6 +106,43 @@ export class WebviewController<Configuration> extends WebviewBaseController<Conf
);
}

/**
* Wraps an error into a TRPC error message format suitable for sending via `postMessage`.
*
* This function manually constructs the error object by extracting the necessary properties
* from the `errorEntry`. This is important because when using `postMessage` to send data
* from the extension to the webview, the data is serialized (e.g., using `JSON.stringify`).
* During serialization, only own enumerable properties of the object are included, while
* properties inherited from the prototype chain or non-enumerable properties are omitted.
*
* Error objects like instances of `Error` or `TRPCError` often have their properties
* (such as `message`, `name`, and `stack`) either inherited from the prototype or defined
* as non-enumerable. As a result, directly passing such error objects to `postMessage`
* would result in the webview receiving an error object without these essential properties.
*
* By explicitly constructing a plain object with the required error properties, we ensure
* that all necessary information is included during serialization and properly received
* by the webview.
*
* @param error - The error to be wrapped.
* @param operationId - The ID of the operation associated with the error.
* @returns An object containing the operation ID and a plain error object with own enumerable properties.
*/
wrapInTrpcErrorMessage(error: unknown, operationId: string) {
const errorEntry = getTRPCErrorFromUnknown(error);

return {
id: operationId,
error: {
code: errorEntry.code,
name: errorEntry.name,
message: errorEntry.message,
stack: errorEntry.stack,
cause: errorEntry.cause,
},
};
}

protected _getWebview(): vscode.Webview {
return this._panel.webview;
}
Expand Down
15 changes: 10 additions & 5 deletions src/webviews/api/extension-server/trpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import { callWithTelemetryAndErrorHandling } from '@microsoft/vscode-azext-utils';
import { initTRPC } from '@trpc/server';
// eslint-disable-next-line import/no-internal-modules
import { type MiddlewareResult } from '@trpc/server/dist/unstable-core-do-not-import/middleware';
import { type MiddlewareResult } from '@trpc/server/dist/unstable-core-do-not-import/middleware'; //TODO: the API for v11 is not stable and will change, revisit when upgrading TRPC

/**
* Initialization of tRPC backend.
Expand Down Expand Up @@ -44,13 +44,18 @@ export const trpcToTelemetry = t.middleware(async ({ path, type, next }) => {
const result = await next();

if (!result.ok) {
context.telemetry.properties.result = 'Failed';
context.telemetry.properties.error = result.error.message;

/**
* we're not any error here as we just want to log it here and let the
* we're not handling any error here as we just want to log it here and let the
* caller of the RPC call handle the error there.
*/

context.telemetry.properties.result = 'Failed';
context.telemetry.properties.error = result.error.name;
context.telemetry.properties.errorMessage = result.error.message;
context.telemetry.properties.errorStack = result.error.stack;
if (result.error.cause) {
context.telemetry.properties.errorCause = JSON.stringify(result.error.cause, null, 0);
}
}

return result;
Expand Down
9 changes: 6 additions & 3 deletions src/webviews/api/webview-client/vscodeLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,13 @@ export interface VsCodeLinkResponseMessage {
id: string;
result?: unknown;
error?: {
code?: number;
name: string;
message: string;

code?: number;
stack?: string;
cause?: unknown;
data?: unknown;
name: string;
};
complete?: boolean;
}
Expand Down Expand Up @@ -60,7 +63,7 @@ function vscodeLink(options: VSCodeLinkOptions): TRPCLink<AppRouter> {
* Notes to maintainers:
* - types of messages have been derived from node_modules/@trpc/client/src/links/types.ts
* It was not straightforward to import them directly due to the use of `@trpc/server/unstable-core-do-not-import`
* Fell free to revisit once tRPC reaches version 11.0.0
* TODO: Fell free to revisit once tRPC reaches version 11.0.0
*/

// The link function required by tRPC client
Expand Down
85 changes: 54 additions & 31 deletions src/webviews/mongoClusters/collectionView/CollectionView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,14 @@ export const CollectionView = (): JSX.Element => {

setCurrentContext((prev) => ({ ...prev, isLoading: false, isFirstTimeLoad: false }));
})
.catch((_error) => {
.catch((error) => {
void trpcClient.common.displayErrorMessage.mutate({
message: 'Error while running the query',
modal: false,
cause: error instanceof Error ? error.message : String(error),
});
})
.finally(() => {
setCurrentContext((prev) => ({ ...prev, isLoading: false, isFirstTimeLoad: false }));
});
}, [currentContext.currrentQueryDefinition]);
Expand Down Expand Up @@ -181,7 +188,11 @@ export const CollectionView = (): JSX.Element => {
}));
})
.catch((error) => {
console.debug('Failed to perform an action:', error);
void trpcClient.common.displayErrorMessage.mutate({
message: 'Error while loading the data',
modal: false,
cause: error instanceof Error ? error.message : String(error),
});
});
break;
}
Expand All @@ -195,7 +206,11 @@ export const CollectionView = (): JSX.Element => {
}));
})
.catch((error) => {
console.debug('Failed to perform an action:', error);
void trpcClient.common.displayErrorMessage.mutate({
message: 'Error while loading the data',
modal: false,
cause: error instanceof Error ? error.message : String(error),
});
});
break;
case Views.JSON:
Expand All @@ -208,7 +223,11 @@ export const CollectionView = (): JSX.Element => {
}));
})
.catch((error) => {
console.debug('Failed to perform an action:', error);
void trpcClient.common.displayErrorMessage.mutate({
message: 'Error while loading the data',
modal: false,
cause: error instanceof Error ? error.message : String(error),
});
});
break;
default:
Expand All @@ -223,7 +242,11 @@ export const CollectionView = (): JSX.Element => {
void (await currentContextRef.current.queryEditor?.setJsonSchema(schema));
})
.catch((error) => {
console.debug('Failed to perform an action:', error);
void trpcClient.common.displayErrorMessage.mutate({
message: 'Error while loading the autocompletion data',
modal: false,
cause: error instanceof Error ? error.message : String(error),
});
});
}

Expand Down Expand Up @@ -259,46 +282,46 @@ export const CollectionView = (): JSX.Element => {
},
}));
})
.catch((error: unknown) => {
if (error instanceof Error) {
console.error('Error deleting the document:', error.message);
} else {
console.error('Unexpected error when deleting a document:', error);
}
.catch((error) => {
void trpcClient.common.displayErrorMessage.mutate({
message: 'Error deleting selected documents',
modal: false,
cause: error instanceof Error ? error.message : String(error),
});
});
}

function handleViewDocumentRequest(): void {
trpcClient.mongoClusters.collectionView.viewDocumentById
.mutate(currentContext.dataSelection.selectedDocumentObjectIds[0])
.catch((error: unknown) => {
if (error instanceof Error) {
console.error('Error opening document:', error.message);
} else {
console.error('Unexpected error opening document:', error);
}
.catch((error) => {
void trpcClient.common.displayErrorMessage.mutate({
message: 'Error opening the document view',
modal: false,
cause: error instanceof Error ? error.message : String(error),
});
});
}

function handleEditDocumentRequest(): void {
trpcClient.mongoClusters.collectionView.editDocumentById
.mutate(currentContext.dataSelection.selectedDocumentObjectIds[0])
.catch((error: unknown) => {
if (error instanceof Error) {
console.error('Error opening document:', error.message);
} else {
console.error('Unexpected error opening document:', error);
}
.catch((error) => {
void trpcClient.common.displayErrorMessage.mutate({
message: 'Error opening the document view',
modal: false,
cause: error instanceof Error ? error.message : String(error),
});
});
}

function handleAddDocumentRequest(): void {
trpcClient.mongoClusters.collectionView.addDocument.mutate().catch((error: unknown) => {
if (error instanceof Error) {
console.error('Error adding document:', error.message);
} else {
console.error('Unexpected error adding document:', error);
}
trpcClient.mongoClusters.collectionView.addDocument.mutate().catch((error) => {
void trpcClient.common.displayErrorMessage.mutate({
message: 'Error opening the document view',
modal: false,
cause: error instanceof Error ? error.message : String(error),
});
});
}

Expand Down Expand Up @@ -340,7 +363,7 @@ export const CollectionView = (): JSX.Element => {
},
})
.catch((error) => {
console.debug('Failed to report query event:', error);
console.debug('Failed to report an event:', error);
});
}

Expand Down Expand Up @@ -371,7 +394,7 @@ export const CollectionView = (): JSX.Element => {
},
})
.catch((error) => {
console.debug('Failed to report query event:', error);
console.debug('Failed to report an event:', error);
});
}}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const ToolbarQueryOperations = (): JSX.Element => {
},
})
.catch((error) => {
console.debug('Failed to report query event:', error);
console.debug('Failed to report an event:', error);
});
};

Expand All @@ -81,7 +81,7 @@ const ToolbarQueryOperations = (): JSX.Element => {
},
})
.catch((error) => {
console.debug('Failed to report query event:', error);
console.debug('Failed to report an event:', error);
});
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ export const ToolbarTableNavigation = (): JSX.Element => {
depth: newPath.length ?? 0,
},
})
.catch((_error) => {
console.debug(_error);
.catch((error) => {
console.debug('Failed to report an event:', error);
});
}

Expand All @@ -75,8 +75,8 @@ export const ToolbarTableNavigation = (): JSX.Element => {
depth: newPath.length ?? 0,
},
})
.catch((_error) => {
console.debug(_error);
.catch((error) => {
console.debug('Failed to report an event:', error);
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const ToolbarViewNavigation = (): JSX.Element => {
},
})
.catch((error) => {
console.debug('Failed to report pagination event:', error);
console.debug('Failed to report an event:', error);
});
}

Expand Down Expand Up @@ -73,7 +73,7 @@ export const ToolbarViewNavigation = (): JSX.Element => {
},
})
.catch((error) => {
console.debug('Failed to report pagination event:', error);
console.debug('Failed to report an event:', error);
});
}

Expand All @@ -97,7 +97,7 @@ export const ToolbarViewNavigation = (): JSX.Element => {
},
})
.catch((error) => {
console.debug('Failed to report pagination event:', error);
console.debug('Failed to report an event:', error);
});
}

Expand Down Expand Up @@ -125,7 +125,7 @@ export const ToolbarViewNavigation = (): JSX.Element => {
},
})
.catch((error) => {
console.debug('Failed to report pagination event:', error);
console.debug('Failed to report an event:', error);
});
}

Expand Down
Loading
Loading