@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 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.3.0/index.js';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@recapi/client",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Browser and Node client for the recapi recommendation API: local decayed history, event batching, attribution.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
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. `metadata` (the shop's own
589
- * object, set with the item) is there only when the item has some, so an
590
- * item without it keeps the shape it had before metadata existed.
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
  }