@emit-vision/sdk-js 0.4.2 → 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,1067 +1 @@
1
- // src/flag-eval.ts
2
- var FlagEvaluator = class {
3
- constructor(cfg, onFlagsLoaded, captureExposure2, debug) {
4
- this.cfg = cfg;
5
- this.onFlagsLoaded = onFlagsLoaded;
6
- this.captureExposure = captureExposure2;
7
- this.debug = debug;
8
- }
9
- cfg;
10
- onFlagsLoaded;
11
- captureExposure;
12
- debug;
13
- cache = /* @__PURE__ */ new Map();
14
- async evaluateFlags(opts) {
15
- const env = opts.environment ?? "";
16
- const ttl = opts.ttlMs ?? this.cfg.ttlMs;
17
- const key = `${env}:${opts.evaluationKey}`;
18
- const cached = this.cache.get(key);
19
- if (cached && Date.now() < cached.expiresAt) {
20
- this.debug("flag eval cache hit", { key });
21
- return cached.flags;
22
- }
23
- return this.fetchAndCacheFlags(opts, env, ttl, key);
24
- }
25
- async getFlag(flagKey, fallback, opts) {
26
- try {
27
- const flags = await this.evaluateFlags(opts);
28
- const value = flags[flagKey];
29
- return value !== void 0 ? value : fallback;
30
- } catch {
31
- return fallback;
32
- }
33
- }
34
- async refreshFlags(opts) {
35
- const env = opts.environment ?? "";
36
- const ttl = opts.ttlMs ?? this.cfg.ttlMs;
37
- const key = `${env}:${opts.evaluationKey}`;
38
- this.cache.delete(key);
39
- return this.fetchAndCacheFlags(opts, env, ttl, key);
40
- }
41
- async fetchAndCacheFlags(opts, env, ttl, cacheKey) {
42
- let flags = {};
43
- let evaluations = {};
44
- try {
45
- const body = {
46
- environment: env,
47
- userKey: opts.evaluationKey
48
- };
49
- if (opts.flagKeys && opts.flagKeys.length > 0) {
50
- body.flagKeys = opts.flagKeys;
51
- }
52
- const response = await this.cfg.fetchImpl(
53
- `${this.cfg.endpoint}/v1/flags/evaluate`,
54
- {
55
- method: "POST",
56
- headers: {
57
- "content-type": "application/json",
58
- "x-emit-api-key": this.cfg.apiKey
59
- },
60
- body: JSON.stringify(body)
61
- }
62
- );
63
- if (response.ok) {
64
- const data = await response.json();
65
- evaluations = data.evaluations;
66
- flags = Object.fromEntries(
67
- Object.entries(evaluations).map(([k, v]) => [k, v.variantValue])
68
- );
69
- this.debug("flag eval complete", { count: Object.keys(flags).length });
70
- } else {
71
- this.debug("flag eval response error", { status: response.status });
72
- }
73
- } catch (err) {
74
- this.debug("flag eval network error", { error: err });
75
- }
76
- this.cache.set(cacheKey, { flags, expiresAt: Date.now() + ttl });
77
- this.onFlagsLoaded(flags);
78
- if (this.cfg.flagExposures) {
79
- for (const [flagKey, entry] of Object.entries(evaluations)) {
80
- this.captureExposure(
81
- flagKey,
82
- entry.variantKey,
83
- entry.variantValue,
84
- entry.reason,
85
- env
86
- );
87
- }
88
- }
89
- return flags;
90
- }
91
- };
92
-
93
- // src/util.ts
94
- function parseDsn(dsn) {
95
- const parsed = new URL(dsn);
96
- const apiKey = decodeURIComponent(parsed.username);
97
- if (!apiKey) {
98
- throw new Error("emit-vision dsn must include the API key before the @");
99
- }
100
- const pathname = stripTrailingSlash(parsed.pathname) ?? "";
101
- const endpointPath = pathname.endsWith("/v1") ? pathname.slice(0, -"/v1".length) : pathname;
102
- return {
103
- apiKey,
104
- endpoint: stripTrailingSlash(`${parsed.origin}${endpointPath}`)
105
- };
106
- }
107
- function stripTrailingSlash(value) {
108
- if (!value) {
109
- return value;
110
- }
111
- return value.endsWith("/") ? value.slice(0, -1) : value;
112
- }
113
- function serializeError(error) {
114
- if (error instanceof Error) {
115
- return {
116
- message: error.message,
117
- name: error.name,
118
- stack: error.stack,
119
- handled: true
120
- };
121
- }
122
- return {
123
- message: typeof error === "string" ? error : JSON.stringify(error),
124
- handled: true
125
- };
126
- }
127
- function resolveTransport(options) {
128
- const dsn = options.dsn ? parseDsn(options.dsn) : null;
129
- const apiKey = options.apiKey ?? dsn?.apiKey;
130
- if (!apiKey) {
131
- throw new Error("emit-vision init requires either apiKey or dsn");
132
- }
133
- return {
134
- apiKey,
135
- endpoint: stripTrailingSlash(options.endpoint ?? dsn?.endpoint)
136
- };
137
- }
138
- var ANON_ID_KEY = "__ev_anon_id";
139
- function getOrCreateAnonymousId() {
140
- try {
141
- const stored = localStorage.getItem(ANON_ID_KEY);
142
- if (stored) return stored;
143
- const id = crypto.randomUUID();
144
- localStorage.setItem(ANON_ID_KEY, id);
145
- return id;
146
- } catch {
147
- return void 0;
148
- }
149
- }
150
- function resolveAutoCapture(options) {
151
- const legacyDefault = options.autoCaptureErrors === void 0 ? true : options.autoCaptureErrors;
152
- return {
153
- errors: options.autoCapture?.errors ?? legacyDefault,
154
- unhandledRejections: options.autoCapture?.unhandledRejections ?? legacyDefault,
155
- flagExposures: options.autoCapture?.flagExposures ?? true,
156
- pageViews: options.autoCapture?.pageViews ?? false
157
- };
158
- }
159
-
160
- // src/client-helpers.ts
161
- var SDK_NAME = "emit-vision-js";
162
- var SDK_VERSION = "0.4.0";
163
- var MAX_EVENT_BYTES = 64 * 1024;
164
- var MAX_BATCH_BYTES = 1 * 1024 * 1024;
165
- function byteLength(s) {
166
- return new TextEncoder().encode(s).length;
167
- }
168
- function trimEventIfOversized(payload) {
169
- const json = JSON.stringify(payload);
170
- const originalBytes = byteLength(json);
171
- if (originalBytes <= MAX_EVENT_BYTES) {
172
- return { payload, trimmedFields: null, originalBytes };
173
- }
174
- const propBytes = payload.properties ? byteLength(JSON.stringify(payload.properties)) : 0;
175
- const ctxBytes = payload.context ? byteLength(JSON.stringify(payload.context)) : 0;
176
- if (typeof console.warn === "function") {
177
- console.warn(
178
- `[emit-vision] event truncated \u2014 "${payload.name}" was ${originalBytes} bytes (max ${MAX_EVENT_BYTES}); properties/context trimmed.`
179
- );
180
- }
181
- const trimmedFields = [];
182
- if (propBytes > 0) trimmedFields.push("properties");
183
- if (ctxBytes > 0) trimmedFields.push("context");
184
- if (payload.type === "error" && payload.error.stack) {
185
- trimmedFields.push("error.stack");
186
- }
187
- const base = {
188
- properties: propBytes > 0 ? { _truncated: true, _originalBytes: propBytes } : void 0,
189
- context: ctxBytes > 0 ? { _truncated: true, _originalBytes: ctxBytes } : void 0
190
- };
191
- if (payload.type === "error") {
192
- return {
193
- payload: {
194
- ...payload,
195
- ...base,
196
- error: { ...payload.error, stack: void 0 }
197
- },
198
- trimmedFields,
199
- originalBytes
200
- };
201
- }
202
- return { payload: { ...payload, ...base }, trimmedFields, originalBytes };
203
- }
204
- function buildBatchBody(chunk) {
205
- return JSON.stringify({
206
- events: chunk.map((item) => ({
207
- ...item,
208
- sdk: { name: SDK_NAME, version: SDK_VERSION }
209
- }))
210
- });
211
- }
212
- function splitBatch(batch) {
213
- if (batch.length <= 1) return [batch];
214
- const body = buildBatchBody(batch);
215
- if (byteLength(body) <= MAX_BATCH_BYTES) return [batch];
216
- const mid = Math.floor(batch.length / 2);
217
- return [...splitBatch(batch.slice(0, mid)), ...splitBatch(batch.slice(mid))];
218
- }
219
-
220
- // src/client-internals.ts
221
- function debugLog(message, opts, payload) {
222
- if (!opts.debug || typeof console.debug !== "function") return;
223
- const prefix = opts.label ? `[emit-vision:${opts.label}]` : "[emit-vision]";
224
- if (payload === void 0) {
225
- console.debug(prefix, message);
226
- return;
227
- }
228
- console.debug(prefix, message, payload);
229
- }
230
- function buildMetaEvent(originalName, originalBytes, trimmedFields, opts) {
231
- return {
232
- type: "event",
233
- name: "sdk.event_truncated",
234
- properties: {
235
- originalName,
236
- originalBytes,
237
- trimmedFields,
238
- sdkName: SDK_NAME
239
- },
240
- environment: opts.environment,
241
- release: opts.release
242
- };
243
- }
244
- function buildWithDefaults(clientUser, clientContext, clientDeployment, clientFeatureFlags, clientTags, sdkOpts, captureOpts) {
245
- const {
246
- context,
247
- deployment,
248
- featureFlags,
249
- tags,
250
- user,
251
- environment,
252
- release,
253
- sessionId,
254
- ...rest
255
- } = captureOpts;
256
- return {
257
- ...rest,
258
- environment: environment ?? sdkOpts.environment,
259
- release: release ?? sdkOpts.release,
260
- sessionId: sessionId ?? sdkOpts.sessionId,
261
- user: user ?? clientUser,
262
- context: Object.keys(clientContext).length ? { ...clientContext, ...context } : context,
263
- deployment: mergeDeploymentFn(clientDeployment, deployment),
264
- featureFlags: mergeFeatureFlagsFn(clientFeatureFlags, featureFlags),
265
- tags: Object.keys(clientTags).length ? { ...clientTags, ...tags } : tags
266
- };
267
- }
268
- function mergeDeploymentFn(clientDeployment, overrideDeployment) {
269
- const merged = { ...clientDeployment ?? {}, ...overrideDeployment ?? {} };
270
- return Object.keys(merged).length > 0 ? merged : void 0;
271
- }
272
- function mergeFeatureFlagsFn(clientFlags, overrideFlags) {
273
- const merged = { ...clientFlags, ...overrideFlags ?? {} };
274
- return Object.keys(merged).length > 0 ? merged : void 0;
275
- }
276
- function buildFlushDisabledMessage(label, consecutiveFailures, dropped) {
277
- return `[emit-vision${label ? `:${label}` : ""}] disabled after ${consecutiveFailures} consecutive flush failures; dropping ${dropped} queued events. Re-init the SDK to resume.`;
278
- }
279
- function buildAuthDisabledMessage(label, status, dropped) {
280
- 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.`;
281
- }
282
- var AUTH_FAILURE_CAP = 3;
283
- var FlushHttpError = class extends Error {
284
- constructor(status, retryAfterSeconds, serverMessage) {
285
- super(`emit-vision flush failed with ${status}`);
286
- this.status = status;
287
- this.retryAfterSeconds = retryAfterSeconds;
288
- this.serverMessage = serverMessage;
289
- }
290
- status;
291
- retryAfterSeconds;
292
- serverMessage;
293
- };
294
- async function executeHttpSend(chunk, endpoint, apiKey, fetchImpl) {
295
- const response = await fetchImpl(`${endpoint}/v1/batch`, {
296
- method: "POST",
297
- headers: { "content-type": "application/json", "x-emit-api-key": apiKey },
298
- body: buildBatchBody(chunk),
299
- keepalive: true
300
- });
301
- if (!response.ok) {
302
- const retryAfter = Number(response.headers?.get?.("retry-after"));
303
- let serverMessage;
304
- try {
305
- const body = await response.json();
306
- if (body && typeof body.message === "string") serverMessage = body.message;
307
- } catch {
308
- }
309
- throw new FlushHttpError(
310
- response.status,
311
- Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : void 0,
312
- serverMessage
313
- );
314
- }
315
- }
316
-
317
- // src/retry.ts
318
- var FlushRetryController = class {
319
- constructor(opts) {
320
- this.opts = opts;
321
- }
322
- opts;
323
- consecutiveFailures = 0;
324
- backoffUntil = 0;
325
- disabled = false;
326
- isDisabled() {
327
- return this.disabled;
328
- }
329
- isBackingOff(now = Date.now()) {
330
- return !this.disabled && now < this.backoffUntil;
331
- }
332
- msUntilReady(now = Date.now()) {
333
- if (this.disabled) return Infinity;
334
- return Math.max(0, this.backoffUntil - now);
335
- }
336
- forceDisable() {
337
- this.disabled = true;
338
- this.backoffUntil = 0;
339
- }
340
- reset() {
341
- this.consecutiveFailures = 0;
342
- this.backoffUntil = 0;
343
- }
344
- /** Push the next retry out to honor a server-sent Retry-After hint. Capped at 5 minutes; never shortens an existing backoff. */
345
- applyRetryAfter(seconds, now = Date.now()) {
346
- if (this.disabled || !Number.isFinite(seconds) || seconds <= 0) return;
347
- const cappedMs = Math.min(seconds, 300) * 1e3;
348
- this.backoffUntil = Math.max(this.backoffUntil, now + cappedMs);
349
- }
350
- recordFailure(now = Date.now()) {
351
- this.consecutiveFailures += 1;
352
- if (this.opts.failureCap > 0 && this.consecutiveFailures >= this.opts.failureCap) {
353
- const wasDisabled = this.disabled;
354
- this.disabled = true;
355
- this.backoffUntil = 0;
356
- return {
357
- backoffMs: 0,
358
- consecutiveFailures: this.consecutiveFailures,
359
- justDisabled: !wasDisabled
360
- };
361
- }
362
- if (this.opts.initialMs <= 0) {
363
- this.backoffUntil = 0;
364
- return {
365
- backoffMs: 0,
366
- consecutiveFailures: this.consecutiveFailures,
367
- justDisabled: false
368
- };
369
- }
370
- const exp = Math.min(
371
- this.opts.initialMs * Math.pow(2, this.consecutiveFailures - 1),
372
- this.opts.maxMs
373
- );
374
- const jitter = exp * 0.2 * (Math.random() * 2 - 1);
375
- const backoffMs = Math.max(0, Math.floor(exp + jitter));
376
- this.backoffUntil = now + backoffMs;
377
- return {
378
- backoffMs,
379
- consecutiveFailures: this.consecutiveFailures,
380
- justDisabled: false
381
- };
382
- }
383
- };
384
-
385
- // src/persistence.ts
386
- var MAX_PERSISTED_EVENTS = 200;
387
- var MAX_PERSISTED_AGE_MS = 24 * 60 * 60 * 1e3;
388
- function persistenceKey(apiKey, projectId) {
389
- return `emit-vision:queue:${projectId ?? "-"}:${apiKey.slice(0, 16)}`;
390
- }
391
- function getStorage() {
392
- try {
393
- if (typeof localStorage === "undefined") return null;
394
- return localStorage;
395
- } catch {
396
- return null;
397
- }
398
- }
399
- function persistQueue(key, queue) {
400
- const storage = getStorage();
401
- if (!storage || queue.length === 0) return;
402
- const record = {
403
- savedAt: Date.now(),
404
- events: queue.slice(0, MAX_PERSISTED_EVENTS)
405
- };
406
- try {
407
- storage.setItem(key, JSON.stringify(record));
408
- } catch {
409
- }
410
- }
411
- function restoreQueue(key, now = Date.now()) {
412
- const storage = getStorage();
413
- if (!storage) return [];
414
- try {
415
- const raw = storage.getItem(key);
416
- storage.removeItem(key);
417
- if (!raw) return [];
418
- const parsed = JSON.parse(raw);
419
- if (typeof parsed.savedAt !== "number" || !Array.isArray(parsed.events)) {
420
- return [];
421
- }
422
- if (now - parsed.savedAt > MAX_PERSISTED_AGE_MS) return [];
423
- return parsed.events.slice(0, MAX_PERSISTED_EVENTS);
424
- } catch {
425
- return [];
426
- }
427
- }
428
- function clearPersistedQueue(key) {
429
- const storage = getStorage();
430
- if (!storage) return;
431
- try {
432
- storage.removeItem(key);
433
- } catch {
434
- }
435
- }
436
-
437
- // src/devtools-channel.ts
438
- function dispatchDevToolsEvent(type, payload) {
439
- if (typeof window === "undefined" || typeof window.dispatchEvent !== "function") {
440
- return;
441
- }
442
- window.dispatchEvent(
443
- new CustomEvent("__emitVisionDevTools", {
444
- detail: { type, payload, timestamp: Date.now() }
445
- })
446
- );
447
- }
448
-
449
- // src/client-flush.ts
450
- var FlushManager = class {
451
- constructor(options, fetchImpl) {
452
- this.options = options;
453
- this.fetchImpl = fetchImpl;
454
- this.consentGiven = !options.consentRequired || (options.consentGiven ?? false);
455
- this.storageKey = persistenceKey(options.apiKey, options.projectId);
456
- this.retry = new FlushRetryController({
457
- initialMs: options.retryBackoffInitialMs,
458
- maxMs: options.retryBackoffMaxMs,
459
- failureCap: options.maxConsecutiveFlushFailures
460
- });
461
- this.timer = setInterval(() => {
462
- if (this.queue.length === 0 || this.retry.isBackingOff()) return;
463
- this.flush().catch(
464
- (err) => debugLog("background flush error", this.options, { error: err })
465
- );
466
- }, options.flushIntervalMs);
467
- }
468
- options;
469
- fetchImpl;
470
- queue = [];
471
- timer;
472
- flushScheduled = false;
473
- retry;
474
- storageKey;
475
- consentGiven;
476
- authFailureCount = 0;
477
- enqueue(payload) {
478
- if (this.retry.isDisabled()) return;
479
- const {
480
- payload: trimmed,
481
- trimmedFields,
482
- originalBytes
483
- } = trimEventIfOversized(payload);
484
- this.queue.push(trimmed);
485
- if (trimmedFields !== null) {
486
- const meta = buildMetaEvent(
487
- payload.name,
488
- originalBytes,
489
- trimmedFields,
490
- this.options
491
- );
492
- this.queue.push(meta);
493
- }
494
- debugLog("queued payload", this.options, {
495
- type: payload.type,
496
- name: payload.name,
497
- queueSize: this.queue.length,
498
- flushOnCapture: this.options.flushOnCapture,
499
- batchSize: this.options.batchSize
500
- });
501
- if (this.options.flushOnCapture) {
502
- if (!this.retry.isBackingOff()) {
503
- this.flush().catch(
504
- (err) => debugLog("capture flush error", this.options, { error: err })
505
- );
506
- }
507
- return;
508
- }
509
- if (this.queue.length >= this.options.batchSize && !this.retry.isBackingOff()) {
510
- this.flush().catch(
511
- (err) => debugLog("batch flush error", this.options, { error: err })
512
- );
513
- }
514
- }
515
- giveConsent() {
516
- this.consentGiven = true;
517
- return this.flush();
518
- }
519
- /** Restore any queue persisted by a previous page load, prepending it so older events flush first. */
520
- restorePersisted() {
521
- const restored = restoreQueue(this.storageKey);
522
- if (restored.length === 0) return;
523
- this.queue.unshift(...restored);
524
- debugLog("restored persisted queue", this.options, {
525
- count: restored.length
526
- });
527
- }
528
- /** Spill the current queue to localStorage (pagehide / tab hidden). No-op without consent. */
529
- persistNow() {
530
- if (!this.consentGiven || this.retry.isDisabled()) return;
531
- persistQueue(this.storageKey, this.queue);
532
- }
533
- async flush() {
534
- if (this.retry.isDisabled()) return;
535
- if (!this.consentGiven) return;
536
- const batch = this.queue.splice(0, this.options.batchSize);
537
- if (batch.length === 0) return;
538
- debugLog("flushing batch", this.options, { count: batch.length });
539
- const chunks = splitBatch(batch);
540
- for (let i = 0; i < chunks.length; i++) {
541
- const tailCount = chunks.slice(i + 1).reduce((s, c) => s + c.length, 0);
542
- try {
543
- await this.flushChunk(chunks[i], tailCount);
544
- } catch (err) {
545
- if (!this.retry.isDisabled()) {
546
- const tail = chunks.slice(i + 1).flat();
547
- if (tail.length > 0) {
548
- this.queue.splice(chunks[i].length, 0, ...tail);
549
- }
550
- }
551
- throw err;
552
- }
553
- }
554
- if (this.options.flushOnCapture && this.queue.length > 0) {
555
- this.scheduleFlush();
556
- }
557
- }
558
- close() {
559
- if (this.timer) {
560
- clearInterval(this.timer);
561
- this.timer = void 0;
562
- }
563
- }
564
- async flushChunk(chunk, tailCount) {
565
- try {
566
- await executeHttpSend(
567
- chunk,
568
- this.options.endpoint,
569
- this.options.apiKey,
570
- this.fetchImpl
571
- );
572
- } catch (err) {
573
- this.handleFlushFailure(
574
- chunk,
575
- tailCount,
576
- err instanceof FlushHttpError ? {
577
- reason: "status",
578
- status: err.status,
579
- retryAfterSeconds: err.retryAfterSeconds,
580
- serverMessage: err.serverMessage
581
- } : { reason: "network", error: err }
582
- );
583
- throw err;
584
- }
585
- this.retry.reset();
586
- this.authFailureCount = 0;
587
- clearPersistedQueue(this.storageKey);
588
- debugLog("flush complete", this.options, { count: chunk.length });
589
- dispatchDevToolsEvent("flush", { count: chunk.length });
590
- }
591
- handleFlushFailure(batch, tailCount, detail) {
592
- if (detail.reason === "status" && (detail.status === 401 || detail.status === 403)) {
593
- this.authFailureCount += 1;
594
- if (this.authFailureCount >= AUTH_FAILURE_CAP) {
595
- const dropped = this.queue.length + batch.length + tailCount;
596
- this.queue = [];
597
- this.close();
598
- this.retry.forceDisable();
599
- const msg = buildAuthDisabledMessage(
600
- this.options.label,
601
- detail.status,
602
- dropped
603
- );
604
- if (typeof console.warn === "function") console.warn(msg);
605
- debugLog("disabled (auth)", this.options, {
606
- status: detail.status,
607
- authFailures: this.authFailureCount,
608
- dropped
609
- });
610
- return;
611
- }
612
- }
613
- const result = this.retry.recordFailure();
614
- if (detail.reason === "status" && (detail.status === 429 || detail.status === 503) && detail.retryAfterSeconds !== void 0) {
615
- this.retry.applyRetryAfter(detail.retryAfterSeconds);
616
- }
617
- if (result.justDisabled) {
618
- const dropped = this.queue.length + batch.length + tailCount;
619
- this.queue = [];
620
- this.close();
621
- const msg = buildFlushDisabledMessage(
622
- this.options.label,
623
- result.consecutiveFailures,
624
- dropped
625
- );
626
- if (typeof console.warn === "function") console.warn(msg);
627
- debugLog("disabled", this.options, {
628
- consecutiveFailures: result.consecutiveFailures,
629
- dropped
630
- });
631
- return;
632
- }
633
- this.queue.unshift(...batch);
634
- this.persistNow();
635
- const base = {
636
- restoredCount: batch.length,
637
- consecutiveFailures: result.consecutiveFailures,
638
- backoffMs: result.backoffMs
639
- };
640
- if (detail.reason === "network") {
641
- debugLog("flush network error", this.options, {
642
- error: detail.error,
643
- ...base
644
- });
645
- } else {
646
- debugLog("flush failed", this.options, {
647
- status: detail.status,
648
- serverMessage: detail.serverMessage,
649
- ...base
650
- });
651
- }
652
- }
653
- scheduleFlush() {
654
- if (this.flushScheduled) return;
655
- this.flushScheduled = true;
656
- queueMicrotask(() => {
657
- this.flushScheduled = false;
658
- if (this.queue.length === 0 || this.retry.isBackingOff()) return;
659
- this.flush().catch(
660
- (err) => debugLog("scheduled flush error", this.options, { error: err })
661
- );
662
- });
663
- }
664
- };
665
-
666
- // src/client-autocapture.ts
667
- var BrowserAutoCapture = class {
668
- constructor(onCapture, withDefaults, onPageView, config) {
669
- this.onCapture = onCapture;
670
- this.withDefaults = withDefaults;
671
- this.onPageView = onPageView;
672
- this.config = config;
673
- }
674
- onCapture;
675
- withDefaults;
676
- onPageView;
677
- config;
678
- attach() {
679
- if (typeof window === "undefined") return;
680
- if (this.config.errors) {
681
- window.addEventListener("error", this.handleWindowError);
682
- }
683
- if (this.config.unhandledRejections) {
684
- window.addEventListener(
685
- "unhandledrejection",
686
- this.handleUnhandledRejection
687
- );
688
- }
689
- if (this.config.pageViews) {
690
- this.onPageView();
691
- window.addEventListener("popstate", this.handlePopState);
692
- this.patchHistoryMethod("pushState");
693
- this.patchHistoryMethod("replaceState");
694
- }
695
- }
696
- detach() {
697
- if (typeof window === "undefined") return;
698
- if (this.config.errors) {
699
- window.removeEventListener("error", this.handleWindowError);
700
- }
701
- if (this.config.unhandledRejections) {
702
- window.removeEventListener(
703
- "unhandledrejection",
704
- this.handleUnhandledRejection
705
- );
706
- }
707
- if (this.config.pageViews) {
708
- window.removeEventListener("popstate", this.handlePopState);
709
- this.restoreHistoryMethod("pushState");
710
- this.restoreHistoryMethod("replaceState");
711
- }
712
- }
713
- handleWindowError = (event) => {
714
- const serialized = serializeError(event.error ?? event.message);
715
- this.onCapture({
716
- type: "error",
717
- name: "error",
718
- error: { ...serialized, handled: false },
719
- ...this.withDefaults({
720
- context: {
721
- source: "window.error",
722
- url: window.location.href,
723
- page: window.location.pathname
724
- }
725
- })
726
- });
727
- };
728
- handlePopState = () => {
729
- this.onPageView();
730
- };
731
- handleUnhandledRejection = (event) => {
732
- const serialized = serializeError(event.reason);
733
- this.onCapture({
734
- type: "error",
735
- name: "error",
736
- error: { ...serialized, handled: false },
737
- ...this.withDefaults({
738
- context: {
739
- source: "window.unhandledrejection",
740
- url: window.location.href,
741
- page: window.location.pathname
742
- }
743
- })
744
- });
745
- };
746
- patchHistoryMethod(method) {
747
- const original = history[method].bind(history);
748
- const historyWithCustom = history;
749
- historyWithCustom[`__ev_orig_${method}`] = original;
750
- historyWithCustom[method] = (...args) => {
751
- original(...args);
752
- this.onPageView();
753
- };
754
- }
755
- restoreHistoryMethod(method) {
756
- const historyWithCustom = history;
757
- const original = historyWithCustom[`__ev_orig_${method}`];
758
- if (original) {
759
- historyWithCustom[method] = original;
760
- }
761
- }
762
- };
763
-
764
- // src/client.ts
765
- var EmitVisionClient = class {
766
- constructor(options) {
767
- this.options = options;
768
- this.groupId = options.groupId;
769
- this.deployment = options.deployment ? { ...options.deployment } : void 0;
770
- this.featureFlags = options.featureFlags ? { ...options.featureFlags } : {};
771
- this.flusher = new FlushManager(options, options.fetchImpl);
772
- this.flusher.restorePersisted();
773
- if (typeof window !== "undefined") {
774
- window.addEventListener("pagehide", this.persistOnHide);
775
- }
776
- if (typeof document !== "undefined") {
777
- document.addEventListener("visibilitychange", this.persistOnHidden);
778
- }
779
- this.flagEval = new FlagEvaluator(
780
- {
781
- endpoint: options.endpoint,
782
- apiKey: options.apiKey,
783
- ttlMs: options.flagEvalTtlMs,
784
- flagExposures: options.autoCapture.flagExposures,
785
- fetchImpl: options.fetchImpl
786
- },
787
- (flags) => {
788
- this.featureFlags = { ...this.featureFlags, ...flags };
789
- },
790
- (flagKey, variantKey, variantValue, reason, env) => this.captureExposure(flagKey, variantKey, variantValue, reason, env),
791
- (msg, data) => debugLog(msg, this.options, data)
792
- );
793
- this.autoCapture = new BrowserAutoCapture(
794
- (payload) => this.flusher.enqueue(payload),
795
- (opts) => this.withDefaults(opts),
796
- () => this.capturePageView(),
797
- options.autoCapture
798
- );
799
- this.autoCapture.attach();
800
- debugLog("initialized", this.options, {
801
- endpoint: options.endpoint,
802
- environment: options.environment,
803
- release: options.release,
804
- autoCapture: options.autoCapture,
805
- batchSize: options.batchSize,
806
- flushIntervalMs: options.flushIntervalMs,
807
- flushOnCapture: options.flushOnCapture
808
- });
809
- dispatchDevToolsEvent("init", {
810
- sdkVersion: SDK_VERSION,
811
- endpoint: options.endpoint,
812
- environment: options.environment,
813
- release: options.release,
814
- sessionId: options.sessionId ?? "",
815
- autoCapture: options.autoCapture,
816
- projectId: options.projectId,
817
- consentRequired: options.consentRequired
818
- });
819
- }
820
- options;
821
- user;
822
- groupId;
823
- context = {};
824
- deployment;
825
- featureFlags = {};
826
- tags = {};
827
- flagEval;
828
- autoCapture;
829
- flusher;
830
- persistOnHide = () => this.flusher.persistNow();
831
- persistOnHidden = () => {
832
- if (typeof document !== "undefined" && document.visibilityState === "hidden") {
833
- this.flusher.persistNow();
834
- }
835
- };
836
- captureEvent(name, properties, options = {}) {
837
- const eventId = options.eventId ?? (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : void 0);
838
- debugLog("queue event", this.options, { name, properties, options });
839
- this.flusher.enqueue({
840
- type: "event",
841
- name,
842
- properties,
843
- ...this.withDefaults({ ...options, eventId })
844
- });
845
- dispatchDevToolsEvent("event", {
846
- name,
847
- properties,
848
- eventId,
849
- timestamp: options.timestamp,
850
- environment: options.environment,
851
- sessionId: options.sessionId
852
- });
853
- }
854
- captureError(error, options = {}) {
855
- const serialized = serializeError(error);
856
- debugLog("queue error", this.options, {
857
- message: serialized.message,
858
- options
859
- });
860
- this.flusher.enqueue({
861
- type: "error",
862
- name: "error",
863
- error: serialized,
864
- ...this.withDefaults(options)
865
- });
866
- }
867
- identify(userIdOrUser, traits = {}) {
868
- this.user = typeof userIdOrUser === "string" ? { id: userIdOrUser, ...traits } : { ...userIdOrUser };
869
- debugLog("identified user", this.options, this.user);
870
- const { id, ...rest } = this.user;
871
- dispatchDevToolsEvent("identify", { userId: id, traits: rest });
872
- }
873
- group(groupId, traits) {
874
- this.groupId = groupId;
875
- debugLog("set group", this.options, { groupId, traits });
876
- dispatchDevToolsEvent("group", { groupId, traits });
877
- }
878
- setContext(context) {
879
- this.context = { ...this.context, ...context };
880
- debugLog("updated context", this.options, this.context);
881
- }
882
- setDeploymentContext(context) {
883
- this.deployment = Object.keys(context).length > 0 ? { ...context } : void 0;
884
- debugLog("updated deployment context", this.options, this.deployment);
885
- }
886
- setFeatureFlags(featureFlags) {
887
- this.featureFlags = { ...featureFlags };
888
- debugLog("updated feature flags", this.options, this.featureFlags);
889
- }
890
- evaluateFlags(opts) {
891
- return this.flagEval.evaluateFlags({
892
- ...opts,
893
- environment: opts.environment ?? this.options.environment
894
- });
895
- }
896
- getFlag(flagKey, fallback, opts) {
897
- return this.flagEval.getFlag(flagKey, fallback, {
898
- ...opts,
899
- environment: opts.environment ?? this.options.environment
900
- });
901
- }
902
- refreshFlags(opts) {
903
- return this.flagEval.refreshFlags({
904
- ...opts,
905
- environment: opts.environment ?? this.options.environment
906
- });
907
- }
908
- capturePageView(path) {
909
- const page = path ?? (typeof window !== "undefined" ? window.location.pathname : void 0);
910
- if (!page) return;
911
- this.captureEvent("$page_view", { page });
912
- }
913
- captureFeedback(feedback, options = {}) {
914
- const url = feedback.url ?? (typeof window !== "undefined" ? window.location.href : void 0);
915
- this.captureEvent(
916
- "$feedback",
917
- { message: feedback.message, ...feedback.rating !== void 0 ? { rating: feedback.rating } : {}, ...url ? { url } : {} },
918
- options
919
- );
920
- }
921
- captureExposure(flagKey, variantKey, variantValue, reason, environment) {
922
- this.captureEvent("$flag_exposure", {
923
- flagKey,
924
- variantKey,
925
- ...variantValue !== void 0 ? { variantValue } : {},
926
- ...reason ? { reason } : {},
927
- environment: environment ?? this.options.environment
928
- });
929
- }
930
- setTags(tags) {
931
- this.tags = { ...this.tags, ...tags };
932
- debugLog("updated tags", this.options, this.tags);
933
- }
934
- consent() {
935
- return this.flusher.giveConsent();
936
- }
937
- flush() {
938
- return this.flusher.flush();
939
- }
940
- close() {
941
- if (typeof window !== "undefined") {
942
- window.removeEventListener("pagehide", this.persistOnHide);
943
- }
944
- if (typeof document !== "undefined") {
945
- document.removeEventListener("visibilitychange", this.persistOnHidden);
946
- }
947
- this.flusher.close();
948
- this.autoCapture.detach();
949
- debugLog("closed client", this.options);
950
- }
951
- withDefaults(opts) {
952
- return buildWithDefaults(
953
- this.user,
954
- this.context,
955
- this.deployment,
956
- this.featureFlags,
957
- this.tags,
958
- this.options,
959
- { groupId: this.groupId, ...opts }
960
- );
961
- }
962
- };
963
-
964
- // src/index.ts
965
- var client;
966
- function init(options) {
967
- const transport = resolveTransport(options);
968
- const autoCapture = resolveAutoCapture(options);
969
- const resolved = {
970
- ...options,
971
- flushIntervalMs: options.flushIntervalMs ?? 5e3,
972
- batchSize: options.batchSize ?? 20,
973
- debug: options.debug ?? false,
974
- fetchImpl: options.fetchImpl ?? globalThis.fetch.bind(globalThis),
975
- flushOnCapture: options.flushOnCapture ?? false,
976
- flagEvalTtlMs: options.flagEvalTtlMs ?? 6e4,
977
- retryBackoffInitialMs: options.retryBackoffInitialMs ?? 1e3,
978
- retryBackoffMaxMs: options.retryBackoffMaxMs ?? 3e4,
979
- maxConsecutiveFlushFailures: options.maxConsecutiveFlushFailures ?? 20,
980
- apiKey: transport.apiKey,
981
- endpoint: transport.endpoint ?? options.endpoint ?? "http://localhost:4301",
982
- autoCapture,
983
- sessionId: options.sessionId ?? getOrCreateAnonymousId()
984
- };
985
- client?.close();
986
- client = new EmitVisionClient(resolved);
987
- return client;
988
- }
989
- function captureEvent(name, properties, options) {
990
- requireClient().captureEvent(name, properties, options);
991
- }
992
- function captureError(error, options) {
993
- requireClient().captureError(error, options);
994
- }
995
- function capturePageView(path) {
996
- requireClient().capturePageView(path);
997
- }
998
- function captureFeedback(feedback, options) {
999
- requireClient().captureFeedback(feedback, options);
1000
- }
1001
- function identify(userIdOrUser, traits) {
1002
- requireClient().identify(userIdOrUser, traits);
1003
- }
1004
- function setContext(context) {
1005
- requireClient().setContext(context);
1006
- }
1007
- function setDeploymentContext(context) {
1008
- requireClient().setDeploymentContext(context);
1009
- }
1010
- function setFeatureFlags(featureFlags) {
1011
- requireClient().setFeatureFlags(featureFlags);
1012
- }
1013
- function setTags(tags) {
1014
- requireClient().setTags(tags);
1015
- }
1016
- async function flush() {
1017
- await requireClient().flush();
1018
- }
1019
- async function consent() {
1020
- await requireClient().consent();
1021
- }
1022
- function group(groupId, traits) {
1023
- requireClient().group(groupId, traits);
1024
- }
1025
- async function evaluateFlags(options) {
1026
- return requireClient().evaluateFlags(options);
1027
- }
1028
- async function getFlag(flagKey, fallback, options) {
1029
- return requireClient().getFlag(flagKey, fallback, options);
1030
- }
1031
- async function refreshFlags(options) {
1032
- return requireClient().refreshFlags(options);
1033
- }
1034
- function captureExposure(flagKey, variantKey, variantValue, reason, environment) {
1035
- requireClient().captureExposure(
1036
- flagKey,
1037
- variantKey,
1038
- variantValue,
1039
- reason,
1040
- environment
1041
- );
1042
- }
1043
- function requireClient() {
1044
- if (!client) {
1045
- throw new Error("emit-vision SDK has not been initialized");
1046
- }
1047
- return client;
1048
- }
1049
- export {
1050
- captureError,
1051
- captureEvent,
1052
- captureExposure,
1053
- captureFeedback,
1054
- capturePageView,
1055
- consent,
1056
- evaluateFlags,
1057
- flush,
1058
- getFlag,
1059
- group,
1060
- identify,
1061
- init,
1062
- refreshFlags,
1063
- setContext,
1064
- setDeploymentContext,
1065
- setFeatureFlags,
1066
- setTags
1067
- };
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};