Skip to content

Commit 22016b0

Browse files
authored
fix(instantsearch.js): fix user token not being set in time for the first query (#6377)
1 parent c5f9a44 commit 22016b0

File tree

8 files changed

+337
-91
lines changed

8 files changed

+337
-91
lines changed

bundlesize.config.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@
1010
},
1111
{
1212
"path": "./packages/instantsearch.js/dist/instantsearch.production.min.js",
13-
"maxSize": "83.25 kB"
13+
"maxSize": "83.50 kB"
1414
},
1515
{
1616
"path": "./packages/instantsearch.js/dist/instantsearch.development.js",
17-
"maxSize": "180.25 kB"
17+
"maxSize": "181.50 kB"
1818
},
1919
{
2020
"path": "packages/react-instantsearch-core/dist/umd/ReactInstantSearchCore.min.js",
21-
"maxSize": "51 kB"
21+
"maxSize": "51.25 kB"
2222
},
2323
{
2424
"path": "packages/react-instantsearch/dist/umd/ReactInstantSearch.min.js",

packages/instantsearch.js/src/lib/__tests__/InstantSearch-integration-test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,7 @@ describe('network requests', () => {
489489
"params": {
490490
"clickAnalytics": true,
491491
"query": "",
492+
"userToken": "cookie-key",
492493
},
493494
},
494495
]
@@ -563,6 +564,7 @@ describe('network requests', () => {
563564
"params": {
564565
"clickAnalytics": true,
565566
"query": "",
567+
"userToken": "cookie-key",
566568
},
567569
},
568570
]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* Create UUID according to
3+
* https://www.ietf.org/rfc/rfc4122.txt.
4+
*
5+
* @returns Generated UUID.
6+
*/
7+
export function createUUID(): string {
8+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
9+
/* eslint-disable no-bitwise */
10+
const r = (Math.random() * 16) | 0;
11+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
12+
/* eslint-enable */
13+
return v.toString(16);
14+
});
15+
}

packages/instantsearch.js/src/middlewares/__tests__/createInsightsMiddleware.ts

Lines changed: 146 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { history } from '../../lib/routers';
2121
import { warning } from '../../lib/utils';
2222
import { dynamicWidgets, hits, refinementList } from '../../widgets';
2323

24+
import type { InsightsProps } from '..';
2425
import type { SearchClient } from '../../index.es';
2526
import type { PlainSearchParameters } from 'algoliasearch-helper';
2627
import type { JSDOM } from 'jsdom';
@@ -50,6 +51,10 @@ describe('insights', () => {
5051
searchClient = searchClientWithCredentials,
5152
started = true,
5253
insights = false,
54+
}: {
55+
searchClient?: SearchClient;
56+
started?: boolean;
57+
insights?: InsightsProps | boolean;
5358
} = {}) => {
5459
castToJestMock(searchClient.search).mockClear();
5560
const { analytics, insightsClient } = createInsights();
@@ -437,37 +442,6 @@ describe('insights', () => {
437442
);
438443
});
439444

