diff --git a/.github/actions/cache/action.yml b/.github/actions/cache/action.yml index 86f733c4c8..8672628a1e 100644 --- a/.github/actions/cache/action.yml +++ b/.github/actions/cache/action.yml @@ -39,6 +39,13 @@ runs: path: /home/runner/work/api-clients-automation/api-clients-automation/clients/algoliasearch-client-javascript/client-analytics/dist key: ${{ runner.os }}-js-client-analytics-${{ hashFiles('clients/algoliasearch-client-javascript/client-analytics/**') }} + - name: Restore built JavaScript insights client + if: ${{ inputs.job == 'cts' }} + uses: actions/cache@v2 + with: + path: /home/runner/work/api-clients-automation/api-clients-automation/clients/algoliasearch-client-javascript/client-insights/dist + key: ${{ runner.os }}-js-client-insights-${{ hashFiles('clients/algoliasearch-client-javascript/client-insights/**') }} + - name: Restore built Java client if: ${{ inputs.job == 'cts' }} uses: actions/cache@v2 diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index fa2c370f4e..cc7afff473 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -46,6 +46,7 @@ runs: echo "::set-output name=JS_RECOMMEND_CLIENT_CHANGED::$(git diff --shortstat origin/${{ github.base_ref }}..HEAD -- clients/algoliasearch-client-javascript/recommend | wc -l)" echo "::set-output name=JS_PERSO_CLIENT_CHANGED::$(git diff --shortstat origin/${{ github.base_ref }}..HEAD -- clients/algoliasearch-client-javascript/client-personalization | wc -l)" echo "::set-output name=JS_ANALYTICS_CLIENT_CHANGED::$(git diff --shortstat origin/${{ github.base_ref }}..HEAD -- clients/algoliasearch-client-javascript/client-analytics | wc -l)" + echo "::set-output name=JS_INSIGHTS_CLIENT_CHANGED::$(git diff --shortstat origin/${{ github.base_ref }}..HEAD -- clients/algoliasearch-client-javascript/client-insights | wc -l)" echo "::set-output name=JS_TEMPLATE_CHANGED::$(git diff --shortstat origin/${{ github.base_ref }}..HEAD -- templates/javascript | wc -l)" echo "::set-output name=JAVA_CLIENT_CHANGED::$(git diff --shortstat origin/${{ github.base_ref }}..HEAD -- clients/algoliasearch-client-java-2 | wc -l)" @@ -88,6 +89,9 @@ outputs: RUN_JS_CLIENT_ANALYTICS: description: Determine if the `client_javascript_analytics` job should run value: ${{ github.ref == 'refs/heads/main' || steps.diff.outputs.GITHUB_ACTIONS_CHANGED > 0 || steps.diff.outputs.COMMON_SPECS_CHANGED > 0 || steps.diff.outputs.ANALYTICS_SPECS_CHANGED > 0 || steps.diff.outputs.SCRIPTS_CHANGED > 0 || steps.diff.outputs.JS_ANALYTICS_CLIENT_CHANGED > 0 || steps.diff.outputs.JS_TEMPLATE_CHANGED > 0 }} + RUN_JS_CLIENT_INSIGHTS: + description: Determine if the `client_javascript_insights` job should run + value: ${{ github.ref == 'refs/heads/main' || steps.diff.outputs.GITHUB_ACTIONS_CHANGED > 0 || steps.diff.outputs.COMMON_SPECS_CHANGED > 0 || steps.diff.outputs.INSIGHTS_SPECS_CHANGED > 0 || steps.diff.outputs.SCRIPTS_CHANGED > 0 || steps.diff.outputs.JS_INSIGHTS_CLIENT_CHANGED > 0 || steps.diff.outputs.JS_TEMPLATE_CHANGED > 0 }} # java client variables RUN_JAVA_CLIENT: diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index c7ff569a5f..bda25d1b96 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -38,6 +38,7 @@ jobs: RUN_JS_CLIENT_RECOMMEND: ${{ steps.setup.outputs.RUN_JS_CLIENT_RECOMMEND }} RUN_JS_CLIENT_PERSO: ${{ steps.setup.outputs.RUN_JS_CLIENT_PERSO }} RUN_JS_CLIENT_ANALYTICS: ${{ steps.setup.outputs.RUN_JS_CLIENT_ANALYTICS }} + RUN_JS_CLIENT_INSIGHTS: ${{ steps.setup.outputs.RUN_JS_CLIENT_INSIGHTS }} RUN_JAVA_CLIENT: ${{ steps.setup.outputs.RUN_JAVA_CLIENT }} @@ -107,6 +108,22 @@ jobs: - name: Lint analytics specs run: yarn eslint --ext=yml specs/analytics + specs_insights: + runs-on: ubuntu-20.04 + needs: setup + if: ${{ always() && needs.setup.outputs.RUN_SPECS_INSIGHTS == 'true' }} + steps: + - uses: actions/checkout@v2 + + - name: Restore cache + uses: ./.github/actions/cache + + - name: Checking insights specs + run: yarn build:specs insights + + - name: Lint insights specs + run: yarn eslint --ext=yml specs/insights + client_javascript_search: runs-on: ubuntu-20.04 needs: [specs_search] @@ -207,6 +224,31 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' run: yarn build:clients javascript analytics + client_javascript_insights: + runs-on: ubuntu-20.04 + needs: [specs_insights] + if: ${{ always() && needs.setup.outputs.RUN_JS_CLIENT_INSIGHTS == 'true' }} + steps: + - uses: actions/checkout@v2 + + - name: Restore cache + uses: ./.github/actions/cache + + - name: Cache insights client + id: cache + uses: actions/cache@v2 + with: + path: /home/runner/work/api-clients-automation/api-clients-automation/clients/algoliasearch-client-javascript/client-insights/dist + key: ${{ runner.os }}-js-client-insights-${{ hashFiles('clients/algoliasearch-client-javascript/client-insights/**') }} + + - name: Generate insights client + if: steps.cache.outputs.cache-hit != 'true' + run: yarn generate javascript insights + + - name: Build insights client + if: steps.cache.outputs.cache-hit != 'true' + run: yarn build:clients javascript insights + client_java_search: runs-on: ubuntu-20.04 needs: [specs_search] @@ -241,6 +283,7 @@ jobs: - client_javascript_recommend - client_javascript_perso - client_javascript_analytics + - client_javascript_insights - client_java_search if: ${{ always() && needs.setup.outputs.RUN_CTS == 'true' }} diff --git a/clients/algoliasearch-client-javascript/client-insights/.gitignore b/clients/algoliasearch-client-javascript/client-insights/.gitignore new file mode 100644 index 0000000000..8aafcdc3fd --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-insights/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +.openapi-generator +.env diff --git a/clients/algoliasearch-client-javascript/client-insights/.openapi-generator-ignore b/clients/algoliasearch-client-javascript/client-insights/.openapi-generator-ignore new file mode 100644 index 0000000000..abfb5c9516 --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-insights/.openapi-generator-ignore @@ -0,0 +1,7 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +git_push.sh diff --git a/clients/algoliasearch-client-javascript/client-insights/api.ts b/clients/algoliasearch-client-javascript/client-insights/api.ts new file mode 100644 index 0000000000..59b02c4607 --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-insights/api.ts @@ -0,0 +1,3 @@ +// This is the entrypoint for the package +export * from './src/apis'; +export * from './model/models'; diff --git a/clients/algoliasearch-client-javascript/client-insights/model/errorBase.ts b/clients/algoliasearch-client-javascript/client-insights/model/errorBase.ts new file mode 100644 index 0000000000..a533aa7a15 --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-insights/model/errorBase.ts @@ -0,0 +1,6 @@ +/** + * Error. + */ +export type ErrorBase = { + message?: string; +}; diff --git a/clients/algoliasearch-client-javascript/client-insights/model/insightEvent.ts b/clients/algoliasearch-client-javascript/client-insights/model/insightEvent.ts new file mode 100644 index 0000000000..fde9c5580c --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-insights/model/insightEvent.ts @@ -0,0 +1,49 @@ +/** + * Insights event. + */ +export type InsightEvent = { + /** + * An eventType can be a click, a conversion, or a view. + */ + eventType: InsightEvent.EventTypeEnum; + /** + * A user-defined string used to categorize events. + */ + eventName: string; + /** + * Name of the targeted index. + */ + index: string; + /** + * A user identifier. Depending if the user is logged-in or not, several strategies can be used from a sessionId to a technical identifier. + */ + userToken: string; + /** + * Time of the event expressed in milliseconds since the Unix epoch. + */ + timestamp?: number; + /** + * Algolia queryID. This is required when an event is tied to a search. + */ + queryID?: string; + /** + * An array of index objectID. Limited to 20 objects. An event can’t have both objectIDs and filters at the same time. + */ + objectIDs?: string[]; + /** + * An array of filters. Limited to 10 filters. An event can’t have both objectIDs and filters at the same time. + */ + filters?: string[]; + /** + * Position of the click in the list of Algolia search results. This field is required if a queryID is provided. One position must be provided for each objectID. + */ + positions?: number[]; +}; + +export namespace InsightEvent { + export enum EventTypeEnum { + Click = 'click', + Conversion = 'conversion', + View = 'view', + } +} diff --git a/clients/algoliasearch-client-javascript/client-insights/model/insightEvents.ts b/clients/algoliasearch-client-javascript/client-insights/model/insightEvents.ts new file mode 100644 index 0000000000..69728810b2 --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-insights/model/insightEvents.ts @@ -0,0 +1,11 @@ +import type { InsightEvent } from './insightEvent'; + +/** + * Object containing the events sent. + */ +export type InsightEvents = { + /** + * Array of events sent. + */ + events: InsightEvent[]; +}; diff --git a/clients/algoliasearch-client-javascript/client-insights/model/models.ts b/clients/algoliasearch-client-javascript/client-insights/model/models.ts new file mode 100644 index 0000000000..fa4726b58b --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-insights/model/models.ts @@ -0,0 +1,46 @@ +/* eslint-disable no-param-reassign */ +import type { RequestOptions } from '../utils/types'; + +export * from './errorBase'; +export * from './insightEvent'; +export * from './insightEvents'; +export * from './pushEventsResponse'; + +export interface Authentication { + /** + * Apply authentication settings to header and query params. + */ + applyToRequest: (requestOptions: RequestOptions) => Promise | void; +} + +export class ApiKeyAuth implements Authentication { + apiKey: string = ''; + + constructor(private location: string, private paramName: string) {} + + applyToRequest(requestOptions: RequestOptions): void { + if (this.location === 'query') { + requestOptions.queryParameters[this.paramName] = this.apiKey; + } else if ( + this.location === 'header' && + requestOptions && + requestOptions.headers + ) { + requestOptions.headers[this.paramName] = this.apiKey; + } else if ( + this.location === 'cookie' && + requestOptions && + requestOptions.headers + ) { + if (requestOptions.headers.Cookie) { + requestOptions.headers.Cookie += `; ${ + this.paramName + }=${encodeURIComponent(this.apiKey)}`; + } else { + requestOptions.headers.Cookie = `${this.paramName}=${encodeURIComponent( + this.apiKey + )}`; + } + } + } +} diff --git a/clients/algoliasearch-client-javascript/client-insights/model/pushEventsResponse.ts b/clients/algoliasearch-client-javascript/client-insights/model/pushEventsResponse.ts new file mode 100644 index 0000000000..ed9b5dd044 --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-insights/model/pushEventsResponse.ts @@ -0,0 +1,6 @@ +export type PushEventsResponse = { + /** + * A message confirming the event push. + */ + message: string; +}; diff --git a/clients/algoliasearch-client-javascript/client-insights/package.json b/clients/algoliasearch-client-javascript/client-insights/package.json new file mode 100644 index 0000000000..987293db90 --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-insights/package.json @@ -0,0 +1,24 @@ +{ + "name": "@algolia/client-insights", + "version": "5.0.0", + "description": "JavaScript client for @algolia/client-insights", + "repository": "algolia/algoliasearch-client-javascript", + "author": "Algolia", + "private": true, + "license": "MIT", + "main": "dist/api.js", + "types": "dist/api.d.ts", + "scripts": { + "clean": "rm -Rf node_modules/ *.js", + "build": "tsc", + "test": "yarn build && node dist/client.js" + }, + "engines": { + "node": "^16.0.0", + "yarn": "^3.0.0" + }, + "devDependencies": { + "@types/node": "16.11.11", + "typescript": "4.5.4" + } +} diff --git a/clients/algoliasearch-client-javascript/client-insights/src/apis.ts b/clients/algoliasearch-client-javascript/client-insights/src/apis.ts new file mode 100644 index 0000000000..9dea0a8916 --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-insights/src/apis.ts @@ -0,0 +1,7 @@ +import { InsightsApi } from './insightsApi'; + +export * from './insightsApi'; +export * from '../utils/errors'; +export { EchoRequester } from '../utils/requester/EchoRequester'; + +export const APIS = [InsightsApi]; diff --git a/clients/algoliasearch-client-javascript/client-insights/src/insightsApi.ts b/clients/algoliasearch-client-javascript/client-insights/src/insightsApi.ts new file mode 100644 index 0000000000..9f0ad84cdd --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-insights/src/insightsApi.ts @@ -0,0 +1,118 @@ +import type { InsightEvents } from '../model/insightEvents'; +import { ApiKeyAuth } from '../model/models'; +import type { PushEventsResponse } from '../model/pushEventsResponse'; +import { Transporter } from '../utils/Transporter'; +import type { Requester } from '../utils/requester/Requester'; +import type { Headers, Host, Request, RequestOptions } from '../utils/types'; + +export enum InsightsApiKeys { + apiKey, + appId, +} + +export class InsightsApi { + protected authentications = { + apiKey: new ApiKeyAuth('header', 'X-Algolia-API-Key'), + appId: new ApiKeyAuth('header', 'X-Algolia-Application-Id'), + }; + + private transporter: Transporter; + + private sendRequest( + request: Request, + requestOptions: RequestOptions + ): Promise { + if (this.authentications.apiKey.apiKey) { + this.authentications.apiKey.applyToRequest(requestOptions); + } + + if (this.authentications.appId.apiKey) { + this.authentications.appId.applyToRequest(requestOptions); + } + + return this.transporter.request(request, requestOptions); + } + + constructor( + appId: string, + apiKey: string, + options?: { requester?: Requester; hosts?: Host[] } + ) { + this.setApiKey(InsightsApiKeys.appId, appId); + this.setApiKey(InsightsApiKeys.apiKey, apiKey); + + this.transporter = new Transporter({ + hosts: options?.hosts ?? this.getDefaultHosts(), + baseHeaders: { + 'content-type': 'application/x-www-form-urlencoded', + }, + userAgent: 'Algolia for Javascript', + timeouts: { + connect: 2, + read: 5, + write: 30, + }, + requester: options?.requester, + }); + } + + getDefaultHosts(): Host[] { + return [ + { url: `insights.algolia.io`, accept: 'readWrite', protocol: 'https' }, + ]; + } + + setRequest(requester: Requester): void { + this.transporter.setRequester(requester); + } + + setHosts(hosts: Host[]): void { + this.transporter.setHosts(hosts); + } + + setApiKey(key: InsightsApiKeys, value: string): void { + this.authentications[InsightsApiKeys[key]].apiKey = value; + } + + /** + * This command pushes an array of events. + * + * @summary Pushes an array of events. + * @param pushEvents - The pushEvents parameters. + * @param pushEvents.insightEvents - The insightEvents. + */ + pushEvents({ insightEvents }: PushEventsProps): Promise { + const path = '/1/events'; + const headers: Headers = { Accept: 'application/json' }; + const queryParameters: Record = {}; + + if (insightEvents === null || insightEvents === undefined) { + throw new Error( + 'Required parameter insightEvents was null or undefined when calling pushEvents.' + ); + } + + if (insightEvents.events === null || insightEvents.events === undefined) { + throw new Error( + 'Required parameter insightEvents.events was null or undefined when calling pushEvents.' + ); + } + + const request: Request = { + method: 'POST', + path, + data: insightEvents, + }; + + const requestOptions: RequestOptions = { + headers, + queryParameters, + }; + + return this.sendRequest(request, requestOptions); + } +} + +export type PushEventsProps = { + insightEvents: InsightEvents; +}; diff --git a/clients/algoliasearch-client-javascript/client-insights/tsconfig.json b/clients/algoliasearch-client-javascript/client-insights/tsconfig.json new file mode 100644 index 0000000000..2f72c93ccb --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-insights/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "module": "commonjs", + "noImplicitAny": false, + "suppressImplicitAnyIndexErrors": true, + "target": "ES6", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "strict": true, + "moduleResolution": "node", + "removeComments": true, + "sourceMap": true, + "noLib": false, + "declaration": true, + "lib": ["dom", "es6", "es5", "dom.iterable", "scripthost"], + "outDir": "dist", + "typeRoots": ["node_modules/@types"], + "types": ["node"] + }, + "include": ["src", "model", "api.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/clients/algoliasearch-client-javascript/client-insights/utils/Response.ts b/clients/algoliasearch-client-javascript/client-insights/utils/Response.ts new file mode 100644 index 0000000000..bd22de7df9 --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-insights/utils/Response.ts @@ -0,0 +1,23 @@ +import type { Response } from './types'; + +export function isNetworkError({ + isTimedOut, + status, +}: Omit): boolean { + return !isTimedOut && ~~status === 0; +} + +export function isRetryable({ + isTimedOut, + status, +}: Omit): boolean { + return ( + isTimedOut || + isNetworkError({ isTimedOut, status }) || + (~~(status / 100) !== 2 && ~~(status / 100) !== 4) + ); +} + +export function isSuccess({ status }: Pick): boolean { + return ~~(status / 100) === 2; +} diff --git a/clients/algoliasearch-client-javascript/client-insights/utils/StatefulHost.ts b/clients/algoliasearch-client-javascript/client-insights/utils/StatefulHost.ts new file mode 100644 index 0000000000..162c4ed1c6 --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-insights/utils/StatefulHost.ts @@ -0,0 +1,34 @@ +import type { Host } from './types'; + +const EXPIRATION_DELAY = 2 * 60 * 1000; + +export class StatefulHost implements Host { + url: string; + accept: 'read' | 'readWrite' | 'write'; + protocol: 'http' | 'https'; + + private lastUpdate: number; + private status: 'down' | 'timedout' | 'up'; + + constructor(host: Host, status: StatefulHost['status'] = 'up') { + this.url = host.url; + this.accept = host.accept; + this.protocol = host.protocol; + + this.status = status; + this.lastUpdate = Date.now(); + } + + isUp(): boolean { + return ( + this.status === 'up' || Date.now() - this.lastUpdate > EXPIRATION_DELAY + ); + } + + isTimedout(): boolean { + return ( + this.status === 'timedout' && + Date.now() - this.lastUpdate <= EXPIRATION_DELAY + ); + } +} diff --git a/clients/algoliasearch-client-javascript/client-insights/utils/Transporter.ts b/clients/algoliasearch-client-javascript/client-insights/utils/Transporter.ts new file mode 100644 index 0000000000..48b4edebfd --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-insights/utils/Transporter.ts @@ -0,0 +1,243 @@ +import { isRetryable, isSuccess } from './Response'; +import { StatefulHost } from './StatefulHost'; +import type { Cache } from './cache/Cache'; +import { MemoryCache } from './cache/MemoryCache'; +import { RetryError } from './errors'; +import { + deserializeFailure, + deserializeSuccess, + serializeData, + serializeHeaders, + serializeUrl, +} from './helpers'; +import { HttpRequester } from './requester/HttpRequester'; +import type { Requester } from './requester/Requester'; +import { + stackTraceWithoutCredentials, + stackFrameWithoutCredentials, +} from './stackTrace'; +import type { + Headers, + Host, + Request, + RequestOptions, + StackFrame, + Timeouts, + Response, + EndRequest, +} from './types'; + +export class Transporter { + private hosts: Host[]; + private baseHeaders: Headers; + private hostsCache: Cache; + private userAgent: string; + private timeouts: Timeouts; + private requester: Requester; + + constructor({ + hosts, + baseHeaders, + userAgent, + timeouts, + requester = new HttpRequester(), + }: { + hosts: Host[]; + baseHeaders: Headers; + userAgent: string; + timeouts: Timeouts; + requester?: Requester; + }) { + this.hosts = hosts; + this.hostsCache = new MemoryCache(); + this.baseHeaders = baseHeaders; + this.userAgent = userAgent; + this.timeouts = timeouts; + this.requester = requester; + } + + setHosts(hosts: Host[]): void { + this.hosts = hosts; + this.hostsCache.clear(); + } + + setRequester(requester: Requester): void { + this.requester = requester; + } + + async createRetryableOptions(compatibleHosts: Host[]): Promise<{ + hosts: Host[]; + getTimeout: (retryCount: number, timeout: number) => number; + }> { + const statefulHosts = await Promise.all( + compatibleHosts.map((statelessHost) => { + return this.hostsCache.get(statelessHost, () => { + return Promise.resolve(new StatefulHost(statelessHost)); + }); + }) + ); + const hostsUp = statefulHosts.filter((host) => host.isUp()); + const hostsTimeouted = statefulHosts.filter((host) => host.isTimedout()); + + /** + * Note, we put the hosts that previously timeouted on the end of the list. + */ + const hostsAvailable = [...hostsUp, ...hostsTimeouted]; + + const hosts = hostsAvailable.length > 0 ? hostsAvailable : compatibleHosts; + + return { + hosts, + getTimeout(timeoutsCount: number, baseTimeout: number): number { + /** + * Imagine that you have 4 hosts, if timeouts will increase + * on the following way: 1 (timeouted) > 4 (timeouted) > 5 (200). + * + * Note that, the very next request, we start from the previous timeout. + * + * 5 (timeouted) > 6 (timeouted) > 7 ... + * + * This strategy may need to be reviewed, but is the strategy on the our + * current v3 version. + */ + const timeoutMultiplier = + hostsTimeouted.length === 0 && timeoutsCount === 0 + ? 1 + : hostsTimeouted.length + 3 + timeoutsCount; + + return timeoutMultiplier * baseTimeout; + }, + }; + } + + async request( + request: Request, + requestOptions: RequestOptions + ): Promise { + const stackTrace: StackFrame[] = []; + + const isRead = request.method === 'GET'; + + /** + * First we prepare the payload that do not depend from hosts. + */ + const data = serializeData(request, requestOptions); + const headers = serializeHeaders(this.baseHeaders, requestOptions); + const method = request.method; + + // On `GET`, the data is proxied to query parameters. + const dataQueryParameters: Record = isRead + ? { + ...request.data, + ...requestOptions.data, + } + : {}; + + const queryParameters = { + 'x-algolia-agent': this.userAgent, + ...dataQueryParameters, + ...requestOptions.queryParameters, + }; + + let timeoutsCount = 0; + + const retry = async ( + hosts: Host[], + getTimeout: (timeoutsCount: number, timeout: number) => number + ): Promise => { + /** + * We iterate on each host, until there is no host left. + */ + const host = hosts.pop(); + if (host === undefined) { + throw new RetryError(stackTraceWithoutCredentials(stackTrace)); + } + + let responseTimeout = requestOptions.timeout; + if (responseTimeout === undefined) { + responseTimeout = isRead ? this.timeouts.read : this.timeouts.write; + } + + const payload: EndRequest = { + data, + headers, + method, + url: serializeUrl(host, request.path, queryParameters), + connectTimeout: getTimeout(timeoutsCount, this.timeouts.connect), + responseTimeout: getTimeout(timeoutsCount, responseTimeout), + }; + + /** + * The stackFrame is pushed to the stackTrace so we + * can have information about onRetry and onFailure + * decisions. + */ + const pushToStackTrace = (response: Response): StackFrame => { + const stackFrame: StackFrame = { + request: payload, + response, + host, + triesLeft: hosts.length, + }; + + stackTrace.push(stackFrame); + + return stackFrame; + }; + + const response = await this.requester.send(payload, request); + + if (isRetryable(response)) { + const stackFrame = pushToStackTrace(response); + + // If response is a timeout, we increase the number of timeouts so we can increase the timeout later. + if (response.isTimedOut) { + timeoutsCount++; + } + /** + * Failures are individually sent to the logger, allowing + * the end user to debug / store stack frames even + * when a retry error does not happen. + */ + // eslint-disable-next-line no-console -- this will be fixed with the new `Logger` + console.log( + 'Retryable failure', + stackFrameWithoutCredentials(stackFrame) + ); + + /** + * We also store the state of the host in failure cases. If the host, is + * down it will remain down for the next 2 minutes. In a timeout situation, + * this host will be added end of the list of hosts on the next request. + */ + await this.hostsCache.set( + host, + new StatefulHost(host, response.isTimedOut ? 'timedout' : 'down') + ); + return retry(hosts, getTimeout); + } + if (isSuccess(response)) { + return deserializeSuccess(response); + } + + pushToStackTrace(response); + throw deserializeFailure(response, stackTrace); + }; + + /** + * Finally, for each retryable host perform request until we got a non + * retryable response. Some notes here: + * + * 1. The reverse here is applied so we can apply a `pop` later on => more performant. + * 2. We also get from the retryable options a timeout multiplier that is tailored + * for the current context. + */ + const compatibleHosts = this.hosts.filter( + (host) => + host.accept === 'readWrite' || + (isRead ? host.accept === 'read' : host.accept === 'write') + ); + const options = await this.createRetryableOptions(compatibleHosts); + return retry([...options.hosts].reverse(), options.getTimeout); + } +} diff --git a/clients/algoliasearch-client-javascript/client-insights/utils/cache/Cache.ts b/clients/algoliasearch-client-javascript/client-insights/utils/cache/Cache.ts new file mode 100644 index 0000000000..625862660c --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-insights/utils/cache/Cache.ts @@ -0,0 +1,27 @@ +export interface Cache { + /** + * Gets the value of the given `key`. + */ + get: ( + key: Record | string, + defaultValue: () => Promise + ) => Promise; + + /** + * Sets the given value with the given `key`. + */ + set: ( + key: Record | string, + value: TValue + ) => Promise; + + /** + * Deletes the given `key`. + */ + delete: (key: Record | string) => Promise; + + /** + * Clears the cache. + */ + clear: () => Promise; +} diff --git a/clients/algoliasearch-client-javascript/client-insights/utils/cache/MemoryCache.ts b/clients/algoliasearch-client-javascript/client-insights/utils/cache/MemoryCache.ts new file mode 100644 index 0000000000..f7853f39bc --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-insights/utils/cache/MemoryCache.ts @@ -0,0 +1,39 @@ +import type { Cache } from './Cache'; + +export class MemoryCache implements Cache { + private cache: Record = {}; + + async get( + key: Record | string, + defaultValue: () => Promise + ): Promise { + const keyAsString = JSON.stringify(key); + + if (keyAsString in this.cache) { + return Promise.resolve(this.cache[keyAsString]); + } + + return await defaultValue(); + } + + set( + key: Record | string, + value: TValue + ): Promise { + this.cache[JSON.stringify(key)] = value; + + return Promise.resolve(value); + } + + delete(key: Record | string): Promise { + delete this.cache[JSON.stringify(key)]; + + return Promise.resolve(); + } + + clear(): Promise { + this.cache = {}; + + return Promise.resolve(); + } +} diff --git a/clients/algoliasearch-client-javascript/client-insights/utils/errors.ts b/clients/algoliasearch-client-javascript/client-insights/utils/errors.ts new file mode 100644 index 0000000000..a02f3004ad --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-insights/utils/errors.ts @@ -0,0 +1,38 @@ +import type { Response, StackFrame } from './types'; + +class ErrorWithStackTrace extends Error { + stackTrace: StackFrame[]; + + constructor(message: string, stackTrace: StackFrame[]) { + super(message); + // the array and object should be frozen to reflect the stackTrace at the time of the error + this.stackTrace = stackTrace; + } +} + +export class RetryError extends ErrorWithStackTrace { + constructor(stackTrace: StackFrame[]) { + super( + 'Unreachable hosts - your application id may be incorrect. If the error persists, contact support@algolia.com.', + stackTrace + ); + } +} + +export class ApiError extends ErrorWithStackTrace { + status: number; + + constructor(message: string, status: number, stackTrace: StackFrame[]) { + super(message, stackTrace); + this.status = status; + } +} + +export class DeserializationError extends Error { + response: Response; + + constructor(message: string, response: Response) { + super(message); + this.response = response; + } +} diff --git a/clients/algoliasearch-client-javascript/client-insights/utils/helpers.ts b/clients/algoliasearch-client-javascript/client-insights/utils/helpers.ts new file mode 100644 index 0000000000..5d64ac8868 --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-insights/utils/helpers.ts @@ -0,0 +1,117 @@ +import { ApiError, DeserializationError } from './errors'; +import type { + Headers, + Host, + Request, + RequestOptions, + Response, + StackFrame, +} from './types'; + +export function shuffle(array: TData[]): TData[] { + const shuffledArray = array; + + for (let c = array.length - 1; c > 0; c--) { + const b = Math.floor(Math.random() * (c + 1)); + const a = array[c]; + + shuffledArray[c] = array[b]; + shuffledArray[b] = a; + } + + return shuffledArray; +} + +export function serializeUrl( + host: Host, + path: string, + queryParameters: Readonly> +): string { + const queryParametersAsString = serializeQueryParameters(queryParameters); + let url = `${host.protocol}://${host.url}/${ + path.charAt(0) === '/' ? path.substr(1) : path + }`; + + if (queryParametersAsString.length) { + url += `?${queryParametersAsString}`; + } + + return url; +} + +export function serializeQueryParameters( + parameters: Readonly> +): string { + const isObjectOrArray = (value: any): boolean => + Object.prototype.toString.call(value) === '[object Object]' || + Object.prototype.toString.call(value) === '[object Array]'; + + return Object.keys(parameters) + .map( + (key) => + `${key}=${ + isObjectOrArray(parameters[key]) + ? JSON.stringify(parameters[key]) + : parameters[key] + }` + ) + .join('&'); +} + +export function serializeData( + request: Request, + requestOptions: RequestOptions +): string | undefined { + if ( + request.method === 'GET' || + (request.data === undefined && requestOptions.data === undefined) + ) { + return undefined; + } + + const data = Array.isArray(request.data) + ? request.data + : { ...request.data, ...requestOptions.data }; + + return JSON.stringify(data); +} + +export function serializeHeaders( + baseHeaders: Headers, + requestOptions: RequestOptions +): Headers { + const headers: Headers = { + ...baseHeaders, + ...requestOptions.headers, + }; + const serializedHeaders: Headers = {}; + + Object.keys(headers).forEach((header) => { + const value = headers[header]; + serializedHeaders[header.toLowerCase()] = value; + }); + + return serializedHeaders; +} + +export function deserializeSuccess(response: Response): TObject { + try { + return JSON.parse(response.content); + } catch (e) { + throw new DeserializationError((e as Error).message, response); + } +} + +export function deserializeFailure( + { content, status }: Response, + stackFrame: StackFrame[] +): Error { + let message = content; + try { + message = JSON.parse(content).message; + } catch (e) { + // .. + } + + return new ApiError(message, status, stackFrame); +} diff --git a/clients/algoliasearch-client-javascript/client-insights/utils/requester/EchoRequester.ts b/clients/algoliasearch-client-javascript/client-insights/utils/requester/EchoRequester.ts new file mode 100644 index 0000000000..41a3dd9041 --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-insights/utils/requester/EchoRequester.ts @@ -0,0 +1,17 @@ +import type { EndRequest, Request, Response } from '../types'; + +import { Requester } from './Requester'; + +export class EchoRequester extends Requester { + constructor(private status = 200) { + super(); + } + + send(_request: EndRequest, originalRequest: Request): Promise { + return Promise.resolve({ + content: JSON.stringify(originalRequest), + isTimedOut: false, + status: this.status, + }); + } +} diff --git a/clients/algoliasearch-client-javascript/client-insights/utils/requester/HttpRequester.ts b/clients/algoliasearch-client-javascript/client-insights/utils/requester/HttpRequester.ts new file mode 100644 index 0000000000..3697d290fb --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-insights/utils/requester/HttpRequester.ts @@ -0,0 +1,94 @@ +import http from 'http'; +import https from 'https'; + +import type { EndRequest, Response } from '../types'; + +import { Requester } from './Requester'; + +// Global agents allow us to reuse the TCP protocol with multiple clients +const agentOptions = { keepAlive: true }; +const httpAgent = new http.Agent(agentOptions); +const httpsAgent = new https.Agent(agentOptions); + +export class HttpRequester extends Requester { + send(request: EndRequest): Promise { + return new Promise((resolve) => { + let responseTimeout: NodeJS.Timeout | undefined; + // eslint-disable-next-line prefer-const -- linter thinks this is not reassigned + let connectTimeout: NodeJS.Timeout | undefined; + const url = new URL(request.url); + const path = + url.search === null ? url.pathname : `${url.pathname}${url.search}`; + const options: https.RequestOptions = { + agent: url.protocol === 'https:' ? httpsAgent : httpAgent, + hostname: url.hostname, + path, + method: request.method, + headers: request.headers, + ...(url.port !== undefined ? { port: url.port || '' } : {}), + }; + + const req = (url.protocol === 'https:' ? https : http).request( + options, + (response) => { + let contentBuffers: Buffer[] = []; + + response.on('data', (chunk) => { + contentBuffers = contentBuffers.concat(chunk); + }); + + response.on('end', () => { + clearTimeout(connectTimeout as NodeJS.Timeout); + clearTimeout(responseTimeout as NodeJS.Timeout); + + resolve({ + status: response.statusCode || 0, + content: Buffer.concat(contentBuffers).toString(), + isTimedOut: false, + }); + }); + } + ); + + const createTimeout = ( + timeout: number, + content: string + ): NodeJS.Timeout => { + return setTimeout(() => { + req.destroy(); + + resolve({ + status: 0, + content, + isTimedOut: true, + }); + }, timeout * 1000); + }; + + connectTimeout = createTimeout( + request.connectTimeout, + 'Connection timeout' + ); + + req.on('error', (error) => { + clearTimeout(connectTimeout as NodeJS.Timeout); + clearTimeout(responseTimeout!); + resolve({ status: 0, content: error.message, isTimedOut: false }); + }); + + req.once('response', () => { + clearTimeout(connectTimeout as NodeJS.Timeout); + responseTimeout = createTimeout( + request.responseTimeout, + 'Socket timeout' + ); + }); + + if (request.data !== undefined) { + req.write(request.data); + } + + req.end(); + }); + } +} diff --git a/clients/algoliasearch-client-javascript/client-insights/utils/requester/Requester.ts b/clients/algoliasearch-client-javascript/client-insights/utils/requester/Requester.ts new file mode 100644 index 0000000000..41c0606575 --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-insights/utils/requester/Requester.ts @@ -0,0 +1,8 @@ +import type { EndRequest, Request, Response } from '../types'; + +export abstract class Requester { + abstract send( + request: EndRequest, + originalRequest: Request + ): Promise; +} diff --git a/clients/algoliasearch-client-javascript/client-insights/utils/stackTrace.ts b/clients/algoliasearch-client-javascript/client-insights/utils/stackTrace.ts new file mode 100644 index 0000000000..14750a54f2 --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-insights/utils/stackTrace.ts @@ -0,0 +1,30 @@ +import type { StackFrame } from './types'; + +export function stackTraceWithoutCredentials( + stackTrace: StackFrame[] +): StackFrame[] { + return stackTrace.map((stackFrame) => + stackFrameWithoutCredentials(stackFrame) + ); +} + +export function stackFrameWithoutCredentials( + stackFrame: StackFrame +): StackFrame { + const modifiedHeaders: Record = stackFrame.request.headers[ + 'x-algolia-api-key' + ] + ? { 'x-algolia-api-key': '*****' } + : {}; + + return { + ...stackFrame, + request: { + ...stackFrame.request, + headers: { + ...stackFrame.request.headers, + ...modifiedHeaders, + }, + }, + }; +} diff --git a/clients/algoliasearch-client-javascript/client-insights/utils/types.ts b/clients/algoliasearch-client-javascript/client-insights/utils/types.ts new file mode 100644 index 0000000000..d2a478c1a0 --- /dev/null +++ b/clients/algoliasearch-client-javascript/client-insights/utils/types.ts @@ -0,0 +1,65 @@ +export type Method = 'DELETE' | 'GET' | 'PATCH' | 'POST' | 'PUT'; + +export type Request = { + method: Method; + path: string; + data?: Record; +}; + +export type RequestOptions = { + /** + * Custom timeout for the request. Note that, in normal situacions + * the given timeout will be applied. But the transporter layer may + * increase this timeout if there is need for it. + */ + timeout?: number; + + /** + * Custom headers for the request. This headers are + * going to be merged the transporter headers. + */ + headers?: Record; + + /** + * Custom query parameters for the request. This query parameters are + * going to be merged the transporter query parameters. + */ + queryParameters: Record; + data?: Record; +}; + +export type EndRequest = { + method: Method; + url: string; + connectTimeout: number; + responseTimeout: number; + headers: Headers; + data?: string; +}; + +export type Response = { + content: string; + isTimedOut: boolean; + status: number; +}; + +export type Headers = Record; + +export type Host = { + url: string; + accept: 'read' | 'readWrite' | 'write'; + protocol: 'http' | 'https'; +}; + +export type StackFrame = { + request: EndRequest; + response: Response; + host: Host; + triesLeft: number; +}; + +export type Timeouts = { + readonly connect: number; + readonly read: number; + readonly write: number; +}; diff --git a/openapitools.json b/openapitools.json index 68b579e23b..8cf8fe32b6 100644 --- a/openapitools.json +++ b/openapitools.json @@ -87,6 +87,26 @@ "isAnalyticsHost": true } }, + "javascript-insights": { + "generatorName": "typescript-node", + "templateDir": "#{cwd}/templates/javascript/", + "config": "#{cwd}/openapitools.json", + "apiPackage": "src", + "output": "#{cwd}/clients/algoliasearch-client-javascript/client-insights", + "glob": "specs/insights/spec.yml", + "gitHost": "algolia", + "gitUserId": "algolia", + "gitRepoId": "algoliasearch-client-javascript", + "additionalProperties": { + "modelPropertyNaming": "original", + "supportsES6": true, + "npmName": "@algolia/client-insights", + "npmVersion": "5.0.0", + + "packageName": "@algolia/client-insights", + "isInsightsHost": true + } + }, "java-search": { "generatorName": "java", "templateDir": "#{cwd}/templates/java/", diff --git a/specs/insights/paths/pushEvents.yml b/specs/insights/paths/pushEvents.yml index bf62725b88..945370372d 100644 --- a/specs/insights/paths/pushEvents.yml +++ b/specs/insights/paths/pushEvents.yml @@ -1 +1,89 @@ -# post: +post: + tags: + - insights + operationId: pushEvents + description: This command pushes an array of events. + summary: Pushes an array of events. + requestBody: + required: true + content: + application/json: + schema: + title: InsightEvents + type: object + description: Object containing the events sent. + additionalProperties: false + required: + - events + properties: + events: + type: array + description: Array of events sent. + items: + title: InsightEvent + type: object + description: Insights event. + additionalProperties: false + properties: + eventType: + type: string + description: An eventType can be a click, a conversion, or a view. + enum: [click, conversion, view] + eventName: + type: string + description: A user-defined string used to categorize events. + index: + type: string + description: Name of the targeted index. + userToken: + type: string + description: A user identifier. Depending if the user is logged-in or not, several strategies can be used from a sessionId to a technical identifier. + timestamp: + type: integer + description: Time of the event expressed in milliseconds since the Unix epoch. + queryID: + type: string + description: Algolia queryID. This is required when an event is tied to a search. + objectIDs: + type: array + description: An array of index objectID. Limited to 20 objects. An event can’t have both objectIDs and filters at the same time. + items: + type: string + filters: + type: array + description: An array of filters. Limited to 10 filters. An event can’t have both objectIDs and filters at the same time. + items: + type: string + positions: + type: array + description: Position of the click in the list of Algolia search results. This field is required if a queryID is provided. One position must be provided for each objectID. + items: + type: integer + required: + - eventType + - eventName + - index + - userToken + responses: + '200': + description: OK + content: + application/json: + schema: + title: pushEventsResponse + type: object + additionalProperties: false + required: + - message + properties: + message: + type: string + description: A message confirming the event push. + '400': + $ref: ../../common/responses/BadRequest.yml + '402': + $ref: ../../common/responses/FeatureNotEnabled.yml + '403': + $ref: ../../common/responses/MethodNotAllowed.yml + '404': + $ref: ../../common/responses/IndexNotFound.yml diff --git a/specs/insights/spec.yml b/specs/insights/spec.yml index 4b362a7eb1..4cb05bc906 100644 --- a/specs/insights/spec.yml +++ b/specs/insights/spec.yml @@ -1,17 +1,17 @@ -# openapi: 3.0.2 -# info: -# title: Insights API -# description: API powering the Insights feature of Algolia. -# version: 0.0.1 -# components: -# securitySchemes: -# appId: -# $ref: '../common/securitySchemes.yml#/appId' -# apiKey: -# $ref: '../common/securitySchemes.yml#/apiKey' -# security: -# - appId: [] -# apiKey: [] -# paths: -# /1/events: -# $ref: paths/pushEvents.yml +openapi: 3.0.2 +info: + title: Insights API + description: API powering the Insights feature of Algolia. + version: 0.0.1 +components: + securitySchemes: + appId: + $ref: '../common/securitySchemes.yml#/appId' + apiKey: + $ref: '../common/securitySchemes.yml#/apiKey' +security: + - appId: [] + apiKey: [] +paths: + /1/events: + $ref: paths/pushEvents.yml diff --git a/templates/javascript/api-single.mustache b/templates/javascript/api-single.mustache index 2c64259f5b..5a0416ee55 100644 --- a/templates/javascript/api-single.mustache +++ b/templates/javascript/api-single.mustache @@ -68,7 +68,7 @@ export class {{classname}} { this.transporter = new Transporter({ hosts: options?.hosts ?? this.getDefaultHosts( - {{^hasRegionalHost}}appId{{/hasRegionalHost}} + {{#isSearchHost}}appId{{/isSearchHost}} {{#hasRegionalHost}}region{{/hasRegionalHost}} ), baseHeaders: { @@ -113,6 +113,12 @@ export class {{classname}} { } {{/isAnalyticsHost}} + {{#isInsightsHost}} + public getDefaultHosts(): Host[] { + return [{ url: `insights.algolia.io`, accept: 'readWrite', protocol: 'https' }]; + } + {{/isInsightsHost}} + public setRequest(requester: Requester): void { this.transporter.setRequester(requester); } diff --git a/tests/CTS/clients/insights/pushEvents.json b/tests/CTS/clients/insights/pushEvents.json new file mode 100644 index 0000000000..27ebfaf751 --- /dev/null +++ b/tests/CTS/clients/insights/pushEvents.json @@ -0,0 +1,75 @@ +[ + { + "method": "pushEvents", + "parameters": [ + { + "insightEvents": { + "events": [ + { + "eventType": "click", + "eventName": "Product Clicked", + "index": "products", + "userToken": "user-123456", + "timestamp": 1641290601962, + "objectIDs": ["9780545139700", "9780439784542"], + "queryID": "43b15df305339e827f0ac0bdc5ebcaa7", + "positions": [7, 6] + }, + { + "eventType": "view", + "eventName":"Product Detail Page Viewed", + "index": "products", + "userToken": "user-123456", + "timestamp": 1641290601962, + "objectIDs": ["9780545139700", "9780439784542"] + }, + { + "eventType": "conversion", + "eventName": "Product Purchased", + "index": "products", + "userToken": "user-123456", + "timestamp": 1641290601962, + "objectIDs": ["9780545139700", "9780439784542"], + "queryID": "43b15df305339e827f0ac0bdc5ebcaa7" + } + ] + } + } + ], + "request": { + "path": "/1/events", + "method": "POST", + "data": { + "events": [ + { + "eventType": "click", + "eventName": "Product Clicked", + "index": "products", + "userToken": "user-123456", + "timestamp": 1641290601962, + "objectIDs": ["9780545139700", "9780439784542"], + "queryID": "43b15df305339e827f0ac0bdc5ebcaa7", + "positions": [7, 6] + }, + { + "eventType": "view", + "eventName":"Product Detail Page Viewed", + "index": "products", + "userToken": "user-123456", + "timestamp": 1641290601962, + "objectIDs": ["9780545139700", "9780439784542"] + }, + { + "eventType": "conversion", + "eventName": "Product Purchased", + "index": "products", + "userToken": "user-123456", + "timestamp": 1641290601962, + "objectIDs": ["9780545139700", "9780439784542"], + "queryID": "43b15df305339e827f0ac0bdc5ebcaa7" + } + ] + } + } + } +] diff --git a/tests/output/javascript/insights.test.ts b/tests/output/javascript/insights.test.ts new file mode 100644 index 0000000000..36fe80cdec --- /dev/null +++ b/tests/output/javascript/insights.test.ts @@ -0,0 +1,81 @@ +// @ts-nocheck +import { InsightsApi, EchoRequester } from '@algolia/client-insights'; + +const client = new InsightsApi( + process.env.ALGOLIA_APPLICATION_ID, + process.env.ALGOLIA_SEARCH_KEY, + { requester: new EchoRequester() } +); + +describe('pushEvents', () => { + test('pushEvents', async () => { + const req = await client.pushEvents({ + insightEvents: { + events: [ + { + eventType: 'click', + eventName: 'Product Clicked', + index: 'products', + userToken: 'user-123456', + timestamp: 1641290601962, + objectIDs: ['9780545139700', '9780439784542'], + queryID: '43b15df305339e827f0ac0bdc5ebcaa7', + positions: [7, 6], + }, + { + eventType: 'view', + eventName: 'Product Detail Page Viewed', + index: 'products', + userToken: 'user-123456', + timestamp: 1641290601962, + objectIDs: ['9780545139700', '9780439784542'], + }, + { + eventType: 'conversion', + eventName: 'Product Purchased', + index: 'products', + userToken: 'user-123456', + timestamp: 1641290601962, + objectIDs: ['9780545139700', '9780439784542'], + queryID: '43b15df305339e827f0ac0bdc5ebcaa7', + }, + ], + }, + }); + expect(req).toMatchObject({ + path: '/1/events', + method: 'POST', + data: { + events: [ + { + eventType: 'click', + eventName: 'Product Clicked', + index: 'products', + userToken: 'user-123456', + timestamp: 1641290601962, + objectIDs: ['9780545139700', '9780439784542'], + queryID: '43b15df305339e827f0ac0bdc5ebcaa7', + positions: [7, 6], + }, + { + eventType: 'view', + eventName: 'Product Detail Page Viewed', + index: 'products', + userToken: 'user-123456', + timestamp: 1641290601962, + objectIDs: ['9780545139700', '9780439784542'], + }, + { + eventType: 'conversion', + eventName: 'Product Purchased', + index: 'products', + userToken: 'user-123456', + timestamp: 1641290601962, + objectIDs: ['9780545139700', '9780439784542'], + queryID: '43b15df305339e827f0ac0bdc5ebcaa7', + }, + ], + }, + }); + }); +}); diff --git a/tests/output/javascript/search.test.ts b/tests/output/javascript/search.test.ts index b0d5668e59..31af67f315 100644 --- a/tests/output/javascript/search.test.ts +++ b/tests/output/javascript/search.test.ts @@ -7,329 +7,309 @@ const client = new SearchApi( { requester: new EchoRequester() } ); -describe('getDictionarySettings', () => { - test('get getDictionarySettings results', async () => { - const req = await client.getDictionarySettings(); +describe('addApiKey', () => { + test('addApiKey', async () => { + const req = await client.addApiKey({ + apiKey: { + acl: ['search', 'addObject'], + description: 'my new api key', + validity: 300, + maxQueriesPerIPPerHour: 100, + maxHitsPerQuery: 20, + }, + }); expect(req).toMatchObject({ - path: '/1/dictionaries/*/settings', - method: 'GET', + path: '/1/keys', + method: 'POST', + data: { + acl: ['search', 'addObject'], + description: 'my new api key', + validity: 300, + maxQueriesPerIPPerHour: 100, + maxHitsPerQuery: 20, + }, }); }); }); -describe('searchSynonyms', () => { - test('searchSynonyms', async () => { - const req = await client.searchSynonyms({ - indexName: 'indexName', - query: 'queryString', - type: 'onewaysynonym', +describe('batchDictionaryEntries', () => { + test('get batchDictionaryEntries results with minimal parameters', async () => { + const req = await client.batchDictionaryEntries({ + dictionaryName: 'dictionaryName', + batchDictionaryEntries: { + requests: [ + { action: 'addEntry', body: { objectID: '1', language: 'en' } }, + { action: 'deleteEntry', body: { objectID: '2', language: 'fr' } }, + ], + }, }); expect(req).toMatchObject({ - path: '/1/indexes/indexName/synonyms/search', + path: '/1/dictionaries/dictionaryName/batch', method: 'POST', + data: { + requests: [ + { action: 'addEntry', body: { objectID: '1', language: 'en' } }, + { action: 'deleteEntry', body: { objectID: '2', language: 'fr' } }, + ], + }, + }); + }); + + test('get batchDictionaryEntries results with all parameters', async () => { + const req = await client.batchDictionaryEntries({ + dictionaryName: 'dictionaryName', + batchDictionaryEntries: { + clearExistingDictionaryEntries: false, + requests: [ + { + action: 'addEntry', + body: { + objectID: '1', + language: 'en', + word: 'yo', + words: ['yo', 'algolia'], + decomposition: ['yo', 'algolia'], + state: 'enabled', + }, + }, + { + action: 'deleteEntry', + body: { + objectID: '2', + language: 'fr', + word: 'salut', + words: ['salut', 'algolia'], + decomposition: ['salut', 'algolia'], + state: 'enabled', + }, + }, + ], + }, + }); + expect(req).toMatchObject({ + path: '/1/dictionaries/dictionaryName/batch', + method: 'POST', + data: { + clearExistingDictionaryEntries: false, + requests: [ + { + action: 'addEntry', + body: { + objectID: '1', + language: 'en', + word: 'yo', + words: ['yo', 'algolia'], + decomposition: ['yo', 'algolia'], + state: 'enabled', + }, + }, + { + action: 'deleteEntry', + body: { + objectID: '2', + language: 'fr', + word: 'salut', + words: ['salut', 'algolia'], + decomposition: ['salut', 'algolia'], + state: 'enabled', + }, + }, + ], + }, }); }); }); -describe('saveSynonyms', () => { - test('saveSynonyms', async () => { - const req = await client.saveSynonyms({ +describe('batchRules', () => { + test('batchRules', async () => { + const req = await client.batchRules({ indexName: 'indexName', - synonymHit: [ + rule: [ { - objectID: 'id1', - type: 'synonym', - synonyms: ['car', 'vehicule', 'auto'], + objectID: 'a-rule-id', + conditions: [{ pattern: 'smartphone', anchoring: 'contains' }], + consequence: { params: { filters: 'category:smartphone' } }, }, { - objectID: 'id2', - type: 'onewaysynonym', - input: 'iphone', - synonyms: ['ephone', 'aphone', 'yphone'], + objectID: 'a-second-rule-id', + conditions: [{ pattern: 'apple', anchoring: 'contains' }], + consequence: { params: { filters: 'brand:apple' } }, }, ], forwardToReplicas: true, - replaceExistingSynonyms: false, + clearExistingRules: true, }); expect(req).toMatchObject({ - path: '/1/indexes/indexName/synonyms/batch', + path: '/1/indexes/indexName/rules/batch', method: 'POST', data: [ { - objectID: 'id1', - type: 'synonym', - synonyms: ['car', 'vehicule', 'auto'], + objectID: 'a-rule-id', + conditions: [{ pattern: 'smartphone', anchoring: 'contains' }], + consequence: { params: { filters: 'category:smartphone' } }, }, { - objectID: 'id2', - type: 'onewaysynonym', - input: 'iphone', - synonyms: ['ephone', 'aphone', 'yphone'], + objectID: 'a-second-rule-id', + conditions: [{ pattern: 'apple', anchoring: 'contains' }], + consequence: { params: { filters: 'brand:apple' } }, }, ], }); }); }); -describe('deleteRule', () => { - test('deleteRule', async () => { - const req = await client.deleteRule({ - indexName: 'indexName', - objectID: 'id1', - }); - expect(req).toMatchObject({ - path: '/1/indexes/indexName/rules/id1', - method: 'DELETE', - }); - }); -}); - -describe('searchForFacetValues', () => { - test('get searchForFacetValues results with minimal parameters', async () => { - const req = await client.searchForFacetValues({ - indexName: 'indexName', - facetName: 'facetName', - }); +describe('browse', () => { + test('get browse results with minimal parameters', async () => { + const req = await client.browse({ indexName: 'indexName' }); expect(req).toMatchObject({ - path: '/1/indexes/indexName/facets/facetName/query', + path: '/1/indexes/indexName/browse', method: 'POST', }); }); - test('get searchForFacetValues results with all parameters', async () => { - const req = await client.searchForFacetValues({ + test('get browse results with all parameters', async () => { + const req = await client.browse({ indexName: 'indexName', - facetName: 'facetName', - searchForFacetValuesRequest: { + browseRequest: { params: "query=foo&facetFilters=['bar']", - facetQuery: 'foo', - maxFacetHits: 42, + cursor: 'cts', }, }); expect(req).toMatchObject({ - path: '/1/indexes/indexName/facets/facetName/query', + path: '/1/indexes/indexName/browse', method: 'POST', - data: { - params: "query=foo&facetFilters=['bar']", - facetQuery: 'foo', - maxFacetHits: 42, - }, + data: { params: "query=foo&facetFilters=['bar']", cursor: 'cts' }, }); }); }); -describe('getSynonym', () => { - test('getSynonym', async () => { - const req = await client.getSynonym({ - indexName: 'indexName', - objectID: 'id1', - }); +describe('clearAllSynonyms', () => { + test('clearAllSynonyms', async () => { + const req = await client.clearAllSynonyms({ indexName: 'indexName' }); expect(req).toMatchObject({ - path: '/1/indexes/indexName/synonyms/id1', - method: 'GET', + path: '/1/indexes/indexName/synonyms/clear', + method: 'POST', }); }); }); -describe('search', () => { - test('search', async () => { - const req = await client.search({ - indexName: 'indexName', - searchParams: { $objectName: 'Query', query: 'queryString' }, - }); +describe('clearRules', () => { + test('clearRules', async () => { + const req = await client.clearRules({ indexName: 'indexName' }); expect(req).toMatchObject({ - path: '/1/indexes/indexName/query', + path: '/1/indexes/indexName/rules/clear', method: 'POST', - data: { query: 'queryString' }, }); }); }); -describe('setDictionarySettings', () => { - test('get setDictionarySettings results with minimal parameters', async () => { - const req = await client.setDictionarySettings({ - dictionarySettingsRequest: { - disableStandardEntries: { plurals: { fr: false, en: false, ru: true } }, - }, - }); - expect(req).toMatchObject({ - path: '/1/dictionaries/*/settings', - method: 'PUT', - data: { - disableStandardEntries: { plurals: { fr: false, en: false, ru: true } }, - }, - }); - }); - - test('get setDictionarySettings results with all parameters', async () => { - const req = await client.setDictionarySettings({ - dictionarySettingsRequest: { - disableStandardEntries: { - plurals: { fr: false, en: false, ru: true }, - stopwords: { fr: false }, - compounds: { ru: true }, - }, - }, - }); +describe('deleteApiKey', () => { + test('deleteApiKey', async () => { + const req = await client.deleteApiKey({ key: 'myTestApiKey' }); expect(req).toMatchObject({ - path: '/1/dictionaries/*/settings', - method: 'PUT', - data: { - disableStandardEntries: { - plurals: { fr: false, en: false, ru: true }, - stopwords: { fr: false }, - compounds: { ru: true }, - }, - }, + path: '/1/keys/myTestApiKey', + method: 'DELETE', }); }); }); -describe('getRule', () => { - test('getRule', async () => { - const req = await client.getRule({ +describe('deleteRule', () => { + test('deleteRule', async () => { + const req = await client.deleteRule({ indexName: 'indexName', objectID: 'id1', }); expect(req).toMatchObject({ path: '/1/indexes/indexName/rules/id1', - method: 'GET', + method: 'DELETE', }); }); }); -describe('searchDictionaryEntries', () => { - test('get searchDictionaryEntries results with minimal parameters', async () => { - const req = await client.searchDictionaryEntries({ - dictionaryName: 'dictionaryName', - searchDictionaryEntries: { query: 'foo' }, +describe('deleteSynonym', () => { + test('deleteSynonym', async () => { + const req = await client.deleteSynonym({ + indexName: 'indexName', + objectID: 'id1', }); expect(req).toMatchObject({ - path: '/1/dictionaries/dictionaryName/search', - method: 'POST', - data: { query: 'foo' }, + path: '/1/indexes/indexName/synonyms/id1', + method: 'DELETE', }); }); +}); - test('get searchDictionaryEntries results with all parameters', async () => { - const req = await client.searchDictionaryEntries({ - dictionaryName: 'dictionaryName', - searchDictionaryEntries: { - query: 'foo', - page: 4, - hitsPerPage: 2, - language: 'fr', - }, - }); +describe('getApiKey', () => { + test('getApiKey', async () => { + const req = await client.getApiKey({ key: 'myTestApiKey' }); expect(req).toMatchObject({ - path: '/1/dictionaries/dictionaryName/search', - method: 'POST', - data: { query: 'foo', page: 4, hitsPerPage: 2, language: 'fr' }, + path: '/1/keys/myTestApiKey', + method: 'GET', }); }); }); -describe('batchRules', () => { - test('batchRules', async () => { - const req = await client.batchRules({ - indexName: 'indexName', - rule: [ - { - objectID: 'a-rule-id', - conditions: [{ pattern: 'smartphone', anchoring: 'contains' }], - consequence: { params: { filters: 'category:smartphone' } }, - }, - { - objectID: 'a-second-rule-id', - conditions: [{ pattern: 'apple', anchoring: 'contains' }], - consequence: { params: { filters: 'brand:apple' } }, - }, - ], - forwardToReplicas: true, - clearExistingRules: true, - }); +describe('getDictionaryLanguages', () => { + test('get getDictionaryLanguages', async () => { + const req = await client.getDictionaryLanguages(); expect(req).toMatchObject({ - path: '/1/indexes/indexName/rules/batch', - method: 'POST', - data: [ - { - objectID: 'a-rule-id', - conditions: [{ pattern: 'smartphone', anchoring: 'contains' }], - consequence: { params: { filters: 'category:smartphone' } }, - }, - { - objectID: 'a-second-rule-id', - conditions: [{ pattern: 'apple', anchoring: 'contains' }], - consequence: { params: { filters: 'brand:apple' } }, - }, - ], + path: '/1/dictionaries/*/languages', + method: 'GET', }); }); }); -describe('updateApiKey', () => { - test('updateApiKey', async () => { - const req = await client.updateApiKey({ - key: 'myApiKey', - apiKey: { - acl: ['search', 'addObject'], - validity: 300, - maxQueriesPerIPPerHour: 100, - maxHitsPerQuery: 20, - }, - }); +describe('getDictionarySettings', () => { + test('get getDictionarySettings results', async () => { + const req = await client.getDictionarySettings(); expect(req).toMatchObject({ - path: '/1/keys/myApiKey', - method: 'PUT', - data: { - acl: ['search', 'addObject'], - validity: 300, - maxQueriesPerIPPerHour: 100, - maxHitsPerQuery: 20, - }, + path: '/1/dictionaries/*/settings', + method: 'GET', }); }); }); -describe('getDictionaryLanguages', () => { - test('get getDictionaryLanguages', async () => { - const req = await client.getDictionaryLanguages(); +describe('getRule', () => { + test('getRule', async () => { + const req = await client.getRule({ + indexName: 'indexName', + objectID: 'id1', + }); expect(req).toMatchObject({ - path: '/1/dictionaries/*/languages', + path: '/1/indexes/indexName/rules/id1', method: 'GET', }); }); }); -describe('deleteApiKey', () => { - test('deleteApiKey', async () => { - const req = await client.deleteApiKey({ key: 'myTestApiKey' }); +describe('getSynonym', () => { + test('getSynonym', async () => { + const req = await client.getSynonym({ + indexName: 'indexName', + objectID: 'id1', + }); expect(req).toMatchObject({ - path: '/1/keys/myTestApiKey', - method: 'DELETE', + path: '/1/indexes/indexName/synonyms/id1', + method: 'GET', }); }); }); -describe('searchRules', () => { - test('searchRules', async () => { - const req = await client.searchRules({ - indexName: 'indexName', - searchRulesParams: { query: 'something' }, - }); +describe('listApiKeys', () => { + test('listApiKeys', async () => { + const req = await client.listApiKeys(); expect(req).toMatchObject({ - path: '/1/indexes/indexName/rules/search', - method: 'POST', - data: { query: 'something' }, + path: '/1/keys', + method: 'GET', }); }); }); -describe('clearAllSynonyms', () => { - test('clearAllSynonyms', async () => { - const req = await client.clearAllSynonyms({ indexName: 'indexName' }); +describe('restoreApiKey', () => { + test('restoreApiKey', async () => { + const req = await client.restoreApiKey({ key: 'myApiKey' }); expect(req).toMatchObject({ - path: '/1/indexes/indexName/synonyms/clear', + path: '/1/keys/myApiKey/restore', method: 'POST', }); }); @@ -359,216 +339,236 @@ describe('saveRule', () => { }); }); -describe('addApiKey', () => { - test('addApiKey', async () => { - const req = await client.addApiKey({ - apiKey: { - acl: ['search', 'addObject'], - description: 'my new api key', - validity: 300, - maxQueriesPerIPPerHour: 100, - maxHitsPerQuery: 20, - }, +describe('saveSynonym', () => { + test('saveSynonym', async () => { + const req = await client.saveSynonym({ + indexName: 'indexName', + objectID: 'id1', + synonymHit: { + objectID: 'id1', + type: 'synonym', + synonyms: ['car', 'vehicule', 'auto'], + }, + forwardToReplicas: true, + }); + expect(req).toMatchObject({ + path: '/1/indexes/indexName/synonyms/id1', + method: 'PUT', + data: { + objectID: 'id1', + type: 'synonym', + synonyms: ['car', 'vehicule', 'auto'], + }, + }); + }); +}); + +describe('saveSynonyms', () => { + test('saveSynonyms', async () => { + const req = await client.saveSynonyms({ + indexName: 'indexName', + synonymHit: [ + { + objectID: 'id1', + type: 'synonym', + synonyms: ['car', 'vehicule', 'auto'], + }, + { + objectID: 'id2', + type: 'onewaysynonym', + input: 'iphone', + synonyms: ['ephone', 'aphone', 'yphone'], + }, + ], + forwardToReplicas: true, + replaceExistingSynonyms: false, + }); + expect(req).toMatchObject({ + path: '/1/indexes/indexName/synonyms/batch', + method: 'POST', + data: [ + { + objectID: 'id1', + type: 'synonym', + synonyms: ['car', 'vehicule', 'auto'], + }, + { + objectID: 'id2', + type: 'onewaysynonym', + input: 'iphone', + synonyms: ['ephone', 'aphone', 'yphone'], + }, + ], + }); + }); +}); + +describe('search', () => { + test('search', async () => { + const req = await client.search({ + indexName: 'indexName', + searchParams: { $objectName: 'Query', query: 'queryString' }, }); expect(req).toMatchObject({ - path: '/1/keys', + path: '/1/indexes/indexName/query', method: 'POST', - data: { - acl: ['search', 'addObject'], - description: 'my new api key', - validity: 300, - maxQueriesPerIPPerHour: 100, - maxHitsPerQuery: 20, - }, + data: { query: 'queryString' }, }); }); }); -describe('restoreApiKey', () => { - test('restoreApiKey', async () => { - const req = await client.restoreApiKey({ key: 'myApiKey' }); +describe('searchDictionaryEntries', () => { + test('get searchDictionaryEntries results with minimal parameters', async () => { + const req = await client.searchDictionaryEntries({ + dictionaryName: 'dictionaryName', + searchDictionaryEntries: { query: 'foo' }, + }); expect(req).toMatchObject({ - path: '/1/keys/myApiKey/restore', + path: '/1/dictionaries/dictionaryName/search', method: 'POST', + data: { query: 'foo' }, }); }); -}); -describe('getApiKey', () => { - test('getApiKey', async () => { - const req = await client.getApiKey({ key: 'myTestApiKey' }); + test('get searchDictionaryEntries results with all parameters', async () => { + const req = await client.searchDictionaryEntries({ + dictionaryName: 'dictionaryName', + searchDictionaryEntries: { + query: 'foo', + page: 4, + hitsPerPage: 2, + language: 'fr', + }, + }); expect(req).toMatchObject({ - path: '/1/keys/myTestApiKey', - method: 'GET', + path: '/1/dictionaries/dictionaryName/search', + method: 'POST', + data: { query: 'foo', page: 4, hitsPerPage: 2, language: 'fr' }, }); }); }); -describe('browse', () => { - test('get browse results with minimal parameters', async () => { - const req = await client.browse({ indexName: 'indexName' }); +describe('searchForFacetValues', () => { + test('get searchForFacetValues results with minimal parameters', async () => { + const req = await client.searchForFacetValues({ + indexName: 'indexName', + facetName: 'facetName', + }); expect(req).toMatchObject({ - path: '/1/indexes/indexName/browse', + path: '/1/indexes/indexName/facets/facetName/query', method: 'POST', }); }); - test('get browse results with all parameters', async () => { - const req = await client.browse({ + test('get searchForFacetValues results with all parameters', async () => { + const req = await client.searchForFacetValues({ indexName: 'indexName', - browseRequest: { + facetName: 'facetName', + searchForFacetValuesRequest: { params: "query=foo&facetFilters=['bar']", - cursor: 'cts', + facetQuery: 'foo', + maxFacetHits: 42, }, }); expect(req).toMatchObject({ - path: '/1/indexes/indexName/browse', + path: '/1/indexes/indexName/facets/facetName/query', method: 'POST', - data: { params: "query=foo&facetFilters=['bar']", cursor: 'cts' }, + data: { + params: "query=foo&facetFilters=['bar']", + facetQuery: 'foo', + maxFacetHits: 42, + }, }); }); }); -describe('deleteSynonym', () => { - test('deleteSynonym', async () => { - const req = await client.deleteSynonym({ +describe('searchRules', () => { + test('searchRules', async () => { + const req = await client.searchRules({ indexName: 'indexName', - objectID: 'id1', + searchRulesParams: { query: 'something' }, }); expect(req).toMatchObject({ - path: '/1/indexes/indexName/synonyms/id1', - method: 'DELETE', + path: '/1/indexes/indexName/rules/search', + method: 'POST', + data: { query: 'something' }, }); }); }); -describe('clearRules', () => { - test('clearRules', async () => { - const req = await client.clearRules({ indexName: 'indexName' }); +describe('searchSynonyms', () => { + test('searchSynonyms', async () => { + const req = await client.searchSynonyms({ + indexName: 'indexName', + query: 'queryString', + type: 'onewaysynonym', + }); expect(req).toMatchObject({ - path: '/1/indexes/indexName/rules/clear', + path: '/1/indexes/indexName/synonyms/search', method: 'POST', }); }); }); -describe('batchDictionaryEntries', () => { - test('get batchDictionaryEntries results with minimal parameters', async () => { - const req = await client.batchDictionaryEntries({ - dictionaryName: 'dictionaryName', - batchDictionaryEntries: { - requests: [ - { action: 'addEntry', body: { objectID: '1', language: 'en' } }, - { action: 'deleteEntry', body: { objectID: '2', language: 'fr' } }, - ], +describe('setDictionarySettings', () => { + test('get setDictionarySettings results with minimal parameters', async () => { + const req = await client.setDictionarySettings({ + dictionarySettingsRequest: { + disableStandardEntries: { plurals: { fr: false, en: false, ru: true } }, }, }); expect(req).toMatchObject({ - path: '/1/dictionaries/dictionaryName/batch', - method: 'POST', + path: '/1/dictionaries/*/settings', + method: 'PUT', data: { - requests: [ - { action: 'addEntry', body: { objectID: '1', language: 'en' } }, - { action: 'deleteEntry', body: { objectID: '2', language: 'fr' } }, - ], + disableStandardEntries: { plurals: { fr: false, en: false, ru: true } }, }, }); }); - test('get batchDictionaryEntries results with all parameters', async () => { - const req = await client.batchDictionaryEntries({ - dictionaryName: 'dictionaryName', - batchDictionaryEntries: { - clearExistingDictionaryEntries: false, - requests: [ - { - action: 'addEntry', - body: { - objectID: '1', - language: 'en', - word: 'yo', - words: ['yo', 'algolia'], - decomposition: ['yo', 'algolia'], - state: 'enabled', - }, - }, - { - action: 'deleteEntry', - body: { - objectID: '2', - language: 'fr', - word: 'salut', - words: ['salut', 'algolia'], - decomposition: ['salut', 'algolia'], - state: 'enabled', - }, - }, - ], + test('get setDictionarySettings results with all parameters', async () => { + const req = await client.setDictionarySettings({ + dictionarySettingsRequest: { + disableStandardEntries: { + plurals: { fr: false, en: false, ru: true }, + stopwords: { fr: false }, + compounds: { ru: true }, + }, }, }); expect(req).toMatchObject({ - path: '/1/dictionaries/dictionaryName/batch', - method: 'POST', + path: '/1/dictionaries/*/settings', + method: 'PUT', data: { - clearExistingDictionaryEntries: false, - requests: [ - { - action: 'addEntry', - body: { - objectID: '1', - language: 'en', - word: 'yo', - words: ['yo', 'algolia'], - decomposition: ['yo', 'algolia'], - state: 'enabled', - }, - }, - { - action: 'deleteEntry', - body: { - objectID: '2', - language: 'fr', - word: 'salut', - words: ['salut', 'algolia'], - decomposition: ['salut', 'algolia'], - state: 'enabled', - }, - }, - ], + disableStandardEntries: { + plurals: { fr: false, en: false, ru: true }, + stopwords: { fr: false }, + compounds: { ru: true }, + }, }, }); }); }); -describe('listApiKeys', () => { - test('listApiKeys', async () => { - const req = await client.listApiKeys(); - expect(req).toMatchObject({ - path: '/1/keys', - method: 'GET', - }); - }); -}); - -describe('saveSynonym', () => { - test('saveSynonym', async () => { - const req = await client.saveSynonym({ - indexName: 'indexName', - objectID: 'id1', - synonymHit: { - objectID: 'id1', - type: 'synonym', - synonyms: ['car', 'vehicule', 'auto'], +describe('updateApiKey', () => { + test('updateApiKey', async () => { + const req = await client.updateApiKey({ + key: 'myApiKey', + apiKey: { + acl: ['search', 'addObject'], + validity: 300, + maxQueriesPerIPPerHour: 100, + maxHitsPerQuery: 20, }, - forwardToReplicas: true, }); expect(req).toMatchObject({ - path: '/1/indexes/indexName/synonyms/id1', + path: '/1/keys/myApiKey', method: 'PUT', data: { - objectID: 'id1', - type: 'synonym', - synonyms: ['car', 'vehicule', 'auto'], + acl: ['search', 'addObject'], + validity: 300, + maxQueriesPerIPPerHour: 100, + maxHitsPerQuery: 20, }, }); }); diff --git a/tests/package.json b/tests/package.json index 9615d4dad1..57a1eddd51 100644 --- a/tests/package.json +++ b/tests/package.json @@ -9,6 +9,7 @@ }, "dependencies": { "@algolia/client-search": "5.0.0", + "@algolia/client-insights": "5.0.0", "@algolia/recommend": "5.0.0" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 2fddadd4d7..5017abf761 100644 --- a/yarn.lock +++ b/yarn.lock @@ -40,6 +40,15 @@ __metadata: languageName: unknown linkType: soft +"@algolia/client-insights@5.0.0, @algolia/client-insights@workspace:clients/algoliasearch-client-javascript/client-insights": + version: 0.0.0-use.local + resolution: "@algolia/client-insights@workspace:clients/algoliasearch-client-javascript/client-insights" + dependencies: + "@types/node": 16.11.11 + typescript: 4.5.4 + languageName: unknown + linkType: soft + "@algolia/client-personalization@5.0.0, @algolia/client-personalization@workspace:clients/algoliasearch-client-javascript/client-personalization": version: 0.0.0-use.local resolution: "@algolia/client-personalization@workspace:clients/algoliasearch-client-javascript/client-personalization" @@ -5683,6 +5692,7 @@ fsevents@^2.3.2: version: 0.0.0-use.local resolution: "tests@workspace:tests" dependencies: + "@algolia/client-insights": 5.0.0 "@algolia/client-search": 5.0.0 "@algolia/recommend": 5.0.0 "@apidevtools/swagger-parser": 10.0.3