@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 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 retry;
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.3.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: { ...payload, ...base, error: { ...payload.error, stack: void 0 } },
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
- var EmitVisionClient = class {
274
- constructor(options) {
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 = options.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) => this.debug("background flush error", { error: err })
362
+ (err) => debugLog("background flush error", this.options, { error: err })
304
363
  );
305
364
  }, options.flushIntervalMs);
306
- if (options.autoCapture.errors && typeof window !== "undefined") {
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 (options.autoCapture.unhandledRejections && typeof window !== "undefined") {
526
+ if (this.config.unhandledRejections) {
310
527
  window.addEventListener(
311
528
  "unhandledrejection",
312
529
  this.handleUnhandledRejection
313
530
  );
314
531
  }
315
- if (options.autoCapture.pageViews && typeof window !== "undefined") {
316
- this.capturePageView();
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
- this.debug("initialized", {
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
- retry;
652
+ autoCapture;
653
+ flusher;
343
654
  captureEvent(name, properties, options = {}) {
344
- this.debug("queue event", { name, properties, options });
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
- this.debug("queue error", {
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
- this.debug("identified user", this.user);
678
+ debugLog("identified user", this.options, this.user);
369
679
  }
370
680
  setContext(context) {
371
681
  this.context = { ...this.context, ...context };
372
- this.debug("updated context", this.context);
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
- this.debug("updated deployment context", this.deployment);
686
+ debugLog("updated deployment context", this.options, this.deployment);
377
687
  }
378
688
  setFeatureFlags(featureFlags) {
379
689
  this.featureFlags = { ...featureFlags };
380
- this.debug("updated feature flags", this.featureFlags);
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
- this.debug("updated tags", this.tags);
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
- async flushChunk(chunk) {
446
- let response;
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.stopBackgroundWork();
516
- if (typeof window !== "undefined" && this.options.autoCapture.errors) {
517
- window.removeEventListener("error", this.handleWindowError);
518
- }
519
- if (typeof window !== "undefined" && this.options.autoCapture.unhandledRejections) {
520
- window.removeEventListener(
521
- "unhandledrejection",
522
- this.handleUnhandledRejection
523
- );
524
- }
525
- if (typeof window !== "undefined" && this.options.autoCapture.pageViews) {
526
- window.removeEventListener("popstate", this.handlePopState);
527
- this.restoreHistoryMethod("pushState");
528
- this.restoreHistoryMethod("replaceState");
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.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
+ }