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: error reporting plugin #1601

Merged
merged 47 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
d6be68d
feat: error reporting plugin
MoumitaM Jan 30, 2024
1a7757f
chore: size limit updated
MoumitaM Jan 30, 2024
183e15c
Merge branch 'develop' into feature/sdk-1096-error-reporting-plugin
MoumitaM Jan 30, 2024
edfcaa8
chore: size limit updated
MoumitaM Jan 30, 2024
3e07d44
Merge branch 'develop' into feature/sdk-1096-error-reporting-plugin
MoumitaM Jan 30, 2024
97a6a99
Merge branch 'develop' into feature/sdk-1096-error-reporting-plugin
MoumitaM Jan 31, 2024
97c049e
chore: refactor onerror fn and minor changes
MoumitaM Jan 31, 2024
0b2eed8
chore: review comment addressed
MoumitaM Feb 1, 2024
10f7ce8
refactor: instead of bugsnag core pkg used only required part of even…
MoumitaM Feb 7, 2024
35c89e2
chore: code refactoring
MoumitaM Feb 7, 2024
76b4305
Merge branch 'develop' into feature/sdk-1096-error-reporting-plugin
MoumitaM Feb 7, 2024
cb6e0e8
chore: code refactoring
MoumitaM Feb 8, 2024
ebb0bdf
chore: review commit addressed
MoumitaM Feb 9, 2024
95e68b5
Merge branch 'develop' into feature/sdk-1096-error-reporting-plugin
MoumitaM Feb 9, 2024
a05de70
chore: review comment addressed
MoumitaM Feb 9, 2024
7009640
chore: modified isruddersdkerror fn logic to filter integration sdk e…
MoumitaM Feb 11, 2024
650d8e1
chore: fix plugin loading
MoumitaM Feb 12, 2024
5a50cb3
chore: log message updated
MoumitaM Feb 12, 2024
1cdcd12
chore: resolve conflict
MoumitaM Feb 22, 2024
d2f141f
Merge branch 'develop' into feature/sdk-1096-error-reporting-plugin
bardisg Feb 22, 2024
a9eccd1
Merge branch 'develop' into feature/sdk-1096-error-reporting-plugin
MoumitaM Feb 26, 2024
dea1395
chore: used template literal instead of string concatenation in rollup
MoumitaM Feb 26, 2024
3b48ef4
chore: resolve merge conflicts
MoumitaM Jul 3, 2024
2a6cbd7
fix: unit test cases
MoumitaM Jul 3, 2024
86f74f3
chore: resolve merge conflict
MoumitaM Jul 4, 2024
d3e89ae
Merge branch 'develop' into feature/sdk-1096-error-reporting-plugin
MoumitaM Jul 5, 2024
d2683ed
chore: unit test cases
MoumitaM Jul 8, 2024
99b3c68
chore: lock file modified
MoumitaM Jul 8, 2024
668da10
chore: more unit tests
MoumitaM Jul 8, 2024
679a0e8
Merge branch 'develop' into feature/sdk-1096-error-reporting-plugin
MoumitaM Jul 8, 2024
1556c0a
chore: ignore coverage for third party code
MoumitaM Jul 8, 2024
bc486f7
chore: ignore coverage for third party code in sonar
MoumitaM Jul 9, 2024
5c3489c
chore: test cases
MoumitaM Jul 9, 2024
8ba6802
chore: ignore coverage for third party code in sonar
MoumitaM Jul 9, 2024
8dfa5f5
chore: remove reference
MoumitaM Jul 10, 2024
f217c93
chore: revert formatting changes
MoumitaM Jul 10, 2024
b29f0d7
chore: address review comments
MoumitaM Jul 11, 2024
7a13456
chore: address review comments
MoumitaM Jul 11, 2024
fd1e424
chore: updated plugin signature for backward compatibility
MoumitaM Jul 15, 2024
55a7a9f
chore: added bugsnag plugin for backward compatibility
MoumitaM Jul 17, 2024
786928c
Merge branch 'develop' into feature/sdk-1096-error-reporting-plugin
MoumitaM Jul 17, 2024
0651b04
chore: size limit updated
MoumitaM Jul 17, 2024
017be47
chore: review comment address
MoumitaM Jul 18, 2024
5ee27fc
chore: update metrics service url
MoumitaM Jul 18, 2024
0b7e6fb
chore: address review comment
MoumitaM Jul 18, 2024
e2e94f3
chore: address review comment
MoumitaM Jul 18, 2024
a1bdc87
chore: address review comment
MoumitaM Jul 19, 2024
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
3 changes: 3 additions & 0 deletions jest/jest.setup-dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ 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;
};

