1import { isQueryValid } from './validator.js'; 2 3export const IS_REDIRECT_URL_PARAM = 'fsb'; 4export const IS_REDIRECT_VALUE = '1'; 5 6export class AnalyticsOptions { 7 constructor(maxQueryLength) { 8 this.maxQueryLength = maxQueryLength; 9 } 10} 11 12// Analytics 13export class AnalyticsManager { 14 constructor(delay, siteId, options) { 15 this.delay = delay; 16 this.siteId = siteId; 17 this.options = options ?? new AnalyticsOptions(255); 18 19 this.hits = 0; 20 this.query = null; 21 this.skipAnalyticOnce = sessionStorage.getItem(IS_REDIRECT_URL_PARAM) === IS_REDIRECT_VALUE; 22 23 this.debouncedAnalytics = this._debounceAnalytic(this.sendSearchAnalyticEvent.bind(this), this.delay); 24 } 25 26 updateQuery(query) { 27 this.query = query; 28 } 29 30 updateHits(hits) { 31 this.hits = hits; 32 } 33 34 // Logic 35 setShouldSkipForNextInit() { 36 sessionStorage.setItem(IS_REDIRECT_URL_PARAM, IS_REDIRECT_VALUE); 37 } 38 39 clearShouldSkip() { 40 sessionStorage.removeItem(IS_REDIRECT_URL_PARAM); 41 } 42 43 flush() { 44 this.debouncedAnalytics.flush(); 45 } 46 47 // Util 48 _debounceAnalytic(fn, time) { 49 let timerId = undefined; 50 let lastArgs = []; 51 52 const debounced = function (...args) { 53 lastArgs = args; 54 if (timerId) { 55 clearTimeout(timerId); 56 } 57 58 timerId = setTimeout(() => { 59 fn(...lastArgs); // calls sendSearchAnalyticEvent 60 timerId = undefined; 61 }, time); 62 }; 63 64 debounced.flush = () => { 65 if (timerId) { 66 clearTimeout(timerId); 67 fn(...lastArgs); // calls sendSearchAnalyticEvent 68 timerId = undefined; 69 } 70 }; 71 72 return debounced; 73 } 74 75 // Indirect 76 debounceAnalytic(url) { 77 this.debouncedAnalytics(url); 78 } 79 80 // Direct 81 async sendSearchAnalyticEvent(url) { 82 await this._sendSearchAnalyticEvent(url, this.query, this.hits !== 0); 83 } 84 85 async _sendSearchAnalyticEvent(url, query, hasResults) { 86 try { 87 if (isQueryValid(query)) { 88 return; 89 } 90 91 const params = { siteId: this.siteId, type: 'query', query: this.parseQuery(query), hasResult: hasResults ? 'true' : 'false' }; 92 const formData = new FormData(); 93 for (const key in params) { 94 formData.append(key, params[key]); 95 } 96 97 await this._sendAnalytic(url, formData); 98 } catch (error) { 99 console.error('Could not send search analytic:', error); 100 } 101 } 102 103 async sendClickAnalyticEvent(url, query, searchIndexId) { 104 try { 105 if (isQueryValid(query)) { 106 return; 107 } 108 109 const params = { siteId: this.siteId, type: 'click', query: this.parseQuery(query), searchIndexId: searchIndexId }; 110 const formData = new FormData(); 111 for (const key in params) { 112 formData.append(key, params[key]); 113 } 114 115 await this._sendAnalytic(url, formData); 116 } catch (error) { 117 console.error(error); 118 console.error('Could not send click analytic'); 119 } 120 } 121 122 async _sendAnalytic(url, formData) { 123 // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon 124 navigator.sendBeacon(url, formData); 125 } 126 127 parseQuery(query) { 128 return query.slice(0, this.options.maxQueryLength); 129 } 130}
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.