Skip to content
New issue

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

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

Already on GitHub? # to your account

cleanup: wrap ResponseErrors for Sentry #1061

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
13 changes: 2 additions & 11 deletions src/clients/sidecar/apis/ConnectionsResourceApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,9 @@
* Do not edit the class manually.
*/

import type { Connection, ConnectionSpec, ConnectionsList } from "../models/index";
import { ConnectionFromJSON, ConnectionSpecToJSON, ConnectionsListFromJSON } from "../models/index";
import * as runtime from "../runtime";
import type { Connection, ConnectionSpec, ConnectionsList, Failure } from "../models/index";
import {
ConnectionFromJSON,
ConnectionToJSON,
ConnectionSpecFromJSON,
ConnectionSpecToJSON,
ConnectionsListFromJSON,
ConnectionsListToJSON,
FailureFromJSON,
FailureToJSON,
} from "../models/index";

export interface GatewayV1ConnectionsIdDeleteRequest {
id: string;
Expand Down
104 changes: 72 additions & 32 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ export function getNestedErrorChain(error: Error): Record<string, string | undef
return chain;
}

export class WrappedResponseError extends Error {
constructor(
public readonly message: string,
public readonly cause: AnyResponseError,
) {
super(message);
this.name = "WrappedResponseError";
}
}

/**
* Log the provided error along with any additional information, and optionally send to Sentry.
*
Expand All @@ -73,52 +83,82 @@ export async function logError(
extra: Record<string, string> = {},
sendTelemetry: boolean = false,
): Promise<void> {
let errorMessage: string = "";
const errorInfo: ErrorInfo | undefined = await extractErrorInformation(e, messagePrefix);
if (!errorInfo) {
// not an Error type, just log the message
logger.error(`${messagePrefix}: ${e}`, extra);
return;
}

logger.error(errorInfo.message, { ...errorInfo.errorContext, ...extra });
if (sendTelemetry) {
Sentry.captureException(e, {
contexts: { response: { status_code: errorInfo.responseStatusCode } },
extra: { ...errorInfo.errorContext, ...extra },
});
}
}

interface ErrorInfo {
error: Error;
message: string;
errorContext: Record<string, string | number | boolean | null | undefined>;
responseStatusCode: number | undefined;
}

export async function extractErrorInformation(
e: unknown,
messagePrefix: string,
): Promise<ErrorInfo | undefined> {
if (!(e instanceof Error)) {
return;
}
const error = e as Error;

let message = "";
let errorContext: Record<string, string | number | boolean | null | undefined> = {};
let responseStatusCode: number | undefined;

if ((e as any).response) {
if ((error as AnyResponseError).response) {
// one of our ResponseError types, attempt to extract more information before logging
const error = e as AnyResponseError;
const errorBody = await error.response.clone().text();
errorMessage = `[${messagePrefix}] error response:`;
const responseError = e as AnyResponseError;
const resp: Response = responseError.response;
const errorBody: string = await resp.clone().text();
message = `[${messagePrefix}] error response:`;
errorContext = {
responseStatus: error.response.status,
responseStatusText: error.response.statusText,
responseStatus: resp.status,
responseStatusText: resp.statusText,
responseBody: errorBody.slice(0, 5000), // limit to 5000 characters
responseErrorType: error.name,
};
responseStatusCode = error.response.status;
responseStatusCode = responseError.response.status;
// wrap the error and keep the current ResponseError as the `cause` property
e = new WrappedResponseError(
`ResponseError: ${resp.status} ${resp.statusText} @ ${resp.url}`,
responseError,
);
} else {
// something we caught that wasn't actually a ResponseError type but was passed in here anyway
errorMessage = `[${messagePrefix}] error: ${e}`;
if (e instanceof Error) {
// top-level error
// non-ResponseError Error type
message = `[${messagePrefix}] error: ${e}`;
errorContext = {
errorType: e.name,
errorMessage: e.message,
errorStack: e.stack,
};
}

// also handle any nested errors starting from the `cause` property
if (hasErrorCause(error)) {
const errorChain = getNestedErrorChain(error.cause);
if (errorChain.length) {
errorContext = {
errorType: e.name,
errorMessage: e.message,
errorStack: e.stack,
...errorContext,
errors: JSON.stringify(errorChain, null, 2),
};
// also handle any nested errors starting from the `cause` property
if (hasErrorCause(e)) {
const errorChain = getNestedErrorChain(e.cause);
if (errorChain.length) {
errorContext = {
...errorContext,
errors: JSON.stringify(errorChain, null, 2),
};
}
}
}
}

logger.error(errorMessage, { ...errorContext, ...extra });
if (sendTelemetry) {
Sentry.captureException(e, {
contexts: { response: { status_code: responseStatusCode } },
extra: { ...errorContext, ...extra },
});
}
return { error, message, errorContext, responseStatusCode };
}

/** Shows the error notification with `message` and custom action buttons.
Expand Down