// TODO: remove once we use globalThis in analytics v1.1 too
// Setup mocking for window.navigator
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"crypto-es": "2.1.0",
"crypto-js": "4.2.0",
"deep-object-diff": "1.1.9",
"error-stack-parser": "2.1.4",
"get-value": "3.0.1",
"is": "3.3.0",
"join-component": "1.1.0",
Expand Down
3 changes: 3 additions & 0 deletions packages/analytics-js-common/src/constants/metrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const METRICS_PAYLOAD_VERSION = '1';

Check warning on line 1 in packages/analytics-js-common/src/constants/metrics.ts

View check run for this annotation

Codecov / codecov/patch

packages/analytics-js-common/src/constants/metrics.ts#L1

Added line #L1 was not covered by tests

export { METRICS_PAYLOAD_VERSION };

Check warning on line 3 in packages/analytics-js-common/src/constants/metrics.ts

View check run for this annotation

Codecov / codecov/patch

packages/analytics-js-common/src/constants/metrics.ts#L3

Added line #L3 was not covered by tests
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ErrorState } from '@rudderstack/analytics-js-common/types/ErrorHandler';
import type { ILogger } from '../../types/Logger';

export interface IExternalSourceLoadConfig {
Expand All @@ -18,7 +19,7 @@ export interface IExternalSrcLoader {
shouldAlwaysThrow?: boolean,
): void;
leaveBreadcrumb(breadcrumb: string): void;
notifyError(error: Error): void;
notifyError(error: Error, errorState: ErrorState): void;
};
logger?: ILogger;
timeout: number;
Expand Down
13 changes: 12 additions & 1 deletion packages/analytics-js-common/src/types/ApplicationState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,22 @@ export type PluginsState = {
totalPluginsToLoad: Signal<number>;
};

export type BreadcrumbMetaData = {
[index: string]: any;
};
export type BreadcrumbType = 'error' | 'manual';
export type Breadcrumb = {
type: BreadcrumbType;
name: string;
timestamp: Date;
metaData: BreadcrumbMetaData;
};

export type ReportingState = {
isErrorReportingEnabled: Signal<boolean>;
isMetricsReportingEnabled: Signal<boolean>;
errorReportingProviderPluginName: Signal<PluginName | undefined>;
isErrorReportingPluginLoaded: Signal<boolean>;
breadcrumbs: Signal<Breadcrumb[]>;
};

export type SessionState = {
Expand Down
13 changes: 10 additions & 3 deletions packages/analytics-js-common/src/types/ErrorHandler.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import type { IPluginEngine } from './PluginEngine';
import type { ILogger } from './Logger';
import type { IExternalSrcLoader } from '../services/ExternalSrcLoader/types';
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 {
logger?: ILogger;
pluginEngine?: IPluginEngine;
errorBuffer: BufferQueue<PreLoadErrorData>;
init(externalSrcLoader: IExternalSrcLoader): void;
init(httpClient: IHttpClient, externalSrcLoader: IExternalSrcLoader): void;
onError(
error: SDKError,
context?: string,
Expand All @@ -18,7 +19,7 @@
errorType?: string,
): void;
leaveBreadcrumb(breadcrumb: string): void;
notifyError(error: Error): void;
notifyError(error: Error, errorState: ErrorState): void;
attachErrorListeners(): void;
}

Expand All @@ -37,3 +38,9 @@
error: SDKError;
errorState: ErrorState;
};

