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..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 @@ -16,21 +16,51 @@ 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 aa6f8bb81b0..f12a6254848 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'; @@ -9,7 +9,7 @@ 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 { 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,6 +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 } from 'xforge-common/analytics.service'; import versionData from '../../../version.json'; import { environment } from '../environments/environment'; import { SFProjectProfileDoc } from './core/models/sf-project-profile-doc'; @@ -44,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', @@ -91,7 +90,9 @@ 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, + private readonly activatedRoute: ActivatedRoute ) { super(noticeService); this.breakpointObserver @@ -125,18 +126,26 @@ 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) { - // eslint-disable-next-line @typescript-eslint/naming-convention - gtag('config', 'UA-22170471-15', { page_path: 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); + 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; + }) + ); + navEndEvent$ + .pipe(quietTakeUntilDestroyed(this.destroyRef)) + .subscribe(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..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,6 +24,7 @@ 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 { GoogleTagManagerModule } from 'angular-google-tag-manager'; import { environment } from '../environments/environment'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; @@ -82,7 +83,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 147395431bc..6a9561b50e5 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,8 @@ export const environment = { realtimeUrl: '/realtime-api/', authDomain: 'login.languagetechnology.org', authClientId: 'tY2wXn40fsL5VsPM4uIHNtU6ZUEXGeFn', - offlineDBVersion: 8 + offlineDBVersion: 8, + 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 9aa263296c4..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 @@ -16,5 +16,8 @@ export const environment = { realtimeUrl: '/', authDomain: 'sil-appbuilder.auth0.com', authClientId: 'aoAGb9Yx1H5WIsvCW6JJCteJhSa37ftH', - offlineDBVersion: 8 + offlineDBVersion: 8, + 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 fa846f11138..dc0a52c5a7f 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,8 @@ export const environment = { realtimeUrl: '/realtime-api/', authDomain: 'dev-sillsdev.auth0.com', authClientId: '4eHLjo40mAEGFU6zUxdYjnpnC1K1Ydnj', - offlineDBVersion: 8 + offlineDBVersion: 8, + 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 7b592e0a551..3b1f21dd893 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/environments/environment.ts @@ -23,5 +23,8 @@ export const environment = { realtimeUrl: '/', authDomain: 'sil-appbuilder.auth0.com', authClientId: 'aoAGb9Yx1H5WIsvCW6JJCteJhSa37ftH', - offlineDBVersion: 8 + offlineDBVersion: 8, + 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 06e4f96f0dd..52ebb71cce8 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/index.html +++ b/src/SIL.XForge.Scripture/ClientApp/src/index.html @@ -1,16 +1,6 @@ - - - - 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..ccbbc9cf64e --- /dev/null +++ b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/analytics.service.spec.ts @@ -0,0 +1,14 @@ +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', () => { + ['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 new file mode 100644 index 00000000000..dddad78672c --- /dev/null +++ b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/analytics.service.ts @@ -0,0 +1,79 @@ +import { Injectable } from '@angular/core'; +import { GoogleTagManagerService } from 'angular-google-tag-manager'; +import { OnlineStatusService } from './online-status.service'; + +interface TagEvent { + event: TagEventType; +} + +export interface PageViewEvent extends TagEvent { + event: TagEventType.PageView; + pageName: string; + title?: string; +} + +export enum TagEventType { + PageView = 'virtualPageView' +} + +@Injectable({ providedIn: 'root' }) +export class AnalyticsService { + 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 event The URL of the page that was navigated to. + */ + logNavigation(event: PageViewEvent): void { + event.event = TagEventType.PageView; + event.pageName = sanitizeUrl(event.pageName); + this.gtmService.pushTag(event); + } +} + +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 @@ - - +