diff --git a/packages/headless/src/api/search/search/result.ts b/packages/headless/src/api/search/search/result.ts index 2158ed85746..cb61b025701 100644 --- a/packages/headless/src/api/search/search/result.ts +++ b/packages/headless/src/api/search/search/result.ts @@ -177,4 +177,9 @@ export interface Result { * Whether the result item has been previously viewed by one of the users specified in the `canSeeUserProfileOf` section of the [search token](https://docs.coveo.com/en/13/api-reference/search-api#tag/Search-V2/operation/token) generated to perform the search request. */ isUserActionView: boolean; + + /** + * The unique identifier of the search that returned this result. + */ + searchUid: string; } diff --git a/packages/headless/src/features/result-preview/result-preview-analytics-actions.test.ts b/packages/headless/src/features/result-preview/result-preview-analytics-actions.test.ts index fa94246a891..4baeb18ba8d 100644 --- a/packages/headless/src/features/result-preview/result-preview-analytics-actions.test.ts +++ b/packages/headless/src/features/result-preview/result-preview-analytics-actions.test.ts @@ -13,7 +13,7 @@ jest.mock('@coveo/relay'); jest.mock('coveo.analytics'); describe('#logDocumentQuickview', () => { - const testResult = buildMockNonEmptyResult(); + const testResult = buildMockNonEmptyResult({searchUid: 'someid'}); let engine: SearchEngine; const makeDocumentQuickview = jest.fn(); const emit = jest.fn(); diff --git a/packages/headless/src/features/result-preview/result-preview-analytics-actions.ts b/packages/headless/src/features/result-preview/result-preview-analytics-actions.ts index 270e6f3efcf..6978211f665 100644 --- a/packages/headless/src/features/result-preview/result-preview-analytics-actions.ts +++ b/packages/headless/src/features/result-preview/result-preview-analytics-actions.ts @@ -1,4 +1,5 @@ import {ItemClick} from '@coveo/relay-event-types'; +import {SearchAnalyticsProvider} from '../../api/analytics/search-analytics'; import {Result} from '../../api/search/search/result'; import { ClickAction, @@ -17,6 +18,11 @@ export const logDocumentQuickview = (result: Result): ClickAction => { const id = documentIdentifier(result); return client.makeDocumentQuickview(info, id); }, + __legacy__provider: (getState) => { + const customAnalyticsProvider = new SearchAnalyticsProvider(getState); + customAnalyticsProvider.getSearchUID = () => result.searchUid ?? ''; + return customAnalyticsProvider; + }, analyticsType: 'itemClick', analyticsPayloadBuilder: (state): ItemClick => { const docInfo = partialDocumentInformation(result, state); diff --git a/packages/headless/src/features/result/result-analytics-actions.test.ts b/packages/headless/src/features/result/result-analytics-actions.test.ts index dcad7dd2850..7a63d6cb44b 100644 --- a/packages/headless/src/features/result/result-analytics-actions.test.ts +++ b/packages/headless/src/features/result/result-analytics-actions.test.ts @@ -13,7 +13,9 @@ jest.mock('@coveo/relay'); jest.mock('coveo.analytics'); describe('#logDocumentOpen', () => { - const testResult = buildMockNonEmptyResult(); + const testResult = buildMockNonEmptyResult({ + searchUid: 'example searchUid', + }); let engine: SearchEngine; const makeDocumentOpen = jest.fn(); const emit = jest.fn(); diff --git a/packages/headless/src/features/result/result-analytics-actions.ts b/packages/headless/src/features/result/result-analytics-actions.ts index 5e82de5a55b..dce02e87164 100644 --- a/packages/headless/src/features/result/result-analytics-actions.ts +++ b/packages/headless/src/features/result/result-analytics-actions.ts @@ -1,4 +1,5 @@ import {ItemClick} from '@coveo/relay-event-types'; +import {SearchAnalyticsProvider} from '../../api/analytics/search-analytics'; import {Result} from '../../api/search/search/result'; import { partialDocumentInformation, @@ -18,6 +19,11 @@ export const logDocumentOpen = (result: Result): ClickAction => documentIdentifier(result) ); }, + __legacy__provider: (getState) => { + const customAnalyticsProvider = new SearchAnalyticsProvider(getState); + customAnalyticsProvider.getSearchUID = () => result.searchUid ?? ''; + return customAnalyticsProvider; + }, analyticsType: 'itemClick', analyticsPayloadBuilder: (state): ItemClick => { const docInfo = partialDocumentInformation(result, state); diff --git a/packages/headless/src/features/search/search-slice.test.ts b/packages/headless/src/features/search/search-slice.test.ts index a6835e83983..e4c5109075f 100644 --- a/packages/headless/src/features/search/search-slice.test.ts +++ b/packages/headless/src/features/search/search-slice.test.ts @@ -148,6 +148,22 @@ describe('search-slice', () => { expect(finalState.searchResponseId).toBe('a_new_id'); }); + it('when a executeSearch fulfilled is received, results should contain last #searchUid', () => { + state.searchResponseId = 'an_initial_id'; + const response = buildMockSearchResponse({results: [newResult]}); + response.searchUid = 'a_new_id'; + const search = buildMockSearch({ + response, + }); + + const finalState = searchReducer( + state, + executeSearch.fulfilled(search, '', {legacy: logSearchboxSubmit()}) + ); + + expect(finalState.results[0].searchUid).toBe('a_new_id'); + }); + it('when a executeSearch fulfilled is received, it overwrites the #questionAnswer', () => { state.questionAnswer = buildMockQuestionsAnswers({ question: 'When?', @@ -200,6 +216,26 @@ describe('search-slice', () => { expect(finalState.searchResponseId).toBe('an_initial_id'); }); + it('when a fetchMoreResults fulfilled is received, previous results keep their #searchUiD', () => { + state.results = state.results.map((result) => ({ + ...result, + searchUid: 'an_initial_id', + })); + const response = buildMockSearchResponse({results: [newResult]}); + response.searchUid = 'a_new_id'; + const search = buildMockSearch({ + response, + }); + + const finalState = searchReducer( + state, + fetchMoreResults.fulfilled(search, '') + ); + + expect(finalState.results[0].searchUid).toBe('an_initial_id'); + expect(finalState.results[1].searchUid).toBe('a_new_id'); + }); + it('when a fetchMoreResults fulfilled is received, keeps the previous #questionAnswer', () => { const originalQuestionAnswers = buildMockQuestionsAnswers({ question: 'Why?', diff --git a/packages/headless/src/features/search/search-slice.ts b/packages/headless/src/features/search/search-slice.ts index 7281201000d..a48fce9e50b 100644 --- a/packages/headless/src/features/search/search-slice.ts +++ b/packages/headless/src/features/search/search-slice.ts @@ -44,7 +44,10 @@ function handleFulfilledNewSearch( action: ReturnType ) { handleFulfilledSearch(state, action); - state.results = action.payload.response.results; + state.results = action.payload.response.results.map((result) => ({ + ...result, + searchUid: action.payload.response.searchUid, + })); state.searchResponseId = action.payload.response.searchUid; state.questionAnswer = action.payload.response.questionAnswer; state.extendedResults = action.payload.response.extendedResults; @@ -81,7 +84,13 @@ export const searchReducer = createReducer( }); builder.addCase(fetchMoreResults.fulfilled, (state, action) => { handleFulfilledSearch(state, action); - state.results = [...state.results, ...action.payload.response.results]; + state.results = [ + ...state.results, + ...action.payload.response.results.map((result) => ({ + ...result, + searchUid: action.payload.response.searchUid, + })), + ]; }); builder.addCase(fetchPage.fulfilled, (state, action) => { handleFulfilledSearch(state, action); diff --git a/packages/headless/src/test/mock-result.ts b/packages/headless/src/test/mock-result.ts index f9c6008b892..11e668a6ea5 100644 --- a/packages/headless/src/test/mock-result.ts +++ b/packages/headless/src/test/mock-result.ts @@ -33,6 +33,7 @@ export function buildMockResult(config: Partial = {}): Result { absentTerms: [], raw: buildMockRaw(), isUserActionView: false, + searchUid: '', ...config, }; }