From b829335ba5fdbcfbbdd684e1127ef4c557126dd3 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 5 Apr 2023 17:49:29 +0200 Subject: [PATCH 1/3] 100414: Missing search_result statistics fix --- .../search-page/search-tracker.component.ts | 143 ++++++++++++++---- .../statistics/angulartics/dspace-provider.ts | 3 +- src/app/statistics/statistics.service.ts | 7 +- 3 files changed, 120 insertions(+), 33 deletions(-) diff --git a/src/app/search-page/search-tracker.component.ts b/src/app/search-page/search-tracker.component.ts index e7f59a2f23..eb7ac6fcc3 100644 --- a/src/app/search-page/search-tracker.component.ts +++ b/src/app/search-page/search-tracker.component.ts @@ -1,6 +1,6 @@ -import { Component, Inject, OnInit } from '@angular/core'; +import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; import { Angulartics2 } from 'angulartics2'; -import { map, switchMap } from 'rxjs/operators'; +import { filter, map, switchMap, take } from 'rxjs/operators'; import { SearchComponent } from './search.component'; import { SidebarService } from '../shared/sidebar/sidebar.service'; import { HostWindowService } from '../shared/host-window.service'; @@ -10,10 +10,17 @@ import { SearchConfigurationService } from '../core/shared/search/search-configu import { SearchService } from '../core/shared/search/search.service'; import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model'; import { SearchObjects } from '../shared/search/search-objects.model'; -import { Router } from '@angular/router'; +import { NavigationStart, Router } from '@angular/router'; import { RemoteData } from '../core/data/remote-data'; import { DSpaceObject } from '../core/shared/dspace-object.model'; import { getFirstSucceededRemoteData } from '../core/shared/operators'; +import { inspect } from 'util'; +import { hasValue, hasValueOperator, isNotEmpty } from '../shared/empty.util'; +import { Subscription } from 'rxjs/internal/Subscription'; +import { Observable } from 'rxjs/internal/Observable'; +import { ITEM_MODULE_PATH } from '../item-page/item-page-routing-paths'; +import { COLLECTION_MODULE_PATH } from '../collection-page/collection-page-routing-paths'; +import { COMMUNITY_MODULE_PATH } from '../community-page/community-page-routing-paths'; /** * This component triggers a page view statistic @@ -29,7 +36,17 @@ import { getFirstSucceededRemoteData } from '../core/shared/operators'; } ] }) -export class SearchTrackerComponent extends SearchComponent implements OnInit { +export class SearchTrackerComponent extends SearchComponent implements OnInit, OnDestroy { + /** + * Regex to match UUIDs + */ + 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; + + /** + * Array to track all subscriptions and unsubscribe them onDestroy + * @type {Array} + */ + subs: Subscription[] = []; constructor( protected service: SearchService, @@ -44,8 +61,33 @@ export class SearchTrackerComponent extends SearchComponent implements OnInit { } ngOnInit(): void { - // super.ngOnInit(); - this.getSearchOptions().pipe( + this.subs.push( + this.getSearchOptionsAndObjects().subscribe((options) => { + this.trackEvent(this.transformOptionsToEventProperties(options)); + }), + this.router.events.pipe( + filter((event) => event instanceof NavigationStart), + map((event: NavigationStart) => this.getDsoUUIDFromUrl(event.url)), + hasValueOperator(), + switchMap((uuid) => + this.getSearchOptionsAndObjects().pipe( + take(1), + map((options) => this.transformOptionsToEventProperties(Object.assign({}, options, { + object: uuid, + }))) + ) + ), + ).subscribe((options) => { + this.trackEvent(options); + }), + ); + } + + /** + * Get a combination of the currently applied search options and search query response + */ + getSearchOptionsAndObjects(): Observable<{ config: PaginatedSearchOptions, searchQueryResponse: SearchObjects }> { + return this.getSearchOptions().pipe( switchMap((options: PaginatedSearchOptions) => this.service.searchEntries(options).pipe( getFirstSucceededRemoteData(), @@ -53,31 +95,70 @@ export class SearchTrackerComponent extends SearchComponent implements OnInit { config: options, searchQueryResponse: rd.payload })) - )), - ).subscribe(({ config, searchQueryResponse }) => { - const filters: { filter: string, operator: string, value: string, label: string; }[] = []; - const appliedFilters = searchQueryResponse.appliedFilters || []; - for (let i = 0, filtersLength = appliedFilters.length; i < filtersLength; i++) { - const appliedFilter = appliedFilters[i]; - filters.push(appliedFilter); + ) + ), + ); + } + + /** + * Transform the given options containing search-options, query-response and optional object UUID into properties + * that can be sent to Angularitics for triggering a search event + * @param options + */ + transformOptionsToEventProperties(options: { config: PaginatedSearchOptions, searchQueryResponse: SearchObjects, object?: string }): any { + const filters: { filter: string, operator: string, value: string, label: string; }[] = []; + const appliedFilters = options.searchQueryResponse.appliedFilters || []; + for (let i = 0, filtersLength = appliedFilters.length; i < filtersLength; i++) { + const appliedFilter = appliedFilters[i]; + filters.push(appliedFilter); + } + return { + action: 'search', + properties: { + searchOptions: options.config, + page: { + size: options.config.pagination.size, // same as searchQueryResponse.page.elementsPerPage + totalElements: options.searchQueryResponse.pageInfo.totalElements, + totalPages: options.searchQueryResponse.pageInfo.totalPages, + number: options.config.pagination.currentPage, // same as searchQueryResponse.page.currentPage + }, + sort: { + by: options.config.sort.field, + order: options.config.sort.direction + }, + filters: filters, + object: options.object, + }, + }; + } + + /** + * Track an event with given properties + * @param properties + */ + trackEvent(properties: any) { + this.angulartics2.eventTrack.next(properties); + } + + /** + * Get the UUID from a DSO url + * Return null if the url isn't a community, collection or item page or the UUID couldn't be found + * @param url + */ + getDsoUUIDFromUrl(url: string): string { + if (isNotEmpty(url)) { + if (url.startsWith(`/${ITEM_MODULE_PATH}`) || url.startsWith(`/${COLLECTION_MODULE_PATH}`) || url.startsWith(`/${COMMUNITY_MODULE_PATH}`)) { + const uuid = url.substring(url.lastIndexOf('/') + 1); + if (uuid.match(this.uuidRegex)) { + return uuid; } - this.angulartics2.eventTrack.next({ - action: 'search', - properties: { - searchOptions: config, - page: { - size: config.pagination.size, // same as searchQueryResponse.page.elementsPerPage - totalElements: searchQueryResponse.pageInfo.totalElements, - totalPages: searchQueryResponse.pageInfo.totalPages, - number: config.pagination.currentPage, // same as searchQueryResponse.page.currentPage - }, - sort: { - by: config.sort.field, - order: config.sort.direction - }, - filters: filters, - }, - }); - }); + } + } + return null; + } + + ngOnDestroy() { + super.ngOnDestroy(); + this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); } } diff --git a/src/app/statistics/angulartics/dspace-provider.ts b/src/app/statistics/angulartics/dspace-provider.ts index cd1aab94bd..30a56ad505 100644 --- a/src/app/statistics/angulartics/dspace-provider.ts +++ b/src/app/statistics/angulartics/dspace-provider.ts @@ -31,7 +31,8 @@ export class Angulartics2DSpace { event.properties.searchOptions, event.properties.page, event.properties.sort, - event.properties.filters + event.properties.filters, + event.properties.object, ); } } diff --git a/src/app/statistics/statistics.service.ts b/src/app/statistics/statistics.service.ts index 9e12e627b5..b9d078ad22 100644 --- a/src/app/statistics/statistics.service.ts +++ b/src/app/statistics/statistics.service.ts @@ -45,12 +45,14 @@ export class StatisticsService { * @param page: An object that describes the pagination status * @param sort: An object that describes the sort status * @param filters: An array of search filters used to filter the result set + * @param object: Object clicked */ trackSearchEvent( searchOptions: SearchOptions, page: { size: number, totalElements: number, totalPages: number, number: number }, sort: { by: string, order: string }, - filters?: { filter: string, operator: string, value: string, label: string }[] + filters?: { filter: string, operator: string, value: string, label: string }[], + object?: string, ) { const body = { query: searchOptions.query, @@ -87,6 +89,9 @@ export class StatisticsService { } Object.assign(body, { appliedFilters: bodyFilters }); } + if (hasValue(object)) { + Object.assign(body, { object }); + } this.sendEvent('/statistics/searchevents', body); } From 41e4e870619e1c5a30b9d740dafa693107e4d88d Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 20 Apr 2023 10:13:55 +0200 Subject: [PATCH 2/3] 100414: Add entities to allowed object route --- src/app/search-page/search-tracker.component.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/app/search-page/search-tracker.component.ts b/src/app/search-page/search-tracker.component.ts index eb7ac6fcc3..6aa043cb54 100644 --- a/src/app/search-page/search-tracker.component.ts +++ b/src/app/search-page/search-tracker.component.ts @@ -42,6 +42,13 @@ export class SearchTrackerComponent extends SearchComponent implements OnInit, O */ 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; + /** + * List of paths that are considered to be the start of a route to an object page (excluding "/", e.g. "items") + * These are expected to end on an object UUID + * If they match the route we're navigating to, an object property will be added to the search event sent + */ + allowedObjectPaths: string[] = ['entities', ITEM_MODULE_PATH, COLLECTION_MODULE_PATH, COMMUNITY_MODULE_PATH]; + /** * Array to track all subscriptions and unsubscribe them onDestroy * @type {Array} @@ -142,12 +149,12 @@ export class SearchTrackerComponent extends SearchComponent implements OnInit, O /** * Get the UUID from a DSO url - * Return null if the url isn't a community, collection or item page or the UUID couldn't be found + * Return null if the url isn't an object page (allowedObjectPaths) or the UUID couldn't be found * @param url */ getDsoUUIDFromUrl(url: string): string { if (isNotEmpty(url)) { - if (url.startsWith(`/${ITEM_MODULE_PATH}`) || url.startsWith(`/${COLLECTION_MODULE_PATH}`) || url.startsWith(`/${COMMUNITY_MODULE_PATH}`)) { + if (this.allowedObjectPaths.some((path) => url.startsWith(`/${path}`))) { const uuid = url.substring(url.lastIndexOf('/') + 1); if (uuid.match(this.uuidRegex)) { return uuid; From 3221621e6cab233c2efdf217c6dc06c011ee48fe Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 11 May 2023 11:17:19 +0200 Subject: [PATCH 3/3] 100414: Missing search result statistics - renaming object to clickedObject --- src/app/search-page/search-tracker.component.ts | 6 +++--- src/app/statistics/angulartics/dspace-provider.ts | 2 +- src/app/statistics/statistics.service.ts | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/app/search-page/search-tracker.component.ts b/src/app/search-page/search-tracker.component.ts index 6aa043cb54..1912b237ec 100644 --- a/src/app/search-page/search-tracker.component.ts +++ b/src/app/search-page/search-tracker.component.ts @@ -80,7 +80,7 @@ export class SearchTrackerComponent extends SearchComponent implements OnInit, O this.getSearchOptionsAndObjects().pipe( take(1), map((options) => this.transformOptionsToEventProperties(Object.assign({}, options, { - object: uuid, + clickedObject: uuid, }))) ) ), @@ -112,7 +112,7 @@ export class SearchTrackerComponent extends SearchComponent implements OnInit, O * that can be sent to Angularitics for triggering a search event * @param options */ - transformOptionsToEventProperties(options: { config: PaginatedSearchOptions, searchQueryResponse: SearchObjects, object?: string }): any { + transformOptionsToEventProperties(options: { config: PaginatedSearchOptions, searchQueryResponse: SearchObjects, clickedObject?: string }): any { const filters: { filter: string, operator: string, value: string, label: string; }[] = []; const appliedFilters = options.searchQueryResponse.appliedFilters || []; for (let i = 0, filtersLength = appliedFilters.length; i < filtersLength; i++) { @@ -134,7 +134,7 @@ export class SearchTrackerComponent extends SearchComponent implements OnInit, O order: options.config.sort.direction }, filters: filters, - object: options.object, + clickedObject: options.clickedObject, }, }; } diff --git a/src/app/statistics/angulartics/dspace-provider.ts b/src/app/statistics/angulartics/dspace-provider.ts index 30a56ad505..5297f6fc38 100644 --- a/src/app/statistics/angulartics/dspace-provider.ts +++ b/src/app/statistics/angulartics/dspace-provider.ts @@ -32,7 +32,7 @@ export class Angulartics2DSpace { event.properties.page, event.properties.sort, event.properties.filters, - event.properties.object, + event.properties.clickedObject, ); } } diff --git a/src/app/statistics/statistics.service.ts b/src/app/statistics/statistics.service.ts index b9d078ad22..e9c21aa37d 100644 --- a/src/app/statistics/statistics.service.ts +++ b/src/app/statistics/statistics.service.ts @@ -45,14 +45,14 @@ export class StatisticsService { * @param page: An object that describes the pagination status * @param sort: An object that describes the sort status * @param filters: An array of search filters used to filter the result set - * @param object: Object clicked + * @param clickedObject: UUID of object clicked */ trackSearchEvent( searchOptions: SearchOptions, page: { size: number, totalElements: number, totalPages: number, number: number }, sort: { by: string, order: string }, filters?: { filter: string, operator: string, value: string, label: string }[], - object?: string, + clickedObject?: string, ) { const body = { query: searchOptions.query, @@ -89,8 +89,8 @@ export class StatisticsService { } Object.assign(body, { appliedFilters: bodyFilters }); } - if (hasValue(object)) { - Object.assign(body, { object }); + if (hasValue(clickedObject)) { + Object.assign(body, { clickedObject }); } this.sendEvent('/statistics/searchevents', body); }