diff --git a/aio/src/app/app.module.ts b/aio/src/app/app.module.ts index c14ce329d3..2b533646e0 100644 --- a/aio/src/app/app.module.ts +++ b/aio/src/app/app.module.ts @@ -1,5 +1,5 @@ import { BrowserModule } from '@angular/platform-browser'; -import { NgModule } from '@angular/core'; +import { ErrorHandler, NgModule } from '@angular/core'; import { HttpClientModule } from '@angular/common/http'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @@ -31,6 +31,7 @@ import { TopMenuComponent } from 'app/layout/top-menu/top-menu.component'; import { FooterComponent } from 'app/layout/footer/footer.component'; import { NavMenuComponent } from 'app/layout/nav-menu/nav-menu.component'; import { NavItemComponent } from 'app/layout/nav-item/nav-item.component'; +import { ReportingErrorHandler } from 'app/shared/reporting-error-handler'; import { ScrollService } from 'app/shared/scroll.service'; import { ScrollSpyService } from 'app/shared/scroll-spy.service'; import { SearchBoxComponent } from 'app/search/search-box/search-box.component'; @@ -124,6 +125,7 @@ export const svgIconProviders = [ providers: [ Deployment, DocumentService, + { provide: ErrorHandler, useClass: ReportingErrorHandler }, GaService, Logger, Location, diff --git a/aio/src/app/shared/ga.service.ts b/aio/src/app/shared/ga.service.ts index ffd620cac5..122bfa4a93 100644 --- a/aio/src/app/shared/ga.service.ts +++ b/aio/src/app/shared/ga.service.ts @@ -30,6 +30,9 @@ export class GaService { } ga(...args: any[]) { - (this.window as any)['ga'](...args); + const gaFn = (this.window as any)['ga']; + if (gaFn) { + gaFn(...args); + } } } diff --git a/aio/src/app/shared/reporting-error-handler.spec.ts b/aio/src/app/shared/reporting-error-handler.spec.ts new file mode 100644 index 0000000000..e8aef4440b --- /dev/null +++ b/aio/src/app/shared/reporting-error-handler.spec.ts @@ -0,0 +1,64 @@ +import { ErrorHandler, ReflectiveInjector } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { WindowToken } from 'app/shared/window'; +import { AppModule } from 'app/app.module'; + +import { ReportingErrorHandler } from './reporting-error-handler'; + +describe('ReportingErrorHandler service', () => { + let handler: ReportingErrorHandler; + let superHandler: jasmine.Spy; + let onerrorSpy: jasmine.Spy; + + beforeEach(() => { + onerrorSpy = jasmine.createSpy('onerror'); + superHandler = spyOn(ErrorHandler.prototype, 'handleError'); + + const injector = ReflectiveInjector.resolveAndCreate([ + { provide: ErrorHandler, useClass: ReportingErrorHandler }, + { provide: WindowToken, useFactory: () => ({ onerror: onerrorSpy }) } + ]); + handler = injector.get(ErrorHandler); + }); + + it('should be registered on the AppModule', () => { + handler = TestBed.configureTestingModule({ imports: [AppModule] }).get(ErrorHandler); + expect(handler).toEqual(jasmine.any(ReportingErrorHandler)); + }); + + describe('handleError', () => { + it('should call the super class handleError', () => { + const error = new Error(); + handler.handleError(error); + expect(superHandler).toHaveBeenCalledWith(error); + }); + + it('should cope with the super handler throwing an error', () => { + const error = new Error('initial error'); + superHandler.and.throwError('super handler error'); + handler.handleError(error); + + expect(onerrorSpy).toHaveBeenCalledTimes(2); + + // Error from super handler is reported first + expect(onerrorSpy.calls.argsFor(0)[0]).toEqual('super handler error'); + expect(onerrorSpy.calls.argsFor(0)[4]).toEqual(jasmine.any(Error)); + + // Then error from initial exception + expect(onerrorSpy.calls.argsFor(1)[0]).toEqual('initial error'); + expect(onerrorSpy.calls.argsFor(1)[4]).toEqual(error); + }); + + it('should send an error object to window.onerror', () => { + const error = new Error('this is an error message'); + handler.handleError(error); + expect(onerrorSpy).toHaveBeenCalledWith(error.message, undefined, undefined, undefined, error); + }); + + it('should send an error string to window.onerror', () => { + const error = 'this is an error message'; + handler.handleError(error); + expect(onerrorSpy).toHaveBeenCalledWith(error); + }); + }); +}); diff --git a/aio/src/app/shared/reporting-error-handler.ts b/aio/src/app/shared/reporting-error-handler.ts new file mode 100644 index 0000000000..6289d6e057 --- /dev/null +++ b/aio/src/app/shared/reporting-error-handler.ts @@ -0,0 +1,37 @@ +import { ErrorHandler, Inject, Injectable } from '@angular/core'; +import { WindowToken } from './window'; + +/** + * Extend the default error handling to report errors to an external service - e.g Google Analytics. + * + * Errors outside the Angular application may also be handled by `window.onerror`. + */ +@Injectable() +export class ReportingErrorHandler extends ErrorHandler { + + constructor(@Inject(WindowToken) private window: Window) { + super(); + } + + /** + * Send error info to Google Analytics, in addition to the default handling. + * @param error Information about the error. + */ + handleError(error: string | Error) { + + try { + super.handleError(error); + } catch (e) { + this.reportError(e); + } + this.reportError(error); + } + + private reportError(error: string | Error) { + if (typeof error === 'string') { + this.window.onerror(error); + } else { + this.window.onerror(error.message, undefined, undefined, undefined, error); + } + } +} diff --git a/aio/src/index.html b/aio/src/index.html index 0a29f7dfb1..4350c2aa19 100644 --- a/aio/src/index.html +++ b/aio/src/index.html @@ -52,6 +52,38 @@ + +