@emit-vision/sdk-js 0.1.0 → 0.2.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/dist/index.d.ts +10 -1
- package/dist/index.js +71 -14
- package/dist/retry.d.ts +25 -0
- package/dist/retry.js +56 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -56,8 +56,14 @@ export type EmitVisionOptions = {
|
|
|
56
56
|
fetchImpl?: typeof fetch;
|
|
57
57
|
/** TTL in milliseconds for in-memory flag evaluation cache. Default: 60 000 (1 minute). */
|
|
58
58
|
flagEvalTtlMs?: number;
|
|
59
|
+
/** Initial delay (ms) before retrying after a flush failure. Default: 1000. Doubles each consecutive failure up to {@link retryBackoffMaxMs}. Set to 0 to retry immediately on every tick. */
|
|
60
|
+
retryBackoffInitialMs?: number;
|
|
61
|
+
/** Maximum delay (ms) between flush retries. Default: 30000. */
|
|
62
|
+
retryBackoffMaxMs?: number;
|
|
63
|
+
/** Disable the SDK after this many consecutive flush failures: the queue is dropped, future captures become no-ops, and a single warning is logged. Default: 20. Set to 0 to disable the cap and retry forever. */
|
|
64
|
+
maxConsecutiveFlushFailures?: number;
|
|
59
65
|
};
|
|
60
|
-
type RequiredEmitVisionOptions = Required<Pick<EmitVisionOptions, "apiKey" | "endpoint" | "flushIntervalMs" | "batchSize" | "fetchImpl" | "flushOnCapture" | "flagEvalTtlMs">> & Pick<EmitVisionOptions, "environment" | "release" | "sessionId" | "label" | "deployment" | "featureFlags" | "debug"> & {
|
|
66
|
+
type RequiredEmitVisionOptions = Required<Pick<EmitVisionOptions, "apiKey" | "endpoint" | "flushIntervalMs" | "batchSize" | "fetchImpl" | "flushOnCapture" | "flagEvalTtlMs" | "retryBackoffInitialMs" | "retryBackoffMaxMs" | "maxConsecutiveFlushFailures">> & Pick<EmitVisionOptions, "environment" | "release" | "sessionId" | "label" | "deployment" | "featureFlags" | "debug"> & {
|
|
61
67
|
autoCapture: Required<AutoCaptureOptions> & {
|
|
62
68
|
flagExposures: boolean;
|
|
63
69
|
};
|
|
@@ -74,6 +80,7 @@ declare class EmitVisionClient {
|
|
|
74
80
|
private flushScheduled;
|
|
75
81
|
private readonly fetchImpl;
|
|
76
82
|
private readonly flagCache;
|
|
83
|
+
private readonly retry;
|
|
77
84
|
constructor(options: RequiredEmitVisionOptions);
|
|
78
85
|
captureEvent(name: string, properties?: Record<string, unknown>, options?: CaptureOptions): void;
|
|
79
86
|
captureError(error: unknown, options?: CaptureOptions): void;
|
|
@@ -102,6 +109,8 @@ declare class EmitVisionClient {
|
|
|
102
109
|
private fetchAndCacheFlags;
|
|
103
110
|
setTags(tags: Record<string, string>): void;
|
|
104
111
|
flush(): Promise<void>;
|
|
112
|
+
private handleFlushFailure;
|
|
113
|
+
private stopBackgroundWork;
|
|
105
114
|
close(): void;
|
|
106
115
|
private enqueue;
|
|
107
116
|
private scheduleFlush;
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FlushRetryController } from "./retry.js";
|
|
1
2
|
const SDK_NAME = "emit-vision-js";
|
|
2
3
|
const SDK_VERSION = "0.1.0";
|
|
3
4
|
class EmitVisionClient {
|
|
@@ -12,6 +13,7 @@ class EmitVisionClient {
|
|
|
12
13
|
flushScheduled = false;
|
|
13
14
|
fetchImpl;
|
|
14
15
|
flagCache = new Map();
|
|
16
|
+
retry;
|
|
15
17
|
constructor(options) {
|
|
16
18
|
this.options = options;
|
|
17
19
|
this.fetchImpl = options.fetchImpl;
|
|
@@ -19,8 +21,13 @@ class EmitVisionClient {
|
|
|
19
21
|
? { ...options.deployment }
|
|
20
22
|
: undefined;
|
|
21
23
|
this.featureFlags = options.featureFlags ? { ...options.featureFlags } : {};
|
|
24
|
+
this.retry = new FlushRetryController({
|
|
25
|
+
initialMs: options.retryBackoffInitialMs,
|
|
26
|
+
maxMs: options.retryBackoffMaxMs,
|
|
27
|
+
failureCap: options.maxConsecutiveFlushFailures,
|
|
28
|
+
});
|
|
22
29
|
this.timer = setInterval(() => {
|
|
23
|
-
if (this.queue.length === 0) {
|
|
30
|
+
if (this.queue.length === 0 || this.retry.isBackingOff()) {
|
|
24
31
|
return;
|
|
25
32
|
}
|
|
26
33
|
this.flush().catch((err) => this.debug("background flush error", { error: err }));
|
|
@@ -185,6 +192,9 @@ class EmitVisionClient {
|
|
|
185
192
|
this.debug("updated tags", this.tags);
|
|
186
193
|
}
|
|
187
194
|
async flush() {
|
|
195
|
+
if (this.retry.isDisabled()) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
188
198
|
const batch = this.queue.splice(0, this.options.batchSize);
|
|
189
199
|
if (batch.length === 0) {
|
|
190
200
|
return;
|
|
@@ -208,31 +218,69 @@ class EmitVisionClient {
|
|
|
208
218
|
});
|
|
209
219
|
}
|
|
210
220
|
catch (err) {
|
|
211
|
-
this.
|
|
212
|
-
this.debug("flush network error", {
|
|
213
|
-
error: err,
|
|
214
|
-
restoredCount: batch.length,
|
|
215
|
-
});
|
|
221
|
+
this.handleFlushFailure(batch, { reason: "network", error: err });
|
|
216
222
|
throw err;
|
|
217
223
|
}
|
|
218
224
|
if (!response.ok) {
|
|
219
|
-
|
|
220
|
-
this.
|
|
225
|
+
const error = new Error(`emit-vision flush failed with ${response.status}`);
|
|
226
|
+
this.handleFlushFailure(batch, {
|
|
227
|
+
reason: "status",
|
|
221
228
|
status: response.status,
|
|
222
|
-
restoredCount: batch.length,
|
|
223
229
|
});
|
|
224
|
-
throw
|
|
230
|
+
throw error;
|
|
225
231
|
}
|
|
232
|
+
this.retry.reset();
|
|
226
233
|
this.debug("flush complete", { count: batch.length });
|
|
227
234
|
if (this.options.flushOnCapture && this.queue.length > 0) {
|
|
228
235
|
this.scheduleFlush();
|
|
229
236
|
}
|
|
230
237
|
}
|
|
231
|
-
|
|
238
|
+
handleFlushFailure(batch, detail) {
|
|
239
|
+
const result = this.retry.recordFailure();
|
|
240
|
+
if (result.justDisabled) {
|
|
241
|
+
const droppedQueued = this.queue.length;
|
|
242
|
+
const droppedBatch = batch.length;
|
|
243
|
+
this.queue = [];
|
|
244
|
+
this.stopBackgroundWork();
|
|
245
|
+
const message = `[emit-vision${this.options.label ? `:${this.options.label}` : ""}] ` +
|
|
246
|
+
`disabled after ${result.consecutiveFailures} consecutive flush failures; ` +
|
|
247
|
+
`dropping ${droppedQueued + droppedBatch} queued events. ` +
|
|
248
|
+
`Re-init the SDK to resume.`;
|
|
249
|
+
if (typeof console.warn === "function") {
|
|
250
|
+
console.warn(message);
|
|
251
|
+
}
|
|
252
|
+
this.debug("disabled", {
|
|
253
|
+
consecutiveFailures: result.consecutiveFailures,
|
|
254
|
+
dropped: droppedQueued + droppedBatch,
|
|
255
|
+
});
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
this.queue.unshift(...batch);
|
|
259
|
+
if (detail.reason === "network") {
|
|
260
|
+
this.debug("flush network error", {
|
|
261
|
+
error: detail.error,
|
|
262
|
+
restoredCount: batch.length,
|
|
263
|
+
consecutiveFailures: result.consecutiveFailures,
|
|
264
|
+
backoffMs: result.backoffMs,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
this.debug("flush failed", {
|
|
269
|
+
status: detail.status,
|
|
270
|
+
restoredCount: batch.length,
|
|
271
|
+
consecutiveFailures: result.consecutiveFailures,
|
|
272
|
+
backoffMs: result.backoffMs,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
stopBackgroundWork() {
|
|
232
277
|
if (this.timer) {
|
|
233
278
|
clearInterval(this.timer);
|
|
234
279
|
this.timer = undefined;
|
|
235
280
|
}
|
|
281
|
+
}
|
|
282
|
+
close() {
|
|
283
|
+
this.stopBackgroundWork();
|
|
236
284
|
if (typeof window !== "undefined" && this.options.autoCapture.errors) {
|
|
237
285
|
window.removeEventListener("error", this.handleWindowError);
|
|
238
286
|
}
|
|
@@ -243,6 +291,9 @@ class EmitVisionClient {
|
|
|
243
291
|
this.debug("closed client");
|
|
244
292
|
}
|
|
245
293
|
enqueue(payload) {
|
|
294
|
+
if (this.retry.isDisabled()) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
246
297
|
this.queue.push(payload);
|
|
247
298
|
this.debug("queued payload", {
|
|
248
299
|
type: payload.type,
|
|
@@ -255,10 +306,13 @@ class EmitVisionClient {
|
|
|
255
306
|
this.debug("capture flush scheduled", {
|
|
256
307
|
queueSize: this.queue.length,
|
|
257
308
|
});
|
|
258
|
-
this.
|
|
309
|
+
if (!this.retry.isBackingOff()) {
|
|
310
|
+
this.flush().catch((err) => this.debug("capture flush error", { error: err }));
|
|
311
|
+
}
|
|
259
312
|
return;
|
|
260
313
|
}
|
|
261
|
-
if (this.queue.length >= this.options.batchSize
|
|
314
|
+
if (this.queue.length >= this.options.batchSize &&
|
|
315
|
+
!this.retry.isBackingOff()) {
|
|
262
316
|
this.flush().catch((err) => this.debug("batch flush error", { error: err }));
|
|
263
317
|
}
|
|
264
318
|
}
|
|
@@ -269,7 +323,7 @@ class EmitVisionClient {
|
|
|
269
323
|
this.flushScheduled = true;
|
|
270
324
|
queueMicrotask(() => {
|
|
271
325
|
this.flushScheduled = false;
|
|
272
|
-
if (this.queue.length === 0) {
|
|
326
|
+
if (this.queue.length === 0 || this.retry.isBackingOff()) {
|
|
273
327
|
return;
|
|
274
328
|
}
|
|
275
329
|
this.flush().catch((err) => this.debug("scheduled flush error", { error: err }));
|
|
@@ -358,6 +412,9 @@ export function init(options) {
|
|
|
358
412
|
fetchImpl: options.fetchImpl ?? globalThis.fetch.bind(globalThis),
|
|
359
413
|
flushOnCapture: options.flushOnCapture ?? false,
|
|
360
414
|
flagEvalTtlMs: options.flagEvalTtlMs ?? 60_000,
|
|
415
|
+
retryBackoffInitialMs: options.retryBackoffInitialMs ?? 1_000,
|
|
416
|
+
retryBackoffMaxMs: options.retryBackoffMaxMs ?? 30_000,
|
|
417
|
+
maxConsecutiveFlushFailures: options.maxConsecutiveFlushFailures ?? 20,
|
|
361
418
|
apiKey: transport.apiKey,
|
|
362
419
|
endpoint: transport.endpoint ?? options.endpoint ?? "http://localhost:4301",
|
|
363
420
|
autoCapture,
|
package/dist/retry.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type FlushRetryOptions = {
|
|
2
|
+
/** Initial backoff in ms after the first failure. Doubles each consecutive failure. Set to 0 to disable backoff. */
|
|
3
|
+
initialMs: number;
|
|
4
|
+
/** Maximum backoff in ms between retries. */
|
|
5
|
+
maxMs: number;
|
|
6
|
+
/** Hard-shutdown threshold: after this many consecutive failures the SDK disables itself permanently. Set to 0 to disable the cap. */
|
|
7
|
+
failureCap: number;
|
|
8
|
+
};
|
|
9
|
+
export type RecordFailureResult = {
|
|
10
|
+
backoffMs: number;
|
|
11
|
+
consecutiveFailures: number;
|
|
12
|
+
justDisabled: boolean;
|
|
13
|
+
};
|
|
14
|
+
export declare class FlushRetryController {
|
|
15
|
+
private readonly opts;
|
|
16
|
+
private consecutiveFailures;
|
|
17
|
+
private backoffUntil;
|
|
18
|
+
private disabled;
|
|
19
|
+
constructor(opts: FlushRetryOptions);
|
|
20
|
+
isDisabled(): boolean;
|
|
21
|
+
isBackingOff(now?: number): boolean;
|
|
22
|
+
msUntilReady(now?: number): number;
|
|
23
|
+
reset(): void;
|
|
24
|
+
recordFailure(now?: number): RecordFailureResult;
|
|
25
|
+
}
|
package/dist/retry.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export class FlushRetryController {
|
|
2
|
+
opts;
|
|
3
|
+
consecutiveFailures = 0;
|
|
4
|
+
backoffUntil = 0;
|
|
5
|
+
disabled = false;
|
|
6
|
+
constructor(opts) {
|
|
7
|
+
this.opts = opts;
|
|
8
|
+
}
|
|
9
|
+
isDisabled() {
|
|
10
|
+
return this.disabled;
|
|
11
|
+
}
|
|
12
|
+
isBackingOff(now = Date.now()) {
|
|
13
|
+
return !this.disabled && now < this.backoffUntil;
|
|
14
|
+
}
|
|
15
|
+
msUntilReady(now = Date.now()) {
|
|
16
|
+
if (this.disabled)
|
|
17
|
+
return Infinity;
|
|
18
|
+
return Math.max(0, this.backoffUntil - now);
|
|
19
|
+
}
|
|
20
|
+
reset() {
|
|
21
|
+
this.consecutiveFailures = 0;
|
|
22
|
+
this.backoffUntil = 0;
|
|
23
|
+
}
|
|
24
|
+
recordFailure(now = Date.now()) {
|
|
25
|
+
this.consecutiveFailures += 1;
|
|
26
|
+
if (this.opts.failureCap > 0 &&
|
|
27
|
+
this.consecutiveFailures >= this.opts.failureCap) {
|
|
28
|
+
const wasDisabled = this.disabled;
|
|
29
|
+
this.disabled = true;
|
|
30
|
+
this.backoffUntil = 0;
|
|
31
|
+
return {
|
|
32
|
+
backoffMs: 0,
|
|
33
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
34
|
+
justDisabled: !wasDisabled,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (this.opts.initialMs <= 0) {
|
|
38
|
+
this.backoffUntil = 0;
|
|
39
|
+
return {
|
|
40
|
+
backoffMs: 0,
|
|
41
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
42
|
+
justDisabled: false,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const exp = Math.min(this.opts.initialMs * Math.pow(2, this.consecutiveFailures - 1), this.opts.maxMs);
|
|
46
|
+
// ±20% jitter so multiple clients don't synchronize their retries
|
|
47
|
+
const jitter = exp * 0.2 * (Math.random() * 2 - 1);
|
|
48
|
+
const backoffMs = Math.max(0, Math.floor(exp + jitter));
|
|
49
|
+
this.backoffUntil = now + backoffMs;
|
|
50
|
+
return {
|
|
51
|
+
backoffMs,
|
|
52
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
53
|
+
justDisabled: false,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|