Skip to content

Commit 27d3c58

Browse files
committed
add referrer to pageview events
1 parent 9fc7b57 commit 27d3c58

12 files changed

Lines changed: 244 additions & 13 deletions
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { of as observableOf } from 'rxjs';
2+
import { RouteService } from './route.service';
3+
import { BrowserReferrerService } from './browser.referrer.service';
4+
5+
describe(`BrowserReferrerService`, () => {
6+
let service: BrowserReferrerService;
7+
const documentReferrer = 'https://www.referrer.com';
8+
const origin = 'https://www.dspace.org';
9+
let routeService: RouteService;
10+
11+
beforeEach(() => {
12+
routeService = {
13+
getPreviousUrl: () => observableOf('')
14+
} as any;
15+
service = new BrowserReferrerService(
16+
{ referrer: documentReferrer },
17+
routeService,
18+
{ getCurrentOrigin: () => origin } as any
19+
);
20+
});
21+
22+
describe(`getReferrer`, () => {
23+
let prevUrl: string;
24+
25+
describe(`when getPreviousUrl is an empty string`, () => {
26+
beforeEach(() => {
27+
prevUrl = '';
28+
spyOn(routeService, 'getPreviousUrl').and.returnValue(observableOf(prevUrl));
29+
});
30+
31+
it(`should return document.referrer`, (done: DoneFn) => {
32+
service.getReferrer().subscribe((emittedReferrer: string) => {
33+
expect(emittedReferrer).toBe(documentReferrer);
34+
done();
35+
});
36+
});
37+
});
38+
39+
describe(`when getPreviousUrl is not empty`, () => {
40+
beforeEach(() => {
41+
prevUrl = '/some/local/route';
42+
spyOn(routeService, 'getPreviousUrl').and.returnValue(observableOf(prevUrl));
43+
});
44+
45+
it(`should return the value emitted by getPreviousUrl combined with the origin from HardRedirectService`, (done: DoneFn) => {
46+
service.getReferrer().subscribe((emittedReferrer: string) => {
47+
expect(emittedReferrer).toBe(origin + prevUrl);
48+
done();
49+
});
50+
});
51+
});
52+
});
53+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { ReferrerService } from './referrer.service';
2+
import { Observable } from 'rxjs';
3+
import { map } from 'rxjs/operators';
4+
import { isEmpty } from '../../shared/empty.util';
5+
import { URLCombiner } from '../url-combiner/url-combiner';
6+
import { Inject, Injectable } from '@angular/core';
7+
import { DOCUMENT } from '@angular/common';
8+
import { HardRedirectService } from './hard-redirect.service';
9+
import { RouteService } from './route.service';
10+
11+
/**
12+
* A service to determine the referrer
13+
*
14+
* The browser implementation will get the referrer from document.referrer, in the event that the
15+
* previous page visited was not an angular URL. If it was, the route history in the store must be
16+
* used, since document.referrer doesn't get updated on route changes
17+
*/
18+
@Injectable()
19+
export class BrowserReferrerService extends ReferrerService {
20+
21+
constructor(
22+
@Inject(DOCUMENT) protected document: any,
23+
protected routeService: RouteService,
24+
protected hardRedirectService: HardRedirectService,
25+
) {
26+
super();
27+
}
28+
29+
/**
30+
* Return the referrer
31+
*
32+
* Return the referrer URL based on the route history in the store. If there is no route history
33+
* in the store yet, document.referrer will be used
34+
*/
35+
public getReferrer(): Observable<string> {
36+
return this.routeService.getPreviousUrl().pipe(
37+
map((prevUrl: string) => {
38+
// if we don't have anything in the history yet, return document.referrer
39+
// (note that that may be empty too, e.g. if you've just opened a new browser tab)
40+
if (isEmpty(prevUrl)) {
41+
return this.document.referrer;
42+
} else {
43+
return new URLCombiner(this.hardRedirectService.getCurrentOrigin(), prevUrl).toString();
44+
}
45+
})
46+
);
47+
}
48+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Injectable } from '@angular/core';
2+
import { Observable } from 'rxjs';
3+
4+
/**
5+
* A service to determine the referrer, i.e. the previous URL that led the user to the current one
6+
*/
7+
@Injectable()
8+
export abstract class ReferrerService {
9+
10+
/**
11+
* Return the referrer
12+
*/
13+
abstract getReferrer(): Observable<string>;
14+
15+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { ServerReferrerService } from './server.referrer.service';
2+
3+
describe(`ServerReferrerService`, () => {
4+
let service: ServerReferrerService;
5+
const referrer = 'https://www.referrer.com';
6+
7+
describe(`getReferrer`, () => {
8+
describe(`when the referer header is set`, () => {
9+
beforeEach(() => {
10+
service = new ServerReferrerService({ headers: { referer: referrer }});
11+
});
12+
13+
it(`should return the referer header`, (done: DoneFn) => {
14+
service.getReferrer().subscribe((emittedReferrer: string) => {
15+
expect(emittedReferrer).toBe(referrer);
16+
done();
17+
});
18+
});
19+
});
20+
21+
describe(`when the referer header is not set`, () => {
22+
beforeEach(() => {
23+
service = new ServerReferrerService({ headers: {}});
24+
});
25+
26+
it(`should return an empty string`, (done: DoneFn) => {
27+
service.getReferrer().subscribe((emittedReferrer: string) => {
28+
expect(emittedReferrer).toBe('');
29+
done();
30+
});
31+
});
32+
});
33+
});
34+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { ReferrerService } from './referrer.service';
2+
import { Observable, of as observableOf } from 'rxjs';
3+
import { Inject, Injectable } from '@angular/core';
4+
import { REQUEST } from '@nguniversal/express-engine/tokens';
5+
6+
/**
7+
* A service to determine the referrer
8+
*
9+
* The server implementation will get the referrer from the 'Referer' header of the request sent to
10+
* the express server
11+
*/
12+
@Injectable()
13+
export class ServerReferrerService extends ReferrerService {
14+
15+
constructor(
16+
@Inject(REQUEST) protected request: any,
17+
) {
18+
super();
19+
}
20+
21+
/**
22+
* Return the referrer
23+
*
24+
* Return the 'Referer' header from the request, or an empty string if the header wasn't set
25+
* (for consistency with the document.referrer property on the browser side)
26+
*/
27+
public getReferrer(): Observable<string> {
28+
const referrer = this.request.headers.referer || '';
29+
return observableOf(referrer);
30+
}
31+
}

src/app/statistics/angulartics/dspace-provider.spec.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ describe('Angulartics2DSpace', () => {
1111

1212
beforeEach(() => {
1313
angulartics2 = {
14-
eventTrack: observableOf({action: 'pageView', properties: {object: 'mock-object'}}),
14+
eventTrack: observableOf({action: 'pageView', properties: {
15+
object: 'mock-object',
16+
referrer: 'https://www.referrer.com'
17+
}}),
1518
filterDeveloperMode: () => filter(() => true)
1619
} as any;
1720
statisticsService = jasmine.createSpyObj('statisticsService', {trackViewEvent: null});
@@ -20,7 +23,7 @@ describe('Angulartics2DSpace', () => {
2023

2124
it('should use the statisticsService', () => {
2225
provider.startTracking();
23-
expect(statisticsService.trackViewEvent).toHaveBeenCalledWith('mock-object' as any);
26+
expect(statisticsService.trackViewEvent).toHaveBeenCalledWith('mock-object' as any, 'https://www.referrer.com');
2427
});
2528

2629
});

src/app/statistics/angulartics/dspace-provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export class Angulartics2DSpace {
2525

2626
private eventTrack(event) {
2727
if (event.action === 'pageView') {
28-
this.statisticsService.trackViewEvent(event.properties.object);
28+
this.statisticsService.trackViewEvent(event.properties.object, event.properties.referrer);
2929
} else if (event.action === 'search') {
3030
this.statisticsService.trackSearchEvent(
3131
event.properties.searchOptions,
Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
import { Component, Input, OnInit } from '@angular/core';
1+
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
22
import { Angulartics2 } from 'angulartics2';
33
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
4+
import { Subscription } from 'rxjs/internal/Subscription';
5+
import { take } from 'rxjs/operators';
6+
import { hasValue } from '../../../shared/empty.util';
7+
import { ReferrerService } from '../../../core/services/referrer.service';
48

59
/**
610
* This component triggers a page view statistic
@@ -10,18 +14,43 @@ import { DSpaceObject } from '../../../core/shared/dspace-object.model';
1014
styleUrls: ['./view-tracker.component.scss'],
1115
templateUrl: './view-tracker.component.html',
1216
})
13-
export class ViewTrackerComponent implements OnInit {
17+
export class ViewTrackerComponent implements OnInit, OnDestroy {
18+
/**
19+
* The DSpaceObject to track a view event about
20+
*/
1421
@Input() object: DSpaceObject;
1522

23+
/**
24+
* The subscription on this.referrerService.getReferrer()
25+
* @protected
26+
*/
27+
protected sub: Subscription;
28+
1629
constructor(
17-
public angulartics2: Angulartics2
30+
public angulartics2: Angulartics2,
31+
public referrerService: ReferrerService
1832
) {
1933
}
2034

2135
ngOnInit(): void {
22-
this.angulartics2.eventTrack.next({
23-
action: 'pageView',
24-
properties: {object: this.object},
25-
});
36+
this.sub = this.referrerService.getReferrer()
37+
.pipe(take(1))
38+
.subscribe((referrer: string) => {
39+
this.angulartics2.eventTrack.next({
40+
action: 'pageView',
41+
properties: {
42+
object: this.object,
43+
referrer
44+
},
45+
});
46+
});
47+
}
48+
49+
ngOnDestroy(): void {
50+
// unsubscribe in the case that this component is destroyed before
51+
// this.referrerService.getReferrer() has emitted
52+
if (hasValue(this.sub)) {
53+
this.sub.unsubscribe();
54+
}
2655
}
2756
}

src/app/statistics/statistics.service.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,13 @@ describe('StatisticsService', () => {
2626

2727
it('should send a request to track an item view ', () => {
2828
const mockItem: any = {uuid: 'mock-item-uuid', type: 'item'};
29-
service.trackViewEvent(mockItem);
29+
service.trackViewEvent(mockItem, 'https://www.referrer.com');
3030
const request: TrackRequest = requestService.send.calls.mostRecent().args[0];
3131
expect(request.body).toBeDefined('request.body');
3232
const body = JSON.parse(request.body);
3333
expect(body.targetId).toBe('mock-item-uuid');
3434
expect(body.targetType).toBe('item');
35+
expect(body.referrer).toBe('https://www.referrer.com');
3536
});
3637
});
3738

src/app/statistics/statistics.service.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,16 @@ export class StatisticsService {
3131
/**
3232
* To track a page view
3333
* @param dso: The dso which was viewed
34+
* @param referrer: The referrer used by the client to reach the dso page
3435
*/
35-
trackViewEvent(dso: DSpaceObject) {
36+
trackViewEvent(
37+
dso: DSpaceObject,
38+
referrer: string
39+
) {
3640
this.sendEvent('/statistics/viewevents', {
3741
targetId: dso.uuid,
38-
targetType: (dso as any).type
42+
targetType: (dso as any).type,
43+
referrer
3944
});
4045
}
4146

0 commit comments

Comments
 (0)