440-
it('warns when userToken is not set', () => {
441-
const { insightsClient, instantSearchInstance } = createTestEnvironment();
442-
443-
instantSearchInstance.use(
444-
createInsightsMiddleware({
445-
insightsClient,
446-
insightsInitParams: {
447-
useCookie: false,
448-
anonymousUserToken: false,
449-
},
450-
})
451-
);
452-
453-
expect(() =>
454-
instantSearchInstance.sendEventToInsights({
455-
eventType: 'view',
456-
insightsMethod: 'viewedObjectIDs',
457-
payload: {
458-
eventName: 'Hits Viewed',
459-
index: '',
460-
objectIDs: ['1', '2'],
461-
},
462-
widgetType: 'ais.hits',
463-
})
464-
).toWarnDev(
465-
`[InstantSearch.js]: Cannot send event to Algolia Insights because \`userToken\` is not set.
466-
467-
See documentation: https://www.algolia.com/doc/guides/building-search-ui/going-further/send-insights-events/js/#setting-the-usertoken`
468-
);
469-
});
470-
471445
it('applies clickAnalytics if $$automatic: undefined', () => {
472446
const { insightsClient, instantSearchInstance } = createTestEnvironment();
473447
instantSearchInstance.use(
@@ -778,37 +752,34 @@ See documentation: https://www.algolia.com/doc/guides/building-search-ui/going-f
778752
});
779753

780754
it('handles multiple setUserToken calls before search.start()', async () => {
781-
const { insightsClient } = createInsights();
782-
const indexName = 'my-index';
783-
const instantSearchInstance = instantsearch({
784-
searchClient: createSearchClient({
785-
// @ts-expect-error only available in search client v4
786-
transporter: {
787-
headers: {
788-
'x-algolia-application-id': 'myAppId',
789-
'x-algolia-api-key': 'myApiKey',
790-
},
791-
},
792-
}),
793-
indexName,
794-
});
755+
const { insightsClient, instantSearchInstance, getUserToken } =
756+
createTestEnvironment();
795757

796-
const middleware = createInsightsMiddleware({
797-
insightsClient,
798-
});
799-
instantSearchInstance.use(middleware);
758+
instantSearchInstance.use(
759+
createInsightsMiddleware({
760+
insightsClient,
761+
})
762+
);
800763

801764
insightsClient('setUserToken', 'abc');
802765
insightsClient('setUserToken', 'def');
803766

804-
instantSearchInstance.start();
767+
expect(getUserToken()).toEqual('def');
805768

806-
await wait(0);
769+
instantSearchInstance.addWidgets([connectSearchBox(() => ({}))({})]);
807770

808-
expect(
809-
(instantSearchInstance.mainHelper!.state as PlainSearchParameters)
810-
.userToken
811-
).toEqual('def');
771+
await wait(0);
772+
expect(instantSearchInstance.client.search).toHaveBeenCalledTimes(1);
773+
expect(instantSearchInstance.client.search).toHaveBeenLastCalledWith([
774+
{
775+
indexName: 'my-index',
776+
params: {
777+
clickAnalytics: true,
778+
query: '',
779+
userToken: getUserToken(),
780+
},
781+
},
782+
]);
812783
});
813784

814785
it('searches once per unique userToken', async () => {
@@ -836,7 +807,8 @@ See documentation: https://www.algolia.com/doc/guides/building-search-ui/going-f
836807
});
837808

838809
it("doesn't search when userToken is falsy", async () => {
839-
const { insightsClient, instantSearchInstance } = createTestEnvironment();
810+
const { insightsClient, instantSearchInstance, getUserToken } =
811+
createTestEnvironment();
840812

841813
instantSearchInstance.addWidgets([connectSearchBox(() => ({}))({})]);
842814

@@ -866,8 +838,8 @@ See documentation: https://www.algolia.com/doc/guides/building-search-ui/going-f
866838
indexName: 'my-index',
867839
params: {
868840
clickAnalytics: true,
869-
870841
query: '',
842+
userToken: getUserToken(),
871843
},
872844
},
873845
]);
@@ -878,6 +850,81 @@ See documentation: https://www.algolia.com/doc/guides/building-search-ui/going-f
878850
expect(instantSearchInstance.client.search).toHaveBeenCalledTimes(2);
879851
});
880852

