Skip to content

Commit d2fa8cd

Browse files
authored
Merge pull request DSpace#2200 from atmire/w2p-100414_Missing-search-result-statistics-PR
Missing search results statistics
2 parents 9c78020 + ff1175c commit d2fa8cd

9 files changed

Lines changed: 138 additions & 104 deletions

File tree

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,8 +333,9 @@ export class SearchService implements OnDestroy {
333333
* Send search event to rest api using angularitics
334334
* @param config Paginated search options used
335335
* @param searchQueryResponse The response objects of the performed search
336+
* @param clickedObject Optional UUID of an object a search was performed and clicked for
336337
*/
337-
trackSearch(config: PaginatedSearchOptions, searchQueryResponse: SearchObjects<DSpaceObject>) {
338+
trackSearch(config: PaginatedSearchOptions, searchQueryResponse: SearchObjects<DSpaceObject>, clickedObject?: string) {
338339
const filters: { filter: string, operator: string, value: string, label: string; }[] = [];
339340
const appliedFilters = searchQueryResponse.appliedFilters || [];
340341
for (let i = 0, filtersLength = appliedFilters.length; i < filtersLength; i++) {
@@ -356,6 +357,7 @@ export class SearchService implements OnDestroy {
356357
order: config.sort.direction
357358
},
358359
filters: filters,
360+
clickedObject,
359361
},
360362
});
361363
}

src/app/search-page/search-page.module.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { CoreModule } from '../core/core.module';
44
import { SharedModule } from '../shared/shared.module';
55
import { SidebarService } from '../shared/sidebar/sidebar.service';
66
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
7-
import { SearchTrackerComponent } from './search-tracker.component';
87
import { StatisticsModule } from '../statistics/statistics.module';
98
import { SearchPageComponent } from './search-page.component';
109
import { SearchFilterService } from '../core/shared/search/search-filter.service';
@@ -16,7 +15,6 @@ import { SearchModule } from '../shared/search/search.module';
1615

1716
const components = [
1817
SearchPageComponent,
19-
SearchTrackerComponent,
2018
ThemedSearchPageComponent
2119
];
2220

src/app/search-page/search-tracker.component.html

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/app/search-page/search-tracker.component.scss

Lines changed: 0 additions & 3 deletions
This file was deleted.

src/app/search-page/search-tracker.component.ts

Lines changed: 0 additions & 83 deletions
This file was deleted.

src/app/shared/search/search.component.spec.ts

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import { SearchObjects } from './models/search-objects.model';
3131
import { DSpaceObject } from '../../core/shared/dspace-object.model';
3232
import { SearchFilterConfig } from './models/search-filter-config.model';
3333
import { FilterType } from './models/filter-type.model';
34+
import { getCommunityPageRoute } from '../../community-page/community-page-routing-paths';
35+
import { getCollectionPageRoute } from '../../collection-page/collection-page-routing-paths';
3436

3537
let comp: SearchComponent;
3638
let fixture: ComponentFixture<SearchComponent>;
@@ -101,8 +103,9 @@ const searchServiceStub = jasmine.createSpyObj('SearchService', {
101103
search: mockResultsRD$,
102104
getSearchLink: '/search',
103105
getScopes: observableOf(['test-scope']),
104-
getSearchConfigurationFor: createSuccessfulRemoteDataObject$(searchConfig)
105-
});
106+
getSearchConfigurationFor: createSuccessfulRemoteDataObject$(searchConfig),
107+
trackSearch: {},
108+
}) as SearchService;
106109
const configurationParam = 'default';
107110
const queryParam = 'test query';
108111
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
@@ -327,4 +330,64 @@ describe('SearchComponent', () => {
327330
}));
328331

