Skip to content

Commit a02c2c1

Browse files
sarahdayanHaroenv
authored andcommitted
feat(createAlgoliaInsightsPlugin): automatically load Insights when not passed (#1106)
1 parent 8144cf3 commit a02c2c1

File tree

8 files changed

+270
-5
lines changed

8 files changed

+270
-5
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"rollup-plugin-license": "2.9.1",
8080
"rollup-plugin-terser": "7.0.2",
8181
"shipjs": "0.26.1",
82+
"search-insights": "2.3.0",
8283
"start-server-and-test": "1.15.2",
8384
"stylelint": "13.13.1",
8485
"stylelint-a11y": "1.2.3",

packages/autocomplete-plugin-algolia-insights/src/__tests__/createAlgoliaInsightsPlugin.test.ts

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
getAlgoliaResults,
55
} from '@algolia/autocomplete-preset-algolia';
66
import { noop } from '@algolia/autocomplete-shared';
7+
import { fireEvent } from '@testing-library/dom';
78
import userEvent from '@testing-library/user-event';
89
import insightsClient from 'search-insights';
910

@@ -12,11 +13,17 @@ import {
1213
createPlayground,
1314
createSearchClient,
1415
createSource,
16+
defer,
1517
runAllMicroTasks,
1618
} from '../../../../test/utils';
1719
import { createAlgoliaInsightsPlugin } from '../createAlgoliaInsightsPlugin';
1820

19-
jest.useFakeTimers();
21+
beforeEach(() => {
22+
(window as any).AlgoliaAnalyticsObject = undefined;
23+
(window as any).aa = undefined;
24+
25+
document.body.innerHTML = '';
26+
});
2027

