[CST-6753] track Google Analytics statistic only if user accepts cookie consents

This commit is contained in:
Giuseppe Digilio
2022-09-27 21:47:15 +02:00
parent e9a87a607a
commit f3f89b3dc1
6 changed files with 190 additions and 40 deletions

View File

@@ -10,10 +10,12 @@ import { AuthService } from '../../core/auth/auth.service';
import { CookieService } from '../../core/services/cookie.service';
import { getTestScheduler } from 'jasmine-marbles';
import { MetadataValue } from '../../core/shared/metadata.models';
import {clone, cloneDeep} from 'lodash';
import { clone, cloneDeep } from 'lodash';
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
import {createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$} from '../remote-data.utils';
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
import { ConfigurationProperty } from '../../core/shared/configuration-property.model';
import { ANONYMOUS_STORAGE_NAME_KLARO } from './klaro-configuration';
import { TestScheduler } from 'rxjs/testing';
describe('BrowserKlaroService', () => {
const trackingIdProp = 'google.analytics.key';
@@ -29,7 +31,7 @@ describe('BrowserKlaroService', () => {
let configurationDataService: ConfigurationDataService;
const createConfigSuccessSpy = (...values: string[]) => jasmine.createSpyObj('configurationDataService', {
findByPropertyName: createSuccessfulRemoteDataObject$({
... new ConfigurationProperty(),
...new ConfigurationProperty(),
name: trackingIdProp,
values: values,
}),
@@ -42,7 +44,9 @@ describe('BrowserKlaroService', () => {
let findByPropertyName;
beforeEach(() => {
user = new EPerson();
user = Object.assign(new EPerson(), {
uuid: 'test-user'
});
translateService = getMockTranslateService();
ePersonService = jasmine.createSpyObj('ePersonService', {
@@ -104,7 +108,7 @@ describe('BrowserKlaroService', () => {
services: [{
name: appName,
purposes: [purpose]
},{
}, {
name: googleAnalytics,
purposes: [purpose]
}],
@@ -219,6 +223,40 @@ describe('BrowserKlaroService', () => {
});
});
describe('getSavedPreferences', () => {
let scheduler: TestScheduler;
beforeEach(() => {
scheduler = getTestScheduler();
});
describe('when no user is autheticated', () => {
beforeEach(() => {
spyOn(service as any, 'getUser$').and.returnValue(observableOf(undefined));
});
it('should return the cookie consents object', () => {
scheduler.schedule(() => service.getSavedPreferences().subscribe());
scheduler.flush();
expect(cookieService.get).toHaveBeenCalledWith(ANONYMOUS_STORAGE_NAME_KLARO);
});
});
describe('when user is autheticated', () => {
beforeEach(() => {
spyOn(service as any, 'getUser$').and.returnValue(observableOf(user));
});
it('should return the cookie consents object', () => {
scheduler.schedule(() => service.getSavedPreferences().subscribe());
scheduler.flush();
expect(cookieService.get).toHaveBeenCalledWith('klaro-' + user.uuid);
});
});
});
describe('setSettingsForUser when there are changes', () => {
const cookieConsent = { test: 'testt' };
const cookieConsentString = '{test: \'testt\'}';
@@ -271,40 +309,40 @@ describe('BrowserKlaroService', () => {
});
it('should not filter googleAnalytics when servicesToHide are empty', () => {
const filteredConfig = (service as any).filterConfigServices([]);
expect(filteredConfig).toContain(jasmine.objectContaining({name: googleAnalytics}));
expect(filteredConfig).toContain(jasmine.objectContaining({ name: googleAnalytics }));
});
it('should filter services using names passed as servicesToHide', () => {
const filteredConfig = (service as any).filterConfigServices([googleAnalytics]);
expect(filteredConfig).not.toContain(jasmine.objectContaining({name: googleAnalytics}));
expect(filteredConfig).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));
});
it('should have been initialized with googleAnalytics', () => {
service.initialize();
expect(service.klaroConfig.services).toContain(jasmine.objectContaining({name: googleAnalytics}));
expect(service.klaroConfig.services).toContain(jasmine.objectContaining({ name: googleAnalytics }));
});
it('should filter googleAnalytics when empty configuration is retrieved', () => {
configurationDataService.findByPropertyName = jasmine.createSpy().withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue(
createSuccessfulRemoteDataObject$({
... new ConfigurationProperty(),
...new ConfigurationProperty(),
name: googleAnalytics,
values: [],
}));
service.initialize();
expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({name: googleAnalytics}));
expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));
});
it('should filter googleAnalytics when an error occurs', () => {
configurationDataService.findByPropertyName = jasmine.createSpy().withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue(
createFailedRemoteDataObject$('Erro while loading GA')
);
service.initialize();
expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({name: googleAnalytics}));
expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));
});
it('should filter googleAnalytics when an invalid payload is retrieved', () => {
configurationDataService.findByPropertyName = jasmine.createSpy().withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue(
createSuccessfulRemoteDataObject$(null)
);
service.initialize();
expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({name: googleAnalytics}));
expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));
});
});
});

