Skip to content
Merged
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
5 changes: 5 additions & 0 deletions src/services/rollbar/clientConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ export function buildClientConfig(accessToken: string): Configuration {

// Filter out client-side navigation cancellations
'cancelled navigation',

// Opaque cross-origin script errors: the browser masks the details of an uncaught error
// thrown by a different-origin script (CORS), leaving only this string with no stack or
// payload. Unactionable, and sourced from third-party scripts we don't control.
'Script error',
],
maxItems: 10, // Max items per page load
// uncaught / unhandledrejection coverage is owned by the early window listeners in queue.ts —
Expand Down
29 changes: 20 additions & 9 deletions src/services/rollbar/queue.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,6 @@ describe('rollbar queue', () => {
expect(rollbarInstance.warn).not.toHaveBeenCalled();
expect(errorHandler).toBeTypeOf('function');
expect(removeEventListenerSpy).toHaveBeenCalledWith('error', errorHandler, true);
expect(removeEventListenerSpy).toHaveBeenCalledWith('unhandledrejection', expect.any(Function));
});
});

Expand All @@ -175,21 +174,33 @@ describe('rollbar queue', () => {
remove();
});

it('should buffer unhandledrejection events until init', async() => {
it('should report a non-Error thrown value under a fallback message with the value as custom data', async() => {
const queue = await importQueue();
const remove = queue.installEarlyListeners();
const reason = new Error('rejected promise');
// A synchronous `throw` of a non-Error value would otherwise reach Rollbar as a bare object
// and be filed as a generic "null or missing arguments." item (issue #3566, subtask 3).
const thrown = { code: 'BOOM' };

window.dispatchEvent(new PromiseRejectionEvent('unhandledrejection', {
promise: Promise.resolve(),
reason,
}));
expect(rollbarInstance.error).not.toHaveBeenCalled();
window.dispatchEvent(new ErrorEvent('error', { message: 'Uncaught object', error: thrown }));
await queue.init();

expect(rollbarInstance.error).toHaveBeenCalledWith(
'Uncaught object',
{ client_timestamp: CALL_TIME_S, error: thrown },
);

remove();
});

it('should fall back to the event message when there is no error object', async() => {
const queue = await importQueue();
const remove = queue.installEarlyListeners();

window.dispatchEvent(new ErrorEvent('error', { message: 'Script error.', error: null }));
await queue.init();

expect(rollbarInstance.error).toHaveBeenCalledWith(
reason,
'Script error.',
{ client_timestamp: CALL_TIME_S },
);

Expand Down
36 changes: 26 additions & 10 deletions src/services/rollbar/queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,32 @@ function withClientTimestamp(args: Array<Rollbar.LogArgument>, timestamp: number
return next;
}

const UNCAUGHT_ERROR_FALLBACK_MESSAGE = 'Uncaught error';

/**
* Captures uncaught errors / unhandled rejections during (and after) the SDK deferral window.
* Kept for the page lifetime on success — removed if init fails. Rollbar's own capture flags stay
* off to avoid double-reporting.
* Coerces a thrown value into arguments Rollbar can build an occurrence from. Passed a bare
* non-Error object (or `null`) as its sole argument, Rollbar discards the payload and files a
* generic "Item sent with null or missing arguments." occurrence — so anything that is not an
* `Error` or `string` is reported under {@link UNCAUGHT_ERROR_FALLBACK_MESSAGE} with the raw value
* preserved as custom data.
*/
function toReport(value: unknown, message: string): Array<Rollbar.LogArgument> {
if (value instanceof Error || typeof value === 'string') {
return [ value ];
}
if (value === null || value === undefined) {
return [ message || UNCAUGHT_ERROR_FALLBACK_MESSAGE ];
}
return [ message || UNCAUGHT_ERROR_FALLBACK_MESSAGE, { error: value } ];
}

/**
* Captures uncaught errors during (and after) the SDK deferral window. Kept for the page lifetime
* on success — removed if init fails. Rollbar's own `captureUncaught` stays off to avoid
* double-reporting; `captureUnhandledRejections` is left off deliberately — on public instances
* unhandled rejections are dominated by wallet-extension / third-party noise with no usable
* payload (they file empty "null or missing arguments" items), and genuine page crashes surface as
* `critical` through the React error boundary, not here.
*/
export function installEarlyListeners(): () => void {
if (!isEnabled() || earlyListenersInstalled || typeof window === 'undefined') {
Expand All @@ -152,19 +174,13 @@ export function installEarlyListeners(): () => void {
if (event.target instanceof Element) {
return;
}
log('error', [ event.error ?? event.message ]);
};

const handleRejection = (event: PromiseRejectionEvent) => {
log('error', [ event.reason ]);
log('error', toReport(event.error, event.message));
};

window.addEventListener('error', handleError, true);
window.addEventListener('unhandledrejection', handleRejection);

const uninstall = () => {
window.removeEventListener('error', handleError, true);
window.removeEventListener('unhandledrejection', handleRejection);
earlyListenersInstalled = false;
if (uninstallEarlyListeners === uninstall) {
uninstallEarlyListeners = undefined;
Expand Down
Loading