From 6bd4f04174fc0cefaf3b5be2adef617f97474d72 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Mon, 24 Mar 2025 19:16:50 +0100 Subject: [PATCH] [CST-18964] PR review --- .../bitstream-download-page.component.spec.ts | 61 ++++++++--- .../bitstream-download-page.component.ts | 14 ++- .../cookies/browser-orejime.service.spec.ts | 30 ++++++ .../shared/cookies/browser-orejime.service.ts | 9 +- src/app/statistics/matomo.service.spec.ts | 54 ++++++++-- src/app/statistics/matomo.service.ts | 101 ++++++++++++++++-- src/config/app-config.interface.ts | 2 - src/config/default-app-config.ts | 2 - src/config/matomo-config.interface.ts | 9 -- 9 files changed, 229 insertions(+), 53 deletions(-) delete mode 100644 src/config/matomo-config.interface.ts diff --git a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts index 0cc293c6f7..f2c951300b 100644 --- a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts +++ b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts @@ -1,4 +1,7 @@ -import { CommonModule } from '@angular/common'; +import { + CommonModule, + Location, +} from '@angular/common'; import { PLATFORM_ID } from '@angular/core'; import { ComponentFixture, @@ -14,6 +17,7 @@ import { of as observableOf } from 'rxjs'; import { getForbiddenRoute } from '../../app-routing-paths'; import { AuthService } from '../../core/auth/auth.service'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { SignpostingDataService } from '../../core/data/signposting-data.service'; import { HardRedirectService } from '../../core/services/hard-redirect.service'; @@ -21,6 +25,7 @@ import { ServerResponseService } from '../../core/services/server-response.servi import { Bitstream } from '../../core/shared/bitstream.model'; import { FileService } from '../../core/shared/file.service'; import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; +import { MatomoService } from '../../statistics/matomo.service'; import { BitstreamDownloadPageComponent } from './bitstream-download-page.component'; describe('BitstreamDownloadPageComponent', () => { @@ -33,10 +38,13 @@ describe('BitstreamDownloadPageComponent', () => { let hardRedirectService: HardRedirectService; let activatedRoute; let router; + let location: Location; + let dsoNameService: DSONameService; let bitstream: Bitstream; let serverResponseService: jasmine.SpyObj; let signpostingDataService: jasmine.SpyObj; + let matomoService: jasmine.SpyObj; const mocklink = { href: 'http://test.org', @@ -54,6 +62,7 @@ describe('BitstreamDownloadPageComponent', () => { authService = jasmine.createSpyObj('authService', { isAuthenticated: observableOf(true), setRedirectUrl: {}, + getShortlivedToken: observableOf('token'), }); authorizationService = jasmine.createSpyObj('authorizationSerivice', { isAuthorized: observableOf(true), @@ -63,9 +72,18 @@ describe('BitstreamDownloadPageComponent', () => { retrieveFileDownloadLink: observableOf('content-url-with-headers'), }); - hardRedirectService = jasmine.createSpyObj('fileService', { + hardRedirectService = jasmine.createSpyObj('hardRedirectService', { redirect: {}, }); + + location = jasmine.createSpyObj('location', { + back: {}, + }); + + dsoNameService = jasmine.createSpyObj('dsoNameService', { + getName: 'Test Bitstream', + }); + bitstream = Object.assign(new Bitstream(), { uuid: 'bitstreamUuid', _links: { @@ -94,6 +112,8 @@ describe('BitstreamDownloadPageComponent', () => { signpostingDataService = jasmine.createSpyObj('SignpostingDataService', { getLinks: observableOf([mocklink, mocklink2]), }); + matomoService = jasmine.createSpyObj('MatomoService', ['appendVisitorId']); + matomoService.appendVisitorId.and.callFake((link) => observableOf(link)); } function initTestbed() { @@ -108,7 +128,10 @@ describe('BitstreamDownloadPageComponent', () => { { provide: HardRedirectService, useValue: hardRedirectService }, { provide: ServerResponseService, useValue: serverResponseService }, { provide: SignpostingDataService, useValue: signpostingDataService }, + { provide: MatomoService, useValue: matomoService }, { provide: PLATFORM_ID, useValue: 'server' }, + { provide: Location, useValue: location }, + { provide: DSONameService, useValue: dsoNameService }, ], }) .compileComponents(); @@ -142,9 +165,11 @@ describe('BitstreamDownloadPageComponent', () => { component = fixture.componentInstance; fixture.detectChanges(); }); - it('should redirect to the content link', () => { - expect(hardRedirectService.redirect).toHaveBeenCalledWith('bitstream-content-link'); - }); + it('should redirect to the content link', waitForAsync(() => { + fixture.whenStable().then(() => { + expect(hardRedirectService.redirect).toHaveBeenCalledWith('bitstream-content-link'); + }); + })); it('should add the signposting links', () => { expect(serverResponseService.setHeader).toHaveBeenCalled(); }); @@ -159,9 +184,11 @@ describe('BitstreamDownloadPageComponent', () => { component = fixture.componentInstance; fixture.detectChanges(); }); - it('should redirect to an updated content link', () => { - expect(hardRedirectService.redirect).toHaveBeenCalledWith('content-url-with-headers'); - }); + it('should redirect to an updated content link', waitForAsync(() => { + fixture.whenStable().then(() => { + expect(hardRedirectService.redirect).toHaveBeenCalledWith('content-url-with-headers'); + }); + })); }); describe('when the user is not authorized and logged in', () => { beforeEach(waitForAsync(() => { @@ -174,9 +201,11 @@ describe('BitstreamDownloadPageComponent', () => { component = fixture.componentInstance; fixture.detectChanges(); }); - it('should navigate to the forbidden route', () => { - expect(router.navigateByUrl).toHaveBeenCalledWith(getForbiddenRoute(), { skipLocationChange: true }); - }); + it('should navigate to the forbidden route', waitForAsync(() => { + fixture.whenStable().then(() => { + expect(router.navigateByUrl).toHaveBeenCalledWith(getForbiddenRoute(), { skipLocationChange: true }); + }); + })); }); describe('when the user is not authorized and not logged in', () => { beforeEach(waitForAsync(() => { @@ -190,10 +219,12 @@ describe('BitstreamDownloadPageComponent', () => { component = fixture.componentInstance; fixture.detectChanges(); }); - it('should navigate to the login page', () => { - expect(authService.setRedirectUrl).toHaveBeenCalled(); - expect(router.navigateByUrl).toHaveBeenCalledWith('login'); - }); + it('should navigate to the login page', waitForAsync(() => { + fixture.whenStable().then(() => { + expect(authService.setRedirectUrl).toHaveBeenCalled(); + expect(router.navigateByUrl).toHaveBeenCalledWith('login'); + }); + })); }); }); }); diff --git a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts index ee329df16e..dc060e9884 100644 --- a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts +++ b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts @@ -44,6 +44,7 @@ import { hasValue, isNotEmpty, } from '../../shared/empty.util'; +import { MatomoService } from '../../statistics/matomo.service'; @Component({ selector: 'ds-bitstream-download-page', @@ -73,6 +74,7 @@ export class BitstreamDownloadPageComponent implements OnInit { public dsoNameService: DSONameService, private signpostingDataService: SignpostingDataService, private responseService: ServerResponseService, + private matomoService: MatomoService, @Inject(PLATFORM_ID) protected platformId: string, ) { this.initPageLinks(); @@ -109,14 +111,20 @@ export class BitstreamDownloadPageComponent implements OnInit { return [isAuthorized, isLoggedIn, bitstream, fileLink]; })); } else { - return [[isAuthorized, isLoggedIn, bitstream, '']]; + return [[isAuthorized, isLoggedIn, bitstream, bitstream._links.content.href]]; } }), - ).subscribe(([isAuthorized, isLoggedIn, bitstream, fileLink]: [boolean, boolean, Bitstream, string]) => { + switchMap(([isAuthorized, isLoggedIn, bitstream, fileLink]: [boolean, boolean, Bitstream, string]) => + this.matomoService.appendVisitorId(fileLink) + .pipe( + map((fileLinkWithVisitorId) => [isAuthorized, isLoggedIn, bitstream, fileLinkWithVisitorId]), + ), + ), + ).subscribe(([isAuthorized, isLoggedIn, , fileLink]: [boolean, boolean, Bitstream, string]) => { if (isAuthorized && isLoggedIn && isNotEmpty(fileLink)) { this.hardRedirectService.redirect(fileLink); } else if (isAuthorized && !isLoggedIn) { - this.hardRedirectService.redirect(bitstream._links.content.href); + this.hardRedirectService.redirect(fileLink); } else if (!isAuthorized && isLoggedIn) { this.router.navigateByUrl(getForbiddenRoute(), { skipLocationChange: true }); } else if (!isAuthorized && !isLoggedIn) { diff --git a/src/app/shared/cookies/browser-orejime.service.spec.ts b/src/app/shared/cookies/browser-orejime.service.spec.ts index d7e226a42e..126e113227 100644 --- a/src/app/shared/cookies/browser-orejime.service.spec.ts +++ b/src/app/shared/cookies/browser-orejime.service.spec.ts @@ -28,6 +28,7 @@ import { ANONYMOUS_STORAGE_NAME_OREJIME } from './orejime-configuration'; describe('BrowserOrejimeService', () => { const trackingIdProp = 'google.analytics.key'; const trackingIdTestValue = 'mock-tracking-id'; + const matomoTrackingId = 'matomo-tracking-id'; const googleAnalytics = 'google-analytics'; const recaptchaProp = 'registration.verification.enabled'; const recaptchaValue = 'true'; @@ -310,9 +311,11 @@ describe('BrowserOrejimeService', () => { describe('initialize google analytics configuration', () => { let GOOGLE_ANALYTICS_KEY; let REGISTRATION_VERIFICATION_ENABLED_KEY; + let MATOMO_ENABLED; beforeEach(() => { GOOGLE_ANALYTICS_KEY = clone((service as any).GOOGLE_ANALYTICS_KEY); REGISTRATION_VERIFICATION_ENABLED_KEY = clone((service as any).REGISTRATION_VERIFICATION_ENABLED_KEY); + MATOMO_ENABLED = clone((service as any).MATOMO_ENABLED); spyOn((service as any), 'getUser$').and.returnValue(observableOf(user)); translateService.get.and.returnValue(observableOf('loading...')); spyOn(service, 'addAppMessages'); @@ -361,6 +364,15 @@ describe('BrowserOrejimeService', () => { name: trackingIdTestValue, values: ['false'], }), + ) + .withArgs(MATOMO_ENABLED) + .and + .returnValue( + createSuccessfulRemoteDataObject$({ + ... new ConfigurationProperty(), + name: matomoTrackingId, + values: ['false'], + }), ); service.initialize(); @@ -380,6 +392,15 @@ describe('BrowserOrejimeService', () => { name: trackingIdTestValue, values: ['false'], }), + ) + .withArgs(MATOMO_ENABLED) + .and + .returnValue( + createSuccessfulRemoteDataObject$({ + ... new ConfigurationProperty(), + name: matomoTrackingId, + values: ['false'], + }), ); service.initialize(); expect(service.orejimeConfig.apps).not.toContain(jasmine.objectContaining({ name: googleAnalytics })); @@ -398,6 +419,15 @@ describe('BrowserOrejimeService', () => { name: trackingIdTestValue, values: ['false'], }), + ) + .withArgs(MATOMO_ENABLED) + .and + .returnValue( + createSuccessfulRemoteDataObject$({ + ... new ConfigurationProperty(), + name: matomoTrackingId, + values: ['false'], + }), ); service.initialize(); expect(service.orejimeConfig.apps).not.toContain(jasmine.objectContaining({ name: googleAnalytics })); diff --git a/src/app/shared/cookies/browser-orejime.service.ts b/src/app/shared/cookies/browser-orejime.service.ts index ba4a61fa2c..f52251773a 100644 --- a/src/app/shared/cookies/browser-orejime.service.ts +++ b/src/app/shared/cookies/browser-orejime.service.ts @@ -90,6 +90,7 @@ export class BrowserOrejimeService extends OrejimeService { private readonly GOOGLE_ANALYTICS_SERVICE_NAME = 'google-analytics'; + private readonly MATOMO_ENABLED = 'matomo.enabled'; /** * Initial Orejime configuration @@ -134,7 +135,13 @@ export class BrowserOrejimeService extends OrejimeService { ), ); - const hideMatomo$ = observableOf(!(environment.matomo?.trackerUrl && environment.matomo?.siteId)); + const hideMatomo$ = + this.configService.findByPropertyName(this.MATOMO_ENABLED).pipe( + getFirstCompletedRemoteData(), + map((remoteData) => + !remoteData.hasSucceeded || !remoteData.payload || isEmpty(remoteData.payload.values) || remoteData.payload.values[0].toLowerCase() !== 'true', + ), + ); const appsToHide$: Observable = observableCombineLatest([hideGoogleAnalytics$, hideRegistrationVerification$, hideMatomo$]).pipe( map(([hideGoogleAnalytics, hideRegistrationVerification, hideMatomo]) => { diff --git a/src/app/statistics/matomo.service.spec.ts b/src/app/statistics/matomo.service.spec.ts index 60cbffdfb9..d06ed27226 100644 --- a/src/app/statistics/matomo.service.spec.ts +++ b/src/app/statistics/matomo.service.spec.ts @@ -1,4 +1,8 @@ -import { TestBed } from '@angular/core/testing'; +import { + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; import { MatomoInitializerService, MatomoTracker, @@ -7,12 +11,19 @@ import { MatomoTestingModule } from 'ngx-matomo-client/testing'; import { of } from 'rxjs'; import { environment } from '../../environments/environment'; +import { ConfigurationDataService } from '../core/data/configuration-data.service'; import { NativeWindowRef, NativeWindowService, } from '../core/services/window.service'; +import { ConfigurationProperty } from '../core/shared/configuration-property.model'; import { OrejimeService } from '../shared/cookies/orejime.service'; -import { MatomoService } from './matomo.service'; +import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; +import { + MATOMO_SITE_ID, + MATOMO_TRACKER_URL, + MatomoService, +} from './matomo.service'; describe('MatomoService', () => { let service: MatomoService; @@ -20,12 +31,14 @@ describe('MatomoService', () => { let matomoInitializer: jasmine.SpyObj; let orejimeService: jasmine.SpyObj; let nativeWindowService: jasmine.SpyObj; + let configService: jasmine.SpyObj; beforeEach(() => { - matomoTracker = jasmine.createSpyObj('MatomoTracker', ['setConsentGiven', 'forgetConsentGiven']); + matomoTracker = jasmine.createSpyObj('MatomoTracker', ['setConsentGiven', 'forgetConsentGiven', 'getVisitorId']); matomoInitializer = jasmine.createSpyObj('MatomoInitializerService', ['initializeTracker']); orejimeService = jasmine.createSpyObj('OrejimeService', ['getSavedPreferences']); nativeWindowService = jasmine.createSpyObj('NativeWindowService', [], { nativeWindow: {} }); + configService = jasmine.createSpyObj('ConfigurationDataService', ['findByPropertyName']); TestBed.configureTestingModule({ imports: [MatomoTestingModule.forRoot()], @@ -34,6 +47,7 @@ describe('MatomoService', () => { { provide: MatomoInitializerService, useValue: matomoInitializer }, { provide: OrejimeService, useValue: orejimeService }, { provide: NativeWindowService, useValue: nativeWindowService }, + { provide: ConfigurationDataService, useValue: configService }, ], }); @@ -50,9 +64,23 @@ describe('MatomoService', () => { expect(nativeWindowService.nativeWindow.changeMatomoConsent).toBe(service.changeMatomoConsent); }); + 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(); + }); + it('should initialize tracker with correct parameters in production', () => { environment.production = true; - environment.matomo = { siteId: '1', trackerUrl: 'http://example.com' }; + configService.findByPropertyName.withArgs(MATOMO_TRACKER_URL).and.returnValue( + createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(),{ values: ['http://example.com'] })), + ); + configService.findByPropertyName.withArgs(MATOMO_SITE_ID).and.returnValue( + createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { values: ['1'] }))); orejimeService.getSavedPreferences.and.returnValue(of({ matomo: true })); service.init(); @@ -72,13 +100,17 @@ describe('MatomoService', () => { expect(matomoInitializer.initializeTracker).not.toHaveBeenCalled(); }); - it('should call setConsentGiven when consent is true', () => { - service.changeMatomoConsent(true); - expect(matomoTracker.setConsentGiven).toHaveBeenCalled(); + describe('with visitorId set', () => { + beforeEach(() => { + matomoTracker.getVisitorId.and.returnValue(Promise.resolve('12345')); + }); + + it('should add trackerId parameter', fakeAsync(() => { + service.appendVisitorId('http://example.com/') + .subscribe(url => expect(url).toEqual('http://example.com/?trackerId=12345')); + tick(); + })); + }); - it('should call forgetConsentGiven when consent is false', () => { - service.changeMatomoConsent(false); - expect(matomoTracker.forgetConsentGiven).toHaveBeenCalled(); - }); }); diff --git a/src/app/statistics/matomo.service.ts b/src/app/statistics/matomo.service.ts index 16f413517f..dab351e6b5 100644 --- a/src/app/statistics/matomo.service.ts +++ b/src/app/statistics/matomo.service.ts @@ -6,10 +6,29 @@ import { MatomoInitializerService, MatomoTracker, } from 'ngx-matomo-client'; +import { + combineLatest, + from as fromPromise, + Observable, + switchMap, +} from 'rxjs'; +import { + map, + take, + tap, +} from 'rxjs/operators'; import { environment } from '../../environments/environment'; +import { ConfigurationDataService } from '../core/data/configuration-data.service'; +import { RemoteData } from '../core/data/remote-data'; import { NativeWindowService } from '../core/services/window.service'; +import { ConfigurationProperty } from '../core/shared/configuration-property.model'; +import { getFirstCompletedRemoteData } from '../core/shared/operators'; import { OrejimeService } from '../shared/cookies/orejime.service'; +import { isNotEmpty } from '../shared/empty.util'; + +export const MATOMO_TRACKER_URL = 'matomo.tracker.url'; +export const MATOMO_SITE_ID = 'matomo.request.siteid'; /** * Service to manage Matomo analytics integration. @@ -18,6 +37,10 @@ import { OrejimeService } from '../shared/cookies/orejime.service'; @Injectable({ providedIn: 'root', }) +/** + * Service responsible for managing Matomo analytics tracking and consent. + * Provides methods for initializing tracking, managing consent, and appending visitor identifiers. + */ export class MatomoService { /** Injects the MatomoInitializerService to initialize the Matomo tracker. */ @@ -32,6 +55,9 @@ export class MatomoService { /** Injects the NativeWindowService to access the native window object. */ _window = inject(NativeWindowService); + /** Injects the ConfigurationService. */ + configService = inject(ConfigurationDataService); + /** * Initializes the Matomo tracker if in production environment. * Sets up the changeMatomoConsent function on the native window object. @@ -45,16 +71,16 @@ export class MatomoService { 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, - }); - } - }); + preferences$ + .pipe( + tap(preferences => this.changeMatomoConsent(preferences?.matomo)), + switchMap(_ => combineLatest([this.getSiteId$(), this.getTrackerUrl$()])), + ) + .subscribe(([siteId, trackerUrl]) => { + if (siteId && trackerUrl) { + this.matomoInitializer.initializeTracker({ siteId, trackerUrl }); + } + }); } } @@ -69,4 +95,59 @@ export class MatomoService { this.matomoTracker.forgetConsentGiven(); } }; + + /** + * Appends the Matomo visitor ID to the given URL. + * @param url - The original URL to which the visitor ID will be added. + * @returns An Observable that emits the URL with the visitor ID appended. + */ + appendVisitorId(url: string): Observable { + return fromPromise(this.matomoTracker.getVisitorId()) + .pipe( + map(visitorId => this.appendTrackerId(url, visitorId)), + take(1), + ); + } + + /** + * Retrieves the Matomo tracker URL from the configuration service. + * @returns An Observable that emits the Matomo tracker URL if available. + */ + getTrackerUrl$() { + return this.configService.findByPropertyName(MATOMO_TRACKER_URL) + .pipe( + getFirstCompletedRemoteData(), + map((res: RemoteData) => { + return res.hasSucceeded && res.payload && isNotEmpty(res.payload.values) && res.payload.values[0]; + }), + ); + } + + /** + * Retrieves the Matomo site ID from the configuration service. + * @returns An Observable that emits the Matomo site ID if available. + */ + getSiteId$() { + return this.configService.findByPropertyName(MATOMO_SITE_ID) + .pipe( + getFirstCompletedRemoteData(), + map((res: RemoteData) => { + return res.hasSucceeded && res.payload && isNotEmpty(res.payload.values) && res.payload.values[0]; + }), + ); + } + + /** + * Appends the visitor ID as a query parameter to the given URL. + * @param url - The original URL to modify + * @param visitorId - The visitor ID to append to the URL + * @returns The updated URL with the visitor ID added as a 'trackerId' query parameter + */ + private appendTrackerId(url: string, visitorId: string) { + const updatedURL = new URL(url); + if (visitorId != null) { + updatedURL.searchParams.append('trackerId', visitorId); + } + return updatedURL.toString(); + } } diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts index a52cd6be35..7f5f019958 100644 --- a/src/config/app-config.interface.ts +++ b/src/config/app-config.interface.ts @@ -24,7 +24,6 @@ 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'; @@ -67,7 +66,6 @@ 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 2ea08946fe..8b2eabacee 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -19,7 +19,6 @@ 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'; @@ -601,5 +600,4 @@ export class DefaultAppConfig implements AppConfig { isVisible: false, }; - matomo: MatomoConfig = {}; } diff --git a/src/config/matomo-config.interface.ts b/src/config/matomo-config.interface.ts deleted file mode 100644 index 95dc267268..0000000000 --- a/src/config/matomo-config.interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Config } from './config.interface'; - -/** - * Configuration interface for Matomo tracking - */ -export interface MatomoConfig extends Config { - trackerUrl?: string; - siteId?: string; -}