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

feat: move error reporting functionality to the core module #1984

Closed
wants to merge 49 commits into from
Closed
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
02d2f5d
feat: move error reporting functionality to the core module
saikumarrs Dec 31, 2024
b93cb5a
chore: remove unnecessary console statement in test suite
saikumarrs Jan 2, 2025
3026f5f
chore: delete duplicate constants file
saikumarrs Jan 2, 2025
9b0f514
chore: restore error messages filtering logic
saikumarrs Jan 2, 2025
3fd7a5a
refactor: move code around to the right places
saikumarrs Jan 2, 2025
65e3190
chore: remove unnecessary test suites
saikumarrs Jan 2, 2025
9a8df29
fix: error filtering and logging
saikumarrs Jan 2, 2025
c34b0a2
chore: fix test target
saikumarrs Jan 2, 2025
d40d8cf
test: add tests for errorhandler
saikumarrs Jan 2, 2025
702aeb6
test: add more test cases for coverage
saikumarrs Jan 2, 2025
d5d6dff
refactor: remove the need to create new http client
saikumarrs Jan 2, 2025
d1b9ac4
test: refactor browser test suite
saikumarrs Jan 2, 2025
a30f526
chore: adjust size limits
saikumarrs Jan 2, 2025
d2e051e
test: improve browser test suite
saikumarrs Jan 2, 2025
059388a
test: improve code coverage
saikumarrs Jan 2, 2025
a3d8561
chore: use sonarqube scan
saikumarrs Jan 3, 2025
69ac89b
chore: minor improvements
saikumarrs Jan 3, 2025
cb0f336
chore: minor improvements 2
saikumarrs Jan 3, 2025
115ab6a
fix: error handling
saikumarrs Jan 3, 2025
733387f
refactor: address ai bot review comments
saikumarrs Jan 3, 2025
f51cb95
test: add missing unit test for source configuration data
saikumarrs Jan 3, 2025
c852699
chore: address ai bot review comments
saikumarrs Jan 3, 2025
cb8e1d8
fix: user and context details in the error payload
saikumarrs Jan 3, 2025
5c1fbe4
chore: remove invalid test case
saikumarrs Jan 3, 2025
a590ed8
test: fix failing tests
saikumarrs Jan 3, 2025
75f05c6
fix: restore user name in the payload type
saikumarrs Jan 3, 2025
7e0d815
chore: replace deprecated jest apis
saikumarrs Jan 3, 2025
a749734
Merge remote-tracking branch 'origin/develop' into feat.move-error-re…
saikumarrs Jan 7, 2025
4606387
chore: avoid fixing paths in reports
saikumarrs Jan 7, 2025
6e6dba8
chore: fix reports paths to relative
saikumarrs Jan 7, 2025
10bf1a8
chore: fix report paths
saikumarrs Jan 7, 2025
dcf0c70
Merge remote-tracking branch 'origin/develop' into feat.move-error-re…
saikumarrs Jan 7, 2025
8cf04e3
Merge remote-tracking branch 'origin/develop' into feat.move-error-re…
saikumarrs Jan 8, 2025
073c1c6
fix: error message prefix
saikumarrs Jan 9, 2025
96c79cc
fix: allow errors with simple stack trace
saikumarrs Jan 9, 2025
cd9b9e9
fix: add custom message separator
saikumarrs Jan 9, 2025
f623d4b
fix: filter only unhandled errors
saikumarrs Jan 9, 2025
56afb07
fix: callback invocations
saikumarrs Jan 9, 2025
85f5ac0
fix: remove unwanted error handling logic
saikumarrs Jan 9, 2025
7fd54ba
test: add more test cases for coverage
saikumarrs Jan 9, 2025
1b0b0db
test: add more test cases for coverage
saikumarrs Jan 9, 2025
e67f70e
test: add more test cases for improving coverage of error handler
saikumarrs Jan 9, 2025
7a74983
fix: add missing event properties
saikumarrs Jan 10, 2025
a59a002
test: add more test cases for improving coverage of error handler
saikumarrs Jan 10, 2025
c9c6453
test: add more test cases for improving coverage in multiple modules
saikumarrs Jan 10, 2025
ef841ae
test: add more test cases for improving coverage in config manager
saikumarrs Jan 10, 2025
0ecb545
test: add more test cases for improving coverage in plugin engine
saikumarrs Jan 10, 2025
8451e77
test: address ai bot review comments
saikumarrs Jan 10, 2025
f27ad1c
Merge remote-tracking branch 'origin/develop' into feat.move-error-re…
saikumarrs Jan 12, 2025
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: 17 additions & 3 deletions jest/jest.setup-dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,23 @@ global.window.innerHeight = 1024;
global.window.__BUNDLE_ALL_PLUGINS__ = false;
global.window.__IS_LEGACY_BUILD__ = false;
global.window.__IS_DYNAMIC_CUSTOM_BUNDLE__ = false;
global.PromiseRejectionEvent = function (reason) {
this.reason = reason;
};

