Skip to content

Commit fbacebb

Browse files
Merge branch 'w2p-100414_Missing-search-result-statistics-7.2' into w2p-100414_Missing-search-result-statistics-7.4
2 parents ca86437 + cf12fb2 commit fbacebb

3 files changed

Lines changed: 127 additions & 33 deletions

File tree

Lines changed: 119 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { Component, Inject, OnInit } from '@angular/core';
1+
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
22
import { Angulartics2 } from 'angulartics2';
3-
import { map, switchMap } from 'rxjs/operators';
3+
import { filter, map, switchMap, take } from 'rxjs/operators';
44
import { SearchComponent } from '../shared/search/search.component';
55
import { SidebarService } from '../shared/sidebar/sidebar.service';
66
import { HostWindowService } from '../shared/host-window.service';
@@ -10,10 +10,17 @@ import { SearchConfigurationService } from '../core/shared/search/search-configu
1010
import { SearchService } from '../core/shared/search/search.service';
1111
import { PaginatedSearchOptions } from '../shared/search/models/paginated-search-options.model';
1212
import { SearchObjects } from '../shared/search/models/search-objects.model';
13-
import { Router } from '@angular/router';
13+
import { NavigationStart, Router } from '@angular/router';
1414
import { RemoteData } from '../core/data/remote-data';
1515
import { DSpaceObject } from '../core/shared/dspace-object.model';
1616
import { getFirstSucceededRemoteData } from '../core/shared/operators';
17+
import { inspect } from 'util';
18+
import { hasValue, hasValueOperator, isNotEmpty } from '../shared/empty.util';
19+
import { Subscription } from 'rxjs/internal/Subscription';
20+
import { Observable } from 'rxjs/internal/Observable';
21+
import { ITEM_MODULE_PATH } from '../item-page/item-page-routing-paths';
22+
import { COLLECTION_MODULE_PATH } from '../collection-page/collection-page-routing-paths';
23+
import { COMMUNITY_MODULE_PATH } from '../community-page/community-page-routing-paths';
1724

1825
/**
1926
* This component triggers a page view statistic
@@ -29,7 +36,24 @@ import { getFirstSucceededRemoteData } from '../core/shared/operators';
2936
}
3037
]
3138
})
32-
export class SearchTrackerComponent extends SearchComponent implements OnInit {
39+
export class SearchTrackerComponent extends SearchComponent implements OnInit, OnDestroy {
40+
/**
41+
* Regex to match UUIDs
42+
*/
43+
uuidRegex = /\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b/g;
44+
45+
/**
46+
* List of paths that are considered to be the start of a route to an object page (excluding "/", e.g. "items")
47+
* These are expected to end on an object UUID
48+
* If they match the route we're navigating to, an object property will be added to the search event sent
49+
*/
50+
allowedObjectPaths: string[] = ['entities', ITEM_MODULE_PATH, COLLECTION_MODULE_PATH, COMMUNITY_MODULE_PATH];
51+
52+
/**
53+
* Array to track all subscriptions and unsubscribe them onDestroy
54+
* @type {Array}
55+
*/
56+
subs: Subscription[] = [];
3357

