@recapi/client 0.3.0 → 0.4.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 +33 -5
- package/index.d.ts +7 -1
- package/package.json +1 -1
- package/src/client.js +9 -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.4.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
|
|
@@ -64,7 +64,7 @@ const observer = new IntersectionObserver(async (entries) => {
|
|
|
64
64
|
if (!entries.some((entry) => entry.isIntersecting)) return;
|
|
65
65
|
observer.disconnect();
|
|
66
66
|
const { items } = await recapi.recommend({ count: 8, tagsAny: ['sports'] });
|
|
67
|
-
render(slot, items); // [{ itemId, score, slot, metadata }]
|
|
67
|
+
render(slot, items); // [{ itemId, score, slot, why, metadata }]
|
|
68
68
|
}, { rootMargin: '0px 0px 400px 0px' });
|
|
69
69
|
observer.observe(slot);
|
|
70
70
|
|
|
@@ -250,6 +250,14 @@ stands in for WordPress, run by `scripts/php.sh`) and runs the printed
|
|
|
250
250
|
script against the SDK. The quick start fills in the current version and
|
|
251
251
|
your address (`jssdk.WooCommerce`).
|
|
252
252
|
|
|
253
|
+
### Magento
|
|
254
|
+
|
|
255
|
+
The Magento 2 module ([`sdk/php/magento`](../php/magento/README.md)) prints
|
|
256
|
+
the SDK on every storefront page for views and add-to-carts, and sends
|
|
257
|
+
orders from the server with the PHP library. With consent, its script
|
|
258
|
+
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.
|
|
260
|
+
|
|
253
261
|
### Node
|
|
254
262
|
|
|
255
263
|
```js
|
|
@@ -269,6 +277,23 @@ await recapi.destroy(); // flushes queued events before exit
|
|
|
269
277
|
Timers do not keep a Node process alive. Call `await client.flush()` or
|
|
270
278
|
`await client.destroy()` before exiting, or queued events are lost.
|
|
271
279
|
|
|
280
|
+
### Showing why an item is listed
|
|
281
|
+
|
|
282
|
+
Every item carries `why`, a short [reason code](../../docs/api.md#reason-codes)
|
|
283
|
+
that names no item of the visitor's history, so any page can show it. Pick
|
|
284
|
+
your own words, and show no label for a code you don't know (new ones may
|
|
285
|
+
come):
|
|
286
|
+
|
|
287
|
+
```js
|
|
288
|
+
const LABELS = {
|
|
289
|
+
for_you: 'Picked for you', similar: 'Similar items', next: 'Up next',
|
|
290
|
+
trending: 'Trending now', popular: 'Popular', new: 'New',
|
|
291
|
+
};
|
|
292
|
+
for (const item of items) {
|
|
293
|
+
const label = LABELS[item.why]; // undefined for explore, more and future codes: no label
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
272
297
|
## API
|
|
273
298
|
|
|
274
299
|
### `createClient(options)`
|
|
@@ -308,17 +333,17 @@ Timers do not keep a Node process alive. Call `await client.flush()` or
|
|
|
308
333
|
control characters. A stored history entry whose id breaks the rule,
|
|
309
334
|
left by an earlier SDK version, is dropped when the page loads.
|
|
310
335
|
- **`recommend({ count, tagsAny, tagsAll, exclude, contextItemId, placement, discovery })`**
|
|
311
|
-
resolves to `{ recommendationId, fallback, items: [{ itemId, score, slot, metadata }] }`.
|
|
336
|
+
resolves to `{ recommendationId, fallback, items: [{ itemId, score, slot, why, metadata }] }`.
|
|
312
337
|
`metadata` is the object you stored with the item
|
|
313
338
|
([Item metadata](../../docs/api.md#item-metadata): name, picture, link,
|
|
314
339
|
price…), absent when it has none, so you can render the list without a
|
|
315
|
-
lookup.
|
|
340
|
+
lookup. `why` is the item's [reason code](#showing-why-an-item-is-listed).
|
|
316
341
|
It never retries and waits at most 1.5 s (less if `timeoutMs` is lower):
|
|
317
342
|
a late recommendation is worthless. See [Fallback](#fallback) for what
|
|
318
343
|
happens when the call fails. The user's rejected items (the newest 200)
|
|
319
344
|
are added to `exclude`, after yours, up to the API's limit of 500. The SDK does not send `explain`, which needs
|
|
320
345
|
a secret key: call the API from your backend for that.
|
|
321
|
-
- **`popular({ tagsAny, tagsAll, count })`** resolves to `{ items: [{ itemId, score, metadata }] }`.
|
|
346
|
+
- **`popular({ tagsAny, tagsAll, count })`** resolves to `{ items: [{ itemId, score, why, metadata }] }`.
|
|
322
347
|
A public key goes in `?key=`, so the response can be cached by a CDN and
|
|
323
348
|
the request needs no CORS preflight. A secret key (Node) goes in the
|
|
324
349
|
`Authorization` header, because the API refuses a secret key in a URL.
|
|
@@ -560,6 +585,9 @@ minifier, to keep zero dev dependencies.
|
|
|
560
585
|
|
|
561
586
|
## Changes
|
|
562
587
|
|
|
588
|
+
- 2026-10-31: version 0.4.0. Recommended and popular items carry `why`,
|
|
589
|
+
a reason code you can turn into a label for visitors
|
|
590
|
+
([Showing why an item is listed](#showing-why-an-item-is-listed)).
|
|
563
591
|
- 2026-10-31: version 0.3.0. Recommended and popular items carry the
|
|
564
592
|
item's `metadata` (name, picture, link, price… stored with
|
|
565
593
|
`PUT /v1/items`) when it has some, fallback lists included
|
package/index.d.ts
CHANGED
|
@@ -99,6 +99,10 @@ export interface RecommendParams {
|
|
|
99
99
|
discovery?: number;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/** An item's reason code: map the ones you want to your own label, show none for an unknown one. */
|
|
103
|
+
export type ReasonCode =
|
|
104
|
+
| 'for_you' | 'similar' | 'next' | 'trending' | 'popular' | 'new' | 'explore' | 'more' | (string & {});
|
|
105
|
+
|
|
102
106
|
/** The object stored with the item (`PUT /v1/items`, `metadata`): name, picture, link, price... */
|
|
103
107
|
export type ItemMetadata = Record<string, unknown>;
|
|
104
108
|
|
|
@@ -106,6 +110,8 @@ export interface RecommendedItem {
|
|
|
106
110
|
itemId: string;
|
|
107
111
|
score: number;
|
|
108
112
|
slot: 'exploit' | 'explore' | 'backfill' | string;
|
|
113
|
+
/** Why the item is listed, to label it for visitors (docs/api.md, "Reason codes"). New codes may be added. */
|
|
114
|
+
why?: ReasonCode;
|
|
109
115
|
/** Absent when the item has no metadata. */
|
|
110
116
|
metadata?: ItemMetadata;
|
|
111
117
|
}
|
|
@@ -130,7 +136,7 @@ export interface PopularParams {
|
|
|
130
136
|
}
|
|
131
137
|
|
|
132
138
|
export interface PopularResult {
|
|
133
|
-
items: Array<{ itemId: string; score: number; metadata?: ItemMetadata }>;
|
|
139
|
+
items: Array<{ itemId: string; score: number; why?: ReasonCode; metadata?: ItemMetadata }>;
|
|
134
140
|
}
|
|
135
141
|
|
|
136
142
|
/**
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -448,7 +448,7 @@ export function createClient(options) {
|
|
|
448
448
|
* @param {{ count?: number, tagsAny?: string[], tagsAll?: string[], exclude?: string[],
|
|
449
449
|
* contextItemId?: string, placement?: string, discovery?: number }} [params]
|
|
450
450
|
* @returns {Promise<{ recommendationId: string | null, fallback: boolean,
|
|
451
|
-
* items: Array<{ itemId: string, score: number, slot: string, metadata?: object }> }>}
|
|
451
|
+
* items: Array<{ itemId: string, score: number, slot: string, why?: string, metadata?: object }> }>}
|
|
452
452
|
*/
|
|
453
453
|
async recommend(params = {}) {
|
|
454
454
|
const deadline = Math.min(timeoutMs, RECOMMEND_TIMEOUT_MS);
|
|
@@ -476,7 +476,7 @@ export function createClient(options) {
|
|
|
476
476
|
* preflight; a secret key stays in the `Authorization` header.
|
|
477
477
|
*
|
|
478
478
|
* @param {{ tagsAny?: string[], tagsAll?: string[], count?: number }} [params]
|
|
479
|
-
* @returns {Promise<{ items: Array<{ itemId: string, score: number, metadata?: object }> }>}
|
|
479
|
+
* @returns {Promise<{ items: Array<{ itemId: string, score: number, why?: string, metadata?: object }> }>}
|
|
480
480
|
*/
|
|
481
481
|
async popular(params = {}) {
|
|
482
482
|
const items = await fetchPopular(params);
|
|
@@ -585,12 +585,15 @@ export function createClient(options) {
|
|
|
585
585
|
}
|
|
586
586
|
|
|
587
587
|
/**
|
|
588
|
-
* An item of a list as the SDK returns it. `
|
|
589
|
-
* object, set with the item)
|
|
590
|
-
*
|
|
588
|
+
* An item of a list as the SDK returns it. `why` (the reason code) and
|
|
589
|
+
* `metadata` (the shop's own object, set with the item) are there only when
|
|
590
|
+
* the API sent them, so an item keeps the shape it had before they existed.
|
|
591
591
|
*/
|
|
592
|
-
function toItem({ item_id: itemId, score, metadata }, slot) {
|
|
592
|
+
function toItem({ item_id: itemId, score, why, metadata }, slot) {
|
|
593
593
|
const item = slot === undefined ? { itemId, score } : { itemId, score, slot };
|
|
594
|
+
if (typeof why === 'string' && why !== '') {
|
|
595
|
+
item.why = why;
|
|
596
|
+
}
|
|
594
597
|
if (metadata !== null && typeof metadata === 'object' && !Array.isArray(metadata) && Object.keys(metadata).length > 0) {
|
|
595
598
|
item.metadata = metadata;
|
|
596
599
|
}
|