Skip to content

Commit

Permalink
feat(createAlgoliaInsightsPlugin): automatically load Insights when n…
Browse files Browse the repository at this point in the history
…ot passed (#1106)
  • Loading branch information
sarahdayan authored and Haroenv committed Apr 24, 2023
1 parent 8144cf3 commit a02c2c1
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 5 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"rollup-plugin-license": "2.9.1",
"rollup-plugin-terser": "7.0.2",
"shipjs": "0.26.1",
"search-insights": "2.3.0",
"start-server-and-test": "1.15.2",
"stylelint": "13.13.1",
"stylelint-a11y": "1.2.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getAlgoliaResults,
} from '@algolia/autocomplete-preset-algolia';
import { noop } from '@algolia/autocomplete-shared';
import { fireEvent } from '@testing-library/dom';
import userEvent from '@testing-library/user-event';
import insightsClient from 'search-insights';

Expand All @@ -12,11 +13,17 @@ import {
createPlayground,
createSearchClient,
createSource,
defer,
runAllMicroTasks,
} from '../../../../test/utils';
import { createAlgoliaInsightsPlugin } from '../createAlgoliaInsightsPlugin';

jest.useFakeTimers();
beforeEach(() => {
(window as any).AlgoliaAnalyticsObject = undefined;
(window as any).aa = undefined;

document.body.innerHTML = '';
});

