@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 +8 -7
- package/dist/index.d.cts +131 -8
- package/dist/index.d.ts +131 -8
- package/dist/index.js +8 -7
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -232,7 +232,7 @@ class SearchServerClient {
|
|
|
232
232
|
await flush();
|
|
233
233
|
return result;
|
|
234
234
|
};
|
|
235
|
-
const report = (
|
|
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 (
|
|
299
|
-
if (
|
|
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(
|
|
302
|
+
if (Number(request.headers.get("content-length") ?? 0) > relayedBytes)
|
|
303
303
|
return tooLarge();
|
|
304
|
-
const body = await
|
|
304
|
+
const body = await request.text();
|
|
305
305
|
if (new TextEncoder().encode(body).length > relayedBytes)
|
|
306
306
|
return tooLarge();
|
|
307
|
-
const found = { ...requestVisitor(
|
|
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
|
|
113
|
-
*
|
|
114
|
-
*
|
|
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
|
|
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
|
|
134
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
113
|
-
*
|
|
114
|
-
*
|
|
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
|
|
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
|
|
134
|
-
*
|
|
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
|
-
/**
|
|
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 = (
|
|
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 (
|
|
253
|
-
if (
|
|
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(
|
|
256
|
+
if (Number(request.headers.get("content-length") ?? 0) > relayedBytes)
|
|
257
257
|
return tooLarge();
|
|
258
|
-
const body = await
|
|
258
|
+
const body = await request.text();
|
|
259
259
|
if (new TextEncoder().encode(body).length > relayedBytes)
|
|
260
260
|
return tooLarge();
|
|
261
|
-
const found = { ...requestVisitor(
|
|
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.
|
|
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",
|