853+
it('sets an anonymous token as the userToken if none given', async () => {
854+
const { instantSearchInstance, getUserToken } = createTestEnvironment({
855+
insights: true,
856+
});
857+
858+
instantSearchInstance.addWidgets([connectSearchBox(() => ({}))({})]);
859+
860+
await wait(0);
861+
expect(instantSearchInstance.client.search).toHaveBeenCalledTimes(1);
862+
expect(instantSearchInstance.client.search).toHaveBeenLastCalledWith([
863+
{
864+
indexName: 'my-index',
865+
params: {
866+
clickAnalytics: true,
867+
query: '',
868+
userToken: getUserToken(),
869+
},
870+
},
871+
]);
872+
873+
expect(getUserToken()).toEqual(expect.stringMatching(/^anonymous-/));
874+
});
875+
876+
it('saves an anonymous token to a cookie if useCookie is true in insights init props', () => {
877+
const { getUserToken } = createTestEnvironment({
878+
insights: {
879+
insightsInitParams: {
880+
useCookie: true,
881+
},
882+
},
883+
});
884+
885+
const userToken = getUserToken();
886+
expect(userToken).toEqual(expect.stringMatching(/^anonymous-/));
887+
expect(document.cookie).toBe(`_ALGOLIA=${userToken}`);
888+
});
889+
890+
it('saves an anonymous token to a cookie if useCookie is true insights init method', async () => {
891+
const { instantSearchInstance, insightsClient, getUserToken } =
892+
createTestEnvironment({
893+
insights: true,
894+
started: false,
895+
});
896+
897+
insightsClient('init', { partial: true, useCookie: true });
898+
899+
instantSearchInstance.start();
900+
901+
await wait(0);
902+
const userToken = getUserToken();
903+
expect(userToken).toEqual(expect.stringMatching(/^anonymous-/));
904+
expect(document.cookie).toBe(`_ALGOLIA=${userToken}`);
905+
});
906+
907+
it('uses `userToken` from insights init props over anything else', async () => {
908+
document.cookie = '_ALGOLIA=abc';
909+
910+
const { instantSearchInstance, insightsClient, getUserToken } =
911+
createTestEnvironment({
912+
insights: {
913+
insightsInitParams: {
914+
userToken: 'def',
915+
},
916+
},
917+
started: false,
918+
});
919+
920+
insightsClient('init', { partial: true, userToken: 'ghi' });
921+
922+
instantSearchInstance.start();
923+
924+
await wait(0);
925+
expect(getUserToken()).toBe('def');
926+
});
927+
881928
describe('authenticatedUserToken', () => {
882929
describe('before `init`', () => {
883930
it('uses the `authenticatedUserToken` as the `userToken` when defined', () => {
@@ -921,6 +968,39 @@ See documentation: https://www.algolia.com/doc/guides/building-search-ui/going-f
921968

922969
expect(getUserToken()).toEqual('abc');
923970
});
971+
972+
it('uses the `authenticatedUserToken` when a `userToken` is set after', () => {
973+
const { insightsClient, instantSearchInstance, getUserToken } =
974+
createTestEnvironment();
975+
976+
insightsClient('setAuthenticatedUserToken', 'def');
977+
978+
instantSearchInstance.use(
979+
createInsightsMiddleware({ insightsClient })
980+
);
981+
982+
insightsClient('setUserToken', 'abc');
983+
984+
expect(getUserToken()).toEqual('def');
985+
});
986+
987+
it('resets the token to the `userToken` when `authenticatedUserToken` is set as undefined', () => {
988+
const { insightsClient, instantSearchInstance, getUserToken } =
989+
createTestEnvironment();
990+
991+
insightsClient('setUserToken', 'abc');
992+
insightsClient('setAuthenticatedUserToken', 'def');
993+
994+
instantSearchInstance.use(
995+
createInsightsMiddleware({ insightsClient })
996+
);
997+
998+
expect(getUserToken()).toEqual('def');
999+
1000+
insightsClient('setAuthenticatedUserToken', undefined);
1001+
1002+
expect(getUserToken()).toEqual('abc');
1003+
});
9241004
});
9251005

9261006
describe('after `init`', () => {
@@ -1361,13 +1441,15 @@ See documentation: https://www.algolia.com/doc/guides/building-search-ui/going-f
13611441

13621442
// Dynamic widgets will trigger 2 searches. To avoid missing the cache on the second search, createInsightsMiddleware delays setting the userToken.
13631443

1444+
const userToken = getUserToken();
1445+
13641446
expect(searchClient.search).toHaveBeenCalledTimes(2);
1365-
expect(
1366-
searchClient.search.mock.calls[0][0][0].params.userToken
1367-
).toBeUndefined();
1368-
expect(
1369-
searchClient.search.mock.calls[1][0][0].params.userToken
1370-
).toBeUndefined();
1447+
expect(searchClient.search.mock.calls[0][0][0].params.userToken).toBe(
1448+
userToken
1449+
);
1450+
expect(searchClient.search.mock.calls[1][0][0].params.userToken).toBe(
1451+
userToken
1452+
);
13711453

13721454
await wait(0);
13731455

@@ -1383,7 +1465,7 @@ See documentation: https://www.algolia.com/doc/guides/building-search-ui/going-f
13831465
expect(searchClient.search).toHaveBeenCalledTimes(3);
13841466
expect(searchClient.search).toHaveBeenLastCalledWith([
13851467
expect.objectContaining({
1386-
params: expect.objectContaining({ userToken: getUserToken() }),
1468+
params: expect.objectContaining({ userToken }),
13871469
}),
13881470
]);
13891471
});

0 commit comments

Comments
 (0)