mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(aio): report application errors to Google Analytics (#22011)
This is a basic implementation of error logging using the limited facilities provided by Google Analytics. Errors within the Angular app itself will be handled by a new `ReportingErrorHandler` service, which overrides and extends the built-in `ErrorHandler`. Further, errors outside the app, which arrive at `window.onerror` will also be reported to Google Analytics. Closes #21943 PR Close #22011
This commit is contained in:
committed by
Miško Hevery
parent
0b23573573
commit
eb0da530a7
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,38 @@
|
||||
</script>
|
||||
<!-- End Google Analytics -->
|
||||
|
||||
<script>
|
||||
// Report fatal errors to Google Analytics
|
||||
window.onerror = function() {
|
||||
ga('send', 'exception', {exDescription: formatError.apply(null, arguments), exFatal: true});
|
||||
|
||||
function formatError(msg, url, line, col, e) {
|
||||
var stack;
|
||||
msg = msg.replace(/^Error: /, '');
|
||||
if (e) {
|
||||
stack = e.stack
|
||||
// strip the leading "Error: " from the stack trace
|
||||
.replace(/^Error: /, '')
|
||||
// strip the message from the stack trace, if present
|
||||
.replace(msg + '\n', '')
|
||||
// strip leading spaces
|
||||
.replace(/^ +/gm, '')
|
||||
// strip all leading "at " for each frame
|
||||
.replace(/^at /gm, '')
|
||||
// replace long urls with just the last segment: `filename:line:column`
|
||||
.replace(/(?: \(|@)http.+\/([^/)]+)\)?(?:\n|$)/gm, '@$1\n')
|
||||
// replace "eval code" in Edge
|
||||
.replace(/ *\(eval code(:\d+:\d+)\)(?:\n|$)/gm, '@???$1\n')
|
||||
} else {
|
||||
line = line || '?';
|
||||
col = col || '?';
|
||||
stack = url + ':' + line + ':' + col;
|
||||
}
|
||||
return (msg + '\n' + stack).substr(0, 150);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script>
|
||||
if (window.document.documentMode) {
|
||||
// polyfill IE11 in a blocking way
|
||||
|
||||
Reference in New Issue
Block a user