add referrer to pageview events

This commit is contained in:
Art Lowel
2023-05-02 17:09:55 +02:00
parent 9fc7b57157
commit 27d3c58d00
12 changed files with 244 additions and 13 deletions

View File

@@ -0,0 +1,53 @@
import { of as observableOf } from 'rxjs';
import { RouteService } from './route.service';
import { BrowserReferrerService } from './browser.referrer.service';
describe(`BrowserReferrerService`, () => {
let service: BrowserReferrerService;
const documentReferrer = 'https://www.referrer.com';
const origin = 'https://www.dspace.org';
let routeService: RouteService;
beforeEach(() => {
routeService = {
getPreviousUrl: () => observableOf('')
} as any;
service = new BrowserReferrerService(
{ referrer: documentReferrer },
routeService,
{ getCurrentOrigin: () => origin } as any
);
});
describe(`getReferrer`, () => {
let prevUrl: string;
describe(`when getPreviousUrl is an empty string`, () => {
beforeEach(() => {
prevUrl = '';
spyOn(routeService, 'getPreviousUrl').and.returnValue(observableOf(prevUrl));
});
it(`should return document.referrer`, (done: DoneFn) => {
service.getReferrer().subscribe((emittedReferrer: string) => {
expect(emittedReferrer).toBe(documentReferrer);
done();
});
});
});
describe(`when getPreviousUrl is not empty`, () => {
beforeEach(() => {
prevUrl = '/some/local/route';
spyOn(routeService, 'getPreviousUrl').and.returnValue(observableOf(prevUrl));
});
it(`should return the value emitted by getPreviousUrl combined with the origin from HardRedirectService`, (done: DoneFn) => {
service.getReferrer().subscribe((emittedReferrer: string) => {
expect(emittedReferrer).toBe(origin + prevUrl);
done();
});
});
});
});
});

View File

@@ -0,0 +1,48 @@
import { ReferrerService } from './referrer.service';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { isEmpty } from '../../shared/empty.util';
import { URLCombiner } from '../url-combiner/url-combiner';
import { Inject, Injectable } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { HardRedirectService } from './hard-redirect.service';
import { RouteService } from './route.service';
/**
* A service to determine the referrer
*
* The browser implementation will get the referrer from document.referrer, in the event that the
* previous page visited was not an angular URL. If it was, the route history in the store must be
* used, since document.referrer doesn't get updated on route changes
*/
@Injectable()
export class BrowserReferrerService extends ReferrerService {
constructor(
@Inject(DOCUMENT) protected document: any,
protected routeService: RouteService,
protected hardRedirectService: HardRedirectService,
) {
super();
}
/**
* Return the referrer
*
* Return the referrer URL based on the route history in the store. If there is no route history
* in the store yet, document.referrer will be used
*/
public getReferrer(): Observable<string> {
return this.routeService.getPreviousUrl().pipe(
map((prevUrl: string) => {
// if we don't have anything in the history yet, return document.referrer
// (note that that may be empty too, e.g. if you've just opened a new browser tab)
if (isEmpty(prevUrl)) {
return this.document.referrer;
} else {
return new URLCombiner(this.hardRedirectService.getCurrentOrigin(), prevUrl).toString();
}
})
);
}
}

View File

@@ -0,0 +1,15 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
/**
* A service to determine the referrer, i.e. the previous URL that led the user to the current one
*/
@Injectable()
export abstract class ReferrerService {
/**
* Return the referrer
*/
abstract getReferrer(): Observable<string>;
}

View File

@@ -0,0 +1,34 @@
import { ServerReferrerService } from './server.referrer.service';
describe(`ServerReferrerService`, () => {
let service: ServerReferrerService;
const referrer = 'https://www.referrer.com';
describe(`getReferrer`, () => {
describe(`when the referer header is set`, () => {
beforeEach(() => {
service = new ServerReferrerService({ headers: { referer: referrer }});
});
it(`should return the referer header`, (done: DoneFn) => {
service.getReferrer().subscribe((emittedReferrer: string) => {
expect(emittedReferrer).toBe(referrer);
done();
});
});
});
describe(`when the referer header is not set`, () => {
beforeEach(() => {
service = new ServerReferrerService({ headers: {}});
});
it(`should return an empty string`, (done: DoneFn) => {
service.getReferrer().subscribe((emittedReferrer: string) => {
expect(emittedReferrer).toBe('');
done();
});
});
});
});
});

View File

@@ -0,0 +1,31 @@
import { ReferrerService } from './referrer.service';
import { Observable, of as observableOf } from 'rxjs';
import { Inject, Injectable } from '@angular/core';
import { REQUEST } from '@nguniversal/express-engine/tokens';
/**
* A service to determine the referrer
*
* The server implementation will get the referrer from the 'Referer' header of the request sent to
* the express server
*/
@Injectable()
export class ServerReferrerService extends ReferrerService {
constructor(
@Inject(REQUEST) protected request: any,
) {
super();
}
/**
* Return the referrer
*
* Return the 'Referer' header from the request, or an empty string if the header wasn't set
* (for consistency with the document.referrer property on the browser side)
*/
public getReferrer(): Observable<string> {
const referrer = this.request.headers.referer || '';
return observableOf(referrer);
}
}

View File

