@scoperat/tracker 0.3.1 → 0.4.1
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 +9 -8
- package/dist/index.d.ts +30 -0
- package/dist/index.js +197 -52
- package/dist/index.js.map +1 -1
- package/dist/widget/index.js +197 -52
- package/dist/widget/index.js.map +1 -1
- package/dist/widget.global.js +97 -96
- package/dist/widget.global.js.map +1 -1
- package/package.json +29 -17
package/README.md
CHANGED
|
@@ -32,13 +32,14 @@ scoperat.reset();
|
|
|
32
32
|
|
|
33
33
|
Initialize the tracker. Must be called before any other method.
|
|
34
34
|
|
|
35
|
-
| Option | Type | Default | Description
|
|
36
|
-
| --------------- | ------------------------- | ------------------------ |
|
|
37
|
-
| `endpoint` | `string` | `window.location.origin` | API base URL
|
|
38
|
-
| `flushAt` | `number` | `10` | Flush after this many events
|
|
39
|
-
| `flushInterval` | `number` | `3000` | Auto-flush interval in ms
|
|
40
|
-
| `timeout` | `number` | `10000` | Request timeout in ms
|
|
41
|
-
| `
|
|
35
|
+
| Option | Type | Default | Description |
|
|
36
|
+
| --------------- | ------------------------- | ------------------------ | ------------------------------------ |
|
|
37
|
+
| `endpoint` | `string` | `window.location.origin` | API base URL |
|
|
38
|
+
| `flushAt` | `number` | `10` | Flush after this many events |
|
|
39
|
+
| `flushInterval` | `number` | `3000` | Auto-flush interval in ms |
|
|
40
|
+
| `timeout` | `number` | `10000` | Request timeout in ms |
|
|
41
|
+
| `maxQueueSize` | `number` | `1000` | Max buffered events (oldest dropped) |
|
|
42
|
+
| `onError` | `(error, events) => void` | — | Error callback |
|
|
42
43
|
|
|
43
44
|
### `scoperat.identify(userId, traits?)`
|
|
44
45
|
|
|
@@ -65,7 +66,7 @@ Flush remaining events and stop the auto-flush timer. Call before app teardown.
|
|
|
65
66
|
- **Auto-batching** — events are buffered and sent in batches for efficiency
|
|
66
67
|
- **Anonymous IDs** — persistent anonymous ID via `localStorage`, auto-generated on first visit
|
|
67
68
|
- **sendBeacon fallback** — uses `navigator.sendBeacon` on page hide for reliable delivery
|
|
68
|
-
- **Retry on failure** —
|
|
69
|
+
- **Retry on failure** — transient failures (network errors, 5xx, 408/429) are re-queued and retried with exponential backoff; non-retryable 4xx batches are dropped
|
|
69
70
|
- **Zero dependencies** — no runtime dependencies
|
|
70
71
|
- **Tree-shakeable** — marked as side-effect-free for optimal bundling
|
|
71
72
|
- **ESM-only** — ships as pure ES modules
|
package/dist/index.d.ts
CHANGED
|
@@ -37,6 +37,12 @@ interface InitOptions {
|
|
|
37
37
|
flushInterval?: number;
|
|
38
38
|
timeout?: number;
|
|
39
39
|
onError?: (error: Error, events: TrackEvent[]) => void;
|
|
40
|
+
/**
|
|
41
|
+
* Maximum number of events buffered while the ingest API is
|
|
42
|
+
* unreachable. When full, the oldest non-error event is dropped
|
|
43
|
+
* (surfaced through `onError`). Defaults to 1000.
|
|
44
|
+
*/
|
|
45
|
+
maxQueueSize?: number;
|
|
40
46
|
/** Session inactivity timeout in ms. Defaults to 30 minutes. */
|
|
41
47
|
sessionTimeout?: number;
|
|
42
48
|
/** Automatically capture uncaught errors and unhandled rejections. Defaults to false. */
|
|
@@ -108,8 +114,16 @@ declare class TrackerImpl {
|
|
|
108
114
|
private timeout;
|
|
109
115
|
private onError?;
|
|
110
116
|
private buffer;
|
|
117
|
+
private maxQueueSize;
|
|
111
118
|
private timer;
|
|
112
119
|
private visibilityHandler;
|
|
120
|
+
private pageHideHandler;
|
|
121
|
+
private uninstallErrorHandlers;
|
|
122
|
+
/** Consecutive flush failures, drives exponential retry backoff. */
|
|
123
|
+
private flushFailures;
|
|
124
|
+
/** Timestamp before which automatic flushes are suppressed (backoff). */
|
|
125
|
+
private nextFlushAllowedAt;
|
|
126
|
+
private exceptionFlushScheduled;
|
|
113
127
|
private userId;
|
|
114
128
|
private orgId;
|
|
115
129
|
private traits;
|
|
@@ -188,7 +202,22 @@ declare class TrackerImpl {
|
|
|
188
202
|
addBreadcrumb(category: string, message: string, data?: Record<string, unknown>): void;
|
|
189
203
|
track(event: string, properties?: Record<string, unknown>): void;
|
|
190
204
|
track(event: TrackEvent): void;
|
|
205
|
+
/**
|
|
206
|
+
* Push an event onto the buffer, evicting the oldest event when the
|
|
207
|
+
* queue is full. `$exception` events are preferentially retained.
|
|
208
|
+
*/
|
|
209
|
+
private enqueue;
|
|
210
|
+
/** Flush unless we're inside a retry-backoff window. */
|
|
211
|
+
private maybeFlush;
|
|
212
|
+
private scheduleExceptionFlush;
|
|
191
213
|
flush(): Promise<FlushResult>;
|
|
214
|
+
/**
|
|
215
|
+
* Record a failed flush and push out the next automatic flush with
|
|
216
|
+
* exponential backoff (capped) so a down ingest endpoint isn't
|
|
217
|
+
* hammered on every `flushInterval` tick. Explicit `flush()` calls
|
|
218
|
+
* are unaffected.
|
|
219
|
+
*/
|
|
220
|
+
private recordFlushFailure;
|
|
192
221
|
/**
|
|
193
222
|
* Fire-and-forget flush that survives page unload.
|
|
194
223
|
*
|
|
@@ -208,6 +237,7 @@ declare class TrackerImpl {
|
|
|
208
237
|
refreshFlags(): Promise<void>;
|
|
209
238
|
shutdown(): Promise<void>;
|
|
210
239
|
}
|
|
240
|
+
|
|
211
241
|
declare const scoperat: TrackerImpl;
|
|
212
242
|
|
|
213
243
|
export { type EvaluatedFlag, type EventSource, FlagClient, type FlushResult, type IdentifyOptions, type IdentifyTraits, type InitOptions, type TrackEvent, scoperat as default };
|
package/dist/index.js
CHANGED
|
@@ -103,54 +103,107 @@ function getBrowserContext() {
|
|
|
103
103
|
|
|
104
104
|
// src/errors.ts
|
|
105
105
|
var DEDUP_COOLDOWN_MS = 6e4;
|
|
106
|
+
var MAX_DEDUP_ENTRIES = 200;
|
|
107
|
+
var MAX_MESSAGE_LENGTH = 2048;
|
|
108
|
+
var MAX_STACK_LENGTH = 16384;
|
|
109
|
+
var MAX_REASON_JSON_LENGTH = 1024;
|
|
106
110
|
var dedupMap = /* @__PURE__ */ new Map();
|
|
107
|
-
function
|
|
111
|
+
function truncate2(str, max) {
|
|
112
|
+
return str.length > max ? str.slice(0, max) + "\u2026" : str;
|
|
113
|
+
}
|
|
114
|
+
function fingerprint(type, message, stack) {
|
|
108
115
|
const firstFrame = stack?.split("\n")[1]?.trim() ?? "";
|
|
109
|
-
return `${type}:${firstFrame}`;
|
|
116
|
+
return `${type}:${truncate2(message, 100)}:${firstFrame}`;
|
|
110
117
|
}
|
|
111
|
-
function isDuplicate(
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
118
|
+
function isDuplicate(fp) {
|
|
119
|
+
const now = Date.now();
|
|
120
|
+
const lastSent = dedupMap.get(fp);
|
|
121
|
+
if (lastSent && now - lastSent < DEDUP_COOLDOWN_MS) return true;
|
|
122
|
+
dedupMap.set(fp, now);
|
|
123
|
+
pruneDedupMap(now);
|
|
115
124
|
return false;
|
|
116
125
|
}
|
|
126
|
+
function pruneDedupMap(now) {
|
|
127
|
+
if (dedupMap.size <= MAX_DEDUP_ENTRIES) return;
|
|
128
|
+
for (const [key, ts] of dedupMap) {
|
|
129
|
+
if (now - ts >= DEDUP_COOLDOWN_MS) dedupMap.delete(key);
|
|
130
|
+
}
|
|
131
|
+
for (const key of dedupMap.keys()) {
|
|
132
|
+
if (dedupMap.size <= MAX_DEDUP_ENTRIES) break;
|
|
133
|
+
dedupMap.delete(key);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
117
136
|
function sendException(tracker, props, getBreadcrumbs2) {
|
|
118
|
-
const fp =
|
|
137
|
+
const fp = fingerprint(props.type, props.message, props.stack);
|
|
119
138
|
if (isDuplicate(fp)) return;
|
|
120
139
|
tracker.track("$exception", {
|
|
121
140
|
...props,
|
|
141
|
+
message: truncate2(props.message, MAX_MESSAGE_LENGTH),
|
|
142
|
+
stack: props.stack ? truncate2(props.stack, MAX_STACK_LENGTH) : void 0,
|
|
122
143
|
context: { breadcrumbs: getBreadcrumbs2() }
|
|
123
144
|
});
|
|
124
145
|
}
|
|
146
|
+
function describeRejectionReason(reason) {
|
|
147
|
+
if (reason instanceof Error) {
|
|
148
|
+
return {
|
|
149
|
+
type: reason.name || "UnhandledRejection",
|
|
150
|
+
message: reason.message,
|
|
151
|
+
stack: reason.stack
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (typeof reason === "object" && reason !== null) {
|
|
155
|
+
let json;
|
|
156
|
+
try {
|
|
157
|
+
json = JSON.stringify(reason) ?? Object.prototype.toString.call(reason);
|
|
158
|
+
} catch {
|
|
159
|
+
json = Object.prototype.toString.call(reason);
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
type: "UnhandledRejection",
|
|
163
|
+
message: `Non-Error promise rejection: ${truncate2(json, MAX_REASON_JSON_LENGTH)}`,
|
|
164
|
+
stack: void 0
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
type: "UnhandledRejection",
|
|
169
|
+
message: String(reason),
|
|
170
|
+
stack: void 0
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
var uninstallHandlers = null;
|
|
125
174
|
function installGlobalHandlers(tracker, getBreadcrumbs2) {
|
|
126
|
-
if (typeof window === "undefined") return
|
|
127
|
-
|
|
128
|
-
|
|
175
|
+
if (typeof window === "undefined") return () => {
|
|
176
|
+
};
|
|
177
|
+
if (uninstallHandlers) return uninstallHandlers;
|
|
178
|
+
const onError = (event) => {
|
|
179
|
+
const error = event.error;
|
|
180
|
+
const message = error?.message || (typeof event.message === "string" && event.message ? event.message : "Unknown error");
|
|
181
|
+
const opaque = !error && /^Script error\.?$/.test(message);
|
|
129
182
|
sendException(
|
|
130
183
|
tracker,
|
|
131
184
|
{
|
|
132
|
-
type:
|
|
133
|
-
message
|
|
134
|
-
stack:
|
|
185
|
+
type: error?.name || "Error",
|
|
186
|
+
message,
|
|
187
|
+
stack: error?.stack,
|
|
135
188
|
handled: false,
|
|
136
189
|
mechanism: "onerror",
|
|
137
190
|
source: "browser",
|
|
138
|
-
filename,
|
|
139
|
-
lineno,
|
|
140
|
-
colno
|
|
191
|
+
filename: event.filename || void 0,
|
|
192
|
+
lineno: event.lineno || void 0,
|
|
193
|
+
colno: event.colno || void 0,
|
|
194
|
+
...opaque ? { opaque: true } : {}
|
|
141
195
|
},
|
|
142
196
|
getBreadcrumbs2
|
|
143
197
|
);
|
|
144
198
|
};
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
199
|
+
const onRejection = (event) => {
|
|
200
|
+
const { type, message, stack } = describeRejectionReason(event.reason);
|
|
148
201
|
sendException(
|
|
149
202
|
tracker,
|
|
150
203
|
{
|
|
151
|
-
type
|
|
152
|
-
message
|
|
153
|
-
stack
|
|
204
|
+
type,
|
|
205
|
+
message,
|
|
206
|
+
stack,
|
|
154
207
|
handled: false,
|
|
155
208
|
mechanism: "onunhandledrejection",
|
|
156
209
|
source: "browser"
|
|
@@ -158,6 +211,14 @@ function installGlobalHandlers(tracker, getBreadcrumbs2) {
|
|
|
158
211
|
getBreadcrumbs2
|
|
159
212
|
);
|
|
160
213
|
};
|
|
214
|
+
window.addEventListener("error", onError);
|
|
215
|
+
window.addEventListener("unhandledrejection", onRejection);
|
|
216
|
+
uninstallHandlers = () => {
|
|
217
|
+
window.removeEventListener("error", onError);
|
|
218
|
+
window.removeEventListener("unhandledrejection", onRejection);
|
|
219
|
+
uninstallHandlers = null;
|
|
220
|
+
};
|
|
221
|
+
return uninstallHandlers;
|
|
161
222
|
}
|
|
162
223
|
|
|
163
224
|
// src/flag-client.ts
|
|
@@ -262,7 +323,9 @@ var DEFAULT_FLUSH_AT = 10;
|
|
|
262
323
|
var DEFAULT_FLUSH_INTERVAL = 3e3;
|
|
263
324
|
var DEFAULT_TIMEOUT = 1e4;
|
|
264
325
|
var DEFAULT_SESSION_TIMEOUT = 30 * 60 * 1e3;
|
|
326
|
+
var DEFAULT_MAX_QUEUE_SIZE = 1e3;
|
|
265
327
|
var MAX_BATCH_SIZE = 100;
|
|
328
|
+
var MAX_RETRY_BACKOFF = 6e4;
|
|
266
329
|
var ANON_ID_KEY = "__scoperat_anon_id";
|
|
267
330
|
var SESSION_ID_KEY = "__scoperat_session_id";
|
|
268
331
|
var LAST_ACTIVITY_KEY = "__scoperat_last_activity";
|
|
@@ -303,8 +366,16 @@ var TrackerImpl = class {
|
|
|
303
366
|
timeout = DEFAULT_TIMEOUT;
|
|
304
367
|
onError;
|
|
305
368
|
buffer = [];
|
|
369
|
+
maxQueueSize = DEFAULT_MAX_QUEUE_SIZE;
|
|
306
370
|
timer = null;
|
|
307
371
|
visibilityHandler = null;
|
|
372
|
+
pageHideHandler = null;
|
|
373
|
+
uninstallErrorHandlers = null;
|
|
374
|
+
/** Consecutive flush failures, drives exponential retry backoff. */
|
|
375
|
+
flushFailures = 0;
|
|
376
|
+
/** Timestamp before which automatic flushes are suppressed (backoff). */
|
|
377
|
+
nextFlushAllowedAt = 0;
|
|
378
|
+
exceptionFlushScheduled = false;
|
|
308
379
|
userId;
|
|
309
380
|
orgId;
|
|
310
381
|
traits = {};
|
|
@@ -322,12 +393,12 @@ var TrackerImpl = class {
|
|
|
322
393
|
this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
323
394
|
this.onError = options.onError;
|
|
324
395
|
this.sessionTimeout = options.sessionTimeout ?? DEFAULT_SESSION_TIMEOUT;
|
|
396
|
+
this.maxQueueSize = options.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE;
|
|
325
397
|
this.anonymousId = getOrCreateAnonId();
|
|
326
398
|
if (this.timer) clearInterval(this.timer);
|
|
327
399
|
this.timer = setInterval(() => {
|
|
328
400
|
if (this.buffer.length > 0) {
|
|
329
|
-
this.
|
|
330
|
-
});
|
|
401
|
+
this.maybeFlush();
|
|
331
402
|
}
|
|
332
403
|
}, this.flushInterval);
|
|
333
404
|
if (typeof window !== "undefined") {
|
|
@@ -340,9 +411,18 @@ var TrackerImpl = class {
|
|
|
340
411
|
}
|
|
341
412
|
};
|
|
342
413
|
window.addEventListener("visibilitychange", this.visibilityHandler);
|
|
414
|
+
if (this.pageHideHandler) {
|
|
415
|
+
window.removeEventListener("pagehide", this.pageHideHandler);
|
|
416
|
+
}
|
|
417
|
+
this.pageHideHandler = () => {
|
|
418
|
+
if (this.buffer.length > 0) {
|
|
419
|
+
this.flushSync();
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
window.addEventListener("pagehide", this.pageHideHandler);
|
|
343
423
|
}
|
|
344
424
|
if (options.captureErrors) {
|
|
345
|
-
installGlobalHandlers(this, getBreadcrumbs);
|
|
425
|
+
this.uninstallErrorHandlers = installGlobalHandlers(this, getBreadcrumbs);
|
|
346
426
|
startBreadcrumbCollection();
|
|
347
427
|
}
|
|
348
428
|
this._flagClient?.destroy();
|
|
@@ -356,13 +436,10 @@ var TrackerImpl = class {
|
|
|
356
436
|
userId: this.userId,
|
|
357
437
|
anonymousId: this.anonymousId,
|
|
358
438
|
orgId: this.orgId
|
|
359
|
-
}),
|
|
360
|
-
onEvaluation: (key, flag) => this.track("$flag_evaluation", {
|
|
361
|
-
flag_key: key,
|
|
362
|
-
flag_value: flag.value,
|
|
363
|
-
flag_type: flag.type,
|
|
364
|
-
flag_source: flag.source
|
|
365
439
|
})
|
|
440
|
+
// Flag evaluations are an internal system signal, not a product
|
|
441
|
+
// event — we deliberately don't emit `$flag_evaluation` into the
|
|
442
|
+
// user's event stream (the server also refuses to persist it).
|
|
366
443
|
});
|
|
367
444
|
}
|
|
368
445
|
/** The underlying flag evaluation client. Use for direct SDK integration. */
|
|
@@ -600,7 +677,7 @@ var TrackerImpl = class {
|
|
|
600
677
|
emitSessionEvent(event, sessionId, reason, occurredAt) {
|
|
601
678
|
if (!this.writeKey) return;
|
|
602
679
|
const traitsContext = Object.keys(this.traits).length > 0 ? { traits: this.traits } : {};
|
|
603
|
-
this.
|
|
680
|
+
this.enqueue({
|
|
604
681
|
event,
|
|
605
682
|
properties: { reason },
|
|
606
683
|
occurred_at: occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -646,17 +723,20 @@ var TrackerImpl = class {
|
|
|
646
723
|
}
|
|
647
724
|
captureException(error, context) {
|
|
648
725
|
if (!this.writeKey) return;
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
726
|
+
const { mechanism, ...extra } = context ?? {};
|
|
727
|
+
sendException(
|
|
728
|
+
this,
|
|
729
|
+
{
|
|
730
|
+
type: error.name || "Error",
|
|
731
|
+
message: error.message,
|
|
732
|
+
stack: error.stack,
|
|
733
|
+
handled: true,
|
|
734
|
+
mechanism: typeof mechanism === "string" ? mechanism : "manual",
|
|
735
|
+
source: "browser",
|
|
736
|
+
extra: Object.keys(extra).length > 0 ? extra : void 0
|
|
737
|
+
},
|
|
738
|
+
getBreadcrumbs
|
|
739
|
+
);
|
|
660
740
|
}
|
|
661
741
|
addBreadcrumb(category, message, data) {
|
|
662
742
|
addBreadcrumb({
|
|
@@ -685,12 +765,44 @@ var TrackerImpl = class {
|
|
|
685
765
|
...evt.context
|
|
686
766
|
}
|
|
687
767
|
};
|
|
688
|
-
this.
|
|
689
|
-
if (
|
|
690
|
-
this.
|
|
691
|
-
|
|
768
|
+
this.enqueue(enriched);
|
|
769
|
+
if (enriched.event === "$exception") {
|
|
770
|
+
this.scheduleExceptionFlush();
|
|
771
|
+
} else if (this.buffer.length >= this.flushAt) {
|
|
772
|
+
this.maybeFlush();
|
|
692
773
|
}
|
|
693
774
|
}
|
|
775
|
+
/**
|
|
776
|
+
* Push an event onto the buffer, evicting the oldest event when the
|
|
777
|
+
* queue is full. `$exception` events are preferentially retained.
|
|
778
|
+
*/
|
|
779
|
+
enqueue(evt) {
|
|
780
|
+
if (this.buffer.length >= this.maxQueueSize) {
|
|
781
|
+
const idx = this.buffer.findIndex((e) => e.event !== "$exception");
|
|
782
|
+
const dropped = this.buffer.splice(idx === -1 ? 0 : idx, 1);
|
|
783
|
+
if (dropped.length > 0) {
|
|
784
|
+
this.onError?.(
|
|
785
|
+
new Error("Event buffer full \u2014 dropped oldest event"),
|
|
786
|
+
dropped
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
this.buffer.push(evt);
|
|
791
|
+
}
|
|
792
|
+
/** Flush unless we're inside a retry-backoff window. */
|
|
793
|
+
maybeFlush() {
|
|
794
|
+
if (Date.now() < this.nextFlushAllowedAt) return;
|
|
795
|
+
this.flush().catch(() => {
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
scheduleExceptionFlush() {
|
|
799
|
+
if (this.exceptionFlushScheduled) return;
|
|
800
|
+
this.exceptionFlushScheduled = true;
|
|
801
|
+
setTimeout(() => {
|
|
802
|
+
this.exceptionFlushScheduled = false;
|
|
803
|
+
this.maybeFlush();
|
|
804
|
+
}, 0);
|
|
805
|
+
}
|
|
694
806
|
async flush() {
|
|
695
807
|
if (!this.writeKey || this.buffer.length === 0) {
|
|
696
808
|
return { success: true, ingested: 0 };
|
|
@@ -708,17 +820,41 @@ var TrackerImpl = class {
|
|
|
708
820
|
});
|
|
709
821
|
if (!res.ok) {
|
|
710
822
|
const body = await res.text().catch(() => "");
|
|
711
|
-
|
|
823
|
+
const err = new Error(`API returned ${res.status}: ${body}`);
|
|
824
|
+
const retryable = res.status === 408 || res.status === 429 || res.status >= 500;
|
|
825
|
+
if (retryable) {
|
|
826
|
+
this.buffer.unshift(...batch);
|
|
827
|
+
}
|
|
828
|
+
this.recordFlushFailure();
|
|
829
|
+
this.onError?.(err, batch);
|
|
830
|
+
return { success: false, ingested: 0 };
|
|
712
831
|
}
|
|
832
|
+
this.flushFailures = 0;
|
|
833
|
+
this.nextFlushAllowedAt = 0;
|
|
713
834
|
const json = await res.json();
|
|
714
835
|
return { success: true, ingested: json.ingested };
|
|
715
836
|
} catch (error) {
|
|
716
837
|
this.buffer.unshift(...batch);
|
|
838
|
+
this.recordFlushFailure();
|
|
717
839
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
718
840
|
this.onError?.(err, batch);
|
|
719
841
|
return { success: false, ingested: 0 };
|
|
720
842
|
}
|
|
721
843
|
}
|
|
844
|
+
/**
|
|
845
|
+
* Record a failed flush and push out the next automatic flush with
|
|
846
|
+
* exponential backoff (capped) so a down ingest endpoint isn't
|
|
847
|
+
* hammered on every `flushInterval` tick. Explicit `flush()` calls
|
|
848
|
+
* are unaffected.
|
|
849
|
+
*/
|
|
850
|
+
recordFlushFailure() {
|
|
851
|
+
this.flushFailures += 1;
|
|
852
|
+
const delay = Math.min(
|
|
853
|
+
this.flushInterval * 2 ** this.flushFailures,
|
|
854
|
+
MAX_RETRY_BACKOFF
|
|
855
|
+
);
|
|
856
|
+
this.nextFlushAllowedAt = Date.now() + delay;
|
|
857
|
+
}
|
|
722
858
|
/**
|
|
723
859
|
* Fire-and-forget flush that survives page unload.
|
|
724
860
|
*
|
|
@@ -770,12 +906,21 @@ var TrackerImpl = class {
|
|
|
770
906
|
}
|
|
771
907
|
this._flagClient?.destroy();
|
|
772
908
|
this._flagClient = null;
|
|
773
|
-
if (typeof window !== "undefined"
|
|
774
|
-
|
|
775
|
-
|
|
909
|
+
if (typeof window !== "undefined") {
|
|
910
|
+
if (this.visibilityHandler) {
|
|
911
|
+
window.removeEventListener("visibilitychange", this.visibilityHandler);
|
|
912
|
+
this.visibilityHandler = null;
|
|
913
|
+
}
|
|
914
|
+
if (this.pageHideHandler) {
|
|
915
|
+
window.removeEventListener("pagehide", this.pageHideHandler);
|
|
916
|
+
this.pageHideHandler = null;
|
|
917
|
+
}
|
|
776
918
|
}
|
|
919
|
+
this.uninstallErrorHandlers?.();
|
|
920
|
+
this.uninstallErrorHandlers = null;
|
|
777
921
|
while (this.buffer.length > 0) {
|
|
778
|
-
await this.flush();
|
|
922
|
+
const result = await this.flush();
|
|
923
|
+
if (!result.success) break;
|
|
779
924
|
}
|
|
780
925
|
}
|
|
781
926
|
};
|