[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 { CookieService } from '../../core/services/cookie.service';
import { getTestScheduler } from 'jasmine-marbles'; import { getTestScheduler } from 'jasmine-marbles';
import { MetadataValue } from '../../core/shared/metadata.models'; 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 { 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 { ConfigurationProperty } from '../../core/shared/configuration-property.model';
import { ANONYMOUS_STORAGE_NAME_KLARO } from './klaro-configuration';
import { TestScheduler } from 'rxjs/testing';
describe('BrowserKlaroService', () => { describe('BrowserKlaroService', () => {
const trackingIdProp = 'google.analytics.key'; const trackingIdProp = 'google.analytics.key';
@@ -29,7 +31,7 @@ describe('BrowserKlaroService', () => {
let configurationDataService: ConfigurationDataService; let configurationDataService: ConfigurationDataService;
const createConfigSuccessSpy = (...values: string[]) => jasmine.createSpyObj('configurationDataService', { const createConfigSuccessSpy = (...values: string[]) => jasmine.createSpyObj('configurationDataService', {
findByPropertyName: createSuccessfulRemoteDataObject$({ findByPropertyName: createSuccessfulRemoteDataObject$({
... new ConfigurationProperty(), ...new ConfigurationProperty(),
name: trackingIdProp, name: trackingIdProp,
values: values, values: values,
}), }),
@@ -42,7 +44,9 @@ describe('BrowserKlaroService', () => {
let findByPropertyName; let findByPropertyName;
beforeEach(() => { beforeEach(() => {
user = new EPerson(); user = Object.assign(new EPerson(), {
uuid: 'test-user'
});
translateService = getMockTranslateService(); translateService = getMockTranslateService();
ePersonService = jasmine.createSpyObj('ePersonService', { ePersonService = jasmine.createSpyObj('ePersonService', {
@@ -104,7 +108,7 @@ describe('BrowserKlaroService', () => {
services: [{ services: [{
name: appName, name: appName,
purposes: [purpose] purposes: [purpose]
},{ }, {
name: googleAnalytics, name: googleAnalytics,
purposes: [purpose] 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', () => { describe('setSettingsForUser when there are changes', () => {
const cookieConsent = { test: 'testt' }; const cookieConsent = { test: 'testt' };
const cookieConsentString = '{test: \'testt\'}'; const cookieConsentString = '{test: \'testt\'}';
@@ -271,40 +309,40 @@ describe('BrowserKlaroService', () => {
}); });
it('should not filter googleAnalytics when servicesToHide are empty', () => { it('should not filter googleAnalytics when servicesToHide are empty', () => {
const filteredConfig = (service as any).filterConfigServices([]); 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', () => { it('should filter services using names passed as servicesToHide', () => {
const filteredConfig = (service as any).filterConfigServices([googleAnalytics]); 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', () => { it('should have been initialized with googleAnalytics', () => {
service.initialize(); 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', () => { it('should filter googleAnalytics when empty configuration is retrieved', () => {
configurationDataService.findByPropertyName = jasmine.createSpy().withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue( configurationDataService.findByPropertyName = jasmine.createSpy().withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue(
createSuccessfulRemoteDataObject$({ createSuccessfulRemoteDataObject$({
... new ConfigurationProperty(), ...new ConfigurationProperty(),
name: googleAnalytics, name: googleAnalytics,
values: [], values: [],
})); }));
service.initialize(); 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', () => { it('should filter googleAnalytics when an error occurs', () => {
configurationDataService.findByPropertyName = jasmine.createSpy().withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue( configurationDataService.findByPropertyName = jasmine.createSpy().withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue(
createFailedRemoteDataObject$('Erro while loading GA') createFailedRemoteDataObject$('Erro while loading GA')
); );
service.initialize(); 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', () => { it('should filter googleAnalytics when an invalid payload is retrieved', () => {
configurationDataService.findByPropertyName = jasmine.createSpy().withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue( configurationDataService.findByPropertyName = jasmine.createSpy().withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue(
createSuccessfulRemoteDataObject$(null) createSuccessfulRemoteDataObject$(null)
); );
service.initialize(); 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 { cloneDeep, debounce } from 'lodash';
import { ANONYMOUS_STORAGE_NAME_KLARO, klaroConfiguration } from './klaro-configuration'; import { ANONYMOUS_STORAGE_NAME_KLARO, klaroConfiguration } from './klaro-configuration';
import { Operation } from 'fast-json-patch'; 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'; 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 * Initialize configuration for the logged in user
* @param user The authenticated 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 ANONYMOUS_STORAGE_NAME_KLARO = 'klaro-anonymous';
export const GOOGLE_ANALYTICS_KLARO_KEY = 'google-analytics';
/** /**
* Klaro configuration * Klaro configuration
* For more information see https://kiprotect.com/docs/klaro/annotated-config * 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'], purposes: ['statistical'],
required: false, required: false,
cookies: [ cookies: [

View File

@@ -1,5 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
/** /**
* Abstract class representing a service for handling Klaro consent preferences and UI * Abstract class representing a service for handling Klaro consent preferences and UI
*/ */
@@ -11,7 +13,12 @@ export abstract class KlaroService {
abstract initialize(); abstract initialize();
/** /**
* Shows a the dialog with the current consent preferences * Shows a dialog with the current consent preferences
*/ */
abstract showSettings(); 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 { Angulartics2GoogleTagManager } from 'angulartics2';
import { of } from 'rxjs';
import { GoogleAnalyticsService } from './google-analytics.service';
import { ConfigurationDataService } from '../core/data/configuration-data.service'; import { ConfigurationDataService } from '../core/data/configuration-data.service';
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
import { ConfigurationProperty } from '../core/shared/configuration-property.model'; 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', () => { describe('GoogleAnalyticsService', () => {
const trackingIdProp = 'google.analytics.key'; const trackingIdProp = 'google.analytics.key';
@@ -12,6 +16,7 @@ describe('GoogleAnalyticsService', () => {
let service: GoogleAnalyticsService; let service: GoogleAnalyticsService;
let angularticsSpy: Angulartics2GoogleTagManager; let angularticsSpy: Angulartics2GoogleTagManager;
let configSpy: ConfigurationDataService; let configSpy: ConfigurationDataService;
let klaroServiceSpy: jasmine.SpyObj<KlaroService>;
let scriptElementMock: any; let scriptElementMock: any;
let srcSpy: any; let srcSpy: any;
let innerHTMLSpy: any; let innerHTMLSpy: any;
@@ -31,6 +36,10 @@ describe('GoogleAnalyticsService', () => {
'startTracking', 'startTracking',
]); ]);
klaroServiceSpy = jasmine.createSpyObj('KlaroService', {
'getSavedPreferences': jasmine.createSpy('getSavedPreferences')
});
configSpy = createConfigSuccessSpy(trackingIdTestValue); configSpy = createConfigSuccessSpy(trackingIdTestValue);
scriptElementMock = { scriptElementMock = {
@@ -53,7 +62,11 @@ describe('GoogleAnalyticsService', () => {
body: bodyElementSpy, 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', () => { it('should be created', () => {
@@ -73,7 +86,11 @@ describe('GoogleAnalyticsService', () => {
findByPropertyName: createFailedRemoteDataObject$(), 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', () => { it('should NOT add a script to the body', () => {
@@ -91,7 +108,10 @@ describe('GoogleAnalyticsService', () => {
describe('when the tracking id is empty', () => { describe('when the tracking id is empty', () => {
beforeEach(() => { beforeEach(() => {
configSpy = createConfigSuccessSpy(); 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', () => { 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', () => { it('should create a script tag whose innerHTML contains the tracking id', () => {
service.addTrackingIdToPage(); service.addTrackingIdToPage();
expect(documentSpy.createElement).toHaveBeenCalledTimes(2); expect(documentSpy.createElement).toHaveBeenCalledTimes(2);

View File

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