3458
constructor(
3559
protected service: SearchService,
@@ -44,40 +68,104 @@ export class SearchTrackerComponent extends SearchComponent implements OnInit {
4468
}
4569

4670
ngOnInit(): void {
47-
// super.ngOnInit();
48-
this.getSearchOptions().pipe(
71+
this.subs.push(
72+
this.getSearchOptionsAndObjects().subscribe((options) => {
73+
this.trackEvent(this.transformOptionsToEventProperties(options));
74+
}),
75+
this.router.events.pipe(
76+
filter((event) => event instanceof NavigationStart),
77+
map((event: NavigationStart) => this.getDsoUUIDFromUrl(event.url)),
78+
hasValueOperator(),
79+
switchMap((uuid) =>
80+
this.getSearchOptionsAndObjects().pipe(
81+
take(1),
82+
map((options) => this.transformOptionsToEventProperties(Object.assign({}, options, {
83+
clickedObject: uuid,
84+
})))
85+
)
86+
),
87+
).subscribe((options) => {
88+
this.trackEvent(options);
89+
}),
90+
);
91+
}
92+
93+
/**
94+
* Get a combination of the currently applied search options and search query response
95+
*/
96+
getSearchOptionsAndObjects(): Observable<{ config: PaginatedSearchOptions, searchQueryResponse: SearchObjects<DSpaceObject> }> {
97+
return this.getSearchOptions().pipe(
4998
switchMap((options: PaginatedSearchOptions) =>
5099
this.service.searchEntries(options).pipe(
51100
getFirstSucceededRemoteData(),
52101
map((rd: RemoteData<SearchObjects<DSpaceObject>>) => ({
53102
config: options,
54103
searchQueryResponse: rd.payload
55104
}))
56-
)),
57-
).subscribe(({ config, searchQueryResponse }) => {
58-
const filters: { filter: string, operator: string, value: string, label: string; }[] = [];
59-
const appliedFilters = searchQueryResponse.appliedFilters || [];
60-
for (let i = 0, filtersLength = appliedFilters.length; i < filtersLength; i++) {
61-
const appliedFilter = appliedFilters[i];
62-
filters.push(appliedFilter);
105+
)
106+
),
107+
);
108+
}
109+
110+
/**
111+
* Transform the given options containing search-options, query-response and optional object UUID into properties
112+
* that can be sent to Angularitics for triggering a search event
113+
* @param options
114+
*/
115+
transformOptionsToEventProperties(options: { config: PaginatedSearchOptions, searchQueryResponse: SearchObjects<DSpaceObject>, clickedObject?: string }): any {
116+
const filters: { filter: string, operator: string, value: string, label: string; }[] = [];
117+
const appliedFilters = options.searchQueryResponse.appliedFilters || [];
118+
for (let i = 0, filtersLength = appliedFilters.length; i < filtersLength; i++) {
119+
const appliedFilter = appliedFilters[i];
120+
filters.push(appliedFilter);
121+
}
122+
return {
123+
action: 'search',
124+
properties: {
125+
searchOptions: options.config,
126+
page: {
127+
size: options.config.pagination.size, // same as searchQueryResponse.page.elementsPerPage
128+
totalElements: options.searchQueryResponse.pageInfo.totalElements,
129+
totalPages: options.searchQueryResponse.pageInfo.totalPages,
130+
number: options.config.pagination.currentPage, // same as searchQueryResponse.page.currentPage
131+
},
132+
sort: {
133+
by: options.config.sort.field,
134+
order: options.config.sort.direction
135+
},
136+
filters: filters,
137+
clickedObject: options.clickedObject,
138+
},
139+
};
140+
}
141+
142+
/**
143+
* Track an event with given properties
144+
* @param properties
145+
*/
146+
trackEvent(properties: any) {
147+
this.angulartics2.eventTrack.next(properties);
148+
}
149+
150+
/**
151+
* Get the UUID from a DSO url
152+
* Return null if the url isn't an object page (allowedObjectPaths) or the UUID couldn't be found
153+
* @param url
154+
*/
155+
getDsoUUIDFromUrl(url: string): string {
156+
if (isNotEmpty(url)) {
157+
if (this.allowedObjectPaths.some((path) => url.startsWith(`/${path}`))) {
158+
const uuid = url.substring(url.lastIndexOf('/') + 1);
159+
if (uuid.match(this.uuidRegex)) {
160+
return uuid;
63161
}
64-
this.angulartics2.eventTrack.next({
65-
action: 'search',
66-
properties: {
67-
searchOptions: config,
68-
page: {
69-
size: config.pagination.size, // same as searchQueryResponse.page.elementsPerPage
70-
totalElements: searchQueryResponse.pageInfo.totalElements,
71-
totalPages: searchQueryResponse.pageInfo.totalPages,
72-
number: config.pagination.currentPage, // same as searchQueryResponse.page.currentPage
73-
},
74-
sort: {
75-
by: config.sort.field,
76-
order: config.sort.direction
77-
},
78-
filters: filters,
79-
},
80-
});
81-
});
162+
}
163+
}
164+
return null;
165+
}
166+
167+
ngOnDestroy() {
168+
super.ngOnDestroy();
169+
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
82170
}
83171
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ export class Angulartics2DSpace {
3131
event.properties.searchOptions,
3232
event.properties.page,
3333
event.properties.sort,
34-
event.properties.filters
34+
event.properties.filters,
35+
event.properties.clickedObject,
3536
);
3637
}
3738
}

src/app/statistics/statistics.service.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,14 @@ export class StatisticsService {
4545
* @param page: An object that describes the pagination status
4646
* @param sort: An object that describes the sort status
4747
* @param filters: An array of search filters used to filter the result set
48+
* @param clickedObject: UUID of object clicked
4849
*/
4950
trackSearchEvent(
5051
searchOptions: SearchOptions,
5152
page: { size: number, totalElements: number, totalPages: number, number: number },
5253
sort: { by: string, order: string },
53-
filters?: { filter: string, operator: string, value: string, label: string }[]
54+
filters?: { filter: string, operator: string, value: string, label: string }[],
55+
clickedObject?: string,
5456
) {
5557
const body = {
5658
query: searchOptions.query,
@@ -87,6 +89,9 @@ export class StatisticsService {
8789
}
8890
Object.assign(body, { appliedFilters: bodyFilters });
8991
}
92+
if (hasValue(clickedObject)) {
93+
Object.assign(body, { clickedObject });
94+
}
9095
this.sendEvent('/statistics/searchevents', body);
9196
}
9297

0 commit comments

Comments
 (0)