export enum ErrorType {
HANDLEDEXCEPTION = 'handledException',
UNHANDLEDEXCEPTION = 'unhandledException',
UNHANDLEDREJECTION = 'unhandledPromiseRejection',

Check warning on line 45 in packages/analytics-js-common/src/types/ErrorHandler.ts

View check run for this annotation

Codecov / codecov/patch

packages/analytics-js-common/src/types/ErrorHandler.ts#L43-L45

Added lines #L43 - L45 were not covered by tests
}
70 changes: 70 additions & 0 deletions packages/analytics-js-common/src/types/Metrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { Breadcrumb } from './ApplicationState';

export type MetricServicePayload = {
version: string;
message_id: string;
source: {
name: string;
sdk_version: string;
write_key: string;
install_type: string;
};
errors?: ErrorEventPayload;
};

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

export type ErrorEventType = {
saikumarrs marked this conversation as resolved.
Show resolved Hide resolved
payloadVersion: string;
exceptions: Exception[];
severity: string;
unhandled: boolean;
severityReason: { type: string };
app: {
version: string;
releaseStage: string;
};
device: {
locale?: string;
userAgent?: string;
time?: Date;
};
request: {
url: string;
clientIp: string;
};
breadcrumbs: Breadcrumb[] | [];
context: string;
metaData: {
[index: string]: any;
};
user: {
id: string;
};
};

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

