@findlane/server 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -232,7 +232,7 @@ class SearchServerClient {
232
232
  await flush();
233
233
  return result;
234
234
  };
235
- const report = (name2, options) => retry(() => request(`/analytics/${name2}${analyticsQuery(options)}`, "GET", undefined, options.signal), options.signal);
235
+ const report = (name, options) => retry(() => request(`/analytics/${name}${analyticsQuery(options)}`, "GET", undefined, options.signal), options.signal);
236
236
  const stagePath = (stageId) => `/staged-replacements/${encodeURIComponent(stageId)}`;
237
237
  const beginReplacement = (stageId, signal) => retry(() => request(stagePath(stageId), "POST", undefined, signal), signal);
238
238
  const uploadReplacementBatch = (stageId, batchId, records, signal) => retry(() => request(`${stagePath(stageId)}/batches/${encodeURIComponent(batchId)}`, "POST", { records }, signal), signal);
@@ -295,16 +295,16 @@ class SearchServerClient {
295
295
  forgotten += (await postEvents({ forget: ids.slice(start, start + 1000) }, signal)).forgotten ?? 0;
296
296
  return { forgotten };
297
297
  },
298
- proxy: async (request2, visitor = {}) => {
299
- if (request2.method !== "POST")
298
+ proxy: async (request, visitor = {}) => {
299
+ if (request.method !== "POST")
300
300
  return jsonResponse(JSON.stringify({ error: "Only POST is allowed" }), 405, { allow: "POST" });
301
301
  const tooLarge = () => jsonResponse(JSON.stringify({ error: "The request is too large" }), 413);
302
- if (Number(request2.headers.get("content-length") ?? 0) > relayedBytes)
302
+ if (Number(request.headers.get("content-length") ?? 0) > relayedBytes)
303
303
  return tooLarge();
304
- const body = await request2.text();
304
+ const body = await request.text();
305
305
  if (new TextEncoder().encode(body).length > relayedBytes)
306
306
  return tooLarge();
307
- const found = { ...requestVisitor(request2), ...visitor };
307
+ const found = { ...requestVisitor(request), ...visitor };
308
308
  if (!found.ip)
309
309
  throw new Error("No visitor IP address in the request's headers; pass it: events.proxy(request, { ip })");
310
310
  return relayEvents(body, { ...found, ip: found.ip });
@@ -318,7 +318,8 @@ class SearchServerClient {
318
318
  events: (options = {}) => report("events", options),
319
319
  products: (options = {}) => report("products", options),
320
320
  productCounts: (options = {}) => report("product-counts", options),
321
- product: (objectID, options = {}) => report(`products/${encodeURIComponent(objectID)}`, options)
321
+ product: (objectID, options = {}) => report(`products/${encodeURIComponent(objectID)}`, options),
322
+ revenue: (options = {}) => report("revenue", options)
322
323
  },
323
324
  upsert: (records, signal) => request("/records", "POST", { records }, signal),
324
325
  getRecord: (objectID, signal) => request(`/records/${encodeURIComponent(objectID)}`, "GET", undefined, signal),
package/dist/index.d.cts CHANGED
@@ -77,6 +77,18 @@ export type IndexSettings = {
77
77
  normalizationProfile?: "default-v2" | "default-v1" | "icelandic-ascii-v1";
78
78
  /** An object field of lat and lng numbers, such as `_geoloc`, that `near` and `within` search; null for none. */
79
79
  locationField?: string | null;
80
+ /** Shows each value of a field once, such as a product whose colours or sizes are records sharing its ID:
81
+ * the record (or up to `limit`, 1 to 4) that best matches the search. A text or number field with one value;
82
+ * `total` then counts groups. Null for none. */
83
+ distinct?: {
84
+ field: string;
85
+ limit?: number;
86
+ } | null;
87
+ /** Variants listed inside each record, such as a product's flavours: a list of objects at `field`. Each hit
88
+ * then says which one the search matched (`variant`). Null for none. */
89
+ variants?: {
90
+ field: string;
91
+ } | null;
80
92
  /** When saving: types for fields no record has yet, so settings can come before the records. Records'
81
93
  * types win once they have the field. Not returned when reading settings. */
82
94
  fieldTypes?: Record<string, "text" | "number" | "boolean" | "text[]" | "number[]" | "boolean[]" | "location">;
@@ -109,9 +121,11 @@ export type QuerySuggestion = {
109
121
  q: string;
110
122
  typed?: [number, number];
111
123
  };
112
- /** When a query is, starts with, or contains one of a rule's phrases, the rule's `then` applies to that
113
- * search. Phrases are compared as synonyms are: without case, accents, or the index languages' stop words,
114
- * and with their plurals and other spellings, so a rule for "balloon" also fires for "balloons". */
124
+ /** When a search meets a rule's `if`, the rule's `then` applies to that search: its words (one of the
125
+ * phrases), its filters (such as a category page's), the contexts it sends, and whether it has words; only
126
+ * between the rule's `active` times when it has them. Phrases are compared as synonyms are: without case,
127
+ * accents, or the index languages' stop words, and with their plurals and other spellings, so a rule for
128
+ * "balloon" also fires for "balloons". */
115
129
  export type QueryRule = {
116
130
  /** 1 to 64 letters, digits, `_`, `.`, `:`, or `-`, unique in the index. */
117
131
  id: string;
@@ -119,25 +133,46 @@ export type QueryRule = {
119
133
  description?: string;
120
134
  /** false keeps the rule without applying it. Reading settings always returns it; true when left out. */
121
135
  enabled?: boolean;
136
+ /** Fires only from `from` and before `until`, ISO 8601 times with their zone, such as
137
+ * "2026-11-27T00:00:00Z"; either may be left out (0.20.0). */
138
+ active?: {
139
+ from?: string;
140
+ until?: string;
141
+ };
142
+ /** Every condition given must hold; with none, the rule fires on every search. */
122
143
  if: {
123
- /** 1 to 20 phrases of one to four words. */
124
- phrases: string[];
144
+ /** 1 to 20 phrases of one to four words. Reading settings returns [] for a rule without phrases. */
145
+ phrases?: string[];
125
146
  /** The query is one of the phrases, starts with one, or contains one; "contains" when left out. */
126
147
  match?: "is" | "startsWith" | "contains";
148
+ /** The search filters each field (1 to 10 filter fields) to one of these values, alone or in an `in`
149
+ * list, as a category page filters its category (0.20.0). */
150
+ filters?: Record<string, string | number | boolean | Array<string | number | boolean>>;
151
+ /** The search sends one of these contexts (its `context`), 1 to 10 names (0.20.0). */
152
+ context?: string[];
153
+ /** For a rule without phrases: only searches without words, such as a category page ("none"), or only
154
+ * with them ("some") (0.20.0). */
155
+ words?: "none" | "some";
127
156
  };
128
157
  /** One or more of these. `filters`, `boost` and `bury` take a search's filters, on 1 to 10 fields. */
129
158
  then: {
130
159
  /** Added to the search's filters, except on fields the search (or an earlier rule) already filters.
131
160
  * Records without the field drop out, so `boost` is usually the better choice. */
132
161
  filters?: SearchRequest["filters"];
133
- /** Records matching these come before the others, after `rankFirst`'s groups, in searches with a query
134
- * sorted by relevance. Several rules' boosts add up. */
162
+ /** Records matching these come before the others, after `rankFirst`'s groups, in searches sorted by
163
+ * relevance, with words or without (0.20.0), but not named sorts or distance. Several rules' boosts add up. */
135
164
  boost?: SearchRequest["filters"];
136
165
  /** Records matching these come after the others, in the same searches as `boost`. */
137
166
  bury?: SearchRequest["filters"];
138
167
  /** Up to 100 objectIDs left out of the results. */
139
168
  hide?: string[];
140
- /** Words searched instead of the matched phrase, up to 64 characters; "" removes the phrase. */
169
+ /** 1 to 20 records at places from 1, on every sort, when they pass the search's filters (not necessarily its
170
+ * words); a later rule's pin at a taken place moves down (0.20.0). */
171
+ pin?: {
172
+ objectID: string;
173
+ position: number;
174
+ }[];
175
+ /** With phrases: words searched instead of the matched phrase, up to 64 characters; "" removes the phrase. */
141
176
  replace?: string;
142
177
  /** JSON up to 4 KB returned with the results in `appliedRules`, such as a banner or a redirect. */
143
178
  userData?: unknown;
@@ -345,6 +380,15 @@ export type SearchRequest = {
345
380
  within?: GeoBox;
346
381
  /** With the index's `relatedResults` on: false asks for no related results (0.18.0). */
347
382
  related?: boolean;
383
+ /** With the index's `distinct` setting: false shows every record, a number 1 to 4 how many of each group's
384
+ * records. */
385
+ distinct?: boolean | number;
386
+ /** Contexts the page sends, which rules with a `context` condition fire on, such as "category-page": a
387
+ * name or up to 10, each 1 to 64 letters, digits, `_`, `:` or `-` (0.20.0). */
388
+ context?: string | string[];
389
+ /** Checks rules as at this time, in milliseconds since 1970, such as a campaign's start, instead of now
390
+ * (0.20.0). Browser search doesn't take it. */
391
+ rulesAt?: number;
348
392
  };
349
393
  /** A point in degrees: lat from -90 to 90, lng from -180 to 180. */
350
394
  export type GeoPoint = {
@@ -425,8 +469,25 @@ export type SearchResponse<T> = {
425
469
  highlights?: Record<string, string | string[]>;
426
470
  /** With `explain`: why the hit matched and what ordered it. */
427
471
  explain?: HitExplanation;
472
+ /** With the index's `distinct` setting: the hit's group (its value of the group field, null for a record
473
+ * without one) and how many of the group's records matched. */
474
+ group?: {
475
+ value: string | number | null;
476
+ matched: number;
477
+ };
478
+ /** With the index's `variants` setting: which item of the record's variants list the search matched, by
479
+ * its words or by filters on the variants' fields, such as the flavour a shopper typed. Absent when none
480
+ * stands out. */
481
+ variant?: {
482
+ index: number;
483
+ };
484
+ /** A rule pinned it at its place (0.20.0). */
485
+ pinned?: true;
428
486
  }[];
487
+ /** The matches; with the index's `distinct` setting, the groups. */
429
488
  total: number;
489
+ /** With `distinct`: the matching records, which `total` counts as groups. */
490
+ totalRecords?: number;
430
491
  /** "gte" when `total` is a lower bound (page count mode). */
431
492
  totalRelation?: "eq" | "gte";
432
493
  hasMore?: boolean;
@@ -854,6 +915,65 @@ export type ProductReport = AnalyticsReport & {
854
915
  together: number;
855
916
  };
856
917
  };
918
+ /** A query's search clicks, and the add-to-carts and orders of the products clicked, in the main currency.
919
+ * `searches` is its searches when it's one of the 1,000 most searched, else null. `typed`: shorter queries
920
+ * typed on the way to it, such as "perfor", whose clicks count here. */
921
+ export type RevenueQuery = {
922
+ query: string;
923
+ searches: number | null;
924
+ clicks: number;
925
+ carts: number;
926
+ orders: number;
927
+ revenue: number;
928
+ typed: string[];
929
+ };
930
+ /** A product's orders and their value, beside those found through search, and the query it was found by most. */
931
+ export type RevenueProduct = {
932
+ objectID: string;
933
+ title: string;
934
+ image: string | null;
935
+ orders: number;
936
+ revenue: number;
937
+ searchOrders: number;
938
+ searchRevenue: number;
939
+ topQuery: string | null;
940
+ };
941
+ /** Revenue from search (0.20.0): purchases and add-to-carts of products the shopper clicked in search results in
942
+ * the 24 hours before, by the same visitor ID, so it needs the index's visitor IDs on and purchases sent as
943
+ * events. Each order counts once; amounts are in the main `currency` (the most orders'), at the prices the
944
+ * events carry, and other currencies' orders are listed apart. Exact, from every event kept, like `products`. */
945
+ export type RevenueReport = AnalyticsReport & {
946
+ computedAt: string;
947
+ currency: string | null;
948
+ orders: number;
949
+ revenue: number;
950
+ searchOrders: number;
951
+ searchRevenue: number;
952
+ carts: number;
953
+ cartValue: number;
954
+ searchCarts: number;
955
+ searchCartValue: number;
956
+ searchClicks: number;
957
+ otherCurrencies: {
958
+ currency: string;
959
+ orders: number;
960
+ revenue: number;
961
+ searchRevenue: number;
962
+ }[];
963
+ /** Orders and search clicks with visitor IDs, the first linked search click in the window, whether the
964
+ * index takes visitor IDs, and purchase lines without a price. */
965
+ coverage: {
966
+ linkedOrders: number;
967
+ linkedClicks: number;
968
+ linkedSince: string | null;
969
+ visitorIds: boolean;
970
+ unpricedLines: number;
971
+ };
972
+ /** The queries, the most revenue first, up to 100. */
973
+ queries: RevenueQuery[];
974
+ /** The products bought after a search, the most revenue from search first, up to 50. */
975
+ products: RevenueProduct[];
976
+ };
857
977
  export declare class FindlaneError extends Error {
858
978
  status: number;
859
979
  constructor(status: number, message: string);
@@ -986,6 +1106,9 @@ export declare class SearchServerClient {
986
1106
  product: (objectID: string, options?: AnalyticsOptions & {
987
1107
  interval?: "hour" | "day";
988
1108
  }) => Promise<ProductReport>;
1109
+ /** Revenue from search: orders and add-to-carts of products found through search, per query and per
1110
+ * product (0.20.0). */
1111
+ revenue: (options?: Omit<AnalyticsOptions, "source">) => Promise<RevenueReport>;
989
1112
  };
990
1113
  upsert: (records: T[], signal?: AbortSignal) => Promise<WriteReceipt>;
991
1114
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<T>>;
package/dist/index.d.ts CHANGED
@@ -77,6 +77,18 @@ export type IndexSettings = {
77
77
  normalizationProfile?: "default-v2" | "default-v1" | "icelandic-ascii-v1";
78
78
  /** An object field of lat and lng numbers, such as `_geoloc`, that `near` and `within` search; null for none. */
79
79
  locationField?: string | null;
80
+ /** Shows each value of a field once, such as a product whose colours or sizes are records sharing its ID:
81
+ * the record (or up to `limit`, 1 to 4) that best matches the search. A text or number field with one value;
82
+ * `total` then counts groups. Null for none. */
83
+ distinct?: {
84
+ field: string;
85
+ limit?: number;
86
+ } | null;
87
+ /** Variants listed inside each record, such as a product's flavours: a list of objects at `field`. Each hit
88
+ * then says which one the search matched (`variant`). Null for none. */
89
+ variants?: {
90
+ field: string;
91
+ } | null;
80
92
  /** When saving: types for fields no record has yet, so settings can come before the records. Records'
81
93
  * types win once they have the field. Not returned when reading settings. */
82
94
  fieldTypes?: Record<string, "text" | "number" | "boolean" | "text[]" | "number[]" | "boolean[]" | "location">;
@@ -109,9 +121,11 @@ export type QuerySuggestion = {
109
121
  q: string;
110
122
  typed?: [number, number];
111
123
  };
112
- /** When a query is, starts with, or contains one of a rule's phrases, the rule's `then` applies to that
113
- * search. Phrases are compared as synonyms are: without case, accents, or the index languages' stop words,
114
- * and with their plurals and other spellings, so a rule for "balloon" also fires for "balloons". */
124
+ /** When a search meets a rule's `if`, the rule's `then` applies to that search: its words (one of the
125
+ * phrases), its filters (such as a category page's), the contexts it sends, and whether it has words; only
126
+ * between the rule's `active` times when it has them. Phrases are compared as synonyms are: without case,
127
+ * accents, or the index languages' stop words, and with their plurals and other spellings, so a rule for
128
+ * "balloon" also fires for "balloons". */
115
129
  export type QueryRule = {
116
130
  /** 1 to 64 letters, digits, `_`, `.`, `:`, or `-`, unique in the index. */
117
131
  id: string;
@@ -119,25 +133,46 @@ export type QueryRule = {
119
133
  description?: string;
120
134
  /** false keeps the rule without applying it. Reading settings always returns it; true when left out. */
121
135
  enabled?: boolean;
136
+ /** Fires only from `from` and before `until`, ISO 8601 times with their zone, such as
137
+ * "2026-11-27T00:00:00Z"; either may be left out (0.20.0). */
138
+ active?: {
139
+ from?: string;
140
+ until?: string;
141
+ };
142
+ /** Every condition given must hold; with none, the rule fires on every search. */
122
143
  if: {
123
- /** 1 to 20 phrases of one to four words. */
124
- phrases: string[];
144
+ /** 1 to 20 phrases of one to four words. Reading settings returns [] for a rule without phrases. */
145
+ phrases?: string[];
125
146
  /** The query is one of the phrases, starts with one, or contains one; "contains" when left out. */
126
147
  match?: "is" | "startsWith" | "contains";
148
+ /** The search filters each field (1 to 10 filter fields) to one of these values, alone or in an `in`
149
+ * list, as a category page filters its category (0.20.0). */
150
+ filters?: Record<string, string | number | boolean | Array<string | number | boolean>>;
151
+ /** The search sends one of these contexts (its `context`), 1 to 10 names (0.20.0). */
152
+ context?: string[];
153
+ /** For a rule without phrases: only searches without words, such as a category page ("none"), or only
154
+ * with them ("some") (0.20.0). */
155
+ words?: "none" | "some";
127
156
  };
128
157
  /** One or more of these. `filters`, `boost` and `bury` take a search's filters, on 1 to 10 fields. */
129
158
  then: {
130
159
  /** Added to the search's filters, except on fields the search (or an earlier rule) already filters.
131
160
  * Records without the field drop out, so `boost` is usually the better choice. */
132
161
  filters?: SearchRequest["filters"];
133
- /** Records matching these come before the others, after `rankFirst`'s groups, in searches with a query
134
- * sorted by relevance. Several rules' boosts add up. */
162
+ /** Records matching these come before the others, after `rankFirst`'s groups, in searches sorted by
163
+ * relevance, with words or without (0.20.0), but not named sorts or distance. Several rules' boosts add up. */
135
164
  boost?: SearchRequest["filters"];
136
165
  /** Records matching these come after the others, in the same searches as `boost`. */
137
166
  bury?: SearchRequest["filters"];
138
167
  /** Up to 100 objectIDs left out of the results. */
139
168
  hide?: string[];
140
- /** Words searched instead of the matched phrase, up to 64 characters; "" removes the phrase. */
169
+ /** 1 to 20 records at places from 1, on every sort, when they pass the search's filters (not necessarily its
170
+ * words); a later rule's pin at a taken place moves down (0.20.0). */
171
+ pin?: {
172
+ objectID: string;
173
+ position: number;
174
+ }[];
175
+ /** With phrases: words searched instead of the matched phrase, up to 64 characters; "" removes the phrase. */
141
176
  replace?: string;
142
177
  /** JSON up to 4 KB returned with the results in `appliedRules`, such as a banner or a redirect. */
143
178
  userData?: unknown;
@@ -345,6 +380,15 @@ export type SearchRequest = {
345
380
  within?: GeoBox;
346
381
  /** With the index's `relatedResults` on: false asks for no related results (0.18.0). */
347
382
  related?: boolean;
383
+ /** With the index's `distinct` setting: false shows every record, a number 1 to 4 how many of each group's
384
+ * records. */
385
+ distinct?: boolean | number;
386
+ /** Contexts the page sends, which rules with a `context` condition fire on, such as "category-page": a
387
+ * name or up to 10, each 1 to 64 letters, digits, `_`, `:` or `-` (0.20.0). */
388
+ context?: string | string[];
389
+ /** Checks rules as at this time, in milliseconds since 1970, such as a campaign's start, instead of now
390
+ * (0.20.0). Browser search doesn't take it. */
391
+ rulesAt?: number;
348
392
  };
349
393
  /** A point in degrees: lat from -90 to 90, lng from -180 to 180. */
350
394
  export type GeoPoint = {
@@ -425,8 +469,25 @@ export type SearchResponse<T> = {
425
469
  highlights?: Record<string, string | string[]>;
426
470
  /** With `explain`: why the hit matched and what ordered it. */
427
471
  explain?: HitExplanation;
472
+ /** With the index's `distinct` setting: the hit's group (its value of the group field, null for a record
473
+ * without one) and how many of the group's records matched. */
474
+ group?: {
475
+ value: string | number | null;
476
+ matched: number;
477
+ };
478
+ /** With the index's `variants` setting: which item of the record's variants list the search matched, by
479
+ * its words or by filters on the variants' fields, such as the flavour a shopper typed. Absent when none
480
+ * stands out. */
481
+ variant?: {
482
+ index: number;
483
+ };
484
+ /** A rule pinned it at its place (0.20.0). */
485
+ pinned?: true;
428
486
  }[];
487
+ /** The matches; with the index's `distinct` setting, the groups. */
429
488
  total: number;
489
+ /** With `distinct`: the matching records, which `total` counts as groups. */
490
+ totalRecords?: number;
430
491
  /** "gte" when `total` is a lower bound (page count mode). */
431
492
  totalRelation?: "eq" | "gte";
432
493
  hasMore?: boolean;
@@ -854,6 +915,65 @@ export type ProductReport = AnalyticsReport & {
854
915
  together: number;
855
916
  };
856
917
  };
918
+ /** A query's search clicks, and the add-to-carts and orders of the products clicked, in the main currency.
919
+ * `searches` is its searches when it's one of the 1,000 most searched, else null. `typed`: shorter queries
920
+ * typed on the way to it, such as "perfor", whose clicks count here. */
921
+ export type RevenueQuery = {
922
+ query: string;
923
+ searches: number | null;
924
+ clicks: number;
925
+ carts: number;
926
+ orders: number;
927
+ revenue: number;
928
+ typed: string[];
929
+ };
930
+ /** A product's orders and their value, beside those found through search, and the query it was found by most. */
931
+ export type RevenueProduct = {
932
+ objectID: string;
933
+ title: string;
934
+ image: string | null;
935
+ orders: number;
936
+ revenue: number;
937
+ searchOrders: number;
938
+ searchRevenue: number;
939
+ topQuery: string | null;
940
+ };
941
+ /** Revenue from search (0.20.0): purchases and add-to-carts of products the shopper clicked in search results in
942
+ * the 24 hours before, by the same visitor ID, so it needs the index's visitor IDs on and purchases sent as
943
+ * events. Each order counts once; amounts are in the main `currency` (the most orders'), at the prices the
944
+ * events carry, and other currencies' orders are listed apart. Exact, from every event kept, like `products`. */
945
+ export type RevenueReport = AnalyticsReport & {
946
+ computedAt: string;
947
+ currency: string | null;
948
+ orders: number;
949
+ revenue: number;
950
+ searchOrders: number;
951
+ searchRevenue: number;
952
+ carts: number;
953
+ cartValue: number;
954
+ searchCarts: number;
955
+ searchCartValue: number;
956
+ searchClicks: number;
957
+ otherCurrencies: {
958
+ currency: string;
959
+ orders: number;
960
+ revenue: number;
961
+ searchRevenue: number;
962
+ }[];
963
+ /** Orders and search clicks with visitor IDs, the first linked search click in the window, whether the
964
+ * index takes visitor IDs, and purchase lines without a price. */
965
+ coverage: {
966
+ linkedOrders: number;
967
+ linkedClicks: number;
968
+ linkedSince: string | null;
969
+ visitorIds: boolean;
970
+ unpricedLines: number;
971
+ };
972
+ /** The queries, the most revenue first, up to 100. */
973
+ queries: RevenueQuery[];
974
+ /** The products bought after a search, the most revenue from search first, up to 50. */
975
+ products: RevenueProduct[];
976
+ };
857
977
  export declare class FindlaneError extends Error {
858
978
  status: number;
859
979
  constructor(status: number, message: string);
@@ -986,6 +1106,9 @@ export declare class SearchServerClient {
986
1106
  product: (objectID: string, options?: AnalyticsOptions & {
987
1107
  interval?: "hour" | "day";
988
1108
  }) => Promise<ProductReport>;
1109
+ /** Revenue from search: orders and add-to-carts of products found through search, per query and per
1110
+ * product (0.20.0). */
1111
+ revenue: (options?: Omit<AnalyticsOptions, "source">) => Promise<RevenueReport>;
989
1112
  };
990
1113
  upsert: (records: T[], signal?: AbortSignal) => Promise<WriteReceipt>;
991
1114
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<T>>;
package/dist/index.js CHANGED
@@ -186,7 +186,7 @@ class SearchServerClient {
186
186
  await flush();
187
187
  return result;
188
188
  };
189
- const report = (name2, options) => retry(() => request(`/analytics/${name2}${analyticsQuery(options)}`, "GET", undefined, options.signal), options.signal);
189
+ const report = (name, options) => retry(() => request(`/analytics/${name}${analyticsQuery(options)}`, "GET", undefined, options.signal), options.signal);
190
190
  const stagePath = (stageId) => `/staged-replacements/${encodeURIComponent(stageId)}`;
191
191
  const beginReplacement = (stageId, signal) => retry(() => request(stagePath(stageId), "POST", undefined, signal), signal);
192
192
  const uploadReplacementBatch = (stageId, batchId, records, signal) => retry(() => request(`${stagePath(stageId)}/batches/${encodeURIComponent(batchId)}`, "POST", { records }, signal), signal);
@@ -249,16 +249,16 @@ class SearchServerClient {
249
249
  forgotten += (await postEvents({ forget: ids.slice(start, start + 1000) }, signal)).forgotten ?? 0;
250
250
  return { forgotten };
251
251
  },
252
- proxy: async (request2, visitor = {}) => {
253
- if (request2.method !== "POST")
252
+ proxy: async (request, visitor = {}) => {
253
+ if (request.method !== "POST")
254
254
  return jsonResponse(JSON.stringify({ error: "Only POST is allowed" }), 405, { allow: "POST" });
255
255
  const tooLarge = () => jsonResponse(JSON.stringify({ error: "The request is too large" }), 413);
256
- if (Number(request2.headers.get("content-length") ?? 0) > relayedBytes)
256
+ if (Number(request.headers.get("content-length") ?? 0) > relayedBytes)
257
257
  return tooLarge();
258
- const body = await request2.text();
258
+ const body = await request.text();
259
259
  if (new TextEncoder().encode(body).length > relayedBytes)
260
260
  return tooLarge();
261
- const found = { ...requestVisitor(request2), ...visitor };
261
+ const found = { ...requestVisitor(request), ...visitor };
262
262
  if (!found.ip)
263
263
  throw new Error("No visitor IP address in the request's headers; pass it: events.proxy(request, { ip })");
264
264
  return relayEvents(body, { ...found, ip: found.ip });
@@ -272,7 +272,8 @@ class SearchServerClient {
272
272
  events: (options = {}) => report("events", options),
273
273
  products: (options = {}) => report("products", options),
274
274
  productCounts: (options = {}) => report("product-counts", options),
275
- product: (objectID, options = {}) => report(`products/${encodeURIComponent(objectID)}`, options)
275
+ product: (objectID, options = {}) => report(`products/${encodeURIComponent(objectID)}`, options),
276
+ revenue: (options = {}) => report("revenue", options)
276
277
  },
277
278
  upsert: (records, signal) => request("/records", "POST", { records }, signal),
278
279
  getRecord: (objectID, signal) => request(`/records/${encodeURIComponent(objectID)}`, "GET", undefined, signal),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@findlane/server",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "Findlane server SDK: upload catalogs, manage index settings, and search with a secret workspace API key.",
5
5
  "keywords": ["findlane", "search", "full-text search", "typo tolerance", "ecommerce search", "search api", "sdk"],
6
6
  "license": "MIT",