329332
});
333+
334+
describe('getDsoUUIDFromUrl', () => {
335+
let url: string;
336+
let result: string;
337+
338+
describe('when the navigated URL is an entity route', () => {
339+
beforeEach(() => {
340+
url = '/entities/publication/9a364471-3f19-4e7b-916a-a24a44ff48e3';
341+
result = (comp as any).getDsoUUIDFromUrl(url);
342+
});
343+
344+
it('should return the UUID', () => {
345+
expect(result).toEqual('9a364471-3f19-4e7b-916a-a24a44ff48e3');
346+
});
347+
});
348+
349+
describe('when the navigated URL is a community route', () => {
350+
beforeEach(() => {
351+
url = `${getCommunityPageRoute('9a364471-3f19-4e7b-916a-a24a44ff48e3')}`;
352+
result = (comp as any).getDsoUUIDFromUrl(url);
353+
});
354+
355+
it('should return the UUID', () => {
356+
expect(result).toEqual('9a364471-3f19-4e7b-916a-a24a44ff48e3');
357+
});
358+
});
359+
360+
describe('when the navigated URL is a collection route', () => {
361+
beforeEach(() => {
362+
url = `${getCollectionPageRoute('9a364471-3f19-4e7b-916a-a24a44ff48e3')}`;
363+
result = (comp as any).getDsoUUIDFromUrl(url);
364+
});
365+
366+
it('should return the UUID', () => {
367+
expect(result).toEqual('9a364471-3f19-4e7b-916a-a24a44ff48e3');
368+
});
369+
});
370+
371+
describe('when the navigated URL is an item route', () => {
372+
beforeEach(() => {
373+
url = '/items/9a364471-3f19-4e7b-916a-a24a44ff48e3';
374+
result = (comp as any).getDsoUUIDFromUrl(url);
375+
});
376+
377+
it('should return the UUID', () => {
378+
expect(result).toEqual('9a364471-3f19-4e7b-916a-a24a44ff48e3');
379+
});
380+
});
381+
382+
describe('when the navigated URL is an invalid route', () => {
383+
beforeEach(() => {
384+
url = '/invalid/object/route/9a364471-3f19-4e7b-916a-a24a44ff48e3';
385+
result = (comp as any).getDsoUUIDFromUrl(url);
386+
});
387+
388+
it('should return null', () => {
389+
expect(result).toBeNull();
390+
});
391+
});
392+
});
330393
});

src/app/shared/search/search.component.ts

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ChangeDetectionStrategy, Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core';
2-
import { Router } from '@angular/router';
2+
import { NavigationStart, Router } from '@angular/router';
33

