Skip to content

Commit

Permalink
Update to Google Analytics 4
Browse files Browse the repository at this point in the history
  • Loading branch information
Nateowami committed May 17, 2023
1 parent 6ab80a4 commit 7c3f77b
Show file tree
Hide file tree
Showing 11 changed files with 119 additions and 28 deletions.
7 changes: 3 additions & 4 deletions src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { TextInfo } from 'realtime-server/lib/esm/scriptureforge/models/text-inf
import { Canon } from 'realtime-server/lib/esm/scriptureforge/scripture-utils/canon';
import { combineLatest, Observable, of, Subscription } from 'rxjs';
import { distinctUntilChanged, filter, map, startWith, tap } from 'rxjs/operators';
import { AnalyticsService } from 'xforge-common/analytics.service';
import { AuthService } from 'xforge-common/auth.service';
import { DataLoadingComponent } from 'xforge-common/data-loading-component';
import { DialogService } from 'xforge-common/dialog.service';
Expand Down Expand Up @@ -47,8 +48,6 @@ import { ProjectDeletedDialogComponent } from './project-deleted-dialog/project-
import { SettingsAuthGuard, SyncAuthGuard, UsersAuthGuard } from './shared/project-router.guard';
import { projectLabel } from './shared/utils';

declare function gtag(...args: any): void;

export const CONNECT_PROJECT_OPTION = '*connect-project*';

