From 2225c2e4287f1d74b7f16d155e9a06cd7b91661d Mon Sep 17 00:00:00 2001 From: Andrea Barbasso <´andrea.barbasso@4science.com´> Date: Wed, 5 Feb 2025 11:37:00 +0100 Subject: [PATCH] [CST-18964] add matomo integration --- package-lock.json | 13 +++++ package.json | 1 + src/app/core/shared/search/search.service.ts | 9 +++- .../shared/cookies/browser-orejime.service.ts | 10 +++- .../shared/cookies/orejime-configuration.ts | 15 ++++++ src/app/statistics/matomo.service.spec.ts | 16 ++++++ src/app/statistics/matomo.service.ts | 52 +++++++++++++++++++ src/app/statistics/mock-matomo-tracker.ts | 3 ++ src/assets/i18n/en.json5 | 4 ++ src/assets/i18n/it.json5 | 7 +++ src/config/app-config.interface.ts | 2 + src/config/default-app-config.ts | 3 ++ src/config/matomo-config.interface.ts | 9 ++++ src/modules/app/browser-app.config.ts | 12 +++++ src/modules/app/browser-init.service.ts | 7 +++ src/modules/app/server-app.config.ts | 6 +++ 16 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 src/app/statistics/matomo.service.spec.ts create mode 100644 src/app/statistics/matomo.service.ts create mode 100644 src/app/statistics/mock-matomo-tracker.ts create mode 100644 src/config/matomo-config.interface.ts diff --git a/package-lock.json b/package-lock.json index b75ddc9ccf..7c22e2d799 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,6 +66,7 @@ "ng2-file-upload": "5.0.0", "ng2-nouislider": "^2.0.0", "ngx-infinite-scroll": "^16.0.0", + "ngx-matomo-client": "^6.4.1", "ngx-pagination": "6.0.3", "ngx-skeleton-loader": "^9.0.0", "ngx-ui-switch": "^14.1.0", @@ -16935,6 +16936,18 @@ "@angular/forms": ">=10.0.0" } }, + "node_modules/ngx-matomo-client": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/ngx-matomo-client/-/ngx-matomo-client-6.4.1.tgz", + "integrity": "sha512-GRriCGW0ULCg9oSZw3ule+o9esELVVJTJ0Z99/zYKGjlyrrHLn5a1e0GSdgICubo59gP1cg9NwsOC0BH7oio9A==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^17.0.0 || ^18.0.0", + "@angular/core": "^17.0.0 || ^18.0.0" + } + }, "node_modules/ngx-pagination": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/ngx-pagination/-/ngx-pagination-6.0.3.tgz", diff --git a/package.json b/package.json index 9e72a4cc9d..0c8ddd5ac9 100644 --- a/package.json +++ b/package.json @@ -153,6 +153,7 @@ "ng2-file-upload": "5.0.0", "ng2-nouislider": "^2.0.0", "ngx-infinite-scroll": "^16.0.0", + "ngx-matomo-client": "^6.4.1", "ngx-pagination": "6.0.3", "ngx-skeleton-loader": "^9.0.0", "ngx-ui-switch": "^14.1.0", diff --git a/src/app/core/shared/search/search.service.ts b/src/app/core/shared/search/search.service.ts index 60a2da6af1..7afefd9659 100644 --- a/src/app/core/shared/search/search.service.ts +++ b/src/app/core/shared/search/search.service.ts @@ -1,6 +1,7 @@ /* eslint-disable max-classes-per-file */ import { Injectable } from '@angular/core'; import { Angulartics2 } from 'angulartics2'; +import { MatomoTracker } from 'ngx-matomo-client'; import { BehaviorSubject, combineLatest as observableCombineLatest, @@ -112,6 +113,7 @@ export class SearchService { private paginationService: PaginationService, private searchConfigurationService: SearchConfigurationService, private angulartics2: Angulartics2, + private matomoTracker: MatomoTracker, ) { this.searchDataService = new SearchDataService(); } @@ -367,7 +369,7 @@ export class SearchService { const appliedFilter = appliedFilters[i]; filters.push(appliedFilter); } - this.angulartics2.eventTrack.next({ + const searchTrackObject = { action: 'search', properties: { searchOptions: config, @@ -384,7 +386,10 @@ export class SearchService { filters: filters, clickedObject, }, - }); + }; + + this.matomoTracker.trackSiteSearch(config.query, config.scope, searchQueryResponse.pageInfo.totalElements, searchTrackObject); + this.angulartics2.eventTrack.next(searchTrackObject); } /** diff --git a/src/app/shared/cookies/browser-orejime.service.ts b/src/app/shared/cookies/browser-orejime.service.ts index a2c66f9500..ba4a61fa2c 100644 --- a/src/app/shared/cookies/browser-orejime.service.ts +++ b/src/app/shared/cookies/browser-orejime.service.ts @@ -39,6 +39,7 @@ import { OrejimeService } from './orejime.service'; import { ANONYMOUS_STORAGE_NAME_OREJIME, getOrejimeConfiguration, + MATOMO_OREJIME_KEY, } from './orejime-configuration'; /** @@ -133,8 +134,10 @@ export class BrowserOrejimeService extends OrejimeService { ), ); - const appsToHide$: Observable = observableCombineLatest([hideGoogleAnalytics$, hideRegistrationVerification$]).pipe( - map(([hideGoogleAnalytics, hideRegistrationVerification]) => { + const hideMatomo$ = observableOf(!(environment.matomo?.trackerUrl && environment.matomo?.siteId)); + + const appsToHide$: Observable = observableCombineLatest([hideGoogleAnalytics$, hideRegistrationVerification$, hideMatomo$]).pipe( + map(([hideGoogleAnalytics, hideRegistrationVerification, hideMatomo]) => { const appsToHideArray: string[] = []; if (hideGoogleAnalytics) { appsToHideArray.push(this.GOOGLE_ANALYTICS_SERVICE_NAME); @@ -142,6 +145,9 @@ export class BrowserOrejimeService extends OrejimeService { if (hideRegistrationVerification) { appsToHideArray.push(CAPTCHA_NAME); } + if (hideMatomo) { + appsToHideArray.push(MATOMO_OREJIME_KEY); + } return appsToHideArray; }), ); diff --git a/src/app/shared/cookies/orejime-configuration.ts b/src/app/shared/cookies/orejime-configuration.ts index 8e99ff30b2..76499fce7d 100644 --- a/src/app/shared/cookies/orejime-configuration.ts +++ b/src/app/shared/cookies/orejime-configuration.ts @@ -19,6 +19,10 @@ export const ANONYMOUS_STORAGE_NAME_OREJIME = 'orejime-anonymous'; export const GOOGLE_ANALYTICS_OREJIME_KEY = 'google-analytics'; +export const MATOMO_OREJIME_KEY = 'matomo'; + +export const MATOMO_COOKIE = 'dsMatomo'; + /** * Orejime configuration * For more information see https://github.com/empreinte-digitale/orejime @@ -134,6 +138,17 @@ export function getOrejimeConfiguration(_window: NativeWindowRef): any { HAS_AGREED_END_USER, ], }, + { + name: MATOMO_OREJIME_KEY, + purposes: ['statistical'], + required: false, + cookies: [ + MATOMO_COOKIE, + ], + callback: (consent: boolean) => { + _window?.nativeWindow.changeMatomoConsent(consent); + }, + }, { name: GOOGLE_ANALYTICS_OREJIME_KEY, purposes: ['statistical'], diff --git a/src/app/statistics/matomo.service.spec.ts b/src/app/statistics/matomo.service.spec.ts new file mode 100644 index 0000000000..4c84d26f44 --- /dev/null +++ b/src/app/statistics/matomo.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { MatomoService } from './matomo.service'; + +describe('MatomoService', () => { + let service: MatomoService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(MatomoService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/statistics/matomo.service.ts b/src/app/statistics/matomo.service.ts new file mode 100644 index 0000000000..d507bb0a50 --- /dev/null +++ b/src/app/statistics/matomo.service.ts @@ -0,0 +1,52 @@ +import { + inject, + Injectable, +} from '@angular/core'; +import { + MatomoInitializerService, + MatomoTracker, +} from 'ngx-matomo-client'; + +import { environment } from '../../environments/environment'; +import { NativeWindowService } from '../core/services/window.service'; +import { OrejimeService } from '../shared/cookies/orejime.service'; + +@Injectable({ + providedIn: 'root', +}) +export class MatomoService { + + matomoInitializer = inject(MatomoInitializerService); + matomoTracker = inject(MatomoTracker); + orejimeService = inject(OrejimeService); + _window = inject(NativeWindowService); + + init() { + if (this._window.nativeWindow) { + this._window.nativeWindow.changeMatomoConsent = this.changeMatomoConsent; + } + + if (environment.production) { + const preferences$ = this.orejimeService.getSavedPreferences(); + + preferences$.subscribe(preferences => { + this.changeMatomoConsent(preferences.matomo); + + if (environment.matomo?.siteId && environment.matomo?.trackerUrl) { + this.matomoInitializer.initializeTracker({ + siteId: environment.matomo.siteId, + trackerUrl: environment.matomo.trackerUrl, + }); + } + }); + } + } + + changeMatomoConsent = (consent: boolean) => { + if (consent) { + this.matomoTracker.setConsentGiven(); + } else { + this.matomoTracker.forgetConsentGiven(); + } + }; +} diff --git a/src/app/statistics/mock-matomo-tracker.ts b/src/app/statistics/mock-matomo-tracker.ts new file mode 100644 index 0000000000..cfd2ca2993 --- /dev/null +++ b/src/app/statistics/mock-matomo-tracker.ts @@ -0,0 +1,3 @@ +export class MockMatomoTracker { + trackSiteSearch = () => {}; +} diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 053e5133d9..08c75b13b4 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1630,6 +1630,10 @@ "cookies.consent.app.description.google-recaptcha": "We use google reCAPTCHA service during registration and password recovery", + "cookies.consent.app.title.matomo": "Matomo", + + "cookies.consent.app.description.matomo": "Allows us to track statistical data", + "cookies.consent.purpose.functional": "Functional", "cookies.consent.purpose.statistical": "Statistical", diff --git a/src/assets/i18n/it.json5 b/src/assets/i18n/it.json5 index 098e9752bc..2d99a178a9 100644 --- a/src/assets/i18n/it.json5 +++ b/src/assets/i18n/it.json5 @@ -2029,6 +2029,13 @@ "cookies.consent.app.description.google-recaptcha": "Utilizziamo il servizio Google reCAPTCHA nelle fasi di registrazione e recupero password", + // "cookies.consent.app.title.matomo": "Matomo", + "cookies.consent.app.title.matomo": "Matomo", + + // "cookies.consent.app.description.matomo": "Allows us to track statistical data", + "cookies.consent.app.description.matomo": "Ci permette di tracciare i dati statistici", + + // "cookies.consent.purpose.functional": "Functional", "cookies.consent.purpose.functional": "Funzionale", diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts index 7f5f019958..a52cd6be35 100644 --- a/src/config/app-config.interface.ts +++ b/src/config/app-config.interface.ts @@ -24,6 +24,7 @@ import { InfoConfig } from './info-config.interface'; import { ItemConfig } from './item-config.interface'; import { LangConfig } from './lang-config.interface'; import { MarkdownConfig } from './markdown-config.interface'; +import { MatomoConfig } from './matomo-config.interface'; import { MediaViewerConfig } from './media-viewer-config.interface'; import { INotificationBoardOptions } from './notifications-config.interfaces'; import { QualityAssuranceConfig } from './quality-assurance.config'; @@ -66,6 +67,7 @@ interface AppConfig extends Config { search: SearchConfig; notifyMetrics: AdminNotifyMetricsRow[]; liveRegion: LiveRegionConfig; + matomo?: MatomoConfig; } /** diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index 3c5e0ef0da..2ea08946fe 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -19,6 +19,7 @@ import { InfoConfig } from './info-config.interface'; import { ItemConfig } from './item-config.interface'; import { LangConfig } from './lang-config.interface'; import { MarkdownConfig } from './markdown-config.interface'; +import { MatomoConfig } from './matomo-config.interface'; import { MediaViewerConfig } from './media-viewer-config.interface'; import { INotificationBoardOptions } from './notifications-config.interfaces'; import { QualityAssuranceConfig } from './quality-assurance.config'; @@ -599,4 +600,6 @@ export class DefaultAppConfig implements AppConfig { messageTimeOutDurationMs: 30000, isVisible: false, }; + + matomo: MatomoConfig = {}; } diff --git a/src/config/matomo-config.interface.ts b/src/config/matomo-config.interface.ts new file mode 100644 index 0000000000..95dc267268 --- /dev/null +++ b/src/config/matomo-config.interface.ts @@ -0,0 +1,9 @@ +import { Config } from './config.interface'; + +/** + * Configuration interface for Matomo tracking + */ +export interface MatomoConfig extends Config { + trackerUrl?: string; + siteId?: string; +} diff --git a/src/modules/app/browser-app.config.ts b/src/modules/app/browser-app.config.ts index 213b9de2f7..4173d60d5f 100644 --- a/src/modules/app/browser-app.config.ts +++ b/src/modules/app/browser-app.config.ts @@ -29,6 +29,11 @@ import { Angulartics2GoogleTagManager, Angulartics2RouterlessModule, } from 'angulartics2'; +import { + provideMatomo, + withRouteData, + withRouter, +} from 'ngx-matomo-client'; import { commonAppConfig } from '../../app/app.config'; import { storeModuleConfig } from '../../app/app.reducer'; @@ -157,5 +162,12 @@ export const browserAppConfig: ApplicationConfig = mergeApplicationConfig({ provide: MathService, useClass: ClientMathService, }, + provideMatomo( + { + mode: 'deferred', + }, + withRouter(), + withRouteData(), + ), ], }, commonAppConfig); diff --git a/src/modules/app/browser-init.service.ts b/src/modules/app/browser-init.service.ts index 525067da3a..562c0e4d0d 100644 --- a/src/modules/app/browser-init.service.ts +++ b/src/modules/app/browser-init.service.ts @@ -45,6 +45,7 @@ import { MenuService } from '../../app/shared/menu/menu.service'; import { ThemeService } from '../../app/shared/theme-support/theme.service'; import { Angulartics2DSpace } from '../../app/statistics/angulartics/dspace-provider'; import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service'; +import { MatomoService } from '../../app/statistics/matomo.service'; import { StoreAction, StoreActionTypes, @@ -85,6 +86,7 @@ export class BrowserInitService extends InitService { protected router: Router, private requestService: RequestService, private halService: HALEndpointService, + private matomoService: MatomoService, ) { super( @@ -124,6 +126,7 @@ export class BrowserInitService extends InitService { this.initI18n(); this.initAngulartics(); this.initGoogleAnalytics(); + this.initMatomo(); this.initRouteListeners(); this.themeService.listenForThemeChanges(true); this.trackAuthTokenExpiration(); @@ -173,6 +176,10 @@ export class BrowserInitService extends InitService { this.googleAnalyticsService.addTrackingIdToPage(); } + protected initMatomo(): void { + this.matomoService.init(); + } + /** * During an external authentication flow invalidate the * data in the cache. This allows the app to fetch fresh content. diff --git a/src/modules/app/server-app.config.ts b/src/modules/app/server-app.config.ts index 4cf9369ddd..9f4bf5c50f 100644 --- a/src/modules/app/server-app.config.ts +++ b/src/modules/app/server-app.config.ts @@ -28,6 +28,7 @@ import { Angulartics2GoogleAnalytics, Angulartics2GoogleGlobalSiteTag, } from 'angulartics2'; +import { MatomoTracker } from 'ngx-matomo-client'; import { commonAppConfig } from '../../app/app.config'; import { storeModuleConfig } from '../../app/app.reducer'; @@ -55,6 +56,7 @@ import { XSRFService } from '../../app/core/xsrf/xsrf.service'; import { AngularticsProviderMock } from '../../app/shared/mocks/angulartics-provider.service.mock'; import { Angulartics2Mock } from '../../app/shared/mocks/angulartics2.service.mock'; import { Angulartics2DSpace } from '../../app/statistics/angulartics/dspace-provider'; +import { MockMatomoTracker } from '../../app/statistics/mock-matomo-tracker'; import { ServerSubmissionService } from '../../app/submission/server-submission.service'; import { SubmissionService } from '../../app/submission/submission.service'; import { TranslateServerLoader } from '../../ngx-translate-loaders/translate-server.loader'; @@ -144,5 +146,9 @@ export const serverAppConfig: ApplicationConfig = mergeApplicationConfig({ provide: MathService, useClass: ServerMathService, }, + { + provide: MatomoTracker, + useClass: MockMatomoTracker, + }, ], }, commonAppConfig);