@recapi/client 0.5.0 → 0.6.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 +51 -3
- package/index.d.ts +4 -1
- package/package.json +1 -1
- package/src/client.js +25 -2
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ are, gzipped, and browsers cache them for a year:
|
|
|
16
16
|
|
|
17
17
|
```html
|
|
18
18
|
<script type="module">
|
|
19
|
-
import { createClient } from 'https://api.example.com/sdk/0.
|
|
19
|
+
import { createClient } from 'https://api.example.com/sdk/0.6.0/index.js';
|
|
20
20
|
|
|
21
21
|
const recapi = createClient({ apiKey: 'rk_pub_…', baseUrl: 'https://api.example.com' });
|
|
22
22
|
recapi.track('item-42', 'view'); // and the calls of the quick start below
|
|
@@ -245,6 +245,31 @@ does when its consent API fails to load. It declares compatibility with
|
|
|
245
245
|
WooCommerce's order tables (HPOS) and checkout blocks when installed as a
|
|
246
246
|
plugin, and registers with the WP Consent API.
|
|
247
247
|
|
|
248
|
+
With a secret key pasted on WooCommerce → recapi (stored as the
|
|
249
|
+
`recapi_secret_key` option, not autoloaded, and never printed back), the
|
|
250
|
+
plugin describes the shop's products itself. Each product changed in a
|
|
251
|
+
request (`woocommerce_new_product`, `woocommerce_update_product`, a stock
|
|
252
|
+
status change, a variation's change as its parent's, a status change such as
|
|
253
|
+
trashing or a scheduled product going live, a deletion) is read again at
|
|
254
|
+
the end of the request and sent once the page is out, as one
|
|
255
|
+
`PUT /v1/items/{item_id}` or one `POST /v1/items:batch`: one try of at most
|
|
256
|
+
2 s through WordPress's HTTP API, a failure logged to WooCommerce → Status →
|
|
257
|
+
Logs (source `recapi`), so a recapi outage never fails a save. A WP-Cron
|
|
258
|
+
event (`recapi_sync`, daily, first run when the key is saved) sends the
|
|
259
|
+
whole catalog, 200 products read and 1,000 sent at a time, oldest first,
|
|
260
|
+
with three tries per batch against an outage; the page shows when it last
|
|
261
|
+
ran, how many products it sent and why any were refused. Every item is
|
|
262
|
+
complete: `active` only while the product is published, not hidden from the
|
|
263
|
+
catalog, purchasable and in stock (the Store API cards' rule), its category
|
|
264
|
+
names as `tags` (not "Uncategorized"), its creation date as `published_at`,
|
|
265
|
+
and `name`, `url`, `picture` (the `woocommerce_thumbnail` size) and `price`
|
|
266
|
+
(as the shop displays it, in its currency) as `metadata`, shrunk below
|
|
267
|
+
2,048 bytes as the Magento module does (picture first, then the name). The
|
|
268
|
+
row then draws every card from it, with no Store API call. Only items that
|
|
269
|
+
changed are billed. The plugin uses WordPress's HTTP API rather than the PHP
|
|
270
|
+
library so it stays one file (a snippet holds it) and follows the site's
|
|
271
|
+
proxy settings.
|
|
272
|
+
|
|
248
273
|
Have past orders? Download them from WooCommerce → recapi and upload the
|
|
249
274
|
file on Import data. The plugin writes the last 90 days of processing and
|
|
250
275
|
completed orders as the dashboard's `user_id,item_id,level,timestamp` CSV,
|
|
@@ -302,7 +327,7 @@ come):
|
|
|
302
327
|
```js
|
|
303
328
|
const LABELS = {
|
|
304
329
|
for_you: 'Picked for you', similar: 'Similar items', next: 'Up next',
|
|
305
|
-
trending: 'Trending now', popular: 'Popular', new: 'New',
|
|
330
|
+
bought_together: 'Often bought together', trending: 'Trending now', popular: 'Popular', new: 'New',
|
|
306
331
|
};
|
|
307
332
|
for (const item of items) {
|
|
308
333
|
const label = LABELS[item.why]; // undefined for explore, more and future codes: no label
|
|
@@ -373,6 +398,9 @@ for (const item of items) {
|
|
|
373
398
|
- **`getHistory()`** returns `{ history, recent, rejected }` exactly as the
|
|
374
399
|
next request would send it (`rejected` goes into `exclude`). Use it for
|
|
375
400
|
debugging.
|
|
401
|
+
- **`getAnonId()`** returns this device's anonymous id, the `anon_id` its
|
|
402
|
+
events carry, or `null` without consent and under Do Not Track. See
|
|
403
|
+
[Login and logout](#login-and-logout).
|
|
376
404
|
- **`destroy()`** removes listeners and timers, then flushes.
|
|
377
405
|
|
|
378
406
|
### Errors
|
|
@@ -516,7 +544,19 @@ so a batch already in flight when the user navigates away still arrives.
|
|
|
516
544
|
from `GET /v1/users/{id}/history`. Merging adds up the decayed scores
|
|
517
545
|
item by item. Use it only when the snapshot does not already contain
|
|
518
546
|
this device's events, typically right after login. Otherwise those
|
|
519
|
-
events count twice.
|
|
547
|
+
events count twice. The snapshot also holds the anonymous browsing of
|
|
548
|
+
the devices the user logged in on before, this one included when it was
|
|
549
|
+
not `reset()` at logout. Send `getAnonId()` with the login request and
|
|
550
|
+
have your backend ask for `GET /v1/users/{id}/history?anon_id=…`: the
|
|
551
|
+
snapshot then leaves out this device's own browsing.
|
|
552
|
+
|
|
553
|
+
```js
|
|
554
|
+
// Browser, at login:
|
|
555
|
+
const res = await fetch('/login', { method: 'POST', body: JSON.stringify({ ...form, anonId: recapi.getAnonId() }) });
|
|
556
|
+
recapi.identify(userId, { history: (await res.json()).recapiHistory });
|
|
557
|
+
// Backend, with the secret key:
|
|
558
|
+
// GET /v1/users/{userId}/history?anon_id={anonId} (leave anon_id out when it is null)
|
|
559
|
+
```
|
|
520
560
|
- Merge rule for the recent lists: take the union of both lists and order
|
|
521
561
|
it by interaction time, oldest first. An item in both lists takes its
|
|
522
562
|
latest time. Then keep the last 20.
|
|
@@ -613,6 +653,14 @@ minifier, to keep zero dev dependencies.
|
|
|
613
653
|
|
|
614
654
|
## Changes
|
|
615
655
|
|
|
656
|
+
- 2026-11-01: version 0.6.0. `getAnonId()` returns the device's anonymous
|
|
657
|
+
id, for the backend to fetch the login snapshot without this device's own
|
|
658
|
+
browsing (`?anon_id=`), which would otherwise count twice on a device not
|
|
659
|
+
reset at logout.
|
|
660
|
+
- 2026-11-01: version 0.5.1. Without consent, and under Do Not Track, the
|
|
661
|
+
SDK no longer touches `localStorage` at all: before, it wrote and removed a
|
|
662
|
+
probe key when the client was created, to check that storage works. Now
|
|
663
|
+
that check waits for the first read or write, which only consent allows.
|
|
616
664
|
- 2026-11-01: version 0.5.0. Do Not Track and Global Privacy Control are
|
|
617
665
|
respected by default: the SDK then stores nothing and sends no events or
|
|
618
666
|
visitor ids, and still asks for recommendations
|
package/index.d.ts
CHANGED
|
@@ -104,7 +104,8 @@ export interface RecommendParams {
|
|
|
104
104
|
|
|
105
105
|
/** An item's reason code: map the ones you want to your own label, show none for an unknown one. */
|
|
106
106
|
export type ReasonCode =
|
|
107
|
-
| 'for_you' | 'similar' | 'next' | 'trending' | 'popular' | 'new' | 'explore' | 'more'
|
|
107
|
+
| 'for_you' | 'similar' | 'next' | 'bought_together' | 'trending' | 'popular' | 'new' | 'explore' | 'more'
|
|
108
|
+
| (string & {});
|
|
108
109
|
|
|
109
110
|
/** The object stored with the item (`PUT /v1/items`, `metadata`): name, picture, link, price... */
|
|
110
111
|
export type ItemMetadata = Record<string, unknown>;
|
|
@@ -159,6 +160,8 @@ export interface RecapiClient {
|
|
|
159
160
|
/** Never rejects; failures go to `onError`. */
|
|
160
161
|
flush(): Promise<void>;
|
|
161
162
|
getHistory(): WireHistory;
|
|
163
|
+
/** This device's anonymous id; null without consent or under Do Not Track. */
|
|
164
|
+
getAnonId(): string | null;
|
|
162
165
|
destroy(): Promise<void>;
|
|
163
166
|
}
|
|
164
167
|
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -121,8 +121,16 @@ export function createClient(options) {
|
|
|
121
121
|
}
|
|
122
122
|
};
|
|
123
123
|
const transport = createTransport({ baseUrl, apiKey, fetch: fetchImpl, timeoutMs });
|
|
124
|
+
// The default adapter probes localStorage with a write, so it is picked at
|
|
125
|
+
// the first storage access: never before consent, nor under Do Not Track.
|
|
126
|
+
let adapter = options.storage;
|
|
127
|
+
const storage = () => (adapter ??= defaultStorage(report));
|
|
124
128
|
const store = createStore(
|
|
125
|
-
|
|
129
|
+
{
|
|
130
|
+
getItem: (key) => storage().getItem(key),
|
|
131
|
+
setItem: (key, value) => storage().setItem(key, value),
|
|
132
|
+
removeItem: (key) => storage().removeItem(key)
|
|
133
|
+
},
|
|
126
134
|
storageKey(options.project ?? apiKey.slice(0, 16)),
|
|
127
135
|
decodePersisted,
|
|
128
136
|
report
|
|
@@ -500,7 +508,8 @@ export function createClient(options) {
|
|
|
500
508
|
* `options.history` takes a server snapshot (from
|
|
501
509
|
* `GET /v1/users/{id}/history`, fetched by your backend with the secret
|
|
502
510
|
* key) and merges it into the local history. Only pass a snapshot that
|
|
503
|
-
* does not already contain this device's events, or they count twice
|
|
511
|
+
* does not already contain this device's events, or they count twice:
|
|
512
|
+
* fetch it with this device's `getAnonId()`.
|
|
504
513
|
* Does nothing under Do Not Track: requests name nobody.
|
|
505
514
|
*
|
|
506
515
|
* @param {string} id
|
|
@@ -580,6 +589,20 @@ export function createClient(options) {
|
|
|
580
589
|
return toWire(history, now(), config);
|
|
581
590
|
},
|
|
582
591
|
|
|
592
|
+
/**
|
|
593
|
+
* This device's anonymous id, the `anon_id` its events carry: null
|
|
594
|
+
* without consent or under Do Not Track. Send it with the login request,
|
|
595
|
+
* so your backend asks `GET /v1/users/{id}/history?anon_id=…` for the
|
|
596
|
+
* snapshot it passes to `identify()`: that snapshot then leaves out this
|
|
597
|
+
* device's browsing, which the local history holds already.
|
|
598
|
+
*
|
|
599
|
+
* @returns {string | null}
|
|
600
|
+
*/
|
|
601
|
+
getAnonId() {
|
|
602
|
+
if (consent) reload();
|
|
603
|
+
return anonId;
|
|
604
|
+
},
|
|
605
|
+
|
|
583
606
|
/**
|
|
584
607
|
* Removes listeners and timers, then flushes what is queued.
|
|
585
608
|
*
|