diff --git a/src/app/statistics/google-analytics.service.spec.ts b/src/app/statistics/google-analytics.service.spec.ts index 5953f3d8cc..9e2b1c7edf 100644 --- a/src/app/statistics/google-analytics.service.spec.ts +++ b/src/app/statistics/google-analytics.service.spec.ts @@ -1,4 +1,4 @@ -import { Angulartics2GoogleTagManager } from 'angulartics2'; +import { Angulartics2GoogleAnalytics, Angulartics2GoogleTagManager } from 'angulartics2'; import { of } from 'rxjs'; import { GoogleAnalyticsService } from './google-analytics.service'; @@ -10,11 +10,13 @@ import { GOOGLE_ANALYTICS_KLARO_KEY } from '../shared/cookies/klaro-configuratio describe('GoogleAnalyticsService', () => { const trackingIdProp = 'google.analytics.key'; - const trackingIdTestValue = 'mock-tracking-id'; + const trackingIdV4TestValue = 'G-mock-tracking-id'; + const trackingIdV3TestValue = 'UA-mock-tracking-id'; const innerHTMLTestValue = 'mock-script-inner-html'; const srcTestValue = 'mock-script-src'; let service: GoogleAnalyticsService; - let angularticsSpy: Angulartics2GoogleTagManager; + let googleAnalyticsSpy: Angulartics2GoogleAnalytics; + let googleTagManagerSpy: Angulartics2GoogleTagManager; let configSpy: ConfigurationDataService; let klaroServiceSpy: jasmine.SpyObj; let scriptElementMock: any; @@ -32,7 +34,10 @@ describe('GoogleAnalyticsService', () => { }); beforeEach(() => { - angularticsSpy = jasmine.createSpyObj('Angulartics2GoogleTagManager', [ + googleAnalyticsSpy = jasmine.createSpyObj('Angulartics2GoogleAnalytics', [ + 'startTracking', + ]); + googleTagManagerSpy = jasmine.createSpyObj('Angulartics2GoogleTagManager', [ 'startTracking', ]); @@ -40,7 +45,7 @@ describe('GoogleAnalyticsService', () => { 'getSavedPreferences': jasmine.createSpy('getSavedPreferences') }); - configSpy = createConfigSuccessSpy(trackingIdTestValue); + configSpy = createConfigSuccessSpy(trackingIdV4TestValue); scriptElementMock = { set src(newVal) { /* noop */ }, @@ -66,7 +71,7 @@ describe('GoogleAnalyticsService', () => { GOOGLE_ANALYTICS_KLARO_KEY: true })); - service = new GoogleAnalyticsService(angularticsSpy, klaroServiceSpy, configSpy, documentSpy ); + service = new GoogleAnalyticsService(googleAnalyticsSpy, googleTagManagerSpy, klaroServiceSpy, configSpy, documentSpy ); }); it('should be created', () => { @@ -90,7 +95,7 @@ describe('GoogleAnalyticsService', () => { GOOGLE_ANALYTICS_KLARO_KEY: true })); - service = new GoogleAnalyticsService(angularticsSpy, klaroServiceSpy, configSpy, documentSpy); + service = new GoogleAnalyticsService(googleAnalyticsSpy, googleTagManagerSpy, klaroServiceSpy, configSpy, documentSpy); }); it('should NOT add a script to the body', () => { @@ -100,7 +105,8 @@ describe('GoogleAnalyticsService', () => { it('should NOT start tracking', () => { service.addTrackingIdToPage(); - expect(angularticsSpy.startTracking).toHaveBeenCalledTimes(0); + expect(googleAnalyticsSpy.startTracking).toHaveBeenCalledTimes(0); + expect(googleTagManagerSpy.startTracking).toHaveBeenCalledTimes(0); }); }); @@ -111,7 +117,7 @@ describe('GoogleAnalyticsService', () => { klaroServiceSpy.getSavedPreferences.and.returnValue(of({ [GOOGLE_ANALYTICS_KLARO_KEY]: true })); - service = new GoogleAnalyticsService(angularticsSpy, klaroServiceSpy, configSpy, documentSpy); + service = new GoogleAnalyticsService(googleAnalyticsSpy, googleTagManagerSpy, klaroServiceSpy, configSpy, documentSpy); }); it('should NOT add a script to the body', () => { @@ -121,15 +127,16 @@ describe('GoogleAnalyticsService', () => { it('should NOT start tracking', () => { service.addTrackingIdToPage(); - expect(angularticsSpy.startTracking).toHaveBeenCalledTimes(0); + expect(googleAnalyticsSpy.startTracking).toHaveBeenCalledTimes(0); + expect(googleTagManagerSpy.startTracking).toHaveBeenCalledTimes(0); }); }); describe('when google-analytics cookie preferences are not existing', () => { beforeEach(() => { - configSpy = createConfigSuccessSpy(trackingIdTestValue); + configSpy = createConfigSuccessSpy(trackingIdV4TestValue); klaroServiceSpy.getSavedPreferences.and.returnValue(of({})); - service = new GoogleAnalyticsService(angularticsSpy, klaroServiceSpy, configSpy, documentSpy); + service = new GoogleAnalyticsService(googleAnalyticsSpy, googleTagManagerSpy, klaroServiceSpy, configSpy, documentSpy); }); it('should NOT add a script to the body', () => { @@ -139,18 +146,19 @@ describe('GoogleAnalyticsService', () => { it('should NOT start tracking', () => { service.addTrackingIdToPage(); - expect(angularticsSpy.startTracking).toHaveBeenCalledTimes(0); + expect(googleAnalyticsSpy.startTracking).toHaveBeenCalledTimes(0); + expect(googleTagManagerSpy.startTracking).toHaveBeenCalledTimes(0); }); }); describe('when google-analytics cookie preferences are set to false', () => { beforeEach(() => { - configSpy = createConfigSuccessSpy(trackingIdTestValue); + configSpy = createConfigSuccessSpy(trackingIdV4TestValue); klaroServiceSpy.getSavedPreferences.and.returnValue(of({ [GOOGLE_ANALYTICS_KLARO_KEY]: false })); - service = new GoogleAnalyticsService(angularticsSpy, klaroServiceSpy, configSpy, documentSpy); + service = new GoogleAnalyticsService(googleAnalyticsSpy, googleTagManagerSpy, klaroServiceSpy, configSpy, documentSpy); }); it('should NOT add a script to the body', () => { @@ -160,18 +168,19 @@ describe('GoogleAnalyticsService', () => { it('should NOT start tracking', () => { service.addTrackingIdToPage(); - expect(angularticsSpy.startTracking).toHaveBeenCalledTimes(0); + expect(googleAnalyticsSpy.startTracking).toHaveBeenCalledTimes(0); + expect(googleTagManagerSpy.startTracking).toHaveBeenCalledTimes(0); }); }); - describe('when both google-analytics cookie and the tracking id are non-empty', () => { + describe('when both google-analytics cookie and the tracking v4 id are non-empty', () => { beforeEach(() => { - configSpy = createConfigSuccessSpy(trackingIdTestValue); + configSpy = createConfigSuccessSpy(trackingIdV4TestValue); klaroServiceSpy.getSavedPreferences.and.returnValue(of({ [GOOGLE_ANALYTICS_KLARO_KEY]: true })); - service = new GoogleAnalyticsService(angularticsSpy, klaroServiceSpy, configSpy, documentSpy); + service = new GoogleAnalyticsService(googleAnalyticsSpy, googleTagManagerSpy, klaroServiceSpy, configSpy, documentSpy); }); it('should create a script tag whose innerHTML contains the tracking id', () => { @@ -183,10 +192,10 @@ describe('GoogleAnalyticsService', () => { expect(documentSpy.createElement('script')).toBe(scriptElementMock); expect(srcSpy).toHaveBeenCalledTimes(1); - expect(srcSpy.calls.argsFor(0)[0]).toContain(trackingIdTestValue); + expect(srcSpy.calls.argsFor(0)[0]).toContain(trackingIdV4TestValue); expect(innerHTMLSpy).toHaveBeenCalledTimes(1); - expect(innerHTMLSpy.calls.argsFor(0)[0]).toContain(trackingIdTestValue); + expect(innerHTMLSpy.calls.argsFor(0)[0]).toContain(trackingIdV4TestValue); }); it('should add a script to the body', () => { @@ -196,9 +205,46 @@ describe('GoogleAnalyticsService', () => { it('should start tracking', () => { service.addTrackingIdToPage(); - expect(angularticsSpy.startTracking).toHaveBeenCalledTimes(1); + expect(googleAnalyticsSpy.startTracking).not.toHaveBeenCalled(); + expect(googleTagManagerSpy.startTracking).toHaveBeenCalledTimes(1); }); }); + + describe('when both google-analytics cookie and the tracking id v3 are non-empty', () => { + + beforeEach(() => { + configSpy = createConfigSuccessSpy(trackingIdV3TestValue); + klaroServiceSpy.getSavedPreferences.and.returnValue(of({ + [GOOGLE_ANALYTICS_KLARO_KEY]: true + })); + service = new GoogleAnalyticsService(googleAnalyticsSpy, googleTagManagerSpy, klaroServiceSpy, configSpy, documentSpy); + }); + + it('should create a script tag whose innerHTML contains the tracking id', () => { + service.addTrackingIdToPage(); + expect(documentSpy.createElement).toHaveBeenCalledTimes(1); + expect(documentSpy.createElement).toHaveBeenCalledWith('script'); + + // sanity check + expect(documentSpy.createElement('script')).toBe(scriptElementMock); + + expect(innerHTMLSpy).toHaveBeenCalledTimes(1); + expect(innerHTMLSpy.calls.argsFor(0)[0]).toContain(trackingIdV3TestValue); + }); + + it('should add a script to the body', () => { + service.addTrackingIdToPage(); + expect(bodyElementSpy.appendChild).toHaveBeenCalledTimes(1); + }); + + it('should start tracking', () => { + service.addTrackingIdToPage(); + expect(googleAnalyticsSpy.startTracking).toHaveBeenCalledTimes(1); + expect(googleTagManagerSpy.startTracking).not.toHaveBeenCalled(); + }); + }); + }); + }); }); diff --git a/src/app/statistics/google-analytics.service.ts b/src/app/statistics/google-analytics.service.ts index d8a9c65959..9c5883d183 100644 --- a/src/app/statistics/google-analytics.service.ts +++ b/src/app/statistics/google-analytics.service.ts @@ -1,7 +1,7 @@ import { DOCUMENT } from '@angular/common'; import { Inject, Injectable } from '@angular/core'; -import { Angulartics2GoogleTagManager } from 'angulartics2'; +import { Angulartics2GoogleAnalytics, Angulartics2GoogleTagManager } from 'angulartics2'; import { combineLatest } from 'rxjs'; import { ConfigurationDataService } from '../core/data/configuration-data.service'; @@ -18,8 +18,8 @@ import { GOOGLE_ANALYTICS_KLARO_KEY } from '../shared/cookies/klaro-configuratio export class GoogleAnalyticsService { constructor( - // private angulartics: Angulartics2GoogleAnalytics, - private angulartics: Angulartics2GoogleTagManager, + private googleAnalytics: Angulartics2GoogleAnalytics, + private googleTagManager: Angulartics2GoogleTagManager, private klaroService: KlaroService, private configService: ConfigurationDataService, @Inject(DOCUMENT) private document: any, @@ -36,7 +36,9 @@ export class GoogleAnalyticsService { const googleKey$ = this.configService.findByPropertyName('google.analytics.key').pipe( getFirstCompletedRemoteData(), ); - combineLatest([this.klaroService.getSavedPreferences(), googleKey$]) + const preferences$ = this.klaroService.getSavedPreferences(); + + combineLatest([preferences$, googleKey$]) .subscribe(([preferences, remoteData]) => { // make sure user has accepted Google Analytics consents if (isEmpty(preferences) || isEmpty(preferences[GOOGLE_ANALYTICS_KLARO_KEY]) || !preferences[GOOGLE_ANALYTICS_KLARO_KEY]) { @@ -55,18 +57,37 @@ export class GoogleAnalyticsService { return; } - // add GTag snippet to page - const keyScript = this.document.createElement('script'); - keyScript.src = `https://www.googletagmanager.com/gtag/js?id=${trackingId}`; - this.document.body.appendChild(keyScript); + if (this.isGTagVersion(trackingId)) { - const libScript = this.document.createElement('script'); - libScript.innerHTML = `window.dataLayer = window.dataLayer || [];function gtag(){window.dataLayer.push(arguments);} + // add GTag snippet to page + const keyScript = this.document.createElement('script'); + keyScript.src = `https://www.googletagmanager.com/gtag/js?id=${trackingId}`; + this.document.body.appendChild(keyScript); + + const libScript = this.document.createElement('script'); + libScript.innerHTML = `window.dataLayer = window.dataLayer || [];function gtag(){window.dataLayer.push(arguments);} gtag('js', new Date());gtag('config', '${trackingId}');`; - this.document.body.appendChild(libScript); + this.document.body.appendChild(libScript); - // start tracking - this.angulartics.startTracking(); + // start tracking + this.googleTagManager.startTracking(); + } else { + // add trackingId snippet to page + const keyScript = this.document.createElement('script'); + keyScript.innerHTML = `(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ + (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), + m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) + })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); + ga('create', '${trackingId}', 'auto');`; + this.document.body.appendChild(keyScript); + + // start tracking + this.googleAnalytics.startTracking(); + } }); } + + private isGTagVersion(trackingId: string) { + return trackingId && trackingId.startsWith('G-'); + } } diff --git a/src/modules/app/browser-app.module.ts b/src/modules/app/browser-app.module.ts index 404c624a8a..6e3ebf5d37 100644 --- a/src/modules/app/browser-app.module.ts +++ b/src/modules/app/browser-app.module.ts @@ -15,13 +15,17 @@ import { AppModule } from '../../app/app.module'; import { ClientCookieService } from '../../app/core/services/client-cookie.service'; import { CookieService } from '../../app/core/services/cookie.service'; import { AuthService } from '../../app/core/auth/auth.service'; -import { Angulartics2RouterlessModule } from 'angulartics2'; +import { Angulartics2GoogleTagManager, Angulartics2RouterlessModule } from 'angulartics2'; import { SubmissionService } from '../../app/submission/submission.service'; import { StatisticsModule } from '../../app/statistics/statistics.module'; import { BrowserKlaroService } from '../../app/shared/cookies/browser-klaro.service'; import { KlaroService } from '../../app/shared/cookies/klaro.service'; import { HardRedirectService } from '../../app/core/services/hard-redirect.service'; -import { BrowserHardRedirectService, locationProvider, LocationToken } from '../../app/core/services/browser-hard-redirect.service'; +import { + BrowserHardRedirectService, + locationProvider, + LocationToken +} from '../../app/core/services/browser-hard-redirect.service'; import { LocaleService } from '../../app/core/locale/locale.service'; import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service'; import { AuthRequestService } from '../../app/core/auth/auth-request.service'; @@ -95,6 +99,10 @@ export function getRequest(transferState: TransferState): any { provide: GoogleAnalyticsService, useClass: GoogleAnalyticsService, }, + { + provide: Angulartics2GoogleTagManager, + useClass: Angulartics2GoogleTagManager + }, { provide: AuthRequestService, useClass: BrowserAuthRequestService, diff --git a/src/modules/app/server-app.module.ts b/src/modules/app/server-app.module.ts index 8565db3e23..fa529c4ad9 100644 --- a/src/modules/app/server-app.module.ts +++ b/src/modules/app/server-app.module.ts @@ -6,7 +6,7 @@ import { ServerModule, ServerTransferStateModule } from '@angular/platform-serve import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import { Angulartics2, Angulartics2GoogleTagManager } from 'angulartics2'; +import { Angulartics2, Angulartics2GoogleAnalytics, Angulartics2GoogleTagManager } from 'angulartics2'; import { AppComponent } from '../../app/app.component'; @@ -58,6 +58,10 @@ export function createTranslateLoader(transferState: TransferState) { provide: Angulartics2, useClass: Angulartics2Mock }, + { + provide: Angulartics2GoogleAnalytics, + useClass: AngularticsProviderMock + }, { provide: Angulartics2GoogleTagManager, useClass: AngularticsProviderMock