1- import { Component , Inject , OnInit } from '@angular/core' ;
1+ import { Component , Inject , OnDestroy , OnInit } from '@angular/core' ;
22import { Angulartics2 } from 'angulartics2' ;
3- import { map , switchMap } from 'rxjs/operators' ;
3+ import { filter , map , switchMap , take } from 'rxjs/operators' ;
44import { SearchComponent } from '../shared/search/search.component' ;
55import { SidebarService } from '../shared/sidebar/sidebar.service' ;
66import { HostWindowService } from '../shared/host-window.service' ;
@@ -10,10 +10,17 @@ import { SearchConfigurationService } from '../core/shared/search/search-configu
1010import { SearchService } from '../core/shared/search/search.service' ;
1111import { PaginatedSearchOptions } from '../shared/search/models/paginated-search-options.model' ;
1212import { SearchObjects } from '../shared/search/models/search-objects.model' ;
13- import { Router } from '@angular/router' ;
13+ import { NavigationStart , Router } from '@angular/router' ;
1414import { RemoteData } from '../core/data/remote-data' ;
1515import { DSpaceObject } from '../core/shared/dspace-object.model' ;
1616import { 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 - 9 a - f ] { 8 } \b - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 4 } - \b [ 0 - 9 a - 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}
0 commit comments