View File

@@ -13,7 +13,7 @@ import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { cloneDeep, debounce } from 'lodash';
import { ANONYMOUS_STORAGE_NAME_KLARO, klaroConfiguration } from './klaro-configuration';
import { Operation } from 'fast-json-patch';
import { getFirstCompletedRemoteData} from '../../core/shared/operators';
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
/**
@@ -121,6 +121,23 @@ export class BrowserKlaroService extends KlaroService {
});
}
/**
* Return saved preferences stored in the klaro cookie
*/
getSavedPreferences(): Observable<any> {
return this.getUser$().pipe(
map((user: EPerson) => {
let storageName;
if (isEmpty(user)) {
storageName = ANONYMOUS_STORAGE_NAME_KLARO;
} else {
storageName = this.getStorageName(user.uuid);
}
return this.cookieService.get(storageName);
})
);
}
/**
* Initialize configuration for the logged in user
* @param user The authenticated user

View File

@@ -12,6 +12,8 @@ export const HAS_AGREED_END_USER = 'dsHasAgreedEndUser';
*/
export const ANONYMOUS_STORAGE_NAME_KLARO = 'klaro-anonymous';
export const GOOGLE_ANALYTICS_KLARO_KEY = 'google-analytics';
/**
* Klaro configuration
* For more information see https://kiprotect.com/docs/klaro/annotated-config
@@ -113,7 +115,7 @@ export const klaroConfiguration: any = {
]
},
{
name: 'google-analytics',
name: GOOGLE_ANALYTICS_KLARO_KEY,
purposes: ['statistical'],
required: false,
cookies: [

View File

@@ -1,5 +1,7 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
/**
* Abstract class representing a service for handling Klaro consent preferences and UI
*/
@@ -11,7 +13,12 @@ export abstract class KlaroService {
abstract initialize();
/**
* Shows a the dialog with the current consent preferences
* Shows a dialog with the current consent preferences
*/
abstract showSettings();
/**
* Return saved preferences stored in the klaro cookie
*/
abstract getSavedPreferences(): Observable<any>;
}

View File

