Skip to content

Commit

Permalink
feat(insights): automatically load search-insights if not passed (#5484)
Browse files Browse the repository at this point in the history
* feat(insights): automatically load search-insights if not passed

FX-2244

* different wording

* fix(insights): notify of error when insights fails to load

* prettier snippet
  • Loading branch information
Haroenv committed Apr 24, 2023
1 parent 2bde630 commit a85797b
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
createSearchClient,
} from '@instantsearch/mocks';
import { wait } from '@instantsearch/testutils/wait';
import { fireEvent } from '@testing-library/dom';

import { createInsightsMiddleware } from '..';
import instantsearch from '../../index.es';
Expand Down Expand Up @@ -99,16 +100,16 @@ describe('insights', () => {

beforeEach(() => {
warning.cache = {};

(window as any).AlgoliaAnalyticsObject = undefined;
(window as any).aa = undefined;

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

describe('usage', () => {
it('throws when insightsClient is not given', () => {
expect(() =>
// @ts-expect-error
createInsightsMiddleware()
).toThrowErrorMatchingInlineSnapshot(
`"The \`insightsClient\` option is required if you want userToken to be automatically set in search calls. If you don't want this behaviour, set it to \`null\`."`
);
it('passes when insightsClient is not given', () => {
expect(() => createInsightsMiddleware()).not.toThrow();
});

it('passes with insightsClient: null', () => {
Expand All @@ -120,6 +121,94 @@ describe('insights', () => {
});
});

describe('insightsClient', () => {
it('does nothing when insightsClient is passed', () => {
const { instantSearchInstance } = createTestEnvironment();

instantSearchInstance.use(
createInsightsMiddleware({ insightsClient: () => {} })
);

expect(document.body).toMatchInlineSnapshot(`<body />`);
expect((window as any).AlgoliaAnalyticsObject).toBe(undefined);
expect((window as any).aa).toBe(undefined);
});

it('does nothing when insightsClient is null', () => {
const { instantSearchInstance } = createTestEnvironment();

instantSearchInstance.use(
createInsightsMiddleware({ insightsClient: null })
);

expect(document.body).toMatchInlineSnapshot(`<body />`);
expect((window as any).AlgoliaAnalyticsObject).toBe(undefined);
expect((window as any).aa).toBe(undefined);
});

it('does nothing when insightsClient is already present', () => {
(window as any).AlgoliaAnalyticsObject = 'aa';
const aa = () => {};
(window as any).aa = aa;

const { instantSearchInstance } = createTestEnvironment();

instantSearchInstance.use(createInsightsMiddleware());

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

it('loads the script when insightsClient is not passed', () => {
const { instantSearchInstance } = createTestEnvironment();

instantSearchInstance.use(createInsightsMiddleware());

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

it('notifies when the script fails to be added', () => {
const { instantSearchInstance } = createTestEnvironment();
const createElement = document.createElement;
document.createElement = () => {
throw new Error('error');
};

instantSearchInstance.on('error', (error) =>
expect(error).toMatchInlineSnapshot(
`[Error: [insights middleware]: could not load search-insights.js. Please load it manually following https://alg.li/insights-init]`
)
);

instantSearchInstance.use(createInsightsMiddleware());

document.createElement = createElement;
});

it('notifies when the script fails to load', () => {
const { instantSearchInstance } = createTestEnvironment();

instantSearchInstance.on('error', (error) =>
expect(error).toMatchInlineSnapshot(
`[Error: [insights middleware]: could not load search-insights.js. Please load it manually following https://alg.li/insights-init]`
)
);

instantSearchInstance.use(createInsightsMiddleware());

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

describe('initialize', () => {
it('passes initParams to insightsClient', () => {
const { insightsClient, instantSearchInstance } = createTestEnvironment();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { getInsightsAnonymousUserTokenInternal } from '../helpers';
import { warning, noop, getAppIdAndApiKey, find } from '../lib/utils';
import {
warning,
noop,
getAppIdAndApiKey,
find,
safelyRunOnBrowser,
} from '../lib/utils';

import type {
InsightsClient,
Expand All @@ -22,9 +28,12 @@ export type InsightsEvent = {
};

export type InsightsProps<
TInsightsClient extends null | InsightsClient = InsightsClient | null
TInsightsClient extends InsightsClient | null | undefined =
| InsightsClient
| null
| undefined
> = {
insightsClient: TInsightsClient;
insightsClient?: TInsightsClient;
insightsInitParams?: {
userHasOptedOut?: boolean;
useCookie?: boolean;
Expand All @@ -34,31 +43,47 @@ export type InsightsProps<
onEvent?: (event: InsightsEvent, insightsClient: TInsightsClient) => void;
};

const ALGOLIA_INSIGHTS_SRC =
'https://cdn.jsdelivr.net/npm/search-insights@2.3.0/dist/search-insights.min.js';

export type CreateInsightsMiddleware = typeof createInsightsMiddleware;

export function createInsightsMiddleware<
TInsightsClient extends null | InsightsClient
>(props: InsightsProps<TInsightsClient>): InternalMiddleware {
>(props: InsightsProps<TInsightsClient> = {}): InternalMiddleware {
const {
insightsClient: _insightsClient,
insightsInitParams,
onEvent,
} = props || {};
} = props;

let insightsClient: InsightsClient = _insightsClient || noop;

let needsToLoadInsightsClient = false;
if (_insightsClient !== null && !_insightsClient) {
if (__DEV__) {
throw new Error(
"The `insightsClient` option is required if you want userToken to be automatically set in search calls. If you don't want this behaviour, set it to `null`."
);
} else {
throw new Error(
'The `insightsClient` option is required. To disable, set it to `null`.'
);
}
}
safelyRunOnBrowser(({ window }: { window: any }) => {
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);
};
}

const hasInsightsClient = Boolean(_insightsClient);
const insightsClient: InsightsClient =
_insightsClient === null ? noop : _insightsClient;
insightsClient = window[pointer];
needsToLoadInsightsClient = true;
}
});
}

return ({ instantSearchInstance }) => {
const [appId, apiKey] = getAppIdAndApiKey(instantSearchInstance.client);
Expand Down Expand Up @@ -105,7 +130,24 @@ export function createInsightsMiddleware<

return {
onStateChange() {},
subscribe() {},
subscribe() {
if (!needsToLoadInsightsClient) return;

const errorMessage =
'[insights middleware]: could not load search-insights.js. Please load it manually following https://alg.li/insights-init';

try {
const script = document.createElement('script');
script.async = true;
script.src = ALGOLIA_INSIGHTS_SRC;
script.onerror = () => {
instantSearchInstance.emit('error', new Error(errorMessage));
};
document.body.appendChild(script);
} catch (cause) {
instantSearchInstance.emit('error', new Error(errorMessage));
}
},
started() {
insightsClient('addAlgoliaAgent', 'insights-middleware');

Expand All @@ -132,7 +174,7 @@ export function createInsightsMiddleware<
};

const anonymousUserToken = getInsightsAnonymousUserTokenInternal();
if (hasInsightsClient && anonymousUserToken) {
if (anonymousUserToken) {
// When `aa('init', { ... })` is called, it creates an anonymous user token in cookie.
// We can set it as userToken.
setUserTokenToSearch(anonymousUserToken);
Expand All @@ -153,7 +195,7 @@ export function createInsightsMiddleware<

instantSearchInstance.sendEventToInsights = (event: InsightsEvent) => {
if (onEvent) {
onEvent(event, _insightsClient);
onEvent(event, _insightsClient as TInsightsClient);
} else if (event.insightsMethod) {
const hasUserToken = Boolean(
(helper.state as PlainSearchParameters).userToken
Expand Down

0 comments on commit a85797b

Please sign in to comment.