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

feat(createAlgoliaInsightsPlugin): automatically load Insights when not passed #1106

Merged
merged 6 commits into from
Mar 13, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion bundlesize.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
},
{
"path": "packages/autocomplete-plugin-algolia-insights/dist/umd/index.production.js",
"maxSize": "2.1 kB"
"maxSize": "2.5 kB"
},
{
"path": "packages/autocomplete-plugin-redirect-url/dist/umd/index.production.js",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
"rollup-plugin-filesize": "9.1.2",
"rollup-plugin-license": "2.9.1",
"rollup-plugin-terser": "7.0.2",
"search-insights": "2.3.0",
"shipjs": "0.24.1",
"start-server-and-test": "1.15.2",
"stylelint": "13.13.1",
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);
};
}
dhayab marked this conversation as resolved.
Show resolved Hide resolved

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);
}
Comment on lines +237 to +246
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a huge fan of using console.errors but since there's no global error handler in Autocomplete, throwing would be hardly catchable for users.

I'd rather use those now (Sentry lets you catch calls to the Console API) and work on an onError handler in a separate PR to centralize error handling.

}
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';
Haroenv marked this conversation as resolved.
Show resolved Hide resolved

export type InsightsClientMethod = keyof InsightsMethodMap;

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

type QueueItemMap = Record<string, unknown>;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be more strictly typed, but it requires updating TypeScript, which then triggers other type issues.

Let's handle this in another PR.


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>(
Haroenv marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -19226,6 +19226,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==

dhayab marked this conversation as resolved.
Show resolved Hide resolved
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