Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Matomo #4018

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions src/app/core/shared/search/search.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import { ViewMode } from '../view-mode.model';
import { SearchService } from './search.service';
import { SearchConfigurationService } from './search-configuration.service';
import anything = jasmine.anything;
import { MatomoTestingModule } from 'ngx-matomo-client/testing';


@Component({
template: '',
Expand All @@ -55,6 +57,7 @@ describe('SearchService', () => {
TestBed.configureTestingModule({
imports: [
CommonModule,
MatomoTestingModule.forRoot(),
RouterTestingModule.withRoutes([
{ path: 'search', component: DummyComponent, pathMatch: 'full' },
]),
Expand Down Expand Up @@ -122,6 +125,7 @@ describe('SearchService', () => {
TestBed.configureTestingModule({
imports: [
CommonModule,
MatomoTestingModule.forRoot(),
RouterTestingModule.withRoutes([
{ path: 'search', component: DummyComponent, pathMatch: 'full' },
]),
Expand Down
9 changes: 7 additions & 2 deletions src/app/core/shared/search/search.service.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -112,6 +113,7 @@
private paginationService: PaginationService,
private searchConfigurationService: SearchConfigurationService,
private angulartics2: Angulartics2,
private matomoTracker: MatomoTracker,
) {
this.searchDataService = new SearchDataService();
}
Expand Down Expand Up @@ -367,7 +369,7 @@
const appliedFilter = appliedFilters[i];
filters.push(appliedFilter);
}
this.angulartics2.eventTrack.next({
const searchTrackObject = {

Check warning on line 372 in src/app/core/shared/search/search.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/shared/search/search.service.ts#L372

Added line #L372 was not covered by tests
action: 'search',
properties: {
searchOptions: config,
Expand All @@ -384,7 +386,10 @@
filters: filters,
clickedObject,
},
});
};

this.matomoTracker.trackSiteSearch(config.query, config.scope, searchQueryResponse.pageInfo.totalElements, searchTrackObject);
this.angulartics2.eventTrack.next(searchTrackObject);

Check warning on line 392 in src/app/core/shared/search/search.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/core/shared/search/search.service.ts#L391-L392

Added lines #L391 - L392 were not covered by tests
}

/**
Expand Down
10 changes: 8 additions & 2 deletions src/app/shared/cookies/browser-orejime.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import {
ANONYMOUS_STORAGE_NAME_OREJIME,
getOrejimeConfiguration,
MATOMO_OREJIME_KEY,
} from './orejime-configuration';

/**
Expand Down Expand Up @@ -133,15 +134,20 @@
),
);

const appsToHide$: Observable<string[]> = observableCombineLatest([hideGoogleAnalytics$, hideRegistrationVerification$]).pipe(
map(([hideGoogleAnalytics, hideRegistrationVerification]) => {
const hideMatomo$ = observableOf(!(environment.matomo?.trackerUrl && environment.matomo?.siteId));

const appsToHide$: Observable<string[]> = observableCombineLatest([hideGoogleAnalytics$, hideRegistrationVerification$, hideMatomo$]).pipe(
map(([hideGoogleAnalytics, hideRegistrationVerification, hideMatomo]) => {
const appsToHideArray: string[] = [];
if (hideGoogleAnalytics) {
appsToHideArray.push(this.GOOGLE_ANALYTICS_SERVICE_NAME);
}
if (hideRegistrationVerification) {
appsToHideArray.push(CAPTCHA_NAME);
}
if (hideMatomo) {
appsToHideArray.push(MATOMO_OREJIME_KEY);

Check warning on line 149 in src/app/shared/cookies/browser-orejime.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/cookies/browser-orejime.service.ts#L149

Added line #L149 was not covered by tests
}
return appsToHideArray;
}),
);
Expand Down
15 changes: 15 additions & 0 deletions src/app/shared/cookies/orejime-configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@

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
Expand Down Expand Up @@ -134,6 +138,17 @@
HAS_AGREED_END_USER,
],
},
{
name: MATOMO_OREJIME_KEY,
purposes: ['statistical'],
required: false,
cookies: [
MATOMO_COOKIE,
],
callback: (consent: boolean) => {
_window?.nativeWindow.changeMatomoConsent(consent);

Check warning on line 149 in src/app/shared/cookies/orejime-configuration.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/cookies/orejime-configuration.ts#L149

Added line #L149 was not covered by tests
},
},
{
name: GOOGLE_ANALYTICS_OREJIME_KEY,
purposes: ['statistical'],
Expand Down
84 changes: 84 additions & 0 deletions src/app/statistics/matomo.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { TestBed } from '@angular/core/testing';
import {
MatomoInitializerService,
MatomoTracker,
} from 'ngx-matomo-client';
import { MatomoTestingModule } from 'ngx-matomo-client/testing';
import { of } from 'rxjs';

import { environment } from '../../environments/environment';
import {
NativeWindowRef,
NativeWindowService,
} from '../core/services/window.service';
import { OrejimeService } from '../shared/cookies/orejime.service';
import { MatomoService } from './matomo.service';

describe('MatomoService', () => {
let service: MatomoService;
let matomoTracker: jasmine.SpyObj<MatomoTracker>;
let matomoInitializer: jasmine.SpyObj<MatomoInitializerService>;
let orejimeService: jasmine.SpyObj<OrejimeService>;
let nativeWindowService: jasmine.SpyObj<NativeWindowRef>;

beforeEach(() => {
matomoTracker = jasmine.createSpyObj('MatomoTracker', ['setConsentGiven', 'forgetConsentGiven']);
matomoInitializer = jasmine.createSpyObj('MatomoInitializerService', ['initializeTracker']);
orejimeService = jasmine.createSpyObj('OrejimeService', ['getSavedPreferences']);
nativeWindowService = jasmine.createSpyObj('NativeWindowService', [], { nativeWindow: {} });

TestBed.configureTestingModule({
imports: [MatomoTestingModule.forRoot()],
providers: [
{ provide: MatomoTracker, useValue: matomoTracker },
{ provide: MatomoInitializerService, useValue: matomoInitializer },
{ provide: OrejimeService, useValue: orejimeService },
{ provide: NativeWindowService, useValue: nativeWindowService },
],
});

service = TestBed.inject(MatomoService);
});

it('should be created', () => {
expect(service).toBeTruthy();
});

it('should set changeMatomoConsent on native window', () => {
orejimeService.getSavedPreferences.and.returnValue(of({ matomo: true }));
service.init();
expect(nativeWindowService.nativeWindow.changeMatomoConsent).toBe(service.changeMatomoConsent);
});

it('should initialize tracker with correct parameters in production', () => {
environment.production = true;
environment.matomo = { siteId: '1', trackerUrl: 'http://example.com' };
orejimeService.getSavedPreferences.and.returnValue(of({ matomo: true }));

service.init();

expect(matomoTracker.setConsentGiven).toHaveBeenCalled();
expect(matomoInitializer.initializeTracker).toHaveBeenCalledWith({
siteId: '1',
trackerUrl: 'http://example.com',
});
});

it('should not initialize tracker if not in production', () => {
environment.production = false;

service.init();

expect(matomoInitializer.initializeTracker).not.toHaveBeenCalled();
});

it('should call setConsentGiven when consent is true', () => {
service.changeMatomoConsent(true);
expect(matomoTracker.setConsentGiven).toHaveBeenCalled();
});

it('should call forgetConsentGiven when consent is false', () => {
service.changeMatomoConsent(false);
expect(matomoTracker.forgetConsentGiven).toHaveBeenCalled();
});
});
72 changes: 72 additions & 0 deletions src/app/statistics/matomo.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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';

/**
* Service to manage Matomo analytics integration.
* Handles initialization and consent management for Matomo tracking.
*/
@Injectable({
providedIn: 'root',
})
export class MatomoService {

/** Injects the MatomoInitializerService to initialize the Matomo tracker. */
matomoInitializer = inject(MatomoInitializerService);

/** Injects the MatomoTracker to manage Matomo tracking operations. */
matomoTracker = inject(MatomoTracker);

/** Injects the OrejimeService to manage cookie consent preferences. */
orejimeService = inject(OrejimeService);

/** Injects the NativeWindowService to access the native window object. */
_window = inject(NativeWindowService);

/**
* Initializes the Matomo tracker if in production environment.
* Sets up the changeMatomoConsent function on the native window object.
* Subscribes to cookie consent preferences and initializes the tracker accordingly.
*/
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,
});
}
});
}
}

/**
* Changes the Matomo consent status based on the given consent value.
* @param consent - A boolean indicating whether consent is given for Matomo tracking.
*/
changeMatomoConsent = (consent: boolean) => {
if (consent) {
this.matomoTracker.setConsentGiven();
} else {
this.matomoTracker.forgetConsentGiven();
}
};
}
4 changes: 4 additions & 0 deletions src/app/statistics/mock-matomo-tracker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export class MockMatomoTracker {
trackSiteSearch = () => {};
trackPageView = () => {};
}
4 changes: 4 additions & 0 deletions src/assets/i18n/en.json5
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions src/assets/i18n/it.json5
Original file line number Diff line number Diff line change
Expand Up @@ -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",

Expand Down
2 changes: 2 additions & 0 deletions src/config/app-config.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -66,6 +67,7 @@ interface AppConfig extends Config {
search: SearchConfig;
notifyMetrics: AdminNotifyMetricsRow[];
liveRegion: LiveRegionConfig;
matomo?: MatomoConfig;
}

/**
Expand Down
3 changes: 3 additions & 0 deletions src/config/default-app-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -599,4 +600,6 @@ export class DefaultAppConfig implements AppConfig {
messageTimeOutDurationMs: 30000,
isVisible: false,
};

matomo: MatomoConfig = {};
}
9 changes: 9 additions & 0 deletions src/config/matomo-config.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Config } from './config.interface';

/**
* Configuration interface for Matomo tracking
*/
export interface MatomoConfig extends Config {
trackerUrl?: string;
siteId?: string;
}
Loading
Loading