Skip to content

Commit bd0b2b4

Browse files
committed
Update to Google Analytics 4
1 parent 6ab80a4 commit bd0b2b4

File tree

11 files changed

+119
-28
lines changed

11 files changed

+119
-28
lines changed

src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { TextInfo } from 'realtime-server/lib/esm/scriptureforge/models/text-inf
1616
import { Canon } from 'realtime-server/lib/esm/scriptureforge/scripture-utils/canon';
1717
import { combineLatest, Observable, of, Subscription } from 'rxjs';
1818
import { distinctUntilChanged, filter, map, startWith, tap } from 'rxjs/operators';
19+
import { AnalyticsService } from 'xforge-common/analytics.service';
1920
import { AuthService } from 'xforge-common/auth.service';
2021
import { DataLoadingComponent } from 'xforge-common/data-loading-component';
2122
import { DialogService } from 'xforge-common/dialog.service';
@@ -47,8 +48,6 @@ import { ProjectDeletedDialogComponent } from './project-deleted-dialog/project-
4748
import { SettingsAuthGuard, SyncAuthGuard, UsersAuthGuard } from './shared/project-router.guard';
4849
import { projectLabel } from './shared/utils';
4950

50-
declare function gtag(...args: any): void;
51-
5251
export const CONNECT_PROJECT_OPTION = '*connect-project*';
5352

5453
@Component({
@@ -106,6 +105,7 @@ export class AppComponent extends DataLoadingComponent implements OnInit, OnDest
106105
readonly urls: ExternalUrlService,
107106
readonly featureFlags: FeatureFlagService,
108107
private readonly pwaService: PwaService,
108+
private readonly analytics: AnalyticsService,
109109
iconRegistry: MdcIconRegistry,
110110
sanitizer: DomSanitizer
111111
) {
@@ -148,8 +148,7 @@ export class AppComponent extends DataLoadingComponent implements OnInit, OnDest
148148
);
149149
this.subscribe(navEndEvent$, e => {
150150
if (this.isAppOnline) {
151-
// eslint-disable-next-line @typescript-eslint/naming-convention
152-
gtag('config', 'UA-22170471-15', { page_path: e.urlAfterRedirects });
151+
this.analytics.logNavigation(e.urlAfterRedirects);
153152
}
154153
});
155154
}

src/SIL.XForge.Scripture/ClientApp/src/environments/environment.prod.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ export const environment = {
1515
realtimeUrl: '/realtime-api/',
1616
authDomain: 'login.languagetechnology.org',
1717
authClientId: 'tY2wXn40fsL5VsPM4uIHNtU6ZUEXGeFn',
18-
offlineDBVersion: 5
18+
offlineDBVersion: 5,
19+
googleTagId: 'G-SVKBDV7K3Q'
1920
};

src/SIL.XForge.Scripture/ClientApp/src/environments/environment.pwa-test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ export const environment = {
1515
realtimeUrl: '/',
1616
authDomain: 'sil-appbuilder.auth0.com',
1717
authClientId: 'aoAGb9Yx1H5WIsvCW6JJCteJhSa37ftH',
18-
offlineDBVersion: 5
18+
offlineDBVersion: 5,
19+
googleTagId: null
1920
};

src/SIL.XForge.Scripture/ClientApp/src/environments/environment.staging.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ export const environment = {
1515
realtimeUrl: '/realtime-api/',
1616
authDomain: 'dev-sillsdev.auth0.com',
1717
authClientId: '4eHLjo40mAEGFU6zUxdYjnpnC1K1Ydnj',
18-
offlineDBVersion: 5
18+
offlineDBVersion: 5,
19+
googleTagId: null
1920
};

src/SIL.XForge.Scripture/ClientApp/src/environments/environment.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,6 @@ export const environment = {
2222
realtimeUrl: '/',
2323
authDomain: 'sil-appbuilder.auth0.com',
2424
authClientId: 'aoAGb9Yx1H5WIsvCW6JJCteJhSa37ftH',
25-
offlineDBVersion: 5
25+
offlineDBVersion: 5,
26+
googleTagId: null
2627
};

