Skip to content
This repository was archived by the owner on Dec 18, 2024. It is now read-only.

Commit 7c9d54b

Browse files
committed
Send exceptions to Analytics for tracking errors like in angular.io
1 parent 8c9dad4 commit 7c9d54b

File tree

4 files changed

+95
-5
lines changed

4 files changed

+95
-5
lines changed

src/app/app-module.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import {BrowserModule} from '@angular/platform-browser';
22
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
3-
import {NgModule} from '@angular/core';
3+
import {ErrorHandler, NgModule} from '@angular/core';
44
import {LocationStrategy, PathLocationStrategy} from '@angular/common';
55
import {RouterModule} from '@angular/router';
66

77
import {MaterialDocsApp} from './material-docs-app';
88
import {MATERIAL_DOCS_ROUTES} from './routes';
99
import {NavBarModule} from './shared/navbar';
1010
import {CookiePopupModule} from './shared/cookie-popup/cookie-popup-module';
11+
import {AnalyticsErrorReportHandler} from './shared/analytics/error-report-handler';
1112

12-
const prefersReducedMotion = typeof matchMedia === 'function' ?
13-
matchMedia('(prefers-reduced-motion)').matches : false;
13+
const prefersReducedMotion =
14+
typeof matchMedia === 'function' ? matchMedia('(prefers-reduced-motion)').matches : false;
1415

1516
@NgModule({
1617
imports: [
@@ -19,13 +20,16 @@ const prefersReducedMotion = typeof matchMedia === 'function' ?
1920
RouterModule.forRoot(MATERIAL_DOCS_ROUTES, {
2021
scrollPositionRestoration: 'enabled',
2122
anchorScrolling: 'enabled',
22-
relativeLinkResolution: 'corrected'
23+
relativeLinkResolution: 'corrected',
2324
}),
2425
NavBarModule,
2526
CookiePopupModule,
2627
],
2728
declarations: [MaterialDocsApp],
28-
providers: [{provide: LocationStrategy, useClass: PathLocationStrategy}],
29+
providers: [
30+
{provide: LocationStrategy, useClass: PathLocationStrategy},
31+
{provide: ErrorHandler, useClass: AnalyticsErrorReportHandler},
32+
],
2933
bootstrap: [MaterialDocsApp],
3034
})
3135
export class AppModule {}

src/app/shared/analytics/analytics.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {Injectable} from '@angular/core';
22

33
import {environment} from '../../../environments/environment';
4+
import {formatErrorEventForAnalytics} from './format-error';
45

56
/** Extension of `Window` with potential Google Analytics fields. */
67
declare global {
@@ -28,12 +29,22 @@ export class AnalyticsService {
2829

2930
constructor() {
3031
this._installGlobalSiteTag();
32+
this._installWindowErrorHandler();
3133

3234
// TODO: Remove this when we fully switch to Google Analytics 4+.
3335
this._legacyGa('create', environment.legacyUniversalAnalyticsMaterialId, 'auto', 'mat');
3436
this._legacyGa('create', environment.legacyUniversalAnalyticsMainId, 'auto', 'ng');
3537
}
3638

39+
reportError(description: string, fatal = true) {
40+
// Limit descriptions to maximum of 150 characters.
41+
// See: https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#exd.
42+
description = description.substring(0, 150);
43+
44+
this._legacyGa('send', 'exception', {exDescription: description, exFatal: fatal});
45+
this._gtag('event', 'exception', {description: description, fatal});
46+
}
47+
3748
locationChanged(url: string) {
3849
this._sendPage(url);
3950
}
@@ -56,6 +67,12 @@ export class AnalyticsService {
5667
}
5768
}
5869

70+
private _gtag(...args: any[]) {
71+
if (window.gtag) {
72+
window.gtag(...args);
73+
}
74+
}
75+
5976
private _installGlobalSiteTag() {
6077
const url =
6178
`https://www.googletagmanager.com/gtag/js?id=${environment.googleAnalyticsMaterialId}`;
@@ -85,4 +102,10 @@ export class AnalyticsService {
85102
el.src = url;
86103
window.document.head.appendChild(el);
87104
}
105+
106+
private _installWindowErrorHandler() {
107+
window.addEventListener('error', event =>
108+
this.reportError(formatErrorEventForAnalytics(event), true)
109+
);
110+
}
88111
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {ErrorHandler, Injectable} from '@angular/core';
2+
import {AnalyticsService} from './analytics';
3+
import {formatErrorForAnalytics} from './format-error';
4+
5+
@Injectable()
6+
export class AnalyticsErrorReportHandler extends ErrorHandler {
7+
constructor(private _analytics: AnalyticsService) {
8+
super();
9+
}
10+
11+
handleError(error: any): void {
12+
super.handleError(error);
13+
14+
// Report the error in Google Analytics.
15+
if (error instanceof Error) {
16+
this._analytics.reportError(formatErrorForAnalytics(error));
17+
} else {
18+
this._analytics.reportError(error.toString());
19+
}
20+
}
21+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Formats an `ErrorEvent` to a human-readable string that can
3+
* be sent to Google Analytics.
4+
*/
5+
export function formatErrorEventForAnalytics(event: ErrorEvent): string {
6+
const {message, filename, colno, lineno, error} = event;
7+
8+
if (error instanceof Error) {
9+
return formatErrorForAnalytics(error);
10+
}
11+
12+
return `${stripErrorMessagePrefix(message)}\n${filename}:${lineno || '?'}:${colno || '?'}`;
13+
}
14+
15+
/**
16+
* Formats an `Error` to a human-readable string that can be sent
17+
* to Google Analytics.
18+
*/
19+
export function formatErrorForAnalytics(error: Error): string {
20+
let stack = '<no-stack>';
21+
22+
if (error.stack) {
23+
stack = stripErrorMessagePrefix(error.stack)
24+
// strip the message from the stack trace, if present
25+
.replace(error.message + '\n', '')
26+
// strip leading spaces
27+
.replace(/^ +/gm, '')
28+
// strip all leading "at " for each frame
29+
.replace(/^at /gm, '')
30+
// replace long urls with just the last segment: `filename:line:column`
31+
.replace(/(?: \(|@)http.+\/([^/)]+)\)?(?:\n|$)/gm, '@$1\n')
32+
// replace "eval code" in Edge
33+
.replace(/ *\(eval code(:\d+:\d+)\)(?:\n|$)/gm, '@???$1\n');
34+
}
35+
36+
return `${error.message}\n${stack}`;
37+
}
38+
39+
/** Strips the error message prefix from a message or stack trace. */
40+
function stripErrorMessagePrefix(input: string): string {
41+
return input.replace(/^(Uncaught )?Error: /, '');
42+
}

0 commit comments

Comments
 (0)