Skip to content

Commit 8086c22

Browse files
authored
Merge pull request #4018 from 4Science/task/main/CST-18964
Add support for Matomo
2 parents 1e4864f + 4ba9a4b commit 8086c22

20 files changed

+490
-21
lines changed

package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@
150150
"ng2-file-upload": "7.0.1",
151151
"ng2-nouislider": "^2.0.0",
152152
"ngx-infinite-scroll": "^18.0.0",
153+
"ngx-matomo-client": "^6.4.1",
153154
"ngx-pagination": "6.0.3",
154155
"ngx-skeleton-loader": "^9.0.0",
155156
"ngx-ui-switch": "^15.0.0",

src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { CommonModule } from '@angular/common';
1+
import {
2+
CommonModule,
3+
Location,
4+
} from '@angular/common';
25
import { PLATFORM_ID } from '@angular/core';
36
import {
47
ComponentFixture,
@@ -14,13 +17,15 @@ import { of as observableOf } from 'rxjs';
1417

1518
import { getForbiddenRoute } from '../../app-routing-paths';
1619
import { AuthService } from '../../core/auth/auth.service';
20+
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
1721
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
1822
import { SignpostingDataService } from '../../core/data/signposting-data.service';
1923
import { HardRedirectService } from '../../core/services/hard-redirect.service';
2024
import { ServerResponseService } from '../../core/services/server-response.service';
2125
import { Bitstream } from '../../core/shared/bitstream.model';
2226
import { FileService } from '../../core/shared/file.service';
2327
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
28+
import { MatomoService } from '../../statistics/matomo.service';
2429
import { BitstreamDownloadPageComponent } from './bitstream-download-page.component';
2530

2631
describe('BitstreamDownloadPageComponent', () => {
@@ -33,10 +38,13 @@ describe('BitstreamDownloadPageComponent', () => {
3338
let hardRedirectService: HardRedirectService;
3439
let activatedRoute;
3540
let router;
41+
let location: Location;
42+
let dsoNameService: DSONameService;
3643

3744
let bitstream: Bitstream;
3845
let serverResponseService: jasmine.SpyObj<ServerResponseService>;
3946
let signpostingDataService: jasmine.SpyObj<SignpostingDataService>;
47+
let matomoService: jasmine.SpyObj<MatomoService>;
4048

4149
const mocklink = {
4250
href: 'http://test.org',
@@ -54,6 +62,7 @@ describe('BitstreamDownloadPageComponent', () => {
5462
authService = jasmine.createSpyObj('authService', {
5563
isAuthenticated: observableOf(true),
5664
setRedirectUrl: {},
65+
getShortlivedToken: observableOf('token'),
5766
});
5867
authorizationService = jasmine.createSpyObj('authorizationSerivice', {
5968
isAuthorized: observableOf(true),
@@ -63,9 +72,18 @@ describe('BitstreamDownloadPageComponent', () => {
6372
retrieveFileDownloadLink: observableOf('content-url-with-headers'),
6473
});
6574

66-
hardRedirectService = jasmine.createSpyObj('fileService', {
75+
hardRedirectService = jasmine.createSpyObj('hardRedirectService', {
6776
redirect: {},
6877
});
78+
79+
location = jasmine.createSpyObj('location', {
80+
back: {},
81+
});
82+
83+
dsoNameService = jasmine.createSpyObj('dsoNameService', {
84+
getName: 'Test Bitstream',
85+
});
86+
6987
bitstream = Object.assign(new Bitstream(), {
7088
uuid: 'bitstreamUuid',
7189
_links: {
@@ -94,6 +112,8 @@ describe('BitstreamDownloadPageComponent', () => {
94112
signpostingDataService = jasmine.createSpyObj('SignpostingDataService', {
95113
getLinks: observableOf([mocklink, mocklink2]),
96114
});
115+
matomoService = jasmine.createSpyObj('MatomoService', ['appendVisitorId']);
116+
matomoService.appendVisitorId.and.callFake((link) => observableOf(link));
97117
}
98118

99119
function initTestbed() {
@@ -108,7 +128,10 @@ describe('BitstreamDownloadPageComponent', () => {
108128
{ provide: HardRedirectService, useValue: hardRedirectService },
109129
{ provide: ServerResponseService, useValue: serverResponseService },
110130
{ provide: SignpostingDataService, useValue: signpostingDataService },
131+
{ provide: MatomoService, useValue: matomoService },
111132
{ provide: PLATFORM_ID, useValue: 'server' },
133+
{ provide: Location, useValue: location },
134+
{ provide: DSONameService, useValue: dsoNameService },
112135
],
113136
})
114137
.compileComponents();
@@ -142,9 +165,11 @@ describe('BitstreamDownloadPageComponent', () => {
142165
component = fixture.componentInstance;
143166
fixture.detectChanges();
144167
});
145-
it('should redirect to the content link', () => {
146-
expect(hardRedirectService.redirect).toHaveBeenCalledWith('bitstream-content-link');
147-
});
168+
it('should redirect to the content link', waitForAsync(() => {
169+
fixture.whenStable().then(() => {
170+
expect(hardRedirectService.redirect).toHaveBeenCalledWith('bitstream-content-link');
171+
});
172+
}));
148173
it('should add the signposting links', () => {
149174
expect(serverResponseService.setHeader).toHaveBeenCalled();
150175
});
@@ -159,9 +184,11 @@ describe('BitstreamDownloadPageComponent', () => {
159184
component = fixture.componentInstance;
160185
fixture.detectChanges();
161186
});
162-
it('should redirect to an updated content link', () => {
163-
expect(hardRedirectService.redirect).toHaveBeenCalledWith('content-url-with-headers');
164-
});
187+
it('should redirect to an updated content link', waitForAsync(() => {
188+
fixture.whenStable().then(() => {
189+
expect(hardRedirectService.redirect).toHaveBeenCalledWith('content-url-with-headers');
190+
});
191+
}));
165192
});
166193
describe('when the user is not authorized and logged in', () => {
167194
beforeEach(waitForAsync(() => {
@@ -174,9 +201,11 @@ describe('BitstreamDownloadPageComponent', () => {
174201
component = fixture.componentInstance;
175202
fixture.detectChanges();
176203
});
177-
it('should navigate to the forbidden route', () => {
178-
expect(router.navigateByUrl).toHaveBeenCalledWith(getForbiddenRoute(), { skipLocationChange: true });
179-
});
204+
it('should navigate to the forbidden route', waitForAsync(() => {
205+
fixture.whenStable().then(() => {
206+
expect(router.navigateByUrl).toHaveBeenCalledWith(getForbiddenRoute(), { skipLocationChange: true });
207+
});
208+
}));
180209
});
181210
describe('when the user is not authorized and not logged in', () => {
182211
beforeEach(waitForAsync(() => {
@@ -190,10 +219,12 @@ describe('BitstreamDownloadPageComponent', () => {
190219
component = fixture.componentInstance;
191220
fixture.detectChanges();
192221
});
193-
it('should navigate to the login page', () => {
194-
expect(authService.setRedirectUrl).toHaveBeenCalled();
195-
expect(router.navigateByUrl).toHaveBeenCalledWith('login');
196-
});
222+
it('should navigate to the login page', waitForAsync(() => {
223+
fixture.whenStable().then(() => {
224+
expect(authService.setRedirectUrl).toHaveBeenCalled();
225+
expect(router.navigateByUrl).toHaveBeenCalledWith('login');
226+
});
227+
}));
197228
});
198229
});
199230
});

src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
hasValue,
4646
isNotEmpty,
4747
} from '../../shared/empty.util';
48+
import { MatomoService } from '../../statistics/matomo.service';
4849

4950
@Component({
5051
selector: 'ds-bitstream-download-page',
@@ -74,6 +75,7 @@ export class BitstreamDownloadPageComponent implements OnInit {
7475
public dsoNameService: DSONameService,
7576
private signpostingDataService: SignpostingDataService,
7677
private responseService: ServerResponseService,
78+
private matomoService: MatomoService,
7779
@Inject(PLATFORM_ID) protected platformId: string,
7880
) {
7981
this.initPageLinks();
@@ -116,14 +118,20 @@ export class BitstreamDownloadPageComponent implements OnInit {
116118
} else if (hasValue(accessToken)) {
117119
return [[isAuthorized, !isLoggedIn, bitstream, '', accessToken]];
118120
} else {
119-
return [[isAuthorized, isLoggedIn, bitstream, '']];
121+
return [[isAuthorized, isLoggedIn, bitstream, bitstream._links.content.href]];
120122
}
121123
}),
124+
switchMap(([isAuthorized, isLoggedIn, bitstream, fileLink, accessToken]: [boolean, boolean, Bitstream, string, string]) =>
125+
this.matomoService.appendVisitorId(fileLink)
126+
.pipe(
127+
map((fileLinkWithVisitorId) => [isAuthorized, isLoggedIn, bitstream, fileLinkWithVisitorId, accessToken]),
128+
),
129+
),
122130
).subscribe(([isAuthorized, isLoggedIn, bitstream, fileLink, accessToken]: [boolean, boolean, Bitstream, string, string]) => {
123131
if (isAuthorized && isLoggedIn && isNotEmpty(fileLink)) {
124132
this.hardRedirectService.redirect(fileLink);
125133
} else if (isAuthorized && !isLoggedIn && !hasValue(accessToken)) {
126-
this.hardRedirectService.redirect(bitstream._links.content.href);
134+
this.hardRedirectService.redirect(fileLink);
127135
} else if (!isAuthorized) {
128136
// Either we have an access token, or we are logged in, or we are not logged in.
129137
// For now, the access token does not care if we are logged in or not.

src/app/core/shared/search/search.service.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { SearchService } from './search.service';
3737
import { SearchConfigurationService } from './search-configuration.service';
3838
import anything = jasmine.anything;
3939

40+
4041
@Component({
4142
template: '',
4243
standalone: true,

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ export class SearchService {
367367
const appliedFilter = appliedFilters[i];
368368
filters.push(appliedFilter);
369369
}
370-
this.angulartics2.eventTrack.next({
370+
const searchTrackObject = {
371371
action: 'search',
372372
properties: {
373373
searchOptions: config,
@@ -384,7 +384,9 @@ export class SearchService {
384384
filters: filters,
385385
clickedObject,
386386
},
387-
});
387+
};
388+
389+
this.angulartics2.eventTrack.next(searchTrackObject);
388390
}
389391

390392
/**

src/app/shared/cookies/browser-orejime.service.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { ANONYMOUS_STORAGE_NAME_OREJIME } from './orejime-configuration';
2828
describe('BrowserOrejimeService', () => {
2929
const trackingIdProp = 'google.analytics.key';
3030
const trackingIdTestValue = 'mock-tracking-id';
31+
const matomoTrackingId = 'matomo-tracking-id';
3132
const googleAnalytics = 'google-analytics';
3233
const recaptchaProp = 'registration.verification.enabled';
3334
const recaptchaValue = 'true';
@@ -310,9 +311,11 @@ describe('BrowserOrejimeService', () => {
310311
describe('initialize google analytics configuration', () => {
311312
let GOOGLE_ANALYTICS_KEY;
312313
let REGISTRATION_VERIFICATION_ENABLED_KEY;
314+
let MATOMO_ENABLED;
313315
beforeEach(() => {
314316
GOOGLE_ANALYTICS_KEY = clone((service as any).GOOGLE_ANALYTICS_KEY);
315317
REGISTRATION_VERIFICATION_ENABLED_KEY = clone((service as any).REGISTRATION_VERIFICATION_ENABLED_KEY);
318+
MATOMO_ENABLED = clone((service as any).MATOMO_ENABLED);
316319
spyOn((service as any), 'getUser$').and.returnValue(observableOf(user));
317320
translateService.get.and.returnValue(observableOf('loading...'));
318321
spyOn(service, 'addAppMessages');
@@ -361,6 +364,15 @@ describe('BrowserOrejimeService', () => {
361364
name: trackingIdTestValue,
362365
values: ['false'],
363366
}),
367+
)
368+
.withArgs(MATOMO_ENABLED)
369+
.and
370+
.returnValue(
371+
createSuccessfulRemoteDataObject$({
372+
... new ConfigurationProperty(),
373+
name: matomoTrackingId,
374+
values: ['false'],
375+
}),
364376
);
365377

366378
service.initialize();
@@ -380,6 +392,15 @@ describe('BrowserOrejimeService', () => {
380392
name: trackingIdTestValue,
381393
values: ['false'],
382394
}),
395+
)
396+
.withArgs(MATOMO_ENABLED)
397+
.and
398+
.returnValue(
399+
createSuccessfulRemoteDataObject$({
400+
... new ConfigurationProperty(),
401+
name: matomoTrackingId,
402+
values: ['false'],
403+
}),
383404
);
384405
service.initialize();
385406
expect(service.orejimeConfig.apps).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));
@@ -398,6 +419,15 @@ describe('BrowserOrejimeService', () => {
398419
name: trackingIdTestValue,
399420
values: ['false'],
400421
}),
422+
)
423+
.withArgs(MATOMO_ENABLED)
424+
.and
425+
.returnValue(
426+
createSuccessfulRemoteDataObject$({
427+
... new ConfigurationProperty(),
428+
name: matomoTrackingId,
429+
values: ['false'],
430+
}),
401431
);
402432
service.initialize();
403433
expect(service.orejimeConfig.apps).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { OrejimeService } from './orejime.service';
3939
import {
4040
ANONYMOUS_STORAGE_NAME_OREJIME,
4141
getOrejimeConfiguration,
42+
MATOMO_OREJIME_KEY,
4243
} from './orejime-configuration';
4344

4445
/**
@@ -89,6 +90,7 @@ export class BrowserOrejimeService extends OrejimeService {
8990

9091
private readonly GOOGLE_ANALYTICS_SERVICE_NAME = 'google-analytics';
9192

93+
private readonly MATOMO_ENABLED = 'matomo.enabled';
9294

9395
/**
9496
* Initial Orejime configuration
@@ -133,15 +135,26 @@ export class BrowserOrejimeService extends OrejimeService {
133135
),
134136
);
135137

136-
const appsToHide$: Observable<string[]> = observableCombineLatest([hideGoogleAnalytics$, hideRegistrationVerification$]).pipe(
137-
map(([hideGoogleAnalytics, hideRegistrationVerification]) => {
138+
const hideMatomo$ =
139+
this.configService.findByPropertyName(this.MATOMO_ENABLED).pipe(
140+
getFirstCompletedRemoteData(),
141+
map((remoteData) =>
142+
!remoteData.hasSucceeded || !remoteData.payload || isEmpty(remoteData.payload.values) || remoteData.payload.values[0].toLowerCase() !== 'true',
143+
),
144+
);
145+
146+
const appsToHide$: Observable<string[]> = observableCombineLatest([hideGoogleAnalytics$, hideRegistrationVerification$, hideMatomo$]).pipe(
147+
map(([hideGoogleAnalytics, hideRegistrationVerification, hideMatomo]) => {
138148
const appsToHideArray: string[] = [];
139149
if (hideGoogleAnalytics) {
140150
appsToHideArray.push(this.GOOGLE_ANALYTICS_SERVICE_NAME);
141151
}
142152
if (hideRegistrationVerification) {
143153
appsToHideArray.push(CAPTCHA_NAME);
144154
}
155+
if (hideMatomo) {
156+
appsToHideArray.push(MATOMO_OREJIME_KEY);
157+
}
145158
return appsToHideArray;
146159
}),
147160
);

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ export const ANONYMOUS_STORAGE_NAME_OREJIME = 'orejime-anonymous';
1919

2020
export const GOOGLE_ANALYTICS_OREJIME_KEY = 'google-analytics';
2121

22+
export const MATOMO_OREJIME_KEY = 'matomo';
23+
24+
export const MATOMO_COOKIE = 'dsMatomo';
25+
2226
/**
2327
* Orejime configuration
2428
* For more information see https://github.com/empreinte-digitale/orejime
@@ -134,6 +138,17 @@ export function getOrejimeConfiguration(_window: NativeWindowRef): any {
134138
HAS_AGREED_END_USER,
135139
],
136140
},
141+
{
142+
name: MATOMO_OREJIME_KEY,
143+
purposes: ['statistical'],
144+
required: false,
145+
cookies: [
146+
MATOMO_COOKIE,
147+
],
148+
callback: (consent: boolean) => {
149+
_window?.nativeWindow.changeMatomoConsent(consent);
150+
},
151+
},
137152
{
138153
name: GOOGLE_ANALYTICS_OREJIME_KEY,
139154
purposes: ['statistical'],

0 commit comments

Comments
 (0)