From 222e90395117b5b88c9347c930992c1535855776 Mon Sep 17 00:00:00 2001 From: Jonathan Lim-Breitbart Date: Fri, 21 Mar 2025 16:49:59 -0700 Subject: [PATCH 1/4] feat(Analytics): Add Google Tag Manager support --- src/app/app.component.spec.ts | 9 +++++++++ src/app/app.component.ts | 16 ++++++++++++++++ src/app/domain/config.ts | 1 + src/app/services/config.service.ts | 18 ++++++++++-------- 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 5320718087d..e9fe64b3d78 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -25,6 +25,7 @@ export class MockConfigService { getConfig(): Observable { const config: Config = new Config(); config.googleAnalyticsId = 'UA-XXXXXX-1'; + config.googleTagManagerId = 'GTM-XXXXXXXX'; this.config$.next(config); return this.config$; } @@ -32,6 +33,10 @@ export class MockConfigService { getGoogleAnalyticsId(): string { return this.config$.getValue().googleAnalyticsId; } + + getGoogleTagMangaerId(): string { + return this.config$.getValue().googleTagManagerId; + } } export class MockUtilService { @@ -99,4 +104,8 @@ describe('AppComponent', () => { it(`should set Google Analytics tracking code`, () => { expect(component.googleAnalyticsId).toEqual('UA-XXXXXX-1'); }); + + it(`should set Google Tag manager tracking code`, () => { + expect(component.googleTagManagerId).toEqual('GTM-XXXXXXXX'); + }); }); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 37f42d8c233..e22c607deee 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -23,6 +23,7 @@ export class AppComponent { theme: string = ''; mediaWatcher: Subscription; googleAnalyticsId: string = null; + googleTagManagerId: string = null; hasAnnouncement: boolean = false; showDefaultMode: boolean = true; showHeaderAndFooter: boolean = true; @@ -141,6 +142,10 @@ export class AppComponent { } setGTagManager() { + this.googleTagManagerId = this.configService.getGoogleTagManagerId(); + if (this.googleTagManagerId) { + this.activateGtm(window, document, 'script', 'dataLayer', this.googleTagManagerId); + } this.googleAnalyticsId = this.configService.getGoogleAnalyticsId(); if (this.googleAnalyticsId) { const gtagScript = this.document.createElement('script'); @@ -155,6 +160,17 @@ export class AppComponent { } } + activateGtm(w, d, s, l, i) { + w[l] = w[l] || []; + w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' }); + var f = d.getElementsByTagName(s)[0], + j = d.createElement(s), + dl = l != 'dataLayer' ? '&l=' + l : ''; + j.async = true; + j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl; + f.parentNode.insertBefore(j, f); + } + fixScrollTop(ev: any) { const topElement = document.querySelector('.top-content'); if (!topElement) { diff --git a/src/app/domain/config.ts b/src/app/domain/config.ts index c067be2bee4..fb63eb9d760 100644 --- a/src/app/domain/config.ts +++ b/src/app/domain/config.ts @@ -1,6 +1,7 @@ export class Config { contextPath: string; googleAnalyticsId?: string; + googleTagManagerId?: string; googleClientId?: string; isGoogleClassroomEnabled?: boolean; microsoftClientId?: string; diff --git a/src/app/services/config.service.ts b/src/app/services/config.service.ts index 2bcda211b5e..071a839a3d4 100644 --- a/src/app/services/config.service.ts +++ b/src/app/services/config.service.ts @@ -19,14 +19,12 @@ export class ConfigService { retrieveConfig(): Observable { const headers = new HttpHeaders({ 'Cache-Control': 'no-cache' }); - return this.http - .get(this.userConfigUrl, { headers: headers }) - .pipe( - tap((config) => { - this.config$.next(config); - this.timeDiff = Date.now() - config.currentTime; - }) - ); + return this.http.get(this.userConfigUrl, { headers: headers }).pipe( + tap((config) => { + this.config$.next(config); + this.timeDiff = Date.now() - config.currentTime; + }) + ); } getContextPath() { @@ -45,6 +43,10 @@ export class ConfigService { return this.config$.getValue().googleAnalyticsId; } + getGoogleTagManagerId() { + return this.config$.getValue().googleTagManagerId; + } + getGoogleClientId() { return this.config$.getValue().googleClientId; } From 3ceaec654c51717b5927cfff05d5fab815a80d92 Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Tue, 25 Mar 2025 11:41:51 -0700 Subject: [PATCH 2/4] Fix unit test by fixing typo. --- src/app/app.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index e9fe64b3d78..9ad8642ceb1 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -34,7 +34,7 @@ export class MockConfigService { return this.config$.getValue().googleAnalyticsId; } - getGoogleTagMangaerId(): string { + getGoogleTagManagerId(): string { return this.config$.getValue().googleTagManagerId; } } From 22f7f842afa77950d1ae66c64e691f1deddf5f1a Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Tue, 25 Mar 2025 11:48:39 -0700 Subject: [PATCH 3/4] Fix deprecations in test --- src/app/app.component.spec.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 9ad8642ceb1..95bfd513ee8 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -1,7 +1,6 @@ import { TestBed, ComponentFixture } from '@angular/core/testing'; import { AppComponent } from './app.component'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { RouterTestingModule } from '@angular/router/testing'; import { MediaChange, MediaObserver } from '@angular/flex-layout'; import { Observable, BehaviorSubject } from 'rxjs'; import { UtilService } from './services/util.service'; @@ -9,12 +8,13 @@ import { Announcement } from './domain/announcement'; import { ConfigService } from './services/config.service'; import { Config } from './domain/config'; import { environment } from '../environments/environment'; +import { provideRouter } from '@angular/router'; export class MockConfigService { private config$: BehaviorSubject = new BehaviorSubject(null); getAnnouncement(): Observable { - return Observable.create((observer) => { + return new Observable((observer) => { const announcement: Announcement = new Announcement(); announcement.visible = true; observer.next(announcement); @@ -41,9 +41,8 @@ export class MockConfigService { export class MockUtilService { getMobileMenuState(): Observable { - return Observable.create((observer) => { - const state: boolean = false; - observer.next(state); + return new Observable((observer) => { + observer.next(false); observer.complete(); }); } @@ -55,7 +54,7 @@ export class MockObservableMedia { } asObservable(): Observable { - return Observable.create((observer) => { + return new Observable((observer) => { observer.next(new MediaChange()); observer.complete(); }); @@ -69,13 +68,13 @@ describe('AppComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ + declarations: [AppComponent], providers: [ { provide: ConfigService, useClass: MockConfigService }, { provide: UtilService, useClass: MockUtilService }, - { provide: MediaObserver, useClass: MockObservableMedia } + { provide: MediaObserver, useClass: MockObservableMedia }, + provideRouter([]) ], - declarations: [AppComponent], - imports: [RouterTestingModule], schemas: [NO_ERRORS_SCHEMA] }); fixture = TestBed.createComponent(AppComponent); From 038719ee45eebd888a1178489a30420c1f362709 Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Tue, 25 Mar 2025 13:57:25 -0700 Subject: [PATCH 4/4] Encapsulate googleTagManagerId and update test to look for the script element. --- src/app/app.component.spec.ts | 7 +++++-- src/app/app.component.ts | 11 +++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 95bfd513ee8..7c261ffa620 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -104,7 +104,10 @@ describe('AppComponent', () => { expect(component.googleAnalyticsId).toEqual('UA-XXXXXX-1'); }); - it(`should set Google Tag manager tracking code`, () => { - expect(component.googleTagManagerId).toEqual('GTM-XXXXXXXX'); + it(`should set Google Tag manager tracking script`, () => { + const scriptElement = document.querySelector( + 'head > script[src="https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXXXX"]' + ); + expect(scriptElement).toBeTruthy(); }); }); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index e22c607deee..808b00ed04f 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -23,7 +23,6 @@ export class AppComponent { theme: string = ''; mediaWatcher: Subscription; googleAnalyticsId: string = null; - googleTagManagerId: string = null; hasAnnouncement: boolean = false; showDefaultMode: boolean = true; showHeaderAndFooter: boolean = true; @@ -141,10 +140,10 @@ export class AppComponent { }); } - setGTagManager() { - this.googleTagManagerId = this.configService.getGoogleTagManagerId(); - if (this.googleTagManagerId) { - this.activateGtm(window, document, 'script', 'dataLayer', this.googleTagManagerId); + private setGTagManager(): void { + const googleTagManagerId = this.configService.getGoogleTagManagerId(); + if (googleTagManagerId) { + this.activateGTM(window, document, 'script', 'dataLayer', googleTagManagerId); } this.googleAnalyticsId = this.configService.getGoogleAnalyticsId(); if (this.googleAnalyticsId) { @@ -160,7 +159,7 @@ export class AppComponent { } } - activateGtm(w, d, s, l, i) { + private activateGTM(w, d, s, l, i): void { w[l] = w[l] || []; w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' }); var f = d.getElementsByTagName(s)[0],