describe('createAlgoliaInsightsPlugin', () => {
test('has a name', () => {
Expand Down Expand Up @@ -70,7 +77,7 @@ describe('createAlgoliaInsightsPlugin', () => {
);
});

test('sets a user agent on the Insights client on subscribe', () => {
test('sets a user agent on on subscribe', () => {
const insightsClient = jest.fn();
const insightsPlugin = createAlgoliaInsightsPlugin({ insightsClient });

Expand Down Expand Up @@ -167,7 +174,129 @@ describe('createAlgoliaInsightsPlugin', () => {
]);
});

describe('automatic pulling', () => {
const consoleError = jest
.spyOn(console, 'error')
.mockImplementation(() => {});

afterAll(() => {
consoleError.mockReset();
});

it('does not load the script when the Insights client is passed', async () => {
createPlayground(createAutocomplete, {
plugins: [createAlgoliaInsightsPlugin({ insightsClient: noop })],
});

await defer(noop, 0);

expect(document.body).toMatchInlineSnapshot(`
<body>
<form>
<input />
</form>
</body>
`);
expect((window as any).AlgoliaAnalyticsObject).toBeUndefined();
expect((window as any).aa).toBeUndefined();
});

it('does not load the script when the Insights client is present in the page', async () => {
(window as any).AlgoliaAnalyticsObject = 'aa';
const aa = noop;
(window as any).aa = aa;

createPlayground(createAutocomplete, {
plugins: [createAlgoliaInsightsPlugin({})],
});

await defer(noop, 0);

expect(document.body).toMatchInlineSnapshot(`
<body>
<form>
<input />
</form>
</body>
`);
expect((window as any).AlgoliaAnalyticsObject).toBe('aa');
expect((window as any).aa).toBe(aa);
expect((window as any).aa.version).toBeUndefined();
});

it('loads the script when the Insights client is not passed and not present in the page', async () => {
createPlayground(createAutocomplete, {
plugins: [createAlgoliaInsightsPlugin({})],
});

await defer(noop, 0);

expect(document.body).toMatchInlineSnapshot(`
<body>
<script
src="https://cdn.jsdelivr.net/npm/search-insights@2.3.0/dist/search-insights.min.js"
/>
<form>
<input />
</form>
</body>
`);
expect((window as any).AlgoliaAnalyticsObject).toBe('aa');
expect((window as any).aa).toEqual(expect.any(Function));
expect((window as any).aa.version).toBe('2.3.0');
});

it('notifies when the script fails to be added', () => {
// @ts-ignore `createElement` is a class method can thus only be called on
// an instance of `Document`, not as a standalone function.
// This is needed to call the actual implementation later in the test.
document.originalCreateElement = document.createElement;

document.createElement = (tagName) => {
if (tagName === 'script') {
throw new Error('error');
}

// @ts-ignore
return document.originalCreateElement(tagName);
};

createPlayground(createAutocomplete, {
plugins: [createAlgoliaInsightsPlugin({})],
});

expect(consoleError).toHaveBeenCalledWith(
'[Autocomplete]: Could not load search-insights.js. Please load it manually following https://alg.li/insights-autocomplete'
);

// @ts-ignore
document.createElement = document.originalCreateElement;
});

it('notifies when the script fails to load', async () => {
createPlayground(createAutocomplete, {
plugins: [createAlgoliaInsightsPlugin({})],
});

await defer(noop, 0);

fireEvent(document.querySelector('script')!, new ErrorEvent('error'));

expect(consoleError).toHaveBeenCalledWith(
'[Autocomplete]: Could not load search-insights.js. Please load it manually following https://alg.li/insights-autocomplete'
);
});
});

describe('onItemsChange', () => {
beforeAll(() => {
jest.useFakeTimers();
});

afterAll(() => {
jest.useRealTimers();
});

test('sends a `viewedObjectIDs` event by default', async () => {
const insightsClient = jest.fn();
const insightsPlugin = createAlgoliaInsightsPlugin({ insightsClient });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
debounce,
isEqual,
noop,
safelyRunOnBrowser,
} from '@algolia/autocomplete-shared';

import { createClickedEvent } from './createClickedEvent';
Expand All @@ -23,6 +24,8 @@ import {
} from './types';

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

type SendViewedObjectIDsParams = {
onItemsChange(params: OnItemsChangeParams): void;
Expand Down Expand Up @@ -51,7 +54,7 @@ export type CreateAlgoliaInsightsPluginParams = {
*
* @link https://www.algolia.com/doc/ui-libraries/autocomplete/api-reference/autocomplete-plugin-algolia-insights/createAlgoliaInsightsPlugin/#param-insightsclient
*/
insightsClient: InsightsClient;
insightsClient?: InsightsClient;
/**
* Hook to send an Insights event when the items change.
*
Expand Down Expand Up @@ -84,11 +87,43 @@ export function createAlgoliaInsightsPlugin(
options: CreateAlgoliaInsightsPluginParams
): AutocompletePlugin<any, undefined> {
const {
insightsClient,
insightsClient: providedInsightsClient,
onItemsChange,
onSelect: onSelectEvent,
onActive: onActiveEvent,
} = getOptions(options);
let insightsClient = providedInsightsClient as InsightsClient;

if (!providedInsightsClient) {
safelyRunOnBrowser(({ window }) => {
const pointer = window.AlgoliaAnalyticsObject || 'aa';

if (typeof pointer === 'string') {
insightsClient = window[pointer];
}

if (!insightsClient) {
window.AlgoliaAnalyticsObject = pointer;

if (!window[pointer]) {
window[pointer] = (...args: any[]) => {
if (!window[pointer].queue) {
window[pointer].queue = [];
}

window[pointer].queue.push(args);
};
}

window[pointer].version = ALGOLIA_INSIGHTS_VERSION;

insightsClient = window[pointer];

loadInsights(window);
}
});
}

const insights = createSearchInsightsApi(insightsClient);
const previousItems = createRef<AlgoliaInsightsHit[]>([]);

Expand Down Expand Up @@ -190,3 +225,23 @@ function getOptions(options: CreateAlgoliaInsightsPluginParams) {
...options,
};
}

function loadInsights(environment: typeof window) {
const errorMessage = `[Autocomplete]: Could not load search-insights.js. Please load it manually following https://alg.li/insights-autocomplete`;

try {
const script = environment.document.createElement('script');
script.async = true;
script.src = ALGOLIA_INSIGHTS_SRC;

script.onerror = () => {
// eslint-disable-next-line no-console
console.error(errorMessage);
};

document.body.appendChild(script);
} catch (cause) {
// eslint-disable-next-line no-console
console.error(errorMessage);
}
}
Original file line number Diff line number Diff line change
@@ -1 +1,30 @@
export type InsightsClient = any;
import type {
InsightsMethodMap,
InsightsClient as _InsightsClient,
} from 'search-insights';

export type {
Init as InsightsInit,
AddAlgoliaAgent as InsightsAddAlgoliaAgent,
SetUserToken as InsightsSetUserToken,
GetUserToken as InsightsGetUserToken,
OnUserTokenChange as InsightsOnUserTokenChange,
} from 'search-insights';

export type InsightsClientMethod = keyof InsightsMethodMap;

export type InsightsClientPayload = {
eventName: string;
queryID: string;
index: string;
objectIDs: string[];
positions?: number[];
};

type QueueItemMap = Record<string, unknown>;

type QueueItem = QueueItemMap[keyof QueueItemMap];

export type InsightsClient = _InsightsClient & {
queue?: QueueItem[];
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { safelyRunOnBrowser } from '../safelyRunOnBrowser';

describe('safelyRunOnBrowser', () => {
const originalWindow = (global as any).window;

afterEach(() => {
(global as any).window = originalWindow;
});

test('runs callback on browsers', () => {
const callback = jest.fn(() => ({ env: 'client' }));

const result = safelyRunOnBrowser(callback);

expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith({ window });
expect(result).toEqual({ env: 'client' });
});

test('does not run callback on servers', () => {
// @ts-expect-error
delete global.window;

const callback = jest.fn(() => ({ env: 'client' }));

const result = safelyRunOnBrowser(callback);

expect(callback).toHaveBeenCalledTimes(0);
expect(result).toBeUndefined();
});
});
1 change: 1 addition & 0 deletions packages/autocomplete-shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from './invariant';
export * from './isEqual';
export * from './MaybePromise';
export * from './noop';
export * from './safelyRunOnBrowser';
export * from './UserAgent';
export * from './userAgents';
export * from './version';
Expand Down
14 changes: 14 additions & 0 deletions packages/autocomplete-shared/src/safelyRunOnBrowser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
type BrowserCallback<TReturn> = (params: { window: typeof window }) => TReturn;

/**
* Safely runs code meant for browser environments only.
*/
export function safelyRunOnBrowser<TReturn>(
callback: BrowserCallback<TReturn>
): TReturn | undefined {
if (typeof window !== 'undefined') {
return callback({ window });
}

return undefined;
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -17506,6 +17506,11 @@ search-insights@1.7.1:
resolved "https://registry.yarnpkg.com/search-insights/-/search-insights-1.7.1.tgz#eddfa56910e28cbbb0df80aec2ab8acf0a86cb6b"
integrity sha512-CSuSKIJp+WcSwYrD9GgIt1e3xmI85uyAefC4/KYGgtvNEm6rt4kBGilhVRmTJXxRE2W1JknvP598Q7SMhm7qKA==

search-insights@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/search-insights/-/search-insights-2.3.0.tgz#9a7bb25428fc7f003bafdb5638e90276113daae6"
integrity sha512-0v/TTO4fbd6I91sFBK/e2zNfD0f51A+fMoYNkMplmR77NpThUye/7gIxNoJ3LejKpZH6Z2KNBIpxxFmDKj10Yw==

search-insights@^2.1.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/search-insights/-/search-insights-2.2.1.tgz#9c93344fbae5fbf2f88c1a81b46b4b5d888c11f7"
Expand Down

0 comments on commit a02c2c1

Please sign in to comment.