From 3c42fb130bbba855a3932b4eded3dd23b4662a2f Mon Sep 17 00:00:00 2001 From: Nathaniel Paulus Date: Wed, 10 May 2023 14:24:58 -0400 Subject: [PATCH 1/7] Update to Google Analytics 4 --- .../ClientApp/src/app/app.component.ts | 7 +- .../src/environments/environment.prod.ts | 3 +- .../src/environments/environment.pwa-test.ts | 3 +- .../src/environments/environment.staging.ts | 3 +- .../ClientApp/src/environments/environment.ts | 3 +- .../ClientApp/src/index.html | 5 +- .../xforge-common/analytics.service.spec.ts | 13 ++++ .../src/xforge-common/analytics.service.ts | 76 +++++++++++++++++++ .../error-reporting-service.spec.ts | 6 +- .../xforge-common/error-reporting.service.ts | 13 ++-- .../Pages/Shared/_Layout.cshtml | 9 ++- 11 files changed, 116 insertions(+), 25 deletions(-) create mode 100644 src/SIL.XForge.Scripture/ClientApp/src/xforge-common/analytics.service.spec.ts create mode 100644 src/SIL.XForge.Scripture/ClientApp/src/xforge-common/analytics.service.ts diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts index aa6f8bb81b0..975439adae2 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts @@ -38,6 +38,7 @@ import { ThemeService } from 'xforge-common/theme.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import versionData from '../../../version.json'; import { environment } from '../environments/environment'; +import { AnalyticsService } from '../xforge-common/analytics.service'; import { SFProjectProfileDoc } from './core/models/sf-project-profile-doc'; import { roleCanAccessTranslate } from './core/models/sf-project-role-info'; import { SFProjectUserConfigDoc } from './core/models/sf-project-user-config-doc'; @@ -91,7 +92,8 @@ export class AppComponent extends DataLoadingComponent implements OnInit, OnDest private readonly pwaService: PwaService, private readonly themeService: ThemeService, onlineStatusService: OnlineStatusService, - private destroyRef: DestroyRef + private destroyRef: DestroyRef, + private readonly analytics: AnalyticsService ) { super(noticeService); this.breakpointObserver @@ -132,8 +134,7 @@ export class AppComponent extends DataLoadingComponent implements OnInit, OnDest ); navEndEvent$.pipe(quietTakeUntilDestroyed(this.destroyRef)).subscribe(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); } }); } diff --git a/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.prod.ts b/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.prod.ts index 147395431bc..5c77930d837 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.prod.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.prod.ts @@ -16,5 +16,6 @@ export const environment = { realtimeUrl: '/realtime-api/', authDomain: 'login.languagetechnology.org', authClientId: 'tY2wXn40fsL5VsPM4uIHNtU6ZUEXGeFn', - offlineDBVersion: 8 + offlineDBVersion: 8, + googleTagId: 'G-SVKBDV7K3Q' }; diff --git a/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.pwa-test.ts b/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.pwa-test.ts index 9aa263296c4..e03081dfbb5 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.pwa-test.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.pwa-test.ts @@ -16,5 +16,6 @@ export const environment = { realtimeUrl: '/', authDomain: 'sil-appbuilder.auth0.com', authClientId: 'aoAGb9Yx1H5WIsvCW6JJCteJhSa37ftH', - offlineDBVersion: 8 + offlineDBVersion: 8, + googleTagId: null }; diff --git a/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.staging.ts b/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.staging.ts index fa846f11138..681c5978d67 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.staging.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.staging.ts @@ -16,5 +16,6 @@ export const environment = { realtimeUrl: '/realtime-api/', authDomain: 'dev-sillsdev.auth0.com', authClientId: '4eHLjo40mAEGFU6zUxdYjnpnC1K1Ydnj', - offlineDBVersion: 8 + offlineDBVersion: 8, + googleTagId: null }; diff --git a/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.ts b/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.ts index 7b592e0a551..104997922cf 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.ts @@ -23,5 +23,6 @@ export const environment = { realtimeUrl: '/', authDomain: 'sil-appbuilder.auth0.com', authClientId: 'aoAGb9Yx1H5WIsvCW6JJCteJhSa37ftH', - offlineDBVersion: 8 + offlineDBVersion: 8, + googleTagId: null }; diff --git a/src/SIL.XForge.Scripture/ClientApp/src/index.html b/src/SIL.XForge.Scripture/ClientApp/src/index.html index 06e4f96f0dd..a50697ed559 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/index.html +++ b/src/SIL.XForge.Scripture/ClientApp/src/index.html @@ -1,14 +1,15 @@ - - + diff --git a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/analytics.service.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/analytics.service.spec.ts new file mode 100644 index 00000000000..4d3b556f4e9 --- /dev/null +++ b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/analytics.service.spec.ts @@ -0,0 +1,13 @@ +import { sanitizeUrl } from './analytics.service'; + +describe('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'); + }); +}); diff --git a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/analytics.service.ts b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/analytics.service.ts new file mode 100644 index 00000000000..b00bb9b1b9c --- /dev/null +++ b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/analytics.service.ts @@ -0,0 +1,76 @@ +import { Injectable } from '@angular/core'; +import { environment } from '../environments/environment'; +import { OnlineStatusService } from './online-status.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 onlineStatus: OnlineStatusService) {} + + /** + * 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.onlineStatus.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)); +} diff --git a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/error-reporting-service.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/error-reporting-service.spec.ts index 8081fb5b9af..33411ac8b1b 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/error-reporting-service.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/error-reporting-service.spec.ts @@ -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'); }); }); diff --git a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/error-reporting.service.ts b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/error-reporting.service.ts index 47dc0881a6a..104d230bff1 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/error-reporting.service.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/error-reporting.service.ts @@ -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; @@ -9,14 +10,14 @@ export interface EventMetadata { providedIn: 'root' }) export class ErrorReportingService { - static beforeSend(metaData: EventMetadata, event: Event): any { + 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; }); @@ -40,10 +41,6 @@ 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'): void { diff --git a/src/SIL.XForge.Scripture/Pages/Shared/_Layout.cshtml b/src/SIL.XForge.Scripture/Pages/Shared/_Layout.cshtml index 66b2841ffce..1a8f2de4371 100644 --- a/src/SIL.XForge.Scripture/Pages/Shared/_Layout.cshtml +++ b/src/SIL.XForge.Scripture/Pages/Shared/_Layout.cshtml @@ -9,14 +9,15 @@ - - + From 0c589c1fdf80585177ecc5df29de91ea4c129b39 Mon Sep 17 00:00:00 2001 From: Nathaniel Paulus Date: Wed, 10 May 2023 14:24:58 -0400 Subject: [PATCH 2/7] Update to Google Analytics 4 --- src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts index 975439adae2..217bfb6a9ba 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts @@ -38,7 +38,7 @@ import { ThemeService } from 'xforge-common/theme.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import versionData from '../../../version.json'; import { environment } from '../environments/environment'; -import { AnalyticsService } from '../xforge-common/analytics.service'; +import { AnalyticsService } from "xforge-common/analytics.service"; import { SFProjectProfileDoc } from './core/models/sf-project-profile-doc'; import { roleCanAccessTranslate } from './core/models/sf-project-role-info'; import { SFProjectUserConfigDoc } from './core/models/sf-project-user-config-doc'; From f5b016902cf6a2252f17c6982ca41ab74de31898 Mon Sep 17 00:00:00 2001 From: Nigel Wells Date: Fri, 29 Sep 2023 13:51:56 +1300 Subject: [PATCH 3/7] * Include additional share URL to test redact * Await online status before sending requests * Moved GA configuration to Analytics Service --- .../ClientApp/src/index.html | 5 +-- .../xforge-common/analytics.service.spec.ts | 5 ++- .../src/xforge-common/analytics.service.ts | 45 ++++++++++++++----- 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/SIL.XForge.Scripture/ClientApp/src/index.html b/src/SIL.XForge.Scripture/ClientApp/src/index.html index a50697ed559..fb0d818c9e3 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/index.html +++ b/src/SIL.XForge.Scripture/ClientApp/src/index.html @@ -1,15 +1,12 @@ - + diff --git a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/analytics.service.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/analytics.service.spec.ts index 4d3b556f4e9..ccbbc9cf64e 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/analytics.service.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/analytics.service.spec.ts @@ -7,7 +7,8 @@ describe('AnalyticsService', () => { }); it('should redact the join key from URL', () => { - const url = 'https://example.com/join/123'; - expect(sanitizeUrl(url)).toEqual('https://example.com/join/redacted'); + ['https://example.com/join/123', 'https://example.com/join/123/en'].forEach(url => { + expect(sanitizeUrl(url)).toContain('https://example.com/join/redacted'); + }); }); }); diff --git a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/analytics.service.ts b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/analytics.service.ts index b00bb9b1b9c..a22344674ae 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/analytics.service.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/analytics.service.ts @@ -4,16 +4,39 @@ import { OnlineStatusService } from './online-status.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; -}; +interface CommandParams {} + +enum GoogleCommands { + Config = 'config', + Event = 'event', + JavaScript = 'js' +} + +interface ConfigParams extends CommandParams { + send_page_view?: boolean; +} + +interface PageViewParams extends CommandParams { + page_location?: string; + page_title?: string; +} @Injectable({ providedIn: 'root' }) export class AnalyticsService { - constructor(private readonly onlineStatus: OnlineStatusService) {} + private initiated?: Promise; + constructor(private readonly onlineStatus: OnlineStatusService) { + if (typeof environment.googleTagId !== 'string') { + return; + } + + this.initiated = new Promise(resolve => { + this.onlineStatus.online.then(() => { + this.send(GoogleCommands.JavaScript, new Date()); + this.send(GoogleCommands.Config, environment.googleTagId, { send_page_view: false } as ConfigParams); + resolve(); + }); + }); + } /** * Logs the page navigation event to the analytics service. This method is responsible for sanitizing the URL before @@ -22,13 +45,11 @@ export class AnalyticsService { */ logNavigation(url: string): void { const sanitizedUrl = sanitizeUrl(url); - this.logEvent('page_view', { page_path: sanitizedUrl }); + this.send(GoogleCommands.Event, 'page_view', { page_location: sanitizedUrl } as PageViewParams); } - private logEvent(eventName: string, eventParams: EventParams): void { - if (this.onlineStatus.isOnline && typeof environment.googleTagId === 'string') { - gtag(eventName, environment.googleTagId, eventParams); - } + private send(command: GoogleCommands, name: any, params?: CommandParams): void { + Promise.all([this.initiated, this.onlineStatus.online]).then(() => gtag(command, name, params)); } } From 6e618f38ba7aff928a1ebdbc39a11edf3a0a60dc Mon Sep 17 00:00:00 2001 From: Nigel Wells Date: Tue, 9 Jul 2024 08:46:39 +1200 Subject: [PATCH 4/7] Include new GTM package --- .../ClientApp/package-lock.json | 13 +++++ .../ClientApp/package.json | 1 + .../ClientApp/src/app/app-routing.module.ts | 42 ++++++++++++--- .../ClientApp/src/app/app.component.ts | 41 ++++++++------ .../ClientApp/src/app/app.module.ts | 9 +++- .../app/checking/checking-routing.module.ts | 18 +++++-- .../app/translate/translate-routing.module.ts | 24 +++++++-- .../src/app/users/users-routing.module.ts | 8 ++- .../src/environments/environment.prod.ts | 4 +- .../src/environments/environment.pwa-test.ts | 4 +- .../src/environments/environment.staging.ts | 4 +- .../ClientApp/src/environments/environment.ts | 4 +- .../ClientApp/src/index.html | 8 --- .../src/xforge-common/analytics.service.ts | 54 ++++++------------- 14 files changed, 151 insertions(+), 83 deletions(-) diff --git a/src/SIL.XForge.Scripture/ClientApp/package-lock.json b/src/SIL.XForge.Scripture/ClientApp/package-lock.json index 9599c8cb53d..ccdff75e659 100644 --- a/src/SIL.XForge.Scripture/ClientApp/package-lock.json +++ b/src/SIL.XForge.Scripture/ClientApp/package-lock.json @@ -29,6 +29,7 @@ "@sillsdev/machine": "^2.4.2", "@sillsdev/scripture": "1.4.1", "angular-file": "^4.0.2", + "angular-google-tag-manager": "^1.9.0", "angular-split": "^16.2.1", "arraydiff": "^0.1.3", "bowser": "^2.11.0", @@ -9224,6 +9225,18 @@ "tslib": "^2.3.0" } }, + "node_modules/angular-google-tag-manager": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/angular-google-tag-manager/-/angular-google-tag-manager-1.9.0.tgz", + "integrity": "sha512-4FIgoeljnbrsWHanKcud6zSGf08sH6Frdk6xcP5pauAk+YVMhxxoCisAsI0HSmzi5jPOguma3F+/+wHdtE3RjA==", + "dependencies": { + "tslib": "^2.5.0" + }, + "peerDependencies": { + "@angular/common": "^17.0.3", + "@angular/compiler": "^17.0.3" + } + }, "node_modules/angular-split": { "version": "16.2.1", "resolved": "https://registry.npmjs.org/angular-split/-/angular-split-16.2.1.tgz", diff --git a/src/SIL.XForge.Scripture/ClientApp/package.json b/src/SIL.XForge.Scripture/ClientApp/package.json index ba803e56d3c..e4d784fe548 100644 --- a/src/SIL.XForge.Scripture/ClientApp/package.json +++ b/src/SIL.XForge.Scripture/ClientApp/package.json @@ -53,6 +53,7 @@ "@sillsdev/machine": "^2.4.2", "@sillsdev/scripture": "1.4.1", "angular-file": "^4.0.2", + "angular-google-tag-manager": "^1.9.0", "angular-split": "^16.2.1", "arraydiff": "^0.1.3", "bowser": "^2.11.0", diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/app-routing.module.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/app-routing.module.ts index f8990e5d240..309789b76f5 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/app-routing.module.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/app-routing.module.ts @@ -16,21 +16,47 @@ import { SettingsComponent } from './settings/settings.component'; import { PageNotFoundComponent } from './shared/page-not-found/page-not-found.component'; import { SettingsAuthGuard, SyncAuthGuard } from './shared/project-router.guard'; import { SyncComponent } from './sync/sync.component'; +import { environment } from '../environments/environment'; const routes: Routes = [ { path: 'callback/auth0', component: MyProjectsComponent, canActivate: [AuthGuard] }, - { path: 'connect-project', component: ConnectProjectComponent, canActivate: [AuthGuard] }, + { + path: 'connect-project', + component: ConnectProjectComponent, + canActivate: [AuthGuard], + title: `Connect Project - ${environment.siteName}` + }, { path: 'login', redirectTo: 'projects', pathMatch: 'full' }, - { path: 'join/:shareKey', component: JoinComponent }, - { path: 'join/:shareKey/:locale', component: JoinComponent }, - { path: 'projects/:projectId/event-log', component: EventMetricsComponent, canActivate: [EventMetricsAuthGuard] }, - { path: 'projects/:projectId/settings', component: SettingsComponent, canActivate: [SettingsAuthGuard] }, - { path: 'projects/:projectId/sync', component: SyncComponent, canActivate: [SyncAuthGuard] }, + { path: 'join/:shareKey', component: JoinComponent, title: `Join Project - ${environment.siteName}` }, + { path: 'join/:shareKey/:locale', component: JoinComponent, title: `Join Project - ${environment.siteName}` }, + { + path: 'projects/:projectId/event-log', component: EventMetricsComponent, canActivate: [EventMetricsAuthGuard] }, + { path: 'projects/:projectId/settings', + component: SettingsComponent, + canActivate: [SettingsAuthGuard], + title: `Project Settings - ${environment.siteName}` + }, + { + path: 'projects/:projectId/sync', + component: SyncComponent, + canActivate: [SyncAuthGuard], + title: `Synchronize Project - ${environment.siteName}` + }, { path: 'projects/:projectId', component: ProjectComponent, canActivate: [AuthGuard] }, { path: 'projects', component: MyProjectsComponent, canActivate: [AuthGuard] }, { path: 'serval-administration/:projectId', component: ServalProjectComponent, canActivate: [ServalAdminAuthGuard] }, - { path: 'serval-administration', component: ServalAdministrationComponent, canActivate: [ServalAdminAuthGuard] }, - { path: 'system-administration', component: SystemAdministrationComponent, canActivate: [SystemAdminAuthGuard] }, + { + path: 'serval-administration', + component: ServalAdministrationComponent, + canActivate: [ServalAdminAuthGuard], + title: `Serval Administration - ${environment.siteName}` + }, + { + path: 'system-administration', + component: SystemAdministrationComponent, + canActivate: [SystemAdminAuthGuard], + title: `System Administration - ${environment.siteName}` + }, { path: '**', component: PageNotFoundComponent } ]; diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts index 217bfb6a9ba..e62652a7605 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts @@ -1,6 +1,6 @@ import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; import { Component, DestroyRef, OnDestroy, OnInit } from '@angular/core'; -import { NavigationEnd, Router } from '@angular/router'; +import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import Bugsnag from '@bugsnag/js'; import { translate } from '@ngneat/transloco'; import { cloneDeep } from 'lodash-es'; @@ -8,8 +8,8 @@ import { CookieService } from 'ngx-cookie-service'; import { SystemRole } from 'realtime-server/lib/esm/common/models/system-role'; import { AuthType, getAuthType, User } from 'realtime-server/lib/esm/common/models/user'; import { SFProjectRole } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-role'; -import { Observable, Subscription } from 'rxjs'; -import { filter, map } from 'rxjs/operators'; +import { Observable, pipe, Subscription } from 'rxjs'; +import { debounceTime, distinctUntilChanged, filter, map } from 'rxjs/operators'; import { ActivatedProjectService } from 'xforge-common/activated-project.service'; import { AuthService } from 'xforge-common/auth.service'; import { DataLoadingComponent } from 'xforge-common/data-loading-component'; @@ -36,9 +36,9 @@ import { quietTakeUntilDestroyed } from 'xforge-common/util/rxjs-util'; import { issuesEmailTemplate, supportedBrowser } from 'xforge-common/utils'; import { ThemeService } from 'xforge-common/theme.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { AnalyticsService, PageViewEvent, TagEventType } from 'xforge-common/analytics.service'; import versionData from '../../../version.json'; import { environment } from '../environments/environment'; -import { AnalyticsService } from "xforge-common/analytics.service"; import { SFProjectProfileDoc } from './core/models/sf-project-profile-doc'; import { roleCanAccessTranslate } from './core/models/sf-project-role-info'; import { SFProjectUserConfigDoc } from './core/models/sf-project-user-config-doc'; @@ -93,7 +93,8 @@ export class AppComponent extends DataLoadingComponent implements OnInit, OnDest private readonly themeService: ThemeService, onlineStatusService: OnlineStatusService, private destroyRef: DestroyRef, - private readonly analytics: AnalyticsService + private readonly analytics: AnalyticsService, + private readonly activatedRoute: ActivatedRoute ) { super(noticeService); this.breakpointObserver @@ -127,17 +128,25 @@ export class AppComponent extends DataLoadingComponent implements OnInit, OnDest pwaService.hasUpdate$.pipe(quietTakeUntilDestroyed(this.destroyRef)).subscribe(() => (this.hasUpdate = true)); // Google Analytics - send data at end of navigation so we get data inside the SPA client-side routing - if (environment.releaseStage === 'live') { - const navEndEvent$ = router.events.pipe( - filter(e => e instanceof NavigationEnd), - map(e => e as NavigationEnd) - ); - navEndEvent$.pipe(quietTakeUntilDestroyed(this.destroyRef)).subscribe(e => { - if (this.isAppOnline) { - this.analytics.logNavigation(e.urlAfterRedirects); - } - }); - } + const navEndEvent$ = router.events.pipe( + filter(e => e instanceof NavigationEnd), + distinctUntilChanged((previous, current) => { + const previousUrl = new URL((previous as NavigationEnd).urlAfterRedirects, location.origin); + const currentUrl = new URL((current as NavigationEnd).urlAfterRedirects, location.origin); + console.log(previousUrl, currentUrl); + return previousUrl.pathname === currentUrl.pathname; + }), + map(e => { + const navEndEvent = e as NavigationEnd; + let route = this.activatedRoute.root; + while (route.firstChild) route = route.firstChild; + return { + pageName: this.locationService.host + navEndEvent.urlAfterRedirects, + title: route.snapshot.routeConfig?.title?.toString() + } as PageViewEvent; + }) + ); + this.subscribe(navEndEvent$, pageViewEvent => this.analytics.logNavigation(pageViewEvent)); } get canInstallOnDevice$(): Observable { diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/app.module.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/app.module.ts index bc363c17f04..3df708f25aa 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/app.module.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/app.module.ts @@ -24,6 +24,8 @@ import { InAppRootOverlayContainer } from 'xforge-common/overlay-container'; import { SupportedBrowsersDialogComponent } from 'xforge-common/supported-browsers-dialog/supported-browsers-dialog.component'; import { UICommonModule } from 'xforge-common/ui-common.module'; import { XForgeCommonModule } from 'xforge-common/xforge-common.module'; +import { en } from 'xforge-common/i18n.service'; +import { GoogleTagManagerModule } from 'angular-google-tag-manager'; import { environment } from '../environments/environment'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; @@ -82,7 +84,12 @@ import { UsersModule } from './users/users.module'; AvatarComponent, MatRipple, GlobalNoticesComponent, - QuillModule.forRoot() + QuillModule.forRoot(), + GoogleTagManagerModule.forRoot({ + id: environment.googleTagManagerId, + gtm_auth: environment.googleTagManagerAuth, + gtm_preview: environment.googleTagManagerPreview + }) ], providers: [ { provide: APP_ID, useValue: 'ng-cli-universal' }, diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking-routing.module.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking-routing.module.ts index 03f03db09b9..f32fdefb196 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking-routing.module.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking-routing.module.ts @@ -4,15 +4,27 @@ import { RouterModule, Routes } from '@angular/router'; import { CheckingAuthGuard } from '../shared/project-router.guard'; import { CheckingOverviewComponent } from './checking-overview/checking-overview.component'; import { CheckingComponent } from './checking/checking.component'; +import { environment } from '../../environments/environment'; const routes: Routes = [ { path: 'projects/:projectId/checking/:bookId/:chapter', component: CheckingComponent, - canActivate: [CheckingAuthGuard] + canActivate: [CheckingAuthGuard], + title: `Community Checking Questions & Answers - ${environment.siteName}` }, - { path: 'projects/:projectId/checking/:bookId', component: CheckingComponent, canActivate: [CheckingAuthGuard] }, - { path: 'projects/:projectId/checking', component: CheckingOverviewComponent, canActivate: [CheckingAuthGuard] } + { + path: 'projects/:projectId/checking/:bookId', + component: CheckingComponent, + canActivate: [CheckingAuthGuard], + title: `Community Checking Questions & Answers - ${environment.siteName}` + }, + { + path: 'projects/:projectId/checking', + component: CheckingOverviewComponent, + canActivate: [CheckingAuthGuard], + title: `Community Checking Management - ${environment.siteName}` + } ]; @NgModule({ diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/translate-routing.module.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/translate-routing.module.ts index 525adc2ed8e..0bcb8a82eeb 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/translate-routing.module.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/translate-routing.module.ts @@ -5,24 +5,38 @@ import { DraftGenerationComponent } from './draft-generation/draft-generation.co import { DraftSourcesComponent } from './draft-generation/draft-sources/draft-sources.component'; import { EditorComponent } from './editor/editor.component'; import { TranslateOverviewComponent } from './translate-overview/translate-overview.component'; +import { environment } from '../../environments/environment'; const routes: Routes = [ { path: 'projects/:projectId/translate/:bookId/:chapter', component: EditorComponent, - canActivate: [TranslateAuthGuard] + canActivate: [TranslateAuthGuard], + title: `Editor & Review - ${environment.siteName}` + }, + { + path: 'projects/:projectId/translate/:bookId', + component: EditorComponent, + canActivate: [TranslateAuthGuard], + title: `Editor & Review - ${environment.siteName}` + }, + { + path: 'projects/:projectId/translate', + component: TranslateOverviewComponent, + canActivate: [TranslateAuthGuard], + title: `Translation Overview - ${environment.siteName}` }, - { path: 'projects/:projectId/translate/:bookId', component: EditorComponent, canActivate: [TranslateAuthGuard] }, - { path: 'projects/:projectId/translate', component: TranslateOverviewComponent, canActivate: [TranslateAuthGuard] }, { path: 'projects/:projectId/draft-generation', component: DraftGenerationComponent, - canActivate: [NmtDraftAuthGuard] + canActivate: [NmtDraftAuthGuard], + title: `Draft Generation - ${environment.siteName}` }, { path: 'projects/:projectId/draft-generation/sources', component: DraftSourcesComponent, - canActivate: [NmtDraftAuthGuard] + canActivate: [NmtDraftAuthGuard], + title: `Configure Draft Sources - ${environment.siteName}` } ]; diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/users/users-routing.module.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/users/users-routing.module.ts index eef384eac1a..dfd7ecfd7c7 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/users/users-routing.module.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/users/users-routing.module.ts @@ -2,9 +2,15 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { UsersAuthGuard } from '../shared/project-router.guard'; import { UsersComponent } from './users.component'; +import { environment } from '../../environments/environment'; const routes: Routes = [ - { path: 'projects/:projectId/users', component: UsersComponent, canActivate: [UsersAuthGuard] } + { + path: 'projects/:projectId/users', + component: UsersComponent, + canActivate: [UsersAuthGuard], + title: `User Management - ${environment.siteName}` + } ]; @NgModule({ diff --git a/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.prod.ts b/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.prod.ts index 5c77930d837..6a9561b50e5 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.prod.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.prod.ts @@ -17,5 +17,7 @@ export const environment = { authDomain: 'login.languagetechnology.org', authClientId: 'tY2wXn40fsL5VsPM4uIHNtU6ZUEXGeFn', offlineDBVersion: 8, - googleTagId: 'G-SVKBDV7K3Q' + googleTagManagerId: 'GTM-P2DF8SLM', + googleTagManagerAuth: 'OCXvABYNFBKJ0TJkAGsvAw', + googleTagManagerPreview: 'env-1' }; diff --git a/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.pwa-test.ts b/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.pwa-test.ts index e03081dfbb5..41facf3ae97 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.pwa-test.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.pwa-test.ts @@ -17,5 +17,7 @@ export const environment = { authDomain: 'sil-appbuilder.auth0.com', authClientId: 'aoAGb9Yx1H5WIsvCW6JJCteJhSa37ftH', offlineDBVersion: 8, - googleTagId: null + googleTagManagerId: 'GTM-P2DF8SLM', + googleTagManagerAuth: 'AC1M72Jw4UydK-bnFoT0Cw', + googleTagManagerPreview: 'env-8' }; diff --git a/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.staging.ts b/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.staging.ts index 681c5978d67..dc0a52c5a7f 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.staging.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.staging.ts @@ -17,5 +17,7 @@ export const environment = { authDomain: 'dev-sillsdev.auth0.com', authClientId: '4eHLjo40mAEGFU6zUxdYjnpnC1K1Ydnj', offlineDBVersion: 8, - googleTagId: null + googleTagManagerId: 'GTM-P2DF8SLM', + googleTagManagerAuth: 'pcbHrZyROB6E6AS0PZsD1Q', + googleTagManagerPreview: 'env-7' }; diff --git a/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.ts b/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.ts index 104997922cf..3b1f21dd893 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.ts @@ -24,5 +24,7 @@ export const environment = { authDomain: 'sil-appbuilder.auth0.com', authClientId: 'aoAGb9Yx1H5WIsvCW6JJCteJhSa37ftH', offlineDBVersion: 8, - googleTagId: null + googleTagManagerId: 'GTM-P2DF8SLM', + googleTagManagerAuth: 'AC1M72Jw4UydK-bnFoT0Cw', + googleTagManagerPreview: 'env-8' }; diff --git a/src/SIL.XForge.Scripture/ClientApp/src/index.html b/src/SIL.XForge.Scripture/ClientApp/src/index.html index fb0d818c9e3..52ebb71cce8 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/index.html +++ b/src/SIL.XForge.Scripture/ClientApp/src/index.html @@ -1,14 +1,6 @@ - - - diff --git a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/analytics.service.ts b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/analytics.service.ts index a22344674ae..7fa20eaec0d 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/analytics.service.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/analytics.service.ts @@ -1,55 +1,35 @@ import { Injectable } from '@angular/core'; -import { environment } from '../environments/environment'; +import { GoogleTagManagerService } from 'angular-google-tag-manager'; import { OnlineStatusService } from './online-status.service'; -declare function gtag(...args: any): void; - -interface CommandParams {} - -enum GoogleCommands { - Config = 'config', - Event = 'event', - JavaScript = 'js' +interface TagEvent { + event: TagEventType; } -interface ConfigParams extends CommandParams { - send_page_view?: boolean; +export interface PageViewEvent extends TagEvent { + event: TagEventType.PageView; + pageName: string; + title?: string; } -interface PageViewParams extends CommandParams { - page_location?: string; - page_title?: string; +export enum TagEventType { + PageView = 'virtualPageView' } @Injectable({ providedIn: 'root' }) export class AnalyticsService { - private initiated?: Promise; - constructor(private readonly onlineStatus: OnlineStatusService) { - if (typeof environment.googleTagId !== 'string') { - return; - } - - this.initiated = new Promise(resolve => { - this.onlineStatus.online.then(() => { - this.send(GoogleCommands.JavaScript, new Date()); - this.send(GoogleCommands.Config, environment.googleTagId, { send_page_view: false } as ConfigParams); - resolve(); - }); - }); - } + constructor(private readonly onlineStatus: OnlineStatusService, private gtmService: GoogleTagManagerService) {} /** * 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. + * @param event The URL of the page that was navigated to. */ - logNavigation(url: string): void { - const sanitizedUrl = sanitizeUrl(url); - this.send(GoogleCommands.Event, 'page_view', { page_location: sanitizedUrl } as PageViewParams); - } - - private send(command: GoogleCommands, name: any, params?: CommandParams): void { - Promise.all([this.initiated, this.onlineStatus.online]).then(() => gtag(command, name, params)); + logNavigation(event: PageViewEvent): void { + event.event = TagEventType.PageView; + event.pageName = sanitizeUrl(event.pageName); + console.log(event); + // this.gtmService.pushTag(event); } } @@ -87,7 +67,7 @@ function redactJoinKey(url: string): string { } /** - * Redacts sensitive information from the given URL. Currently this only redacts the access token and the join key, so + * 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. From 2074e9c08f6b9df91f9372e36ba21dcbf04091c3 Mon Sep 17 00:00:00 2001 From: Nigel Wells Date: Tue, 15 Apr 2025 14:30:26 +1200 Subject: [PATCH 5/7] Fixes after rebase --- .../ClientApp/src/app/app.component.ts | 13 ++++++------- .../src/xforge-common/analytics.service.ts | 8 +++++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts index e62652a7605..f12a6254848 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts @@ -8,8 +8,8 @@ import { CookieService } from 'ngx-cookie-service'; import { SystemRole } from 'realtime-server/lib/esm/common/models/system-role'; import { AuthType, getAuthType, User } from 'realtime-server/lib/esm/common/models/user'; import { SFProjectRole } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-role'; -import { Observable, pipe, Subscription } from 'rxjs'; -import { debounceTime, distinctUntilChanged, filter, map } from 'rxjs/operators'; +import { Observable, Subscription } from 'rxjs'; +import { distinctUntilChanged, filter, map } from 'rxjs/operators'; import { ActivatedProjectService } from 'xforge-common/activated-project.service'; import { AuthService } from 'xforge-common/auth.service'; import { DataLoadingComponent } from 'xforge-common/data-loading-component'; @@ -36,7 +36,7 @@ import { quietTakeUntilDestroyed } from 'xforge-common/util/rxjs-util'; import { issuesEmailTemplate, supportedBrowser } from 'xforge-common/utils'; import { ThemeService } from 'xforge-common/theme.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { AnalyticsService, PageViewEvent, TagEventType } from 'xforge-common/analytics.service'; +import { AnalyticsService, PageViewEvent } from 'xforge-common/analytics.service'; import versionData from '../../../version.json'; import { environment } from '../environments/environment'; import { SFProjectProfileDoc } from './core/models/sf-project-profile-doc'; @@ -45,8 +45,6 @@ import { SFProjectUserConfigDoc } from './core/models/sf-project-user-config-doc import { SFProjectService } from './core/sf-project.service'; import { checkAppAccess } from './shared/utils'; -declare function gtag(...args: any): void; - @Component({ selector: 'app-root', templateUrl: './app.component.html', @@ -133,7 +131,6 @@ export class AppComponent extends DataLoadingComponent implements OnInit, OnDest distinctUntilChanged((previous, current) => { const previousUrl = new URL((previous as NavigationEnd).urlAfterRedirects, location.origin); const currentUrl = new URL((current as NavigationEnd).urlAfterRedirects, location.origin); - console.log(previousUrl, currentUrl); return previousUrl.pathname === currentUrl.pathname; }), map(e => { @@ -146,7 +143,9 @@ export class AppComponent extends DataLoadingComponent implements OnInit, OnDest } as PageViewEvent; }) ); - this.subscribe(navEndEvent$, pageViewEvent => this.analytics.logNavigation(pageViewEvent)); + navEndEvent$ + .pipe(quietTakeUntilDestroyed(this.destroyRef)) + .subscribe(pageViewEvent => this.analytics.logNavigation(pageViewEvent)); } get canInstallOnDevice$(): Observable { diff --git a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/analytics.service.ts b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/analytics.service.ts index 7fa20eaec0d..dddad78672c 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/analytics.service.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/analytics.service.ts @@ -18,7 +18,10 @@ export enum TagEventType { @Injectable({ providedIn: 'root' }) export class AnalyticsService { - constructor(private readonly onlineStatus: OnlineStatusService, private gtmService: GoogleTagManagerService) {} + constructor( + private readonly onlineStatus: OnlineStatusService, + private gtmService: GoogleTagManagerService + ) {} /** * Logs the page navigation event to the analytics service. This method is responsible for sanitizing the URL before @@ -28,8 +31,7 @@ export class AnalyticsService { logNavigation(event: PageViewEvent): void { event.event = TagEventType.PageView; event.pageName = sanitizeUrl(event.pageName); - console.log(event); - // this.gtmService.pushTag(event); + this.gtmService.pushTag(event); } } From 874cbe21bcf9df3f6c88910fd19882146f002c14 Mon Sep 17 00:00:00 2001 From: Nigel Wells Date: Tue, 15 Apr 2025 14:35:10 +1200 Subject: [PATCH 6/7] Removed unused variable --- src/SIL.XForge.Scripture/ClientApp/src/app/app.module.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/app.module.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/app.module.ts index 3df708f25aa..f3e69149a83 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/app.module.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/app.module.ts @@ -24,7 +24,6 @@ import { InAppRootOverlayContainer } from 'xforge-common/overlay-container'; import { SupportedBrowsersDialogComponent } from 'xforge-common/supported-browsers-dialog/supported-browsers-dialog.component'; import { UICommonModule } from 'xforge-common/ui-common.module'; import { XForgeCommonModule } from 'xforge-common/xforge-common.module'; -import { en } from 'xforge-common/i18n.service'; import { GoogleTagManagerModule } from 'angular-google-tag-manager'; import { environment } from '../environments/environment'; import { AppRoutingModule } from './app-routing.module'; From 0ce367aa86c4cc99b049fc0b7f0bce34e0188fd1 Mon Sep 17 00:00:00 2001 From: Nigel Wells Date: Tue, 15 Apr 2025 14:41:12 +1200 Subject: [PATCH 7/7] Prettier fixes --- .../ClientApp/src/app/app-routing.module.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/app-routing.module.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/app-routing.module.ts index 309789b76f5..2de86e7006c 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/app-routing.module.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/app-routing.module.ts @@ -30,8 +30,12 @@ const routes: Routes = [ { path: 'join/:shareKey', component: JoinComponent, title: `Join Project - ${environment.siteName}` }, { path: 'join/:shareKey/:locale', component: JoinComponent, title: `Join Project - ${environment.siteName}` }, { - path: 'projects/:projectId/event-log', component: EventMetricsComponent, canActivate: [EventMetricsAuthGuard] }, - { path: 'projects/:projectId/settings', + path: 'projects/:projectId/event-log', + component: EventMetricsComponent, + canActivate: [EventMetricsAuthGuard] + }, + { + path: 'projects/:projectId/settings', component: SettingsComponent, canActivate: [SettingsAuthGuard], title: `Project Settings - ${environment.siteName}`