2128
describe('createAlgoliaInsightsPlugin', () => {
2229
test('has a name', () => {
@@ -70,7 +77,7 @@ describe('createAlgoliaInsightsPlugin', () => {
7077
);
7178
});
7279

73-
test('sets a user agent on the Insights client on subscribe', () => {
80+
test('sets a user agent on on subscribe', () => {
7481
const insightsClient = jest.fn();
7582
const insightsPlugin = createAlgoliaInsightsPlugin({ insightsClient });
7683

@@ -167,7 +174,129 @@ describe('createAlgoliaInsightsPlugin', () => {
167174
]);
168175
});
169176

177+
describe('automatic pulling', () => {
178+
const consoleError = jest
179+
.spyOn(console, 'error')
180+
.mockImplementation(() => {});
181+
182+
afterAll(() => {
183+
consoleError.mockReset();
184+
});
185+
186+
it('does not load the script when the Insights client is passed', async () => {
187+
createPlayground(createAutocomplete, {
188+
plugins: [createAlgoliaInsightsPlugin({ insightsClient: noop })],
189+
});
190+
191+
await defer(noop, 0);
192+
193+
expect(document.body).toMatchInlineSnapshot(`
194+
<body>
195+
<form>
196+
<input />
197+
</form>
198+
</body>
199+
`);
200+
expect((window as any).AlgoliaAnalyticsObject).toBeUndefined();
201+
expect((window as any).aa).toBeUndefined();
202+
});
203+
204+
it('does not load the script when the Insights client is present in the page', async () => {
205+
(window as any).AlgoliaAnalyticsObject = 'aa';
206+
const aa = noop;
207+
(window as any).aa = aa;
208+
209+
createPlayground(createAutocomplete, {
210+
plugins: [createAlgoliaInsightsPlugin({})],
211+
});
212+
213+
await defer(noop, 0);
214+
215+
expect(document.body).toMatchInlineSnapshot(`
216+
<body>
217+
<form>
218+
<input />
219+
</form>
220+
</body>
221+
`);
222+
expect((window as any).AlgoliaAnalyticsObject).toBe('aa');
223+
expect((window as any).aa).toBe(aa);
224+
expect((window as any).aa.version).toBeUndefined();
225+
});
226+
227+
it('loads the script when the Insights client is not passed and not present in the page', async () => {
228+
createPlayground(createAutocomplete, {
229+
plugins: [createAlgoliaInsightsPlugin({})],
230+
});
231+
232+
await defer(noop, 0);
233+
234+
expect(document.body).toMatchInlineSnapshot(`
235+
<body>
236+
<script
237+
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/search-insights.min.js"
238+
/>
239+
<form>
240+
<input />
241+
</form>
242+
</body>
243+
`);
244+
expect((window as any).AlgoliaAnalyticsObject).toBe('aa');
245+
expect((window as any).aa).toEqual(expect.any(Function));
246+
expect((window as any).aa.version).toBe('2.3.0');
247+
});
248+
249+
it('notifies when the script fails to be added', () => {
250+
// @ts-ignore `createElement` is a class method can thus only be called on
251+
// an instance of `Document`, not as a standalone function.
252+
// This is needed to call the actual implementation later in the test.
253+
document.originalCreateElement = document.createElement;
254+
255+
document.createElement = (tagName) => {
256+
if (tagName === 'script') {
257+
throw new Error('error');
258+
}
259+
260+
// @ts-ignore
261+
return document.originalCreateElement(tagName);
262+
};
263+
264+
createPlayground(createAutocomplete, {
265+
plugins: [createAlgoliaInsightsPlugin({})],
266+
});
267+
268+
expect(consoleError).toHaveBeenCalledWith(
269+
'[Autocomplete]: Could not load search-insights.js. Please load it manually following https://alg.li/insights-autocomplete'
270+
);
271+
272+
// @ts-ignore
273+
document.createElement = document.originalCreateElement;
274+
});
275+
276+
it('notifies when the script fails to load', async () => {
277+
createPlayground(createAutocomplete, {
278+
plugins: [createAlgoliaInsightsPlugin({})],
279+
});
280+
281+
await defer(noop, 0);
282+
283+
fireEvent(document.querySelector('script')!, new ErrorEvent('error'));
284+
285+
expect(consoleError).toHaveBeenCalledWith(
286+
'[Autocomplete]: Could not load search-insights.js. Please load it manually following https://alg.li/insights-autocomplete'
287+
);
288+
});
289+
});
290+
170291
describe('onItemsChange', () => {
292+
beforeAll(() => {
293+
jest.useFakeTimers();
294+
});
295+
296+
afterAll(() => {
297+
jest.useRealTimers();
298+
});
299+
171300
test('sends a `viewedObjectIDs` event by default', async () => {
172301
const insightsClient = jest.fn();
173302
const insightsPlugin = createAlgoliaInsightsPlugin({ insightsClient });

packages/autocomplete-plugin-algolia-insights/src/createAlgoliaInsightsPlugin.ts

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
debounce,
88
isEqual,
99
noop,
10+
safelyRunOnBrowser,
1011
} from '@algolia/autocomplete-shared';
1112

1213
import { createClickedEvent } from './createClickedEvent';
@@ -23,6 +24,8 @@ import {
2324
} from './types';
2425

2526
const VIEW_EVENT_DELAY = 400;
27+
const ALGOLIA_INSIGHTS_VERSION = '2.3.0';
28+
const ALGOLIA_INSIGHTS_SRC = `https://cdn.jsdelivr.net/npm/search-insights@${ALGOLIA_INSIGHTS_VERSION}/dist/search-insights.min.js`;
2629

2730
type SendViewedObjectIDsParams = {
2831
onItemsChange(params: OnItemsChangeParams): void;
@@ -51,7 +54,7 @@ export type CreateAlgoliaInsightsPluginParams = {
5154
*
5255
* @link https://www.algolia.com/doc/ui-libraries/autocomplete/api-reference/autocomplete-plugin-algolia-insights/createAlgoliaInsightsPlugin/#param-insightsclient
5356
*/
54-
insightsClient: InsightsClient;
57+
insightsClient?: InsightsClient;
5558
/**
5659
* Hook to send an Insights event when the items change.
5760
*
@@ -84,11 +87,43 @@ export function createAlgoliaInsightsPlugin(
8487
options: CreateAlgoliaInsightsPluginParams
8588
): AutocompletePlugin<any, undefined> {
8689
const {
87-
insightsClient,
90+
insightsClient: providedInsightsClient,
8891
onItemsChange,
8992
onSelect: onSelectEvent,
9093
onActive: onActiveEvent,
9194
} = getOptions(options);
95+
let insightsClient = providedInsightsClient as InsightsClient;
96+
97+
if (!providedInsightsClient) {
98+
safelyRunOnBrowser(({ window }) => {
99+
const pointer = window.AlgoliaAnalyticsObject || 'aa';
100+
101+
if (typeof pointer === 'string') {
102+
insightsClient = window[pointer];
103+
}
104+
105+
if (!insightsClient) {
106+
window.AlgoliaAnalyticsObject = pointer;
107+
108+
if (!window[pointer]) {
109+
window[pointer] = (...args: any[]) => {
110+
if (!window[pointer].queue) {
111+
window[pointer].queue = [];
112+
}
113+
114+
window[pointer].queue.push(args);
115+
};
116+
}
117+
118+
window[pointer].version = ALGOLIA_INSIGHTS_VERSION;
119+
120+
insightsClient = window[pointer];
121+
122+
loadInsights(window);
123+
}
124+
});
125+
}
126+
92127
const insights = createSearchInsightsApi(insightsClient);
93128
const previousItems = createRef<AlgoliaInsightsHit[]>([]);
94129

@@ -190,3 +225,23 @@ function getOptions(options: CreateAlgoliaInsightsPluginParams) {
190225
...options,
191226
};
192227
}
228+
229+
function loadInsights(environment: typeof window) {
230+
const errorMessage = `[Autocomplete]: Could not load search-insights.js. Please load it manually following https://alg.li/insights-autocomplete`;
231+
232+
try {
233+
const script = environment.document.createElement('script');
234+
script.async = true;
235+
script.src = ALGOLIA_INSIGHTS_SRC;
236+
237+
script.onerror = () => {
238+
// eslint-disable-next-line no-console
239+
console.error(errorMessage);
240+
};
241+
242+
document.body.appendChild(script);
243+
} catch (cause) {
244+
// eslint-disable-next-line no-console
245+
console.error(errorMessage);
246+
}
247+
}
Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,30 @@
1-
export type InsightsClient = any;
1+
import type {
2+
InsightsMethodMap,
3+
InsightsClient as _InsightsClient,
4+
} from 'search-insights';
5+
6+
export type {
7+
Init as InsightsInit,
8+
AddAlgoliaAgent as InsightsAddAlgoliaAgent,
9+
SetUserToken as InsightsSetUserToken,
10+
GetUserToken as InsightsGetUserToken,
11+
OnUserTokenChange as InsightsOnUserTokenChange,
12+
} from 'search-insights';
13+
14+
export type InsightsClientMethod = keyof InsightsMethodMap;
15+
16+
export type InsightsClientPayload = {
17+
eventName: string;
18+
queryID: string;
19+
index: string;
20+
objectIDs: string[];
21+
positions?: number[];
22+
};
23+
24+
type QueueItemMap = Record<string, unknown>;
25+
26+
type QueueItem = QueueItemMap[keyof QueueItemMap];
27+
28+
export type InsightsClient = _InsightsClient & {
29+
queue?: QueueItem[];
30+
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { safelyRunOnBrowser } from '../safelyRunOnBrowser';
2+
3+
describe('safelyRunOnBrowser', () => {
4+
const originalWindow = (global as any).window;
5+
6+
afterEach(() => {
7+
(global as any).window = originalWindow;
8+
});
9+
10+
test('runs callback on browsers', () => {
11+
const callback = jest.fn(() => ({ env: 'client' }));
12+
13+
const result = safelyRunOnBrowser(callback);
14+
15+
expect(callback).toHaveBeenCalledTimes(1);
16+
expect(callback).toHaveBeenCalledWith({ window });
17+
expect(result).toEqual({ env: 'client' });
18+
});
19+
20+
test('does not run callback on servers', () => {
21+
// @ts-expect-error
22+
delete global.window;
23+
24+
const callback = jest.fn(() => ({ env: 'client' }));
25+
26+
const result = safelyRunOnBrowser(callback);
27+
28+
expect(callback).toHaveBeenCalledTimes(0);
29+
expect(result).toBeUndefined();
30+
});
31+
});

packages/autocomplete-shared/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export * from './invariant';
99
export * from './isEqual';
1010
export * from './MaybePromise';
1111
export * from './noop';
12+
export * from './safelyRunOnBrowser';
1213
export * from './UserAgent';
1314
export * from './userAgents';
1415
export * from './version';
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
type BrowserCallback<TReturn> = (params: { window: typeof window }) => TReturn;
2+
3+
/**
4+
* Safely runs code meant for browser environments only.
5+
*/
6+
export function safelyRunOnBrowser<TReturn>(
7+
callback: BrowserCallback<TReturn>
8+
): TReturn | undefined {
9+
if (typeof window !== 'undefined') {
10+
return callback({ window });
11+
}
12+
13+
return undefined;
14+
}

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17506,6 +17506,11 @@ [email protected]:
1750617506
resolved "https://registry.yarnpkg.com/search-insights/-/search-insights-1.7.1.tgz#eddfa56910e28cbbb0df80aec2ab8acf0a86cb6b"
1750717507
integrity sha512-CSuSKIJp+WcSwYrD9GgIt1e3xmI85uyAefC4/KYGgtvNEm6rt4kBGilhVRmTJXxRE2W1JknvP598Q7SMhm7qKA==
1750817508

17509+
17510+
version "2.3.0"
17511+
resolved "https://registry.yarnpkg.com/search-insights/-/search-insights-2.3.0.tgz#9a7bb25428fc7f003bafdb5638e90276113daae6"
17512+
integrity sha512-0v/TTO4fbd6I91sFBK/e2zNfD0f51A+fMoYNkMplmR77NpThUye/7gIxNoJ3LejKpZH6Z2KNBIpxxFmDKj10Yw==
17513+
1750917514
search-insights@^2.1.0:
1751017515
version "2.2.1"
1751117516
resolved "https://registry.yarnpkg.com/search-insights/-/search-insights-2.2.1.tgz#9c93344fbae5fbf2f88c1a81b46b4b5d888c11f7"

0 commit comments

Comments
 (0)