console: programmaticallyTraceError logs to the Console too

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7714
GitOrigin-RevId: 7307093f9db4a871a43011705b42c06332c57d18
This commit is contained in:
Stefano Magni 2023-02-03 15:45:18 +01:00 committed by hasura-bot
parent 710a33bf45
commit 03bf7e8c6f
11 changed files with 136 additions and 60 deletions

View File

@ -64,7 +64,6 @@ export const clickRunQueryButton = () => {
'Could not find run query button in the DOM (.execute-button)'
);
programmaticallyTraceError(error);
console.warn(error);
}
};

View File

@ -164,13 +164,10 @@ export function useCreateHasuraCloudDatasource(
setState(prevState => {
if (prevState.status === 'adding-env-var') {
// this is an unexpected error; so we need alerts about this
programmaticallyTraceError(
new Error('Failed creating env vars in Hasura'),
{
sourceError: error,
errorMessage: error.message ?? '',
}
);
programmaticallyTraceError({
error: 'Failed creating env vars in Hasura',
cause: error,
});
return {
status: 'adding-env-var-failed',
payload: { dbUrl },
@ -178,13 +175,10 @@ export function useCreateHasuraCloudDatasource(
// if adding data-source fails unexpectedly, set the error state
} else if (prevState.status === 'adding-data-source') {
// this is an unexpected error; so we need alerts about this
programmaticallyTraceError(
new Error('Failed adding created data source in Hasura'),
{
sourceError: error,
errorMessage: error.message ?? '',
}
);
programmaticallyTraceError({
error: 'Failed adding created data source in Hasura',
cause: error,
});
return {
status: 'adding-data-source-failed',

View File

@ -134,7 +134,6 @@ export function Analytics(props: AnalyticsProps) {
const overrideError = new Error(
`All the following attributes will be overridden: ${overrideHtmlAttributes} for the element with name "${name}"`
);
console.error(overrideError);
programmaticallyTraceError(overrideError);
}
}

View File

@ -1,13 +1,113 @@
import type { ExceptionContext } from './sentry/captureException';
import * as Sentry from '@sentry/react';
import { captureException } from './sentry/captureException';
type HumanReadableString = string;
type Options = {
level?: 'error' | 'warning';
/**
* Logging to the Console has a specific purpose: Sentry is NOT enabled for every type of Console
* (at the time of writing, Sentry is enabled only for the Cloud Console). Logging to the browser's
* console allows the eventual customers that is trying to understand the root cause of a problem,
* to report what they see in the browser's console in the issue they are going to open.
*/
logToConsole?: boolean;
};
type HumanReadableStringWithErrorCauseAndOptions = Options & {
error: HumanReadableString;
cause?: Error;
};
type ErrorWithOptions = Options & {
error: Error;
};
const noop = () => {};
/**
* Programmatically trace a caught error.
*
* @example <caption>Simplest usage: pass just a string.</caption>
* programmaticallyTraceError('Something went wrong when updating the metadata')
*
* @example <caption>Simplest usage: pass the error you receive.</caption>
* catch (error) {
* programmaticallyTraceError(error)
* }
*
* @example <caption>Pass a human-readable message, but also the causing error.</caption>
* catch (error) {
* programmaticallyTraceError({ error: 'Something went wrong when updating the metadata', cause: error })
* }
*/
export function programmaticallyTraceError(
error: Error,
exceptionContext: ExceptionContext = {},
level: 'error' | 'warning' = 'error'
errorOrErrorWithOptions:
| HumanReadableString
| Error
| HumanReadableStringWithErrorCauseAndOptions
| ErrorWithOptions
) {
captureException(error, exceptionContext, level);
// --------------------------------------------------
// SIMPLE USAGE
// --------------------------------------------------
// If you pass a string, it's converted to an error and passed to Sentry.
if (typeof errorOrErrorWithOptions === 'string') {
captureException(new Error(errorOrErrorWithOptions));
return;
}
// If you pass an error, it's passed to Sentry as is.
if (errorOrErrorWithOptions instanceof Error) {
captureException(errorOrErrorWithOptions);
return;
}
// --------------------------------------------------
// OPTIONS-RICH USAGE
// --------------------------------------------------
const {
error,
level = 'error',
logToConsole = true,
} = errorOrErrorWithOptions;
const consoleLog = logToConsole
? level === 'warning'
? console.warn
: console.error
: noop;
// If you pass a cause, the cause itself is the original error and IT IS the error that will be
// tracked to Sentry. Instead, the passed human-friendly string will be added as a breadcrumb that
// you can see in Sentry right after the error.
if ('cause' in errorOrErrorWithOptions && !!errorOrErrorWithOptions.cause) {
const { cause } = errorOrErrorWithOptions;
const message = typeof error === 'string' ? error : error.message;
Sentry.addBreadcrumb({ level, message });
captureException(cause);
consoleLog(message);
consoleLog(cause);
return;
}
// The string will be converted to an error and tracked in Sentry
if (typeof error === 'string') {
const errorToLog = new Error(error);
captureException(errorToLog, level);
consoleLog(errorToLog);
return;
}
// The error is tracked in Sentry
captureException(error, level);
consoleLog(error);
}

View File

@ -1,23 +1,15 @@
import * as Sentry from '@sentry/react';
export type ExceptionContext = {
sourceError?: Error;
errorMessage?: string;
};
/**
* This function allows us to capture caught exceptions that we want the engineering team to be
* alerted about
* A simple wrapper around Sentry's captureException.
*
* @see https://docs.sentry.io/platforms/javascript/enriching-events/context/
*/
export function captureException(
error: Error,
exceptionContext: ExceptionContext = {},
level: 'error' | 'warning' = 'error'
) {
Sentry.captureException(error, {
level,
contexts: { debug: exceptionContext },
});
}

View File

@ -56,6 +56,12 @@ export function startSentryTracing(globalVars: Globals, envVars: EnvVars) {
// sensitive data
dom: false,
}),
// ATTENTION: functions like programmaticallyTraceError could internally log errors to the
// browser's console, causing an infinite loop!
// new CaptureConsoleIntegration({
// levels: ['error'],
// }),
],
// Allow grouping logs by environment

View File

@ -62,13 +62,10 @@ export function TemplateSummary(props: Props) {
staleTime,
onError: (e: any) => {
// this is unexpected; so get alerted
programmaticallyTraceError(
new Error('failed to get a sample query in template summary'),
{
sourceError: e,
errorMessage: e.message ?? '',
}
);
programmaticallyTraceError({
error: 'failed to get a sample query in template summary',
cause: e,
});
},
});

View File

@ -118,7 +118,6 @@ export const emitOnboardingEvent = (variables: Record<string, unknown>) => {
variables,
cloudHeaders
).catch(error => {
console.error(error);
programmaticallyTraceError(error);
});
};

View File

@ -159,14 +159,10 @@ export function WorkflowProgress(props: WorkflowProgressProps) {
);
},
error => {
programmaticallyTraceError(
new Error('failed subscribing to one click deployment status'),
{
errorMessage: error.message,
sourceError: error,
},
'error'
);
programmaticallyTraceError({
error: 'failed subscribing to one click deployment status',
cause: error,
});
}
);
return () => {

View File

@ -80,12 +80,9 @@ export function useOnSetOpenTelemetryError(
}
);
programmaticallyTraceError(
new Error(
'OpenTelemetry set_opentelemetry_config error not parsed',
// @ts-expect-error This error will automatically disappear with Nx that targets new browsers by default
{ cause: err }
)
);
programmaticallyTraceError({
error: 'OpenTelemetry set_opentelemetry_config error not parsed',
cause: err instanceof Error ? err : undefined,
});
};
}

View File

@ -58,11 +58,8 @@ export function useTrackTypeMisalignments(
}
);
programmaticallyTraceError(
new Error(
'OpenTelemetry metadata not parsed',
// @ts-expect-error This error will automatically disappear with Nx that targets new browsers by default
{ cause: result.error }
)
);
programmaticallyTraceError({
error: 'OpenTelemetry metadata not parsed',
cause: result.error,
});
}