Skip to content

Commit ab04cca

Browse files
committed
Update to Google Analytics 4
1 parent 2671953 commit ab04cca

File tree

11 files changed

+116
-27
lines changed

11 files changed

+116
-27
lines changed

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,11 @@ import { UserService } from 'xforge-common/user.service';
3434
import { issuesEmailTemplate, supportedBrowser } from 'xforge-common/utils';
3535
import versionData from '../../../version.json';
3636
import { environment } from '../environments/environment';
37+
import { AnalyticsService } from '../xforge-common/analytics.service';
3738
import { SFProjectProfileDoc } from './core/models/sf-project-profile-doc';
3839
import { roleCanAccessTranslate } from './core/models/sf-project-role-info';
3940
import { SFProjectService } from './core/sf-project.service';
4041

41-
declare function gtag(...args: any): void;
42-
4342
export const CONNECT_PROJECT_OPTION = '*connect-project*';
4443

4544
@Component({
@@ -81,7 +80,8 @@ export class AppComponent extends DataLoadingComponent implements OnInit, OnDest
8180
readonly urls: ExternalUrlService,
8281
readonly featureFlags: FeatureFlagService,
8382
private readonly pwaService: PwaService,
84-
onlineStatusService: OnlineStatusService
83+
onlineStatusService: OnlineStatusService,
84+
private readonly analytics: AnalyticsService
8585
) {
8686
super(noticeService);
8787
this.subscribe(
@@ -122,8 +122,7 @@ export class AppComponent extends DataLoadingComponent implements OnInit, OnDest
122122
);
123123
this.subscribe(navEndEvent$, e => {
124124
if (this.isAppOnline) {
125-
// eslint-disable-next-line @typescript-eslint/naming-convention
126-
gtag('config', 'UA-22170471-15', { page_path: e.urlAfterRedirects });
125+
this.analytics.logNavigation(e.urlAfterRedirects);
127126
}
128127
});
129128
}

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: 8
18+
offlineDBVersion: 8,
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: 8
18+
offlineDBVersion: 8,
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: 8
18+
offlineDBVersion: 8,
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: 8
25+
offlineDBVersion: 8,
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: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { Injectable } from '@angular/core';
2+
import { environment } from '../environments/environment';
3+
import { OnlineStatusService } from './online-status.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({ providedIn: 'root' })
15+
export class AnalyticsService {
16+
constructor(private readonly onlineStatus: OnlineStatusService) {}
17+
18+
/**
19+
* Logs the page navigation event to the analytics service. This method is responsible for sanitizing the URL before
20+
* logging it.
21+
* @param url The URL of the page that was navigated to.
22+
*/
23+
logNavigation(url: string): void {
24+
const sanitizedUrl = sanitizeUrl(url);
25+
this.logEvent('page_view', { page_path: sanitizedUrl });
26+
}
27+
28+
private logEvent(eventName: string, eventParams: EventParams): void {
29+
if (this.onlineStatus.isOnline && typeof environment.googleTagId === 'string') {
30+
gtag(eventName, environment.googleTagId, eventParams);
31+
}
32+
}
33+
}
34+
35+
const redacted = 'redacted';
36+
37+
// redact access token from the hash
38+
function redactAccessToken(url: string): string {
39+
const urlObj = new URL(url);
40+
const hash = urlObj.hash;
41+
42+
if (hash === '') return url;
43+
44+
const hashObj = new URLSearchParams(hash.slice(1));
45+
const accessToken = hashObj.get('access_token');
46+
47+
if (accessToken === null) return url;
48+
49+
hashObj.set('access_token', redacted);
50+
urlObj.hash = hashObj.toString();
51+
return urlObj.toString();
52+
}
53+
54+
function redactJoinKey(url: string): string {
55+
const urlObj = new URL(url);
56+
const pathParts = urlObj.pathname.split('/');
57+
const joinIndex = pathParts.indexOf('join');
58+
59+
if (joinIndex === -1) {
60+
return url;
61+
}
62+
63+
pathParts[joinIndex + 1] = redacted;
64+
urlObj.pathname = pathParts.join('/');
65+
return urlObj.toString();
66+
}
67+
68+
/**
69+
* Redacts sensitive information from the given URL. Currently this only redacts the access token and the join key, so
70+
* if relying on this method in the future, be sure to check that it is still redacting everything you need it to.
71+
* @param url The URL to sanitize.
72+
* @returns A sanitized version of the URL.
73+
*/
74+
export function sanitizeUrl(url: string): string {
75+
return redactAccessToken(redactJoinKey(url));
76+
}

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: 5 additions & 8 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): any {
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,10 +41,6 @@ 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

4946
addMeta(data: object, tabName: string = 'custom'): void {

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)