From 93f8bd30a1b87185501db19f8fbdf9523c6ed097 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Mon, 9 Mar 2026 16:06:36 +0100 Subject: [PATCH 1/2] fix: include dc_identifier in ViewTrackerResolverService event properties for Matomo custom dimensions --- .../view-tracker-resolver.service.spec.ts | 144 ++++++++++++++++++ .../dspace/view-tracker-resolver.service.ts | 5 +- 2 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 src/app/statistics/angulartics/dspace/view-tracker-resolver.service.spec.ts diff --git a/src/app/statistics/angulartics/dspace/view-tracker-resolver.service.spec.ts b/src/app/statistics/angulartics/dspace/view-tracker-resolver.service.spec.ts new file mode 100644 index 00000000000..3d2882b2e7b --- /dev/null +++ b/src/app/statistics/angulartics/dspace/view-tracker-resolver.service.spec.ts @@ -0,0 +1,144 @@ +import { ViewTrackerResolverService } from './view-tracker-resolver.service'; +import { Angulartics2 } from 'angulartics2'; +import { ReferrerService } from '../../../core/services/referrer.service'; +import { ActivatedRouteSnapshot, ResolveEnd, Router, RouterStateSnapshot } from '@angular/router'; +import { of as observableOf, Subject } from 'rxjs'; + +describe('ViewTrackerResolverService', () => { + let service: ViewTrackerResolverService; + let angulartics2: Angulartics2; + let referrerService: jasmine.SpyObj; + let router: any; + let routerEvents$: Subject; + + const mockDso = { + firstMetadataValue: jasmine.createSpy('firstMetadataValue').and.returnValue('http://hdl.handle.net/123456789/1'), + }; + + const mockReferrer = 'https://www.referrer.com'; + + beforeEach(() => { + routerEvents$ = new Subject(); + + angulartics2 = { + eventTrack: new Subject(), + } as any; + + referrerService = jasmine.createSpyObj('ReferrerService', ['getReferrer']); + referrerService.getReferrer.and.returnValue(observableOf(mockReferrer)); + + router = { + events: routerEvents$.asObservable(), + }; + + service = new ViewTrackerResolverService(angulartics2, referrerService, router); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should include dc_identifier in event properties when dso has dc.identifier.uri metadata', () => { + const routeSnapshot = { + data: { + dso: { + payload: mockDso, + }, + }, + } as any as ActivatedRouteSnapshot; + const stateSnapshot = {} as RouterStateSnapshot; + + let emittedEvent: any; + (angulartics2.eventTrack as Subject).subscribe(event => { + emittedEvent = event; + }); + + service.resolve(routeSnapshot, stateSnapshot); + + // Simulate ResolveEnd event to trigger the subscription + routerEvents$.next(new ResolveEnd(1, '/', '/', {} as any)); + + expect(emittedEvent).toBeDefined(); + expect(emittedEvent.action).toBe('page_view'); + expect(emittedEvent.properties.object).toBe(mockDso); + expect(emittedEvent.properties.referrer).toBe(mockReferrer); + expect(emittedEvent.properties.dc_identifier).toBe('http://hdl.handle.net/123456789/1'); + expect(mockDso.firstMetadataValue).toHaveBeenCalledWith('dc.identifier.uri'); + }); + + it('should set dc_identifier to undefined when dso does not have dc.identifier.uri metadata', () => { + const mockDsoWithoutMetadata = { + firstMetadataValue: jasmine.createSpy('firstMetadataValue').and.returnValue(undefined), + }; + + const routeSnapshot = { + data: { + dso: { + payload: mockDsoWithoutMetadata, + }, + }, + } as any as ActivatedRouteSnapshot; + const stateSnapshot = {} as RouterStateSnapshot; + + let emittedEvent: any; + (angulartics2.eventTrack as Subject).subscribe(event => { + emittedEvent = event; + }); + + service.resolve(routeSnapshot, stateSnapshot); + routerEvents$.next(new ResolveEnd(1, '/', '/', {} as any)); + + expect(emittedEvent).toBeDefined(); + expect(emittedEvent.action).toBe('page_view'); + expect(emittedEvent.properties.dc_identifier).toBeUndefined(); + }); + + it('should handle missing dso gracefully (dc_identifier undefined)', () => { + const routeSnapshot = { + data: { + dso: { + payload: undefined, + }, + }, + } as any as ActivatedRouteSnapshot; + const stateSnapshot = {} as RouterStateSnapshot; + + let emittedEvent: any; + (angulartics2.eventTrack as Subject).subscribe(event => { + emittedEvent = event; + }); + + service.resolve(routeSnapshot, stateSnapshot); + routerEvents$.next(new ResolveEnd(1, '/', '/', {} as any)); + + expect(emittedEvent).toBeDefined(); + expect(emittedEvent.action).toBe('page_view'); + expect(emittedEvent.properties.dc_identifier).toBeUndefined(); + }); + + it('should use custom dsoPath from route data when provided', () => { + const mockDsoCustom = { + firstMetadataValue: jasmine.createSpy('firstMetadataValue').and.returnValue('http://hdl.handle.net/custom/42'), + }; + + const routeSnapshot = { + data: { + dsoPath: 'myCustomDso', + myCustomDso: mockDsoCustom, + }, + } as any as ActivatedRouteSnapshot; + const stateSnapshot = {} as RouterStateSnapshot; + + let emittedEvent: any; + (angulartics2.eventTrack as Subject).subscribe(event => { + emittedEvent = event; + }); + + service.resolve(routeSnapshot, stateSnapshot); + routerEvents$.next(new ResolveEnd(1, '/', '/', {} as any)); + + expect(emittedEvent).toBeDefined(); + expect(emittedEvent.properties.object).toBe(mockDsoCustom); + expect(emittedEvent.properties.dc_identifier).toBe('http://hdl.handle.net/custom/42'); + }); +}); diff --git a/src/app/statistics/angulartics/dspace/view-tracker-resolver.service.ts b/src/app/statistics/angulartics/dspace/view-tracker-resolver.service.ts index fc23c09c51f..40afc6d2c5c 100644 --- a/src/app/statistics/angulartics/dspace/view-tracker-resolver.service.ts +++ b/src/app/statistics/angulartics/dspace/view-tracker-resolver.service.ts @@ -29,11 +29,14 @@ export class ViewTrackerResolverService { switchMap(() => this.referrerService.getReferrer().pipe(take(1)))) .subscribe((referrer: string) => { + const object = this.getNestedProperty(routeSnapshot.data, dsoPath); + const dc_identifier = object?.firstMetadataValue?.('dc.identifier.uri'); this.angulartics2.eventTrack.next({ action: 'page_view', properties: { - object: this.getNestedProperty(routeSnapshot.data, dsoPath), + object, referrer, + dc_identifier, }, }); }); From 58778158381243d54db26f0fe6b17ac76655eeae Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Tue, 10 Mar 2026 08:49:46 +0100 Subject: [PATCH 2/2] Fixed linting error --- .../dspace/view-tracker-resolver.service.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/statistics/angulartics/dspace/view-tracker-resolver.service.spec.ts b/src/app/statistics/angulartics/dspace/view-tracker-resolver.service.spec.ts index 3d2882b2e7b..d1b598c7025 100644 --- a/src/app/statistics/angulartics/dspace/view-tracker-resolver.service.spec.ts +++ b/src/app/statistics/angulartics/dspace/view-tracker-resolver.service.spec.ts @@ -1,7 +1,7 @@ import { ViewTrackerResolverService } from './view-tracker-resolver.service'; import { Angulartics2 } from 'angulartics2'; import { ReferrerService } from '../../../core/services/referrer.service'; -import { ActivatedRouteSnapshot, ResolveEnd, Router, RouterStateSnapshot } from '@angular/router'; +import { ActivatedRouteSnapshot, ResolveEnd, RouterStateSnapshot } from '@angular/router'; import { of as observableOf, Subject } from 'rxjs'; describe('ViewTrackerResolverService', () => { @@ -10,15 +10,15 @@ describe('ViewTrackerResolverService', () => { let referrerService: jasmine.SpyObj; let router: any; let routerEvents$: Subject; - - const mockDso = { - firstMetadataValue: jasmine.createSpy('firstMetadataValue').and.returnValue('http://hdl.handle.net/123456789/1'), - }; + let mockDso: { firstMetadataValue: jasmine.Spy }; const mockReferrer = 'https://www.referrer.com'; beforeEach(() => { routerEvents$ = new Subject(); + mockDso = { + firstMetadataValue: jasmine.createSpy('firstMetadataValue').and.returnValue('http://hdl.handle.net/123456789/1'), + }; angulartics2 = { eventTrack: new Subject(),