44
import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs';
55
import { debounceTime, distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';
@@ -11,7 +11,7 @@ import { DSpaceObject } from '../../core/shared/dspace-object.model';
1111
import { pushInOut } from '../animations/push';
1212
import { HostWindowService } from '../host-window.service';
1313
import { SidebarService } from '../sidebar/sidebar.service';
14-
import { hasValue } from '../empty.util';
14+
import { hasValue, hasValueOperator, isNotEmpty } from '../empty.util';
1515
import { RouteService } from '../../core/services/route.service';
1616
import { SEARCH_CONFIG_SERVICE } from '../../my-dspace-page/my-dspace-page.component';
1717
import { PaginatedSearchOptions } from './models/paginated-search-options.model';
@@ -35,6 +35,9 @@ import { environment } from 'src/environments/environment';
3535
import { SubmissionObject } from '../../core/submission/models/submission-object.model';
3636
import { SearchFilterConfig } from './models/search-filter-config.model';
3737
import { WorkspaceItem } from '../..//core/submission/models/workspaceitem.model';
38+
import { ITEM_MODULE_PATH } from '../../item-page/item-page-routing-paths';
39+
import { COLLECTION_MODULE_PATH } from '../../collection-page/collection-page-routing-paths';
40+
import { COMMUNITY_MODULE_PATH } from '../../community-page/community-page-routing-paths';
3841

3942
@Component({
4043
selector: 'ds-search',
@@ -229,9 +232,21 @@ export class SearchComponent implements OnInit {
229232
searchLink: string;
230233

231234
/**
232-
* Subscription to unsubscribe from
235+
* Regex to match UUIDs
233236
*/
234-
sub: Subscription;
237+
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;
238+
239+
/**
240+
* List of paths that are considered to be the start of a route to an object page (excluding "/", e.g. "items")
241+
* These are expected to end on an object UUID
242+
* If they match the route we're navigating to, an object property will be added to the search event sent
243+
*/
244+
allowedObjectPaths: string[] = ['entities', ITEM_MODULE_PATH, COLLECTION_MODULE_PATH, COMMUNITY_MODULE_PATH];
245+
246+
/**
247+
* Subscriptions to unsubscribe from
248+
*/
249+
subs: Subscription[] = [];
235250

236251
/**
237252
* Emits an event with the current search result entries
@@ -301,7 +316,7 @@ export class SearchComponent implements OnInit {
301316
);
302317
const searchOptions$: Observable<PaginatedSearchOptions> = this.getSearchOptions().pipe(distinctUntilChanged());
303318

304-
this.sub = combineLatest([configuration$, searchSortOptions$, searchOptions$, sortOption$]).pipe(
319+
this.subs.push(combineLatest([configuration$, searchSortOptions$, searchOptions$, sortOption$]).pipe(
305320
filter(([configuration, searchSortOptions, searchOptions, sortOption]: [string, SortOptions[], PaginatedSearchOptions, SortOptions]) => {
306321
// filter for search options related to instanced paginated id
307322
return searchOptions.pagination.id === this.paginationId;
@@ -332,7 +347,9 @@ export class SearchComponent implements OnInit {
332347
this.retrieveSearchResults(newSearchOptions);
333348
this.retrieveFilters(searchOptions);
334349
}
335-
});
350+
}));
351+
352+
this.subscribeToRoutingEvents();
336353
}
337354

338355
/**
@@ -374,12 +391,10 @@ export class SearchComponent implements OnInit {
374391
}
375392

376393
/**
377-
* Unsubscribe from the subscription
394+
* Unsubscribe from the subscriptions
378395
*/
379396
ngOnDestroy(): void {
380-
if (hasValue(this.sub)) {
381-
this.sub.unsubscribe();
382-
}
397+
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
383398
}
384399

385400
/**
@@ -440,6 +455,43 @@ export class SearchComponent implements OnInit {
440455
});
441456
}
442457

458+
/**
459+
* Subscribe to routing events to detect when a user moves away from the search page
460+
* When the user is routing to an object page, it needs to send out a separate search event containing that object's UUID
461+
* This method should only be called once and is essentially what SearchTrackingComponent used to do (now removed)
462+
* @private
463+
*/
464+
private subscribeToRoutingEvents() {
465+
this.subs.push(
466+
this.router.events.pipe(
467+
filter((event) => event instanceof NavigationStart),
468+
map((event: NavigationStart) => this.getDsoUUIDFromUrl(event.url)),
469+
hasValueOperator(),
470+
).subscribe((uuid) => {
471+
if (this.resultsRD$.value.hasSucceeded) {
472+
this.service.trackSearch(this.searchOptions$.value, this.resultsRD$.value.payload as SearchObjects<DSpaceObject>, uuid);
473+
}
474+
}),
475+
);
476+
}
477+
478+
/**
479+
* Get the UUID from a DSO url
480+
* Return null if the url isn't an object page (allowedObjectPaths) or the UUID couldn't be found
481+
* @param url
482+
*/
483+
private getDsoUUIDFromUrl(url: string): string {
484+
if (isNotEmpty(url)) {
485+
if (this.allowedObjectPaths.some((path) => url.startsWith(`/${path}`))) {
486+
const uuid = url.substring(url.lastIndexOf('/') + 1);
487+
if (uuid.match(this.uuidRegex)) {
488+
return uuid;
489+
}
490+
}
491+
}
492+
return null;
493+
}
494+
443495
/**
444496
* Check if the sidebar is collapsed
445497
* @returns {Observable<boolean>} emits true if the sidebar is currently collapsed, false if it is expanded

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)