src/SIL.XForge.Scripture/ClientApp/src/index.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
<!DOCTYPE html>
22
<html lang="en">
33
<head>
4-
<!-- Global site tag (gtag.js) - Google Analytics -->
5-
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-22170471-15"></script>
4+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-SVKBDV7K3Q"></script>
65
<script>
76
window.dataLayer = window.dataLayer || [];
87
function gtag() {
98
dataLayer.push(arguments);
109
}
1110
gtag("js", new Date());
11+
12+
gtag("config", "G-SVKBDV7K3Q");
1213
</script>
1314

1415
<link href="https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined" rel="stylesheet" />
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { sanitizeUrl } from './analytics.service';
2+
3+
describe('AnalyticsService', () => {
4+
it('should redact the access token from URL', () => {
5+
const url = 'https://example.com/#access_token=123';
6+
expect(sanitizeUrl(url)).toEqual('https://example.com/#access_token=redacted');
7+
});
8+
9+
it('should redact the join key from URL', () => {
10+
const url = 'https://example.com/join/123';
11+
expect(sanitizeUrl(url)).toEqual('https://example.com/join/redacted');
12+
});
13+
});
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { Injectable } from '@angular/core';
2+
import { environment } from '../environments/environment';
3+
import { PwaService } from './pwa.service';
4+
5+
declare function gtag(...args: any): void;
6+
7+
// Using a type rather than interface because I intend to turn in into a union type later for each type of event that
8+
// can be reported.
9+
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
10+
type EventParams = {
11+
page_path: string;
12+
};
13+
14+
@Injectable({
15+
providedIn: 'root'
16+
})
17+
export class AnalyticsService {
18+
constructor(private readonly pwaService: PwaService) {}
19+
20+
/**
21+
* Logs the page navigation event to the analytics service. This method is responsible for sanitizing the URL before
22+
* logging it.
23+
* @param url The URL of the page that was navigated to.
24+
*/
25+
logNavigation(url: string): void {
26+
const sanitizedUrl = sanitizeUrl(url);
27+
this.logEvent('page_view', { page_path: sanitizedUrl });
28+
}
29+
30+
private logEvent(eventName: string, eventParams: EventParams): void {
31+
if (this.pwaService.isOnline && typeof environment.googleTagId === 'string') {
32+
gtag(eventName, environment.googleTagId, eventParams);
33+
}
34+
}
35+
}
36+
37+
const redacted = 'redacted';
38+
39+
// redact access token from the hash
40+
function redactAccessToken(url: string): string {
41+
const urlObj = new URL(url);
42+
const hash = urlObj.hash;
43+
44+
if (hash === '') return url;
45+
46+
const hashObj = new URLSearchParams(hash.slice(1));
47+
const accessToken = hashObj.get('access_token');
48+
49+
if (accessToken === null) return url;
50+
51+
hashObj.set('access_token', redacted);
52+
urlObj.hash = hashObj.toString();
53+
return urlObj.toString();
54+
}
55+
56+
function redactJoinKey(url: string): string {
57+
const urlObj = new URL(url);
58+
const pathParts = urlObj.pathname.split('/');
59+
const joinIndex = pathParts.indexOf('join');
60+
61+
if (joinIndex === -1) {
62+
return url;
63+
}
64+
65+
pathParts[joinIndex + 1] = redacted;
66+
urlObj.pathname = pathParts.join('/');
67+
return urlObj.toString();
68+
}
69+
70+
/**
71+
* Redacts sensitive information from the given URL. Currently this only redacts the access token and the join key, so
72+
* if relying on this method in the future, be sure to check that it is still redacting everything you need it to.
73+
* @param url The URL to sanitize.
74+
* @returns A sanitized version of the URL.
75+
*/
76+
export function sanitizeUrl(url: string): string {
77+
return redactAccessToken(redactJoinKey(url));
78+
}