@@ -11,7 +11,10 @@ describe('Angulartics2DSpace', () => {
beforeEach(() => {
angulartics2 = {
eventTrack: observableOf({action: 'pageView', properties: {object: 'mock-object'}}),
eventTrack: observableOf({action: 'pageView', properties: {
object: 'mock-object',
referrer: 'https://www.referrer.com'
}}),
filterDeveloperMode: () => filter(() => true)
} as any;
statisticsService = jasmine.createSpyObj('statisticsService', {trackViewEvent: null});
@@ -20,7 +23,7 @@ describe('Angulartics2DSpace', () => {
it('should use the statisticsService', () => {
provider.startTracking();
expect(statisticsService.trackViewEvent).toHaveBeenCalledWith('mock-object' as any);
expect(statisticsService.trackViewEvent).toHaveBeenCalledWith('mock-object' as any, 'https://www.referrer.com');
});
});

View File

@@ -25,7 +25,7 @@ export class Angulartics2DSpace {
private eventTrack(event) {
if (event.action === 'pageView') {
this.statisticsService.trackViewEvent(event.properties.object);
this.statisticsService.trackViewEvent(event.properties.object, event.properties.referrer);
} else if (event.action === 'search') {
this.statisticsService.trackSearchEvent(
event.properties.searchOptions,

View File

@@ -1,6 +1,10 @@
import { Component, Input, OnInit } from '@angular/core';
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
import { Angulartics2 } from 'angulartics2';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { Subscription } from 'rxjs/internal/Subscription';
import { take } from 'rxjs/operators';
import { hasValue } from '../../../shared/empty.util';
import { ReferrerService } from '../../../core/services/referrer.service';
/**
* This component triggers a page view statistic
@@ -10,18 +14,43 @@ import { DSpaceObject } from '../../../core/shared/dspace-object.model';
styleUrls: ['./view-tracker.component.scss'],
templateUrl: './view-tracker.component.html',
})
export class ViewTrackerComponent implements OnInit {
export class ViewTrackerComponent implements OnInit, OnDestroy {
/**
* The DSpaceObject to track a view event about
*/
@Input() object: DSpaceObject;
/**
* The subscription on this.referrerService.getReferrer()
* @protected
*/
protected sub: Subscription;
constructor(
public angulartics2: Angulartics2
public angulartics2: Angulartics2,
public referrerService: ReferrerService
) {
}
ngOnInit(): void {
this.sub = this.referrerService.getReferrer()
.pipe(take(1))
.subscribe((referrer: string) => {
this.angulartics2.eventTrack.next({
action: 'pageView',
properties: {object: this.object},
properties: {
object: this.object,
referrer
},
});
});
}
ngOnDestroy(): void {
// unsubscribe in the case that this component is destroyed before
// this.referrerService.getReferrer() has emitted
if (hasValue(this.sub)) {
this.sub.unsubscribe();
}
}
}

View File

@@ -26,12 +26,13 @@ describe('StatisticsService', () => {
it('should send a request to track an item view ', () => {
const mockItem: any = {uuid: 'mock-item-uuid', type: 'item'};
service.trackViewEvent(mockItem);
service.trackViewEvent(mockItem, 'https://www.referrer.com');
const request: TrackRequest = requestService.send.calls.mostRecent().args[0];
expect(request.body).toBeDefined('request.body');
const body = JSON.parse(request.body);
expect(body.targetId).toBe('mock-item-uuid');
expect(body.targetType).toBe('item');
expect(body.referrer).toBe('https://www.referrer.com');
});
});

View File

@@ -31,11 +31,16 @@ export class StatisticsService {
/**
* To track a page view
* @param dso: The dso which was viewed
* @param referrer: The referrer used by the client to reach the dso page
*/
trackViewEvent(dso: DSpaceObject) {
trackViewEvent(
dso: DSpaceObject,
referrer: string
) {
this.sendEvent('/statistics/viewevents', {
targetId: dso.uuid,
targetType: (dso as any).type
targetType: (dso as any).type,
referrer
});
}

View File

@@ -33,6 +33,8 @@ import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.se
import { RouterModule, NoPreloading } from '@angular/router';
import { AuthRequestService } from '../../app/core/auth/auth-request.service';
import { BrowserAuthRequestService } from '../../app/core/auth/browser-auth-request.service';
import { ReferrerService } from '../../app/core/services/referrer.service';
import { BrowserReferrerService } from '../../app/core/services/browser.referrer.service';
export const REQ_KEY = makeStateKey<string>('req');
@@ -111,6 +113,10 @@ export function getRequest(transferState: TransferState): any {
provide: AuthRequestService,
useClass: BrowserAuthRequestService,
},
{
provide: ReferrerService,
useClass: BrowserReferrerService,
},
{
provide: LocationToken,
useFactory: locationProvider,

View File

@@ -33,6 +33,8 @@ import { Angulartics2Mock } from '../../app/shared/mocks/angulartics2.service.mo
import { RouterModule } from '@angular/router';
import { AuthRequestService } from '../../app/core/auth/auth-request.service';
import { ServerAuthRequestService } from '../../app/core/auth/server-auth-request.service';
import { ReferrerService } from '../../app/core/services/referrer.service';
import { ServerReferrerService } from '../../app/core/services/server.referrer.service';
export function createTranslateLoader() {
return new TranslateJson5UniversalLoader('dist/server/assets/i18n/', '.json5');
@@ -102,6 +104,10 @@ export function createTranslateLoader() {
provide: HardRedirectService,
useClass: ServerHardRedirectService,
},
{
provide: ReferrerService,
useClass: ServerReferrerService,
},
]
})
export class ServerAppModule {