Skip to content

Commit

Permalink
feat(instantsearch): allow the insights middleware to be added in ans…
Browse files Browse the repository at this point in the history
…wer to a server setting (#5883)


---------

Co-authored-by: Sarah Dayan <5370675+sarahdayan@users.noreply.github.com>
  • Loading branch information
dhayab and sarahdayan authored Oct 18, 2023
1 parent 96a5c8b commit 2a9e654
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 53 deletions.
6 changes: 3 additions & 3 deletions bundlesize.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@
},
{
"path": "packages/react-instantsearch/dist/umd/ReactInstantSearch.min.js",
"maxSize": "56.25 kB"
"maxSize": "56.5 kB"
},
{
"path": "packages/vue-instantsearch/vue2/umd/index.js",
"maxSize": "63 kB"
"maxSize": "63.25 kB"
},
{
"path": "packages/vue-instantsearch/vue3/umd/index.js",
"maxSize": "63.5 kB"
"maxSize": "63.75 kB"
},
{
"path": "packages/vue-instantsearch/vue2/cjs/index.js",
Expand Down
2 changes: 2 additions & 0 deletions packages/algoliasearch-helper/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1317,6 +1317,8 @@ declare namespace algoliasearchHelper {
*/
facets: SearchResults.Facet[];

_automaticInsights?: true;

_rawResults: Array<SearchResponse<T>>;
_state: SearchParameters;

Expand Down
27 changes: 25 additions & 2 deletions packages/instantsearch.js/src/lib/InstantSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ class InstantSearch<
public _createURL: CreateURL<TUiState>;
public _searchFunction?: InstantSearchOptions['searchFunction'];
public _mainHelperSearch?: AlgoliaSearchHelper['search'];
public _insights: InstantSearchOptions['insights'];
public middleware: Array<{
creator: Middleware<TUiState>;
instance: MiddlewareDefinition<TUiState>;
Expand Down Expand Up @@ -342,6 +343,8 @@ See documentation: ${createDocumentationLink({
this._initialUiState = initialUiState as TUiState;
this._initialResults = null;

this._insights = insights;

if (searchFunction) {
warning(
false,
Expand All @@ -358,8 +361,9 @@ See documentation: ${createDocumentationLink({
this.use(createRouterMiddleware(routerOptions));
}

// This is the default middleware,
// any user-provided middleware will be added later and override this one.
// This is the default Insights middleware,
// added when `insights` is set to true by the user.
// Any user-provided middleware will be added later and override this one.
if (insights) {
const insightsOptions = typeof insights === 'boolean' ? {} : insights;
insightsOptions.$$internal = true;
Expand Down Expand Up @@ -667,6 +671,25 @@ See documentation: ${createDocumentationLink({
this.middleware.forEach(({ instance }) => {
instance.started();
});

// This is the automatic Insights middleware,
// added when `insights` is unset and the initial results possess `queryID`.
// Any user-provided middleware will be added later and override this one.
if (typeof this._insights === 'undefined') {
mainHelper.derivedHelpers[0].once('result', () => {
const hasAutomaticInsights = this.mainIndex
.getScopedResults()
.some(({ results }) => results?._automaticInsights);
if (hasAutomaticInsights) {
this.use(
createInsightsMiddleware({
$$internal: true,
$$clickAnalytics: false,
})
);
}
});
}
}

/**
Expand Down
163 changes: 120 additions & 43 deletions packages/instantsearch.js/src/lib/__tests__/InstantSearch-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import {
createSearchClient,
createControlledSearchClient,
createMultiSearchResponse,
createSingleSearchResponse,
} from '@instantsearch/mocks';
import { castToJestMock } from '@instantsearch/testutils/castToJestMock';
import { wait } from '@instantsearch/testutils/wait';
Expand Down Expand Up @@ -474,43 +476,120 @@ See https://www.algolia.com/doc/api-reference/widgets/configure/js/`);
});

describe('insights middleware', () => {
test('does not add insights middleware by default', () => {
const search = new InstantSearch({
searchClient: createSearchClient(),
indexName: 'test',
const createSearchClientWithAutomaticInsightsOptedIn = () =>
createSearchClient({
search: jest.fn((requests) => {
return Promise.resolve(
createMultiSearchResponse(
...requests.map((request) => {
return createSingleSearchResponse<any>({
...(request.indexName === 'indexNameWithAutomaticInsights'
? { _automaticInsights: true }
: undefined),
index: request.indexName,
query: request.query,
...(request.indexName === 'indexNameWithAutomaticInsights'
? { queryID: 'queryID' }
: undefined),
hits: [{ objectID: `${request.indexName}-objectID1` }],
});
})
)
);
}),
});

expect(
search.middleware.map(({ instance: { $$type, $$internal } }) => ({
const mapMiddlewares = (middlewares: InstantSearch['middleware']) =>
middlewares.map(
// @ts-ignore: $$clickAnalytics is only applicable to insights middleware
({ instance: { $$type, $$internal, $$clickAnalytics } }) => ({
$$type,
$$internal,
}))
).toEqual([]);
$$clickAnalytics,
})
);

test('insights: undefined does not add the insights middleware if `_automaticInsights: true` is not found in initial response', async () => {
const search = new InstantSearch({
indexName: 'indexName',
searchClient: createSearchClientWithAutomaticInsightsOptedIn(),
});

search.addWidgets([
virtualSearchBox({}),
index({ indexName: 'indexName2' }).addWidgets([virtualSearchBox({})]),
]);
search.start();

await wait(0);

expect(mapMiddlewares(search.middleware)).toEqual([]);
});

test('insights: undefined adds the insights middleware if `_automaticInsights: true` is found in at least one index in initial response', async () => {
const searchClient = createSearchClientWithAutomaticInsightsOptedIn();
const search = new InstantSearch({
indexName: 'indexNameWithAutomaticInsights',
searchClient,
});

search.addWidgets([
virtualSearchBox({}),
index({ indexName: 'indexName' }).addWidgets([virtualSearchBox({})]),
]);
search.start();

await wait(0);

expect(searchClient.search).toHaveBeenCalledTimes(1);
expect(mapMiddlewares(search.middleware)).toEqual([
{
$$type: 'ais.insights',
$$internal: true,
$$clickAnalytics: false,
},
]);
});

test('insights: true adds only one insights middleware', () => {
const search = new InstantSearch({
searchClient: createSearchClient(),
searchClient: createSearchClientWithAutomaticInsightsOptedIn(),
indexName: 'test',
insights: true,
});

expect(
search.middleware.map(({ instance: { $$type, $$internal } }) => ({
$$type,
$$internal,
}))
).toEqual([
expect(mapMiddlewares(search.middleware)).toEqual([
{
$$type: 'ais.insights',
$$internal: true,
$$clickAnalytics: true,
},
]);
});

test('insights: true adds only one insights middleware when `_automaticInsights: true` is also set', async () => {
const search = new InstantSearch({
searchClient: createSearchClientWithAutomaticInsightsOptedIn(),
indexName: 'indexNameWithAutomaticInsights',
insights: true,
});

search.addWidgets([virtualSearchBox({})]);
search.start();

await wait(0);

expect(mapMiddlewares(search.middleware)).toEqual([
{
$$type: 'ais.insights',
$$internal: true,
$$clickAnalytics: true,
},
]);
});

test('insights: options adds only one insights middleware', () => {
const search = new InstantSearch({
searchClient: createSearchClient(),
searchClient: createSearchClientWithAutomaticInsightsOptedIn(),
indexName: 'test',
insights: {
insightsInitParams: {
Expand All @@ -519,39 +598,31 @@ See https://www.algolia.com/doc/api-reference/widgets/configure/js/`);
},
});

expect(
search.middleware.map(({ instance: { $$type, $$internal } }) => ({
$$type,
$$internal,
}))
).toEqual([
expect(mapMiddlewares(search.middleware)).toEqual([
{
$$type: 'ais.insights',
$$internal: true,
$$clickAnalytics: true,
},
]);
});

test('insights: options passes options to middleware', () => {
const insightsClient = Object.assign(jest.fn(), { version: '2.6.0' });
const search = new InstantSearch({
searchClient: createSearchClient(),
searchClient: createSearchClientWithAutomaticInsightsOptedIn(),
indexName: 'test',
insights: {
insightsClient,
},
});
search.start();

expect(
search.middleware.map(({ instance: { $$type, $$internal } }) => ({
$$type,
$$internal,
}))
).toEqual([
expect(mapMiddlewares(search.middleware)).toEqual([
{
$$type: 'ais.insights',
$$internal: true,
$$clickAnalytics: true,
},
]);

Expand Down Expand Up @@ -581,17 +652,27 @@ See https://www.algolia.com/doc/api-reference/widgets/configure/js/`);

test('insights: false disables default insights', () => {
const search = new InstantSearch({
searchClient: createSearchClient(),
searchClient: createSearchClientWithAutomaticInsightsOptedIn(),
indexName: 'test',
insights: false,
});

expect(
search.middleware.map(({ instance: { $$type, $$internal } }) => ({
$$type,
$$internal,
}))
).toEqual([]);
expect(mapMiddlewares(search.middleware)).toEqual([]);
});

test('insights: false does not add Insights middleware even if `_automaticInsights: true` is set', async () => {
const search = new InstantSearch({
searchClient: createSearchClientWithAutomaticInsightsOptedIn(),
indexName: 'indexNameWithAutomaticInsights',
insights: false,
});

search.addWidgets([virtualSearchBox({})]);
search.start();

await wait(0);

expect(mapMiddlewares(search.middleware)).toEqual([]);
});

test("users' middleware overrides the builtin one", () => {
Expand All @@ -602,15 +683,11 @@ See https://www.algolia.com/doc/api-reference/widgets/configure/js/`);

search.use(createInsightsMiddleware({}));

expect(
search.middleware.map(({ instance: { $$type, $$internal } }) => ({
$$type,
$$internal,
}))
).toEqual([
expect(mapMiddlewares(search.middleware)).toEqual([
{
$$type: 'ais.insights',
$$internal: false,
$$clickAnalytics: true,
},
]);
});
Expand Down
Loading

0 comments on commit 2a9e654

Please sign in to comment.