src/SIL.XForge.Scripture/ClientApp/src/xforge-common/error-reporting-service.spec.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,8 @@ describe('ErrorReportingService', () => {
4343
ErrorReportingService.beforeSend({}, event);
4444
expect(event.breadcrumbs[0].metadata.from).toEqual('http://localhost:5000/somewhere&access_token=thing');
4545
expect(event.breadcrumbs[0].metadata.to).toEqual('http://localhost:5000/somewhere');
46-
expect(event.breadcrumbs[1].metadata.from).toEqual(
47-
'http://localhost:5000/projects#access_token=redacted_for_error_report'
48-
);
46+
expect(event.breadcrumbs[1].metadata.from).toEqual('http://localhost:5000/projects#access_token=redacted');
4947
expect(event.breadcrumbs[1].metadata.to).toEqual('http://localhost:5000/projects');
50-
expect(event.request.url).toEqual('http://localhost:5000/projects#access_token=redacted_for_error_report');
48+
expect(event.request.url).toEqual('http://localhost:5000/projects#access_token=redacted');
5149
});
5250
});

src/SIL.XForge.Scripture/ClientApp/src/xforge-common/error-reporting.service.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Injectable } from '@angular/core';
22
import Bugsnag, { Event, NotifiableError } from '@bugsnag/js';
3+
import { sanitizeUrl } from './analytics.service';
34

45
export interface EventMetadata {
56
[key: string]: object;
@@ -9,14 +10,14 @@ export interface EventMetadata {
910
providedIn: 'root'
1011
})
1112
export class ErrorReportingService {
12-
static beforeSend(metaData: EventMetadata, event: Event) {
13+
static beforeSend(metaData: EventMetadata, event: Event): void {
1314
if (typeof event.request.url === 'string') {
14-
event.request.url = ErrorReportingService.redactAccessToken(event.request.url as string);
15+
event.request.url = sanitizeUrl(event.request.url as string);
1516
}
1617
event.breadcrumbs = event.breadcrumbs.map(breadcrumb => {
1718
if (breadcrumb.type === 'navigation' && breadcrumb.metadata && typeof breadcrumb.metadata.from === 'string') {
18-
breadcrumb.metadata.from = ErrorReportingService.redactAccessToken(breadcrumb.metadata.from);
19-
breadcrumb.metadata.to = ErrorReportingService.redactAccessToken(breadcrumb.metadata.to);
19+
breadcrumb.metadata.from = sanitizeUrl(breadcrumb.metadata.from);
20+
breadcrumb.metadata.to = sanitizeUrl(breadcrumb.metadata.to);
2021
}
2122
return breadcrumb;
2223
});
@@ -40,21 +41,17 @@ export class ErrorReportingService {
4041
} else return error;
4142
}
4243

43-
private static redactAccessToken(url: string): string {
44-
return url.replace(/^(.*#access_token=).*$/, '$1redacted_for_error_report');
45-
}
46-
4744
private metadata: EventMetadata = {};
4845

49-
addMeta(data: object, tabName: string = 'custom') {
46+
addMeta(data: object, tabName: string = 'custom'): void {
5047
this.metadata[tabName] = { ...this.metadata[tabName], ...data };
5148
}
5249

5350
notify(error: NotifiableError, callback?: (err: any, report: any) => void): void {
5451
Bugsnag.notify(error, event => ErrorReportingService.beforeSend(this.metadata, event), callback);
5552
}
5653

57-
silentError(message: string, metadata?: object) {
54+
silentError(message: string, metadata?: object): void {
5855
if (metadata != null) {
5956
this.addMeta(metadata);
6057
}

src/SIL.XForge.Scripture/Pages/Shared/_Layout.cshtml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@
77

88
<head>
99
<environment include="Production">
10-
<!-- Global site tag (gtag.js) - Google Analytics -->
11-
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-22170471-15"></script>
10+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-SVKBDV7K3Q"></script>
1211
<script>
1312
window.dataLayer = window.dataLayer || [];
14-
function gtag() { dataLayer.push(arguments); }
13+
function gtag() {
14+
dataLayer.push(arguments);
15+
}
1516
gtag('js', new Date());
1617
17-
gtag('config', 'UA-22170471-15');
18+
gtag('config', 'G-SVKBDV7K3Q');
1819
</script>
1920
</environment>
2021

0 commit comments

Comments
 (0)