Skip to content

Commit

Permalink
ref(angular): Set ErrorHandler Exception Mechanism to be unhandled by…
Browse files Browse the repository at this point in the history
… default(#3844)

Previously, exceptions caught in the Sentry `ErrorHandler` defaulted to the generic mechanism, meaning they were marked as `handled: true` and `mechanism: generic`. This patch adds a custom mechanism for these events: `handled: false` (because they were caught in our ErrorHandler) and `mechanism: angular`.
  • Loading branch information
onurtemizkan committed Jul 22, 2022
1 parent 25c3dc7 commit 3665831
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 1 deletion.
17 changes: 16 additions & 1 deletion packages/angular/src/errorhandler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { HttpErrorResponse } from '@angular/common/http';
import { ErrorHandler as AngularErrorHandler, Inject, Injectable } from '@angular/core';
import * as Sentry from '@sentry/browser';
import { captureException } from '@sentry/browser';
import { addExceptionMechanism } from '@sentry/utils';

import { runOutsideAngular } from './zone';

Expand Down Expand Up @@ -40,7 +42,20 @@ class SentryErrorHandler implements AngularErrorHandler {
const extractedError = this._extractError(error) || 'Handled unknown error';

// Capture handled exception and send it to Sentry.
const eventId = runOutsideAngular(() => Sentry.captureException(extractedError));
const eventId = runOutsideAngular(() =>
captureException(extractedError, scope => {
scope.addEventProcessor(event => {
addExceptionMechanism(event, {
type: 'angular',
handled: false,
});

return event;
});

return scope;
}),
);

// When in development mode, log the error to console for immediate feedback.
if (this._options.logErrors) {
Expand Down
124 changes: 124 additions & 0 deletions packages/angular/test/errorhandler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { HttpErrorResponse } from '@angular/common/http';
import * as SentryBrowser from '@sentry/browser';
import { Scope } from '@sentry/browser';
import * as SentryUtils from '@sentry/utils';

import { createErrorHandler, SentryErrorHandler } from '../src/errorhandler';

const FakeScope = new Scope();

jest.mock('@sentry/browser', () => {
const original = jest.requireActual('@sentry/browser');
return {
...original,
captureException: (err: unknown, cb: (arg0?: unknown) => unknown) => {
cb(FakeScope);
return original.captureException(err, cb);
},
};
});

const captureExceptionSpy = jest.spyOn(SentryBrowser, 'captureException');

jest.spyOn(console, 'error').mockImplementation();

describe('SentryErrorHandler', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('`createErrorHandler `creates a SentryErrorHandler with an empty config', () => {
const errorHandler = createErrorHandler();

expect(errorHandler).toBeInstanceOf(SentryErrorHandler);
});

it('handleError method assigns the correct mechanism', () => {
const addEventProcessorSpy = jest.spyOn(FakeScope, 'addEventProcessor').mockImplementationOnce(callback => {
void callback({}, { event_id: 'fake-event-id' });
return FakeScope;
});

const addExceptionMechanismSpy = jest.spyOn(SentryUtils, 'addExceptionMechanism');

const errorHandler = createErrorHandler();
errorHandler.handleError(new Error('test'));

expect(addEventProcessorSpy).toBeCalledTimes(1);
expect(addExceptionMechanismSpy).toBeCalledTimes(1);
expect(addExceptionMechanismSpy).toBeCalledWith({}, { handled: false, type: 'angular' });
});

it('handleError method extracts `null` error', () => {
createErrorHandler().handleError(null);

expect(captureExceptionSpy).toHaveBeenCalledTimes(1);
expect(captureExceptionSpy).toHaveBeenCalledWith('Handled unknown error', expect.any(Function));
});

it('handleError method extracts `undefined` error', () => {
createErrorHandler().handleError(undefined);

expect(captureExceptionSpy).toHaveBeenCalledTimes(1);
expect(captureExceptionSpy).toHaveBeenCalledWith('Handled unknown error', expect.any(Function));
});

it('handleError method extracts a string', () => {
const str = 'sentry-test';
createErrorHandler().handleError(str);

expect(captureExceptionSpy).toHaveBeenCalledTimes(1);
expect(captureExceptionSpy).toHaveBeenCalledWith(str, expect.any(Function));
});

it('handleError method extracts an empty Error', () => {
const err = new Error();
createErrorHandler().handleError(err);

expect(captureExceptionSpy).toHaveBeenCalledTimes(1);
expect(captureExceptionSpy).toHaveBeenCalledWith(err, expect.any(Function));
});

it('handleError method extracts an Error with `ngOriginalError`', () => {
const ngErr = new Error('sentry-ng-test');
const err = {
ngOriginalError: ngErr,
};

createErrorHandler().handleError(err);

expect(captureExceptionSpy).toHaveBeenCalledTimes(1);
expect(captureExceptionSpy).toHaveBeenCalledWith(ngErr, expect.any(Function));
});

it('handleError method extracts an `HttpErrorResponse` with `Error`', () => {
const httpErr = new Error('sentry-http-test');
const err = new HttpErrorResponse({ error: httpErr });

createErrorHandler().handleError(err);

expect(captureExceptionSpy).toHaveBeenCalledTimes(1);
expect(captureExceptionSpy).toHaveBeenCalledWith(httpErr, expect.any(Function));
});

it('handleError method extracts an `HttpErrorResponse` with `ErrorEvent`', () => {
const httpErr = new ErrorEvent('http', { message: 'sentry-http-test' });
const err = new HttpErrorResponse({ error: httpErr });

createErrorHandler().handleError(err);

expect(captureExceptionSpy).toHaveBeenCalledTimes(1);
expect(captureExceptionSpy).toHaveBeenCalledWith('sentry-http-test', expect.any(Function));
});

it('handleError method extracts an `HttpErrorResponse` with string', () => {
const err = new HttpErrorResponse({ error: 'sentry-http-test' });
createErrorHandler().handleError(err);

expect(captureExceptionSpy).toHaveBeenCalledTimes(1);
expect(captureExceptionSpy).toHaveBeenCalledWith(
'Server returned code 0 with body "sentry-http-test"',
expect.any(Function),
);
});
});

0 comments on commit 3665831

Please sign in to comment.