@emit-vision/sdk-js 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/dist/index.d.ts +3 -20
- package/dist/index.js +430 -364
- package/package.json +4 -3
package/dist/index.d.ts
CHANGED
|
@@ -69,26 +69,23 @@ type EmitVisionOptions = {
|
|
|
69
69
|
/** 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. */
|
|
70
70
|
maxConsecutiveFlushFailures?: number;
|
|
71
71
|
};
|
|
72
|
-
|
|
73
72
|
type RequiredEmitVisionOptions = Required<Pick<EmitVisionOptions, "apiKey" | "endpoint" | "flushIntervalMs" | "batchSize" | "fetchImpl" | "flushOnCapture" | "flagEvalTtlMs" | "retryBackoffInitialMs" | "retryBackoffMaxMs" | "maxConsecutiveFlushFailures">> & Pick<EmitVisionOptions, "environment" | "release" | "sessionId" | "label" | "deployment" | "featureFlags" | "debug"> & {
|
|
74
73
|
autoCapture: Required<AutoCaptureOptions> & {
|
|
75
74
|
flagExposures: boolean;
|
|
76
75
|
pageViews: boolean;
|
|
77
76
|
};
|
|
78
77
|
};
|
|
78
|
+
|
|
79
79
|
declare class EmitVisionClient {
|
|
80
80
|
private readonly options;
|
|
81
|
-
private queue;
|
|
82
81
|
private user;
|
|
83
82
|
private context;
|
|
84
83
|
private deployment;
|
|
85
84
|
private featureFlags;
|
|
86
85
|
private tags;
|
|
87
|
-
private timer;
|
|
88
|
-
private flushScheduled;
|
|
89
|
-
private readonly fetchImpl;
|
|
90
86
|
private readonly flagEval;
|
|
91
|
-
private readonly
|
|
87
|
+
private readonly autoCapture;
|
|
88
|
+
private readonly flusher;
|
|
92
89
|
constructor(options: RequiredEmitVisionOptions);
|
|
93
90
|
captureEvent(name: string, properties?: Record<string, unknown>, options?: CaptureOptions): void;
|
|
94
91
|
captureError(error: unknown, options?: CaptureOptions): void;
|
|
@@ -103,22 +100,8 @@ declare class EmitVisionClient {
|
|
|
103
100
|
captureExposure(flagKey: string, variantKey: string, variantValue?: string | number | boolean | null, reason?: string, environment?: string): void;
|
|
104
101
|
setTags(tags: Record<string, string>): void;
|
|
105
102
|
flush(): Promise<void>;
|
|
106
|
-
private flushChunk;
|
|
107
|
-
private handleFlushFailure;
|
|
108
|
-
private stopBackgroundWork;
|
|
109
103
|
close(): void;
|
|
110
|
-
private enqueueMetaEvent;
|
|
111
|
-
private enqueue;
|
|
112
|
-
private scheduleFlush;
|
|
113
104
|
private withDefaults;
|
|
114
|
-
private mergeDeployment;
|
|
115
|
-
private mergeFeatureFlags;
|
|
116
|
-
private readonly handleWindowError;
|
|
117
|
-
private readonly handlePopState;
|
|
118
|
-
private patchHistoryMethod;
|
|
119
|
-
private restoreHistoryMethod;
|
|
120
|
-
private readonly handleUnhandledRejection;
|
|
121
|
-
private debug;
|
|
122
105
|
}
|
|
123
106
|
|
|
124
107
|
declare function init(options: EmitVisionOptions): EmitVisionClient;
|
package/dist/index.js
CHANGED
|
@@ -1,61 +1,3 @@
|
|
|
1
|
-
// src/retry.ts
|
|
2
|
-
var FlushRetryController = class {
|
|
3
|
-
constructor(opts) {
|
|
4
|
-
this.opts = opts;
|
|
5
|
-
}
|
|
6
|
-
opts;
|
|
7
|
-
consecutiveFailures = 0;
|
|
8
|
-
backoffUntil = 0;
|
|
9
|
-
disabled = false;
|
|
10
|
-
isDisabled() {
|
|
11
|
-
return this.disabled;
|
|
12
|
-
}
|
|
13
|
-
isBackingOff(now = Date.now()) {
|
|
14
|
-
return !this.disabled && now < this.backoffUntil;
|
|
15
|
-
}
|
|
16
|
-
msUntilReady(now = Date.now()) {
|
|
17
|
-
if (this.disabled) 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 && this.consecutiveFailures >= this.opts.failureCap) {
|
|
27
|
-
const wasDisabled = this.disabled;
|
|
28
|
-
this.disabled = true;
|
|
29
|
-
this.backoffUntil = 0;
|
|
30
|
-
return {
|
|
31
|
-
backoffMs: 0,
|
|
32
|
-
consecutiveFailures: this.consecutiveFailures,
|
|
33
|
-
justDisabled: !wasDisabled
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
if (this.opts.initialMs <= 0) {
|
|
37
|
-
this.backoffUntil = 0;
|
|
38
|
-
return {
|
|
39
|
-
backoffMs: 0,
|
|
40
|
-
consecutiveFailures: this.consecutiveFailures,
|
|
41
|
-
justDisabled: false
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
const exp = Math.min(
|
|
45
|
-
this.opts.initialMs * Math.pow(2, this.consecutiveFailures - 1),
|
|
46
|
-
this.opts.maxMs
|
|
47
|
-
);
|
|
48
|
-
const jitter = exp * 0.2 * (Math.random() * 2 - 1);
|
|
49
|
-
const backoffMs = Math.max(0, Math.floor(exp + jitter));
|
|
50
|
-
this.backoffUntil = now + backoffMs;
|
|
51
|
-
return {
|
|
52
|
-
backoffMs,
|
|
53
|
-
consecutiveFailures: this.consecutiveFailures,
|
|
54
|
-
justDisabled: false
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
|
|
59
1
|
// src/flag-eval.ts
|
|
60
2
|
var FlagEvaluator = class {
|
|
61
3
|
constructor(cfg, onFlagsLoaded, captureExposure2, debug) {
|
|
@@ -215,9 +157,9 @@ function resolveAutoCapture(options) {
|
|
|
215
157
|
};
|
|
216
158
|
}
|
|
217
159
|
|
|
218
|
-
// src/client.ts
|
|
160
|
+
// src/client-helpers.ts
|
|
219
161
|
var SDK_NAME = "emit-vision-js";
|
|
220
|
-
var SDK_VERSION = "0.
|
|
162
|
+
var SDK_VERSION = "0.4.0";
|
|
221
163
|
var MAX_EVENT_BYTES = 64 * 1024;
|
|
222
164
|
var MAX_BATCH_BYTES = 1 * 1024 * 1024;
|
|
223
165
|
function byteLength(s) {
|
|
@@ -248,7 +190,11 @@ function trimEventIfOversized(payload) {
|
|
|
248
190
|
};
|
|
249
191
|
if (payload.type === "error") {
|
|
250
192
|
return {
|
|
251
|
-
payload: {
|
|
193
|
+
payload: {
|
|
194
|
+
...payload,
|
|
195
|
+
...base,
|
|
196
|
+
error: { ...payload.error, stack: void 0 }
|
|
197
|
+
},
|
|
252
198
|
trimmedFields,
|
|
253
199
|
originalBytes
|
|
254
200
|
};
|
|
@@ -270,55 +216,423 @@ function splitBatch(batch) {
|
|
|
270
216
|
const mid = Math.floor(batch.length / 2);
|
|
271
217
|
return [...splitBatch(batch.slice(0, mid)), ...splitBatch(batch.slice(mid))];
|
|
272
218
|
}
|
|
273
|
-
|
|
274
|
-
|
|
219
|
+
|
|
220
|
+
// src/client-internals.ts
|
|
221
|
+
function debugLog(message, opts, payload) {
|
|
222
|
+
if (!opts.debug || typeof console.debug !== "function") return;
|
|
223
|
+
const prefix = opts.label ? `[emit-vision:${opts.label}]` : "[emit-vision]";
|
|
224
|
+
if (payload === void 0) {
|
|
225
|
+
console.debug(prefix, message);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
console.debug(prefix, message, payload);
|
|
229
|
+
}
|
|
230
|
+
function buildMetaEvent(originalName, originalBytes, trimmedFields, opts) {
|
|
231
|
+
return {
|
|
232
|
+
type: "event",
|
|
233
|
+
name: "sdk.event_truncated",
|
|
234
|
+
properties: {
|
|
235
|
+
originalName,
|
|
236
|
+
originalBytes,
|
|
237
|
+
trimmedFields,
|
|
238
|
+
sdkName: SDK_NAME
|
|
239
|
+
},
|
|
240
|
+
environment: opts.environment,
|
|
241
|
+
release: opts.release
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function buildWithDefaults(clientUser, clientContext, clientDeployment, clientFeatureFlags, clientTags, sdkOpts, captureOpts) {
|
|
245
|
+
const {
|
|
246
|
+
context,
|
|
247
|
+
deployment,
|
|
248
|
+
featureFlags,
|
|
249
|
+
tags,
|
|
250
|
+
user,
|
|
251
|
+
environment,
|
|
252
|
+
release,
|
|
253
|
+
sessionId,
|
|
254
|
+
...rest
|
|
255
|
+
} = captureOpts;
|
|
256
|
+
return {
|
|
257
|
+
...rest,
|
|
258
|
+
environment: environment ?? sdkOpts.environment,
|
|
259
|
+
release: release ?? sdkOpts.release,
|
|
260
|
+
sessionId: sessionId ?? sdkOpts.sessionId,
|
|
261
|
+
user: user ?? clientUser,
|
|
262
|
+
context: Object.keys(clientContext).length ? { ...clientContext, ...context } : context,
|
|
263
|
+
deployment: mergeDeploymentFn(clientDeployment, deployment),
|
|
264
|
+
featureFlags: mergeFeatureFlagsFn(clientFeatureFlags, featureFlags),
|
|
265
|
+
tags: Object.keys(clientTags).length ? { ...clientTags, ...tags } : tags
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function mergeDeploymentFn(clientDeployment, overrideDeployment) {
|
|
269
|
+
const merged = { ...clientDeployment ?? {}, ...overrideDeployment ?? {} };
|
|
270
|
+
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
271
|
+
}
|
|
272
|
+
function mergeFeatureFlagsFn(clientFlags, overrideFlags) {
|
|
273
|
+
const merged = { ...clientFlags, ...overrideFlags ?? {} };
|
|
274
|
+
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
275
|
+
}
|
|
276
|
+
function buildFlushDisabledMessage(label, consecutiveFailures, dropped) {
|
|
277
|
+
return `[emit-vision${label ? `:${label}` : ""}] disabled after ${consecutiveFailures} consecutive flush failures; dropping ${dropped} queued events. Re-init the SDK to resume.`;
|
|
278
|
+
}
|
|
279
|
+
async function executeHttpSend(chunk, endpoint, apiKey, fetchImpl) {
|
|
280
|
+
const response = await fetchImpl(`${endpoint}/v1/batch`, {
|
|
281
|
+
method: "POST",
|
|
282
|
+
headers: { "content-type": "application/json", "x-emit-api-key": apiKey },
|
|
283
|
+
body: buildBatchBody(chunk),
|
|
284
|
+
keepalive: true
|
|
285
|
+
});
|
|
286
|
+
if (!response.ok) {
|
|
287
|
+
throw new Error(`emit-vision flush failed with ${response.status}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/retry.ts
|
|
292
|
+
var FlushRetryController = class {
|
|
293
|
+
constructor(opts) {
|
|
294
|
+
this.opts = opts;
|
|
295
|
+
}
|
|
296
|
+
opts;
|
|
297
|
+
consecutiveFailures = 0;
|
|
298
|
+
backoffUntil = 0;
|
|
299
|
+
disabled = false;
|
|
300
|
+
isDisabled() {
|
|
301
|
+
return this.disabled;
|
|
302
|
+
}
|
|
303
|
+
isBackingOff(now = Date.now()) {
|
|
304
|
+
return !this.disabled && now < this.backoffUntil;
|
|
305
|
+
}
|
|
306
|
+
msUntilReady(now = Date.now()) {
|
|
307
|
+
if (this.disabled) return Infinity;
|
|
308
|
+
return Math.max(0, this.backoffUntil - now);
|
|
309
|
+
}
|
|
310
|
+
reset() {
|
|
311
|
+
this.consecutiveFailures = 0;
|
|
312
|
+
this.backoffUntil = 0;
|
|
313
|
+
}
|
|
314
|
+
recordFailure(now = Date.now()) {
|
|
315
|
+
this.consecutiveFailures += 1;
|
|
316
|
+
if (this.opts.failureCap > 0 && this.consecutiveFailures >= this.opts.failureCap) {
|
|
317
|
+
const wasDisabled = this.disabled;
|
|
318
|
+
this.disabled = true;
|
|
319
|
+
this.backoffUntil = 0;
|
|
320
|
+
return {
|
|
321
|
+
backoffMs: 0,
|
|
322
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
323
|
+
justDisabled: !wasDisabled
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
if (this.opts.initialMs <= 0) {
|
|
327
|
+
this.backoffUntil = 0;
|
|
328
|
+
return {
|
|
329
|
+
backoffMs: 0,
|
|
330
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
331
|
+
justDisabled: false
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const exp = Math.min(
|
|
335
|
+
this.opts.initialMs * Math.pow(2, this.consecutiveFailures - 1),
|
|
336
|
+
this.opts.maxMs
|
|
337
|
+
);
|
|
338
|
+
const jitter = exp * 0.2 * (Math.random() * 2 - 1);
|
|
339
|
+
const backoffMs = Math.max(0, Math.floor(exp + jitter));
|
|
340
|
+
this.backoffUntil = now + backoffMs;
|
|
341
|
+
return {
|
|
342
|
+
backoffMs,
|
|
343
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
344
|
+
justDisabled: false
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// src/client-flush.ts
|
|
350
|
+
var FlushManager = class {
|
|
351
|
+
constructor(options, fetchImpl) {
|
|
275
352
|
this.options = options;
|
|
276
|
-
this.fetchImpl =
|
|
277
|
-
this.deployment = options.deployment ? { ...options.deployment } : void 0;
|
|
278
|
-
this.featureFlags = options.featureFlags ? { ...options.featureFlags } : {};
|
|
353
|
+
this.fetchImpl = fetchImpl;
|
|
279
354
|
this.retry = new FlushRetryController({
|
|
280
355
|
initialMs: options.retryBackoffInitialMs,
|
|
281
356
|
maxMs: options.retryBackoffMaxMs,
|
|
282
357
|
failureCap: options.maxConsecutiveFlushFailures
|
|
283
358
|
});
|
|
284
|
-
this.flagEval = new FlagEvaluator(
|
|
285
|
-
{
|
|
286
|
-
endpoint: options.endpoint,
|
|
287
|
-
apiKey: options.apiKey,
|
|
288
|
-
ttlMs: options.flagEvalTtlMs,
|
|
289
|
-
flagExposures: options.autoCapture.flagExposures,
|
|
290
|
-
fetchImpl: options.fetchImpl
|
|
291
|
-
},
|
|
292
|
-
(flags) => {
|
|
293
|
-
this.featureFlags = { ...this.featureFlags, ...flags };
|
|
294
|
-
},
|
|
295
|
-
(flagKey, variantKey, variantValue, reason, env) => this.captureExposure(flagKey, variantKey, variantValue, reason, env),
|
|
296
|
-
(msg, data) => this.debug(msg, data)
|
|
297
|
-
);
|
|
298
359
|
this.timer = setInterval(() => {
|
|
299
|
-
if (this.queue.length === 0 || this.retry.isBackingOff())
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
360
|
+
if (this.queue.length === 0 || this.retry.isBackingOff()) return;
|
|
302
361
|
this.flush().catch(
|
|
303
|
-
(err) =>
|
|
362
|
+
(err) => debugLog("background flush error", this.options, { error: err })
|
|
304
363
|
);
|
|
305
364
|
}, options.flushIntervalMs);
|
|
306
|
-
|
|
365
|
+
}
|
|
366
|
+
options;
|
|
367
|
+
fetchImpl;
|
|
368
|
+
queue = [];
|
|
369
|
+
timer;
|
|
370
|
+
flushScheduled = false;
|
|
371
|
+
retry;
|
|
372
|
+
enqueue(payload) {
|
|
373
|
+
if (this.retry.isDisabled()) return;
|
|
374
|
+
const {
|
|
375
|
+
payload: trimmed,
|
|
376
|
+
trimmedFields,
|
|
377
|
+
originalBytes
|
|
378
|
+
} = trimEventIfOversized(payload);
|
|
379
|
+
this.queue.push(trimmed);
|
|
380
|
+
if (trimmedFields !== null) {
|
|
381
|
+
const meta = buildMetaEvent(
|
|
382
|
+
payload.name,
|
|
383
|
+
originalBytes,
|
|
384
|
+
trimmedFields,
|
|
385
|
+
this.options
|
|
386
|
+
);
|
|
387
|
+
this.queue.push(meta);
|
|
388
|
+
}
|
|
389
|
+
debugLog("queued payload", this.options, {
|
|
390
|
+
type: payload.type,
|
|
391
|
+
name: payload.name,
|
|
392
|
+
queueSize: this.queue.length,
|
|
393
|
+
flushOnCapture: this.options.flushOnCapture,
|
|
394
|
+
batchSize: this.options.batchSize
|
|
395
|
+
});
|
|
396
|
+
if (this.options.flushOnCapture) {
|
|
397
|
+
if (!this.retry.isBackingOff()) {
|
|
398
|
+
this.flush().catch(
|
|
399
|
+
(err) => debugLog("capture flush error", this.options, { error: err })
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (this.queue.length >= this.options.batchSize && !this.retry.isBackingOff()) {
|
|
405
|
+
this.flush().catch(
|
|
406
|
+
(err) => debugLog("batch flush error", this.options, { error: err })
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async flush() {
|
|
411
|
+
if (this.retry.isDisabled()) return;
|
|
412
|
+
const batch = this.queue.splice(0, this.options.batchSize);
|
|
413
|
+
if (batch.length === 0) return;
|
|
414
|
+
debugLog("flushing batch", this.options, { count: batch.length });
|
|
415
|
+
const chunks = splitBatch(batch);
|
|
416
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
417
|
+
const tailCount = chunks.slice(i + 1).reduce((s, c) => s + c.length, 0);
|
|
418
|
+
try {
|
|
419
|
+
await this.flushChunk(chunks[i], tailCount);
|
|
420
|
+
} catch (err) {
|
|
421
|
+
if (!this.retry.isDisabled()) {
|
|
422
|
+
const tail = chunks.slice(i + 1).flat();
|
|
423
|
+
if (tail.length > 0) {
|
|
424
|
+
this.queue.splice(chunks[i].length, 0, ...tail);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
throw err;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (this.options.flushOnCapture && this.queue.length > 0) {
|
|
431
|
+
this.scheduleFlush();
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
close() {
|
|
435
|
+
if (this.timer) {
|
|
436
|
+
clearInterval(this.timer);
|
|
437
|
+
this.timer = void 0;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
async flushChunk(chunk, tailCount) {
|
|
441
|
+
try {
|
|
442
|
+
await executeHttpSend(
|
|
443
|
+
chunk,
|
|
444
|
+
this.options.endpoint,
|
|
445
|
+
this.options.apiKey,
|
|
446
|
+
this.fetchImpl
|
|
447
|
+
);
|
|
448
|
+
} catch (err) {
|
|
449
|
+
const m = err instanceof Error ? /flush failed with (\d+)/.exec(err.message) : null;
|
|
450
|
+
this.handleFlushFailure(
|
|
451
|
+
chunk,
|
|
452
|
+
tailCount,
|
|
453
|
+
m ? { reason: "status", status: Number(m[1]) } : { reason: "network", error: err }
|
|
454
|
+
);
|
|
455
|
+
throw err;
|
|
456
|
+
}
|
|
457
|
+
this.retry.reset();
|
|
458
|
+
debugLog("flush complete", this.options, { count: chunk.length });
|
|
459
|
+
}
|
|
460
|
+
handleFlushFailure(batch, tailCount, detail) {
|
|
461
|
+
const result = this.retry.recordFailure();
|
|
462
|
+
if (result.justDisabled) {
|
|
463
|
+
const dropped = this.queue.length + batch.length + tailCount;
|
|
464
|
+
this.queue = [];
|
|
465
|
+
this.close();
|
|
466
|
+
const msg = buildFlushDisabledMessage(
|
|
467
|
+
this.options.label,
|
|
468
|
+
result.consecutiveFailures,
|
|
469
|
+
dropped
|
|
470
|
+
);
|
|
471
|
+
if (typeof console.warn === "function") console.warn(msg);
|
|
472
|
+
debugLog("disabled", this.options, {
|
|
473
|
+
consecutiveFailures: result.consecutiveFailures,
|
|
474
|
+
dropped
|
|
475
|
+
});
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
this.queue.unshift(...batch);
|
|
479
|
+
const base = {
|
|
480
|
+
restoredCount: batch.length,
|
|
481
|
+
consecutiveFailures: result.consecutiveFailures,
|
|
482
|
+
backoffMs: result.backoffMs
|
|
483
|
+
};
|
|
484
|
+
if (detail.reason === "network") {
|
|
485
|
+
debugLog("flush network error", this.options, {
|
|
486
|
+
error: detail.error,
|
|
487
|
+
...base
|
|
488
|
+
});
|
|
489
|
+
} else {
|
|
490
|
+
debugLog("flush failed", this.options, {
|
|
491
|
+
status: detail.status,
|
|
492
|
+
...base
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
scheduleFlush() {
|
|
497
|
+
if (this.flushScheduled) return;
|
|
498
|
+
this.flushScheduled = true;
|
|
499
|
+
queueMicrotask(() => {
|
|
500
|
+
this.flushScheduled = false;
|
|
501
|
+
if (this.queue.length === 0 || this.retry.isBackingOff()) return;
|
|
502
|
+
this.flush().catch(
|
|
503
|
+
(err) => debugLog("scheduled flush error", this.options, { error: err })
|
|
504
|
+
);
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
// src/client-autocapture.ts
|
|
510
|
+
var BrowserAutoCapture = class {
|
|
511
|
+
constructor(onCapture, withDefaults, onPageView, config) {
|
|
512
|
+
this.onCapture = onCapture;
|
|
513
|
+
this.withDefaults = withDefaults;
|
|
514
|
+
this.onPageView = onPageView;
|
|
515
|
+
this.config = config;
|
|
516
|
+
}
|
|
517
|
+
onCapture;
|
|
518
|
+
withDefaults;
|
|
519
|
+
onPageView;
|
|
520
|
+
config;
|
|
521
|
+
attach() {
|
|
522
|
+
if (typeof window === "undefined") return;
|
|
523
|
+
if (this.config.errors) {
|
|
307
524
|
window.addEventListener("error", this.handleWindowError);
|
|
308
525
|
}
|
|
309
|
-
if (
|
|
526
|
+
if (this.config.unhandledRejections) {
|
|
310
527
|
window.addEventListener(
|
|
311
528
|
"unhandledrejection",
|
|
312
529
|
this.handleUnhandledRejection
|
|
313
530
|
);
|
|
314
531
|
}
|
|
315
|
-
if (
|
|
316
|
-
this.
|
|
532
|
+
if (this.config.pageViews) {
|
|
533
|
+
this.onPageView();
|
|
317
534
|
window.addEventListener("popstate", this.handlePopState);
|
|
318
535
|
this.patchHistoryMethod("pushState");
|
|
319
536
|
this.patchHistoryMethod("replaceState");
|
|
320
537
|
}
|
|
321
|
-
|
|
538
|
+
}
|
|
539
|
+
detach() {
|
|
540
|
+
if (typeof window === "undefined") return;
|
|
541
|
+
if (this.config.errors) {
|
|
542
|
+
window.removeEventListener("error", this.handleWindowError);
|
|
543
|
+
}
|
|
544
|
+
if (this.config.unhandledRejections) {
|
|
545
|
+
window.removeEventListener(
|
|
546
|
+
"unhandledrejection",
|
|
547
|
+
this.handleUnhandledRejection
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
if (this.config.pageViews) {
|
|
551
|
+
window.removeEventListener("popstate", this.handlePopState);
|
|
552
|
+
this.restoreHistoryMethod("pushState");
|
|
553
|
+
this.restoreHistoryMethod("replaceState");
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
handleWindowError = (event) => {
|
|
557
|
+
const serialized = serializeError(event.error ?? event.message);
|
|
558
|
+
this.onCapture({
|
|
559
|
+
type: "error",
|
|
560
|
+
name: "error",
|
|
561
|
+
error: { ...serialized, handled: false },
|
|
562
|
+
...this.withDefaults({
|
|
563
|
+
context: {
|
|
564
|
+
source: "window.error",
|
|
565
|
+
url: window.location.href,
|
|
566
|
+
page: window.location.pathname
|
|
567
|
+
}
|
|
568
|
+
})
|
|
569
|
+
});
|
|
570
|
+
};
|
|
571
|
+
handlePopState = () => {
|
|
572
|
+
this.onPageView();
|
|
573
|
+
};
|
|
574
|
+
handleUnhandledRejection = (event) => {
|
|
575
|
+
const serialized = serializeError(event.reason);
|
|
576
|
+
this.onCapture({
|
|
577
|
+
type: "error",
|
|
578
|
+
name: "error",
|
|
579
|
+
error: { ...serialized, handled: false },
|
|
580
|
+
...this.withDefaults({
|
|
581
|
+
context: {
|
|
582
|
+
source: "window.unhandledrejection",
|
|
583
|
+
url: window.location.href,
|
|
584
|
+
page: window.location.pathname
|
|
585
|
+
}
|
|
586
|
+
})
|
|
587
|
+
});
|
|
588
|
+
};
|
|
589
|
+
patchHistoryMethod(method) {
|
|
590
|
+
const original = history[method].bind(history);
|
|
591
|
+
const historyWithCustom = history;
|
|
592
|
+
historyWithCustom[`__ev_orig_${method}`] = original;
|
|
593
|
+
historyWithCustom[method] = (...args) => {
|
|
594
|
+
original(...args);
|
|
595
|
+
this.onPageView();
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
restoreHistoryMethod(method) {
|
|
599
|
+
const historyWithCustom = history;
|
|
600
|
+
const original = historyWithCustom[`__ev_orig_${method}`];
|
|
601
|
+
if (original) {
|
|
602
|
+
historyWithCustom[method] = original;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
// src/client.ts
|
|
608
|
+
var EmitVisionClient = class {
|
|
609
|
+
constructor(options) {
|
|
610
|
+
this.options = options;
|
|
611
|
+
this.deployment = options.deployment ? { ...options.deployment } : void 0;
|
|
612
|
+
this.featureFlags = options.featureFlags ? { ...options.featureFlags } : {};
|
|
613
|
+
this.flusher = new FlushManager(options, options.fetchImpl);
|
|
614
|
+
this.flagEval = new FlagEvaluator(
|
|
615
|
+
{
|
|
616
|
+
endpoint: options.endpoint,
|
|
617
|
+
apiKey: options.apiKey,
|
|
618
|
+
ttlMs: options.flagEvalTtlMs,
|
|
619
|
+
flagExposures: options.autoCapture.flagExposures,
|
|
620
|
+
fetchImpl: options.fetchImpl
|
|
621
|
+
},
|
|
622
|
+
(flags) => {
|
|
623
|
+
this.featureFlags = { ...this.featureFlags, ...flags };
|
|
624
|
+
},
|
|
625
|
+
(flagKey, variantKey, variantValue, reason, env) => this.captureExposure(flagKey, variantKey, variantValue, reason, env),
|
|
626
|
+
(msg, data) => debugLog(msg, this.options, data)
|
|
627
|
+
);
|
|
628
|
+
this.autoCapture = new BrowserAutoCapture(
|
|
629
|
+
(payload) => this.flusher.enqueue(payload),
|
|
630
|
+
(opts) => this.withDefaults(opts),
|
|
631
|
+
() => this.capturePageView(),
|
|
632
|
+
options.autoCapture
|
|
633
|
+
);
|
|
634
|
+
this.autoCapture.attach();
|
|
635
|
+
debugLog("initialized", this.options, {
|
|
322
636
|
endpoint: options.endpoint,
|
|
323
637
|
environment: options.environment,
|
|
324
638
|
release: options.release,
|
|
@@ -329,20 +643,17 @@ var EmitVisionClient = class {
|
|
|
329
643
|
});
|
|
330
644
|
}
|
|
331
645
|
options;
|
|
332
|
-
queue = [];
|
|
333
646
|
user;
|
|
334
647
|
context = {};
|
|
335
648
|
deployment;
|
|
336
649
|
featureFlags = {};
|
|
337
650
|
tags = {};
|
|
338
|
-
timer;
|
|
339
|
-
flushScheduled = false;
|
|
340
|
-
fetchImpl;
|
|
341
651
|
flagEval;
|
|
342
|
-
|
|
652
|
+
autoCapture;
|
|
653
|
+
flusher;
|
|
343
654
|
captureEvent(name, properties, options = {}) {
|
|
344
|
-
|
|
345
|
-
this.enqueue({
|
|
655
|
+
debugLog("queue event", this.options, { name, properties, options });
|
|
656
|
+
this.flusher.enqueue({
|
|
346
657
|
type: "event",
|
|
347
658
|
name,
|
|
348
659
|
properties,
|
|
@@ -351,12 +662,11 @@ var EmitVisionClient = class {
|
|
|
351
662
|
}
|
|
352
663
|
captureError(error, options = {}) {
|
|
353
664
|
const serialized = serializeError(error);
|
|
354
|
-
|
|
665
|
+
debugLog("queue error", this.options, {
|
|
355
666
|
message: serialized.message,
|
|
356
|
-
handled: serialized.handled,
|
|
357
667
|
options
|
|
358
668
|
});
|
|
359
|
-
this.enqueue({
|
|
669
|
+
this.flusher.enqueue({
|
|
360
670
|
type: "error",
|
|
361
671
|
name: "error",
|
|
362
672
|
error: serialized,
|
|
@@ -365,19 +675,19 @@ var EmitVisionClient = class {
|
|
|
365
675
|
}
|
|
366
676
|
identify(userIdOrUser, traits = {}) {
|
|
367
677
|
this.user = typeof userIdOrUser === "string" ? { id: userIdOrUser, ...traits } : { ...userIdOrUser };
|
|
368
|
-
|
|
678
|
+
debugLog("identified user", this.options, this.user);
|
|
369
679
|
}
|
|
370
680
|
setContext(context) {
|
|
371
681
|
this.context = { ...this.context, ...context };
|
|
372
|
-
|
|
682
|
+
debugLog("updated context", this.options, this.context);
|
|
373
683
|
}
|
|
374
684
|
setDeploymentContext(context) {
|
|
375
685
|
this.deployment = Object.keys(context).length > 0 ? { ...context } : void 0;
|
|
376
|
-
|
|
686
|
+
debugLog("updated deployment context", this.options, this.deployment);
|
|
377
687
|
}
|
|
378
688
|
setFeatureFlags(featureFlags) {
|
|
379
689
|
this.featureFlags = { ...featureFlags };
|
|
380
|
-
|
|
690
|
+
debugLog("updated feature flags", this.options, this.featureFlags);
|
|
381
691
|
}
|
|
382
692
|
evaluateFlags(opts) {
|
|
383
693
|
return this.flagEval.evaluateFlags({
|
|
@@ -413,270 +723,26 @@ var EmitVisionClient = class {
|
|
|
413
723
|
}
|
|
414
724
|
setTags(tags) {
|
|
415
725
|
this.tags = { ...this.tags, ...tags };
|
|
416
|
-
|
|
417
|
-
}
|
|
418
|
-
async flush() {
|
|
419
|
-
if (this.retry.isDisabled()) {
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
const batch = this.queue.splice(0, this.options.batchSize);
|
|
423
|
-
if (batch.length === 0) {
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
this.debug("flushing batch", { count: batch.length });
|
|
427
|
-
const chunks = splitBatch(batch);
|
|
428
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
429
|
-
try {
|
|
430
|
-
await this.flushChunk(chunks[i]);
|
|
431
|
-
} catch (err) {
|
|
432
|
-
if (!this.retry.isDisabled()) {
|
|
433
|
-
const tail = chunks.slice(i + 1).flat();
|
|
434
|
-
if (tail.length > 0) {
|
|
435
|
-
this.queue.splice(chunks[i].length, 0, ...tail);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
throw err;
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
if (this.options.flushOnCapture && this.queue.length > 0) {
|
|
442
|
-
this.scheduleFlush();
|
|
443
|
-
}
|
|
726
|
+
debugLog("updated tags", this.options, this.tags);
|
|
444
727
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
try {
|
|
448
|
-
response = await this.fetchImpl(`${this.options.endpoint}/v1/batch`, {
|
|
449
|
-
method: "POST",
|
|
450
|
-
headers: {
|
|
451
|
-
"content-type": "application/json",
|
|
452
|
-
"x-emit-api-key": this.options.apiKey
|
|
453
|
-
},
|
|
454
|
-
body: buildBatchBody(chunk),
|
|
455
|
-
keepalive: true
|
|
456
|
-
});
|
|
457
|
-
} catch (err) {
|
|
458
|
-
this.handleFlushFailure(chunk, { reason: "network", error: err });
|
|
459
|
-
throw err;
|
|
460
|
-
}
|
|
461
|
-
if (!response.ok) {
|
|
462
|
-
const error = new Error(
|
|
463
|
-
`emit-vision flush failed with ${response.status}`
|
|
464
|
-
);
|
|
465
|
-
this.handleFlushFailure(chunk, {
|
|
466
|
-
reason: "status",
|
|
467
|
-
status: response.status
|
|
468
|
-
});
|
|
469
|
-
throw error;
|
|
470
|
-
}
|
|
471
|
-
this.retry.reset();
|
|
472
|
-
this.debug("flush complete", { count: chunk.length });
|
|
473
|
-
}
|
|
474
|
-
handleFlushFailure(batch, detail) {
|
|
475
|
-
const result = this.retry.recordFailure();
|
|
476
|
-
if (result.justDisabled) {
|
|
477
|
-
const droppedQueued = this.queue.length;
|
|
478
|
-
const droppedBatch = batch.length;
|
|
479
|
-
this.queue = [];
|
|
480
|
-
this.stopBackgroundWork();
|
|
481
|
-
const message = `[emit-vision${this.options.label ? `:${this.options.label}` : ""}] disabled after ${result.consecutiveFailures} consecutive flush failures; dropping ${droppedQueued + droppedBatch} queued events. Re-init the SDK to resume.`;
|
|
482
|
-
if (typeof console.warn === "function") {
|
|
483
|
-
console.warn(message);
|
|
484
|
-
}
|
|
485
|
-
this.debug("disabled", {
|
|
486
|
-
consecutiveFailures: result.consecutiveFailures,
|
|
487
|
-
dropped: droppedQueued + droppedBatch
|
|
488
|
-
});
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
this.queue.unshift(...batch);
|
|
492
|
-
if (detail.reason === "network") {
|
|
493
|
-
this.debug("flush network error", {
|
|
494
|
-
error: detail.error,
|
|
495
|
-
restoredCount: batch.length,
|
|
496
|
-
consecutiveFailures: result.consecutiveFailures,
|
|
497
|
-
backoffMs: result.backoffMs
|
|
498
|
-
});
|
|
499
|
-
} else {
|
|
500
|
-
this.debug("flush failed", {
|
|
501
|
-
status: detail.status,
|
|
502
|
-
restoredCount: batch.length,
|
|
503
|
-
consecutiveFailures: result.consecutiveFailures,
|
|
504
|
-
backoffMs: result.backoffMs
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
stopBackgroundWork() {
|
|
509
|
-
if (this.timer) {
|
|
510
|
-
clearInterval(this.timer);
|
|
511
|
-
this.timer = void 0;
|
|
512
|
-
}
|
|
728
|
+
flush() {
|
|
729
|
+
return this.flusher.flush();
|
|
513
730
|
}
|
|
514
731
|
close() {
|
|
515
|
-
this.
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}
|
|
530
|
-
this.debug("closed client");
|
|
531
|
-
}
|
|
532
|
-
enqueueMetaEvent(originalName, originalBytes, trimmedFields) {
|
|
533
|
-
this.queue.push({
|
|
534
|
-
type: "event",
|
|
535
|
-
name: "sdk.event_truncated",
|
|
536
|
-
properties: { originalName, originalBytes, trimmedFields, sdkName: SDK_NAME },
|
|
537
|
-
environment: this.options.environment,
|
|
538
|
-
release: this.options.release
|
|
539
|
-
});
|
|
540
|
-
}
|
|
541
|
-
enqueue(payload) {
|
|
542
|
-
if (this.retry.isDisabled()) {
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
const { payload: trimmed, trimmedFields, originalBytes } = trimEventIfOversized(payload);
|
|
546
|
-
this.queue.push(trimmed);
|
|
547
|
-
if (trimmedFields !== null) {
|
|
548
|
-
this.enqueueMetaEvent(payload.name, originalBytes, trimmedFields);
|
|
549
|
-
}
|
|
550
|
-
this.debug("queued payload", {
|
|
551
|
-
type: payload.type,
|
|
552
|
-
name: payload.name,
|
|
553
|
-
queueSize: this.queue.length,
|
|
554
|
-
flushOnCapture: this.options.flushOnCapture,
|
|
555
|
-
batchSize: this.options.batchSize
|
|
556
|
-
});
|
|
557
|
-
if (this.options.flushOnCapture) {
|
|
558
|
-
this.debug("capture flush scheduled", {
|
|
559
|
-
queueSize: this.queue.length
|
|
560
|
-
});
|
|
561
|
-
if (!this.retry.isBackingOff()) {
|
|
562
|
-
this.flush().catch(
|
|
563
|
-
(err) => this.debug("capture flush error", { error: err })
|
|
564
|
-
);
|
|
565
|
-
}
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
if (this.queue.length >= this.options.batchSize && !this.retry.isBackingOff()) {
|
|
569
|
-
this.flush().catch(
|
|
570
|
-
(err) => this.debug("batch flush error", { error: err })
|
|
571
|
-
);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
scheduleFlush() {
|
|
575
|
-
if (this.flushScheduled) {
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
|
-
this.flushScheduled = true;
|
|
579
|
-
queueMicrotask(() => {
|
|
580
|
-
this.flushScheduled = false;
|
|
581
|
-
if (this.queue.length === 0 || this.retry.isBackingOff()) {
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
this.flush().catch(
|
|
585
|
-
(err) => this.debug("scheduled flush error", { error: err })
|
|
586
|
-
);
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
withDefaults(options) {
|
|
590
|
-
const {
|
|
591
|
-
context,
|
|
592
|
-
deployment,
|
|
593
|
-
featureFlags,
|
|
594
|
-
tags,
|
|
595
|
-
user,
|
|
596
|
-
environment,
|
|
597
|
-
release,
|
|
598
|
-
sessionId,
|
|
599
|
-
...rest
|
|
600
|
-
} = options;
|
|
601
|
-
return {
|
|
602
|
-
...rest,
|
|
603
|
-
environment: environment ?? this.options.environment,
|
|
604
|
-
release: release ?? this.options.release,
|
|
605
|
-
sessionId: sessionId ?? this.options.sessionId,
|
|
606
|
-
user: user ?? this.user,
|
|
607
|
-
context: Object.keys(this.context).length ? { ...this.context, ...context } : context,
|
|
608
|
-
deployment: this.mergeDeployment(deployment),
|
|
609
|
-
featureFlags: this.mergeFeatureFlags(featureFlags),
|
|
610
|
-
tags: Object.keys(this.tags).length ? { ...this.tags, ...tags } : tags
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
|
-
mergeDeployment(deployment) {
|
|
614
|
-
const merged = {
|
|
615
|
-
...this.deployment ?? {},
|
|
616
|
-
...deployment ?? {}
|
|
617
|
-
};
|
|
618
|
-
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
619
|
-
}
|
|
620
|
-
mergeFeatureFlags(featureFlags) {
|
|
621
|
-
const merged = {
|
|
622
|
-
...this.featureFlags,
|
|
623
|
-
...featureFlags ?? {}
|
|
624
|
-
};
|
|
625
|
-
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
626
|
-
}
|
|
627
|
-
handleWindowError = (event) => {
|
|
628
|
-
const serialized = serializeError(event.error ?? event.message);
|
|
629
|
-
this.debug("auto-captured window error", {
|
|
630
|
-
message: serialized.message
|
|
631
|
-
});
|
|
632
|
-
this.enqueue({
|
|
633
|
-
type: "error",
|
|
634
|
-
name: "error",
|
|
635
|
-
error: { ...serialized, handled: false },
|
|
636
|
-
...this.withDefaults({ context: { source: "window.error" } })
|
|
637
|
-
});
|
|
638
|
-
};
|
|
639
|
-
handlePopState = () => {
|
|
640
|
-
this.capturePageView();
|
|
641
|
-
};
|
|
642
|
-
patchHistoryMethod(method) {
|
|
643
|
-
const original = history[method].bind(history);
|
|
644
|
-
history[`__ev_orig_${method}`] = original;
|
|
645
|
-
history[method] = (...args) => {
|
|
646
|
-
original(...args);
|
|
647
|
-
this.capturePageView();
|
|
648
|
-
};
|
|
649
|
-
}
|
|
650
|
-
restoreHistoryMethod(method) {
|
|
651
|
-
const original = history[`__ev_orig_${method}`];
|
|
652
|
-
if (original) {
|
|
653
|
-
history[method] = original;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
handleUnhandledRejection = (event) => {
|
|
657
|
-
const serialized = serializeError(event.reason);
|
|
658
|
-
this.debug("auto-captured rejection", {
|
|
659
|
-
message: serialized.message
|
|
660
|
-
});
|
|
661
|
-
this.enqueue({
|
|
662
|
-
type: "error",
|
|
663
|
-
name: "error",
|
|
664
|
-
error: { ...serialized, handled: false },
|
|
665
|
-
...this.withDefaults({
|
|
666
|
-
context: { source: "window.unhandledrejection" }
|
|
667
|
-
})
|
|
668
|
-
});
|
|
669
|
-
};
|
|
670
|
-
debug(message, payload) {
|
|
671
|
-
if (!this.options.debug || typeof console.debug !== "function") {
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
const prefix = this.options.label ? `[emit-vision:${this.options.label}]` : "[emit-vision]";
|
|
675
|
-
if (payload === void 0) {
|
|
676
|
-
console.debug(prefix, message);
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
console.debug(prefix, message, payload);
|
|
732
|
+
this.flusher.close();
|
|
733
|
+
this.autoCapture.detach();
|
|
734
|
+
debugLog("closed client", this.options);
|
|
735
|
+
}
|
|
736
|
+
withDefaults(opts) {
|
|
737
|
+
return buildWithDefaults(
|
|
738
|
+
this.user,
|
|
739
|
+
this.context,
|
|
740
|
+
this.deployment,
|
|
741
|
+
this.featureFlags,
|
|
742
|
+
this.tags,
|
|
743
|
+
this.options,
|
|
744
|
+
opts
|
|
745
|
+
);
|
|
680
746
|
}
|
|
681
747
|
};
|
|
682
748
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emit-vision/sdk-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Browser SDK for self-hosted emit-vision analytics and error tracking.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
}
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
|
-
"build": "tsup --config tsup.config.ts"
|
|
37
|
+
"build": "tsup --config tsup.config.ts",
|
|
38
|
+
"prepack": "pnpm run build"
|
|
38
39
|
}
|
|
39
|
-
}
|
|
40
|
+
}
|