@recapi/client 0.4.0 → 0.5.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 +41 -7
- package/index.d.ts +4 -1
- package/package.json +1 -1
- package/src/client.js +18 -6
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.5.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
|
|
@@ -103,7 +103,15 @@ in two places, both shown with your address in the dashboard's quick start:
|
|
|
103
103
|
so they are unchanged. Each card shows the picture (resized by Shopify's
|
|
104
104
|
CDN with `?width=400`), the title and `price_min` formatted with
|
|
105
105
|
`Intl.NumberFormat` in `Shopify.currency.active` and the page's `lang`
|
|
106
|
-
(no price if either is missing).
|
|
106
|
+
(no price if either is missing). An item whose
|
|
107
|
+
[metadata](../../docs/api.md#item-metadata) has a `name` and an http(s)
|
|
108
|
+
`url` skips that lookup: its card is drawn from `name`, `url`, `picture`
|
|
109
|
+
(http(s) only) and `price` (text shown as it is, or a number in the
|
|
110
|
+
shop's currency, `{{ shop.currency }}`, shown only to visitors browsing in
|
|
111
|
+
it; others get Shopify's converted price from the lookup). A row whose
|
|
112
|
+
items all carry metadata then costs one request instead of seven. The
|
|
113
|
+
snippet can't send metadata: the shop sends it with `PUT /v1/items`, and
|
|
114
|
+
marks a sold-out product inactive there, since no lookup checks it. A default style makes them a product row
|
|
107
115
|
in the theme's colours and fonts; all its selectors sit inside
|
|
108
116
|
`:where(#recapi-recommendations …)`, which has no specificity, so any of
|
|
109
117
|
the theme's rules overrides it. It follows the shop's cookie banner through
|
|
@@ -224,7 +232,11 @@ shortcode shows none unless given one: `[recapi heading="You may also like"]`.
|
|
|
224
232
|
The widget fills it like the Shopify snippet's, once it nears the screen,
|
|
225
233
|
loading the 6 recommended products in one request to WooCommerce's public Store API
|
|
226
234
|
(`/wc/store/v1/products?include=…`) and skipping those not purchasable or
|
|
227
|
-
out of stock.
|
|
235
|
+
out of stock. Items whose [metadata](../../docs/api.md#item-metadata)
|
|
236
|
+
has a `name` and an http(s) `url` are left out of that request and drawn
|
|
237
|
+
from it, as in the Shopify snippet (a number `price` is in the shop's
|
|
238
|
+
currency, `woocommerce_currency`, and shown only while the visitor browses
|
|
239
|
+
in it), so a row whose items all carry metadata needs no Store API call. Consent comes from the
|
|
228
240
|
[WP Consent API](https://wordpress.org/plugins/wp-consent-api/)
|
|
229
241
|
(`wp_has_consent('statistics')`, `wp_listen_for_consent_change`), which
|
|
230
242
|
banner plugins feed and which reports allowed where no banner manages
|
|
@@ -256,7 +268,10 @@ The Magento 2 module ([`sdk/php/magento`](../php/magento/README.md)) prints
|
|
|
256
268
|
the SDK on every storefront page for views and add-to-carts, and sends
|
|
257
269
|
orders from the server with the PHP library. With consent, its script
|
|
258
270
|
mirrors the visitor's id and clicked recommendations into a first-party
|
|
259
|
-
cookie so the server's purchases are credited like the SDK's own.
|
|
271
|
+
cookie so the server's purchases are credited like the SDK's own. On product
|
|
272
|
+
pages it fills recapi's row, drawing each card from the item's `metadata`
|
|
273
|
+
or, without it, from the store; the row is printed with the store's own
|
|
274
|
+
products, which stay while recapi has none.
|
|
260
275
|
|
|
261
276
|
### Node
|
|
262
277
|
|
|
@@ -310,7 +325,7 @@ for (const item of items) {
|
|
|
310
325
|
| `timeoutMs` | `5000` | per-request timeout; `recommend()` waits 1.5 s at most |
|
|
311
326
|
| `historyConfig` | spec defaults | `{ halfLifeSeconds, topK, recentSize }`; must match the server |
|
|
312
327
|
| `consent` | `true` | storage consent, see [Consent](#consent-and-eprivacy) |
|
|
313
|
-
| `respectDoNotTrack` | `
|
|
328
|
+
| `respectDoNotTrack` | `true` | when DNT or Global Privacy Control is on: store nothing, send no events, see [Do Not Track](#do-not-track) |
|
|
314
329
|
| `onError` | – | gets errors from background work (flushes, storage, rejected events) and those `recommend()` fell back from |
|
|
315
330
|
| `now`, `fetch`, `sleep` | `Date.now`, global `fetch`, `setTimeout` | for tests |
|
|
316
331
|
|
|
@@ -550,8 +565,21 @@ if (rec !== null) {
|
|
|
550
565
|
Events are still sent, without `anon_id`. Don't call `track()` if you
|
|
551
566
|
need no network at all. Withdrawing consent deletes the stored data.
|
|
552
567
|
Granting it merges the in-memory history into storage.
|
|
553
|
-
|
|
554
|
-
|
|
568
|
+
|
|
569
|
+
### Do Not Track
|
|
570
|
+
|
|
571
|
+
When the browser sends Do Not Track (`navigator.doNotTrack === '1'`) or
|
|
572
|
+
Global Privacy Control (`navigator.globalPrivacyControl === true`), the
|
|
573
|
+
SDK follows nobody: it reads and writes no storage, `track()` and
|
|
574
|
+
`identify()` do nothing, and `setConsent(true)` doesn't lift it (the
|
|
575
|
+
browser's setting is the visitor's standing answer). `recommend()` and
|
|
576
|
+
`popular()` still work: the request carries no visitor id and no history,
|
|
577
|
+
so the list is popular, trending or similar items (with `contextItemId`).
|
|
578
|
+
The Shopify and WooCommerce snippets and Magento's storefront script
|
|
579
|
+
build on the SDK and behave the same (Magento's order observer runs on the
|
|
580
|
+
server and still sends the purchase). A site with its own legal basis for tracking those visitors
|
|
581
|
+
passes `respectDoNotTrack: false` and gets the behaviour of 0.4.0 and
|
|
582
|
+
earlier.
|
|
555
583
|
|
|
556
584
|
## Custom storage
|
|
557
585
|
|
|
@@ -585,6 +613,12 @@ minifier, to keep zero dev dependencies.
|
|
|
585
613
|
|
|
586
614
|
## Changes
|
|
587
615
|
|
|
616
|
+
- 2026-11-01: version 0.5.0. Do Not Track and Global Privacy Control are
|
|
617
|
+
respected by default: the SDK then stores nothing and sends no events or
|
|
618
|
+
visitor ids, and still asks for recommendations
|
|
619
|
+
([Do Not Track](#do-not-track)). Before, `respectDoNotTrack` was off by
|
|
620
|
+
default and only stopped storage. Snippets pasted with an older version
|
|
621
|
+
get it too: older versions' paths redirect to the current one.
|
|
588
622
|
- 2026-10-31: version 0.4.0. Recommended and popular items carry `why`,
|
|
589
623
|
a reason code you can turn into a label for visitors
|
|
590
624
|
([Showing why an item is listed](#showing-why-an-item-is-listed)).
|
package/index.d.ts
CHANGED
|
@@ -78,7 +78,10 @@ export interface ClientOptions {
|
|
|
78
78
|
historyConfig?: Partial<HistoryConfig>;
|
|
79
79
|
/** Initial storage consent, default `true`. */
|
|
80
80
|
consent?: boolean;
|
|
81
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Default `true`: when Do Not Track / Global Privacy Control is on, store
|
|
83
|
+
* nothing, send no events and no visitor ids; recommendations still work.
|
|
84
|
+
*/
|
|
82
85
|
respectDoNotTrack?: boolean;
|
|
83
86
|
/** Receives errors from background work (flushes, storage) and those `recommend()` fell back from. */
|
|
84
87
|
onError?: (error: unknown) => void;
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -80,8 +80,9 @@ const PUBLIC_KEY_PREFIX = 'rk_pub_';
|
|
|
80
80
|
* @property {number} [timeoutMs] Per-request timeout.
|
|
81
81
|
* @property {Partial<HistoryConfig>} [historyConfig]
|
|
82
82
|
* @property {boolean} [consent] Initial storage consent (default `true`).
|
|
83
|
-
* @property {boolean} [respectDoNotTrack]
|
|
84
|
-
*
|
|
83
|
+
* @property {boolean} [respectDoNotTrack] When the browser sends Do Not
|
|
84
|
+
* Track or Global Privacy Control (default `true`): store nothing, send no
|
|
85
|
+
* events and no visitor ids; recommendations are still asked for.
|
|
85
86
|
* @property {(error: unknown) => void} [onError] Receives background errors,
|
|
86
87
|
* and the errors `recommend()` fell back from.
|
|
87
88
|
* @property {() => number} [now] Clock in unix ms.
|
|
@@ -127,8 +128,14 @@ export function createClient(options) {
|
|
|
127
128
|
report
|
|
128
129
|
);
|
|
129
130
|
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
// DNT and GPC ask sites not to follow the visitor, and the homepage
|
|
132
|
+
// promises our script listens: such a page sends recapi nothing about them
|
|
133
|
+
// and keeps nothing on the device, so its lists are popular or similar
|
|
134
|
+
// items only. A banner's consent doesn't lift it: the browser's answer is
|
|
135
|
+
// the visitor's standing one. `respectDoNotTrack: false` is for a site with
|
|
136
|
+
// its own legal basis.
|
|
137
|
+
const quiet = (options.respectDoNotTrack ?? true) && doNotTrackEnabled();
|
|
138
|
+
let consent = !quiet && (options.consent ?? true);
|
|
132
139
|
|
|
133
140
|
/** @type {HistoryState} */
|
|
134
141
|
let history = emptyHistory();
|
|
@@ -400,6 +407,7 @@ export function createClient(options) {
|
|
|
400
407
|
* `reject` is -10). A negative level is an explicit rejection ("not
|
|
401
408
|
* interested"): the item is never recommended to this user again until
|
|
402
409
|
* a positive event on it. Don't send skips or bounces as rejections.
|
|
410
|
+
* Does nothing under Do Not Track (see `respectDoNotTrack`).
|
|
403
411
|
* @param {{ recommendationId?: string | null }} [options] Overrides the
|
|
404
412
|
* automatic attribution. `null` (a fallback list's id) is no override.
|
|
405
413
|
*/
|
|
@@ -415,6 +423,7 @@ export function createClient(options) {
|
|
|
415
423
|
"level must be an integer between -10 and 10, or 'view', 'cart', 'purchase' or 'reject'"
|
|
416
424
|
);
|
|
417
425
|
}
|
|
426
|
+
if (quiet) return;
|
|
418
427
|
const t = now();
|
|
419
428
|
let attributed = recommendationId;
|
|
420
429
|
mutate(() => {
|
|
@@ -492,12 +501,14 @@ export function createClient(options) {
|
|
|
492
501
|
* `GET /v1/users/{id}/history`, fetched by your backend with the secret
|
|
493
502
|
* key) and merges it into the local history. Only pass a snapshot that
|
|
494
503
|
* does not already contain this device's events, or they count twice.
|
|
504
|
+
* Does nothing under Do Not Track: requests name nobody.
|
|
495
505
|
*
|
|
496
506
|
* @param {string} id
|
|
497
507
|
* @param {{ history?: ServerSnapshot }} [options]
|
|
498
508
|
*/
|
|
499
509
|
identify(id, { history: snapshot } = {}) {
|
|
500
510
|
if (!isId(id)) throw new RecapiError('invalid_request', 'userId must be 1-256 bytes of UTF-8 without control characters');
|
|
511
|
+
if (quiet) return;
|
|
501
512
|
mutate(() => {
|
|
502
513
|
if (userId && userId !== id) {
|
|
503
514
|
history = emptyHistory();
|
|
@@ -531,12 +542,13 @@ export function createClient(options) {
|
|
|
531
542
|
* consent nothing is read from or written to storage and no anonymous id
|
|
532
543
|
* exists; the history lives in memory for the current page only.
|
|
533
544
|
* Withdrawing deletes the stored data. Granting merges any previously
|
|
534
|
-
* stored history with the in-memory one.
|
|
545
|
+
* stored history with the in-memory one. Does nothing under Do Not Track:
|
|
546
|
+
* nothing is stored whatever the banner says.
|
|
535
547
|
*
|
|
536
548
|
* @param {boolean} value
|
|
537
549
|
*/
|
|
538
550
|
setConsent(value) {
|
|
539
|
-
if (value === consent) return;
|
|
551
|
+
if (quiet || value === consent) return;
|
|
540
552
|
consent = Boolean(value);
|
|
541
553
|
if (!consent) {
|
|
542
554
|
store.remove();
|