// Only define the mock if it's not already defined (e.g., in a real browser)
if (typeof PromiseRejectionEvent === 'undefined') {
// Mock class (very minimal)
class PromiseRejectionEvent extends Event {
constructor(type, eventInitDict) {
super(type, eventInitDict);
this.promise = eventInitDict?.promise;
this.reason = eventInitDict?.reason;
}
}

// Attach it to the global object so tests can use it.
global.PromiseRejectionEvent = PromiseRejectionEvent;
// If you rely on "window" instead:
// global.window.PromiseRejectionEvent = PromiseRejectionEvent;
}

// TODO: remove once we use globalThis in analytics v1.1 too
// Setup mocking for window.navigator
Expand Down
9 changes: 3 additions & 6 deletions packages/analytics-js-common/__mocks__/ErrorHandler.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import type { IErrorHandler, PreLoadErrorData } from '../src/types/ErrorHandler';
import { BufferQueue } from './BufferQueue';
import type { IErrorHandler } from '../src/types/ErrorHandler';
import { defaultHttpClient } from './HttpClient';

// Mock all the methods of the ErrorHandler class
class ErrorHandler implements IErrorHandler {
onError = jest.fn();
leaveBreadcrumb = jest.fn();
notifyError = jest.fn();
init = jest.fn();
attachErrorListeners = jest.fn();
errorBuffer = new BufferQueue<PreLoadErrorData>();
httpClient = defaultHttpClient;
}

const defaultErrorHandler = new ErrorHandler();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class HttpClient implements IHttpClient {
getAsyncData = jest.fn();
setAuthHeader = jest.fn();
resetAuthHeader = jest.fn();
init = jest.fn();
}

const defaultHttpClient = new HttpClient();
Expand Down
86 changes: 79 additions & 7 deletions packages/analytics-js-common/__tests__/utilities/errors.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,90 @@
import { dispatchErrorEvent } from '../../src/utilities/errors';
import { dispatchErrorEvent, getStacktrace } from '../../src/utilities/errors';

