@emit-vision/sdk-js 0.4.3 → 0.5.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.js CHANGED
@@ -1,1092 +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
- }
6
- var FlagEvaluator = class {
7
- constructor(cfg, onFlagsLoaded, captureExposure2, debug) {
8
- this.cfg = cfg;
9
- this.onFlagsLoaded = onFlagsLoaded;
10
- this.captureExposure = captureExposure2;
11
- this.debug = debug;
12
- }
13
- cfg;
14
- onFlagsLoaded;
15
- captureExposure;
16
- debug;
17
- cache = /* @__PURE__ */ new Map();
18
- async evaluateFlags(opts) {
19
- const env = opts.environment ?? "";
20
- const ttl = opts.ttlMs ?? this.cfg.ttlMs;
21
- const key = cacheKeyFor(env, opts);
22
- const cached = this.cache.get(key);
23
- if (cached && Date.now() < cached.expiresAt) {
24
- this.debug("flag eval cache hit", { key });
25
- return cached.flags;
26
- }
27
- return this.fetchAndCacheFlags(opts, env, ttl, key);
28
- }
29
- async getFlag(flagKey, fallback, opts) {
30
- try {
31
- const flags = await this.evaluateFlags(
32
- opts.flagKeys?.length ? opts : { ...opts, flagKeys: [flagKey] }
33
- );
34
- const value = flags[flagKey];
35
- return value !== void 0 ? value : fallback;
36
- } catch {
37
- return fallback;
38
- }
39
- }
40
- async refreshFlags(opts) {
41
- const env = opts.environment ?? "";
42
- const ttl = opts.ttlMs ?? this.cfg.ttlMs;
43
- const key = cacheKeyFor(env, opts);
44
- this.cache.delete(key);
45
- return this.fetchAndCacheFlags(opts, env, ttl, key);
46
- }
47
- async fetchAndCacheFlags(opts, env, ttl, cacheKey) {
48
- let flags = {};
49
- let evaluations = {};
50
- try {
51
- const body = {
52
- environment: env,
53
- userKey: opts.evaluationKey
54
- };
55
- if (opts.flagKeys && opts.flagKeys.length > 0) {
56
- body.flagKeys = opts.flagKeys;
57
- }
58
- const response = await this.cfg.fetchImpl(
59
- `${this.cfg.endpoint}/v1/flags/evaluate`,
60
- {
61
- method: "POST",
62
- headers: {
63
- "content-type": "application/json",
64
- "x-emit-api-key": this.cfg.apiKey
65
- },
66
- body: JSON.stringify(body)
67
- }
68
- );
69
- if (response.ok) {
70
- const data = await response.json();
71
- evaluations = data.evaluations;
72
- flags = Object.fromEntries(
73
- Object.entries(evaluations).map(([k, v]) => [k, v.variantValue])
74
- );
75
- this.debug("flag eval complete", { count: Object.keys(flags).length });
76
- } else {
77
- this.debug("flag eval response error", { status: response.status });
78
- }
79
- } catch (err) {
80
- this.debug("flag eval network error", { error: err });
81
- }
82
- this.cache.set(cacheKey, { flags, expiresAt: Date.now() + ttl });
83
- this.onFlagsLoaded(flags);
84
- if (this.cfg.flagExposures) {
85
- for (const [flagKey, entry] of Object.entries(evaluations)) {
86
- this.captureExposure(
87
- flagKey,
88
- entry.variantKey,
89
- entry.variantValue,
90
- entry.reason,
91
- env
92
- );
93
- }
94
- }
95
- return flags;
96
- }
97
- };
98
-
99
- // src/util.ts
100
- function parseDsn(dsn) {
101
- const parsed = new URL(dsn);
102
- const apiKey = decodeURIComponent(parsed.username);
103
- if (!apiKey) {
104
- throw new Error("emit-vision dsn must include the API key before the @");
105
- }
106
- const pathname = stripTrailingSlash(parsed.pathname) ?? "";
107
- const endpointPath = pathname.endsWith("/v1") ? pathname.slice(0, -"/v1".length) : pathname;
108
- return {
109
- apiKey,
110
- endpoint: stripTrailingSlash(`${parsed.origin}${endpointPath}`)
111
- };
112
- }
113
- function stripTrailingSlash(value) {
114
- if (!value) {
115
- return value;
116
- }
117
- return value.endsWith("/") ? value.slice(0, -1) : value;
118
- }
119
- function serializeError(error) {
120
- if (error instanceof Error) {
121
- return {
122
- message: error.message,
123
- name: error.name,
124
- stack: error.stack,
125
- handled: true
126
- };
127
- }
128
- return {
129
- message: typeof error === "string" ? error : JSON.stringify(error),
130
- handled: true
131
- };
132
- }
133
- function resolveTransport(options) {
134
- const dsn = options.dsn ? parseDsn(options.dsn) : null;
135
- const apiKey = options.apiKey ?? dsn?.apiKey;
136
- if (!apiKey) {
137
- throw new Error("emit-vision init requires either apiKey or dsn");
138
- }
139
- return {
140
- apiKey,
141
- endpoint: stripTrailingSlash(options.endpoint ?? dsn?.endpoint)
142
- };
143
- }
144
- var ANON_ID_KEY = "__ev_anon_id";
145
- function getOrCreateAnonymousId() {
146
- try {
147
- const stored = localStorage.getItem(ANON_ID_KEY);
148
- if (stored) return stored;
149
- const id = crypto.randomUUID();
150
- localStorage.setItem(ANON_ID_KEY, id);
151
- return id;
152
- } catch {
153
- return void 0;
154
- }
155
- }
156
- function resolveAutoCapture(options) {
157
- const legacyDefault = options.autoCaptureErrors === void 0 ? true : options.autoCaptureErrors;
158
- return {
159
- errors: options.autoCapture?.errors ?? legacyDefault,
160
- unhandledRejections: options.autoCapture?.unhandledRejections ?? legacyDefault,
161
- flagExposures: options.autoCapture?.flagExposures ?? true,
162
- pageViews: options.autoCapture?.pageViews ?? false
163
- };
164
- }
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
-
175
- // src/client-helpers.ts
176
- var SDK_NAME = "emit-vision-js";
177
- var SDK_VERSION = "0.4.0";
178
- var MAX_EVENT_BYTES = 64 * 1024;
179
- var MAX_BATCH_BYTES = 1 * 1024 * 1024;
180
- function byteLength(s) {
181
- return new TextEncoder().encode(s).length;
182
- }
183
- function trimEventIfOversized(payload) {
184
- const json = JSON.stringify(payload);
185
- const originalBytes = byteLength(json);
186
- if (originalBytes <= MAX_EVENT_BYTES) {
187
- return { payload, trimmedFields: null, originalBytes };
188
- }
189
- const propBytes = payload.properties ? byteLength(JSON.stringify(payload.properties)) : 0;
190
- const ctxBytes = payload.context ? byteLength(JSON.stringify(payload.context)) : 0;
191
- if (typeof console.warn === "function") {
192
- console.warn(
193
- `[emit-vision] event truncated \u2014 "${payload.name}" was ${originalBytes} bytes (max ${MAX_EVENT_BYTES}); properties/context trimmed.`
194
- );
195
- }
196
- const trimmedFields = [];
197
- if (propBytes > 0) trimmedFields.push("properties");
198
- if (ctxBytes > 0) trimmedFields.push("context");
199
- if (payload.type === "error" && payload.error.stack) {
200
- trimmedFields.push("error.stack");
201
- }
202
- const base = {
203
- properties: propBytes > 0 ? { _truncated: true, _originalBytes: propBytes } : void 0,
204
- context: ctxBytes > 0 ? { _truncated: true, _originalBytes: ctxBytes } : void 0
205
- };
206
- if (payload.type === "error") {
207
- return {
208
- payload: {
209
- ...payload,
210
- ...base,
211
- error: { ...payload.error, stack: void 0 }
212
- },
213
- trimmedFields,
214
- originalBytes
215
- };
216
- }
217
- return { payload: { ...payload, ...base }, trimmedFields, originalBytes };
218
- }
219
- function buildBatchBody(chunk) {
220
- return JSON.stringify({
221
- events: chunk.map((item) => ({
222
- ...item,
223
- sdk: { name: SDK_NAME, version: SDK_VERSION }
224
- }))
225
- });
226
- }
227
- function splitBatch(batch) {
228
- if (batch.length <= 1) return [batch];
229
- const body = buildBatchBody(batch);
230
- if (byteLength(body) <= MAX_BATCH_BYTES) return [batch];
231
- const mid = Math.floor(batch.length / 2);
232
- return [...splitBatch(batch.slice(0, mid)), ...splitBatch(batch.slice(mid))];
233
- }
234
-
235
- // src/client-internals.ts
236
- function debugLog(message, opts, payload) {
237
- if (!opts.debug || typeof console.debug !== "function") return;
238
- const prefix = opts.label ? `[emit-vision:${opts.label}]` : "[emit-vision]";
239
- if (payload === void 0) {
240
- console.debug(prefix, message);
241
- return;
242
- }
243
- console.debug(prefix, message, payload);
244
- }
245
- function buildMetaEvent(originalName, originalBytes, trimmedFields, opts) {
246
- return {
247
- type: "event",
248
- name: "sdk.event_truncated",
249
- properties: {
250
- originalName,
251
- originalBytes,
252
- trimmedFields,
253
- sdkName: SDK_NAME
254
- },
255
- environment: opts.environment,
256
- release: opts.release
257
- };
258
- }
259
- function buildWithDefaults(clientUser, clientContext, clientDeployment, clientFeatureFlags, clientTags, sdkOpts, captureOpts) {
260
- const {
261
- context,
262
- deployment,
263
- featureFlags,
264
- tags,
265
- user,
266
- environment,
267
- release,
268
- sessionId,
269
- ...rest
270
- } = captureOpts;
271
- return {
272
- ...rest,
273
- environment: environment ?? sdkOpts.environment,
274
- release: release ?? sdkOpts.release,
275
- sessionId: sessionId ?? sdkOpts.sessionId,
276
- user: user ?? clientUser,
277
- context: Object.keys(clientContext).length ? { ...clientContext, ...context } : context,
278
- deployment: mergeDeploymentFn(clientDeployment, deployment),
279
- featureFlags: mergeFeatureFlagsFn(clientFeatureFlags, featureFlags),
280
- tags: Object.keys(clientTags).length ? { ...clientTags, ...tags } : tags
281
- };
282
- }
283
- function mergeDeploymentFn(clientDeployment, overrideDeployment) {
284
- const merged = { ...clientDeployment ?? {}, ...overrideDeployment ?? {} };
285
- return Object.keys(merged).length > 0 ? merged : void 0;
286
- }
287
- function mergeFeatureFlagsFn(clientFlags, overrideFlags) {
288
- const merged = { ...clientFlags, ...overrideFlags ?? {} };
289
- return Object.keys(merged).length > 0 ? merged : void 0;
290
- }
291
- function buildFlushDisabledMessage(label, consecutiveFailures, dropped) {
292
- return `[emit-vision${label ? `:${label}` : ""}] disabled after ${consecutiveFailures} consecutive flush failures; dropping ${dropped} queued events. Re-init the SDK to resume.`;
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
- };
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;
312
- const response = await fetchImpl(`${endpoint}/v1/batch`, {
313
- method: "POST",
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 }
325
- });
326
- if (!response.ok) {
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
- );
339
- }
340
- }
341
-
342
- // src/retry.ts
343
- var FlushRetryController = class {
344
- constructor(opts) {
345
- this.opts = opts;
346
- }
347
- opts;
348
- consecutiveFailures = 0;
349
- backoffUntil = 0;
350
- disabled = false;
351
- isDisabled() {
352
- return this.disabled;
353
- }
354
- isBackingOff(now = Date.now()) {
355
- return !this.disabled && now < this.backoffUntil;
356
- }
357
- msUntilReady(now = Date.now()) {
358
- if (this.disabled) return Infinity;
359
- return Math.max(0, this.backoffUntil - now);
360
- }
361
- forceDisable() {
362
- this.disabled = true;
363
- this.backoffUntil = 0;
364
- }
365
- reset() {
366
- this.consecutiveFailures = 0;
367
- this.backoffUntil = 0;
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
- }
375
- recordFailure(now = Date.now()) {
376
- this.consecutiveFailures += 1;
377
- if (this.opts.failureCap > 0 && this.consecutiveFailures >= this.opts.failureCap) {
378
- const wasDisabled = this.disabled;
379
- this.disabled = true;
380
- this.backoffUntil = 0;
381
- return {
382
- backoffMs: 0,
383
- consecutiveFailures: this.consecutiveFailures,
384
- justDisabled: !wasDisabled
385
- };
386
- }
387
- if (this.opts.initialMs <= 0) {
388
- this.backoffUntil = 0;
389
- return {
390
- backoffMs: 0,
391
- consecutiveFailures: this.consecutiveFailures,
392
- justDisabled: false
393
- };
394
- }
395
- const exp = Math.min(
396
- this.opts.initialMs * Math.pow(2, this.consecutiveFailures - 1),
397
- this.opts.maxMs
398
- );
399
- const jitter = exp * 0.2 * (Math.random() * 2 - 1);
400
- const backoffMs = Math.max(0, Math.floor(exp + jitter));
401
- this.backoffUntil = now + backoffMs;
402
- return {
403
- backoffMs,
404
- consecutiveFailures: this.consecutiveFailures,
405
- justDisabled: false
406
- };
407
- }
408
- };
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
-
474
- // src/client-flush.ts
475
- var FlushManager = class {
476
- constructor(options, fetchImpl) {
477
- this.options = options;
478
- this.fetchImpl = fetchImpl;
479
- this.consentGiven = !options.consentRequired || (options.consentGiven ?? false);
480
- this.storageKey = persistenceKey(options.apiKey, options.projectId);
481
- this.retry = new FlushRetryController({
482
- initialMs: options.retryBackoffInitialMs,
483
- maxMs: options.retryBackoffMaxMs,
484
- failureCap: options.maxConsecutiveFlushFailures
485
- });
486
- this.timer = setInterval(() => {
487
- if (this.queue.length === 0 || this.retry.isBackingOff()) return;
488
- this.flush().catch(
489
- (err) => debugLog("background flush error", this.options, { error: err })
490
- );
491
- }, options.flushIntervalMs);
492
- }
493
- options;
494
- fetchImpl;
495
- queue = [];
496
- timer;
497
- flushScheduled = false;
498
- retry;
499
- storageKey;
500
- consentGiven;
501
- authFailureCount = 0;
502
- enqueue(payload) {
503
- if (this.retry.isDisabled()) return;
504
- const {
505
- payload: trimmed,
506
- trimmedFields,
507
- originalBytes
508
- } = trimEventIfOversized(payload);
509
- this.queue.push(trimmed);
510
- if (trimmedFields !== null) {
511
- const meta = buildMetaEvent(
512
- payload.name,
513
- originalBytes,
514
- trimmedFields,
515
- this.options
516
- );
517
- this.queue.push(meta);
518
- }
519
- debugLog("queued payload", this.options, {
520
- type: payload.type,
521
- name: payload.name,
522
- queueSize: this.queue.length,
523
- flushOnCapture: this.options.flushOnCapture,
524
- batchSize: this.options.batchSize
525
- });
526
- if (this.options.flushOnCapture) {
527
- if (!this.retry.isBackingOff()) {
528
- this.flush().catch(
529
- (err) => debugLog("capture flush error", this.options, { error: err })
530
- );
531
- }
532
- return;
533
- }
534
- if (this.queue.length >= this.options.batchSize && !this.retry.isBackingOff()) {
535
- this.flush().catch(
536
- (err) => debugLog("batch flush error", this.options, { error: err })
537
- );
538
- }
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
- }
558
- async flush() {
559
- if (this.retry.isDisabled()) return;
560
- if (!this.consentGiven) return;
561
- const batch = this.queue.splice(0, this.options.batchSize);
562
- if (batch.length === 0) return;
563
- debugLog("flushing batch", this.options, { count: batch.length });
564
- const chunks = splitBatch(batch);
565
- for (let i = 0; i < chunks.length; i++) {
566
- const tailCount = chunks.slice(i + 1).reduce((s, c) => s + c.length, 0);
567
- try {
568
- await this.flushChunk(chunks[i], tailCount);
569
- } catch (err) {
570
- if (!this.retry.isDisabled()) {
571
- const tail = chunks.slice(i + 1).flat();
572
- if (tail.length > 0) {
573
- this.queue.splice(chunks[i].length, 0, ...tail);
574
- }
575
- }
576
- throw err;
577
- }
578
- }
579
- if (this.options.flushOnCapture && this.queue.length > 0) {
580
- this.scheduleFlush();
581
- }
582
- }
583
- close() {
584
- if (this.timer) {
585
- clearInterval(this.timer);
586
- this.timer = void 0;
587
- }
588
- }
589
- async flushChunk(chunk, tailCount) {
590
- try {
591
- await executeHttpSend(
592
- chunk,
593
- this.options.endpoint,
594
- this.options.apiKey,
595
- this.fetchImpl
596
- );
597
- } catch (err) {
598
- this.handleFlushFailure(
599
- chunk,
600
- tailCount,
601
- err instanceof FlushHttpError ? {
602
- reason: "status",
603
- status: err.status,
604
- retryAfterSeconds: err.retryAfterSeconds,
605
- serverMessage: err.serverMessage
606
- } : { reason: "network", error: err }
607
- );
608
- throw err;
609
- }
610
- this.retry.reset();
611
- this.authFailureCount = 0;
612
- clearPersistedQueue(this.storageKey);
613
- debugLog("flush complete", this.options, { count: chunk.length });
614
- dispatchDevToolsEvent("flush", { count: chunk.length });
615
- }
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
- }
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
- }
642
- if (result.justDisabled) {
643
- const dropped = this.queue.length + batch.length + tailCount;
644
- this.queue = [];
645
- this.close();
646
- const msg = buildFlushDisabledMessage(
647
- this.options.label,
648
- result.consecutiveFailures,
649
- dropped
650
- );
651
- if (typeof console.warn === "function") console.warn(msg);
652
- debugLog("disabled", this.options, {
653
- consecutiveFailures: result.consecutiveFailures,
654
- dropped
655
- });
656
- return;
657
- }
658
- this.queue.unshift(...batch);
659
- this.persistNow();
660
- const base = {
661
- restoredCount: batch.length,
662
- consecutiveFailures: result.consecutiveFailures,
663
- backoffMs: result.backoffMs
664
- };
665
- if (detail.reason === "network") {
666
- debugLog("flush network error", this.options, {
667
- error: detail.error,
668
- ...base
669
- });
670
- } else {
671
- debugLog("flush failed", this.options, {
672
- status: detail.status,
673
- serverMessage: detail.serverMessage,
674
- ...base
675
- });
676
- }
677
- }
678
- scheduleFlush() {
679
- if (this.flushScheduled) return;
680
- this.flushScheduled = true;
681
- queueMicrotask(() => {
682
- this.flushScheduled = false;
683
- if (this.queue.length === 0 || this.retry.isBackingOff()) return;
684
- this.flush().catch(
685
- (err) => debugLog("scheduled flush error", this.options, { error: err })
686
- );
687
- });
688
- }
689
- };
690
-
691
- // src/client-autocapture.ts
692
- var BrowserAutoCapture = class {
693
- constructor(onCapture, withDefaults, onPageView, config) {
694
- this.onCapture = onCapture;
695
- this.withDefaults = withDefaults;
696
- this.onPageView = onPageView;
697
- this.config = config;
698
- }
699
- onCapture;
700
- withDefaults;
701
- onPageView;
702
- config;
703
- attach() {
704
- if (typeof window === "undefined") return;
705
- if (this.config.errors) {
706
- window.addEventListener("error", this.handleWindowError);
707
- }
708
- if (this.config.unhandledRejections) {
709
- window.addEventListener(
710
- "unhandledrejection",
711
- this.handleUnhandledRejection
712
- );
713
- }
714
- if (this.config.pageViews) {
715
- this.onPageView();
716
- window.addEventListener("popstate", this.handlePopState);
717
- this.patchHistoryMethod("pushState");
718
- this.patchHistoryMethod("replaceState");
719
- }
720
- }
721
- detach() {
722
- if (typeof window === "undefined") return;
723
- if (this.config.errors) {
724
- window.removeEventListener("error", this.handleWindowError);
725
- }
726
- if (this.config.unhandledRejections) {
727
- window.removeEventListener(
728
- "unhandledrejection",
729
- this.handleUnhandledRejection
730
- );
731
- }
732
- if (this.config.pageViews) {
733
- window.removeEventListener("popstate", this.handlePopState);
734
- this.restoreHistoryMethod("pushState");
735
- this.restoreHistoryMethod("replaceState");
736
- }
737
- }
738
- handleWindowError = (event) => {
739
- const serialized = serializeError(event.error ?? event.message);
740
- this.onCapture({
741
- type: "error",
742
- name: "error",
743
- error: { ...serialized, handled: false },
744
- ...this.withDefaults({
745
- context: {
746
- source: "window.error",
747
- url: window.location.href,
748
- page: window.location.pathname
749
- }
750
- })
751
- });
752
- };
753
- handlePopState = () => {
754
- this.onPageView();
755
- };
756
- handleUnhandledRejection = (event) => {
757
- const serialized = serializeError(event.reason);
758
- this.onCapture({
759
- type: "error",
760
- name: "error",
761
- error: { ...serialized, handled: false },
762
- ...this.withDefaults({
763
- context: {
764
- source: "window.unhandledrejection",
765
- url: window.location.href,
766
- page: window.location.pathname
767
- }
768
- })
769
- });
770
- };
771
- patchHistoryMethod(method) {
772
- const original = history[method].bind(history);
773
- const historyWithCustom = history;
774
- historyWithCustom[`__ev_orig_${method}`] = original;
775
- historyWithCustom[method] = (...args) => {
776
- original(...args);
777
- this.onPageView();
778
- };
779
- }
780
- restoreHistoryMethod(method) {
781
- const historyWithCustom = history;
782
- const original = historyWithCustom[`__ev_orig_${method}`];
783
- if (original) {
784
- historyWithCustom[method] = original;
785
- }
786
- }
787
- };
788
-
789
- // src/client.ts
790
- var EmitVisionClient = class {
791
- constructor(options) {
792
- this.options = options;
793
- this.groupId = options.groupId;
794
- this.deployment = options.deployment ? { ...options.deployment } : void 0;
795
- this.featureFlags = options.featureFlags ? { ...options.featureFlags } : {};
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
- }
804
- this.flagEval = new FlagEvaluator(
805
- {
806
- endpoint: options.endpoint,
807
- apiKey: options.apiKey,
808
- ttlMs: options.flagEvalTtlMs,
809
- flagExposures: options.autoCapture.flagExposures,
810
- fetchImpl: options.fetchImpl
811
- },
812
- (flags) => {
813
- this.featureFlags = { ...this.featureFlags, ...flags };
814
- },
815
- (flagKey, variantKey, variantValue, reason, env) => this.captureExposure(flagKey, variantKey, variantValue, reason, env),
816
- (msg, data) => debugLog(msg, this.options, data)
817
- );
818
- this.autoCapture = new BrowserAutoCapture(
819
- (payload) => this.flusher.enqueue(payload),
820
- (opts) => this.withDefaults(opts),
821
- () => this.capturePageView(),
822
- options.autoCapture
823
- );
824
- this.autoCapture.attach();
825
- debugLog("initialized", this.options, {
826
- endpoint: options.endpoint,
827
- environment: options.environment,
828
- release: options.release,
829
- autoCapture: options.autoCapture,
830
- batchSize: options.batchSize,
831
- flushIntervalMs: options.flushIntervalMs,
832
- flushOnCapture: options.flushOnCapture
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
- });
844
- }
845
- options;
846
- user;
847
- groupId;
848
- context = {};
849
- deployment;
850
- featureFlags = {};
851
- tags = {};
852
- flagEval;
853
- autoCapture;
854
- flusher;
855
- persistOnHide = () => this.flusher.persistNow();
856
- persistOnHidden = () => {
857
- if (typeof document !== "undefined" && document.visibilityState === "hidden") {
858
- this.flusher.persistNow();
859
- }
860
- };
861
- captureEvent(name, properties, options = {}) {
862
- const eventId = options.eventId ?? (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : void 0);
863
- debugLog("queue event", this.options, { name, properties, options });
864
- this.flusher.enqueue({
865
- type: "event",
866
- name,
867
- properties,
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
877
- });
878
- }
879
- captureError(error, options = {}) {
880
- const serialized = serializeError(error);
881
- debugLog("queue error", this.options, {
882
- message: serialized.message,
883
- options
884
- });
885
- this.flusher.enqueue({
886
- type: "error",
887
- name: "error",
888
- error: serialized,
889
- ...this.withDefaults(options)
890
- });
891
- }
892
- identify(userIdOrUser, traits = {}) {
893
- this.user = typeof userIdOrUser === "string" ? { id: userIdOrUser, ...traits } : { ...userIdOrUser };
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 });
902
- }
903
- setContext(context) {
904
- this.context = { ...this.context, ...context };
905
- debugLog("updated context", this.options, this.context);
906
- }
907
- setDeploymentContext(context) {
908
- this.deployment = Object.keys(context).length > 0 ? { ...context } : void 0;
909
- debugLog("updated deployment context", this.options, this.deployment);
910
- }
911
- setFeatureFlags(featureFlags) {
912
- this.featureFlags = { ...featureFlags };
913
- debugLog("updated feature flags", this.options, this.featureFlags);
914
- }
915
- evaluateFlags(opts) {
916
- return this.flagEval.evaluateFlags({
917
- ...opts,
918
- environment: opts.environment ?? this.options.environment
919
- });
920
- }
921
- getFlag(flagKey, fallback, opts) {
922
- return this.flagEval.getFlag(flagKey, fallback, {
923
- ...opts,
924
- environment: opts.environment ?? this.options.environment
925
- });
926
- }
927
- refreshFlags(opts) {
928
- return this.flagEval.refreshFlags({
929
- ...opts,
930
- environment: opts.environment ?? this.options.environment
931
- });
932
- }
933
- capturePageView(path) {
934
- const page = path ?? (typeof window !== "undefined" ? window.location.pathname : void 0);
935
- if (!page) return;
936
- this.captureEvent("$page_view", { page });
937
- }
938
- captureFeedback(feedback, options = {}) {
939
- const url = feedback.url ?? (typeof window !== "undefined" ? window.location.href : void 0);
940
- this.captureEvent(
941
- "$feedback",
942
- { message: feedback.message, ...feedback.rating !== void 0 ? { rating: feedback.rating } : {}, ...url ? { url } : {} },
943
- options
944
- );
945
- }
946
- captureExposure(flagKey, variantKey, variantValue, reason, environment) {
947
- this.captureEvent("$flag_exposure", {
948
- flagKey,
949
- variantKey,
950
- ...variantValue !== void 0 ? { variantValue } : {},
951
- ...reason ? { reason } : {},
952
- environment: environment ?? this.options.environment
953
- });
954
- }
955
- setTags(tags) {
956
- this.tags = { ...this.tags, ...tags };
957
- debugLog("updated tags", this.options, this.tags);
958
- }
959
- consent() {
960
- return this.flusher.giveConsent();
961
- }
962
- flush() {
963
- return this.flusher.flush();
964
- }
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
- }
972
- this.flusher.close();
973
- this.autoCapture.detach();
974
- debugLog("closed client", this.options);
975
- }
976
- withDefaults(opts) {
977
- return buildWithDefaults(
978
- this.user,
979
- this.context,
980
- this.deployment,
981
- this.featureFlags,
982
- this.tags,
983
- this.options,
984
- { groupId: this.groupId, ...opts }
985
- );
986
- }
987
- };
988
-
989
- // src/index.ts
990
- var client;
991
- function init(options) {
992
- const transport = resolveTransport(options);
993
- const autoCapture = resolveAutoCapture(options);
994
- const resolved = {
995
- ...options,
996
- flushIntervalMs: options.flushIntervalMs ?? 5e3,
997
- batchSize: options.batchSize ?? 20,
998
- debug: options.debug ?? false,
999
- fetchImpl: options.fetchImpl ?? globalThis.fetch.bind(globalThis),
1000
- flushOnCapture: options.flushOnCapture ?? false,
1001
- flagEvalTtlMs: options.flagEvalTtlMs ?? 6e4,
1002
- retryBackoffInitialMs: options.retryBackoffInitialMs ?? 1e3,
1003
- retryBackoffMaxMs: options.retryBackoffMaxMs ?? 3e4,
1004
- maxConsecutiveFlushFailures: options.maxConsecutiveFlushFailures ?? 20,
1005
- apiKey: transport.apiKey,
1006
- endpoint: transport.endpoint ?? options.endpoint ?? "http://localhost:4301",
1007
- autoCapture,
1008
- sessionId: options.sessionId ?? getOrCreateAnonymousId()
1009
- };
1010
- client?.close();
1011
- client = new EmitVisionClient(resolved);
1012
- return client;
1013
- }
1014
- function captureEvent(name, properties, options) {
1015
- requireClient().captureEvent(name, properties, options);
1016
- }
1017
- function captureError(error, options) {
1018
- requireClient().captureError(error, options);
1019
- }
1020
- function capturePageView(path) {
1021
- requireClient().capturePageView(path);
1022
- }
1023
- function captureFeedback(feedback, options) {
1024
- requireClient().captureFeedback(feedback, options);
1025
- }
1026
- function identify(userIdOrUser, traits) {
1027
- requireClient().identify(userIdOrUser, traits);
1028
- }
1029
- function setContext(context) {
1030
- requireClient().setContext(context);
1031
- }
1032
- function setDeploymentContext(context) {
1033
- requireClient().setDeploymentContext(context);
1034
- }
1035
- function setFeatureFlags(featureFlags) {
1036
- requireClient().setFeatureFlags(featureFlags);
1037
- }
1038
- function setTags(tags) {
1039
- requireClient().setTags(tags);
1040
- }
1041
- async function flush() {
1042
- await requireClient().flush();
1043
- }
1044
- async function consent() {
1045
- await requireClient().consent();
1046
- }
1047
- function group(groupId, traits) {
1048
- requireClient().group(groupId, traits);
1049
- }
1050
- async function evaluateFlags(options) {
1051
- return requireClient().evaluateFlags(options);
1052
- }
1053
- async function getFlag(flagKey, fallback, options) {
1054
- return requireClient().getFlag(flagKey, fallback, options);
1055
- }
1056
- async function refreshFlags(options) {
1057
- return requireClient().refreshFlags(options);
1058
- }
1059
- function captureExposure(flagKey, variantKey, variantValue, reason, environment) {
1060
- requireClient().captureExposure(
1061
- flagKey,
1062
- variantKey,
1063
- variantValue,
1064
- reason,
1065
- environment
1066
- );
1067
- }
1068
- function requireClient() {
1069
- if (!client) {
1070
- throw new Error("emit-vision SDK has not been initialized");
1071
- }
1072
- return client;
1073
- }
1074
- export {
1075
- captureError,
1076
- captureEvent,
1077
- captureExposure,
1078
- captureFeedback,
1079
- capturePageView,
1080
- consent,
1081
- evaluateFlags,
1082
- flush,
1083
- getFlag,
1084
- group,
1085
- identify,
1086
- init,
1087
- refreshFlags,
1088
- setContext,
1089
- setDeploymentContext,
1090
- setFeatureFlags,
1091
- setTags
1092
- };
1
+ var MAX_PERSISTED_EVENTS=200;var MAX_PERSISTED_AGE_MS=24*60*60*1e3;function persistenceKey(apiKey,projectId){return`emit-vision:queue:${projectId??"-"}:${apiKey.slice(0,16)}`}function getStorage(){try{if(typeof localStorage==="undefined")return null;return localStorage}catch{return null}}function persistQueue(key,queue){const storage=getStorage();if(!storage||queue.length===0)return;const record={savedAt:Date.now(),events:queue.slice(0,MAX_PERSISTED_EVENTS)};try{storage.setItem(key,JSON.stringify(record))}catch{}}function restoreQueue(key,now=Date.now()){const storage=getStorage();if(!storage)return[];try{const raw=storage.getItem(key);storage.removeItem(key);if(!raw)return[];const parsed=JSON.parse(raw);if(typeof parsed.savedAt!=="number"||!Array.isArray(parsed.events)){return[]}if(now-parsed.savedAt>MAX_PERSISTED_AGE_MS)return[];return parsed.events.slice(0,MAX_PERSISTED_EVENTS)}catch{return[]}}function clearPersistedQueue(key){const storage=getStorage();if(!storage)return;try{storage.removeItem(key)}catch{}}var SESSION_ATTRIBUTION_TTL_MS=30*60*1e3;function attributionPersistenceKey(apiKey,projectId){return`emit-vision:attribution:${projectId??"-"}:${apiKey.slice(0,16)}`}function getOrCreateSessionAttribution(key,captureFresh,now=Date.now()){const storage=getStorage();if(!storage)return captureFresh();const stored=readStoredAttribution(storage,key,now);const attribution=stored??captureFresh();writeStoredAttribution(storage,key,attribution,now);return attribution}function readStoredAttribution(storage,key,now){try{const raw=storage.getItem(key);if(!raw)return null;const parsed=JSON.parse(raw);if(typeof parsed.savedAt!=="number"||typeof parsed.referrer!=="string"||typeof parsed.utm!=="object"||parsed.utm===null){return null}if(now-parsed.savedAt>SESSION_ATTRIBUTION_TTL_MS)return null;return{referrer:parsed.referrer,utm:parsed.utm}}catch{return null}}function writeStoredAttribution(storage,key,attribution,now){try{storage.setItem(key,JSON.stringify({...attribution,savedAt:now}))}catch{}}var CONSENT_TTL_MS=365*24*60*60*1e3;function getStorage2(custom){if(custom)return custom;try{if(typeof localStorage==="undefined")return null;return localStorage}catch{return null}}function consentStorageKey(apiKey,projectId){return`emit-vision:consent:${persistenceKey(apiKey,projectId)}`}function loadStoredConsent(key,now=Date.now(),custom){const storage=getStorage2(custom);if(!storage)return null;try{const raw=storage.getItem(key);if(!raw)return null;const parsed=JSON.parse(raw);const valid=(parsed.state==="granted"||parsed.state==="declined")&&typeof parsed.decidedAt==="number";if(!valid||now-parsed.decidedAt>CONSENT_TTL_MS){void Promise.resolve(storage.removeItem(key)).catch(()=>void 0);return null}return parsed}catch{return null}}function saveStoredConsent(key,record,custom){try{const result=getStorage2(custom)?.setItem(key,JSON.stringify(record));void Promise.resolve(result).catch(()=>void 0)}catch{}}function browserSendsGpc(){try{return typeof navigator!=="undefined"&&navigator.globalPrivacyControl===true}catch{return false}}var ConsentController=class{constructor(config){this.config=config;this.key=consentStorageKey(config.apiKey,config.projectId);this.current=this.initialState()}config;current="unknown";key;listeners=new Set;get state(){return this.current}onChange(cb){this.listeners.add(cb);return()=>{this.listeners.delete(cb)}}set(choice,source="api"){if(this.current===choice)return false;this.current=choice;saveStoredConsent(this.key,{state:choice,decidedAt:Date.now()},this.config.consentStorage);this.report(choice,source);for(const cb of[...this.listeners]){try{cb(choice)}catch{}}return true}initialState(){const{consentRequired,consentGiven,honorGlobalPrivacyControl}=this.config;if(!consentRequired||consentGiven)return"granted";const stored=loadStoredConsent(this.key,Date.now(),this.config.consentStorage);if(stored)return stored.state;if(honorGlobalPrivacyControl!==false&&browserSendsGpc()){this.set("declined","gpc");return"declined"}return"unknown"}report(choice,source){const{endpoint,apiKey,fetchImpl}=this.config;const body=JSON.stringify({choice:choice==="granted"?"accepted":"declined",source});try{void Promise.resolve(fetchImpl(`${endpoint}/v1/consent`,{method:"POST",keepalive:true,credentials:"omit",headers:{"content-type":"application/json","x-emit-api-key":apiKey},body})).catch(()=>void 0)}catch{}}};function scrubUrl(raw,allowQueryKeys=[]){let parsed;try{parsed=new URL(raw)}catch{return""}if(parsed.origin==="null")return"";const kept=new URLSearchParams;parsed.searchParams.forEach((value,key)=>{if(allowQueryKeys.includes(key))kept.append(key,value)});const query=kept.toString();return`${parsed.origin}${parsed.pathname}${query?`?${query}`:""}`}function scrubReferrer(raw){return raw===""?"":scrubUrl(raw)}function readReferrer(){return typeof document!=="undefined"?scrubReferrer(document.referrer):""}function currentPageUrl(allowQueryKeys=[]){if(typeof window==="undefined")return void 0;return scrubUrl(window.location.href,allowQueryKeys)||void 0}function buildFeedbackProperties(feedback,urlQueryAllowlist){const url=feedback.url??currentPageUrl(urlQueryAllowlist);return{message:feedback.message,...feedback.rating!==void 0?{rating:feedback.rating}:{},...url?{url}:{}}}function buildExposureProperties(flagKey,variantKey,variantValue,reason,environment){return{flagKey,variantKey,...variantValue!==void 0?{variantValue}:{},...reason?{reason}:{},environment}}function newEventId(){return typeof crypto!=="undefined"&&typeof crypto.randomUUID==="function"?crypto.randomUUID():void 0}function cacheKeyFor(env,opts){const keys=opts.flagKeys?.length?[...opts.flagKeys].sort().join(","):"*";return`${env}:${opts.evaluationKey}:${keys}`}var FlagEvaluator=class{constructor(cfg,onFlagsLoaded,captureExposure2,debug){this.cfg=cfg;this.onFlagsLoaded=onFlagsLoaded;this.captureExposure=captureExposure2;this.debug=debug}cfg;onFlagsLoaded;captureExposure;debug;cache=new Map;async evaluateFlags(opts){const env=opts.environment??"";const ttl=opts.ttlMs??this.cfg.ttlMs;const key=cacheKeyFor(env,opts);const cached=this.cache.get(key);if(cached&&Date.now()<cached.expiresAt){this.debug("flag eval cache hit",{key});return cached.flags}return this.fetchAndCacheFlags(opts,env,ttl,key)}async getFlag(flagKey,fallback,opts){try{const flags=await this.evaluateFlags(opts.flagKeys?.length?opts:{...opts,flagKeys:[flagKey]});const value=flags[flagKey];return value!==void 0?value:fallback}catch{return fallback}}async refreshFlags(opts){const env=opts.environment??"";const ttl=opts.ttlMs??this.cfg.ttlMs;const key=cacheKeyFor(env,opts);this.cache.delete(key);return this.fetchAndCacheFlags(opts,env,ttl,key)}async fetchAndCacheFlags(opts,env,ttl,cacheKey){let flags={};let evaluations={};try{const body={environment:env,userKey:opts.evaluationKey};if(opts.flagKeys&&opts.flagKeys.length>0){body.flagKeys=opts.flagKeys}const response=await this.cfg.fetchImpl(`${this.cfg.endpoint}/v1/flags/evaluate`,{method:"POST",headers:{"content-type":"application/json","x-emit-api-key":this.cfg.apiKey},body:JSON.stringify(body)});if(response.ok){const data=await response.json();evaluations=data.evaluations;flags=Object.fromEntries(Object.entries(evaluations).map(([k,v])=>[k,v.variantValue]));this.debug("flag eval complete",{count:Object.keys(flags).length})}else{this.debug("flag eval response error",{status:response.status})}}catch(err){this.debug("flag eval network error",{error:err})}this.cache.set(cacheKey,{flags,expiresAt:Date.now()+ttl});this.onFlagsLoaded(flags);if(this.cfg.flagExposures){for(const[flagKey,entry]of Object.entries(evaluations)){this.captureExposure(flagKey,entry.variantKey,entry.variantValue,entry.reason,env)}}return flags}};var COMPRESS_THRESHOLD_BYTES=8*1024;async function gzipBatchBody(body){if(typeof CompressionStream!=="function")return null;const stream=new Blob([body]).stream().pipeThrough(new CompressionStream("gzip"));const buffer=await new Response(stream).arrayBuffer();return new Uint8Array(buffer)}var INGEST_LIMITS={name:200,environment:80,release:160,sessionId:160,groupId:200};var TRUNCATABLE_FIELDS=["environment","release","sessionId","groupId"];function clipCodePoints(value,max){if(value.length<=max)return value;let out="";for(const char of value){if(out.length+char.length>max)break;out+=char}return out}function sanitizeForIngest(item){if(item.name.length===0||item.name.length>INGEST_LIMITS.name){return{item:null,trimmedFields:["name"]}}const trimmedFields=[];const patch={};for(const field of TRUNCATABLE_FIELDS){const value=item[field];if(typeof value!=="string")continue;if(value.length>INGEST_LIMITS[field]){patch[field]=clipCodePoints(value,INGEST_LIMITS[field]);trimmedFields.push(field)}}return{item:trimmedFields.length?{...item,...patch}:item,trimmedFields}}var SDK_NAME="emit-vision-js";var SDK_VERSION="0.5.0";var MAX_EVENT_BYTES=64*1024;var MAX_BATCH_BYTES=1*1024*1024;function byteLength(s){return new TextEncoder().encode(s).length}var TRUNCATION_SUFFIX="\u2026[truncated]";function isEmptyObject(value){return!value||Object.keys(value).length===0}function truncateUtf8(value,maxBytes){let bytes=0;let out="";for(const char of value){bytes+=byteLength(char);if(bytes>maxBytes)break;out+=char}return out}function markNonEmpty(value){if(isEmptyObject(value))return void 0;return{_truncated:true,_originalBytes:byteLength(JSON.stringify(value))}}function sizeOf(value){return byteLength(JSON.stringify(value))}function truncateErrorMessage(payload){if(payload.type!=="error")return null;const withMessage=message=>({...payload,error:{...payload.error,message}});let budget=MAX_EVENT_BYTES-sizeOf(withMessage(TRUNCATION_SUFFIX))+byteLength(TRUNCATION_SUFFIX);for(let attempt=0;attempt<5&&budget>0;attempt++){const kept=truncateUtf8(payload.error.message,budget);if(kept.length===0)return null;const candidate=withMessage(kept+TRUNCATION_SUFFIX);const overshoot=sizeOf(candidate)-MAX_EVENT_BYTES;if(overshoot<=0)return candidate;budget-=overshoot}return null}function trimEventIfOversized(payload){const originalBytes=sizeOf(payload);if(originalBytes<=MAX_EVENT_BYTES){return{payload,trimmedFields:null,originalBytes}}const trimmedFields=[];const properties=markNonEmpty(payload.properties);const context=markNonEmpty(payload.context);if(properties)trimmedFields.push("properties");if(context)trimmedFields.push("context");let current={...payload,...properties?{properties}:{},...context?{context}:{}};if(current.type==="error"&&current.error.stack){trimmedFields.push("error.stack");current={...current,error:{...current.error,stack:void 0}}}if(sizeOf(current)>MAX_EVENT_BYTES){const shortened=truncateErrorMessage(current);if(shortened){trimmedFields.push("error.message");current=shortened}}const dropped=sizeOf(current)>MAX_EVENT_BYTES;if(typeof console.warn==="function"){console.warn(`[emit-vision] event ${dropped?"dropped":"truncated"} \u2014 "${payload.name.slice(0,200)}" was ${originalBytes} bytes (max ${MAX_EVENT_BYTES}); ${dropped?"still oversized after trimming":"oversized fields trimmed"}.`)}return{payload:dropped?null:current,trimmedFields,originalBytes}}function buildBatchBody(chunk){return JSON.stringify({events:chunk.map(item=>({...item,sdk:{name:SDK_NAME,version:SDK_VERSION}}))})}function splitBatch(batch){if(batch.length<=1)return[batch];const body=buildBatchBody(batch);if(byteLength(body)<=MAX_BATCH_BYTES)return[batch];const mid=Math.floor(batch.length/2);return[...splitBatch(batch.slice(0,mid)),...splitBatch(batch.slice(mid))]}function debugLog(message,opts,payload){if(!opts.debug||typeof console.debug!=="function")return;const prefix=opts.label?`[emit-vision:${opts.label}]`:"[emit-vision]";if(payload===void 0){console.debug(prefix,message);return}console.debug(prefix,message,payload)}var MAX_ORIGINAL_NAME_CHARS=200;function buildMetaEvent(originalName,originalBytes,trimmedFields,opts,dropped=false){const meta={type:"event",name:"sdk.event_truncated",properties:{originalName:originalName.slice(0,MAX_ORIGINAL_NAME_CHARS),originalBytes,trimmedFields,sdkName:SDK_NAME,...dropped?{dropped:true}:{}},environment:opts.environment,release:opts.release};return sanitizeForIngest(meta).item??meta}function buildWithDefaults(clientUser,clientContext,clientDeployment,clientFeatureFlags,clientTags,sdkOpts,captureOpts){const{context,deployment,featureFlags,tags,user,environment,release,sessionId,...rest}=captureOpts;return{...rest,environment:environment??sdkOpts.environment,release:release??sdkOpts.release,sessionId:sessionId??sdkOpts.sessionId,user:user??clientUser,context:Object.keys(clientContext).length?{...clientContext,...context}:context,deployment:mergeDeploymentFn(clientDeployment,deployment),featureFlags:mergeFeatureFlagsFn(clientFeatureFlags,featureFlags),tags:Object.keys(clientTags).length?{...clientTags,...tags}:tags}}function mergeDeploymentFn(clientDeployment,overrideDeployment){const merged={...clientDeployment??{},...overrideDeployment??{}};return Object.keys(merged).length>0?merged:void 0}function mergeFeatureFlagsFn(clientFlags,overrideFlags){const merged={...clientFlags,...overrideFlags??{}};return Object.keys(merged).length>0?merged:void 0}function buildFlushDisabledMessage(label,consecutiveFailures,dropped){return`[emit-vision${label?`:${label}`:""}] disabled after ${consecutiveFailures} consecutive flush failures; dropping ${dropped} queued events. Re-init the SDK to resume.`}function buildAuthDisabledMessage(label,status,dropped){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.`}var AUTH_FAILURE_CAP=3;var FlushHttpError=class extends Error{constructor(status,retryAfterSeconds,serverMessage){super(`emit-vision flush failed with ${status}`);this.status=status;this.retryAfterSeconds=retryAfterSeconds;this.serverMessage=serverMessage}status;retryAfterSeconds;serverMessage};async function executeHttpSend(chunk,endpoint,apiKey,fetchImpl){const body=buildBatchBody(chunk);const compressed=byteLength(body)>=COMPRESS_THRESHOLD_BYTES?await gzipBatchBody(body):null;const response=await fetchImpl(`${endpoint}/v1/batch`,{method:"POST",headers:{"content-type":"application/json","x-emit-api-key":apiKey,...compressed?{"content-encoding":"gzip"}:{}},body:compressed??body,...compressed?{}:{keepalive:true}});if(!response.ok){const retryAfter=Number(response.headers?.get?.("retry-after"));let serverMessage;try{const body2=await response.json();if(body2&&typeof body2.message==="string")serverMessage=body2.message}catch{}throw new FlushHttpError(response.status,Number.isFinite(retryAfter)&&retryAfter>0?retryAfter:void 0,serverMessage)}}function prepareQueuedPayloads(rawPayload,opts){const{item:sanitized,trimmedFields:limitFields}=sanitizeForIngest(rawPayload);if(sanitized===null){if(typeof console.warn==="function"){console.warn(`[emit-vision] event dropped \u2014 name is empty or longer than the ingest limit: "${rawPayload.name.slice(0,200)}".`)}const originalBytes2=byteLength(JSON.stringify(rawPayload));return[buildMetaEvent(rawPayload.name,originalBytes2,limitFields,opts,true)]}const{payload:trimmed,trimmedFields:sizeFields,originalBytes}=trimEventIfOversized(sanitized);const trimmedFields=[...limitFields,...sizeFields??[]];if(trimmedFields.length===0)return[sanitized];const dropped=trimmed===null;const meta=buildMetaEvent(rawPayload.name,originalBytes,trimmedFields,opts,dropped);return dropped?[meta]:[trimmed,meta]}var ClientFlags=class{constructor(options,onFlags,onExposure){this.options=options;this.evaluator=new FlagEvaluator({endpoint:options.endpoint,apiKey:options.apiKey,ttlMs:options.flagEvalTtlMs,flagExposures:options.autoCapture.flagExposures,fetchImpl:options.fetchImpl},onFlags,onExposure,(msg,data)=>debugLog(msg,options,data))}options;evaluator;evaluate(opts){return this.evaluator.evaluateFlags(this.withEnv(opts))}get(flagKey,fallback,opts){return this.evaluator.getFlag(flagKey,fallback,this.withEnv(opts))}refresh(opts){return this.evaluator.refreshFlags(this.withEnv(opts))}withEnv(opts){return{...opts,environment:opts.environment??this.options.environment}}};function dispatchDevToolsEvent(type,payload){if(typeof window==="undefined"||typeof window.dispatchEvent!=="function"){return}window.dispatchEvent(new CustomEvent("__emitVisionDevTools",{detail:{type,payload,timestamp:Date.now()}}))}function announceInit(options){debugLog("initialized",options,{endpoint:options.endpoint,environment:options.environment,release:options.release,autoCapture:options.autoCapture,batchSize:options.batchSize,flushIntervalMs:options.flushIntervalMs,flushOnCapture:options.flushOnCapture});dispatchDevToolsEvent("init",{sdkVersion:SDK_VERSION,endpoint:options.endpoint,environment:options.environment,release:options.release,sessionId:options.sessionId??"",autoCapture:options.autoCapture,projectId:options.projectId,consentRequired:options.consentRequired})}function announceEvent(options,name,properties,eventId,captureOptions){debugLog("queue event",options,{name,properties,options:captureOptions});dispatchDevToolsEvent("event",{name,properties,eventId,timestamp:captureOptions.timestamp,environment:captureOptions.environment,sessionId:captureOptions.sessionId})}function announceIdentify(options,user){debugLog("identified user",options,user);const{id,...traits}=user;dispatchDevToolsEvent("identify",{userId:id,traits})}function announceGroup(options,groupId,traits){debugLog("set group",options,{groupId,traits});dispatchDevToolsEvent("group",{groupId,traits})}function announceError(options,message,captureOptions){debugLog("queue error",options,{message,options:captureOptions})}function parseDsn(dsn){const parsed=new URL(dsn);const apiKey=decodeURIComponent(parsed.username);if(!apiKey){throw new Error("emit-vision dsn must include the API key before the @")}const pathname=stripTrailingSlash(parsed.pathname)??"";const endpointPath=pathname.endsWith("/v1")?pathname.slice(0,-"/v1".length):pathname;return{apiKey,endpoint:stripTrailingSlash(`${parsed.origin}${endpointPath}`)}}function stripTrailingSlash(value){if(!value){return value}return value.endsWith("/")?value.slice(0,-1):value}function serializeError(error){if(error instanceof Error){return{message:error.message,name:error.name,stack:error.stack,handled:true}}return{message:typeof error==="string"?error:JSON.stringify(error),handled:true}}function resolveTransport(options){const dsn=options.dsn?parseDsn(options.dsn):null;const apiKey=options.apiKey??dsn?.apiKey;if(!apiKey){throw new Error("emit-vision init requires either apiKey or dsn")}return{apiKey,endpoint:stripTrailingSlash(options.endpoint??dsn?.endpoint)}}var ANON_ID_KEY="__ev_anon_id";function getOrCreateAnonymousId(){try{const stored=localStorage.getItem(ANON_ID_KEY);if(stored)return stored;const id=crypto.randomUUID();localStorage.setItem(ANON_ID_KEY,id);return id}catch{return void 0}}function resolveAutoCapture(options){const legacyDefault=options.autoCaptureErrors===void 0?true:options.autoCaptureErrors;return{errors:options.autoCapture?.errors??legacyDefault,unhandledRejections:options.autoCapture?.unhandledRejections??legacyDefault,flagExposures:options.autoCapture?.flagExposures??true,pageViews:options.autoCapture?.pageViews??false}}var FlushRetryController=class{constructor(opts){this.opts=opts}opts;consecutiveFailures=0;backoffUntil=0;disabled=false;isDisabled(){return this.disabled}isBackingOff(now=Date.now()){return!this.disabled&&now<this.backoffUntil}msUntilReady(now=Date.now()){if(this.disabled)return Infinity;return Math.max(0,this.backoffUntil-now)}forceDisable(){this.disabled=true;this.backoffUntil=0}reset(){this.consecutiveFailures=0;this.backoffUntil=0}applyRetryAfter(seconds,now=Date.now()){if(this.disabled||!Number.isFinite(seconds)||seconds<=0)return;const cappedMs=Math.min(seconds,300)*1e3;this.backoffUntil=Math.max(this.backoffUntil,now+cappedMs)}recordFailure(now=Date.now()){this.consecutiveFailures+=1;if(this.opts.failureCap>0&&this.consecutiveFailures>=this.opts.failureCap){const wasDisabled=this.disabled;this.disabled=true;this.backoffUntil=0;return{backoffMs:0,consecutiveFailures:this.consecutiveFailures,justDisabled:!wasDisabled}}if(this.opts.initialMs<=0){this.backoffUntil=0;return{backoffMs:0,consecutiveFailures:this.consecutiveFailures,justDisabled:false}}const exp=Math.min(this.opts.initialMs*Math.pow(2,this.consecutiveFailures-1),this.opts.maxMs);const jitter=exp*.2*(Math.random()*2-1);const backoffMs=Math.max(0,Math.floor(exp+jitter));this.backoffUntil=now+backoffMs;return{backoffMs,consecutiveFailures:this.consecutiveFailures,justDisabled:false}}};var FlushManager=class{constructor(options,fetchImpl,consent2=new ConsentController({...options,fetchImpl})){this.options=options;this.fetchImpl=fetchImpl;this.consent=consent2;this.storageKey=persistenceKey(options.apiKey,options.projectId);this.retry=new FlushRetryController({initialMs:options.retryBackoffInitialMs,maxMs:options.retryBackoffMaxMs,failureCap:options.maxConsecutiveFlushFailures});this.timer=setInterval(()=>{if(this.queue.length===0||this.retry.isBackingOff())return;this.flush().catch(err=>debugLog("background flush error",this.options,{error:err}))},options.flushIntervalMs)}options;fetchImpl;consent;queue=[];timer;flushScheduled=false;retry;storageKey;authFailureCount=0;enqueue(payload){if(this.retry.isDisabled()||this.consent.state==="declined")return;this.queue.push(...prepareQueuedPayloads(payload,this.options));debugLog("queued payload",this.options,{type:payload.type,name:payload.name,queueSize:this.queue.length,flushOnCapture:this.options.flushOnCapture,batchSize:this.options.batchSize});if(this.options.flushOnCapture){if(!this.retry.isBackingOff()){this.flush().catch(err=>debugLog("capture flush error",this.options,{error:err}))}return}if(this.queue.length>=this.options.batchSize&&!this.retry.isBackingOff()){this.flush().catch(err=>debugLog("batch flush error",this.options,{error:err}))}}discardQueue(){this.queue=[];clearPersistedQueue(this.storageKey)}restorePersisted(){if(this.consent.state==="declined"){clearPersistedQueue(this.storageKey);return}const restored=restoreQueue(this.storageKey);if(restored.length===0)return;this.queue.unshift(...restored);debugLog("restored persisted queue",this.options,{count:restored.length})}persistNow(){if(this.consent.state!=="granted"||this.retry.isDisabled())return;persistQueue(this.storageKey,this.queue)}async flush(){if(this.retry.isDisabled())return;if(this.consent.state!=="granted")return;const batch=this.queue.splice(0,this.options.batchSize);if(batch.length===0)return;debugLog("flushing batch",this.options,{count:batch.length});const chunks=splitBatch(batch);for(let i=0;i<chunks.length;i++){const tailCount=chunks.slice(i+1).reduce((s,c)=>s+c.length,0);try{await this.flushChunk(chunks[i],tailCount)}catch(err){if(!this.retry.isDisabled()){const tail=chunks.slice(i+1).flat();if(tail.length>0){this.queue.splice(chunks[i].length,0,...tail)}}throw err}}if(this.options.flushOnCapture&&this.queue.length>0){this.scheduleFlush()}}close(){if(this.timer){clearInterval(this.timer);this.timer=void 0}}async flushChunk(chunk,tailCount){try{await executeHttpSend(chunk,this.options.endpoint,this.options.apiKey,this.fetchImpl)}catch(err){this.handleFlushFailure(chunk,tailCount,err instanceof FlushHttpError?{reason:"status",status:err.status,retryAfterSeconds:err.retryAfterSeconds,serverMessage:err.serverMessage}:{reason:"network",error:err});throw err}this.retry.reset();this.authFailureCount=0;clearPersistedQueue(this.storageKey);debugLog("flush complete",this.options,{count:chunk.length});dispatchDevToolsEvent("flush",{count:chunk.length})}handleFlushFailure(batch,tailCount,detail){if(detail.reason==="status"&&(detail.status===401||detail.status===403)){this.authFailureCount+=1;if(this.authFailureCount>=AUTH_FAILURE_CAP){const dropped=this.queue.length+batch.length+tailCount;this.queue=[];this.close();this.retry.forceDisable();const msg=buildAuthDisabledMessage(this.options.label,detail.status,dropped);if(typeof console.warn==="function")console.warn(msg);debugLog("disabled (auth)",this.options,{status:detail.status,authFailures:this.authFailureCount,dropped});return}}const result=this.retry.recordFailure();if(detail.reason==="status"&&(detail.status===429||detail.status===503)&&detail.retryAfterSeconds!==void 0){this.retry.applyRetryAfter(detail.retryAfterSeconds)}if(result.justDisabled){const dropped=this.queue.length+batch.length+tailCount;this.queue=[];this.close();const msg=buildFlushDisabledMessage(this.options.label,result.consecutiveFailures,dropped);if(typeof console.warn==="function")console.warn(msg);debugLog("disabled",this.options,{consecutiveFailures:result.consecutiveFailures,dropped});return}this.queue.unshift(...batch);this.persistNow();const base={restoredCount:batch.length,consecutiveFailures:result.consecutiveFailures,backoffMs:result.backoffMs};if(detail.reason==="network"){debugLog("flush network error",this.options,{error:detail.error,...base})}else{debugLog("flush failed",this.options,{status:detail.status,serverMessage:detail.serverMessage,...base})}}scheduleFlush(){if(this.flushScheduled)return;this.flushScheduled=true;queueMicrotask(()=>{this.flushScheduled=false;if(this.queue.length===0||this.retry.isBackingOff())return;this.flush().catch(err=>debugLog("scheduled flush error",this.options,{error:err}))})}};var ConsentApi=class{constructor(controller,flusher){this.controller=controller;this.flusher=flusher}controller;flusher;grant(opts={}){this.controller.set("granted",opts.source);return this.flusher.flush()}decline(opts={}){this.controller.set("declined",opts.source);this.flusher.discardQueue()}get state(){return this.controller.state}onChange(cb){return this.controller.onChange(cb)}};function persistOnHide(flusher){const onPageHide=()=>flusher.persistNow();const onVisibility=()=>{if(document.visibilityState==="hidden")flusher.persistNow()};if(typeof window!=="undefined"){window.addEventListener("pagehide",onPageHide)}if(typeof document!=="undefined"){document.addEventListener("visibilitychange",onVisibility)}return()=>{if(typeof window!=="undefined"){window.removeEventListener("pagehide",onPageHide)}if(typeof document!=="undefined"){document.removeEventListener("visibilitychange",onVisibility)}}}var BrowserAutoCapture=class{constructor(onCapture,withDefaults,onPageView,config,urlQueryAllowlist=[]){this.onCapture=onCapture;this.withDefaults=withDefaults;this.onPageView=onPageView;this.config=config;this.urlQueryAllowlist=urlQueryAllowlist}onCapture;withDefaults;onPageView;config;urlQueryAllowlist;attach(){if(typeof window==="undefined")return;if(this.config.errors){window.addEventListener("error",this.handleWindowError)}if(this.config.unhandledRejections){window.addEventListener("unhandledrejection",this.handleUnhandledRejection)}if(this.config.pageViews){this.onPageView();window.addEventListener("popstate",this.handlePopState);this.patchHistoryMethod("pushState");this.patchHistoryMethod("replaceState")}}detach(){if(typeof window==="undefined")return;if(this.config.errors){window.removeEventListener("error",this.handleWindowError)}if(this.config.unhandledRejections){window.removeEventListener("unhandledrejection",this.handleUnhandledRejection)}if(this.config.pageViews){window.removeEventListener("popstate",this.handlePopState);this.restoreHistoryMethod("pushState");this.restoreHistoryMethod("replaceState")}}handleWindowError=event=>{const serialized=serializeError(event.error??event.message);this.onCapture({type:"error",name:"error",error:{...serialized,handled:false},...this.withDefaults({context:{source:"window.error",url:scrubUrl(window.location.href,this.urlQueryAllowlist),page:window.location.pathname}})})};handlePopState=()=>{this.onPageView()};handleUnhandledRejection=event=>{const serialized=serializeError(event.reason);this.onCapture({type:"error",name:"error",error:{...serialized,handled:false},...this.withDefaults({context:{source:"window.unhandledrejection",url:scrubUrl(window.location.href,this.urlQueryAllowlist),page:window.location.pathname}})})};patchHistoryMethod(method){const original=history[method].bind(history);const historyWithCustom=history;historyWithCustom[`__ev_orig_${method}`]=original;historyWithCustom[method]=(...args)=>{original(...args);this.onPageView()}}restoreHistoryMethod(method){const historyWithCustom=history;const original=historyWithCustom[`__ev_orig_${method}`];if(original){historyWithCustom[method]=original}}};var UTM_PARAM_KEYS=["source","medium","campaign","content"];function parseUtmParams(search){if(!search)return{};const params=new URLSearchParams(search);const result={};for(const key of UTM_PARAM_KEYS){const value=params.get(`utm_${key}`);if(value!==null)result[`utm_${key}`]=value}return result}function buildPageViewProperties(path){const page=path??(typeof window!=="undefined"?window.location.pathname:void 0);if(!page)return void 0;const referrer=readReferrer();const utm=typeof window!=="undefined"?parseUtmParams(window.location.search):{};return{page,referrer,...utm}}var AttributionTracker=class{constructor(options){this.options=options}options;sessionAttribution;properties(){if(!this.sessionAttribution){this.sessionAttribution=getOrCreateSessionAttribution(attributionPersistenceKey(this.options.apiKey,this.options.projectId),()=>({referrer:readReferrer(),utm:typeof window!=="undefined"?parseUtmParams(window.location.search):{}}))}const{referrer,utm}=this.sessionAttribution;return{referrer,...utm}}};var EmitVisionClient=class{constructor(options){this.options=options;this.groupId=options.groupId;this.deployment=options.deployment&&{...options.deployment};this.featureFlags=options.featureFlags?{...options.featureFlags}:{};const consentController=new ConsentController(options);this.flusher=new FlushManager(options,options.fetchImpl,consentController);this.consentApi=new ConsentApi(consentController,this.flusher);this.attribution=new AttributionTracker({apiKey:options.apiKey,projectId:options.projectId});this.flusher.restorePersisted();this.stopPersistOnHide=persistOnHide(this.flusher);this.flags=new ClientFlags(options,flags=>{this.featureFlags={...this.featureFlags,...flags}},(flagKey,variantKey,variantValue,reason,env)=>this.captureExposure(flagKey,variantKey,variantValue,reason,env));this.autoCapture=new BrowserAutoCapture(payload=>this.flusher.enqueue(payload),opts=>this.withDefaults(opts),()=>this.capturePageView(),options.autoCapture,options.urlQueryAllowlist);this.autoCapture.attach();announceInit(options)}options;user;groupId;context={};deployment;featureFlags={};tags={};flags;autoCapture;flusher;consentApi;stopPersistOnHide;attribution;captureEvent(name,properties,options={}){const eventId=options.eventId??newEventId();const attributedProperties={...this.attribution.properties(),...properties};this.flusher.enqueue({type:"event",name,properties:attributedProperties,...this.withDefaults({...options,eventId})});announceEvent(this.options,name,attributedProperties,eventId,options)}captureError(error,options={}){const serialized=serializeError(error);announceError(this.options,serialized.message,options);this.flusher.enqueue({type:"error",name:"error",error:serialized,properties:this.attribution.properties(),...this.withDefaults(options)})}identify(userIdOrUser,traits={}){this.user=typeof userIdOrUser==="string"?{id:userIdOrUser,...traits}:{...userIdOrUser};announceIdentify(this.options,this.user)}group(groupId,traits){this.groupId=groupId;announceGroup(this.options,groupId,traits)}setContext(context){this.context={...this.context,...context};debugLog("updated context",this.options,this.context)}setDeploymentContext(context){this.deployment=Object.keys(context).length>0?{...context}:void 0;debugLog("updated deployment context",this.options,this.deployment)}setFeatureFlags(featureFlags){this.featureFlags={...featureFlags};debugLog("updated feature flags",this.options,this.featureFlags)}evaluateFlags(opts){return this.flags.evaluate(opts)}getFlag(flagKey,fallback,opts){return this.flags.get(flagKey,fallback,opts)}refreshFlags(opts){return this.flags.refresh(opts)}capturePageView(path){const props=buildPageViewProperties(path);if(props)this.captureEvent("$page_view",props)}captureFeedback(feedback,options={}){this.captureEvent("$feedback",buildFeedbackProperties(feedback,this.options.urlQueryAllowlist),options)}captureExposure(flagKey,variantKey,variantValue,reason,environment=this.options.environment){const props=buildExposureProperties(flagKey,variantKey,variantValue,reason,environment);this.captureEvent("$flag_exposure",props)}setTags(tags){this.tags={...this.tags,...tags};debugLog("updated tags",this.options,this.tags)}grantConsent(opts={}){return this.consentApi.grant(opts)}declineConsent(opts={}){this.consentApi.decline(opts)}getConsentState(){return this.consentApi.state}onConsentChange(cb){return this.consentApi.onChange(cb)}consent(){return this.grantConsent()}flush(){return this.flusher.flush()}close(){this.stopPersistOnHide();this.flusher.close();this.autoCapture.detach();debugLog("closed client",this.options)}withDefaults(opts){return buildWithDefaults(this.user,this.context,this.deployment,this.featureFlags,this.tags,this.options,{groupId:this.groupId,...opts})}};var client;function init(options){const transport=resolveTransport(options);const autoCapture=resolveAutoCapture(options);warnIfLocalhostFallback(transport.endpoint);const resolved={...options,flushIntervalMs:options.flushIntervalMs??5e3,batchSize:options.batchSize??20,debug:options.debug??false,fetchImpl:options.fetchImpl??globalThis.fetch.bind(globalThis),flushOnCapture:options.flushOnCapture??false,flagEvalTtlMs:options.flagEvalTtlMs??6e4,retryBackoffInitialMs:options.retryBackoffInitialMs??1e3,retryBackoffMaxMs:options.retryBackoffMaxMs??3e4,maxConsecutiveFlushFailures:options.maxConsecutiveFlushFailures??20,urlQueryAllowlist:options.urlQueryAllowlist??[],apiKey:transport.apiKey,endpoint:transport.endpoint??options.endpoint??"http://localhost:4301",autoCapture,sessionId:options.sessionId??getOrCreateAnonymousId()};client?.close();client=new EmitVisionClient(resolved);return client}function captureEvent(name,properties,options){requireClient().captureEvent(name,properties,options)}function captureError(error,options){requireClient().captureError(error,options)}function capturePageView(path){requireClient().capturePageView(path)}function captureFeedback(feedback,options){requireClient().captureFeedback(feedback,options)}function identify(userIdOrUser,traits){requireClient().identify(userIdOrUser,traits)}function setContext(context){requireClient().setContext(context)}function setDeploymentContext(context){requireClient().setDeploymentContext(context)}function setFeatureFlags(featureFlags){requireClient().setFeatureFlags(featureFlags)}function setTags(tags){requireClient().setTags(tags)}async function flush(){await requireClient().flush()}async function grantConsent(options){await requireClient().grantConsent(options)}function declineConsent(options){requireClient().declineConsent(options)}function getConsentState(){return requireClient().getConsentState()}function onConsentChange(cb){return requireClient().onConsentChange(cb)}async function consent(){await requireClient().grantConsent()}function group(groupId,traits){requireClient().group(groupId,traits)}async function evaluateFlags(options){return requireClient().evaluateFlags(options)}async function getFlag(flagKey,fallback,options){return requireClient().getFlag(flagKey,fallback,options)}async function refreshFlags(options){return requireClient().refreshFlags(options)}function captureExposure(flagKey,variantKey,variantValue,reason,environment){requireClient().captureExposure(flagKey,variantKey,variantValue,reason,environment)}function warnIfLocalhostFallback(endpoint){if(endpoint)return;const isProduction=typeof process!=="undefined"&&false;if(!isProduction||typeof console.warn!=="function")return;console.warn("[emit-vision] no dsn/endpoint configured \u2014 falling back to http://localhost:4301. Events will not reach your instance. Pass `dsn` (copy it from your emit-vision setup page).")}function requireClient(){if(!client){throw new Error("emit-vision SDK has not been initialized")}return client}export{captureError,captureEvent,captureExposure,captureFeedback,capturePageView,consent,consentStorageKey,declineConsent,evaluateFlags,flush,getConsentState,getFlag,grantConsent,group,identify,init,onConsentChange,refreshFlags,setContext,setDeploymentContext,setFeatureFlags,setTags};