From 50ff2ab6cb4fad83bf90c229850ed49d16067383 Mon Sep 17 00:00:00 2001 From: Yen Truong <36055303+yen-tt@users.noreply.github.com> Date: Tue, 17 May 2022 09:07:22 -0400 Subject: [PATCH] keep results on search failure (#1747) Rendering failures leading to the catch statements in Universal and Vertical search would clear all results. User would still expects other results from the response to render on page. This pr updates the `storage.set` calls to only update the searchState, instead of storing a new empty results, so the non-problematic result sets can still be render. The catch statements in core.js will terminate any further rendering of sub components once it encounter a problematic sub component. User expects sdk to render as much as it can. To ensure non-problematic sub-components can still render, this pr added more catch statements in component.js to gracefully handle errors coming from individual child component's data transformation, initialization and mount stage. J=none TEST=manual use `dev-keep-data-on-search-failure` for SDK version ([from this test branch](https://github.com/yext/answers-search-ui/pull/1745)) in the config file of the site with the issue. Searched 'Listings', and see that the same error appeared in console but the other cards still render as normal. use `dev-dont-clear-data-on-failure` to test changes in this pr through local html pages that use SDK directly. See that errors are logged but the loading icon is reset to complete during error handling process --- src/core/core.js | 53 ++++++++++++++++++++++++------ src/core/models/directanswer.js | 1 - src/core/models/verticalresults.js | 2 -- src/ui/components/component.js | 32 ++++++++++++++---- 4 files changed, 68 insertions(+), 20 deletions(-) diff --git a/src/core/core.js b/src/core/core.js index b1288e975..b18aa319f 100644 --- a/src/core/core.js +++ b/src/core/core.js @@ -321,11 +321,13 @@ export default class Core { } this.updateHistoryAfterSearch(queryTrigger); window.performance.mark('yext.answers.verticalQueryResponseRendered'); - }).catch(error => { - console.error('Vertical search failed with the following error: ' + error); - this.storage.set(StorageKeys.VERTICAL_RESULTS, new VerticalResults()); - this.storage.set(StorageKeys.DIRECT_ANSWER, new DirectAnswer()); - this.storage.set(StorageKeys.LOCATION_BIAS, new LocationBias({})); + }) + .catch(error => { + this._markSearchComplete(Searcher.VERTICAL); + throw error; + }) + .catch(error => { + console.error('The following problem was encountered during vertical search: ' + error); }); } @@ -415,14 +417,45 @@ export default class Core { } this.updateHistoryAfterSearch(queryTrigger); window.performance.mark('yext.answers.universalQueryResponseRendered'); - }).catch(error => { - console.error('Universal search failed with the following error: ' + error); - this.storage.set(StorageKeys.UNIVERSAL_RESULTS, new UniversalResults({})); - this.storage.set(StorageKeys.DIRECT_ANSWER, new DirectAnswer()); - this.storage.set(StorageKeys.LOCATION_BIAS, new LocationBias({})); + }) + .catch(error => { + this._markSearchComplete(Searcher.UNIVERSAL); + throw error; + }) + .catch(error => { + console.error('The following problem was encountered during universal search: ' + error); }); } + /** + * Update the search state of the results in storage to SEARCH_COMPLETE + * when handling errors from universal and vertical search. This will + * trigger a rerender of the components, which could potentially throw + * a new error. + * + * @param {Searcher} searcherType + */ + _markSearchComplete (searcherType) { + const resultStorageKey = searcherType === Searcher.UNIVERSAL + ? StorageKeys.UNIVERSAL_RESULTS + : StorageKeys.VERTICAL_RESULTS; + const results = this.storage.get(resultStorageKey); + if (results && results.searchState !== SearchStates.SEARCH_COMPLETE) { + results.searchState = SearchStates.SEARCH_COMPLETE; + this.storage.set(resultStorageKey, results); + } + const directanswer = this.storage.get(StorageKeys.DIRECT_ANSWER); + if (directanswer && directanswer.searchState !== SearchStates.SEARCH_COMPLETE) { + directanswer.searchState = SearchStates.SEARCH_COMPLETE; + this.storage.set(StorageKeys.DIRECT_ANSWER, directanswer); + } + const locationbias = this.storage.get(StorageKeys.LOCATION_BIAS); + if (locationbias && locationbias.searchState !== SearchStates.SEARCH_COMPLETE) { + locationbias.searchState = SearchStates.SEARCH_COMPLETE; + this.storage.set(StorageKeys.LOCATION_BIAS, locationbias); + } + } + /** * Builds the object passed as a parameter to onUniversalSearch. This object * contains information about the universal search's query and result counts. diff --git a/src/core/models/directanswer.js b/src/core/models/directanswer.js index 4717ba3f8..2be7219f7 100644 --- a/src/core/models/directanswer.js +++ b/src/core/models/directanswer.js @@ -5,7 +5,6 @@ import SearchStates from '../storage/searchstates'; export default class DirectAnswer { constructor (directAnswer = {}) { Object.assign(this, { searchState: SearchStates.SEARCH_COMPLETE }, directAnswer); - Object.freeze(this); } /** diff --git a/src/core/models/verticalresults.js b/src/core/models/verticalresults.js index e20b1aa1f..5623181b3 100644 --- a/src/core/models/verticalresults.js +++ b/src/core/models/verticalresults.js @@ -17,8 +17,6 @@ export default class VerticalResults { * @type {ResultsContext} */ this.resultsContext = data.resultsContext; - - Object.freeze(this); } /** diff --git a/src/ui/components/component.js b/src/ui/components/component.js index baaa715c5..f8ba7d539 100644 --- a/src/ui/components/component.js +++ b/src/ui/components/component.js @@ -376,20 +376,38 @@ export default class Component { // Process the DOM to determine if we should create // in-memory sub-components for rendering const domComponents = DOM.queryAll(this._container, '[data-component]:not([data-is-component-mounted])'); - const data = this.transformData - ? this.transformData(cloneDeep(this._state.get())) - : this._state.get(); - domComponents.forEach(c => this._createSubcomponent(c, data)); - + let data; + try { + data = this.transformData + ? this.transformData(cloneDeep(this._state.get())) + : this._state.get(); + } catch (e) { + console.error(`The following problem occurred while transforming data for sub-components of ${this.name}: `, e); + } + domComponents.forEach(c => { + try { + this._createSubcomponent(c, data); + } catch (e) { + console.error('The following problem occurred while initializing sub-component: ', c, e); + } + }); if (this._progressivelyRenderChildren) { this._children.forEach(child => { setTimeout(() => { - child.mount(); + try { + child.mount(); + } catch (e) { + console.error('The following problem occurred while mounting sub-component: ', child, e); + } }); }); } else { this._children.forEach(child => { - child.mount(); + try { + child.mount(); + } catch (e) { + console.error('The following problem occurred while mounting sub-component: ', child, e); + } }); }