diff --git a/.github/actions/playwright-atomic/action.yml b/.github/actions/playwright-atomic/action.yml index 7d193e23863..26b15e05f2f 100644 --- a/.github/actions/playwright-atomic/action.yml +++ b/.github/actions/playwright-atomic/action.yml @@ -7,6 +7,9 @@ inputs: shardTotal: description: 'The total number of shards' required: true + testsToRun: + description: 'The tests to run' + required: true uploadArtifacts: description: 'Whether to upload artifacts' required: false @@ -19,7 +22,7 @@ runs: working-directory: packages/atomic shell: bash - name: Run Playwright tests - run: npx playwright test --shard=${{ inputs.shardIndex }}/${{ inputs.shardTotal }} + run: npx playwright test ${{ inputs.testsToRun }} --shard=${{ inputs.shardIndex }}/${{ inputs.shardTotal }} working-directory: packages/atomic shell: bash - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4 diff --git a/.github/workflows/prbot.yml b/.github/workflows/prbot.yml index aad9589e3c1..d06bda24535 100644 --- a/.github/workflows/prbot.yml +++ b/.github/workflows/prbot.yml @@ -78,41 +78,45 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - uses: ./.github/actions/setup - uses: ./.github/actions/e2e-atomic-csp + prepare-playwright-atomic: + name: 'Determine Playwright E2E tests to run' + if: ${{ always() && github.event_name == 'pull_request'}} + runs-on: ubuntu-latest + env: + maximumShards: 24 + outputs: + testsToRun: ${{ steps.determine-tests.outputs.testsToRun }} + shardIndex: ${{ steps.set-matrix.outputs.shardIndex }} + shardTotal: ${{ steps.set-matrix.outputs.shardTotal }} + steps: + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 + with: + fetch-depth: 0 + - uses: ./.github/actions/setup + - run: npm run build + - name: Identify E2E Test Files to run + id: determine-tests + run: node ./scripts/ci/find-tests.mjs testsToRun + env: + projectRoot: ${{ github.workspace }} + shell: bash + - name: Determine Shard Values + id: set-matrix + run: node ./scripts/ci/determine-shard.mjs shardIndex shardTotal + env: + testsToRun: ${{ steps.determine-tests.outputs.testsToRun }} + maximumShards: ${{ env.maximumShards }} + shell: bash playwright-atomic: name: 'Run Playwright tests for Atomic' - needs: build + needs: prepare-playwright-atomic + if: ${{ always() }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: - shardIndex: - [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - ] - shardTotal: [24] + shardIndex: ${{fromJson(needs.prepare-playwright-atomic.outputs.shardIndex || '[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24]')}} + shardTotal: ${{fromJson(needs.prepare-playwright-atomic.outputs.shardTotal || '[24]')}} steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - uses: ./.github/actions/setup @@ -121,6 +125,7 @@ jobs: with: shardIndex: ${{ matrix.shardIndex }} shardTotal: ${{ matrix.shardTotal }} + testsToRun: ${{ needs.prepare-playwright-atomic.outputs.testsToRun }} merge-playwright-reports: name: 'Merge Playwright reports' environment: PR Artifacts diff --git a/packages/atomic/src/components/recommendations/atomic-recs-list/atomic-recs-list.tsx b/packages/atomic/src/components/recommendations/atomic-recs-list/atomic-recs-list.tsx index eea0f7a3790..a131f53d691 100644 --- a/packages/atomic/src/components/recommendations/atomic-recs-list/atomic-recs-list.tsx +++ b/packages/atomic/src/components/recommendations/atomic-recs-list/atomic-recs-list.tsx @@ -205,6 +205,7 @@ export class AtomicRecsList implements InitializableComponent { } private get recommendationListStateWithAugment() { + // TODO: some changes return { ...this.recommendationListState, firstRequestExecuted: diff --git a/packages/atomic/src/components/search/atomic-did-you-mean/atomic-did-you-mean.tsx b/packages/atomic/src/components/search/atomic-did-you-mean/atomic-did-you-mean.tsx index eb6d510c8f9..565a7cb04be 100644 --- a/packages/atomic/src/components/search/atomic-did-you-mean/atomic-did-you-mean.tsx +++ b/packages/atomic/src/components/search/atomic-did-you-mean/atomic-did-you-mean.tsx @@ -6,7 +6,7 @@ import { buildQueryTrigger, QueryTriggerState, } from '@coveo/headless'; -import {Component, h, Prop, State} from '@stencil/core'; +import {Component, h, Prop, State, Watch} from '@stencil/core'; import { BindStateToController, InitializableComponent, @@ -68,6 +68,11 @@ export class AtomicDidYouMean implements InitializableComponent { @Prop({reflect: true}) public queryCorrectionMode: 'legacy' | 'next' = 'next'; + @Watch('queryCorrectionMode') + public updateQueryCorrectionMode() { + this.didYouMean.updateQueryCorrectionMode(this.queryCorrectionMode); + } + public initialize() { this.didYouMean = buildDidYouMean(this.bindings.engine, { options: { diff --git a/packages/headless/src/api/platform-service-params.ts b/packages/headless/src/api/platform-service-params.ts index 75849c72276..ca23e822d8f 100644 --- a/packages/headless/src/api/platform-service-params.ts +++ b/packages/headless/src/api/platform-service-params.ts @@ -28,10 +28,6 @@ export interface ClientIDParam { clientId?: string; } -export interface VisitorIDParam { - visitorId?: string; -} - export interface FoldingParam { filterField?: string; parentField?: string; diff --git a/packages/headless/src/api/search/facet-search/base/base-facet-search-request.ts b/packages/headless/src/api/search/facet-search/base/base-facet-search-request.ts index 6d5d9674b94..456d9812903 100644 --- a/packages/headless/src/api/search/facet-search/base/base-facet-search-request.ts +++ b/packages/headless/src/api/search/facet-search/base/base-facet-search-request.ts @@ -1,4 +1,4 @@ -import {BaseParam, VisitorIDParam} from '../../../platform-service-params.js'; +import {BaseParam} from '../../../platform-service-params.js'; import {AuthenticationParam} from '../../search-api-params.js'; import {SearchRequest} from '../../search/search-request.js'; @@ -16,7 +16,6 @@ export interface FacetSearchRequestOptions { export interface BaseFacetSearchRequest extends FacetSearchRequestOptions, BaseParam, - VisitorIDParam, AuthenticationParam { field: string; searchContext?: SearchRequest; diff --git a/packages/headless/src/api/search/html/html-api-client.ts b/packages/headless/src/api/search/html/html-api-client.ts index bbc7b405e13..1145979467f 100644 --- a/packages/headless/src/api/search/html/html-api-client.ts +++ b/packages/headless/src/api/search/html/html-api-client.ts @@ -41,9 +41,6 @@ export const buildContentURL = (req: HtmlRequest, path: string) => { if (req.requestedOutputSize !== undefined) { url.addParam('requestedOutputSize', `${req.requestedOutputSize}`); } - if (req.visitorId !== undefined) { - url.addParam('visitorId', `${req.visitorId}`); - } return url.href; }; diff --git a/packages/headless/src/api/search/html/html-request.ts b/packages/headless/src/api/search/html/html-request.ts index 19b91020907..90fea551b28 100644 --- a/packages/headless/src/api/search/html/html-request.ts +++ b/packages/headless/src/api/search/html/html-request.ts @@ -1,4 +1,4 @@ -import {BaseParam, VisitorIDParam} from '../../platform-service-params.js'; +import {BaseParam} from '../../platform-service-params.js'; import {AuthenticationParam} from '../search-api-params.js'; export interface HtmlRequestOptions { @@ -8,8 +8,7 @@ export interface HtmlRequestOptions { export type HtmlRequest = BaseParam & HtmlRequestOptions & - AuthenticationParam & - VisitorIDParam & { + AuthenticationParam & { enableNavigation: boolean; requestedOutputSize: number; q: string; diff --git a/packages/headless/src/api/search/plan/plan-request.ts b/packages/headless/src/api/search/plan/plan-request.ts index c3f94208456..8ef74e3e7d2 100644 --- a/packages/headless/src/api/search/plan/plan-request.ts +++ b/packages/headless/src/api/search/plan/plan-request.ts @@ -2,7 +2,6 @@ import { BaseParam, ContextParam, LocaleParam, - VisitorIDParam, } from '../../platform-service-params.js'; import { AnalyticsParam, @@ -20,6 +19,5 @@ export type PlanRequest = BaseParam & PipelineParam & LocaleParam & TimezoneParam & - VisitorIDParam & AnalyticsParam & AuthenticationParam; diff --git a/packages/headless/src/api/search/query-suggest/query-suggest-request.ts b/packages/headless/src/api/search/query-suggest/query-suggest-request.ts index 7a53eb3fbf0..280f49ad90b 100644 --- a/packages/headless/src/api/search/query-suggest/query-suggest-request.ts +++ b/packages/headless/src/api/search/query-suggest/query-suggest-request.ts @@ -2,7 +2,6 @@ import { BaseParam, ContextParam, LocaleParam, - VisitorIDParam, } from '../../platform-service-params.js'; import { ActionsHistoryParam, @@ -24,7 +23,6 @@ export type QuerySuggestRequest = BaseParam & LocaleParam & TimezoneParam & ActionsHistoryParam & - VisitorIDParam & AuthenticationParam & AnalyticsParam & { count: number; diff --git a/packages/headless/src/api/search/recommendation/recommendation-request.ts b/packages/headless/src/api/search/recommendation/recommendation-request.ts index 74be238a1c1..9da006cd8c6 100644 --- a/packages/headless/src/api/search/recommendation/recommendation-request.ts +++ b/packages/headless/src/api/search/recommendation/recommendation-request.ts @@ -4,7 +4,6 @@ import { DictionaryFieldContextParam, LocaleParam, NumberOfResultsParam, - VisitorIDParam, } from '../../platform-service-params.js'; import { ActionsHistoryParam, @@ -35,7 +34,6 @@ export type RecommendationRequest = BaseParam & ReferrerParam & LocaleParam & TimezoneParam & - VisitorIDParam & AnalyticsParam & AuthenticationParam & NumberOfResultsParam; diff --git a/packages/headless/src/api/search/search-api-client.test.ts b/packages/headless/src/api/search/search-api-client.test.ts index 6aaf39bcc35..ef84f8da43f 100644 --- a/packages/headless/src/api/search/search-api-client.test.ts +++ b/packages/headless/src/api/search/search-api-client.test.ts @@ -251,7 +251,6 @@ describe('search api client', () => { fieldsToInclude: state.fields.fieldsToInclude, pipeline: state.pipeline, searchHub: state.searchHub, - visitorId: expect.any(String), }, preprocessRequest: NoopPreprocessRequest, requestMetadata: {method: 'search'}, @@ -346,7 +345,6 @@ describe('search api client', () => { searchHub: state.searchHub, timezone: state.configuration.search.timezone, locale: state.configuration.search.locale, - visitorId: expect.any(String), }, preprocessRequest: NoopPreprocessRequest, requestMetadata: {method: 'plan'}, @@ -416,7 +414,6 @@ describe('search api client', () => { timezone: state.configuration.search.timezone, locale: state.configuration.search.locale, actionsHistory: expect.any(Array), - visitorId: expect.any(String), }, preprocessRequest: NoopPreprocessRequest, requestMetadata: {method: 'querySuggest'}, @@ -570,7 +567,6 @@ describe('search api client', () => { ignoreValues: [], searchContext: { ...searchRequest, - visitorId: expect.any(String), analytics: { ...searchRequest.analytics, clientId: expect.any(String), @@ -622,7 +618,6 @@ it calls PlatformClient.call with the category facet search params`, async () => ignorePaths: [], searchContext: { ...searchRequest, - visitorId: expect.any(String), analytics: { ...searchRequest.analytics, clientId: expect.any(String), @@ -685,7 +680,6 @@ it calls PlatformClient.call with the category facet search params`, async () => actionsHistory: expect.any(Array), tab: originLevel2, referrer: originLevel3, - visitorId: expect.any(String), numberOfResults: recommendationState.pagination.numberOfResults, }, preprocessRequest: NoopPreprocessRequest, diff --git a/packages/headless/src/api/search/search/search-request.ts b/packages/headless/src/api/search/search/search-request.ts index 2a7c1f0ccac..f73c4847f86 100644 --- a/packages/headless/src/api/search/search/search-request.ts +++ b/packages/headless/src/api/search/search/search-request.ts @@ -6,7 +6,6 @@ import { FoldingParam, LocaleParam, NumberOfResultsParam, - VisitorIDParam, } from '../../platform-service-params.js'; import { ActionsHistoryParam, @@ -54,7 +53,6 @@ export type SearchRequest = BaseParam & PipelineParam & SearchHubParam & FacetOptionsParam & - VisitorIDParam & DebugParam & LocaleParam & FoldingParam & diff --git a/packages/headless/src/api/service/insight/query-suggest/query-suggest-request.ts b/packages/headless/src/api/service/insight/query-suggest/query-suggest-request.ts index fc747775f5b..7fd1cd140f1 100644 --- a/packages/headless/src/api/service/insight/query-suggest/query-suggest-request.ts +++ b/packages/headless/src/api/service/insight/query-suggest/query-suggest-request.ts @@ -1,8 +1,4 @@ -import { - ContextParam, - LocaleParam, - VisitorIDParam, -} from '../../../platform-service-params.js'; +import {ContextParam, LocaleParam} from '../../../platform-service-params.js'; import { ActionsHistoryParam, AnalyticsParam, @@ -18,7 +14,6 @@ export type InsightQuerySuggestRequest = InsightParam & LocaleParam & TimezoneParam & ActionsHistoryParam & - VisitorIDParam & AuthenticationParam & AnalyticsParam & { count: number; diff --git a/packages/headless/src/controllers/core/did-you-mean/headless-core-did-you-mean.test.ts b/packages/headless/src/controllers/core/did-you-mean/headless-core-did-you-mean.test.ts index dcc1b3f11cc..8acd164094a 100644 --- a/packages/headless/src/controllers/core/did-you-mean/headless-core-did-you-mean.test.ts +++ b/packages/headless/src/controllers/core/did-you-mean/headless-core-did-you-mean.test.ts @@ -71,6 +71,15 @@ describe('did you mean', () => { expect(applyDidYouMeanCorrection).toHaveBeenCalledWith('bar'); }); + it('should allow to update the query correction mode', () => { + const initialState = createMockState(); + initialState.didYouMean.queryCorrectionMode = 'legacy'; + initDidYouMean({}, initialState); + dym.updateQueryCorrectionMode('next'); + + expect(engine.dispatch).toHaveBeenCalledWith(setCorrectionMode('next')); + }); + it('should dispatch disableAutomaticQueryCorrection at initialization when specified', () => { initDidYouMean({options: {automaticallyCorrectQuery: false}}); expect(disableAutomaticQueryCorrection).toHaveBeenCalledTimes(1); diff --git a/packages/headless/src/controllers/core/did-you-mean/headless-core-did-you-mean.ts b/packages/headless/src/controllers/core/did-you-mean/headless-core-did-you-mean.ts index 744058a678a..4e341cbf93d 100644 --- a/packages/headless/src/controllers/core/did-you-mean/headless-core-did-you-mean.ts +++ b/packages/headless/src/controllers/core/did-you-mean/headless-core-did-you-mean.ts @@ -28,6 +28,8 @@ export interface DidYouMeanProps { options?: DidYouMeanOptions; } +type QueryCorrectionMode = 'legacy' | 'next'; + export interface DidYouMeanOptions { /** * Whether to automatically apply corrections for queries that would otherwise return no results. @@ -46,7 +48,7 @@ export interface DidYouMeanOptions { * * Default value is `next`. */ - queryCorrectionMode?: 'legacy' | 'next'; + queryCorrectionMode?: QueryCorrectionMode; } export interface DidYouMean extends Controller { /** @@ -54,6 +56,17 @@ export interface DidYouMean extends Controller { */ applyCorrection(): void; + /** + * Update which query correction system to use + * + * `legacy`: Query correction is powered by the legacy index system. This system relies on an algorithm using solely the index content to compute the suggested terms. + * `next`: Query correction is powered by a machine learning system, requiring a valid query suggestion model configured in your Coveo environment to function properly. This system relies on machine learning algorithms to compute the suggested terms. + * + * @param queryCorrectionMode - the query correction mode to use + * + */ + updateQueryCorrectionMode(queryCorrectionMode: QueryCorrectionMode): void; + /** * The state of the `DidYouMean` controller. */ @@ -138,6 +151,9 @@ export function buildCoreDidYouMean( applyDidYouMeanCorrection(this.state.queryCorrection.correctedQuery) ); }, + updateQueryCorrectionMode(queryCorrectionMode: QueryCorrectionMode) { + dispatch(setCorrectionMode(queryCorrectionMode)); + }, }; } diff --git a/packages/headless/src/features/facets/facet-search-set/category/category-facet-search-request-builder.test.ts b/packages/headless/src/features/facets/facet-search-set/category/category-facet-search-request-builder.test.ts index 1c96d91c92f..d27c5d84a9a 100644 --- a/packages/headless/src/features/facets/facet-search-set/category/category-facet-search-request-builder.test.ts +++ b/packages/headless/src/features/facets/facet-search-set/category/category-facet-search-request-builder.test.ts @@ -91,7 +91,6 @@ describe('#buildCategoryFacetSearchRequest', () => { expect((await buildParams()).searchContext).toEqual({ ...request, - visitorId: expect.any(String), analytics: { ...request.analytics, clientId: expect.any(String), diff --git a/packages/headless/src/features/facets/facet-search-set/specific/specific-facet-search-request-builder.test.ts b/packages/headless/src/features/facets/facet-search-set/specific/specific-facet-search-request-builder.test.ts index 878792f7dbe..8b2d80a2da6 100644 --- a/packages/headless/src/features/facets/facet-search-set/specific/specific-facet-search-request-builder.test.ts +++ b/packages/headless/src/features/facets/facet-search-set/specific/specific-facet-search-request-builder.test.ts @@ -80,7 +80,6 @@ describe('#buildSpecificFacetSearchRequest', () => { expect((await buildParams()).searchContext).toEqual({ ...request, - visitorId: expect.any(String), analytics: { ...request.analytics, clientId: expect.any(String), diff --git a/packages/headless/src/features/insight-search/insight-query-suggest-request.ts b/packages/headless/src/features/insight-search/insight-query-suggest-request.ts index 54d01b43016..e8a8866d8da 100644 --- a/packages/headless/src/features/insight-search/insight-query-suggest-request.ts +++ b/packages/headless/src/features/insight-search/insight-query-suggest-request.ts @@ -1,7 +1,4 @@ -import { - historyStore, - getVisitorID, -} from '../../api/analytics/coveo-analytics-utils.js'; +import {historyStore} from '../../api/analytics/coveo-analytics-utils.js'; import {getOrganizationEndpoint} from '../../api/platform-client.js'; import {InsightQuerySuggestRequest} from '../../api/service/insight/query-suggest/query-suggest-request.js'; import {InsightAppState} from '../../state/insight-app-state.js'; @@ -37,7 +34,6 @@ export const buildInsightQuerySuggestRequest = async ( context: s.insightCaseContext.caseContext, }), ...(s.configuration.analytics.enabled && { - visitorId: await getVisitorID(s.configuration.analytics), ...(s.configuration.analytics.enabled && (await fromAnalyticsStateToAnalyticsParams(s.configuration.analytics))), }), diff --git a/packages/headless/src/features/insight-search/insight-result-preview-request-builder.test.ts b/packages/headless/src/features/insight-search/insight-result-preview-request-builder.test.ts index 20d5de08561..420c9ddc2f8 100644 --- a/packages/headless/src/features/insight-search/insight-result-preview-request-builder.test.ts +++ b/packages/headless/src/features/insight-search/insight-result-preview-request-builder.test.ts @@ -45,7 +45,6 @@ describe('ResultPreviewRequestBuilder', () => { requestedOutputSize: 0, uniqueId: '1', url: expectedUrl, - visitorId: expect.any(String), }); }); }); diff --git a/packages/headless/src/features/insight-search/insight-result-preview-request-builder.ts b/packages/headless/src/features/insight-search/insight-result-preview-request-builder.ts index 23308859f2e..8afc2d0a50d 100644 --- a/packages/headless/src/features/insight-search/insight-result-preview-request-builder.ts +++ b/packages/headless/src/features/insight-search/insight-result-preview-request-builder.ts @@ -1,4 +1,3 @@ -import {getVisitorID} from '../../api/analytics/coveo-analytics-utils.js'; import {getOrganizationEndpoint} from '../../api/platform-client.js'; import { HtmlRequest, @@ -15,8 +14,7 @@ export async function buildInsightResultPreviewRequest( state: StateNeededByInsightHtmlEndpoint, options: HtmlRequestOptions ): Promise { - const {accessToken, organizationId, analytics, environment} = - state.configuration; + const {accessToken, organizationId, environment} = state.configuration; const {insightId} = state.insightConfiguration; const q = state.query?.q || ''; @@ -32,9 +30,6 @@ export async function buildInsightResultPreviewRequest( accessToken, organizationId, enableNavigation: false, - ...(analytics.enabled && { - visitorId: await getVisitorID(state.configuration.analytics), - }), q, ...options, requestedOutputSize: options.requestedOutputSize || 0, diff --git a/packages/headless/src/features/query-suggest/query-suggest-actions.ts b/packages/headless/src/features/query-suggest/query-suggest-actions.ts index 9333740bd6d..eb57d0f069c 100644 --- a/packages/headless/src/features/query-suggest/query-suggest-actions.ts +++ b/packages/headless/src/features/query-suggest/query-suggest-actions.ts @@ -1,9 +1,6 @@ import {NumberValue} from '@coveo/bueno'; import {createAction, createAsyncThunk} from '@reduxjs/toolkit'; -import { - getVisitorID, - historyStore, -} from '../../api/analytics/coveo-analytics-utils.js'; +import {historyStore} from '../../api/analytics/coveo-analytics-utils.js'; import {getSearchApiBaseUrl} from '../../api/platform-client.js'; import {QuerySuggestRequest} from '../../api/search/query-suggest/query-suggest-request.js'; import {QuerySuggestSuccessResponse} from '../../api/search/query-suggest/query-suggest-response.js'; @@ -181,7 +178,6 @@ export const buildQuerySuggestRequest = async ( ...(s.searchHub && {searchHub: s.searchHub}), tab: s.configuration.analytics.originLevel2, ...(s.configuration.analytics.enabled && { - visitorId: await getVisitorID(s.configuration.analytics), ...(s.configuration.analytics.enabled && s.configuration.analytics.analyticsMode === 'legacy' ? await legacyFromAnalyticsStateToAnalyticsParams( diff --git a/packages/headless/src/features/recommendation/recommendation-actions.ts b/packages/headless/src/features/recommendation/recommendation-actions.ts index cf9c4203af9..28aa86e7f4f 100644 --- a/packages/headless/src/features/recommendation/recommendation-actions.ts +++ b/packages/headless/src/features/recommendation/recommendation-actions.ts @@ -1,8 +1,5 @@ import {createAction, createAsyncThunk} from '@reduxjs/toolkit'; -import { - getVisitorID, - historyStore, -} from '../../api/analytics/coveo-analytics-utils.js'; +import {historyStore} from '../../api/analytics/coveo-analytics-utils.js'; import {getSearchApiBaseUrl} from '../../api/platform-client.js'; import {RecommendationRequest} from '../../api/search/recommendation/recommendation-request.js'; import { @@ -119,9 +116,6 @@ export const buildRecommendationRequest = async ( ...(s.fields && { fieldsToInclude: s.fields.fieldsToInclude, }), - ...(s.configuration.analytics.enabled && { - visitorId: await getVisitorID(s.configuration.analytics), - }), ...(s.configuration.analytics.enabled && (await buildAnalyticsSection(s, recommendationInterfaceLoad()))), ...(s.configuration.search.authenticationProviders.length && { diff --git a/packages/headless/src/features/result-preview/result-preview-request-builder.test.ts b/packages/headless/src/features/result-preview/result-preview-request-builder.test.ts index 6dd8e079446..760a2dbe5e4 100644 --- a/packages/headless/src/features/result-preview/result-preview-request-builder.test.ts +++ b/packages/headless/src/features/result-preview/result-preview-request-builder.test.ts @@ -34,7 +34,6 @@ describe('ResultPreviewRequestBuilder', () => { requestedOutputSize: 0, uniqueId: '1', url: getSearchApiBaseUrl(''), - visitorId: expect.any(String), }); }); }); diff --git a/packages/headless/src/features/result-preview/result-preview-request-builder.ts b/packages/headless/src/features/result-preview/result-preview-request-builder.ts index e50f61173eb..de52c4f16f4 100644 --- a/packages/headless/src/features/result-preview/result-preview-request-builder.ts +++ b/packages/headless/src/features/result-preview/result-preview-request-builder.ts @@ -1,4 +1,3 @@ -import {getVisitorID} from '../../api/analytics/coveo-analytics-utils.js'; import {getSearchApiBaseUrl} from '../../api/platform-client.js'; import { HtmlRequest, @@ -18,7 +17,7 @@ export async function buildResultPreviewRequest( state: StateNeededByHtmlEndpoint, options: HtmlRequestOptions ): Promise { - const {search, accessToken, organizationId, analytics} = state.configuration; + const {search, accessToken, organizationId} = state.configuration; const q = state.query?.q || ''; return { @@ -31,9 +30,6 @@ export async function buildResultPreviewRequest( accessToken, organizationId, enableNavigation: false, - ...(analytics.enabled && { - visitorId: await getVisitorID(state.configuration.analytics), - }), q, ...options, requestedOutputSize: options.requestedOutputSize || 0, diff --git a/packages/headless/src/features/search-and-folding/legacy/search-and-folding-request.test.ts b/packages/headless/src/features/search-and-folding/legacy/search-and-folding-request.test.ts index e5f0184d0f9..73339845968 100644 --- a/packages/headless/src/features/search-and-folding/legacy/search-and-folding-request.test.ts +++ b/packages/headless/src/features/search-and-folding/legacy/search-and-folding-request.test.ts @@ -109,9 +109,9 @@ describe('buildSearchAndFoldingLoadCollectionRequest', () => { state.configuration.analytics.enabled = true; }); - it('#visitorId is included in the request', async () => { + it('#analytics is included in the request', async () => { const request = await buildSearchAndFoldingLoadCollectionRequest(state); - expect(request.visitorId).toBeDefined(); + expect(request.analytics).toBeDefined(); }); it('#actionsHistory is included in the request', async () => { @@ -127,9 +127,9 @@ describe('buildSearchAndFoldingLoadCollectionRequest', () => { state.configuration.analytics.enabled = false; }); - it('#visitorId is not included in the request', async () => { + it('#analytics is not included in the request', async () => { const request = await buildSearchAndFoldingLoadCollectionRequest(state); - expect(request.visitorId).toBeUndefined(); + expect(request.analytics).toBeUndefined(); }); it('#actionsHistory is not included in the request', async () => { diff --git a/packages/headless/src/features/search-and-folding/legacy/search-and-folding-request.ts b/packages/headless/src/features/search-and-folding/legacy/search-and-folding-request.ts index af50580e28a..adb97c31477 100644 --- a/packages/headless/src/features/search-and-folding/legacy/search-and-folding-request.ts +++ b/packages/headless/src/features/search-and-folding/legacy/search-and-folding-request.ts @@ -1,9 +1,6 @@ import {isNullOrUndefined} from '@coveo/bueno'; import {EventDescription} from 'coveo.analytics'; -import { - getVisitorID, - historyStore, -} from '../../../api/analytics/coveo-analytics-utils.js'; +import {historyStore} from '../../../api/analytics/coveo-analytics-utils.js'; import {getSearchApiBaseUrl} from '../../../api/platform-client.js'; import {SearchRequest} from '../../../api/search/search/search-request.js'; import {SearchAppState} from '../../../state/search-app-state.js'; @@ -32,7 +29,6 @@ export const buildSearchAndFoldingLoadCollectionRequest = async ( referrer: state.configuration.analytics.originLevel3, timezone: state.configuration.search.timezone, ...(state.configuration.analytics.enabled && { - visitorId: await getVisitorID(state.configuration.analytics), actionsHistory: historyStore.getHistory(), }), ...(state.advancedSearchQueries?.aq && { diff --git a/packages/headless/src/features/search-and-folding/search-and-folding-request.test.ts b/packages/headless/src/features/search-and-folding/search-and-folding-request.test.ts index afc14e627d1..3c6a73dbcc5 100644 --- a/packages/headless/src/features/search-and-folding/search-and-folding-request.test.ts +++ b/packages/headless/src/features/search-and-folding/search-and-folding-request.test.ts @@ -140,12 +140,12 @@ describe('buildSearchAndFoldingLoadCollectionRequest', () => { state.configuration.analytics.enabled = true; }); - it('#visitorId is included in the request', async () => { + it('#analytics is included in the request', async () => { const request = buildSearchAndFoldingLoadCollectionRequest( state, buildMockNavigatorContextProvider()() ); - expect(request.visitorId).toBeDefined(); + expect(request.analytics).toBeDefined(); }); }); @@ -156,12 +156,12 @@ describe('buildSearchAndFoldingLoadCollectionRequest', () => { state.configuration.analytics.enabled = false; }); - it('#visitorId is not included in the request', async () => { + it('#analytics is not included in the request', async () => { const request = buildSearchAndFoldingLoadCollectionRequest( state, buildMockNavigatorContextProvider()() ); - expect(request.visitorId).toBeUndefined(); + expect(request.analytics).not.toBeDefined(); }); it('#actionsHistory is not included in the request', async () => { diff --git a/packages/headless/src/features/search-and-folding/search-and-folding-request.ts b/packages/headless/src/features/search-and-folding/search-and-folding-request.ts index 08353ff2493..67c541f0c6c 100644 --- a/packages/headless/src/features/search-and-folding/search-and-folding-request.ts +++ b/packages/headless/src/features/search-and-folding/search-and-folding-request.ts @@ -29,9 +29,6 @@ export const buildSearchAndFoldingLoadCollectionRequest = ( tab: state.configuration.analytics.originLevel2, referrer: navigatorContext.referrer, timezone: state.configuration.search.timezone, - ...(state.configuration.analytics.enabled && { - visitorId: navigatorContext.clientId, - }), ...(state.advancedSearchQueries?.aq && { aq: state.advancedSearchQueries.aq, }), diff --git a/packages/headless/src/features/search/search-request.test.ts b/packages/headless/src/features/search/search-request.test.ts index 09620be2785..cfa14d194d2 100644 --- a/packages/headless/src/features/search/search-request.test.ts +++ b/packages/headless/src/features/search/search-request.test.ts @@ -458,19 +458,19 @@ describe('search request', () => { }); }); - it('should send visitorId if analytics is enable', async () => { + it('should send analytics if analytics is enable', async () => { state.configuration.analytics.enabled = true; expect( (await buildSearchRequest(state, buildMockNavigatorContextProvider()())) - .request.visitorId + .request.analytics ).toBeDefined(); }); - it('should not send visitorId if analytics is disabled', async () => { + it('should not send analytics if analytics is disabled', async () => { state.configuration.analytics.enabled = false; expect( (await buildSearchRequest(state, buildMockNavigatorContextProvider()())) - .request.visitorId + .request.analytics ).not.toBeDefined(); }); diff --git a/packages/headless/src/features/standalone-search-box-set/standalone-search-box-set-actions.ts b/packages/headless/src/features/standalone-search-box-set/standalone-search-box-set-actions.ts index a3210c29584..7d77a96dac3 100644 --- a/packages/headless/src/features/standalone-search-box-set/standalone-search-box-set-actions.ts +++ b/packages/headless/src/features/standalone-search-box-set/standalone-search-box-set-actions.ts @@ -1,6 +1,5 @@ import {BooleanValue, StringValue} from '@coveo/bueno'; import {createAction, createAsyncThunk} from '@reduxjs/toolkit'; -import {getVisitorID} from '../../api/analytics/coveo-analytics-utils.js'; import {getSearchApiBaseUrl} from '../../api/platform-client.js'; import {ExecutionPlan} from '../../api/search/plan/plan-endpoint.js'; import {PlanRequest} from '../../api/search/plan/plan-request.js'; @@ -188,9 +187,6 @@ export const buildPlanRequest = async ( ...(state.context && {context: state.context.contextValues}), ...(state.pipeline && {pipeline: state.pipeline}), ...(state.searchHub && {searchHub: state.searchHub}), - ...(state.configuration.analytics.enabled && { - visitorId: await getVisitorID(state.configuration.analytics), - }), ...(state.configuration.analytics.enabled && state.configuration.analytics.analyticsMode === 'legacy' ? await legacyFromAnalyticsStateToAnalyticsParams( diff --git a/packages/headless/src/test/mock-result-preview-request-builder.ts b/packages/headless/src/test/mock-result-preview-request-builder.ts index 7e3bd51376d..00d05a046e6 100644 --- a/packages/headless/src/test/mock-result-preview-request-builder.ts +++ b/packages/headless/src/test/mock-result-preview-request-builder.ts @@ -13,7 +13,6 @@ export async function buildMockResultPreviewRequest( accessToken: 'access-token-xxxxx', organizationId: 'some-org-id', enableNavigation: false, - visitorId: 'visitor-id', q: 'query', requestedOutputSize: options.requestedOutputSize || 0, ...options, diff --git a/scripts/ci/determine-shard.mjs b/scripts/ci/determine-shard.mjs new file mode 100644 index 00000000000..402ef56775f --- /dev/null +++ b/scripts/ci/determine-shard.mjs @@ -0,0 +1,25 @@ +#!/usr/bin/env node +import {setOutput} from '@actions/core'; + +function getOutputName() { + return process.argv.slice(2, 4); +} + +function allocateShards(testCount, maximumShards) { + const shardTotal = + testCount === 0 ? maximumShards : Math.min(testCount, maximumShards); + const shardIndex = Array.from({length: shardTotal}, (_, i) => i + 1); + return [shardIndex, [shardTotal]]; +} + +const testsToRun = process.env.testsToRun.split(' '); +const maximumShards = parseInt(process.env.maximumShards, 10); + +const [shardIndexOutputName, shardTotalOutputName] = getOutputName(); +const [shardIndex, shardTotal] = allocateShards( + testsToRun.length, + maximumShards +); + +setOutput(shardIndexOutputName, shardIndex); +setOutput(shardTotalOutputName, shardTotal); diff --git a/scripts/ci/find-tests.mjs b/scripts/ci/find-tests.mjs new file mode 100644 index 00000000000..a8d0b11bdcb --- /dev/null +++ b/scripts/ci/find-tests.mjs @@ -0,0 +1,125 @@ +#!/usr/bin/env node +import {setOutput} from '@actions/core'; +import {readdirSync, statSync} from 'fs'; +import {EOL} from 'os'; +import {basename, dirname, join, relative} from 'path'; +import { + getBaseHeadSHAs, + getChangedFiles, + getOutputName, +} from './hasFileChanged.mjs'; +import {listImports, ensureFileExists} from './list-imports.mjs'; + +/** + * Recursively searches for all end-to-end (E2E) test files in a given directory. + * E2E test files are identified by the `.e2e.ts` file extension. + * + * @param dir - The root directory to start the search from. + * @returns An array of strings, each representing the full path to an E2E test file. + */ +function findAllTestFiles(dir) { + function searchFiles(currentDir, testFiles) { + const files = readdirSync(currentDir); + + for (const file of files) { + const fullPath = join(currentDir, file); + const stat = statSync(fullPath); + + if (stat.isDirectory()) { + searchFiles(fullPath, testFiles); + } else if (fullPath.endsWith('.e2e.ts')) { + testFiles.push(fullPath); + } + } + + return testFiles; + } + + return searchFiles(dir, []); +} + +/** + * Creates a mapping of test file names to the set of files they import. + * + * @param testPaths - An array of E2E test file paths. + * @returns A map where each key is a test file name and the value is the set of files it imports. + */ +function createTestFileMappings(testPaths, projectRoot) { + const testFileMappings = testPaths.map((testPath) => { + const imports = new Set(); + const testName = basename(testPath); + const sourceFilePath = join( + dirname(testPath).replace('/e2e', ''), + testName.replace('.e2e.ts', '.tsx') + ); + + ensureFileExists(sourceFilePath); + + [ + relative(projectRoot, sourceFilePath), + ...listImports(projectRoot, sourceFilePath), + ...listImports(projectRoot, testPath), + ].forEach((importedFile) => imports.add(importedFile)); + + return [testName, imports]; + }); + + return new Map(testFileMappings); +} + +/** + * Determines which E2E test files to run based on the files that have changed. + * + * @param changedFiles - An array of files that have changed. + * @param testDependencies - A map of test file names to the set of files they import. + * @returns A space-separated string of test files to run. + */ +function determineTestFilesToRun(changedFiles, testDependencies) { + const testsToRun = new Set(); + for (const changedFile of changedFiles) { + for (const [testFile, sourceFiles] of testDependencies) { + ensureIsNotCoveoPackage(changedFile); + const isChangedTestFile = testFile === basename(changedFile); + const isAffectedSourceFile = sourceFiles.has(changedFile); + if (isChangedTestFile || isAffectedSourceFile) { + testsToRun.add(testFile); + testDependencies.delete(testFile); + } + } + } + return [...testsToRun].join(' '); +} + +function ensureIsNotCoveoPackage(file) { + if (dependsOnCoveoPackage(file)) { + throw new Error('Change detected in an different Coveo package.'); + } +} + +function dependsOnCoveoPackage(file) { + const externalPackages = ['packages/headless', 'packages/bueno']; + for (const pkg of externalPackages) { + if (file.includes(pkg)) { + return true; + } + } +} + +const {base, head} = getBaseHeadSHAs(); +const changedFiles = getChangedFiles(base, head).split(EOL); +const outputName = getOutputName(); +const projectRoot = process.env.projectRoot; +const atomicSourceComponents = join('packages', 'atomic', 'src', 'components'); + +try { + const testFiles = findAllTestFiles(atomicSourceComponents); + const testDependencies = createTestFileMappings(testFiles, projectRoot); + const testsToRun = determineTestFilesToRun(changedFiles, testDependencies); + setOutput(outputName, testsToRun ? testsToRun : '--grep @no-test'); + + if (!testsToRun) { + console.log('No relevant source file changes detected for E2E tests.'); + } +} catch (error) { + console.warn(error?.message || error); +} diff --git a/scripts/ci/hasFileChanged.mjs b/scripts/ci/hasFileChanged.mjs index 89e8105b2e4..a268f258209 100644 --- a/scripts/ci/hasFileChanged.mjs +++ b/scripts/ci/hasFileChanged.mjs @@ -4,7 +4,7 @@ import {context} from '@actions/github'; import {minimatch} from 'minimatch'; import {execSync} from 'node:child_process'; -function getBaseHeadSHAs() { +export function getBaseHeadSHAs() { switch (context.eventName) { case 'pull_request': return { @@ -19,7 +19,7 @@ function getBaseHeadSHAs() { } } -function getChangedFiles(from, to) { +export function getChangedFiles(from, to) { return execSync(`git diff --name-only ${from}..${to}`, { stdio: 'pipe', encoding: 'utf-8', @@ -41,7 +41,7 @@ function checkPatterns(files, patterns) { return false; } -function getOutputName() { +export function getOutputName() { return process.argv[2]; } diff --git a/scripts/ci/list-imports.mjs b/scripts/ci/list-imports.mjs new file mode 100644 index 00000000000..858f9336300 --- /dev/null +++ b/scripts/ci/list-imports.mjs @@ -0,0 +1,97 @@ +#!/usr/bin/env node +import {existsSync, readFileSync} from 'fs'; +import {join, relative, resolve} from 'path'; +import ts from 'typescript'; + +export function ensureFileExists(filePath) { + if (!existsSync(filePath)) { + throw new Error(`File ${filePath} does not exist.`); + } +} + +function getSourceFile(containingFile, fileContent) { + return ts.createSourceFile( + containingFile, + fileContent, + ts.ScriptTarget.ES2021, + true // SetParentNodes - useful for AST transformations + ); +} + +function getImports(sourceFile, filePath, compilerOptions) { + const imports = new Set(); + const alreadyResolved = new Set(); + + const moduleResolutionHost = { + fileExists: (filePath) => existsSync(filePath), + readFile: (filePath) => { + try { + return readFileSync(filePath, 'utf8'); + } catch { + return undefined; + } + }, + }; + + const resolveAndAddImport = (containingFile, importPath) => { + const {resolvedModule} = ts.resolveModuleName( + importPath, + containingFile, + compilerOptions, + moduleResolutionHost + ); + + if (!resolvedModule) { + return null; + } + + imports.add(resolvedModule.resolvedFileName); + return resolvedModule.resolvedFileName; + }; + + const visit = (node, currentFile) => { + if (ts.isImportDeclaration(node)) { + const importPath = node.moduleSpecifier.getText().slice(1, -1); // Remove quotes + const resolvedFileName = + resolveAndAddImport(currentFile, importPath) || + resolveAndAddImport(currentFile, join(importPath, 'index')); // Check if the import is from an index file + + if (resolvedFileName && !alreadyResolved.has(resolvedFileName)) { + alreadyResolved.add(resolvedFileName); + const fileContent = readFileSync(resolvedFileName, 'utf-8'); + const sourceFile = getSourceFile(resolvedFileName, fileContent); + ts.forEachChild(sourceFile, (childNode) => + visit(childNode, resolvedFileName) + ); + } + } + }; + + ts.forEachChild(sourceFile, (node) => visit(node, filePath)); + + return Array.from(imports); +} + +/** + * Function to extract all import statements from a TypeScript file. + * @param filePath Path to the TypeScript file. + * @returns A list of files that are imported by the input file. + */ +export function listImports(projectRoot, filePath) { + ensureFileExists(filePath); + const fileContent = readFileSync(filePath, 'utf-8'); + const sourceFile = getSourceFile(filePath, fileContent); + + const compilerOptions = { + target: ts.ScriptTarget.ES2021, + }; + + const imports = getImports(sourceFile, filePath, compilerOptions); + + const resolvedImports = imports.map((importPath) => { + const absolutePath = resolve(importPath); + return relative(projectRoot, absolutePath); + }); + + return resolvedImports; +}