@@ -1,8 +1,12 @@
import { GoogleAnalyticsService } from './google-analytics.service';
import { Angulartics2GoogleTagManager } from 'angulartics2';
import { of } from 'rxjs';
import { GoogleAnalyticsService } from './google-analytics.service';
import { ConfigurationDataService } from '../core/data/configuration-data.service';
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
import { ConfigurationProperty } from '../core/shared/configuration-property.model';
import { KlaroService } from '../shared/cookies/klaro.service';
import { GOOGLE_ANALYTICS_KLARO_KEY } from '../shared/cookies/klaro-configuration';
describe('GoogleAnalyticsService', () => {
const trackingIdProp = 'google.analytics.key';
@@ -12,6 +16,7 @@ describe('GoogleAnalyticsService', () => {
let service: GoogleAnalyticsService;
let angularticsSpy: Angulartics2GoogleTagManager;
let configSpy: ConfigurationDataService;
let klaroServiceSpy: jasmine.SpyObj<KlaroService>;
let scriptElementMock: any;
let srcSpy: any;
let innerHTMLSpy: any;
@@ -31,6 +36,10 @@ describe('GoogleAnalyticsService', () => {
'startTracking',
]);
klaroServiceSpy = jasmine.createSpyObj('KlaroService', {
'getSavedPreferences': jasmine.createSpy('getSavedPreferences')
});
configSpy = createConfigSuccessSpy(trackingIdTestValue);
scriptElementMock = {
@@ -53,7 +62,11 @@ describe('GoogleAnalyticsService', () => {
body: bodyElementSpy,
});
service = new GoogleAnalyticsService(angularticsSpy, configSpy, documentSpy);
klaroServiceSpy.getSavedPreferences.and.returnValue(of({
GOOGLE_ANALYTICS_KLARO_KEY: true
}));
service = new GoogleAnalyticsService(angularticsSpy, klaroServiceSpy, configSpy, documentSpy );
});
it('should be created', () => {
@@ -73,7 +86,11 @@ describe('GoogleAnalyticsService', () => {
findByPropertyName: createFailedRemoteDataObject$(),
});
service = new GoogleAnalyticsService(angularticsSpy, configSpy, documentSpy);
klaroServiceSpy.getSavedPreferences.and.returnValue(of({
GOOGLE_ANALYTICS_KLARO_KEY: true
}));
service = new GoogleAnalyticsService(angularticsSpy, klaroServiceSpy, configSpy, documentSpy);
});
it('should NOT add a script to the body', () => {
@@ -91,7 +108,10 @@ describe('GoogleAnalyticsService', () => {
describe('when the tracking id is empty', () => {
beforeEach(() => {
configSpy = createConfigSuccessSpy();
service = new GoogleAnalyticsService(angularticsSpy, configSpy, documentSpy);
klaroServiceSpy.getSavedPreferences.and.returnValue(of({
[GOOGLE_ANALYTICS_KLARO_KEY]: true
}));
service = new GoogleAnalyticsService(angularticsSpy, klaroServiceSpy, configSpy, documentSpy);
});
it('should NOT add a script to the body', () => {
@@ -105,7 +125,55 @@ describe('GoogleAnalyticsService', () => {
});
});
describe('when the tracking id is non-empty', () => {
describe('when google-analytics cookie preferences are not existing', () => {
beforeEach(() => {
configSpy = createConfigSuccessSpy(trackingIdTestValue);
klaroServiceSpy.getSavedPreferences.and.returnValue(of({}));
service = new GoogleAnalyticsService(angularticsSpy, klaroServiceSpy, configSpy, documentSpy);
});
it('should NOT add a script to the body', () => {
service.addTrackingIdToPage();
expect(bodyElementSpy.appendChild).toHaveBeenCalledTimes(0);
});
it('should NOT start tracking', () => {
service.addTrackingIdToPage();
expect(angularticsSpy.startTracking).toHaveBeenCalledTimes(0);
});
});
describe('when google-analytics cookie preferences are set to false', () => {
beforeEach(() => {
configSpy = createConfigSuccessSpy(trackingIdTestValue);
klaroServiceSpy.getSavedPreferences.and.returnValue(of({
[GOOGLE_ANALYTICS_KLARO_KEY]: false
}));
service = new GoogleAnalyticsService(angularticsSpy, klaroServiceSpy, configSpy, documentSpy);
});
it('should NOT add a script to the body', () => {
service.addTrackingIdToPage();
expect(bodyElementSpy.appendChild).toHaveBeenCalledTimes(0);
});
it('should NOT start tracking', () => {
service.addTrackingIdToPage();
expect(angularticsSpy.startTracking).toHaveBeenCalledTimes(0);
});
});
describe('when both google-analytics cookie and the tracking id are non-empty', () => {
beforeEach(() => {
configSpy = createConfigSuccessSpy(trackingIdTestValue);
klaroServiceSpy.getSavedPreferences.and.returnValue(of({
[GOOGLE_ANALYTICS_KLARO_KEY]: true
}));
service = new GoogleAnalyticsService(angularticsSpy, klaroServiceSpy, configSpy, documentSpy);
});
it('should create a script tag whose innerHTML contains the tracking id', () => {
service.addTrackingIdToPage();
expect(documentSpy.createElement).toHaveBeenCalledTimes(2);

View File

@@ -1,9 +1,14 @@
import { DOCUMENT } from '@angular/common';
import { Inject, Injectable } from '@angular/core';
import { Angulartics2GoogleTagManager } from 'angulartics2';
import { combineLatest } from 'rxjs';
import { ConfigurationDataService } from '../core/data/configuration-data.service';
import { getFirstCompletedRemoteData } from '../core/shared/operators';
import { isEmpty } from '../shared/empty.util';
import { DOCUMENT } from '@angular/common';
import { KlaroService } from '../shared/cookies/klaro.service';
import { GOOGLE_ANALYTICS_KLARO_KEY } from '../shared/cookies/klaro-configuration';
/**
* Set up Google Analytics on the client side.
@@ -15,9 +20,11 @@ export class GoogleAnalyticsService {
constructor(
// private angulartics: Angulartics2GoogleAnalytics,
private angulartics: Angulartics2GoogleTagManager,
private klaroService: KlaroService,
private configService: ConfigurationDataService,
@Inject(DOCUMENT) private document: any,
) { }
) {
}
/**
* Call this method once when Angular initializes on the client side.
@@ -26,29 +33,40 @@ export class GoogleAnalyticsService {
* page and starts tracking.
*/
addTrackingIdToPage(): void {
this.configService.findByPropertyName('google.analytics.key').pipe(
const googleKey$ = this.configService.findByPropertyName('google.analytics.key').pipe(
getFirstCompletedRemoteData(),
).subscribe((remoteData) => {
// make sure we got a success response from the backend
if (!remoteData.hasSucceeded) { return; }
);
combineLatest([this.klaroService.getSavedPreferences(), 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]) {
return;
}
const trackingId = remoteData.payload.values[0];
// make sure we got a success response from the backend
if (!remoteData.hasSucceeded) {
return;
}
// make sure we received a tracking id
if (isEmpty(trackingId)) { return; }
const trackingId = remoteData.payload.values[0];
// 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);
// make sure we received a tracking id
if (isEmpty(trackingId)) {
return;
}
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);
// 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);
// start tracking
this.angulartics.startTracking();
});
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);
// start tracking
this.angulartics.startTracking();
});
}
}