describe('Errors - utilities', () => {
describe('dispatchErrorEvent', () => {
const dispatchEventMock = jest.fn();
const originalDispatchEvent = globalThis.dispatchEvent;

beforeEach(() => {
globalThis.dispatchEvent = dispatchEventMock;
});

afterEach(() => {
globalThis.dispatchEvent = originalDispatchEvent;
});

it('should dispatch an error event', () => {
const dispatchEvent = jest.fn();
const originalDispatchEvent = globalThis.dispatchEvent;
const error = new Error('Test error');

dispatchErrorEvent(error);

expect(dispatchEventMock).toHaveBeenCalledWith(new ErrorEvent('error', { error }));
expect((error.stack as string).endsWith('[SDK DISPATCHED ERROR]')).toBeTruthy();
});

globalThis.dispatchEvent = dispatchEvent;
it('should decorate stacktrace before dispatching error event', () => {
const error = new Error('Test error');
// @ts-expect-error need to set the value for testing
error.stacktrace = error.stack;
delete error.stack;

dispatchErrorEvent(error);
expect(dispatchEvent).toHaveBeenCalledWith(new ErrorEvent('error', { error }));

// Cleanup
globalThis.dispatchEvent = originalDispatchEvent;
// @ts-expect-error need to check the stacktrace property
expect((error.stacktrace as string).endsWith('[SDK DISPATCHED ERROR]')).toBeTruthy();
});

it('should decorate opera sourceloc before dispatching error event', () => {
const error = new Error('Test error');
// @ts-expect-error need to set the value for testing
error['opera#sourceloc'] = error.stack;
delete error.stack;

dispatchErrorEvent(error);

// @ts-expect-error need to check the opera sourceloc property
expect((error['opera#sourceloc'] as string).endsWith('[SDK DISPATCHED ERROR]')).toBeTruthy();
});
});

describe('getStacktrace', () => {
it('should return stack if it is a string', () => {
const error = new Error('Test error');
expect(getStacktrace(error)).toBe(error.stack);
});

it('should return stacktrace if it is a string', () => {
const error = new Error('Test error');
// @ts-expect-error need to set the value for testing
error.stacktrace = error.stack;
delete error.stack;

// @ts-expect-error need to check the stacktrace property
expect(getStacktrace(error)).toBe(error.stacktrace);
});

it('should return opera sourceloc if it is a string', () => {
const error = new Error('Test error');
// @ts-expect-error need to set the value for testing
error['opera#sourceloc'] = error.stack;
delete error.stack;

// @ts-expect-error need to check the opera sourceloc property
expect(getStacktrace(error)).toBe(error['opera#sourceloc']);
});

it('should return undefined if none of the properties are strings', () => {
const error = new Error('Test error');
delete error.stack;

expect(getStacktrace(error)).toBeUndefined();
});

it('should return undefined if stack is the same as name and message', () => {
const error = new Error('Test error');
error.stack = `${error.name}: ${error.message}`;

expect(getStacktrace(error)).toBeUndefined();
});
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ErrorState } from '../../types/ErrorHandler';
import type { ErrorState, IErrorHandler } from '../../types/ErrorHandler';
import type { ILogger } from '../../types/Logger';

export interface IExternalSourceLoadConfig {
Expand All @@ -11,16 +11,7 @@ export interface IExternalSourceLoadConfig {
}

export interface IExternalSrcLoader {
errorHandler?: {
onError(
error: unknown,
context?: string,
customMessage?: string,
shouldAlwaysThrow?: boolean,
): void;
leaveBreadcrumb(breadcrumb: string): void;
notifyError(error: Error, errorState: ErrorState): void;
};
errorHandler?: IErrorHandler;
logger?: ILogger;
timeout: number;
loadJSFile(config: IExternalSourceLoadConfig): void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ export type Breadcrumb = {
export type ReportingState = {
isErrorReportingEnabled: Signal<boolean>;
isMetricsReportingEnabled: Signal<boolean>;
isErrorReportingPluginLoaded: Signal<boolean>;
breadcrumbs: Signal<Breadcrumb[]>;
};

Expand Down
17 changes: 2 additions & 15 deletions packages/analytics-js-common/src/types/ErrorHandler.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,13 @@
import type { IPluginEngine } from './PluginEngine';
import type { ILogger } from './Logger';
import type { BufferQueue } from '../services/BufferQueue/BufferQueue';
import type { IHttpClient } from './HttpClient';
import type { IExternalSrcLoader } from '../services/ExternalSrcLoader/types';

export type SDKError = unknown | Error | ErrorEvent | Event | PromiseRejectionEvent;

export interface IErrorHandler {
httpClient: IHttpClient;
logger?: ILogger;
pluginEngine?: IPluginEngine;
errorBuffer: BufferQueue<PreLoadErrorData>;
init(httpClient: IHttpClient, externalSrcLoader: IExternalSrcLoader): void;
onError(
error: SDKError,
context?: string,
customMessage?: string,
shouldAlwaysThrow?: boolean,
errorType?: string,
): void;
onError(error: SDKError, context?: string, customMessage?: string, errorType?: string): void;
leaveBreadcrumb(breadcrumb: string): void;
notifyError(error: Error, errorState: ErrorState): void;
attachErrorListeners(): void;
}

export type ErrorState = {
Expand Down
1 change: 1 addition & 0 deletions packages/analytics-js-common/src/types/HttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,5 @@ export interface IHttpClient {
getAsyncData<T = any>(config: IAsyncRequestConfig<T>): void;
setAuthHeader(value: string, noBto?: boolean): void;
resetAuthHeader(): void;
init(errorHandler: IErrorHandler): void;
}
22 changes: 12 additions & 10 deletions packages/analytics-js-common/src/types/Metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,24 @@ export type MetricServicePayload = {
};

export type ErrorEventPayload = {
payloadVersion: string;
notifier: {
name: string;
version: string;
url: string;
};
events: ErrorEventType[];
events: ErrorEvent[];
};

export type ErrorEventType = {
payloadVersion: string;
export type ErrorEvent = {
exceptions: Exception[];
severity: string;
unhandled: boolean;
severityReason: { type: string };
app: {
version: string;
releaseStage: string;
type: string;
};
device: {
locale?: string;
Expand All @@ -41,25 +42,19 @@ export type ErrorEventType = {
clientIp: string;
};
breadcrumbs: Breadcrumb[] | [];
context: string;
metaData: {
[index: string]: any;
};
user: {
id: string;
name: string;
};
};

export type GeneratedEventType = {
errors: Exception[];
};

export interface Exception {
message: string;
errorClass: string;
type: string;
stacktrace: Stackframe[];
}
export interface Stackframe {
file: string;
method?: string;
Expand All @@ -68,3 +63,10 @@ export interface Stackframe {
code?: Record<string, string>;
inProject?: boolean;
}

export interface Exception {
message: string;
errorClass: string;
type: string;
stacktrace: Stackframe[];
}
2 changes: 0 additions & 2 deletions packages/analytics-js-common/src/types/PluginsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,9 @@ export interface IPluginsManager {

export type PluginName =
| 'BeaconQueue'
| 'Bugsnag'
| 'CustomConsentManager'
| 'DeviceModeDestinations'
| 'DeviceModeTransformation'
| 'ErrorReporting'
| 'ExternalAnonymousId'
| 'GoogleLinker'
| 'IubendaConsentManager'
Expand Down
1 change: 1 addition & 0 deletions packages/analytics-js-common/src/types/Source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ export type SourceConfig = {
export type Source = {
id: string;
config?: SourceConfig;
name: string;
workspaceId: string;
};
17 changes: 13 additions & 4 deletions packages/analytics-js-common/src/utilities/checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,20 @@ const isDefinedNotNullAndNotEmptyString = (value: any): boolean =>
isDefinedAndNotNull(value) && value !== '';

/**
* Determines if the input is an instance of Error
* @param obj input value
* @returns true if the input is an instance of Error and false otherwise
* Determines if the input is of type error
* @param value input value
* @returns true if the input is of type error else false
*/
const isTypeOfError = (obj: any): obj is Error => obj instanceof Error;
const isTypeOfError = (value: any): boolean => {
switch (Object.prototype.toString.call(value)) {
case '[object Error]':
case '[object Exception]':
case '[object DOMException]':
return true;
default:
return value instanceof Error;
}
};

export {
isFunction,
Expand Down
39 changes: 36 additions & 3 deletions packages/analytics-js-common/src/utilities/errors.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import { isTypeOfError } from './checks';
import { stringifyWithoutCircular } from './json';

const MANUAL_ERROR_IDENTIFIER = '[MANUAL ERROR]';
const MANUAL_ERROR_IDENTIFIER = '[SDK DISPATCHED ERROR]';

const getStacktrace = (err: any): string | undefined => {
const { stack, stacktrace, name, message } = err;
const operaSourceloc = err['opera#sourceloc'];

const stackString = stack ?? stacktrace ?? operaSourceloc;

if (!!stackString && typeof stackString === 'string' && stack !== `${name}: ${message}`) {
return stackString;
}
return undefined;
};

/**
* Get mutated error with issue prepended to error message
Expand All @@ -21,9 +33,30 @@ const getMutatedError = (err: any, issue: string): Error => {

const dispatchErrorEvent = (error: any) => {
if (isTypeOfError(error)) {
error.stack = `${error.stack ?? ''}\n${MANUAL_ERROR_IDENTIFIER}`;
const errStack = getStacktrace(error);
if (errStack) {
const { stack, stacktrace } = error;
const operaSourceloc = error['opera#sourceloc'];

switch (errStack) {
case stack:
// eslint-disable-next-line no-param-reassign
error.stack = `${stack}\n${MANUAL_ERROR_IDENTIFIER}`;
break;
case stacktrace:
// eslint-disable-next-line no-param-reassign
error.stacktrace = `${stacktrace}\n${MANUAL_ERROR_IDENTIFIER}`;
break;
case operaSourceloc:
default:
// eslint-disable-next-line no-param-reassign
error['opera#sourceloc'] = `${operaSourceloc}\n${MANUAL_ERROR_IDENTIFIER}`;
break;
}
}
}

(globalThis as typeof window).dispatchEvent(new ErrorEvent('error', { error }));
};

export { getMutatedError, dispatchErrorEvent, MANUAL_ERROR_IDENTIFIER };
export { getMutatedError, dispatchErrorEvent, MANUAL_ERROR_IDENTIFIER, getStacktrace };
Loading
Loading