@Component({
Expand Down Expand Up @@ -106,6 +105,7 @@ export class AppComponent extends DataLoadingComponent implements OnInit, OnDest
readonly urls: ExternalUrlService,
readonly featureFlags: FeatureFlagService,
private readonly pwaService: PwaService,
private readonly analytics: AnalyticsService,
iconRegistry: MdcIconRegistry,
sanitizer: DomSanitizer
) {
Expand Down Expand Up @@ -148,8 +148,7 @@ export class AppComponent extends DataLoadingComponent implements OnInit, OnDest
);
this.subscribe(navEndEvent$, e => {
if (this.isAppOnline) {
// eslint-disable-next-line @typescript-eslint/naming-convention
gtag('config', 'UA-22170471-15', { page_path: e.urlAfterRedirects });
this.analytics.logNavigation(e.urlAfterRedirects);
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ export const environment = {
realtimeUrl: '/realtime-api/',
authDomain: 'login.languagetechnology.org',
authClientId: 'tY2wXn40fsL5VsPM4uIHNtU6ZUEXGeFn',
offlineDBVersion: 5
offlineDBVersion: 5,
googleTagId: 'G-SVKBDV7K3Q'
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ export const environment = {
realtimeUrl: '/',
authDomain: 'sil-appbuilder.auth0.com',
authClientId: 'aoAGb9Yx1H5WIsvCW6JJCteJhSa37ftH',
offlineDBVersion: 5
offlineDBVersion: 5,
googleTagId: null
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ export const environment = {
realtimeUrl: '/realtime-api/',
authDomain: 'dev-sillsdev.auth0.com',
authClientId: '4eHLjo40mAEGFU6zUxdYjnpnC1K1Ydnj',
offlineDBVersion: 5
offlineDBVersion: 5,
googleTagId: null
};
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ export const environment = {
realtimeUrl: '/',
authDomain: 'sil-appbuilder.auth0.com',
authClientId: 'aoAGb9Yx1H5WIsvCW6JJCteJhSa37ftH',
offlineDBVersion: 5
offlineDBVersion: 5,
googleTagId: null
};
5 changes: 3 additions & 2 deletions src/SIL.XForge.Scripture/ClientApp/src/index.html
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-22170471-15"></script>
<script async src="https://www.googletagmanager.com/gtag/js?id=G-SVKBDV7K3Q"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());

gtag("config", "G-SVKBDV7K3Q");
</script>

<link href="https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined" rel="stylesheet" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { sanitizeUrl } from './analytics.service';

fdescribe('AnalyticsService', () => {
it('should redact the access token from URL', () => {
const url = 'https://example.com/#access_token=123';
expect(sanitizeUrl(url)).toEqual('https://example.com/#access_token=redacted');
});

it('should redact the join key from URL', () => {
const url = 'https://example.com/join/123';
expect(sanitizeUrl(url)).toEqual('https://example.com/join/redacted');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Injectable } from '@angular/core';
import { environment } from '../environments/environment';
import { PwaService } from './pwa.service';

declare function gtag(...args: any): void;

// Using a type rather than interface because I intend to turn in into a union type later for each type of event that
// can be reported.
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
type EventParams = {
page_path: string;
};

@Injectable({
providedIn: 'root'
})
export class AnalyticsService {
constructor(private readonly pwaService: PwaService) {}

/**
* Logs the page navigation event to the analytics service. This method is responsible for sanitizing the URL before
* logging it.
* @param url The URL of the page that was navigated to.
*/
logNavigation(url: string): void {
const sanitizedUrl = sanitizeUrl(url);
this.logEvent('page_view', { page_path: sanitizedUrl });
}

private logEvent(eventName: string, eventParams: EventParams): void {
if (this.pwaService.isOnline && typeof environment.googleTagId === 'string') {
gtag(eventName, environment.googleTagId, eventParams);
}
}
}

const redacted = 'redacted';

// redact access token from the hash
function redactAccessToken(url: string): string {
const urlObj = new URL(url);
const hash = urlObj.hash;

if (hash === '') return url;

const hashObj = new URLSearchParams(hash.slice(1));
const accessToken = hashObj.get('access_token');

if (accessToken === null) return url;

hashObj.set('access_token', redacted);
urlObj.hash = hashObj.toString();
return urlObj.toString();
}

function redactJoinKey(url: string): string {
const urlObj = new URL(url);
const pathParts = urlObj.pathname.split('/');
const joinIndex = pathParts.indexOf('join');

if (joinIndex === -1) {
return url;
}

pathParts[joinIndex + 1] = redacted;
urlObj.pathname = pathParts.join('/');
return urlObj.toString();
}

/**
* Redacts sensitive information from the given URL. Currently this only redacts the access token and the join key, so
* if relying on this method in the future, be sure to check that it is still redacting everything you need it to.
* @param url The URL to sanitize.
* @returns A sanitized version of the URL.
*/
export function sanitizeUrl(url: string): string {
return redactAccessToken(redactJoinKey(url));
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,8 @@ describe('ErrorReportingService', () => {
ErrorReportingService.beforeSend({}, event);
expect(event.breadcrumbs[0].metadata.from).toEqual('http://localhost:5000/somewhere&access_token=thing');
expect(event.breadcrumbs[0].metadata.to).toEqual('http://localhost:5000/somewhere');
expect(event.breadcrumbs[1].metadata.from).toEqual(
'http://localhost:5000/projects#access_token=redacted_for_error_report'
);
expect(event.breadcrumbs[1].metadata.from).toEqual('http://localhost:5000/projects#access_token=redacted');
expect(event.breadcrumbs[1].metadata.to).toEqual('http://localhost:5000/projects');
expect(event.request.url).toEqual('http://localhost:5000/projects#access_token=redacted_for_error_report');
expect(event.request.url).toEqual('http://localhost:5000/projects#access_token=redacted');
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Injectable } from '@angular/core';
import Bugsnag, { Event, NotifiableError } from '@bugsnag/js';
import { sanitizeUrl } from './analytics.service';

export interface EventMetadata {
[key: string]: object;
Expand All @@ -9,14 +10,14 @@ export interface EventMetadata {
providedIn: 'root'
})
export class ErrorReportingService {
static beforeSend(metaData: EventMetadata, event: Event) {
static beforeSend(metaData: EventMetadata, event: Event): void {
if (typeof event.request.url === 'string') {
event.request.url = ErrorReportingService.redactAccessToken(event.request.url as string);
event.request.url = sanitizeUrl(event.request.url as string);
}
event.breadcrumbs = event.breadcrumbs.map(breadcrumb => {
if (breadcrumb.type === 'navigation' && breadcrumb.metadata && typeof breadcrumb.metadata.from === 'string') {
breadcrumb.metadata.from = ErrorReportingService.redactAccessToken(breadcrumb.metadata.from);
breadcrumb.metadata.to = ErrorReportingService.redactAccessToken(breadcrumb.metadata.to);
breadcrumb.metadata.from = sanitizeUrl(breadcrumb.metadata.from);
breadcrumb.metadata.to = sanitizeUrl(breadcrumb.metadata.to);
}
return breadcrumb;
});
Expand All @@ -40,21 +41,17 @@ export class ErrorReportingService {
} else return error;
}

private static redactAccessToken(url: string): string {
return url.replace(/^(.*#access_token=).*$/, '$1redacted_for_error_report');
}

private metadata: EventMetadata = {};

addMeta(data: object, tabName: string = 'custom') {
addMeta(data: object, tabName: string = 'custom'): void {
this.metadata[tabName] = { ...this.metadata[tabName], ...data };
}

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

silentError(message: string, metadata?: object) {
silentError(message: string, metadata?: object): void {
if (metadata != null) {
this.addMeta(metadata);
}
Expand Down
9 changes: 5 additions & 4 deletions src/SIL.XForge.Scripture/Pages/Shared/_Layout.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@

<head>
<environment include="Production">
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-22170471-15"></script>
<script async src="https://www.googletagmanager.com/gtag/js?id=G-SVKBDV7K3Q"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'UA-22170471-15');
gtag('config', 'G-SVKBDV7K3Q');
</script>
</environment>

Expand Down

0 comments on commit 7c3f77b

Please sign in to comment.