From 504acd79f80827c6105224cf972ea4423d8677e8 Mon Sep 17 00:00:00 2001 From: "Stefan@AWG" Date: Tue, 13 Oct 2020 15:55:01 +0200 Subject: [PATCH 01/73] fix(core): improve regex for replacing salsah links --- .../services/conversion-service/conversion.service.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/app/core/services/conversion-service/conversion.service.ts b/src/app/core/services/conversion-service/conversion.service.ts index 0a71806a59..4fff850d13 100644 --- a/src/app/core/services/conversion-service/conversion.service.ts +++ b/src/app/core/services/conversion-service/conversion.service.ts @@ -863,8 +863,11 @@ export class ConversionService extends ApiService { if (!str) { return; } - const regNum = /\d{4,8}/; // regexp for object id (4-8 DIGITS) - const regLink = /(.*?)<\/a>/i; // regexp for salsah links + const regNum = /\d{3,}/; // regexp for object id (3 or more DIGITS) + const regLink = new RegExp( + '(.*?)', + 'i' + ); // regexp for salsah links let regArr: RegExpExecArray; // check for salsah links in str @@ -876,12 +879,12 @@ export class ConversionService extends ApiService { const resId = regNum.exec(regArr[1])[0]; // replace href attribute with click-directive - // linktext is stored in second regexp-result regArr[2] + // linktext is stored in last regexp-result regArr[regArr.length-1] const replaceValue = '' + - regArr[2] + + regArr[regArr.length - 1] + ''; str = str.replace(regArr[0], replaceValue); } // END while From b12e2a9f111d4da913b6dd6dc2ccf74a4c06c404 Mon Sep 17 00:00:00 2001 From: "Stefan@AWG" Date: Sat, 24 Oct 2020 18:08:07 +0200 Subject: [PATCH 02/73] fix(core): switch from ga to gtag for analytics --- src/app/app.component.ts | 2 + src/app/app.config.ts | 2 +- .../analytics-sercvice/analytics.service.ts | 188 ++---------------- src/environments/environment.prod.ts | 3 +- src/environments/environment.ts | 3 +- src/index.html | 7 + 6 files changed, 36 insertions(+), 169 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 17a0376723..a5582d5792 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -28,6 +28,8 @@ export class AppComponent { private analyticsService: AnalyticsService, private routerEventsService: RouterEventsService ) { + this.analyticsService.initAnalytics(); + this.router.events.subscribe(event => { if (event instanceof NavigationEnd) { this.analyticsService.trackPageView(event.urlAfterRedirects); diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 92335b7b33..eb023cbac0 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -23,7 +23,7 @@ export class AppConfig { * @returns {string} */ public static get ANALYTICS_ENDPOINT(): string { - return 'https://www.google-analytics.com/'; + return 'https://www.googletagmanager.com/gtag/js'; } /** diff --git a/src/app/core/services/analytics-sercvice/analytics.service.ts b/src/app/core/services/analytics-sercvice/analytics.service.ts index 6aa5bc3ebe..af6b304343 100644 --- a/src/app/core/services/analytics-sercvice/analytics.service.ts +++ b/src/app/core/services/analytics-sercvice/analytics.service.ts @@ -1,34 +1,17 @@ import { Injectable } from '@angular/core'; import { AppConfig } from '@awg-app/app.config'; +import { environment } from '../../../../environments/environment'; /** - * The AnalyticsConfig class. - * - * It is used to configure the Analytics setup. - * + * gtag function for Analytics. */ -export class AnalyticsConfig { - /** - * The tracking id for Analytics. - */ - trackingId: string; - - /** - * The cookieDomain for Analytics. - */ - cookieDomain?: string; - - /** - * The debug flag for Analytics. - */ - debug?: boolean; -} +declare let gtag: Function; /** * The Analytics service. * * It handles the configuration for the GoogleAnalytics setup. - * Inspired by https://jaxenter.de/google-analytics-angular-57919 + * Inspired by https://medium.com/madhash/how-to-properly-add-google-analytics-tracking-to-your-angular-web-app-bc7750713c9e * * Provided in: `root`. */ @@ -37,170 +20,43 @@ export class AnalyticsConfig { }) export class AnalyticsService { /** - * Private variable: analyticsConfig. + * Constructor of the AnalyticsService. * - * It stores the analytics object. + * It calls the initialization method. */ - private analyticsConfig: AnalyticsConfig = { trackingId: AppConfig.ANALYTICS_ID }; + constructor() {} /** - * Private variable: isInitialized. + * Public method: initAnalytics. * - * It stores a boolean flag for successful initialization. - */ - private isInitialized = false; - - /** - * Constructor of the AnalyticsService. + * It inits the Analytics script. * - * It calls the initialization method. + * @returns {void} Inits Analytics. */ - constructor() { - this.initializeAnalytics(this.analyticsConfig); + initAnalytics() { + const gtagScript: HTMLScriptElement = document.createElement('script'); + gtagScript.async = true; + gtagScript.src = AppConfig.ANALYTICS_ENDPOINT + '?id=' + AppConfig.ANALYTICS_ID; + document.head.prepend(gtagScript); } /** * Public method: trackPageView. * - * It tracks a page view for Analytics - * if initialization was successful. + * It tracks a page view for Analytics. * * @params {string} page The given page string. * * @returns {void} Sets and sends the page view to GA. */ trackPageView(page: string): void { - // check if Analytics object was initialized - if (!this.isInitialized) { - console.log('Analytics not initialized'); - return; - } - - // set the page to be tracked - this.runAnalytics('set', 'page', page); - - // Send a pageview hit from that page - this.runAnalytics('send', 'pageview'); - } - - /** - * Private method: initializeAnalytics. - * - * It initializes the Analytics environment by setting - * the global analytics object and - * a boolean flag for successful initialization. - * - * @param {AnalyticsConfig} config The given config object. - * - * @returns {void} Sets analytics object and init flag. - */ - private initializeAnalytics(config: AnalyticsConfig): void { - if (!config || !config.trackingId) { - this.isInitialized = false; - console.log('No analytics config found'); - return; - } - - this.createAnalytics(config); - - // enable debug mode if needed - if (config.debug) { - (window as any).ga_debug = { trace: true }; - } - - // create tracker - if (config.cookieDomain) { - // create a tracker with custom cookie domain configuration - this.runAnalytics('create', config.trackingId, { - cookieDomain: config.cookieDomain - }); - } else { - // create a default tracker with automatic cookie domain configuration - this.runAnalytics('create', config.trackingId, 'auto'); - } - - // ignore non-production page calls - // cf. https://developers.google.com/analytics/devguides/collection/analyticsjs/debugging#testing_your_implementation_without_sending_hits - /* istanbul ignore else */ - if (!(document.location.hostname === 'edition.anton-webern.ch')) { + if (environment.GA_SEND_PAGE_VIEW === false) { console.log('Running non-production analytics replacement now'); - this.runAnalytics('set', 'sendHitTask', null); - } - - // flag for successful initialization - this.isInitialized = true; - } - - /** - * Private method: createAnalytics. - * - * It creates a global Analytics object and loads the necessary JS file. - * - * @param {AnalyticsConfig} config The given config object. - * - * @returns {void} Creates the global analytics object. - */ - private createAnalytics(config: AnalyticsConfig): void { - // set debug or default version of analytics.js - const analyticsJS = config.debug ? 'analytics_debug.js' : 'analytics.js'; - const analyticsURL = AppConfig.ANALYTICS_ENDPOINT + analyticsJS; - - /** - * Creates a temporary global ga object and loads analytics.js. - * Parameters o, a, and m are all used internally. They could have been - * declared using 'var', instead they are declared as parameters to save - * 4 bytes ('var '). - * - * @param {Window} i The global context object. - * @param {HTMLDocument} s The DOM document object. - * @param {string} o Must be 'script'. - * @param {string} g Protocol relative URL of the analytics.js script. - * @param {string} r Global name of analytics object. Defaults to 'ga'. - * @param {HTMLElement} a Async script tag. - * @param {HTMLElement} m First script tag in document. - */ - /* istanbul ignore next */ - ((i, s, o, g, r, a, m) => { - // Acts as a pointer to support renaming. - i['GoogleAnalyticsObject'] = r; - - // Creates an initial ga() function. - // The queued commands will be executed once analytics.js loads. - (i[r] = - i[r] || - (() => { - (i[r].q = i[r].q || []).push(arguments); - })), - // Sets the time (as an integer) this tag was executed. - // Used for timing hits. - (i[r].l = 1 * (new Date() as any)); - - // Insert the script tag asynchronously. - // Inserts above current tag to prevent blocking in addition to using the - // async attribute. - (a = s.createElement(o)), (m = s.getElementsByTagName(o)[0]); - a.async = 1; - a.src = g; - m.parentNode.insertBefore(a, m); - })(window, document, 'script', analyticsURL, 'ga'); - } - - /** - * Private method: runAnalytics. - * - * It runs a given task on the global Analytics object. - * - * @param {string} task The given task method. - * @param {string} field The given field. - * @param {string} [option] The given option. - * - * @returns {void} Runs a task on the global analytics object. - */ - private runAnalytics(task: string, field: string, option?: any): void { - if (option || option === null) { - (window as any).ga(task, field, option); - } else { - (window as any).ga(task, field); } + gtag('config', AppConfig.ANALYTICS_ID, { + page_path: page, + anonymize_ip: true, + send_page_view: environment.GA_SEND_PAGE_VIEW + }); } } diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index f669467271..42515f73f6 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -6,5 +6,6 @@ * The list of file replacements can be found in `angular.json`. */ export const environment = { - production: true + production: true, + GA_SEND_PAGE_VIEW: true }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 9d7aed7492..4dd0471ba8 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -8,7 +8,8 @@ * The list of file replacements can be found in `angular.json`. */ export const environment = { - production: false + production: false, + GA_SEND_PAGE_VIEW: false }; /* diff --git a/src/index.html b/src/index.html index 41cc64bbbc..86b9ec3e39 100644 --- a/src/index.html +++ b/src/index.html @@ -14,6 +14,13 @@ + + + From dac95a52dd40c05b758385d9ca0d12f492cc81b4 Mon Sep 17 00:00:00 2001 From: "Stefan@AWG" Date: Mon, 26 Oct 2020 16:10:00 +0100 Subject: [PATCH 03/73] test(core): add tests for analytics service --- src/app/app.component.ts | 2 +- .../analytics.service.spec.ts | 283 +++++++++++++----- .../analytics-sercvice/analytics.service.ts | 76 ++++- src/testing/expect-helper.ts | 20 +- 4 files changed, 287 insertions(+), 94 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index a5582d5792..5c63858c4a 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -28,7 +28,7 @@ export class AppComponent { private analyticsService: AnalyticsService, private routerEventsService: RouterEventsService ) { - this.analyticsService.initAnalytics(); + this.analyticsService.initializeAnalytics(); this.router.events.subscribe(event => { if (event instanceof NavigationEnd) { diff --git a/src/app/core/services/analytics-sercvice/analytics.service.spec.ts b/src/app/core/services/analytics-sercvice/analytics.service.spec.ts index 8a75b36e7a..0a4a622724 100644 --- a/src/app/core/services/analytics-sercvice/analytics.service.spec.ts +++ b/src/app/core/services/analytics-sercvice/analytics.service.spec.ts @@ -5,19 +5,31 @@ import Spy = jasmine.Spy; import { cleanStylesFromDOM } from '@testing/clean-up-helper'; import { expectSpyCall } from '@testing/expect-helper'; -import { AnalyticsConfig, AnalyticsService } from './analytics.service'; +import { AnalyticsService } from './analytics.service'; describe('AnalyticsService', () => { let analyticsService: AnalyticsService; - let gaSpy: Spy; + let gtagSpy: Spy; let initializeAnalyticsSpy: Spy; + let consoleSpy: Spy; - const expectedAnalyticsConfig = { trackingId: 'UA-XXXXX-Y' }; + const expectedAnalyticsEndpoint = 'https://example.com/endpoint/'; + const expectedAnalyticsId = 'UA-XXXXX-Y'; + const expectecdSendPageView = false; const expectedPage = '/test'; const otherPage = '/test2'; + const expectedLogMessage = 'Running non-production analytics replacement now'; + + let mockConsole: { log: (message: string) => void; get: (index: number) => string; clear: () => void }; + let mockAnalytics: { + gtag: (event: string, eventName: string, eventOptions: { [key: string]: string | boolean }) => void; + getGtag: (index: number) => [string, string, { [key: string]: string | boolean }]; + clear: () => void; + }; + beforeEach(() => { TestBed.configureTestingModule({ providers: [AnalyticsService] @@ -25,11 +37,57 @@ describe('AnalyticsService', () => { // inject service analyticsService = TestBed.inject(AnalyticsService); - // set analyticsConfig variable - (analyticsService as any).analyticsConfig = expectedAnalyticsConfig; + // mock analytics object (to catch analytics events) + let analyticsStore = []; + mockAnalytics = { + gtag: ( + event: string, + eventName: string, + eventOptions: { page_path: string; anonymize_ip: boolean; send_page_view: boolean } + ): void => { + analyticsStore.push([event, eventName, eventOptions]); + }, + getGtag: ( + index: number + ): [string, string, { page_path: string; anonymize_ip: boolean; send_page_view: boolean }] => { + return analyticsStore[index] || null; + }, + clear: () => { + analyticsStore = []; + } + }; + + // mock Console (to catch console output) + let consoleArray = []; + mockConsole = { + log: (message: string) => { + consoleArray.push(message); + }, + get: (index: number): string => { + return consoleArray[index]; + }, + + clear: () => { + consoleArray = []; + } + }; + + // set global gtag function + (window as any).gtag = () => {}; + + // spy on service methods + initializeAnalyticsSpy = spyOn(analyticsService, 'initializeAnalytics').and.callThrough(); + gtagSpy = spyOn(window as any, 'gtag').and.callFake(mockAnalytics.gtag); + consoleSpy = spyOn(console, 'log').and.callFake(mockConsole.log); + }); + + afterEach(() => { + // clear mock stores after each test + mockAnalytics.clear(); + mockConsole.clear(); - // spy on global ga object - gaSpy = spyOn(window as any, 'ga').and.callThrough(); + // remove global function + (window as any).gtag = undefined; }); afterAll(() => { @@ -39,115 +97,196 @@ describe('AnalyticsService', () => { it('... should be created', () => { expect(analyticsService).toBeTruthy(); }); - describe('#initializeAnalytics', () => { - beforeEach(() => { - // spy on private service methods - initializeAnalyticsSpy = spyOn(analyticsService, 'initializeAnalytics').and.callThrough(); + + describe('... mock test objects (self-test)', () => { + it('... should use mock console', () => { + console.log('Test'); + + expect(mockConsole.get(0)).toBe('Test'); + }); + + it('... should clear mock console after each run', () => { + expect(mockConsole.get(0)).toBeUndefined(`should be undefined`); }); - it(`... should not init the analytics tracker without config`, () => { - // no config provided - const config: AnalyticsConfig = null; + it('... should use mock analytics', () => { + (window as any).gtag('test', 'analytics', {}); - (analyticsService as any).initializeAnalytics(config); + expect(mockAnalytics.getGtag(0)).toEqual(['test', 'analytics', {}], `should be '[test', 'analytics', {}]`); + }); - expectSpyCall(initializeAnalyticsSpy, 1, config); + it('... should clear mock analytics store after each run', () => { + expect(mockAnalytics.getGtag(0)).toBeNull(`should be null`); + }); + }); + + describe('#initializeAnalytics', () => { + it(`... should not initialize the analytics tracker without endpoint`, () => { + // no endpoint provided + (analyticsService as any).analyticsEndpoint = null; + (analyticsService as any).analyticsId = expectedAnalyticsId; + + analyticsService.initializeAnalytics(); + + expectSpyCall(initializeAnalyticsSpy, 1); expect((analyticsService as any).isInitialized).toBeFalse(); }); - it(`... should not init the analytics tracker without config.trackingId`, () => { - // no tracking id provided - const config = { trackingId: null }; + it(`... should not initialize the analytics tracker without analyticsId`, () => { + // no id provided + (analyticsService as any).analyticsEndpoint = expectedAnalyticsEndpoint; + (analyticsService as any).analyticsId = null; - (analyticsService as any).initializeAnalytics(config); + analyticsService.initializeAnalytics(); - expectSpyCall(initializeAnalyticsSpy, 1, config); + expectSpyCall(initializeAnalyticsSpy, 1); expect((analyticsService as any).isInitialized).toBeFalse(); }); - it(`... should successfully init the analytics tracker with given config.trackingId`, () => { - const config: AnalyticsConfig = expectedAnalyticsConfig; + it(`... should successfully initialize the analytics tracker with given endpoint and id`, () => { + (analyticsService as any).analyticsEndpoint = expectedAnalyticsEndpoint; + (analyticsService as any).analyticsId = expectedAnalyticsId; - (analyticsService as any).initializeAnalytics(expectedAnalyticsConfig); + analyticsService.initializeAnalytics(); - expectSpyCall(initializeAnalyticsSpy, 1, config); + expectSpyCall(initializeAnalyticsSpy, 1); expect(analyticsService['isInitialized']).toBeTruthy(); }); + }); - it(`... should init the tracker in debug mode if config.debug = true`, () => { - const config: AnalyticsConfig = expectedAnalyticsConfig; - config.debug = true; + describe('#trackPageView', () => { + it(`... should do nothing if analytics is not initialized successfully`, () => { + // init analytics + (analyticsService as any).analyticsEndpoint = null; + (analyticsService as any).analyticsId = expectedAnalyticsId; + analyticsService.initializeAnalytics(); - (analyticsService as any).initializeAnalytics(config); + analyticsService.trackPageView(expectedPage); - expectSpyCall(initializeAnalyticsSpy, 1, config); - expectSpyCall(gaSpy, 2, ['set', 'sendHitTask', null]); + expectSpyCall(gtagSpy, 0, null); + expect(gtagSpy.calls.any()).toBeFalse(); + }); - expect(gaSpy.calls.any()).toBeTruthy(); - expect(gaSpy.calls.count()).toBe(2); - expect(gaSpy.calls.first().args).toEqual(['create', config.trackingId, 'auto']); - expect(gaSpy.calls.mostRecent().args).toEqual(['set', 'sendHitTask', null]); + it(`... should do nothing if isInitialized is set to false`, () => { + (analyticsService as any).isInitialized = false; + + analyticsService.trackPageView(expectedPage); - expect((analyticsService as any).isInitialized).toBeTruthy(); + expectSpyCall(gtagSpy, 0, null); + expect(gtagSpy.calls.any()).toBeFalse(); }); - it(`... should use a custom domain if config.cookieDomain is set`, () => { - const config: AnalyticsConfig = expectedAnalyticsConfig; - config.cookieDomain = 'none'; + it(`... should run if analytics is initialized successfully`, () => { + // init analytics + (analyticsService as any).analyticsEndpoint = expectedAnalyticsEndpoint; + (analyticsService as any).analyticsId = expectedAnalyticsId; + analyticsService.initializeAnalytics(); - (analyticsService as any).initializeAnalytics(config); + analyticsService.trackPageView(expectedPage); + + expectSpyCall(gtagSpy, 1); + }); - expectSpyCall(initializeAnalyticsSpy, 1, config); - expectSpyCall(gaSpy, 2, ['set', 'sendHitTask', null]); + it(`... should run if isInitialized is set to true`, () => { + (analyticsService as any).isInitialized = true; - expect(gaSpy.calls.any()).toBeTruthy(); - expect(gaSpy.calls.count()).toBe(2); - expect(gaSpy.calls.first().args).toEqual([ - 'create', - config.trackingId, - { cookieDomain: config.cookieDomain } - ]); - expect(gaSpy.calls.mostRecent().args).toEqual(['set', 'sendHitTask', null]); + analyticsService.trackPageView(expectedPage); - expect((analyticsService as any).isInitialized).toBeTruthy(); + expectSpyCall(gtagSpy, 1); + }); + + it(`... should not track if no page is given`, () => { + // init analytics + (analyticsService as any).analyticsEndpoint = expectedAnalyticsEndpoint; + (analyticsService as any).analyticsId = expectedAnalyticsId; + analyticsService.initializeAnalytics(); + + analyticsService.trackPageView(null); + + expectSpyCall(gtagSpy, 0, null); + expect(gtagSpy.calls.any()).toBeFalse(); }); - }); - describe('#trackPageView', () => { it(`... should track the given page`, () => { - analyticsService.trackPageView(expectedPage); + const expectedAnalyticsEvent = [ + 'config', + expectedAnalyticsId, + { page_path: expectedPage, anonymize_ip: true, send_page_view: expectecdSendPageView } + ]; - expectSpyCall(gaSpy, 2, ['send', 'pageview']); + // init analytics + (analyticsService as any).analyticsEndpoint = expectedAnalyticsEndpoint; + (analyticsService as any).analyticsId = expectedAnalyticsId; + analyticsService.initializeAnalytics(); - expect(gaSpy.calls.any()).toBeTruthy(); - expect(gaSpy.calls.count()).toBe(2); - expect(gaSpy.calls.first().args).toEqual(['set', 'page', expectedPage]); - expect(gaSpy.calls.mostRecent().args).toEqual(['send', 'pageview']); + analyticsService.trackPageView(expectedPage); + + expectSpyCall(gtagSpy, 1, expectedAnalyticsEvent); + expect(mockAnalytics.getGtag(0)).toEqual(expectedAnalyticsEvent, `should be ${expectedAnalyticsEvent}`); }); it(`... should track page changes`, () => { + const expectedAnalyticsEvent = [ + 'config', + expectedAnalyticsId, + { page_path: expectedPage, anonymize_ip: true, send_page_view: expectecdSendPageView } + ]; + const otherAnalyticsEvent = [ + 'config', + expectedAnalyticsId, + { page_path: otherPage, anonymize_ip: true, send_page_view: expectecdSendPageView } + ]; + + // init analytics + (analyticsService as any).analyticsEndpoint = expectedAnalyticsEndpoint; + (analyticsService as any).analyticsId = expectedAnalyticsId; + analyticsService.initializeAnalytics(); + analyticsService.trackPageView(expectedPage); analyticsService.trackPageView(otherPage); - expectSpyCall(gaSpy, 4, ['send', 'pageview']); + expectSpyCall(gtagSpy, 2, otherAnalyticsEvent); + expect(gtagSpy.calls.any()).toBeTruthy(); + expect(gtagSpy.calls.count()).toBe(2); + expect(gtagSpy.calls.first().args).toEqual(expectedAnalyticsEvent); + expect(gtagSpy.calls.allArgs()[0]).toEqual(expectedAnalyticsEvent); + expect(gtagSpy.calls.allArgs()[1]).toEqual(otherAnalyticsEvent); + expect(gtagSpy.calls.mostRecent().args).toEqual(otherAnalyticsEvent); - expect(gaSpy.calls.any()).toBeTruthy(); - expect(gaSpy.calls.count()).toBe(4); - expect(gaSpy.calls.first().args).toEqual(['set', 'page', expectedPage]); - expect(gaSpy.calls.allArgs()[0]).toEqual(['set', 'page', expectedPage]); - expect(gaSpy.calls.allArgs()[2]).toEqual(['set', 'page', otherPage]); - expect(gaSpy.calls.allArgs()[1]).toEqual(['send', 'pageview']); - expect(gaSpy.calls.allArgs()[3]).toEqual(['send', 'pageview']); - expect(gaSpy.calls.mostRecent().args).toEqual(['send', 'pageview']); + expect(mockAnalytics.getGtag(0)).toEqual(expectedAnalyticsEvent, `should be ${expectedAnalyticsEvent}`); + expect(mockAnalytics.getGtag(1)).toEqual(otherAnalyticsEvent, `should be ${otherAnalyticsEvent}`); }); - it(`... should do nothing if analytics tracker is not initialized successfully`, () => { - (analyticsService as any).isInitialized = false; + it(`... should log a replacement message in develop mode`, () => { + expectSpyCall(consoleSpy, 0); + expect(mockConsole.get(0)).toBeUndefined(`should be undefined`); + + // init analytics + (analyticsService as any).analyticsEndpoint = expectedAnalyticsEndpoint; + (analyticsService as any).analyticsId = expectedAnalyticsId; + (analyticsService as any).sendPageView = false; + analyticsService.initializeAnalytics(); + + analyticsService.trackPageView(expectedPage); + + expectSpyCall(consoleSpy, 1, expectedLogMessage); + expect(mockConsole.get(0)).toBe(expectedLogMessage, `should be ${expectedLogMessage}`); + }); + + it(`... should not log a replacement message in production mode`, () => { + expectSpyCall(consoleSpy, 0); + expect(mockConsole.get(0)).toBeUndefined(`should be undefined`); + + // init analytics + (analyticsService as any).analyticsEndpoint = expectedAnalyticsEndpoint; + (analyticsService as any).analyticsId = expectedAnalyticsId; + (analyticsService as any).sendPageView = true; + analyticsService.initializeAnalytics(); analyticsService.trackPageView(expectedPage); - expectSpyCall(gaSpy, 0, null); - expect(gaSpy.calls.any()).toBeFalse(); + expectSpyCall(consoleSpy, 0); + expect(mockConsole.get(0)).toBeUndefined(`should be undefined`); }); }); }); diff --git a/src/app/core/services/analytics-sercvice/analytics.service.ts b/src/app/core/services/analytics-sercvice/analytics.service.ts index af6b304343..305409b8b4 100644 --- a/src/app/core/services/analytics-sercvice/analytics.service.ts +++ b/src/app/core/services/analytics-sercvice/analytics.service.ts @@ -11,7 +11,7 @@ declare let gtag: Function; * The Analytics service. * * It handles the configuration for the GoogleAnalytics setup. - * Inspired by https://medium.com/madhash/how-to-properly-add-google-analytics-tracking-to-your-angular-web-app-bc7750713c9e + * Inspired by https://www.ngdevelop.tech/integrate-google-analytics-with-angular-angular-seo/ * * Provided in: `root`. */ @@ -19,6 +19,38 @@ declare let gtag: Function; providedIn: 'root' }) export class AnalyticsService { + /** + * Private variable: analyticsId. + * + * It stores the analytics id. + * + */ + private analyticsId: string = AppConfig.ANALYTICS_ID; + + /** + * Private variable: analyticsEndpoint. + * + * It stores the analytics endpoint. + */ + private analyticsEndpoint: string = AppConfig.ANALYTICS_ENDPOINT; + + /** + * Private variable: isInitialized. + * + * It stores a boolean flag for successful initialization. + */ + private isInitialized = false; + + /** + * Private variable: sendPageView. + * + * It stores a boolean flag to send page views dependent from environment. + * + * DEVELOP: FALSE + * PRODUCTION: TRUE + */ + private sendPageView = environment.GA_SEND_PAGE_VIEW; + /** * Constructor of the AnalyticsService. * @@ -27,17 +59,18 @@ export class AnalyticsService { constructor() {} /** - * Public method: initAnalytics. + * Public method: initializeAnalytics. * - * It inits the Analytics script. + * It initializes the Analytics script. * * @returns {void} Inits Analytics. */ - initAnalytics() { - const gtagScript: HTMLScriptElement = document.createElement('script'); - gtagScript.async = true; - gtagScript.src = AppConfig.ANALYTICS_ENDPOINT + '?id=' + AppConfig.ANALYTICS_ID; - document.head.prepend(gtagScript); + initializeAnalytics(): void { + if (!this.analyticsEndpoint || !this.analyticsId) { + return; + } + this.prependAnalyticsScript(); + this.isInitialized = true; } /** @@ -47,16 +80,35 @@ export class AnalyticsService { * * @params {string} page The given page string. * - * @returns {void} Sets and sends the page view to GA. + * @returns {void} Configures and sends the page view to Analytics. */ trackPageView(page: string): void { - if (environment.GA_SEND_PAGE_VIEW === false) { + if (!page || this.isInitialized !== true) { + return; + } + + if (this.sendPageView === false) { console.log('Running non-production analytics replacement now'); } - gtag('config', AppConfig.ANALYTICS_ID, { + + gtag('config', this.analyticsId, { page_path: page, anonymize_ip: true, - send_page_view: environment.GA_SEND_PAGE_VIEW + send_page_view: this.sendPageView }); } + + /** + * Private method: prependAnalyticsScript. + * + * It prepends the Analytics