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 @@
+
+