summaryrefslogtreecommitdiff
path: root/shared/logger/src/errorkit
diff options
context:
space:
mode:
Diffstat (limited to 'shared/logger/src/errorkit')
-rw-r--r--shared/logger/src/errorkit/errorkit-logger.ts93
-rw-r--r--shared/logger/src/errorkit/errorkit.ts108
2 files changed, 201 insertions, 0 deletions
diff --git a/shared/logger/src/errorkit/errorkit-logger.ts b/shared/logger/src/errorkit/errorkit-logger.ts
new file mode 100644
index 0000000..1290c41
--- /dev/null
+++ b/shared/logger/src/errorkit/errorkit-logger.ts
@@ -0,0 +1,93 @@
+import type { ErrorHub, ValueOf } from './types';
+import type { LoggerFactory, Logger } from '../types';
+
+/**
+ * Determines the level of logs to send to sentry.
+ *
+ */
+export const ERROR_REPORT_LEVEL = {
+ error: 'error',
+ error_warn: 'error_warn',
+} as const;
+
+type ReportLevel = ValueOf<typeof ERROR_REPORT_LEVEL>;
+
+export class ErrorKitLoggerFactory implements LoggerFactory {
+ private readonly errorKit: ErrorHub;
+ private readonly reportLevel: ReportLevel;
+ constructor(errorKit: ErrorHub, reportLevel?: ReportLevel) {
+ this.errorKit = errorKit;
+ this.reportLevel = reportLevel ?? ERROR_REPORT_LEVEL.error;
+ }
+ loggerFor(name: string): Logger {
+ return new ErrorKitLogger(name, this.errorKit, this.reportLevel);
+ }
+}
+
+interface HasToString {
+ toString(): string;
+}
+
+export class ErrorKitLogger implements Logger {
+ private readonly name: string;
+ private readonly errorKit: ErrorHub;
+ private readonly reportLevel: ReportLevel;
+ constructor(name: string, errorKit: ErrorHub, reportLevel: ReportLevel) {
+ this.name = name;
+ this.errorKit = errorKit;
+ this.reportLevel = reportLevel;
+ }
+
+ private stringifyConsoleArgs(...args: unknown[]): string {
+ return args.reduce((acc: string, val: unknown) => {
+ let tempVal: HasToString;
+ switch (true) {
+ case val instanceof Error: {
+ tempVal = (val as unknown as InstanceType<typeof Error>)
+ .message;
+ break;
+ }
+ case typeof val === 'object': {
+ try {
+ tempVal = JSON.stringify(val);
+ } catch (e) {
+ tempVal = `failed to stringify ${val}`;
+ }
+ break;
+ }
+ case typeof val === 'undefined' || val === null: {
+ tempVal = `${val}`;
+ break;
+ }
+ default: {
+ tempVal = val as HasToString;
+ }
+ }
+
+ return `${acc} ${tempVal.toString()}`;
+ }, `[${this.name}]`) as string;
+ }
+
+ debug(..._args: unknown[]): string {
+ return '';
+ }
+ info(..._args: unknown[]): string {
+ return '';
+ }
+ warn(...args: unknown[]): string {
+ if (this.reportLevel === ERROR_REPORT_LEVEL.error_warn) {
+ this.errorKit.captureMessage(this.stringifyConsoleArgs(...args));
+ }
+ return '';
+ }
+ error(...args: unknown[]): string {
+ const errors = args.filter((item) => item instanceof Error) as Error[];
+ const message = this.stringifyConsoleArgs(...args);
+
+ const error = errors.length === 0 ? new Error(message) : errors[0];
+ error.message = message;
+
+ this.errorKit.captureException(error);
+ return '';
+ }
+}
diff --git a/shared/logger/src/errorkit/errorkit.ts b/shared/logger/src/errorkit/errorkit.ts
new file mode 100644
index 0000000..dd40e26
--- /dev/null
+++ b/shared/logger/src/errorkit/errorkit.ts
@@ -0,0 +1,108 @@
+import { Severity } from '@sentry/types';
+import type { Logger, LoggerFactory } from '../types';
+import type {
+ captureException,
+ captureMessage,
+ addBreadcrumb,
+ ErrorHub,
+ ErrorKitConfig,
+} from './types';
+
+type PartialSentryModule = {
+ captureException: typeof captureException;
+ captureMessage: typeof captureMessage;
+ addBreadcrumb: typeof addBreadcrumb;
+};
+
+export type ErrorKitInstance = InstanceType<typeof ErrorKit>;
+
+export const setupErrorKit = (
+ config: ErrorKitConfig,
+ loggerFactory: LoggerFactory,
+): ErrorKitInstance | undefined => {
+ if (typeof window === 'undefined') return;
+ const log = loggerFactory.loggerFor('errorkit');
+ const isMultiDev = window.location.href.includes('multidev');
+ const BUILD_ENV = process.env.NODE_ENV;
+ const isErrorKitEnabled = BUILD_ENV === 'production' && !isMultiDev;
+
+ const initializeErrorKit =
+ async (): Promise<PartialSentryModule | null> => {
+ let sentry: PartialSentryModule | null = null;
+
+ if (isErrorKitEnabled) {
+ try {
+ const { createSentryConfig } = await import(
+ '@amp-metrics/sentrykit'
+ );
+ const Sentry = await import('@sentry/browser');
+ Sentry.init(createSentryConfig(config));
+
+ sentry = {
+ addBreadcrumb: Sentry.addBreadcrumb,
+ captureException: Sentry.captureException,
+ captureMessage: Sentry.captureMessage,
+ };
+ } catch (e) {
+ log.error('something went wrong setting up errorKit', e);
+ }
+ }
+
+ return sentry;
+ };
+
+ return new ErrorKit(initializeErrorKit(), log, isErrorKitEnabled);
+};
+
+class ErrorKit implements ErrorHub {
+ private readonly sentry: Promise<PartialSentryModule | null>;
+ private readonly logger: Logger;
+ private readonly isErrorKitEnabled: boolean;
+ constructor(
+ sentry: Promise<PartialSentryModule | null>,
+ log: Logger,
+ isErrorKitEnabled: boolean,
+ ) {
+ this.sentry = sentry;
+ this.logger = log;
+ this.isErrorKitEnabled = isErrorKitEnabled;
+
+ if (!isErrorKitEnabled) {
+ log.debug('errorkit is disabled');
+ }
+ }
+
+ async captureMessage(message: string) {
+ if (!this.isErrorKitEnabled) return;
+ const sentry = await this.sentry;
+
+ if (sentry) {
+ sentry.addBreadcrumb({
+ category: 'log.warn',
+ level: Severity.Warning,
+ });
+ sentry.captureMessage(message, Severity.Warning);
+ } else {
+ this.logger.warn(`${message} was not sent to errorKit`);
+ }
+ }
+
+ async captureException(exception: Error) {
+ if (!this.isErrorKitEnabled) return;
+ const sentry = await this.sentry;
+
+ if (sentry) {
+ sentry.addBreadcrumb({
+ type: 'error',
+ category: 'error',
+ level: Severity.Error,
+ });
+ sentry.captureException(exception);
+ } else {
+ this.logger.warn(
+ `The following exception was not sent to errorKit:`,
+ exception,
+ );
+ }
+ }
+}