export interface Exception {
message: string;
errorClass: string;
type: string;
stacktrace: Stackframe[];
}
export interface Stackframe {
file: string;
method?: string;
lineNumber?: number;
columnNumber?: number;
code?: Record<string, string>;
inProject?: boolean;
}
4 changes: 2 additions & 2 deletions packages/analytics-js-plugins/.size-limit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default [
{
name: 'Plugins - Legacy - CDN',
path: 'dist/cdn/legacy/plugins/rsa-plugins-*.min.js',
limit: '15 KiB',
limit: '16 KiB',
},
{
name: 'Plugins Module Federation Mapping - Modern - CDN',
Expand All @@ -21,6 +21,6 @@ export default [
{
name: 'Plugins - Modern - CDN',
path: 'dist/cdn/modern/plugins/rsa-plugins-*.min.js',
limit: '6 KiB',
limit: '7.5 KiB',
},
];
117 changes: 72 additions & 45 deletions packages/analytics-js-plugins/__tests__/errorReporting/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { signal } from '@preact/signals-core';
import { clone } from 'ramda';
import type { IHttpClient } from '@rudderstack/analytics-js-common/types/HttpClient';
import { ErrorReporting } from '../../src/errorReporting';

describe('Plugin - ErrorReporting', () => {
Expand All @@ -10,6 +11,15 @@ describe('Plugin - ErrorReporting', () => {
lifecycle: {
writeKey: signal('dummy-write-key'),
},
reporting: {
isErrorReportingPluginLoaded: signal(false),
breadcrumbs: signal([]),
},
context: {
locale: signal('en-GB'),
userAgent: signal('sample user agent'),
app: signal({ version: 'sample_version', installType: 'sample_installType' }),
},
source: signal({
id: 'test-source-id',
config: {},
Expand All @@ -18,6 +28,7 @@ describe('Plugin - ErrorReporting', () => {

let state: any;

// Deprecated code
const mockPluginEngine = {
invokeSingle: jest.fn(() => Promise.resolve()),
};
Expand All @@ -31,6 +42,7 @@ describe('Plugin - ErrorReporting', () => {
notify: jest.fn(),
leaveBreadcrumb: jest.fn(),
};
// End of deprecated code

beforeEach(() => {
state = clone(originalState);
Expand All @@ -39,64 +51,79 @@ describe('Plugin - ErrorReporting', () => {
it('should add ErrorReporting plugin in the loaded plugin list', () => {
ErrorReporting().initialize(state);
expect(state.plugins.loadedPlugins.value.includes('ErrorReporting')).toBe(true);
expect(state.reporting.isErrorReportingPluginLoaded.value).toBe(true);
expect(state.reporting.breadcrumbs.value[0].name).toBe('Error Reporting Plugin Loaded');
});

it('should reject the promise if source information is not available', async () => {
state.source.value = null;

const pluginInitPromise = ErrorReporting().errorReporting.init(state);

await expect(pluginInitPromise).rejects.toThrow('Invalid source configuration or source id.');
});

it('should invoke the error reporting provider plugin on init', async () => {
const pluginInitPromise = ErrorReporting().errorReporting.init(
it('should invoke the error reporting provider plugin on notify', () => {
const mockHttpClient = {
getAsyncData: jest.fn(),
setAuthHeader: jest.fn(),
} as unknown as IHttpClient;
const newError = new Error();
const normalizedError = Object.create(newError, {
message: { value: 'ReferenceError: testUndefinedFn is not defined' },
stack: {
value: `ReferenceError: testUndefinedFn is not defined at Analytics.page (http://localhost:3001/cdn/modern/iife/rsa.js:1610:3) at RudderAnalytics.page (http://localhost:3001/cdn/modern/iife/rsa.js:1666:84)`,
},
});
ErrorReporting().errorReporting.notify(
{},
undefined,
normalizedError,
state,
mockPluginEngine,
mockExtSrcLoader,
mockLogger,
undefined,
mockHttpClient,
{
severity: 'error',
unhandled: false,
severityReason: { type: 'handledException' },
},
);

await expect(pluginInitPromise).resolves.toBeUndefined(); // because it's just a mock
expect(mockPluginEngine.invokeSingle).toHaveBeenCalledWith(
'errorReportingProvider.init',
state,
mockExtSrcLoader,
mockLogger,
);
expect(mockHttpClient.getAsyncData).toHaveBeenCalled();
});

it('should invoke the error reporting provider plugin on notify', () => {
it('should not notify if the error is not an SDK error', () => {
const mockHttpClient = {
getAsyncData: jest.fn(),
setAuthHeader: jest.fn(),
} as unknown as IHttpClient;
const newError = new Error();
const normalizedError = Object.create(newError, {
message: { value: 'ReferenceError: testUndefinedFn is not defined' },
stack: {
value: `ReferenceError: testUndefinedFn is not defined at Abcd.page (http://localhost:3001/cdn/modern/iife/abc.js:1610:3) at xyz.page (http://localhost:3001/cdn/modern/iife/abc.js:1666:84)`,
},
});
ErrorReporting().errorReporting.notify(
mockPluginEngine,
mockErrReportingProviderClient,
new Error('dummy error'),
{},
undefined,
normalizedError,
state,
mockLogger,
undefined,
mockHttpClient,
{
severity: 'error',
unhandled: false,
severityReason: { type: 'handledException' },
},
);

expect(mockPluginEngine.invokeSingle).toHaveBeenCalledWith(
'errorReportingProvider.notify',
mockErrReportingProviderClient,
new Error('dummy error'),
state,
mockLogger,
);
expect(mockHttpClient.getAsyncData).not.toHaveBeenCalled();
});

it('should invoke the error reporting provider plugin on breadcrumb', () => {
ErrorReporting().errorReporting.breadcrumb(
mockPluginEngine,
mockErrReportingProviderClient,
'dummy breadcrumb',
mockLogger,
);
it('should add a new breadcrumb', () => {
const breadcrumbLength = state.reporting.breadcrumbs.value.length;
ErrorReporting().errorReporting.breadcrumb({}, undefined, 'dummy breadcrumb', undefined, state);

expect(mockPluginEngine.invokeSingle).toHaveBeenCalledWith(
'errorReportingProvider.breadcrumb',
mockErrReportingProviderClient,
'dummy breadcrumb',
mockLogger,
);
expect(state.reporting.breadcrumbs.value.length).toBe(breadcrumbLength + 1);
});

it('should not add a new breadcrumb if the message is missing', () => {
const breadcrumbLength = state.reporting.breadcrumbs.value.length;
ErrorReporting().errorReporting.breadcrumb({}, undefined, undefined, undefined, state);

expect(state.reporting.breadcrumbs.value.length).not.toBe(breadcrumbLength + 1);
});
});
Loading