• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

yext / answers-search-ui / 13118068681

03 Feb 2025 04:25PM UTC coverage: 61.767% (-0.4%) from 62.179%
13118068681

push

github

web-flow
Generative Direct Answers integration

Generative Direct Answers integration

2026 of 3430 branches covered (59.07%)

Branch coverage included in aggregate %.

67 of 113 new or added lines in 7 files covered. (59.29%)

30 existing lines in 4 files now uncovered.

3483 of 5489 relevant lines covered (63.45%)

26.65 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

51.04
/src/core/core.js
1
/** @module Core */
2
import { provideCore } from '@yext/search-core/lib/commonjs';
3
// Using the ESM build for importing the Environment enum due to an issue importing the commonjs version
4
import { CloudChoice, Environment } from '@yext/search-core';
5
import { generateUUID } from './utils/uuid';
6
import SearchDataTransformer from './search/searchdatatransformer';
7

8
import VerticalResults from './models/verticalresults';
9
import UniversalResults from './models/universalresults';
10
import QuestionSubmission from './models/questionsubmission';
11
import Navigation from './models/navigation';
12
import AlternativeVerticals from './models/alternativeverticals';
13
import LocationBias from './models/locationbias';
14
import QueryTriggers from './models/querytriggers';
15

16
import StorageKeys from './storage/storagekeys';
17
import AnalyticsEvent from './analytics/analyticsevent';
18
import FilterRegistry from './filters/filterregistry';
19
import DirectAnswer from './models/directanswer';
20
import AutoCompleteResponseTransformer from './search/autocompleteresponsetransformer';
21

22
import { PRODUCTION, LIB_VERSION, CLOUD_REGION, SANDBOX, GLOBAL_MULTI, GLOBAL_GCP } from './constants';
23
import { SearchParams } from '../ui';
24
import SearchStates from './storage/searchstates';
25
import Searcher from './models/searcher';
26
import { mergeAdditionalHttpHeaders } from './utils/mergeAdditionalHttpHeaders';
27
import GenerativeDirectAnswer from './models/generativedirectanswer';
28

29
/** @typedef {import('./storage/storage').default} Storage */
30

31
/**
32
 * Core is the main application container for all of the network and storage
33
 * related behaviors of the application. It uses an instance of the external Core
34
 * library to perform the actual network calls.
35
 */
