@findlane/browser 0.11.0 → 0.12.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 +11 -2
- 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
|
@@ -147,7 +147,7 @@ export type ClickDetails = {
|
|
|
147
147
|
from?: string;
|
|
148
148
|
};
|
|
149
149
|
/** 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
|
|
150
|
+
* hidden or closed. They carry no cookie, and no ID unless `visitorIds` is on. */
|
|
151
151
|
export type BrowserEvents = {
|
|
152
152
|
/** A product's page was viewed. A repeat of the last view within 5 seconds is the same page view
|
|
153
153
|
* (React's double mount in development, a re-render), so it isn't sent. */
|
|
@@ -163,8 +163,12 @@ export type BrowserEvents = {
|
|
|
163
163
|
items: EventItem[];
|
|
164
164
|
currency?: string;
|
|
165
165
|
}): void;
|
|
166
|
-
/** With `events: "afterConsent"`: true sends the events held so far and those after; false drops them
|
|
166
|
+
/** With `events: "afterConsent"`: true sends the events held so far and those after; false drops them,
|
|
167
|
+
* and with `visitorIds` deletes the IDs from this device. */
|
|
167
168
|
consent(granted: boolean): void;
|
|
169
|
+
/** With `visitorIds`: deletes this browser's visitor and visit IDs from the device, and asks Findlane
|
|
170
|
+
* to delete the events sent with them, which it does within a week. Later events get new IDs. */
|
|
171
|
+
forget(): void;
|
|
168
172
|
/** Sends the events waiting now. */
|
|
169
173
|
flush(): void;
|
|
170
174
|
};
|
|
@@ -183,6 +187,11 @@ export type BrowserClientOptions = {
|
|
|
183
187
|
* `events.proxy`), such as "/findlane/events". Ad blockers block beacons to other sites, not to the
|
|
184
188
|
* page's own. */
|
|
185
189
|
eventsUrl?: string;
|
|
190
|
+
/** Whether events carry random IDs for this browser and this visit, which link one shopper's views,
|
|
191
|
+
* carts and orders. The index must take them too (its Events page). Turn it on only if your consent
|
|
192
|
+
* banner covers analytics, or your shoppers don't need one; with `events: "afterConsent"` the IDs
|
|
193
|
+
* wait for consent as events do. */
|
|
194
|
+
visitorIds?: boolean;
|
|
186
195
|
};
|
|
187
196
|
export declare class SearchBrowserClient {
|
|
188
197
|
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.12.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",
|