@recapi/client 0.6.1 → 0.6.3
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 +40 -6
- package/index.d.ts +5 -1
- package/package.json +1 -1
- package/src/client.js +17 -8
- package/src/transport.js +3 -2
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ are, gzipped, and browsers cache them for a year:
|
|
|
35
35
|
|
|
36
36
|
```html
|
|
37
37
|
<script type="module">
|
|
38
|
-
import { createClient } from 'https://api.example.com/sdk/0.6.
|
|
38
|
+
import { createClient } from 'https://api.example.com/sdk/0.6.3/index.js';
|
|
39
39
|
|
|
40
40
|
const recapi = createClient({ apiKey: 'rk_pub_…', baseUrl: 'https://api.example.com' });
|
|
41
41
|
recapi.track('item-42', 'view'); // and the calls of the quick start below
|
|
@@ -327,6 +327,18 @@ repository root on any machine with Docker and python3:
|
|
|
327
327
|
./scripts/e2e_woocommerce.sh
|
|
328
328
|
```
|
|
329
329
|
|
|
330
|
+
The file's `Version:` header is that version, so a plugin fix an advisory has
|
|
331
|
+
to name needs a new SDK version (a change to the file alone doesn't move it).
|
|
332
|
+
Its `Update URI:` header, on your API's host, tells WordPress (5.8 or later;
|
|
333
|
+
the plugin needs 6.3) that its updates don't come from the WordPress.org
|
|
334
|
+
directory, so a same-named plugin listed there isn't offered as an update. The
|
|
335
|
+
last part is WordPress.org's documented behaviour, on its side of the update
|
|
336
|
+
check: the end-to-end test shows WordPress reading the header, sending it with
|
|
337
|
+
the check, asking the host's `update_plugins_{hostname}` filter and offering
|
|
338
|
+
nothing, and can't show what WordPress.org would do with a same-named listing.
|
|
339
|
+
Nothing answers that filter yet, so a shop gets no update notice. A snippet
|
|
340
|
+
install has neither header's effect: both are comments there.
|
|
341
|
+
|
|
330
342
|
### Magento
|
|
331
343
|
|
|
332
344
|
The Magento 2 module ([`sdk/php/magento`](../php/magento/README.md)) prints
|
|
@@ -493,7 +505,7 @@ for (const item of items) {
|
|
|
493
505
|
| `historyConfig` | spec defaults | `{ halfLifeSeconds, topK, recentSize }`; must match the server |
|
|
494
506
|
| `consent` | `true` | storage consent, see [Consent](#consent-and-eprivacy) |
|
|
495
507
|
| `respectDoNotTrack` | `true` | when DNT or Global Privacy Control is on: store nothing, send no events, see [Do Not Track](#do-not-track) |
|
|
496
|
-
| `onError` |
|
|
508
|
+
| `onError` | `console.warn` for lost events only | gets errors from background work (flushes, storage, rejected events) and those `recommend()` fell back from |
|
|
497
509
|
| `now`, `fetch`, `sleep` | `Date.now`, global `fetch`, `setTimeout` | for tests |
|
|
498
510
|
|
|
499
511
|
### Client methods
|
|
@@ -536,7 +548,8 @@ for (const item of items) {
|
|
|
536
548
|
were tracked with.
|
|
537
549
|
- **`setConsent(boolean)`**: grants or withdraws storage consent.
|
|
538
550
|
- **`flush()`**: sends queued events now. Never rejects. Errors go to
|
|
539
|
-
`onError
|
|
551
|
+
`onError` ([without one](#event-delivery): lost events to `console.warn`), and
|
|
552
|
+
batches that can still succeed stay queued.
|
|
540
553
|
- **`getHistory()`** returns `{ history, recent, rejected }` exactly as the
|
|
541
554
|
next request would send it (`rejected` goes into `exclude`). Use it for
|
|
542
555
|
debugging.
|
|
@@ -654,6 +667,13 @@ event quota or spend cap is reached, events are lost until it resets or
|
|
|
654
667
|
the plan changes. A 202 with some `rejected` events keeps the accepted
|
|
655
668
|
ones, and the rejected ones are reported as `event_rejected`.
|
|
656
669
|
|
|
670
|
+
Without an `onError`, events lost for good (a batch refused as above,
|
|
671
|
+
`event_rejected`, `queue_overflow`) print one `console.warn` line per error
|
|
672
|
+
code per client, so a wrong key shows in the console without flooding a
|
|
673
|
+
visitor's. Failures that can still succeed, storage errors and the errors
|
|
674
|
+
`recommend()` falls back from print nothing. With an `onError`, nothing is
|
|
675
|
+
printed.
|
|
676
|
+
|
|
657
677
|
When the page is hidden (`visibilitychange` → `hidden`, or `pagehide`),
|
|
658
678
|
the SDK sends the queue at once using `fetch` with `keepalive: true`.
|
|
659
679
|
It does not use `navigator.sendBeacon`, because a beacon cannot send the
|
|
@@ -778,23 +798,37 @@ const storage = {
|
|
|
778
798
|
setItem: (k, v) => { cache.set(k, v); AsyncStorage.setItem(k, v); },
|
|
779
799
|
removeItem: (k) => { cache.delete(k); AsyncStorage.removeItem(k); }
|
|
780
800
|
};
|
|
781
|
-
createClient({ apiKey, baseUrl, project: 'myproject', storage });
|
|
801
|
+
const client = createClient({ apiKey, baseUrl, project: 'myproject', storage });
|
|
782
802
|
```
|
|
783
803
|
|
|
804
|
+
React Native has no page-hide event, so send the queue when the app leaves
|
|
805
|
+
the screen: call `client.flush()` when `AppState` changes to `background`.
|
|
806
|
+
`popular()` and the fallback list work on React Native below 0.80 as well,
|
|
807
|
+
whose `URLSearchParams` has no working `set`. We read that class in React
|
|
808
|
+
Native's 0.79 and 0.80 sources and test against a copy of its behaviour; the
|
|
809
|
+
library has not been run on a device.
|
|
810
|
+
|
|
784
811
|
Corrupt or unrecognised stored data is discarded. Bumping the `v1` in the
|
|
785
812
|
key on a format change makes old data invisible.
|
|
786
813
|
|
|
787
814
|
## Size budget
|
|
788
815
|
|
|
789
816
|
Budget: **6 KB gzip** after minification. The sources ship unminified with
|
|
790
|
-
full JSDoc:
|
|
817
|
+
full JSDoc: 58 KB raw in five files, 20 KB as the API serves them (gzip -9),
|
|
791
818
|
and the browser caches them for a year. With comments stripped and no
|
|
792
|
-
minification they are
|
|
819
|
+
minification they are 8.5 KB gzip. A bundler's minifier should bring
|
|
793
820
|
that under the budget, but this has not been measured: the repo has no
|
|
794
821
|
minifier, to keep zero dev dependencies.
|
|
795
822
|
|
|
796
823
|
## Changes
|
|
797
824
|
|
|
825
|
+
- 2026-11-04: version 0.6.3. Without an `onError`, events lost for good (a
|
|
826
|
+
batch refused for good, `event_rejected`, `queue_overflow`) print one
|
|
827
|
+
`console.warn` line per error code per client ([Event delivery](#event-delivery)).
|
|
828
|
+
- 2026-11-03: version 0.6.2. `popular()` and the fallback list of
|
|
829
|
+
`recommend()` no longer throw on React Native below 0.80, whose
|
|
830
|
+
`URLSearchParams.set` is not implemented. Query strings are unchanged
|
|
831
|
+
everywhere else.
|
|
798
832
|
- 2026-11-02: version 0.6.1. The client draws its anonymous id at the
|
|
799
833
|
first call instead of in `createClient`, so a back end on Cloudflare
|
|
800
834
|
Workers may create it at module scope (it threw "Disallowed operation
|
package/index.d.ts
CHANGED
|
@@ -83,7 +83,11 @@ export interface ClientOptions {
|
|
|
83
83
|
* nothing, send no events and no visitor ids; recommendations still work.
|
|
84
84
|
*/
|
|
85
85
|
respectDoNotTrack?: boolean;
|
|
86
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* Receives errors from background work (flushes, storage) and those
|
|
88
|
+
* `recommend()` fell back from. Without it, events lost for good print one
|
|
89
|
+
* `console.warn` line per error code.
|
|
90
|
+
*/
|
|
87
91
|
onError?: (error: unknown) => void;
|
|
88
92
|
/** Clock in unix ms. */
|
|
89
93
|
now?: () => number;
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -84,7 +84,8 @@ const PUBLIC_KEY_PREFIX = 'rk_pub_';
|
|
|
84
84
|
* Track or Global Privacy Control (default `true`): store nothing, send no
|
|
85
85
|
* events and no visitor ids; recommendations are still asked for.
|
|
86
86
|
* @property {(error: unknown) => void} [onError] Receives background errors,
|
|
87
|
-
* and the errors `recommend()` fell back from.
|
|
87
|
+
* and the errors `recommend()` fell back from. Without it, events lost for
|
|
88
|
+
* good print one `console.warn` line per error code.
|
|
88
89
|
* @property {() => number} [now] Clock in unix ms.
|
|
89
90
|
* @property {typeof fetch} [fetch]
|
|
90
91
|
* @property {(ms: number) => Promise<void>} [sleep] Backoff sleep (tests).
|
|
@@ -115,9 +116,16 @@ export function createClient(options) {
|
|
|
115
116
|
const maxBatch = Math.min(Math.max(1, options.maxBatch ?? 100), MAX_EVENTS_PER_REQUEST);
|
|
116
117
|
const maxQueue = Math.max(MAX_EVENTS_PER_REQUEST, maxBatch);
|
|
117
118
|
const config = resolveConfig(options.historyConfig);
|
|
118
|
-
const
|
|
119
|
+
const warned = new Set();
|
|
120
|
+
// `lost`: events are gone for good. Without an onError nothing else would
|
|
121
|
+
// say so; once per code, or a visitor's console fills up.
|
|
122
|
+
const report = (error, lost) => {
|
|
119
123
|
try {
|
|
120
|
-
options.onError
|
|
124
|
+
if (options.onError) options.onError(error);
|
|
125
|
+
else if (lost && !warned.has(error.code)) {
|
|
126
|
+
warned.add(error.code);
|
|
127
|
+
globalThis.console?.warn(`recapi: events lost: ${error.code}: ${error.message}`);
|
|
128
|
+
}
|
|
121
129
|
} catch {
|
|
122
130
|
// A faulty error handler must not break tracking.
|
|
123
131
|
}
|
|
@@ -242,7 +250,7 @@ export function createClient(options) {
|
|
|
242
250
|
|
|
243
251
|
function dropOverflow() {
|
|
244
252
|
const dropped = queue.splice(0, queue.length - maxQueue).length;
|
|
245
|
-
report(new RecapiError('queue_overflow', `dropped ${dropped} oldest events`));
|
|
253
|
+
report(new RecapiError('queue_overflow', `dropped ${dropped} oldest events`), true);
|
|
246
254
|
}
|
|
247
255
|
|
|
248
256
|
function schedule() {
|
|
@@ -266,13 +274,13 @@ export function createClient(options) {
|
|
|
266
274
|
report(new RecapiError('event_rejected', `${response.rejected.length} event(s) rejected`, {
|
|
267
275
|
status: 202,
|
|
268
276
|
details: response.rejected.map((r) => ({ ...r, event: events[r.index] }))
|
|
269
|
-
}));
|
|
277
|
+
}), true);
|
|
270
278
|
}
|
|
271
279
|
}
|
|
272
280
|
|
|
273
281
|
/** Puts a failed batch back at the head of the queue if it may succeed later. */
|
|
274
282
|
function handleSendError(error, events) {
|
|
275
|
-
report(error);
|
|
283
|
+
report(error, !error.retryable);
|
|
276
284
|
if (error instanceof RecapiError && error.retryable) {
|
|
277
285
|
queue.unshift(...events);
|
|
278
286
|
if (queue.length > maxQueue) dropOverflow();
|
|
@@ -314,8 +322,9 @@ export function createClient(options) {
|
|
|
314
322
|
}
|
|
315
323
|
|
|
316
324
|
/**
|
|
317
|
-
* Sends all queued events. Never rejects: failures go to `onError
|
|
318
|
-
*
|
|
325
|
+
* Sends all queued events. Never rejects: failures go to `onError` (without
|
|
326
|
+
* one, events lost for good to `console.warn`), and retryable batches stay
|
|
327
|
+
* queued for the next automatic flush.
|
|
319
328
|
*
|
|
320
329
|
* @returns {Promise<void>}
|
|
321
330
|
*/
|
package/src/transport.js
CHANGED
|
@@ -78,11 +78,12 @@ export function createTransport({ baseUrl, apiKey, fetch, timeoutMs: defaultTime
|
|
|
78
78
|
|
|
79
79
|
/** @param {RequestOptions} options */
|
|
80
80
|
async function request({ method, path, query, body, auth = 'header', keepalive = false, cache, timeoutMs }) {
|
|
81
|
+
// append, not set: React Native below 0.80 throws on set, and these names are unique.
|
|
81
82
|
const params = new URLSearchParams();
|
|
82
83
|
for (const [name, value] of Object.entries(query ?? {})) {
|
|
83
|
-
if (value !== undefined) params.
|
|
84
|
+
if (value !== undefined) params.append(name, String(value));
|
|
84
85
|
}
|
|
85
|
-
if (auth === 'query') params.
|
|
86
|
+
if (auth === 'query') params.append('key', apiKey);
|
|
86
87
|
const qs = params.toString();
|
|
87
88
|
const url = `${root}${path}${qs ? `?${qs}` : ''}`;
|
|
88
89
|
|