36
export default class Core {
37
  constructor (config = {}) {
×
38
    /**
39
     * A reference to the auth token used for all requests
40
     * @type {string}
41
     * @private
42
     */
43
    this._token = config.token;
17✔
44

45
    /**
46
     * A reference to the client API Key used for all requests
47
     * @type {string}
48
     * @private
49
     */
50
    this._apiKey = config.apiKey;
17✔
51

52
    /**
53
     * A reference to the client Answers Key used for all requests
54
     * @type {string}
55
     * @private
56
     */
57
    this._experienceKey = config.experienceKey;
17✔
58

59
    /**
60
     * The answers config version to use for all requests
61
     * @type {string}
62
     * @private
63
     */
64
    this._experienceVersion = config.experienceVersion;
17✔
65

66
    /**
67
     * A reference to the client locale used for all requests. If not specified, defaults to "en" (for
68
     * backwards compatibility).
69
     * @type {string}
70
     * @private
71
     */
72
    this._locale = config.locale;
17✔
73

74
    /**
75
     * A map of field formatters used to format results, if present
76
     * @type {Object<string, function>}
77
     * @private
78
     */
79
    this._fieldFormatters = config.fieldFormatters || {};
17✔
80

81
    /**
82
     * A reference to the core data storage that powers the UI
83
     * @type {Storage}
84
     */
85
    this.storage = config.storage;
17✔
86

87
    /**
88
     * The filterRegistry is in charge of setting, removing, and retrieving filters
89
     * and facet filters from storage.
90
     * @type {FilterRegistry}
91
     */
92
    this.filterRegistry = new FilterRegistry(this.storage);
17✔
93

94
    /**
95
     * A local reference to the analytics reporter, used to report events for this component
96
     * @type {AnalyticsReporter}
97
     */
98
    this._analyticsReporter = config.analyticsReporter;
17✔
99

100
    /**
101
     * A user-given function that returns an analytics event to fire after a universal search.
102
     * @type {Function}
103
     */
104
    this.onUniversalSearch = config.onUniversalSearch || function () {};
17✔
105

106
    /**
107
     * A user-given function that returns an analytics event to fire after a vertical search.
108
     * @type {Function}
109
     */
110
    this.onVerticalSearch = config.onVerticalSearch || function () {};
17✔
111

112
    /**
113
     * The environment which determines which URLs the requests use.
114
     * @type {string}
115
     */
116
    this._environment = config.environment || PRODUCTION;
17✔
117

118
    /**
119
     * Determines the region of the api endpoints used when making search requests.
120
     * @type {string}
121
     */
122
    this._cloudRegion = CLOUD_REGION;
17✔
123

124
    /**
125
     * Determines the cloud choice of the api endpoints used when making search requests.
126
     * @type {string}
127
     */
128
    this._cloudChoice = config.cloudChoice || GLOBAL_MULTI;
17✔
129

130
    /** @type {string} */
131
    this._verticalKey = config.verticalKey;
17✔
132

133
    /** @type {ComponentManager} */
134
    this._componentManager = config.componentManager;
17✔
135

136
    /** @type {import('@yext/search-core').AdditionalHttpHeaders} */
137
    this._additionalHttpHeaders = mergeAdditionalHttpHeaders(config.additionalHttpHeaders);
17✔
138
  }
139

140
  /**
141
   * Sets a reference in core to the global QueryUpdateListener.
142
   *
143
   * @param {QueryUpdateListener} queryUpdateListener
144
   */
145
  setQueryUpdateListener (queryUpdateListener) {
146
    this.queryUpdateListener = queryUpdateListener;
2✔
147
  }
148

149
  /**
150
   * Sets a reference in core to the global ResultsUpdateListener.
151
   *
152
   * @param {ResultsUpdateListener} resultsUpdateListener
153
   */
154
  setResultsUpdateListener (resultsUpdateListener) {
NEW
155
    this.resultsUpdateListener = resultsUpdateListener;
×
156
  }
157

158
  /**
159
   * Initializes the {@link Core} by providing it with an instance of the Core library.
160
   */
161
  init (config) {
162
    const environment = this._environment === SANDBOX ? Environment.SANDBOX : Environment.PROD;
18!
163
    const cloudChoice = this._cloudChoice === GLOBAL_GCP ? CloudChoice.GLOBAL_GCP : CloudChoice.GLOBAL_MULTI;
18!
164
    const params = {
18✔
165
      ...(this._token && { token: this._token }),
18!
166
      ...(this._apiKey && { apiKey: this._apiKey }),
28✔
167
      experienceKey: this._experienceKey,
168
      locale: this._locale,
169
      experienceVersion: this._experienceVersion,
170
      additionalQueryParams: {
171
        jsLibVersion: LIB_VERSION
172
      },
173
      cloudRegion: this._cloudRegion,
174
      cloudChoice,
175
      environment,
176
      ...config
177
    };
178

179
    this._coreLibrary = provideCore(params);
18✔
180
  }
181

182
  /**
183
   * @returns {boolean} A boolean indicating if the {@link Core} has been
184
   *                    initailized.
185
   */
186
  isInitialized () {
187
    return !!this._coreLibrary;
×
188
  }
189

190
  /**
191
   * Send a FOLLOW_UP_QUERY analytics event for subsequent searches from the initial search.
192
   * The following search must contains a different query id from the previous search.
193
   *
194
   * @param {string} newQueryId - id of the new query
195
   * @param {string} searcher - searcher type of the new query ("UNIVERSAL" or "VERTICAL")
196
   */
197
  _reportFollowUpQueryEvent (newQueryId, searcher) {
198
    const previousQueryId = this.storage.get(StorageKeys.QUERY_ID);
1✔
199
    if (previousQueryId && previousQueryId !== newQueryId) {
1!
200
      const event = new AnalyticsEvent('FOLLOW_UP_QUERY').addOptions({ searcher });
×
201
      this._analyticsReporter.report(event);
×
202
    }
203
  }
204

205
  /**
206
   * Search in the context of a vertical
207
   * @param {string} verticalKey vertical ID for the search
208
   * @param {Object} options additional settings for the search.
209
   * @param {boolean} options.useFacets Whether to apply facets to this search, or to reset them instead
210
   * @param {boolean} options.resetPagination Whether to reset the search offset, going back to page 1.
211
   * @param {boolean} options.setQueryParams Whether to persist certain params in the url
212
   * @param {string} options.sendQueryId Whether to send the queryId currently in storage.
213
   *                                     If paging within a query, the same ID should be used.
214
   * @param {Object} query The query details
215
   * @param {string} query.input The input to search for
216
   * @param {boolean} query.append If true, adds the results of this query to the end of
217
   *                               the current results, defaults false
218
   */
219
  verticalSearch (verticalKey, options = {}, query = {}) {
7✔
220
    window.performance.mark('yext.answers.verticalQueryStart');
4✔
221
    if (!query.append) {
4!
222
      const verticalResults = this.storage.get(StorageKeys.VERTICAL_RESULTS);
4✔
223
      if (!verticalResults || verticalResults.searchState !== SearchStates.SEARCH_LOADING) {
4!
224
        this.storage.set(StorageKeys.VERTICAL_RESULTS, VerticalResults.searchLoading());
4✔
225
      }
226
      this.storage.set(StorageKeys.DIRECT_ANSWER, DirectAnswer.searchLoading());
4✔
227
      this.storage.set(StorageKeys.SPELL_CHECK, {});
4✔
228
      this.storage.set(StorageKeys.LOCATION_BIAS, LocationBias.searchLoading());
4✔
229
    }
230

231
    const { resetPagination, useFacets, sendQueryId, setQueryParams } = options;
4✔
232
    if (resetPagination) {
4!
233
      this.storage.delete(StorageKeys.SEARCH_OFFSET);
×
234
    }
235

236
    if (!useFacets) {
4!
237
      this.filterRegistry.setFacetFilterNodes([], []);
4✔
238
    }
239

240
    const context = this.storage.get(StorageKeys.API_CONTEXT);
4✔
241
    const referrerPageUrl = this.storage.get(StorageKeys.REFERRER_PAGE_URL);
4✔
242

243
    const defaultQueryInput = this.storage.get(StorageKeys.QUERY) || '';
4✔
244
    const parsedQuery = Object.assign({}, { input: defaultQueryInput }, query);
4✔
245

246
    if (setQueryParams) {
4!
247
      if (context) {
×
248
        this.storage.setWithPersist(StorageKeys.API_CONTEXT, context);
×
249
      }
250
      if (referrerPageUrl !== undefined) {
×
251
        this.storage.setWithPersist(StorageKeys.REFERRER_PAGE_URL, referrerPageUrl);
×
252
      }
253
    }
254

255
    const searchConfig = this.storage.get(StorageKeys.SEARCH_CONFIG) || {};
4✔
256
    if (!searchConfig.verticalKey) {
4!
257
      this.storage.set(StorageKeys.SEARCH_CONFIG, {
4✔
258
        ...searchConfig,
259
        verticalKey: verticalKey
260
      });
261
    }
262
    const locationRadius = this._getLocationRadius();
4✔
263
    const queryTrigger = this.storage.get(StorageKeys.QUERY_TRIGGER);
4✔
264
    const queryTriggerForApi = this.getQueryTriggerForSearchApi(queryTrigger);
4✔
265

266
    return this._coreLibrary
4✔
267
      .verticalSearch({
268
        verticalKey: verticalKey || searchConfig.verticalKey,
7✔
269
        limit: this.storage.get(StorageKeys.SEARCH_CONFIG)?.limitForVertical,
270
        location: this._getLocationPayload(),
271
        query: parsedQuery.input,
272
        queryId: sendQueryId && this.storage.get(StorageKeys.QUERY_ID),
4!
273
        retrieveFacets: this._isDynamicFiltersEnabled,
274
        facets: this.filterRegistry.getFacetsPayload(),
275
        staticFilter: this.filterRegistry.getStaticFilterPayload(),
276
        offset: this.storage.get(StorageKeys.SEARCH_OFFSET) || 0,
8✔
277
        skipSpellCheck: this.storage.get(StorageKeys.SKIP_SPELL_CHECK),
278
        queryTrigger: queryTriggerForApi,
279
        sessionId: this.getOrSetupSessionId(),
280
        sessionTrackingEnabled: this.storage.get(StorageKeys.SESSIONS_OPT_IN).value,
281
        sortBys: this.storage.get(StorageKeys.SORT_BYS),
282
        /** In the SDK a locationRadius of 0 means "unset my locationRadius" */
283
        locationRadius: locationRadius === 0 ? undefined : locationRadius,
4!
284
        context: context && JSON.parse(context),
5✔
285
        referrerPageUrl: referrerPageUrl,
286
        querySource: this.storage.get(StorageKeys.QUERY_SOURCE),
287
        additionalHttpHeaders: this._additionalHttpHeaders
288
      })
289
      .then(response => SearchDataTransformer.transformVertical(response, this._fieldFormatters, verticalKey))
4✔
290
      .then(data => {
291
        this._persistFacets();
×
292
        this._persistFilters();
×
293
        this._persistLocationRadius();
×
294
        this._reportFollowUpQueryEvent(data[StorageKeys.QUERY_ID], Searcher.VERTICAL);
×
295

NEW
296
        this.storage.set(StorageKeys.SEARCH_ID, data[StorageKeys.SEARCH_ID]);
×
297
        this.storage.set(StorageKeys.QUERY_ID, data[StorageKeys.QUERY_ID]);
×
298
        this.storage.set(StorageKeys.NAVIGATION, data[StorageKeys.NAVIGATION]);
×
299
        this.storage.set(StorageKeys.ALTERNATIVE_VERTICALS, data[StorageKeys.ALTERNATIVE_VERTICALS]);
×
300

301
        if (query.append) {
×
302
          const mergedResults = this.storage.get(StorageKeys.VERTICAL_RESULTS)
×
303
            .append(data[StorageKeys.VERTICAL_RESULTS]);
304
          this.storage.set(StorageKeys.VERTICAL_RESULTS, mergedResults);
×
305
          if (data[StorageKeys.DIRECT_ANSWER].answer) {
×
306
            this.storage.set(StorageKeys.DIRECT_ANSWER, data[StorageKeys.DIRECT_ANSWER]);
×
307
          }
308
        } else {
309
          this.storage.set(StorageKeys.VERTICAL_RESULTS, data[StorageKeys.VERTICAL_RESULTS]);
×
310
          this.storage.set(StorageKeys.DIRECT_ANSWER, data[StorageKeys.DIRECT_ANSWER]);
×
311
        }
312

313
        if (data[StorageKeys.DYNAMIC_FILTERS]) {
×
314
          this.storage.set(StorageKeys.DYNAMIC_FILTERS, data[StorageKeys.DYNAMIC_FILTERS]);
×
315
          this.storage.set(StorageKeys.RESULTS_HEADER, data[StorageKeys.DYNAMIC_FILTERS]);
×
316
        }
317
        if (data[StorageKeys.SPELL_CHECK]) {
×
318
          this.storage.set(StorageKeys.SPELL_CHECK, data[StorageKeys.SPELL_CHECK]);
×
319
        }
320
        if (data[StorageKeys.LOCATION_BIAS]) {
×
321
          this.storage.set(StorageKeys.LOCATION_BIAS, data[StorageKeys.LOCATION_BIAS]);
×
322
        }
323
        this.storage.delete(StorageKeys.SKIP_SPELL_CHECK);
×
324
        this.storage.delete(StorageKeys.QUERY_TRIGGER);
×
325

326
        const exposedParams = {
×
327
          verticalKey: verticalKey,
328
          queryString: parsedQuery.input,
329
          resultsCount: this.storage.get(StorageKeys.VERTICAL_RESULTS).resultsCount,
330
          resultsContext: data[StorageKeys.VERTICAL_RESULTS].resultsContext
331
        };
332
        const analyticsEvent = this.onVerticalSearch(exposedParams);
×
333
        if (typeof analyticsEvent === 'object') {
×
334
          this._analyticsReporter.report(AnalyticsEvent.fromData(analyticsEvent));
×
335
        }
336
        this.updateHistoryAfterSearch(queryTrigger);
×
337
        window.performance.mark('yext.answers.verticalQueryResponseRendered');
×
338
      })
339
      .catch(error => {
340
        this._markSearchComplete(Searcher.VERTICAL);
4✔
341
        throw error;
4✔
342
      })
343
      .catch(error => {
344
        console.error('The following problem was encountered during vertical search: ' + error);
4✔
345
      });
346
  }
347

348
  clearResults () {
349
    this.storage.set(StorageKeys.QUERY, null);
×
350
    this.storage.set(StorageKeys.QUERY_ID, '');
×
NEW
351
    this.storage.set(StorageKeys.SEARCH_ID, '');
×
352
    this.storage.set(StorageKeys.RESULTS_HEADER, {});
×
353
    this.storage.set(StorageKeys.SPELL_CHECK, {}); // TODO has a model but not cleared w new
×
354
    this.storage.set(StorageKeys.DYNAMIC_FILTERS, {}); // TODO has a model but not cleared w new
×
355
    this.storage.set(StorageKeys.QUESTION_SUBMISSION, new QuestionSubmission({}));
×
356
    this.storage.set(StorageKeys.NAVIGATION, new Navigation());
×
357
    this.storage.set(StorageKeys.ALTERNATIVE_VERTICALS, new AlternativeVerticals({}));
×
358
    this.storage.set(StorageKeys.DIRECT_ANSWER, new DirectAnswer({}));
×
359
    this.storage.set(StorageKeys.LOCATION_BIAS, new LocationBias({}));
×
360
    this.storage.set(StorageKeys.VERTICAL_RESULTS, new VerticalResults({}));
×
361
    this.storage.set(StorageKeys.UNIVERSAL_RESULTS, new UniversalResults({}));
×
NEW
362
    this.storage.set(StorageKeys.GENERATIVE_DIRECT_ANSWER, new GenerativeDirectAnswer({}));
×
363
  }
364

365
  /**
366
   * Page within the results of the last query
367
   */
368
  verticalPage () {
369
    this.triggerSearch(QueryTriggers.PAGINATION);
×
370
  }
371

372
  search (queryString, options = {}) {
5✔
373
    const urls = this._getUrls(queryString);
6✔
374
    window.performance.mark('yext.answers.universalQueryStart');
6✔
375
    const { setQueryParams } = options;
6✔
376
    const context = this.storage.get(StorageKeys.API_CONTEXT);
6✔
377
    const referrerPageUrl = this.storage.get(StorageKeys.REFERRER_PAGE_URL);
6✔
378

379
    if (setQueryParams) {
6✔
380
      if (context) {
1!
381
        this.storage.setWithPersist(StorageKeys.API_CONTEXT, context);
×
382
      }
383
      if (referrerPageUrl !== undefined) {
1!
384
        this.storage.setWithPersist(StorageKeys.REFERRER_PAGE_URL, referrerPageUrl);
1✔
385
      }
386
    }
387

388
    this.storage.set(StorageKeys.DIRECT_ANSWER, DirectAnswer.searchLoading());
6✔
389
    const universalResults = this.storage.get(StorageKeys.UNIVERSAL_RESULTS);
6✔
390
    if (!universalResults || universalResults.searchState !== SearchStates.SEARCH_LOADING) {
6✔
391
      this.storage.set(StorageKeys.UNIVERSAL_RESULTS, UniversalResults.searchLoading());
5✔
392
    }
393
    this.storage.set(StorageKeys.QUESTION_SUBMISSION, {});
6✔
394
    this.storage.set(StorageKeys.SPELL_CHECK, {});
6✔
395
    this.storage.set(StorageKeys.LOCATION_BIAS, LocationBias.searchLoading());
6✔
396

397
    const queryTrigger = this.storage.get(StorageKeys.QUERY_TRIGGER);
6✔
398
    const queryTriggerForApi = this.getQueryTriggerForSearchApi(queryTrigger);
6✔
399

400
    return this._coreLibrary
6✔
401
      .universalSearch({
402
        query: queryString,
403
        limit: this.storage.get(StorageKeys.SEARCH_CONFIG)?.universalLimit,
404
        location: this._getLocationPayload(),
405
        skipSpellCheck: this.storage.get(StorageKeys.SKIP_SPELL_CHECK),
406
        queryTrigger: queryTriggerForApi,
407
        sessionId: this.getOrSetupSessionId(),
408
        sessionTrackingEnabled: this.storage.get(StorageKeys.SESSIONS_OPT_IN).value,
409
        context: context && JSON.parse(context),
7✔
410
        referrerPageUrl: referrerPageUrl,
411
        querySource: this.storage.get(StorageKeys.QUERY_SOURCE),
412
        additionalHttpHeaders: this._additionalHttpHeaders
413
      })
414
      .then(response => SearchDataTransformer.transformUniversal(response, urls, this._fieldFormatters))
6✔
415
      .then(data => {
416
        this._reportFollowUpQueryEvent(data[StorageKeys.QUERY_ID], Searcher.UNIVERSAL);
1✔
417
        this.storage.set(StorageKeys.SEARCH_ID, data[StorageKeys.SEARCH_ID]);
1✔
UNCOV
418
        this.storage.set(StorageKeys.QUERY_ID, data[StorageKeys.QUERY_ID]);
×
UNCOV
419
        this.storage.set(StorageKeys.NAVIGATION, data[StorageKeys.NAVIGATION]);
×
UNCOV
420
        this.storage.set(StorageKeys.DIRECT_ANSWER, data[StorageKeys.DIRECT_ANSWER]);
×
UNCOV
421
        this.storage.set(StorageKeys.UNIVERSAL_RESULTS, data[StorageKeys.UNIVERSAL_RESULTS]);
×
UNCOV
422
        this.storage.set(StorageKeys.SPELL_CHECK, data[StorageKeys.SPELL_CHECK]);
×
UNCOV
423
        this.storage.set(StorageKeys.LOCATION_BIAS, data[StorageKeys.LOCATION_BIAS]);
×
424

UNCOV
425
        this.storage.delete(StorageKeys.SKIP_SPELL_CHECK);
×
UNCOV
426
        this.storage.delete(StorageKeys.QUERY_TRIGGER);
×
427

UNCOV
428
        const exposedParams = this._getOnUniversalSearchParams(
×
429
          data[StorageKeys.UNIVERSAL_RESULTS].sections,
430
          queryString);
UNCOV
431
        const analyticsEvent = this.onUniversalSearch(exposedParams);
×
UNCOV
432
        if (typeof analyticsEvent === 'object') {
×
433
          this._analyticsReporter.report(AnalyticsEvent.fromData(analyticsEvent));
×
434
        }
UNCOV
435
        this.updateHistoryAfterSearch(queryTrigger);
×
UNCOV
436
        window.performance.mark('yext.answers.universalQueryResponseRendered');
×
437
      })
438
      .catch(error => {
439
        this._markSearchComplete(Searcher.UNIVERSAL);
6✔
440
        throw error;
6✔
441
      })
442
      .catch(error => {
443
        console.error('The following problem was encountered during universal search: ' + error);
6✔
444
      });
445
  }
446

447
  /**
448
   * Attempt to generate a direct answer for the query from the given vertical results.
449
   *
450
   * @param {VerticalResults[]} verticalResults list of search-core VerticalResults
451
   * @param {string} searcher the type of search that generated these results (Universal or Vertical)
452
   */
453
  generativeDirectAnswer (verticalResults, searcher) {
NEW
454
    const searchId = this.storage.get(StorageKeys.SEARCH_ID);
×
NEW
455
    const searchTerm = this.storage.get(StorageKeys.QUERY);
×
NEW
456
    return this._coreLibrary
×
457
      .generativeDirectAnswer({
458
        searchId,
459
        searchTerm,
460
        results: verticalResults,
461
        additionalHttpHeaders: this._additionalHttpHeaders
462
      })
463
      .then(response => {
NEW
464
        const generativeDirectAnswer = GenerativeDirectAnswer.fromCore(response, searcher, verticalResults);
×
NEW
465
        this.storage.set(StorageKeys.GENERATIVE_DIRECT_ANSWER, generativeDirectAnswer);
×
466
      }).catch(error => {
NEW
467
        this.storage.delete(StorageKeys.GENERATIVE_DIRECT_ANSWER);
×
NEW
468
        console.error('Failed to generate direct answer with the following error: ' + error);
×
469
      });
470
  }
471

472
  /**
473
   * Update the search state of the results in storage to SEARCH_COMPLETE
474
   * when handling errors from universal and vertical search. This will
475
   * trigger a rerender of the components, which could potentially throw
476
   * a new error.
477
   *
478
   * @param {Searcher} searcherType
479
   */
480
  _markSearchComplete (searcherType) {
481
    const resultStorageKey = searcherType === Searcher.UNIVERSAL
10✔
482
      ? StorageKeys.UNIVERSAL_RESULTS
483
      : StorageKeys.VERTICAL_RESULTS;
484
    const results = this.storage.get(resultStorageKey);
10✔
485
    if (results && results.searchState !== SearchStates.SEARCH_COMPLETE) {
10!
486
      results.searchState = SearchStates.SEARCH_COMPLETE;
10✔
487
      this.storage.set(resultStorageKey, results);
10✔
488
    }
489
    const directanswer = this.storage.get(StorageKeys.DIRECT_ANSWER);
10✔
490
    if (directanswer && directanswer.searchState !== SearchStates.SEARCH_COMPLETE) {
10!
491
      directanswer.searchState = SearchStates.SEARCH_COMPLETE;
10✔
492
      this.storage.set(StorageKeys.DIRECT_ANSWER, directanswer);
10✔
493
    }
494
    const locationbias = this.storage.get(StorageKeys.LOCATION_BIAS);
10✔
495
    if (locationbias && locationbias.searchState !== SearchStates.SEARCH_COMPLETE) {
10!
496
      locationbias.searchState = SearchStates.SEARCH_COMPLETE;
10✔
497
      this.storage.set(StorageKeys.LOCATION_BIAS, locationbias);
10✔
498
    }
499
  }
500

501
  /**
502
   * Builds the object passed as a parameter to onUniversalSearch. This object
503
   * contains information about the universal search's query and result counts.
504
   *
505
   * @param {Array<Section>} sections The sections of results.
506
   * @param {string} queryString The search query.
507
   * @return {Object<string, ?>}
508
   */
509
  _getOnUniversalSearchParams (sections, queryString) {
UNCOV
510
    const resultsCountByVertical = sections.reduce(
×
511
      (resultsCountMap, section) => {
512
        const { verticalConfigId, resultsCount, results } = section;
×
513
        resultsCountMap[verticalConfigId] = {
×
514
          totalResultsCount: resultsCount,
515
          displayedResultsCount: results.length
516
        };
517
        return resultsCountMap;
×
518
      },
519
      {});
UNCOV
520
    const exposedParams = {
×
521
      queryString,
522
      sectionsCount: sections.length,
523
      resultsCountByVertical
524
    };
525

UNCOV
526
    return exposedParams;
×
527
  }
528

529
  /**
530
   * Given an input, query for a list of similar results and set into storage
531
   *
532
   * @param {string} input     the string to autocomplete
533
   * @param {string} namespace the namespace to use for the storage key
534
   */
535
  autoCompleteUniversal (input, namespace) {
536
    return this._coreLibrary
1✔
537
      .universalAutocomplete({
538
        input: input,
539
        sessionTrackingEnabled: this.storage.get(StorageKeys.SESSIONS_OPT_IN).value,
540
        additionalHttpHeaders: this._additionalHttpHeaders
541
      })
542
      .then(response => AutoCompleteResponseTransformer.transformAutoCompleteResponse(response))
1✔
543
      .then(data => {
544
        this.storage.set(`${StorageKeys.AUTOCOMPLETE}.${namespace}`, data);
×
545
        return data;
×
546
      }).catch(error => {
547
        console.error('Universal autocomplete failed with the following error: ' + error);
1✔
548
      });
549
  }
550

551
  /**
552
   * Given an input, query for a list of similar results in the provided vertical
553
   * and set into storage
554
   *
555
   * @param {string} input       the string to autocomplete
556
   * @param {string} namespace the namespace to use for the storage key
557
   * @param {string} verticalKey the vertical key for the experience
558
   */
559
  autoCompleteVertical (input, namespace, verticalKey) {
560
    return this._coreLibrary
1✔
561
      .verticalAutocomplete({
562
        input: input,
563
        verticalKey: verticalKey,
564
        sessionTrackingEnabled: this.storage.get(StorageKeys.SESSIONS_OPT_IN).value,
565
        additionalHttpHeaders: this._additionalHttpHeaders
566
      })
567
      .then(response => AutoCompleteResponseTransformer.transformAutoCompleteResponse(response))
1✔
568
      .then(data => {
569
        this.storage.set(`${StorageKeys.AUTOCOMPLETE}.${namespace}`, data);
×
570
        return data;
×
571
      }).catch(error => {
572
        console.error('Vertical autocomplete failed with the following error: ' + error);
1✔
573
      });
574
  }
575

576
  /**
577
   * Given an input, provide a list of suitable filters for autocompletion
578
   *
579
   * @param {string} input  the string to search for filters with
580
   * @param {object} config  the config to serach for filters with
581
   * @param {string} config.namespace  the namespace to use for the storage key
582
   * @param {string} config.verticalKey the vertical key for the config
583
   * @param {object} config.searchParameters  the search parameters for the config v2
584
   */
585
  autoCompleteFilter (input, config) {
586
    const searchParamFields = config.searchParameters.fields.map(field => ({
1✔
587
      fieldApiName: field.fieldId,
588
      entityType: field.entityTypeId,
589
      fetchEntities: field.shouldFetchEntities
590
    }));
591
    return this._coreLibrary
1✔
592
      .filterSearch({
593
        input: input,
594
        verticalKey: config.verticalKey,
595
        fields: searchParamFields,
596
        sectioned: config.searchParameters.sectioned,
597
        sessionTrackingEnabled: this.storage.get(StorageKeys.SESSIONS_OPT_IN).value,
598
        additionalHttpHeaders: this._additionalHttpHeaders
599
      })
600
      .then(
601
        response => AutoCompleteResponseTransformer.transformFilterSearchResponse(response),
1✔
602
        error => console.error(error))
×
603
      .then(data => {
604
        this.storage.set(`${StorageKeys.AUTOCOMPLETE}.${config.namespace}`, data);
×
605
      }).catch(error => {
606
        console.error('Filter search failed with the following error: ' + error);
1✔
607
      });
608
  }
609

610
  /**
611
   * Submits a question to the server and updates the underlying question model
612
   * @param {object} question The question object to submit to the server
613
   * @param {number} question.entityId The entity to associate with the question (required)
614
   * @param {string} question.site The "publisher" of the (e.g. 'FIRST_PARTY')
615
   * @param {string} question.name The name of the author
616
   * @param {string} question.email The email address of the author
617
   * @param {string} question.questionText The question
618
   * @param {string} question.questionDescription Additional information about the question
619
   */
620
  submitQuestion (question) {
621
    return this._coreLibrary
1✔
622
      .submitQuestion({
623
        ...question,
624
        sessionTrackingEnabled: this.storage.get(StorageKeys.SESSIONS_OPT_IN).value,
625
        additionalHttpHeaders: this._additionalHttpHeaders
626
      })
627
      .then(() => {
628
        this.storage.set(
1✔
629
          StorageKeys.QUESTION_SUBMISSION,
630
          QuestionSubmission.submitted());
631
      }).catch(error => {
632
        console.error('Question submission failed with the following error: ' + error);
×
633
        throw error;
×
634
      });
635
  }
636

637
  /**
638
   * Stores the given sortBy into storage, to be used for the next search
639
   * @param {Object} sortByOptions
640
   */
641
  setSortBys (...sortByOptions) {
642
    const sortBys = sortByOptions.map(option => {
×
643
      return {
×
644
        type: option.type,
645
        field: option.field,
646
        direction: option.direction
647
      };
648
    });
649
    this.storage.setWithPersist(StorageKeys.SORT_BYS, sortBys);
×
650
  }
651

652
  /**
653
   * Clears the sortBys key in storage.
654
   */
655
  clearSortBys () {
656
    this.storage.delete(StorageKeys.SORT_BYS);
×
657
  }
658

659
  /**
660
   * Stores the given query into storage, to be used for the next search
661
   * @param {string} query the query to store
662
   */
663
  setQuery (query) {
664
    this.storage.setWithPersist(StorageKeys.QUERY, query);
1✔
665
  }
666

667
  /**
668
   * Stores the provided query ID, to be used in analytics
669
   * @param {string} queryId The query id to store
670
   */
671
  setQueryId (queryId) {
672
    this.storage.set(StorageKeys.QUERY_ID, queryId);
×
673
  }
674

675
  triggerSearch (queryTrigger, newQuery) {
676
    const query = newQuery !== undefined
1!
677
      ? newQuery
678
      : this.storage.get(StorageKeys.QUERY) || '';
1!
679
    queryTrigger
1!
680
      ? this.storage.set(StorageKeys.QUERY_TRIGGER, queryTrigger)
681
      : this.storage.delete(StorageKeys.QUERY_TRIGGER);
682
    this.setQuery(query);
1✔
683
  }
684

685
  /**
686
   * Get all of the {@link FilterNode}s for static filters.
687
   * @returns {Array<FilterNode>}
688
   */
689
  getStaticFilterNodes () {
690
    return this.filterRegistry.getStaticFilterNodes();
×
691
  }
692

693
  /**
694
   * Get all of the active {@link FilterNode}s for facets.
695
   * @returns {Array<FilterNode>}
696
   */
697
  getFacetFilterNodes () {
698
    return this.filterRegistry.getFacetFilterNodes();
×
699
  }
700

701
  /**
702
   * Get the {@link FilterNode} affecting the locationRadius url parameter.
703
   * @returns {FilterNode}
704
   */
705
  getLocationRadiusFilterNode () {
706
    return this.filterRegistry.getFilterNodeByKey(StorageKeys.LOCATION_RADIUS_FILTER_NODE);
4✔
707
  }
708

709
  /**
710
   * Sets the filter nodes used for the current facet filters.
711
   *
712
   * Because the search response only sends back one
713
   * set of facet filters, there can only be one active facet filter node
714
   * at a time.
715
   * @param {Array<string>} availableFieldIds
716
   * @param {Array<FilterNode>} filterNodes
717
   */
718
  setFacetFilterNodes (availableFieldids = [], filterNodes = []) {
×
719
    this.filterRegistry.setFacetFilterNodes(availableFieldids, filterNodes);
×
720
  }
721

722
  /**
723
   * Sets the specified {@link FilterNode} under the given key.
724
   * Will replace a preexisting node if there is one.
725
   * @param {string} namespace
726
   * @param {FilterNode} filterNode
727
   */
728
  setStaticFilterNodes (namespace, filterNode) {
729
    this.filterRegistry.setStaticFilterNodes(namespace, filterNode);
×
730
  }
731

732
  /**
733
   * Sets the locationRadius filterNode.
734
   * @param {FilterNode} filterNode
735
   */
736
  setLocationRadiusFilterNode (filterNode) {
737
    this.filterRegistry.setLocationRadiusFilterNode(filterNode);
×
738
  }
739

740
  /**
741
   * Remove the static FilterNode with this namespace.
742
   * @param {string} namespace
743
   */
744
  clearStaticFilterNode (namespace) {
745
    this.filterRegistry.clearStaticFilterNode(namespace);
×
746
  }
747

748
  /**
749
   * Remove all facet FilterNodes.
750
   */
751
  clearFacetFilterNodes () {
752
    this.filterRegistry.clearFacetFilterNodes();
×
753
  }
754

755
  /**
756
   * Clears the locationRadius filterNode.
757
   */
758
  clearLocationRadiusFilterNode () {
759
    this.filterRegistry.clearLocationRadiusFilterNode();
×
760
  }
761

762
  /**
763
   * Gets the location object needed for search-core
764
   *
765
   * @returns {LatLong|undefined} from search-core
766
   */
767
  _getLocationPayload () {
768
    const geolocation = this.storage.get(StorageKeys.GEOLOCATION);
10✔
769
    return geolocation && {
10!
770
      latitude: geolocation.lat,
771
      longitude: geolocation.lng
772
    };
773
  }
774

775
  /**
776
   * Returns the query trigger for the search API given the SDK query trigger
777
   * @param {QueryTriggers} queryTrigger SDK query trigger
778
   * @returns {QueryTriggers} query trigger if accepted by the search API, null o/w
779
   */
780
  getQueryTriggerForSearchApi (queryTrigger) {
781
    if (![QueryTriggers.INITIALIZE, QueryTriggers.SUGGEST, QueryTriggers.VOICE_SEARCH]
10✔
782
      .includes(queryTrigger)) {
783
      return null;
9✔
784
    }
785
    return queryTrigger;
1✔
786
  }
787

788
  /**
789
   * Depending on the QUERY_TRIGGER, either replaces the history state
790
   * for searches on load/back navigation (INITIALIZE, SUGGEST, QUERY_PARAMETER),
791
   * or pushes a new state.
792
   *
793
   * @param {QueryTriggers} queryTrigger SDK query trigger
794
   * @returns {boolean}
795
   */
796
  updateHistoryAfterSearch (queryTrigger) {
UNCOV
797
    const replaceStateTriggers = [
×
798
      QueryTriggers.INITIALIZE,
799
      QueryTriggers.SUGGEST,
800
      QueryTriggers.QUERY_PARAMETER
801
    ];
UNCOV
802
    if (replaceStateTriggers.includes(queryTrigger)) {
×
UNCOV
803
      this.storage.replaceHistoryWithState();
×
804
    } else {
805
      this.storage.pushStateToHistory();
×
806
    }
807
  }
808

809
  /**
810
   * Returns the current `locationRadius` state
811
   * @returns {number|null}
812
   */
813
  _getLocationRadius () {
814
    const locationRadiusFilterNode = this.getLocationRadiusFilterNode();
4✔
815
    return locationRadiusFilterNode
4!
816
      ? locationRadiusFilterNode.getFilter().value
817
      : null;
818
  }
819

820
  /**
821
   * Persists the current `facetFilters` state into the URL.
822
   */
823
  _persistFacets () {
824
    const persistedFacets = this.filterRegistry.getFacets();
×
825
    this.storage.setWithPersist(StorageKeys.PERSISTED_FACETS, persistedFacets);
×
826
  }
827

828
  /**
829
   * Persists the current `filters` state into the URL.
830
   */
831
  _persistFilters () {
832
    const totalFilterNode = this.filterRegistry.getAllStaticFilterNodesCombined();
×
833
    const persistedFilter = totalFilterNode.getFilter();
×
834
    this.storage.setWithPersist(StorageKeys.PERSISTED_FILTER, persistedFilter);
×
835
  }
836

837
  /**
838
   * Persists the current `locationRadius` state into the URL.
839
   */
840
  _persistLocationRadius () {
841
    const locationRadius = this._getLocationRadius();
×
842
    if (locationRadius || locationRadius === 0) {
×
843
      this.storage.setWithPersist(StorageKeys.PERSISTED_LOCATION_RADIUS, locationRadius);
×
844
    } else {
845
      this.storage.delete(StorageKeys.PERSISTED_LOCATION_RADIUS);
×
846
    }
847
  }
848

849
  enableDynamicFilters () {
850
    this._isDynamicFiltersEnabled = true;
×
851
  }
852

853
  on (evt, storageKey, cb) {
854
    this.storage.registerListener({
×
855
      eventType: evt,
856
      storageKey: storageKey,
857
      callback: cb
858
    });
859
    return this.storage;
×
860
  }
861

862
  /**
863
   * This is needed to support very old usages of the SDK that have not been updated
864
   * to use StorageKeys.VERTICAL_PAGES_CONFIG
865
   */
866
  _getUrls (query) {
867
    const nav = this._componentManager.getActiveComponent('Navigation');
1✔
868
    if (!nav) {
1!
869
      return undefined;
1✔
870
    }
871

872
    const tabs = nav.getState('tabs');
×
873
    const urls = {};
×
874

875
    if (tabs && Array.isArray(tabs)) {
×
876
      for (let i = 0; i < tabs.length; i++) {
×
877
        const params = new SearchParams(tabs[i].url.split('?')[1]);
×
878
        params.set('query', query);
×
879

880
        let url = tabs[i].baseUrl;
×
881
        if (params.toString().length > 0) {
×
882
          url += '?' + params.toString();
×
883
        }
884
        urls[tabs[i].configId] = url;
×
885
      }
886
    }
887
    return urls;
×
888
  }
889

890
  getOrSetupSessionId () {
891
    if (this.storage.get(StorageKeys.SESSIONS_OPT_IN).value) {
10✔
892
      try {
3✔
893
        let sessionId = window.sessionStorage.getItem('sessionId');
3✔
894
        if (!sessionId) {
3✔
895
          sessionId = generateUUID();
2✔
896
          window.sessionStorage.setItem('sessionId', sessionId);
2✔
897
        }
898
        return sessionId;
3✔
899
      } catch (err) {
900
        console.warn('Unable to use browser sessionStorage for sessionId.\n', err);
×
901
      }
902
    }
903
    return null;
7✔
904
  }
905
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc