@findlane/browser 0.11.0 → 0.13.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/README.md +4 -0
- package/dist/index.d.ts +31 -5
- package/dist/index.js +61 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,6 +60,10 @@ import "@findlane/browser/react/styles.css" // optional
|
|
|
60
60
|
|
|
61
61
|
A response has `hits` (each with its `record` and any `highlights`), `total`, `facets`, and `tookMs`. Pass `{ signal }` as a second argument to cancel a request, as search-as-you-type should.
|
|
62
62
|
|
|
63
|
+
## Events
|
|
64
|
+
|
|
65
|
+
`client.events` sends views, clicks, add-to-carts and orders in the background, for the index's Events page and Bought together. With `visitorIds: true`, they also carry random IDs for the browser and the visit, when the index takes them, and `client.events.forget()` deletes the IDs and asks for their events to be deleted. Docs: https://findlane.dev/docs/events
|
|
66
|
+
|
|
63
67
|
## Security
|
|
64
68
|
|
|
65
69
|
The public ID grants search access only to its index, and it is visible in browser source. By default the API returns every uploaded record field. `attributesToRetrieve` requests fewer fields for speed, but it does not make the others private. Origin checks constrain browsers; they do not authenticate other callers. Keep public catalog fields safe to expose. The dashboard can remove origins, add server-enforced restrictions, or rotate the ID.
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,13 @@ export type SearchRequest = {
|
|
|
2
2
|
q?: string;
|
|
3
3
|
/** A sort ID from the index settings, "relevance", or "distance" (with `near`). */
|
|
4
4
|
sort?: string;
|
|
5
|
-
/** Field values to keep
|
|
5
|
+
/** Field values to keep. Each field takes a value (a list field contains it), or an object of
|
|
6
|
+
* conditions that all apply: `in` (any of up to 100 values), `all` (a list field has every one),
|
|
7
|
+
* `not` and `notIn` (records without the field stay in), and `gte`/`lte` for numbers and dates, such
|
|
8
|
+
* as `{ tags: { in: ["kids", "adult"], notIn: ["party"] }, price: { lte: 50 } }`. `"$or"` takes one to
|
|
9
|
+
* four groups of field filters and keeps records that match any group:
|
|
10
|
+
* `{ inStock: true, $or: [{ brand: "Acme" }, { price: { lte: 20 } }] }`. Up to 20 field filters,
|
|
11
|
+
* counting those in groups. */
|
|
6
12
|
filters?: Record<string, unknown>;
|
|
7
13
|
facets?: string[];
|
|
8
14
|
/** Facets (also in `facets`) counted without the filter on their own field, so a list of choices
|
|
@@ -81,6 +87,16 @@ export type SearchResponse<T> = {
|
|
|
81
87
|
/** With the index's `didYouMean` setting: the query with the words typo tolerance corrected, when the
|
|
82
88
|
* results matched some words only by typo ("Showing results for …"), else null. */
|
|
83
89
|
didYouMean?: DidYouMean | null;
|
|
90
|
+
/** The index's query rules the query fired, in the index's order, with their `userData`, such as a banner
|
|
91
|
+
* to show or a page to go to. Left out when none fired. */
|
|
92
|
+
appliedRules?: AppliedRule[];
|
|
93
|
+
/** When a rule replaced words: the query that was searched. */
|
|
94
|
+
rewrittenQuery?: string;
|
|
95
|
+
};
|
|
96
|
+
/** A query rule a search fired, with its `userData`, if it has any. */
|
|
97
|
+
export type AppliedRule = {
|
|
98
|
+
id: string;
|
|
99
|
+
userData?: unknown;
|
|
84
100
|
};
|
|
85
101
|
/** A corrected query. `applied`: the results are already for `q` (a search's "Showing results for");
|
|
86
102
|
* otherwise searching `q` finds results that the query didn't ("Did you mean"). `changes` lists each
|
|
@@ -97,8 +113,9 @@ export type DidYouMean = {
|
|
|
97
113
|
export type RecommendationsRequest = {
|
|
98
114
|
/** Up to this many products in each list, 1 to 20; 8 by default. */
|
|
99
115
|
limit?: number;
|
|
100
|
-
/** Only picks matching these filters, as in a search, such as `{ inStock: true }`.
|
|
101
|
-
*
|
|
116
|
+
/** Only picks matching these filters, as in a search, such as `{ inStock: true }`. Each field's filter
|
|
117
|
+
* takes the same shapes; `$or` is for searches only. The index's browser access decides which fields
|
|
118
|
+
* may be filtered by. */
|
|
102
119
|
filters?: SearchRequest["filters"];
|
|
103
120
|
/** The record fields to return; by default, those the index's browser access returns. */
|
|
104
121
|
attributesToRetrieve?: string[];
|
|
@@ -147,7 +164,7 @@ export type ClickDetails = {
|
|
|
147
164
|
from?: string;
|
|
148
165
|
};
|
|
149
166
|
/** Events from the page, sent in the background: a batch a second, and what's left when the page is
|
|
150
|
-
* hidden or closed. They carry no cookie
|
|
167
|
+
* hidden or closed. They carry no cookie, and no ID unless `visitorIds` is on. */
|
|
151
168
|
export type BrowserEvents = {
|
|
152
169
|
/** A product's page was viewed. A repeat of the last view within 5 seconds is the same page view
|
|
153
170
|
* (React's double mount in development, a re-render), so it isn't sent. */
|
|
@@ -163,8 +180,12 @@ export type BrowserEvents = {
|
|
|
163
180
|
items: EventItem[];
|
|
164
181
|
currency?: string;
|
|
165
182
|
}): void;
|
|
166
|
-
/** With `events: "afterConsent"`: true sends the events held so far and those after; false drops them
|
|
183
|
+
/** With `events: "afterConsent"`: true sends the events held so far and those after; false drops them,
|
|
184
|
+
* and with `visitorIds` deletes the IDs from this device. */
|
|
167
185
|
consent(granted: boolean): void;
|
|
186
|
+
/** With `visitorIds`: deletes this browser's visitor and visit IDs from the device, and asks Findlane
|
|
187
|
+
* to delete the events sent with them, which it does within a week. Later events get new IDs. */
|
|
188
|
+
forget(): void;
|
|
168
189
|
/** Sends the events waiting now. */
|
|
169
190
|
flush(): void;
|
|
170
191
|
};
|
|
@@ -183,6 +204,11 @@ export type BrowserClientOptions = {
|
|
|
183
204
|
* `events.proxy`), such as "/findlane/events". Ad blockers block beacons to other sites, not to the
|
|
184
205
|
* page's own. */
|
|
185
206
|
eventsUrl?: string;
|
|
207
|
+
/** Whether events carry random IDs for this browser and this visit, which link one shopper's views,
|
|
208
|
+
* carts and orders. The index must take them too (its Events page). Turn it on only if your consent
|
|
209
|
+
* banner covers analytics, or your shoppers don't need one; with `events: "afterConsent"` the IDs
|
|
210
|
+
* wait for consent as events do. */
|
|
211
|
+
visitorIds?: boolean;
|
|
186
212
|
};
|
|
187
213
|
export declare class SearchBrowserClient {
|
|
188
214
|
private readonly url;
|
package/dist/index.js
CHANGED
|
@@ -8,22 +8,66 @@ export class FindlaneError extends Error {
|
|
|
8
8
|
}
|
|
9
9
|
/** The hosted Findlane API, used unless `baseUrl` says otherwise. */
|
|
10
10
|
export const defaultBaseUrl = "https://api.findlane.dev";
|
|
11
|
-
|
|
11
|
+
const day = 86_400_000;
|
|
12
|
+
/** The visitor and visit IDs of a public ID, kept on the device: the visitor's in localStorage, made
|
|
13
|
+
* anew 13 months after it was made (use doesn't extend it); the visit's in sessionStorage, made anew
|
|
14
|
+
* after 30 minutes without events. Storage that can't be used (a sandboxed frame's) means no IDs. */
|
|
15
|
+
function deviceIds(publicId) {
|
|
16
|
+
const keys = { visitor: `findlane:visitor:${publicId}`, visit: `findlane:visit:${publicId}` };
|
|
17
|
+
// 16 random bytes in base64url: letters, digits, - and _, as the API takes IDs.
|
|
18
|
+
const random = () => btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(16))))
|
|
19
|
+
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
20
|
+
const read = (storage, key) => {
|
|
21
|
+
const value = JSON.parse(storage.getItem(key) ?? "null");
|
|
22
|
+
return typeof value?.id == "string" && typeof value.at == "number" ? value : null;
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
/** The IDs for a request now, made when there are none or they're old. */
|
|
26
|
+
use(now = Date.now()) {
|
|
27
|
+
try {
|
|
28
|
+
let visitor = read(localStorage, keys.visitor);
|
|
29
|
+
if (!visitor || now - visitor.at > 395 * day)
|
|
30
|
+
localStorage.setItem(keys.visitor, JSON.stringify(visitor = { id: random(), at: now }));
|
|
31
|
+
const visit = read(sessionStorage, keys.visit);
|
|
32
|
+
const visitId = visit && now - visit.at <= 30 * 60_000 ? visit.id : random();
|
|
33
|
+
sessionStorage.setItem(keys.visit, JSON.stringify({ id: visitId, at: now }));
|
|
34
|
+
return { visitorId: visitor.id, visitId };
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
/** Deletes them from the device, and returns those there were. */
|
|
41
|
+
clear() {
|
|
42
|
+
try {
|
|
43
|
+
const ids = [read(localStorage, keys.visitor)?.id, read(sessionStorage, keys.visit)?.id].filter((id) => !!id);
|
|
44
|
+
localStorage.removeItem(keys.visitor);
|
|
45
|
+
sessionStorage.removeItem(keys.visit);
|
|
46
|
+
return ids;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function eventSender(url, fetcher, mode, ids) {
|
|
12
55
|
let queue = [];
|
|
13
56
|
let allowed = mode === "afterConsent" ? undefined : mode;
|
|
14
57
|
let timer;
|
|
15
58
|
let listening = false;
|
|
16
59
|
let lastView = { objectID: "", at: 0 };
|
|
60
|
+
const send = (body) => {
|
|
61
|
+
// A beacon outlives the page; text/plain needs no CORS preflight.
|
|
62
|
+
if (fetcher || !(typeof navigator == "object" && navigator.sendBeacon?.(url, new Blob([body], { type: "text/plain" }))))
|
|
63
|
+
(fetcher ?? fetch)(url, { method: "POST", headers: { "content-type": "text/plain" }, body, keepalive: true })
|
|
64
|
+
.catch(() => undefined);
|
|
65
|
+
};
|
|
17
66
|
const flush = () => {
|
|
18
67
|
clearTimeout(timer);
|
|
19
68
|
timer = undefined;
|
|
20
|
-
while (allowed && queue.length)
|
|
21
|
-
|
|
22
|
-
// A beacon outlives the page; text/plain needs no CORS preflight.
|
|
23
|
-
if (fetcher || !(typeof navigator == "object" && navigator.sendBeacon?.(url, new Blob([body], { type: "text/plain" }))))
|
|
24
|
-
(fetcher ?? fetch)(url, { method: "POST", headers: { "content-type": "text/plain" }, body, keepalive: true })
|
|
25
|
-
.catch(() => undefined);
|
|
26
|
-
}
|
|
69
|
+
while (allowed && queue.length)
|
|
70
|
+
send(JSON.stringify({ ...ids?.use(), events: queue.splice(0, 100) }));
|
|
27
71
|
};
|
|
28
72
|
const add = (event) => {
|
|
29
73
|
if (allowed === false)
|
|
@@ -52,8 +96,15 @@ function eventSender(url, fetcher, mode) {
|
|
|
52
96
|
allowed = granted;
|
|
53
97
|
if (granted)
|
|
54
98
|
flush();
|
|
55
|
-
else
|
|
99
|
+
else {
|
|
56
100
|
queue = [];
|
|
101
|
+
ids?.clear();
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
forget() {
|
|
105
|
+
const forgotten = ids?.clear() ?? [];
|
|
106
|
+
if (forgotten.length)
|
|
107
|
+
send(JSON.stringify({ forget: forgotten }));
|
|
57
108
|
},
|
|
58
109
|
flush,
|
|
59
110
|
};
|
|
@@ -68,7 +119,7 @@ export class SearchBrowserClient {
|
|
|
68
119
|
throw new Error("A valid public index ID (pub_…) is required");
|
|
69
120
|
this.url = `${(options.baseUrl ?? defaultBaseUrl).replace(/\/$/, "")}/api/public/indexes/${options.publicId}`;
|
|
70
121
|
this.fetcher = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
71
|
-
this.events = eventSender(options.eventsUrl ?? `${this.url}/events`, options.fetch, options.events ?? true);
|
|
122
|
+
this.events = eventSender(options.eventsUrl ?? `${this.url}/events`, options.fetch, options.events ?? true, options.visitorIds ? deviceIds(options.publicId) : null);
|
|
72
123
|
}
|
|
73
124
|
async post(operation, request, signal) {
|
|
74
125
|
const response = await this.fetcher(`${this.url}/${operation}`, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@findlane/browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Findlane browser SDK: fast, typo-tolerant public search from the browser with an index's public ID, plus React hooks and components.",
|
|
5
5
|
"keywords": ["findlane", "search", "full-text search", "typo tolerance", "instant search", "ecommerce search", "browser", "react", "autocomplete"],
|
|
6
6
|
"license": "MIT",
|