@emit-vision/sdk-js 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -218,3 +218,7 @@ init({
218
218
  - Do not use `email` or `username` as the `evaluationKey` — use an opaque user ID or session ID.
219
219
  - Use the helper packages when they reduce boilerplate, but keep direct browser
220
220
  SDK usage for the simplest client-side apps.
221
+
222
+ ## License
223
+
224
+ MIT — see [LICENSE](./LICENSE).
package/dist/index.d.ts CHANGED
@@ -38,6 +38,7 @@ type CaptureOptions = {
38
38
  release?: string;
39
39
  sessionId?: string;
40
40
  user?: EmitVisionUser;
41
+ groupId?: string;
41
42
  context?: Record<string, unknown>;
42
43
  deployment?: DeploymentContext;
43
44
  featureFlags?: FeatureFlagsContext;
@@ -60,6 +61,10 @@ type EmitVisionOptions = {
60
61
  flushIntervalMs?: number;
61
62
  batchSize?: number;
62
63
  fetchImpl?: typeof fetch;
64
+ /** Project ID — passed through to the DevTools debug channel so the extension can build dashboard deep links. Not required for event capture. */
65
+ projectId?: string;
66
+ /** Initial group ID to associate with all events. Can be overridden per-event or updated via `group()`. */
67
+ groupId?: string;
63
68
  /** TTL in milliseconds for in-memory flag evaluation cache. Default: 60 000 (1 minute). */
64
69
  flagEvalTtlMs?: number;
65
70
  /** 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. */
@@ -68,17 +73,72 @@ type EmitVisionOptions = {
68
73
  retryBackoffMaxMs?: number;
69
74
  /** 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
75
  maxConsecutiveFlushFailures?: number;
76
+ /** When true, events are queued in memory but not sent until `consent()` is called. Default: false. */
77
+ consentRequired?: boolean;
78
+ /** Set to true when `consentRequired` is true but consent is already known at init time (e.g. read from a cookie server-side). Skips the consent gate. Default: false. */
79
+ consentGiven?: boolean;
71
80
  };
72
81
  type RequiredEmitVisionOptions = Required<Pick<EmitVisionOptions, "apiKey" | "endpoint" | "flushIntervalMs" | "batchSize" | "fetchImpl" | "flushOnCapture" | "flagEvalTtlMs" | "retryBackoffInitialMs" | "retryBackoffMaxMs" | "maxConsecutiveFlushFailures">> & Pick<EmitVisionOptions, "environment" | "release" | "sessionId" | "label" | "deployment" | "featureFlags" | "debug"> & {
73
82
  autoCapture: Required<AutoCaptureOptions> & {
74
83
  flagExposures: boolean;
75
84
  pageViews: boolean;
76
85
  };
86
+ projectId?: string;
87
+ groupId?: string;
88
+ consentRequired?: boolean;
89
+ consentGiven?: boolean;
90
+ };
91
+
92
+ type DevToolsMessage = {
93
+ type: "init";
94
+ payload: {
95
+ sdkVersion: string;
96
+ endpoint: string;
97
+ environment?: string;
98
+ release?: string;
99
+ sessionId: string;
100
+ autoCapture: AutoCaptureOptions;
101
+ projectId?: string;
102
+ consentRequired?: boolean;
103
+ };
104
+ } | {
105
+ type: "event";
106
+ payload: {
107
+ name: string;
108
+ properties?: Record<string, unknown>;
109
+ eventId?: string;
110
+ timestamp?: string;
111
+ environment?: string;
112
+ sessionId?: string;
113
+ };
114
+ } | {
115
+ type: "identify";
116
+ payload: {
117
+ userId?: string;
118
+ traits?: Record<string, unknown>;
119
+ };
120
+ } | {
121
+ type: "flush";
122
+ payload: {
123
+ count: number;
124
+ };
125
+ } | {
126
+ type: "error";
127
+ payload: {
128
+ message: string;
129
+ };
130
+ } | {
131
+ type: "group";
132
+ payload: {
133
+ groupId: string;
134
+ traits?: Record<string, unknown>;
135
+ };
77
136
  };
78
137
 
79
138
  declare class EmitVisionClient {
80
139
  private readonly options;
81
140
  private user;
141
+ private groupId;
82
142
  private context;
83
143
  private deployment;
84
144
  private featureFlags;
@@ -86,10 +146,13 @@ declare class EmitVisionClient {
86
146
  private readonly flagEval;
87
147
  private readonly autoCapture;
88
148
  private readonly flusher;
149
+ private readonly persistOnHide;
150
+ private readonly persistOnHidden;
89
151
  constructor(options: RequiredEmitVisionOptions);
90
152
  captureEvent(name: string, properties?: Record<string, unknown>, options?: CaptureOptions): void;
91
153
  captureError(error: unknown, options?: CaptureOptions): void;
92
154
  identify(userIdOrUser: string | EmitVisionUser, traits?: EmitVisionTraits): void;
155
+ group(groupId: string, traits?: Record<string, unknown>): void;
93
156
  setContext(context: Record<string, unknown>): void;
94
157
  setDeploymentContext(context: DeploymentContext): void;
95
158
  setFeatureFlags(featureFlags: FeatureFlagsContext): void;
@@ -104,6 +167,7 @@ declare class EmitVisionClient {
104
167
  }, options?: CaptureOptions): void;
105
168
  captureExposure(flagKey: string, variantKey: string, variantValue?: string | number | boolean | null, reason?: string, environment?: string): void;
106
169
  setTags(tags: Record<string, string>): void;
170
+ consent(): Promise<void>;
107
171
  flush(): Promise<void>;
108
172
  close(): void;
109
173
  private withDefaults;
@@ -125,9 +189,11 @@ declare function setDeploymentContext(context: DeploymentContext): void;
125
189
  declare function setFeatureFlags(featureFlags: FeatureFlagsContext): void;
126
190
  declare function setTags(tags: Record<string, string>): void;
127
191
  declare function flush(): Promise<void>;
192
+ declare function consent(): Promise<void>;
193
+ declare function group(groupId: string, traits?: Record<string, unknown>): void;
128
194
  declare function evaluateFlags(options: FlagEvaluationOptions): Promise<FeatureFlagsContext>;
129
195
  declare function getFlag<T extends string | number | boolean | null>(flagKey: string, fallback: T, options: FlagEvaluationOptions): Promise<T>;
130
196
  declare function refreshFlags(options: FlagEvaluationOptions): Promise<FeatureFlagsContext>;
131
197
  declare function captureExposure(flagKey: string, variantKey: string, variantValue?: string | number | boolean | null, reason?: string, environment?: string): void;
132
198
 
133
- export { type AutoCaptureOptions, type CaptureOptions, type DeploymentContext, type EmitVisionOptions, type EmitVisionTraits, type EmitVisionUser, type FeatureFlagsContext, type FlagEvaluationOptions, captureError, captureEvent, captureExposure, captureFeedback, capturePageView, evaluateFlags, flush, getFlag, identify, init, refreshFlags, setContext, setDeploymentContext, setFeatureFlags, setTags };
199
+ export { type AutoCaptureOptions, type CaptureOptions, type DeploymentContext, type DevToolsMessage, type EmitVisionOptions, type EmitVisionTraits, type EmitVisionUser, type FeatureFlagsContext, type FlagEvaluationOptions, captureError, captureEvent, captureExposure, captureFeedback, capturePageView, consent, evaluateFlags, flush, getFlag, group, identify, init, refreshFlags, setContext, setDeploymentContext, setFeatureFlags, setTags };
package/dist/index.js CHANGED
@@ -1,4 +1,8 @@
1
1
  // src/flag-eval.ts
2
+ function cacheKeyFor(env, opts) {
3
+ const keys = opts.flagKeys?.length ? [...opts.flagKeys].sort().join(",") : "*";
4
+ return `${env}:${opts.evaluationKey}:${keys}`;
5
+ }
2
6
  var FlagEvaluator = class {
3
7
  constructor(cfg, onFlagsLoaded, captureExposure2, debug) {
4
8
  this.cfg = cfg;
@@ -14,7 +18,7 @@ var FlagEvaluator = class {
14
18
  async evaluateFlags(opts) {
15
19
  const env = opts.environment ?? "";
16
20
  const ttl = opts.ttlMs ?? this.cfg.ttlMs;
17
- const key = `${env}:${opts.evaluationKey}`;
21
+ const key = cacheKeyFor(env, opts);
18
22
  const cached = this.cache.get(key);
19
23
  if (cached && Date.now() < cached.expiresAt) {
20
24
  this.debug("flag eval cache hit", { key });
@@ -24,7 +28,9 @@ var FlagEvaluator = class {
24
28
  }
25
29
  async getFlag(flagKey, fallback, opts) {
26
30
  try {
27
- const flags = await this.evaluateFlags(opts);
31
+ const flags = await this.evaluateFlags(
32
+ opts.flagKeys?.length ? opts : { ...opts, flagKeys: [flagKey] }
33
+ );
28
34
  const value = flags[flagKey];
29
35
  return value !== void 0 ? value : fallback;
30
36
  } catch {
@@ -34,7 +40,7 @@ var FlagEvaluator = class {
34
40
  async refreshFlags(opts) {
35
41
  const env = opts.environment ?? "";
36
42
  const ttl = opts.ttlMs ?? this.cfg.ttlMs;
37
- const key = `${env}:${opts.evaluationKey}`;
43
+ const key = cacheKeyFor(env, opts);
38
44
  this.cache.delete(key);
39
45
  return this.fetchAndCacheFlags(opts, env, ttl, key);
40
46
  }
@@ -157,6 +163,15 @@ function resolveAutoCapture(options) {
157
163
  };
158
164
  }
159
165
 
166
+ // src/compress.ts
167
+ var COMPRESS_THRESHOLD_BYTES = 8 * 1024;
168
+ async function gzipBatchBody(body) {
169
+ if (typeof CompressionStream !== "function") return null;
170
+ const stream = new Blob([body]).stream().pipeThrough(new CompressionStream("gzip"));
171
+ const buffer = await new Response(stream).arrayBuffer();
172
+ return new Uint8Array(buffer);
173
+ }
174
+
160
175
  // src/client-helpers.ts
161
176
  var SDK_NAME = "emit-vision-js";
162
177
  var SDK_VERSION = "0.4.0";
@@ -276,15 +291,51 @@ function mergeFeatureFlagsFn(clientFlags, overrideFlags) {
276
291
  function buildFlushDisabledMessage(label, consecutiveFailures, dropped) {
277
292
  return `[emit-vision${label ? `:${label}` : ""}] disabled after ${consecutiveFailures} consecutive flush failures; dropping ${dropped} queued events. Re-init the SDK to resume.`;
278
293
  }
294
+ function buildAuthDisabledMessage(label, status, dropped) {
295
+ return `[emit-vision${label ? `:${label}` : ""}] SDK disabled: received ${AUTH_FAILURE_CAP} consecutive ${status} responses from the ingest API. This usually means your DSN or API key is invalid \u2014 check the dsn or apiKey option passed to init(). Dropping ${dropped} queued events.`;
296
+ }
297
+ var AUTH_FAILURE_CAP = 3;
298
+ var FlushHttpError = class extends Error {
299
+ constructor(status, retryAfterSeconds, serverMessage) {
300
+ super(`emit-vision flush failed with ${status}`);
301
+ this.status = status;
302
+ this.retryAfterSeconds = retryAfterSeconds;
303
+ this.serverMessage = serverMessage;
304
+ }
305
+ status;
306
+ retryAfterSeconds;
307
+ serverMessage;
308
+ };
279
309
  async function executeHttpSend(chunk, endpoint, apiKey, fetchImpl) {
310
+ const body = buildBatchBody(chunk);
311
+ const compressed = byteLength(body) >= COMPRESS_THRESHOLD_BYTES ? await gzipBatchBody(body) : null;
280
312
  const response = await fetchImpl(`${endpoint}/v1/batch`, {
281
313
  method: "POST",
282
- headers: { "content-type": "application/json", "x-emit-api-key": apiKey },
283
- body: buildBatchBody(chunk),
284
- keepalive: true
314
+ headers: {
315
+ "content-type": "application/json",
316
+ "x-emit-api-key": apiKey,
317
+ ...compressed ? { "content-encoding": "gzip" } : {}
318
+ },
319
+ body: compressed ?? body,
320
+ // keepalive lets a flush survive page unload, but browsers cap keepalive
321
+ // request bytes at ~64 KB total per page — a compressed send can be far
322
+ // bigger than that, so it skips keepalive and relies on the queue/retry
323
+ // path instead. Only the small, uncompressed path keeps the guarantee.
324
+ ...compressed ? {} : { keepalive: true }
285
325
  });
286
326
  if (!response.ok) {
287
- throw new Error(`emit-vision flush failed with ${response.status}`);
327
+ const retryAfter = Number(response.headers?.get?.("retry-after"));
328
+ let serverMessage;
329
+ try {
330
+ const body2 = await response.json();
331
+ if (body2 && typeof body2.message === "string") serverMessage = body2.message;
332
+ } catch {
333
+ }
334
+ throw new FlushHttpError(
335
+ response.status,
336
+ Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : void 0,
337
+ serverMessage
338
+ );
288
339
  }
289
340
  }
290
341
 
@@ -307,10 +358,20 @@ var FlushRetryController = class {
307
358
  if (this.disabled) return Infinity;
308
359
  return Math.max(0, this.backoffUntil - now);
309
360
  }
361
+ forceDisable() {
362
+ this.disabled = true;
363
+ this.backoffUntil = 0;
364
+ }
310
365
  reset() {
311
366
  this.consecutiveFailures = 0;
312
367
  this.backoffUntil = 0;
313
368
  }
369
+ /** Push the next retry out to honor a server-sent Retry-After hint. Capped at 5 minutes; never shortens an existing backoff. */
370
+ applyRetryAfter(seconds, now = Date.now()) {
371
+ if (this.disabled || !Number.isFinite(seconds) || seconds <= 0) return;
372
+ const cappedMs = Math.min(seconds, 300) * 1e3;
373
+ this.backoffUntil = Math.max(this.backoffUntil, now + cappedMs);
374
+ }
314
375
  recordFailure(now = Date.now()) {
315
376
  this.consecutiveFailures += 1;
316
377
  if (this.opts.failureCap > 0 && this.consecutiveFailures >= this.opts.failureCap) {
@@ -346,11 +407,77 @@ var FlushRetryController = class {
346
407
  }
347
408
  };
348
409
 
410
+ // src/persistence.ts
411
+ var MAX_PERSISTED_EVENTS = 200;
412
+ var MAX_PERSISTED_AGE_MS = 24 * 60 * 60 * 1e3;
413
+ function persistenceKey(apiKey, projectId) {
414
+ return `emit-vision:queue:${projectId ?? "-"}:${apiKey.slice(0, 16)}`;
415
+ }
416
+ function getStorage() {
417
+ try {
418
+ if (typeof localStorage === "undefined") return null;
419
+ return localStorage;
420
+ } catch {
421
+ return null;
422
+ }
423
+ }
424
+ function persistQueue(key, queue) {
425
+ const storage = getStorage();
426
+ if (!storage || queue.length === 0) return;
427
+ const record = {
428
+ savedAt: Date.now(),
429
+ events: queue.slice(0, MAX_PERSISTED_EVENTS)
430
+ };
431
+ try {
432
+ storage.setItem(key, JSON.stringify(record));
433
+ } catch {
434
+ }
435
+ }
436
+ function restoreQueue(key, now = Date.now()) {
437
+ const storage = getStorage();
438
+ if (!storage) return [];
439
+ try {
440
+ const raw = storage.getItem(key);
441
+ storage.removeItem(key);
442
+ if (!raw) return [];
443
+ const parsed = JSON.parse(raw);
444
+ if (typeof parsed.savedAt !== "number" || !Array.isArray(parsed.events)) {
445
+ return [];
446
+ }
447
+ if (now - parsed.savedAt > MAX_PERSISTED_AGE_MS) return [];
448
+ return parsed.events.slice(0, MAX_PERSISTED_EVENTS);
449
+ } catch {
450
+ return [];
451
+ }
452
+ }
453
+ function clearPersistedQueue(key) {
454
+ const storage = getStorage();
455
+ if (!storage) return;
456
+ try {
457
+ storage.removeItem(key);
458
+ } catch {
459
+ }
460
+ }
461
+
462
+ // src/devtools-channel.ts
463
+ function dispatchDevToolsEvent(type, payload) {
464
+ if (typeof window === "undefined" || typeof window.dispatchEvent !== "function") {
465
+ return;
466
+ }
467
+ window.dispatchEvent(
468
+ new CustomEvent("__emitVisionDevTools", {
469
+ detail: { type, payload, timestamp: Date.now() }
470
+ })
471
+ );
472
+ }
473
+
349
474
  // src/client-flush.ts
350
475
  var FlushManager = class {
351
476
  constructor(options, fetchImpl) {
352
477
  this.options = options;
353
478
  this.fetchImpl = fetchImpl;
479
+ this.consentGiven = !options.consentRequired || (options.consentGiven ?? false);
480
+ this.storageKey = persistenceKey(options.apiKey, options.projectId);
354
481
  this.retry = new FlushRetryController({
355
482
  initialMs: options.retryBackoffInitialMs,
356
483
  maxMs: options.retryBackoffMaxMs,
@@ -369,6 +496,9 @@ var FlushManager = class {
369
496
  timer;
370
497
  flushScheduled = false;
371
498
  retry;
499
+ storageKey;
500
+ consentGiven;
501
+ authFailureCount = 0;
372
502
  enqueue(payload) {
373
503
  if (this.retry.isDisabled()) return;
374
504
  const {
@@ -407,8 +537,27 @@ var FlushManager = class {
407
537
  );
408
538
  }
409
539
  }
540
+ giveConsent() {
541
+ this.consentGiven = true;
542
+ return this.flush();
543
+ }
544
+ /** Restore any queue persisted by a previous page load, prepending it so older events flush first. */
545
+ restorePersisted() {
546
+ const restored = restoreQueue(this.storageKey);
547
+ if (restored.length === 0) return;
548
+ this.queue.unshift(...restored);
549
+ debugLog("restored persisted queue", this.options, {
550
+ count: restored.length
551
+ });
552
+ }
553
+ /** Spill the current queue to localStorage (pagehide / tab hidden). No-op without consent. */
554
+ persistNow() {
555
+ if (!this.consentGiven || this.retry.isDisabled()) return;
556
+ persistQueue(this.storageKey, this.queue);
557
+ }
410
558
  async flush() {
411
559
  if (this.retry.isDisabled()) return;
560
+ if (!this.consentGiven) return;
412
561
  const batch = this.queue.splice(0, this.options.batchSize);
413
562
  if (batch.length === 0) return;
414
563
  debugLog("flushing batch", this.options, { count: batch.length });
@@ -446,19 +595,50 @@ var FlushManager = class {
446
595
  this.fetchImpl
447
596
  );
448
597
  } catch (err) {
449
- const m = err instanceof Error ? /flush failed with (\d+)/.exec(err.message) : null;
450
598
  this.handleFlushFailure(
451
599
  chunk,
452
600
  tailCount,
453
- m ? { reason: "status", status: Number(m[1]) } : { reason: "network", error: err }
601
+ err instanceof FlushHttpError ? {
602
+ reason: "status",
603
+ status: err.status,
604
+ retryAfterSeconds: err.retryAfterSeconds,
605
+ serverMessage: err.serverMessage
606
+ } : { reason: "network", error: err }
454
607
  );
455
608
  throw err;
456
609
  }
457
610
  this.retry.reset();
611
+ this.authFailureCount = 0;
612
+ clearPersistedQueue(this.storageKey);
458
613
  debugLog("flush complete", this.options, { count: chunk.length });
614
+ dispatchDevToolsEvent("flush", { count: chunk.length });
459
615
  }
460
616
  handleFlushFailure(batch, tailCount, detail) {
617
+ if (detail.reason === "status" && (detail.status === 401 || detail.status === 403)) {
618
+ this.authFailureCount += 1;
619
+ if (this.authFailureCount >= AUTH_FAILURE_CAP) {
620
+ const dropped = this.queue.length + batch.length + tailCount;
621
+ this.queue = [];
622
+ this.close();
623
+ this.retry.forceDisable();
624
+ const msg = buildAuthDisabledMessage(
625
+ this.options.label,
626
+ detail.status,
627
+ dropped
628
+ );
629
+ if (typeof console.warn === "function") console.warn(msg);
630
+ debugLog("disabled (auth)", this.options, {
631
+ status: detail.status,
632
+ authFailures: this.authFailureCount,
633
+ dropped
634
+ });
635
+ return;
636
+ }
637
+ }
461
638
  const result = this.retry.recordFailure();
639
+ if (detail.reason === "status" && (detail.status === 429 || detail.status === 503) && detail.retryAfterSeconds !== void 0) {
640
+ this.retry.applyRetryAfter(detail.retryAfterSeconds);
641
+ }
462
642
  if (result.justDisabled) {
463
643
  const dropped = this.queue.length + batch.length + tailCount;
464
644
  this.queue = [];
@@ -476,6 +656,7 @@ var FlushManager = class {
476
656
  return;
477
657
  }
478
658
  this.queue.unshift(...batch);
659
+ this.persistNow();
479
660
  const base = {
480
661
  restoredCount: batch.length,
481
662
  consecutiveFailures: result.consecutiveFailures,
@@ -489,6 +670,7 @@ var FlushManager = class {
489
670
  } else {
490
671
  debugLog("flush failed", this.options, {
491
672
  status: detail.status,
673
+ serverMessage: detail.serverMessage,
492
674
  ...base
493
675
  });
494
676
  }
@@ -608,9 +790,17 @@ var BrowserAutoCapture = class {
608
790
  var EmitVisionClient = class {
609
791
  constructor(options) {
610
792
  this.options = options;
793
+ this.groupId = options.groupId;
611
794
  this.deployment = options.deployment ? { ...options.deployment } : void 0;
612
795
  this.featureFlags = options.featureFlags ? { ...options.featureFlags } : {};
613
796
  this.flusher = new FlushManager(options, options.fetchImpl);
797
+ this.flusher.restorePersisted();
798
+ if (typeof window !== "undefined") {
799
+ window.addEventListener("pagehide", this.persistOnHide);
800
+ }
801
+ if (typeof document !== "undefined") {
802
+ document.addEventListener("visibilitychange", this.persistOnHidden);
803
+ }
614
804
  this.flagEval = new FlagEvaluator(
615
805
  {
616
806
  endpoint: options.endpoint,
@@ -641,9 +831,20 @@ var EmitVisionClient = class {
641
831
  flushIntervalMs: options.flushIntervalMs,
642
832
  flushOnCapture: options.flushOnCapture
643
833
  });
834
+ dispatchDevToolsEvent("init", {
835
+ sdkVersion: SDK_VERSION,
836
+ endpoint: options.endpoint,
837
+ environment: options.environment,
838
+ release: options.release,
839
+ sessionId: options.sessionId ?? "",
840
+ autoCapture: options.autoCapture,
841
+ projectId: options.projectId,
842
+ consentRequired: options.consentRequired
843
+ });
644
844
  }
645
845
  options;
646
846
  user;
847
+ groupId;
647
848
  context = {};
648
849
  deployment;
649
850
  featureFlags = {};
@@ -651,13 +852,28 @@ var EmitVisionClient = class {
651
852
  flagEval;
652
853
  autoCapture;
653
854
  flusher;
855
+ persistOnHide = () => this.flusher.persistNow();
856
+ persistOnHidden = () => {
857
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") {
858
+ this.flusher.persistNow();
859
+ }
860
+ };
654
861
  captureEvent(name, properties, options = {}) {
862
+ const eventId = options.eventId ?? (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : void 0);
655
863
  debugLog("queue event", this.options, { name, properties, options });
656
864
  this.flusher.enqueue({
657
865
  type: "event",
658
866
  name,
659
867
  properties,
660
- ...this.withDefaults(options)
868
+ ...this.withDefaults({ ...options, eventId })
869
+ });
870
+ dispatchDevToolsEvent("event", {
871
+ name,
872
+ properties,
873
+ eventId,
874
+ timestamp: options.timestamp,
875
+ environment: options.environment,
876
+ sessionId: options.sessionId
661
877
  });
662
878
  }
663
879
  captureError(error, options = {}) {
@@ -676,6 +892,13 @@ var EmitVisionClient = class {
676
892
  identify(userIdOrUser, traits = {}) {
677
893
  this.user = typeof userIdOrUser === "string" ? { id: userIdOrUser, ...traits } : { ...userIdOrUser };
678
894
  debugLog("identified user", this.options, this.user);
895
+ const { id, ...rest } = this.user;
896
+ dispatchDevToolsEvent("identify", { userId: id, traits: rest });
897
+ }
898
+ group(groupId, traits) {
899
+ this.groupId = groupId;
900
+ debugLog("set group", this.options, { groupId, traits });
901
+ dispatchDevToolsEvent("group", { groupId, traits });
679
902
  }
680
903
  setContext(context) {
681
904
  this.context = { ...this.context, ...context };
@@ -733,10 +956,19 @@ var EmitVisionClient = class {
733
956
  this.tags = { ...this.tags, ...tags };
734
957
  debugLog("updated tags", this.options, this.tags);
735
958
  }
959
+ consent() {
960
+ return this.flusher.giveConsent();
961
+ }
736
962
  flush() {
737
963
  return this.flusher.flush();
738
964
  }
739
965
  close() {
966
+ if (typeof window !== "undefined") {
967
+ window.removeEventListener("pagehide", this.persistOnHide);
968
+ }
969
+ if (typeof document !== "undefined") {
970
+ document.removeEventListener("visibilitychange", this.persistOnHidden);
971
+ }
740
972
  this.flusher.close();
741
973
  this.autoCapture.detach();
742
974
  debugLog("closed client", this.options);
@@ -749,7 +981,7 @@ var EmitVisionClient = class {
749
981
  this.featureFlags,
750
982
  this.tags,
751
983
  this.options,
752
- opts
984
+ { groupId: this.groupId, ...opts }
753
985
  );
754
986
  }
755
987
  };
@@ -809,6 +1041,12 @@ function setTags(tags) {
809
1041
  async function flush() {
810
1042
  await requireClient().flush();
811
1043
  }
1044
+ async function consent() {
1045
+ await requireClient().consent();
1046
+ }
1047
+ function group(groupId, traits) {
1048
+ requireClient().group(groupId, traits);
1049
+ }
812
1050
  async function evaluateFlags(options) {
813
1051
  return requireClient().evaluateFlags(options);
814
1052
  }
@@ -839,9 +1077,11 @@ export {
839
1077
  captureExposure,
840
1078
  captureFeedback,
841
1079
  capturePageView,
1080
+ consent,
842
1081
  evaluateFlags,
843
1082
  flush,
844
1083
  getFlag,
1084
+ group,
845
1085
  identify,
846
1086
  init,
847
1087
  refreshFlags,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emit-vision/sdk-js",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
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,19 @@
34
34
  }
35
35
  },
36
36
  "scripts": {
37
- "build": "tsup --config tsup.config.ts"
37
+ "build": "tsup --config tsup.config.ts",
38
+ "size": "size-limit",
39
+ "prepack": "pnpm run build"
40
+ },
41
+ "size-limit": [
42
+ {
43
+ "name": "@emit-vision/sdk-js (gzip)",
44
+ "path": "dist/index.js",
45
+ "limit": "9 kB"
46
+ }
47
+ ],
48
+ "devDependencies": {
49
+ "@size-limit/file": "^11.1.6",
50
+ "size-limit": "^11.1.6"
38
51
  }
39
- }
52
+ }