1class AnalyticsTracker { 2 constructor() { 3 this.url = 4 window.wpeAitkAnalytics.restUrl + 5 'analytics/v1/behavioral-analytics/event'; 6 this.isBeaconSupported = 7 navigator && typeof navigator.sendBeacon === 'function'; 8 } 9 10 trackEvent(eventType, eventData) { 11 const requestData = { 12 // eslint-disable-next-line no-undef 13 session: { id: userSessionStore.getSessionId() || 'anonymous' }, 14 // eslint-disable-next-line no-undef 15 userID: userSessionStore.getUserId() || 'anonymous', 16 }; 17 18 const payload = { 19 ...requestData, 20 ...eventData, 21 // eslint-disable-next-line no-undef 22 _wpnonce: wpeAitkAnalytics.nonce, 23 }; 24 25 const endpoint = `${this.url}/${eventType}`; 26 27 if (this.isBeaconSupported) { 28 const blob = new Blob([JSON.stringify(payload)], { 29 type: 'application/json', 30 }); 31 if (navigator.sendBeacon(endpoint, blob)) { 32 return; 33 } 34 } 35 36 // Fall back to fetch if sendBeacon isn't supported or fails 37 this.sendWithFetch(endpoint, payload); 38 } 39 40 sendWithFetch(endpoint, payload) { 41 fetch(endpoint, { 42 method: 'POST', 43 headers: { 'Content-Type': 'application/json' }, 44 body: JSON.stringify(payload), 45 keepalive: true, 46 }).catch((error) => { 47 console.error('Analytics fetch error: ', error); 48 }); 49 } 50 51 trackPageView(pageViewData) { 52 return this.trackEvent('page_view', pageViewData); 53 } 54 55 trackSearch(searchData) { 56 return this.trackEvent('search', searchData); 57 } 58 59 trackSearchResultClick(clickData) { 60 return this.trackEvent('search_click', clickData); 61 } 62} 63 64const state = { 65 trackerInstance: null, 66 eventQueue: [], 67 searchResults: [], 68 searchObject: null, 69 clickHandlerInitialized: false, 70}; 71 72function removeTrailingSlash(url) { 73 return url.endsWith('/') && url.length > 1 ? url.slice(0, -1) : url; 74} 75 76const AnalyticsTrackerAPI = { 77 createTracker() { 78 if (!state.trackerInstance) { 79 state.trackerInstance = new AnalyticsTracker(); 80 81 // Process queued events 82 if (state.eventQueue.length > 0) { 83 state.eventQueue.forEach(([eventType, data]) => { 84 state.trackerInstance.trackEvent(eventType, data); 85 }); 86 state.eventQueue = []; 87 } 88 } 89 }, 90 91 clickDataParamName: '_recommendations_data', 92 93 trackPageView() { 94 // only track page views for non-search pages that have a document ID 95 const urlParams = new URLSearchParams(window.location.search); 96 // eslint-disable-next-line no-undef 97 if (!wpeAitkAnalytics.documentID || urlParams.has('s')) { 98 return; 99 } 100 101 // Get current URL without click data parameter 102 const currentUrl = new URL(window.location.href); 103 currentUrl.searchParams.delete(this.clickDataParamName); 104 const cleanUrl = currentUrl.toString(); 105 106 const pageViewData = { 107 data: { 108 // eslint-disable-next-line no-undef 109 documentID: wpeAitkAnalytics.documentID, 110 page: { 111 referrer: document.referrer, 112 // eslint-disable-next-line no-undef 113 title: wpeAitkAnalytics.title, 114 url: cleanUrl, 115 }, 116 }, 117 }; 118 119 AnalyticsTrackerAPI.createTracker(); 120 if (state.trackerInstance) { 121 state.trackerInstance.trackPageView(pageViewData); 122 } else { 123 state.eventQueue.push(['page_view', pageViewData]); 124 } 125 }, 126 127 getSearchParams(urlParams) { 128 const searchTerm = urlParams.get('s') || '*'; 129 const sortName = urlParams.get('orderby') || 'default'; 130 // Get all other parameters as filters 131 const filters = []; 132 const excludeKeys = ['s', 'orderby']; 133 134 [...new Set(urlParams.keys())] 135 .filter((key) => !excludeKeys.includes(key)) 136 .forEach((key) => { 137 const items = urlParams.getAll(key).flatMap((item) => item.split(',')); 138 if (items.length > 0) { 139 filters.push({ 140 filterName: key, 141 items: items, 142 }); 143 } 144 }); 145 return { searchTerm, filters, sortName }; 146 }, 147 148 trackSearch(urlParams) { 149 const { searchTerm, filters, sortName } = this.getSearchParams(urlParams); 150 151 // eslint-disable-next-line no-undef 152 state.searchResults = wpeAitkAnalytics.searchResults; 153 154 state.searchObject = { 155 query: searchTerm, 156 filters: filters, 157 sort: { name: sortName }, 158 results: { 159 items: state.searchResults.map((item) => ({ 160 documentID: item.documentID, 161 page: { 162 title: item.title, 163 referrer: document.referrer, 164 url: item.url, 165 }, 166 })), 167 }, 168 }; 169 170 const searchData = { data: { search: state.searchObject } }; 171 172 AnalyticsTrackerAPI.createTracker(); 173 if (state.trackerInstance) { 174 state.trackerInstance.trackSearch(searchData); 175 } else { 176 state.eventQueue.push(['search', searchData]); 177 } 178 }, 179 180 trackSearchResultClick() {
181 const urlParams = new URLSearchParams(window.location.search); 182 const paramData = urlParams.get(this.clickDataParamName); 183 if (!paramData) { 184 return; 185 } 186 187 const url = new URL(window.location.href); 188 url.searchParams.delete(this.clickDataParamName); 189 const cleanUrl = url.toString(); 190 window.history.replaceState(window.history.state, '', cleanUrl); 191 192 if (!document.referrer) { 193 return; 194 } 195 const referrer = new URL(document.referrer); 196 197 if ( 198 referrer.hostname !== window.location.hostname || 199 !referrer.searchParams.has('s') 200 ) { 201 return; 202 } 203 204 const { searchTerm, filters, sortName } = this.getSearchParams( 205 referrer.searchParams 206 ); 207 208 let position; 209 try { 210 const decodedData = JSON.parse(atob(paramData)); 211 position = decodedData.position; 212 } catch (e) { 213 // If parsing fails, position will be undefined 214 } 215 216 const clickData = { 217 data: { 218 // eslint-disable-next-line no-undef 219 documentID: wpeAitkAnalytics.documentID, 220 ...(position !== undefined && { position }), 221 page: { 222 referrer: document.referrer, 223 // eslint-disable-next-line no-undef 224 title: wpeAitkAnalytics.title, 225 url: cleanUrl, 226 }, 227 search: { 228 query: searchTerm, 229 filters: filters, 230 sort: { name: sortName }, 231 }, 232 }, 233 }; 234 235 AnalyticsTrackerAPI.createTracker(); 236 if (state.trackerInstance) { 237 state.trackerInstance.trackSearchResultClick(clickData); 238 } else { 239 state.eventQueue.push(['search_click', clickData]); 240 } 241 }, 242 243 initSearchResultClickTracking() { 244 if ( 245 state.searchResults.length === 0 || 246 !state.searchObject || 247 state.clickHandlerInitialized 248 ) { 249 return; 250 } 251 252 // Get pagination info to calculate global position 253 // eslint-disable-next-line no-undef 254 const currentPage = wpeAitkAnalytics.currentPage || 1; 255 // eslint-disable-next-line no-undef 256 const postsPerPage = wpeAitkAnalytics.postsPerPage; 257 258 document.querySelectorAll('a').forEach((link) => { 259 if (link.href.startsWith('javascript:')) return; 260 261 const currentPagePath = window.location.href.split('#')[0]; 262 const anchorPath = link.href.split('#')[0]; 263 if (link.href.includes('#') && anchorPath === currentPagePath) return; 264 265 // Find the position (0-indexed) on current page 266 const localPosition = state.searchResults.findIndex( 267 (result) => 268 removeTrailingSlash(result.url) === removeTrailingSlash(link.href) 269 ); 270 271 if (localPosition !== -1) { 272 // Calculate global position across all pages (1-indexed) 273 let globalPosition; 274 if (postsPerPage === -1) { 275 // No pagination (-1 means show all posts), use local position only 276 globalPosition = localPosition + 1; 277 } else {
278 // With pagination, calculate offset from previous pages 279 const offset = (currentPage - 1) * postsPerPage; 280 globalPosition = offset + localPosition + 1; 281 } 282 283 const paramData = { position: globalPosition }; 284 let base64 = btoa(JSON.stringify(paramData)); 285 const url = new URL(link.href, window.location.origin); 286 url.searchParams.set(this.clickDataParamName, base64); 287 link.href = url.toString(); 288 } 289 }); 290 state.clickHandlerInitialized = true; 291 }, 292}; 293 294function init() { 295 // Track page view on initial load 296 window.addEventListener('pageshow', () => { 297 AnalyticsTrackerAPI.trackPageView(); 298 AnalyticsTrackerAPI.trackSearchResultClick(); 299 }); 300 301 // Track page views on navigation changes 302 const trackPageViewOnNavigation = () => { 303 AnalyticsTrackerAPI.trackPageView(); 304 }; 305 306 if (window.history) { 307 const originalPushState = window.history.pushState; 308 window.history.pushState = (...args) => { 309 originalPushState.apply(window.history, args); 310 window.dispatchEvent(new Event('ewt:pushstate')); 311 }; 312 window.addEventListener('ewt:pushstate', trackPageViewOnNavigation); 313 window.addEventListener('popstate', trackPageViewOnNavigation); 314 } else { 315 window.addEventListener('hashchange', trackPageViewOnNavigation); 316 } 317 318 // Initialize search tracking when the DOM is loaded 319 document.addEventListener('DOMContentLoaded', () => { 320 const urlParams = new URLSearchParams(window.location.search); 321 if (urlParams.has('s')) { 322 AnalyticsTrackerAPI.trackSearch(urlParams); 323 AnalyticsTrackerAPI.initSearchResultClickTracking(); 324 } 325 }); 326} 327 328init();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.