Skip to content

Commit

Permalink
#2517 improve tRPC error handling (#2520)
Browse files Browse the repository at this point in the history
* reporting errors as a correct TRPC Error Message
* Handling TRPCErrors in webviews
* Minor telemetry error reporting update
  • Loading branch information
tnaum-ms authored Dec 12, 2024
1 parent ca69c8b commit bab2fe6
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 57 deletions.
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

0 comments on commit bab2fe6

Please sign in to comment.