Skip to content
4 changes: 2 additions & 2 deletions bundlesize.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
},
{
"path": "./packages/instantsearch.js/dist/instantsearch.production.min.js",
"maxSize": "122 kB"
"maxSize": "122.5 kB"
},
{
"path": "./packages/instantsearch.js/dist/instantsearch.development.js",
"maxSize": "253.4 kB"
"maxSize": "254.25 kB"
},
{
"path": "packages/react-instantsearch-core/dist/umd/ReactInstantSearchCore.min.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/instantsearch.js/src/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export { default as reverseHighlight } from './reverseHighlight';
export { default as reverseSnippet } from './reverseSnippet';
export { default as highlight } from './highlight';
export { default as snippet } from './snippet';
export { default as insights } from './insights';
export { default as insights, getTelemetrySessionId } from './insights';
export {
default as getInsightsAnonymousUserToken,
getInsightsAnonymousUserTokenInternal,
Expand Down
18 changes: 18 additions & 0 deletions packages/instantsearch.js/src/helpers/insights.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
import { warning, serializePayload, deserializePayload } from '../lib/utils';
import { createUUID } from '../lib/utils/uuid';

import type { InsightsClientMethod, InsightsClientPayload } from '../types';

const TELEMETRY_SESSION_KEY = 'ais.telemetry.sessionId';

export function getTelemetrySessionId(): string {
try {
const existing = sessionStorage.getItem(TELEMETRY_SESSION_KEY);
if (existing) {
return existing;
}
const id = createUUID();
sessionStorage.setItem(TELEMETRY_SESSION_KEY, id);
return id;
} catch {
// sessionStorage unavailable (SSR, privacy mode, etc.)
return createUUID();
}
}

/** @deprecated use bindEvent instead */
export function readDataAttributes(domElement: HTMLElement): {
method: InsightsClientMethod;
Expand Down
1 change: 1 addition & 0 deletions packages/instantsearch.js/src/lib/InstantSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ class InstantSearch<
instance: MiddlewareDefinition<TUiState>;
}> = [];
public sendEventToInsights: (event: InsightsEvent) => void;
public _createdAt: number = Date.now();
/**
* The status of the search. Can be "idle", "loading", "stalled", or "error".
*/
Expand Down
59 changes: 59 additions & 0 deletions packages/instantsearch.js/src/lib/utils/extractWidgetPayload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { createInitArgs } from './render-args';

import type { InstantSearch, Widget, IndexWidget } from '../../types';

export type WidgetMetadata =
| {
type: string | undefined;
widgetType: string | undefined;
params: string[];
}
| {
type: string;
middleware: true;
internal: boolean;
};

export function extractWidgetPayload(
widgets: Array<Widget | IndexWidget>,
instantSearchInstance: InstantSearch,
payload: { widgets: WidgetMetadata[] }
) {
const initOptions = createInitArgs(
instantSearchInstance,
instantSearchInstance.mainIndex,
instantSearchInstance._initialUiState
);

widgets.forEach((widget) => {
let widgetParams: Record<string, unknown> = {};

if (widget.getWidgetRenderState) {
const renderState = widget.getWidgetRenderState(initOptions);

if (renderState && renderState.widgetParams) {
// casting, as we just earlier checked widgetParams exists, and thus an object
widgetParams = renderState.widgetParams as Record<string, unknown>;
}
}

// since we destructure in all widgets, the parameters with defaults are set to "undefined"
const params = Object.keys(widgetParams).filter(
(key) => widgetParams[key] !== undefined
);

payload.widgets.push({
type: widget.$$type,
widgetType: widget.$$widgetType,
params,
});

if (widget.$$type === 'ais.index') {
extractWidgetPayload(
(widget as IndexWidget).getWidgets(),
instantSearchInstance,
payload
);
}
});
}
1 change: 1 addition & 0 deletions packages/instantsearch.js/src/lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export * from './walkIndex';
export * from './logger';
export * from './mergeSearchParameters';
export * from './omit';
export * from './extractWidgetPayload';
export * from './noop';
export * from './range';
export * from './render-args';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1315,7 +1315,7 @@ describe('insights', () => {
} as any,
});
expect(analytics.viewedObjectIDs).toHaveBeenCalledTimes(0);
expect(onEvent).toHaveBeenCalledTimes(1);
// onEvent also receives telemetry events (bootstrap)
expect(onEvent).toHaveBeenCalledWith(
{
insightsMethod: 'viewedObjectIDs',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { getInsightsAnonymousUserTokenInternal } from '../helpers';
import {
getInsightsAnonymousUserTokenInternal,
getTelemetrySessionId,
} from '../helpers';
import {
warning,
noop,
extractWidgetPayload,
getAppIdAndApiKey,
find,
safelyRunOnBrowser,
} from '../lib/utils';

import { createUUID } from '../lib/utils/uuid';
import type { WidgetMetadata } from '../lib/utils/extractWidgetPayload';

import type {
InsightsClient,
Expand Down Expand Up @@ -162,6 +168,7 @@ export function createInsightsMiddleware<

let initialParameters: PlainSearchParameters;
let helper: AlgoliaSearchHelper;
let telemetryOnRender: (() => void) | null = null;

return {
$$type: 'ais.insights',
Expand Down Expand Up @@ -416,10 +423,56 @@ See documentation: https://www.algolia.com/doc/guides/building-search-ui/going-f
);
}
};

// telemetry
const telemetrySessionId = getTelemetrySessionId();

function sendTelemetryEvent(event: any) {
const userToken = helper.state?.userToken;

instantSearchInstance.sendEventToInsights({
insightsMethod: 'sendEvents',
payload: [
{
eventType: 'instantsearch_telemetry',
timestamp: Date.now(),
session_id: telemetrySessionId,
userToken: userToken ? String(userToken) : undefined,
...event,
},
],
} as any);
}

telemetryOnRender = () => {
sendTelemetryEvent({
eventName: '__render__',
performance: {},
widgets: collectWidgets(instantSearchInstance),
});
};

instantSearchInstance.on('render', telemetryOnRender);

sendTelemetryEvent({
eventName: '__start__',
performance: {
timeSincePageLoad:
typeof performance !== 'undefined'
? Math.round(performance.now())
: undefined,
timeSinceInit:
Date.now() - instantSearchInstance._createdAt,
},
});
},
unsubscribe() {
insightsClient('onUserTokenChange', undefined);
instantSearchInstance.sendEventToInsights = noop;
if (telemetryOnRender) {
instantSearchInstance.removeListener('render', telemetryOnRender);
telemetryOnRender = null;
}
if (helper && initialParameters) {
helper.overrideStateWithoutTriggeringChangeEvent({
...helper.state,
Expand Down Expand Up @@ -485,3 +538,24 @@ function normalizeUserToken(userToken?: string | number): string | undefined {

return typeof userToken === 'number' ? userToken.toString() : userToken;
}

function collectWidgets(
instantSearchInstance: InstantSearch
): WidgetMetadata[] {
const widgetPayload: { widgets: WidgetMetadata[] } = { widgets: [] };
extractWidgetPayload(
instantSearchInstance.mainIndex.getWidgets(),
instantSearchInstance,
widgetPayload
);

instantSearchInstance.middleware.forEach((mw) =>
widgetPayload.widgets.push({
middleware: true,
type: mw.instance.$$type,
internal: mw.instance.$$internal,
})
);

return widgetPayload.widgets;
}
Original file line number Diff line number Diff line change
@@ -1,77 +1,17 @@
import {
createInitArgs,
extractWidgetPayload,
getAlgoliaAgent,
safelyRunOnBrowser,
} from '../lib/utils';

import type {
InstantSearch,
InternalMiddleware,
Widget,
IndexWidget,
} from '../types';

type WidgetMetadata =
| {
type: string | undefined;
widgetType: string | undefined;
params: string[];
}
| {
type: string;
middleware: true;
internal: boolean;
};
import type { WidgetMetadata } from '../lib/utils/extractWidgetPayload';
import type { InternalMiddleware } from '../types';

type Payload = {
widgets: WidgetMetadata[];
ua?: string;
};

function extractWidgetPayload(
widgets: Array<Widget | IndexWidget>,
instantSearchInstance: InstantSearch,
payload: Payload
) {
const initOptions = createInitArgs(
instantSearchInstance,
instantSearchInstance.mainIndex,
instantSearchInstance._initialUiState
);

widgets.forEach((widget) => {
let widgetParams: Record<string, unknown> = {};

if (widget.getWidgetRenderState) {
const renderState = widget.getWidgetRenderState(initOptions);

if (renderState && renderState.widgetParams) {
// casting, as we just earlier checked widgetParams exists, and thus an object
widgetParams = renderState.widgetParams as Record<string, unknown>;
}
}

// since we destructure in all widgets, the parameters with defaults are set to "undefined"
const params = Object.keys(widgetParams).filter(
(key) => widgetParams[key] !== undefined
);

payload.widgets.push({
type: widget.$$type,
widgetType: widget.$$widgetType,
params,
});

if (widget.$$type === 'ais.index') {
extractWidgetPayload(
(widget as IndexWidget).getWidgets(),
instantSearchInstance,
payload
);
}
});
}

export function isMetadataEnabled() {
return safelyRunOnBrowser(
({ window }) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -915,7 +915,11 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hits/js/"

const insights = createInsightsMiddleware({
insightsClient: null,
onEvent,
onEvent: (event, aa) => {
if (event.insightsMethod !== 'sendEvents') {
onEvent(event, aa);
}
},
});

return { onEvent, insights };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -918,7 +918,11 @@ describe('infiniteHits', () => {

const insights = createInsightsMiddleware({
insightsClient: null,
onEvent,
onEvent: (event, aa) => {
if (event.insightsMethod !== 'sendEvents') {
onEvent(event, aa);
}
},
});

return { onEvent, insights };
Expand Down
1 change: 1 addition & 0 deletions packages/instantsearch.js/test/createInstantSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export const createInstantSearch = (
emit: jest.fn(),
listenerCount: jest.fn(),
sendEventToInsights: jest.fn(),
_createdAt: Date.now(),
future: {
...INSTANTSEARCH_FUTURE_DEFAULTS,
...(args.future || {}),
Expand Down
9 changes: 6 additions & 3 deletions tests/common/shared/insights.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ export function createInsightsTests(
});

// initial calls because the middleware is attached
expect(window.aa).toHaveBeenCalledTimes(5);
// 5 base calls + 2 telemetry events (bootstrap + render)
expect(window.aa).toHaveBeenCalledTimes(7);
expect(window.aa).toHaveBeenCalledWith(
'addAlgoliaAgent',
'insights-middleware'
Expand Down Expand Up @@ -157,7 +158,8 @@ export function createInsightsTests(
await setup(options);

// initial calls because the middleware is attached
expect(window.aa).toHaveBeenCalledTimes(4);
// 4 base calls + telemetry events (bootstrap + possible render)
expect(window.aa.mock.calls.length).toBeGreaterThanOrEqual(5);
expect(window.aa).toHaveBeenCalledWith(
'addAlgoliaAgent',
'insights-middleware'
Expand All @@ -170,7 +172,8 @@ export function createInsightsTests(
});

// Once result is available
expect(window.aa).toHaveBeenCalledTimes(5);
// 5 base calls + 3 telemetry events (bootstrap + renders)
expect(window.aa).toHaveBeenCalledTimes(8);
expect(window.aa).toHaveBeenCalledWith(
'viewedObjectIDs',
{
Expand Down
Loading
Loading