Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(insights): ensure the same token is used when rendered multiple times server side #6456

Merged
merged 10 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bundlesize.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@
},
{
"path": "./packages/instantsearch.js/dist/instantsearch.development.js",
"maxSize": "181.50 kB"
"maxSize": "185 kB"
},
{
"path": "packages/react-instantsearch-core/dist/umd/ReactInstantSearchCore.min.js",
"maxSize": "51.25 kB"
},
{
"path": "packages/react-instantsearch/dist/umd/ReactInstantSearch.min.js",
"maxSize": "65 kB"
"maxSize": "65.50 kB"
},
{
"path": "packages/vue-instantsearch/vue2/umd/index.js",
Expand Down
4 changes: 1 addition & 3 deletions examples/react/next-app-router/app/Search.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use client';

import { liteClient as algoliasearch } from 'algoliasearch/lite';
import { Hit as AlgoliaHit } from 'instantsearch.js';
import React from 'react';
import {
Expand All @@ -13,8 +12,7 @@ import {
import { InstantSearchNext } from 'react-instantsearch-nextjs';

import { Panel } from '../components/Panel';

const client = algoliasearch('latency', '6be0576ff61c053d5f9a3225e2a90f76');
import { client } from '../lib/client';

type HitProps = {
hit: AlgoliaHit<{
Expand Down
4 changes: 4 additions & 0 deletions examples/react/next-app-router/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import React from 'react';

import { responsesCache } from '../lib/client';

import Search from './Search';

export const dynamic = 'force-dynamic';

export default function Page() {
responsesCache.clear();

return <Search />;
}
9 changes: 9 additions & 0 deletions examples/react/next-app-router/lib/client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createMemoryCache } from '@algolia/client-common';
import { liteClient as algoliasearch } from 'algoliasearch/lite';

export const responsesCache = createMemoryCache();
export const client = algoliasearch(
'latency',
'6be0576ff61c053d5f9a3225e2a90f76',
{ responsesCache }
);
7 changes: 6 additions & 1 deletion examples/react/next-routing/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createMemoryCache } from '@algolia/client-common';
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import { Hit as AlgoliaHit } from 'instantsearch.js';
import { GetServerSideProps } from 'next';
Expand All @@ -21,7 +22,10 @@ import { createInstantSearchRouterNext } from 'react-instantsearch-router-nextjs

import { Panel } from '../components/Panel';

const client = algoliasearch('latency', '6be0576ff61c053d5f9a3225e2a90f76');
const requestsCache = createMemoryCache();
const client = algoliasearch('latency', '6be0576ff61c053d5f9a3225e2a90f76', {
requestsCache,
});

type HitProps = {
hit: AlgoliaHit<{
Expand Down Expand Up @@ -99,6 +103,7 @@ export const getServerSideProps: GetServerSideProps<HomePageProps> =
const serverState = await getServerState(<HomePage url={url} />, {
renderToString,
});
requestsCache.clear();

return {
props: {
Expand Down
7 changes: 6 additions & 1 deletion examples/react/next-routing/pages/test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// This is only to test `onStateChange` does not get called twice
import { createMemoryCache } from '@algolia/client-common';
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import { GetServerSideProps } from 'next';
import Head from 'next/head';
Expand All @@ -16,7 +17,10 @@ import {
} from 'react-instantsearch';
import { createInstantSearchRouterNext } from 'react-instantsearch-router-nextjs';

const client = algoliasearch('latency', '6be0576ff61c053d5f9a3225e2a90f76');
const requestsCache = createMemoryCache();
const client = algoliasearch('latency', '6be0576ff61c053d5f9a3225e2a90f76', {
requestsCache,
});

type HomePageProps = {
serverState?: InstantSearchServerState;
Expand Down Expand Up @@ -74,6 +78,7 @@ export const getServerSideProps: GetServerSideProps<HomePageProps> =
const serverState = await getServerState(<HomePage url={url} />, {
renderToString,
});
requestsCache.clear();

return {
props: {
Expand Down
11 changes: 10 additions & 1 deletion examples/react/next/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createMemoryCache } from '@algolia/client-common';
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import { Hit as AlgoliaHit } from 'instantsearch.js';
import { GetServerSideProps } from 'next';
Expand All @@ -20,7 +21,10 @@ import { createInstantSearchRouterNext } from 'react-instantsearch-router-nextjs

import { Panel } from '../components/Panel';

const client = algoliasearch('latency', '6be0576ff61c053d5f9a3225e2a90f76');
const responsesCache = createMemoryCache();
const client = algoliasearch('latency', '6be0576ff61c053d5f9a3225e2a90f76', {
responsesCache,
});

type HitProps = {
hit: AlgoliaHit<{
Expand Down Expand Up @@ -63,6 +67,9 @@ export default function HomePage({ serverState, url }: HomePageProps) {
}),
}}
insights={true}
future={{
preserveSharedStateOnUnmount: true,
}}
>
<div className="Container">
<div>
Expand Down Expand Up @@ -94,6 +101,8 @@ export const getServerSideProps: GetServerSideProps<HomePageProps> =
renderToString,
});

responsesCache.clear();

return {
props: {
serverState,
Expand Down
7 changes: 6 additions & 1 deletion examples/react/ssr/src/searchClient.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { createMemoryCache } from '@algolia/client-common';
import { liteClient as algoliasearch } from 'algoliasearch/lite';

export const requestsCache = createMemoryCache();
export const searchClient = algoliasearch(
'latency',
'6be0576ff61c053d5f9a3225e2a90f76'
'6be0576ff61c053d5f9a3225e2a90f76',
{
requestsCache,
}
);
2 changes: 2 additions & 0 deletions examples/react/ssr/src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { renderToString } from 'react-dom/server';
import { getServerState } from 'react-instantsearch';

import App from './App';
import { requestsCache } from './searchClient';

const app = express();

Expand All @@ -18,6 +19,7 @@ app.get('/', async (req, res) => {
const serverState = await getServerState(<App location={location} />, {
renderToString,
});
requestsCache.clear();
const html = renderToString(
<App serverState={serverState} location={location} />
);
Expand Down
6 changes: 6 additions & 0 deletions packages/algoliasearch-helper/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1098,6 +1098,12 @@ declare namespace algoliasearchHelper {
* https://www.algolia.com/doc/api-reference/api-parameters/optionalFilters/
*/
optionalFilters?: Array<string | string[]>;
/**
* Unique pseudonymous or anonymous user identifier.
* This helps with analytics and click and conversion events.
* For more information, see [user token](https://www.algolia.com/doc/guides/sending-events/concepts/usertoken/).
*/
userToken?: string;
/**
* If set to false, this query will not be taken into account in the analytics feature.
* default true
Expand Down
72 changes: 72 additions & 0 deletions packages/instantsearch.js/src/lib/__tests__/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,4 +421,76 @@ describe('getInitialResults', () => {
]
`);
});

test('injects clickAnalytics and userToken into the state', async () => {
const search = instantsearch({
indexName: 'indexName',
searchClient: createSearchClient(),
initialUiState: {
indexName: {
query: 'apple',
},
},
insights: true,
});

search.addWidgets([connectSearchBox(() => {})({})]);

search.start();

const requestParams = await waitForResults(search);

expect(requestParams).toEqual([
{
clickAnalytics: true,
query: 'apple',
userToken: expect.stringMatching(/^anonymous-/),
},
]);

const initialResults = getInitialResults(search.mainIndex, requestParams);
expect(initialResults).toEqual({
indexName: {
requestParams: [
{
clickAnalytics: true,
query: 'apple',
userToken: expect.stringMatching(/^anonymous-/),
Haroenv marked this conversation as resolved.
Show resolved Hide resolved
},
],
results: [
{
exhaustiveFacetsCount: true,
exhaustiveNbHits: true,
hits: [],
hitsPerPage: 20,
nbHits: 0,
nbPages: 0,
page: 0,
params: '',
processingTimeMS: 0,
query: '',
},
],
state: {
disjunctiveFacets: [],
disjunctiveFacetsRefinements: {},
facets: [],
facetsExcludes: {},
facetsRefinements: {},
hierarchicalFacets: [],
hierarchicalFacetsRefinements: {},
index: 'indexName',
numericRefinements: {},
query: 'apple',
tagRefinements: [],
clickAnalytics: true,
userToken: expect.stringMatching(/^anonymous-/),
},
},
});
expect(initialResults.indexName.requestParams![0].userToken).toEqual(
initialResults.indexName.state!.userToken
);
});
});
6 changes: 5 additions & 1 deletion packages/instantsearch.js/src/lib/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,11 @@ export function getInitialResults(
// We convert the Helper state to a plain object to pass parsable data
// structures from server to client.
...(searchResults && {
state: { ...searchResults._state },
state: {
...searchResults._state,
clickAnalytics: requestParams?.[0]?.clickAnalytics,
userToken: requestParams?.[0]?.userToken,
},
results: searchResults._rawResults,
}),
...(recommendResults && {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,28 @@ describe('insights', () => {
expect(getUserToken()).toBe('def');
});

it('uses `userToken` from initial results', () => {
const { insightsClient, instantSearchInstance, getUserToken } =
createTestEnvironment();

instantSearchInstance._initialResults = {
[instantSearchInstance.indexName]: {
state: {
userToken: 'from-initial-results',
clickAnalytics: true,
},
},
};

instantSearchInstance.use(
createInsightsMiddleware({
insightsClient,
})
);

expect(getUserToken()).toEqual('from-initial-results');
});

describe('authenticatedUserToken', () => {
describe('before `init`', () => {
it('uses the `authenticatedUserToken` as the `userToken` when defined', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
InsightsMethod,
InsightsMethodMap,
InternalMiddleware,
InstantSearch,
} from '../types';
import type {
AlgoliaSearchHelper,
Expand Down Expand Up @@ -222,10 +223,7 @@ export function createInsightsMiddleware<
});
}

initialParameters = {
userToken: (helper.state as PlainSearchParameters).userToken,
clickAnalytics: helper.state.clickAnalytics,
};
initialParameters = getInitialParameters(instantSearchInstance);

// We don't want to force clickAnalytics when the insights is enabled from the search response.
// This means we don't enable insights for indices that don't opt in
Expand Down Expand Up @@ -522,6 +520,23 @@ See documentation: https://www.algolia.com/doc/guides/building-search-ui/going-f
};
}

function getInitialParameters(
instantSearchInstance: InstantSearch
): PlainSearchParameters {
// in SSR, the initial state we use in this domain is set on the main index
const stateFromInitialResults =
instantSearchInstance._initialResults?.[instantSearchInstance.indexName]
?.state || {};

const stateFromHelper = instantSearchInstance.mainHelper!.state;

return {
userToken: stateFromInitialResults.userToken || stateFromHelper.userToken,
clickAnalytics:
stateFromInitialResults.clickAnalytics || stateFromHelper.clickAnalytics,
};
}

function saveTokenAsCookie(token: string, cookieDuration?: number) {
const MONTH = 30 * 24 * 60 * 60 * 1000;
const d = new Date();
Expand Down
Loading