@emit-vision/sdk-js 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,519 +1,840 @@
1
- import { FlushRetryController } from "./retry.js";
2
- const SDK_NAME = "emit-vision-js";
3
- const SDK_VERSION = "0.1.0";
4
- class EmitVisionClient {
5
- options;
6
- queue = [];
7
- user;
8
- context = {};
9
- deployment;
10
- featureFlags = {};
11
- tags = {};
12
- timer;
13
- flushScheduled = false;
14
- fetchImpl;
15
- flagCache = new Map();
16
- retry;
17
- constructor(options) {
18
- this.options = options;
19
- this.fetchImpl = options.fetchImpl;
20
- this.deployment = options.deployment
21
- ? { ...options.deployment }
22
- : undefined;
23
- this.featureFlags = options.featureFlags ? { ...options.featureFlags } : {};
24
- this.retry = new FlushRetryController({
25
- initialMs: options.retryBackoffInitialMs,
26
- maxMs: options.retryBackoffMaxMs,
27
- failureCap: options.maxConsecutiveFlushFailures,
28
- });
29
- this.timer = setInterval(() => {
30
- if (this.queue.length === 0 || this.retry.isBackingOff()) {
31
- return;
32
- }
33
- this.flush().catch((err) => this.debug("background flush error", { error: err }));
34
- }, options.flushIntervalMs);
35
- if (options.autoCapture.errors && typeof window !== "undefined") {
36
- window.addEventListener("error", this.handleWindowError);
37
- }
38
- if (options.autoCapture.unhandledRejections &&
39
- typeof window !== "undefined") {
40
- window.addEventListener("unhandledrejection", this.handleUnhandledRejection);
41
- }
42
- this.debug("initialized", {
43
- endpoint: options.endpoint,
44
- environment: options.environment,
45
- release: options.release,
46
- autoCapture: options.autoCapture,
47
- batchSize: options.batchSize,
48
- flushIntervalMs: options.flushIntervalMs,
49
- flushOnCapture: options.flushOnCapture,
50
- });
51
- }
52
- captureEvent(name, properties, options = {}) {
53
- this.debug("queue event", { name, properties, options });
54
- this.enqueue({
55
- type: "event",
56
- name,
57
- properties,
58
- ...this.withDefaults(options),
59
- });
60
- }
61
- captureError(error, options = {}) {
62
- const serialized = serializeError(error);
63
- this.debug("queue error", {
64
- message: serialized.message,
65
- handled: serialized.handled,
66
- options,
67
- });
68
- this.enqueue({
69
- type: "error",
70
- name: "error",
71
- error: serialized,
72
- ...this.withDefaults(options),
73
- });
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;
74
22
  }
75
- identify(userIdOrUser, traits = {}) {
76
- this.user =
77
- typeof userIdOrUser === "string"
78
- ? { id: userIdOrUser, ...traits }
79
- : { ...userIdOrUser };
80
- this.debug("identified user", this.user);
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;
81
32
  }
82
- setContext(context) {
83
- this.context = { ...this.context, ...context };
84
- this.debug("updated context", this.context);
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 });
85
75
  }
86
- setDeploymentContext(context) {
87
- this.deployment =
88
- Object.keys(context).length > 0 ? { ...context } : undefined;
89
- this.debug("updated deployment context", this.deployment);
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
+ }
90
88
  }
91
- setFeatureFlags(featureFlags) {
92
- this.featureFlags = { ...featureFlags };
93
- this.debug("updated feature flags", this.featureFlags);
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
+ async function executeHttpSend(chunk, endpoint, apiKey, fetchImpl) {
280
+ const response = await fetchImpl(`${endpoint}/v1/batch`, {
281
+ method: "POST",
282
+ headers: { "content-type": "application/json", "x-emit-api-key": apiKey },
283
+ body: buildBatchBody(chunk),
284
+ keepalive: true
285
+ });
286
+ if (!response.ok) {
287
+ throw new Error(`emit-vision flush failed with ${response.status}`);
288
+ }
289
+ }
290
+
291
+ // src/retry.ts
292
+ var FlushRetryController = class {
293
+ constructor(opts) {
294
+ this.opts = opts;
295
+ }
296
+ opts;
297
+ consecutiveFailures = 0;
298
+ backoffUntil = 0;
299
+ disabled = false;
300
+ isDisabled() {
301
+ return this.disabled;
302
+ }
303
+ isBackingOff(now = Date.now()) {
304
+ return !this.disabled && now < this.backoffUntil;
305
+ }
306
+ msUntilReady(now = Date.now()) {
307
+ if (this.disabled) return Infinity;
308
+ return Math.max(0, this.backoffUntil - now);
309
+ }
310
+ reset() {
311
+ this.consecutiveFailures = 0;
312
+ this.backoffUntil = 0;
313
+ }
314
+ recordFailure(now = Date.now()) {
315
+ this.consecutiveFailures += 1;
316
+ if (this.opts.failureCap > 0 && this.consecutiveFailures >= this.opts.failureCap) {
317
+ const wasDisabled = this.disabled;
318
+ this.disabled = true;
319
+ this.backoffUntil = 0;
320
+ return {
321
+ backoffMs: 0,
322
+ consecutiveFailures: this.consecutiveFailures,
323
+ justDisabled: !wasDisabled
324
+ };
94
325
  }
95
- /**
96
- * Fetch and evaluate all active flags for the given evaluation key.
97
- * Results are cached for `ttlMs` (defaults to `flagEvalTtlMs` from init options).
98
- * On network failure returns an empty object and does not throw.
99
- * Evaluated variants are automatically merged into the SDK's feature-flag context.
100
- */
101
- async evaluateFlags(opts) {
102
- const env = opts.environment ?? this.options.environment ?? "";
103
- const ttl = opts.ttlMs ?? this.options.flagEvalTtlMs;
104
- const key = `${env}:${opts.evaluationKey}`;
105
- const cached = this.flagCache.get(key);
106
- if (cached && Date.now() < cached.expiresAt) {
107
- this.debug("flag eval cache hit", { key });
108
- return cached.flags;
109
- }
110
- return this.fetchAndCacheFlags(opts, env, ttl, key);
326
+ if (this.opts.initialMs <= 0) {
327
+ this.backoffUntil = 0;
328
+ return {
329
+ backoffMs: 0,
330
+ consecutiveFailures: this.consecutiveFailures,
331
+ justDisabled: false
332
+ };
111
333
  }
112
- /**
113
- * Return a single flag's value, evaluated for the given key.
114
- * Returns `fallback` on network failure or when the flag is not found — never throws.
115
- */
116
- async getFlag(flagKey, fallback, opts) {
117
- try {
118
- const flags = await this.evaluateFlags(opts);
119
- const value = flags[flagKey];
120
- return (value !== undefined ? value : fallback);
121
- }
122
- catch {
123
- return fallback;
124
- }
334
+ const exp = Math.min(
335
+ this.opts.initialMs * Math.pow(2, this.consecutiveFailures - 1),
336
+ this.opts.maxMs
337
+ );
338
+ const jitter = exp * 0.2 * (Math.random() * 2 - 1);
339
+ const backoffMs = Math.max(0, Math.floor(exp + jitter));
340
+ this.backoffUntil = now + backoffMs;
341
+ return {
342
+ backoffMs,
343
+ consecutiveFailures: this.consecutiveFailures,
344
+ justDisabled: false
345
+ };
346
+ }
347
+ };
348
+
349
+ // src/client-flush.ts
350
+ var FlushManager = class {
351
+ constructor(options, fetchImpl) {
352
+ this.options = options;
353
+ this.fetchImpl = fetchImpl;
354
+ this.retry = new FlushRetryController({
355
+ initialMs: options.retryBackoffInitialMs,
356
+ maxMs: options.retryBackoffMaxMs,
357
+ failureCap: options.maxConsecutiveFlushFailures
358
+ });
359
+ this.timer = setInterval(() => {
360
+ if (this.queue.length === 0 || this.retry.isBackingOff()) return;
361
+ this.flush().catch(
362
+ (err) => debugLog("background flush error", this.options, { error: err })
363
+ );
364
+ }, options.flushIntervalMs);
365
+ }
366
+ options;
367
+ fetchImpl;
368
+ queue = [];
369
+ timer;
370
+ flushScheduled = false;
371
+ retry;
372
+ enqueue(payload) {
373
+ if (this.retry.isDisabled()) return;
374
+ const {
375
+ payload: trimmed,
376
+ trimmedFields,
377
+ originalBytes
378
+ } = trimEventIfOversized(payload);
379
+ this.queue.push(trimmed);
380
+ if (trimmedFields !== null) {
381
+ const meta = buildMetaEvent(
382
+ payload.name,
383
+ originalBytes,
384
+ trimmedFields,
385
+ this.options
386
+ );
387
+ this.queue.push(meta);
125
388
  }
126
- /**
127
- * Bypass the cache and re-fetch evaluations for the given key.
128
- * Useful after a flag change in the dashboard that should apply immediately.
129
- */
130
- async refreshFlags(opts) {
131
- const env = opts.environment ?? this.options.environment ?? "";
132
- const ttl = opts.ttlMs ?? this.options.flagEvalTtlMs;
133
- const key = `${env}:${opts.evaluationKey}`;
134
- this.flagCache.delete(key);
135
- return this.fetchAndCacheFlags(opts, env, ttl, key);
389
+ debugLog("queued payload", this.options, {
390
+ type: payload.type,
391
+ name: payload.name,
392
+ queueSize: this.queue.length,
393
+ flushOnCapture: this.options.flushOnCapture,
394
+ batchSize: this.options.batchSize
395
+ });
396
+ if (this.options.flushOnCapture) {
397
+ if (!this.retry.isBackingOff()) {
398
+ this.flush().catch(
399
+ (err) => debugLog("capture flush error", this.options, { error: err })
400
+ );
401
+ }
402
+ return;
136
403
  }
137
- captureExposure(flagKey, variantKey, variantValue, reason, environment) {
138
- this.captureEvent("$flag_exposure", {
139
- flagKey,
140
- variantKey,
141
- ...(variantValue !== undefined ? { variantValue } : {}),
142
- ...(reason ? { reason } : {}),
143
- environment: environment ?? this.options.environment,
144
- });
404
+ if (this.queue.length >= this.options.batchSize && !this.retry.isBackingOff()) {
405
+ this.flush().catch(
406
+ (err) => debugLog("batch flush error", this.options, { error: err })
407
+ );
145
408
  }
146
- async fetchAndCacheFlags(opts, env, ttl, cacheKey) {
147
- let flags = {};
148
- let evaluations = {};
149
- try {
150
- const body = {
151
- environment: env,
152
- userKey: opts.evaluationKey,
153
- };
154
- if (opts.flagKeys && opts.flagKeys.length > 0) {
155
- body.flagKeys = opts.flagKeys;
156
- }
157
- const response = await this.fetchImpl(`${this.options.endpoint}/v1/flags/evaluate`, {
158
- method: "POST",
159
- headers: {
160
- "content-type": "application/json",
161
- "x-emit-api-key": this.options.apiKey,
162
- },
163
- body: JSON.stringify(body),
164
- });
165
- if (response.ok) {
166
- const data = (await response.json());
167
- evaluations = data.evaluations;
168
- flags = Object.fromEntries(Object.entries(evaluations).map(([k, v]) => [k, v.variantValue]));
169
- this.debug("flag eval complete", { count: Object.keys(flags).length });
170
- }
171
- else {
172
- this.debug("flag eval response error", { status: response.status });
173
- }
174
- }
175
- catch (err) {
176
- this.debug("flag eval network error", { error: err });
177
- // Return empty flags on network error — never throw to callers
178
- }
179
- this.flagCache.set(cacheKey, { flags, expiresAt: Date.now() + ttl });
180
- // Merge evaluated variants into the existing feature-flag context so that
181
- // subsequent captureEvent / captureError calls include them automatically.
182
- this.featureFlags = { ...this.featureFlags, ...flags };
183
- if (this.options.autoCapture.flagExposures) {
184
- for (const [flagKey, entry] of Object.entries(evaluations)) {
185
- this.captureExposure(flagKey, entry.variantKey, entry.variantValue, entry.reason, env);
186
- }
409
+ }
410
+ async flush() {
411
+ if (this.retry.isDisabled()) return;
412
+ const batch = this.queue.splice(0, this.options.batchSize);
413
+ if (batch.length === 0) return;
414
+ debugLog("flushing batch", this.options, { count: batch.length });
415
+ const chunks = splitBatch(batch);
416
+ for (let i = 0; i < chunks.length; i++) {
417
+ const tailCount = chunks.slice(i + 1).reduce((s, c) => s + c.length, 0);
418
+ try {
419
+ await this.flushChunk(chunks[i], tailCount);
420
+ } catch (err) {
421
+ if (!this.retry.isDisabled()) {
422
+ const tail = chunks.slice(i + 1).flat();
423
+ if (tail.length > 0) {
424
+ this.queue.splice(chunks[i].length, 0, ...tail);
425
+ }
187
426
  }
188
- return flags;
427
+ throw err;
428
+ }
189
429
  }
190
- setTags(tags) {
191
- this.tags = { ...this.tags, ...tags };
192
- this.debug("updated tags", this.tags);
430
+ if (this.options.flushOnCapture && this.queue.length > 0) {
431
+ this.scheduleFlush();
193
432
  }
194
- async flush() {
195
- if (this.retry.isDisabled()) {
196
- return;
197
- }
198
- const batch = this.queue.splice(0, this.options.batchSize);
199
- if (batch.length === 0) {
200
- return;
201
- }
202
- this.debug("flushing batch", { count: batch.length });
203
- let response;
204
- try {
205
- response = await this.fetchImpl(`${this.options.endpoint}/v1/batch`, {
206
- method: "POST",
207
- headers: {
208
- "content-type": "application/json",
209
- "x-emit-api-key": this.options.apiKey,
210
- },
211
- body: JSON.stringify({
212
- events: batch.map((item) => ({
213
- ...item,
214
- sdk: { name: SDK_NAME, version: SDK_VERSION },
215
- })),
216
- }),
217
- keepalive: true,
218
- });
219
- }
220
- catch (err) {
221
- this.handleFlushFailure(batch, { reason: "network", error: err });
222
- throw err;
223
- }
224
- if (!response.ok) {
225
- const error = new Error(`emit-vision flush failed with ${response.status}`);
226
- this.handleFlushFailure(batch, {
227
- reason: "status",
228
- status: response.status,
229
- });
230
- throw error;
231
- }
232
- this.retry.reset();
233
- this.debug("flush complete", { count: batch.length });
234
- if (this.options.flushOnCapture && this.queue.length > 0) {
235
- this.scheduleFlush();
236
- }
433
+ }
434
+ close() {
435
+ if (this.timer) {
436
+ clearInterval(this.timer);
437
+ this.timer = void 0;
237
438
  }
238
- handleFlushFailure(batch, detail) {
239
- const result = this.retry.recordFailure();
240
- if (result.justDisabled) {
241
- const droppedQueued = this.queue.length;
242
- const droppedBatch = batch.length;
243
- this.queue = [];
244
- this.stopBackgroundWork();
245
- const message = `[emit-vision${this.options.label ? `:${this.options.label}` : ""}] ` +
246
- `disabled after ${result.consecutiveFailures} consecutive flush failures; ` +
247
- `dropping ${droppedQueued + droppedBatch} queued events. ` +
248
- `Re-init the SDK to resume.`;
249
- if (typeof console.warn === "function") {
250
- console.warn(message);
251
- }
252
- this.debug("disabled", {
253
- consecutiveFailures: result.consecutiveFailures,
254
- dropped: droppedQueued + droppedBatch,
255
- });
256
- return;
257
- }
258
- this.queue.unshift(...batch);
259
- if (detail.reason === "network") {
260
- this.debug("flush network error", {
261
- error: detail.error,
262
- restoredCount: batch.length,
263
- consecutiveFailures: result.consecutiveFailures,
264
- backoffMs: result.backoffMs,
265
- });
266
- }
267
- else {
268
- this.debug("flush failed", {
269
- status: detail.status,
270
- restoredCount: batch.length,
271
- consecutiveFailures: result.consecutiveFailures,
272
- backoffMs: result.backoffMs,
273
- });
274
- }
439
+ }
440
+ async flushChunk(chunk, tailCount) {
441
+ try {
442
+ await executeHttpSend(
443
+ chunk,
444
+ this.options.endpoint,
445
+ this.options.apiKey,
446
+ this.fetchImpl
447
+ );
448
+ } catch (err) {
449
+ const m = err instanceof Error ? /flush failed with (\d+)/.exec(err.message) : null;
450
+ this.handleFlushFailure(
451
+ chunk,
452
+ tailCount,
453
+ m ? { reason: "status", status: Number(m[1]) } : { reason: "network", error: err }
454
+ );
455
+ throw err;
275
456
  }
276
- stopBackgroundWork() {
277
- if (this.timer) {
278
- clearInterval(this.timer);
279
- this.timer = undefined;
280
- }
457
+ this.retry.reset();
458
+ debugLog("flush complete", this.options, { count: chunk.length });
459
+ }
460
+ handleFlushFailure(batch, tailCount, detail) {
461
+ const result = this.retry.recordFailure();
462
+ if (result.justDisabled) {
463
+ const dropped = this.queue.length + batch.length + tailCount;
464
+ this.queue = [];
465
+ this.close();
466
+ const msg = buildFlushDisabledMessage(
467
+ this.options.label,
468
+ result.consecutiveFailures,
469
+ dropped
470
+ );
471
+ if (typeof console.warn === "function") console.warn(msg);
472
+ debugLog("disabled", this.options, {
473
+ consecutiveFailures: result.consecutiveFailures,
474
+ dropped
475
+ });
476
+ return;
281
477
  }
282
- close() {
283
- this.stopBackgroundWork();
284
- if (typeof window !== "undefined" && this.options.autoCapture.errors) {
285
- window.removeEventListener("error", this.handleWindowError);
286
- }
287
- if (typeof window !== "undefined" &&
288
- this.options.autoCapture.unhandledRejections) {
289
- window.removeEventListener("unhandledrejection", this.handleUnhandledRejection);
290
- }
291
- this.debug("closed client");
478
+ this.queue.unshift(...batch);
479
+ const base = {
480
+ restoredCount: batch.length,
481
+ consecutiveFailures: result.consecutiveFailures,
482
+ backoffMs: result.backoffMs
483
+ };
484
+ if (detail.reason === "network") {
485
+ debugLog("flush network error", this.options, {
486
+ error: detail.error,
487
+ ...base
488
+ });
489
+ } else {
490
+ debugLog("flush failed", this.options, {
491
+ status: detail.status,
492
+ ...base
493
+ });
292
494
  }
293
- enqueue(payload) {
294
- if (this.retry.isDisabled()) {
295
- return;
296
- }
297
- this.queue.push(payload);
298
- this.debug("queued payload", {
299
- type: payload.type,
300
- name: payload.name,
301
- queueSize: this.queue.length,
302
- flushOnCapture: this.options.flushOnCapture,
303
- batchSize: this.options.batchSize,
304
- });
305
- if (this.options.flushOnCapture) {
306
- this.debug("capture flush scheduled", {
307
- queueSize: this.queue.length,
308
- });
309
- if (!this.retry.isBackingOff()) {
310
- this.flush().catch((err) => this.debug("capture flush error", { error: err }));
311
- }
312
- return;
313
- }
314
- if (this.queue.length >= this.options.batchSize &&
315
- !this.retry.isBackingOff()) {
316
- this.flush().catch((err) => this.debug("batch flush error", { error: err }));
317
- }
495
+ }
496
+ scheduleFlush() {
497
+ if (this.flushScheduled) return;
498
+ this.flushScheduled = true;
499
+ queueMicrotask(() => {
500
+ this.flushScheduled = false;
501
+ if (this.queue.length === 0 || this.retry.isBackingOff()) return;
502
+ this.flush().catch(
503
+ (err) => debugLog("scheduled flush error", this.options, { error: err })
504
+ );
505
+ });
506
+ }
507
+ };
508
+
509
+ // src/client-autocapture.ts
510
+ var BrowserAutoCapture = class {
511
+ constructor(onCapture, withDefaults, onPageView, config) {
512
+ this.onCapture = onCapture;
513
+ this.withDefaults = withDefaults;
514
+ this.onPageView = onPageView;
515
+ this.config = config;
516
+ }
517
+ onCapture;
518
+ withDefaults;
519
+ onPageView;
520
+ config;
521
+ attach() {
522
+ if (typeof window === "undefined") return;
523
+ if (this.config.errors) {
524
+ window.addEventListener("error", this.handleWindowError);
318
525
  }
319
- scheduleFlush() {
320
- if (this.flushScheduled) {
321
- return;
322
- }
323
- this.flushScheduled = true;
324
- queueMicrotask(() => {
325
- this.flushScheduled = false;
326
- if (this.queue.length === 0 || this.retry.isBackingOff()) {
327
- return;
328
- }
329
- this.flush().catch((err) => this.debug("scheduled flush error", { error: err }));
330
- });
526
+ if (this.config.unhandledRejections) {
527
+ window.addEventListener(
528
+ "unhandledrejection",
529
+ this.handleUnhandledRejection
530
+ );
331
531
  }
332
- withDefaults(options) {
333
- const { context, deployment, featureFlags, tags, user, environment, release, sessionId, ...rest } = options;
334
- return {
335
- ...rest,
336
- environment: environment ?? this.options.environment,
337
- release: release ?? this.options.release,
338
- sessionId: sessionId ?? this.options.sessionId,
339
- user: user ?? this.user,
340
- context: Object.keys(this.context).length
341
- ? { ...this.context, ...context }
342
- : context,
343
- deployment: this.mergeDeployment(deployment),
344
- featureFlags: this.mergeFeatureFlags(featureFlags),
345
- tags: Object.keys(this.tags).length ? { ...this.tags, ...tags } : tags,
346
- };
532
+ if (this.config.pageViews) {
533
+ this.onPageView();
534
+ window.addEventListener("popstate", this.handlePopState);
535
+ this.patchHistoryMethod("pushState");
536
+ this.patchHistoryMethod("replaceState");
347
537
  }
348
- mergeDeployment(deployment) {
349
- const merged = {
350
- ...(this.deployment ?? {}),
351
- ...(deployment ?? {}),
352
- };
353
- return Object.keys(merged).length > 0 ? merged : undefined;
538
+ }
539
+ detach() {
540
+ if (typeof window === "undefined") return;
541
+ if (this.config.errors) {
542
+ window.removeEventListener("error", this.handleWindowError);
354
543
  }
355
- mergeFeatureFlags(featureFlags) {
356
- const merged = {
357
- ...this.featureFlags,
358
- ...(featureFlags ?? {}),
359
- };
360
- return Object.keys(merged).length > 0 ? merged : undefined;
544
+ if (this.config.unhandledRejections) {
545
+ window.removeEventListener(
546
+ "unhandledrejection",
547
+ this.handleUnhandledRejection
548
+ );
361
549
  }
362
- handleWindowError = (event) => {
363
- const serialized = serializeError(event.error ?? event.message);
364
- this.debug("auto-captured window error", {
365
- message: serialized.message,
366
- });
367
- this.enqueue({
368
- type: "error",
369
- name: "error",
370
- error: { ...serialized, handled: false },
371
- ...this.withDefaults({ context: { source: "window.error" } }),
372
- });
373
- };
374
- handleUnhandledRejection = (event) => {
375
- const serialized = serializeError(event.reason);
376
- this.debug("auto-captured rejection", {
377
- message: serialized.message,
378
- });
379
- this.enqueue({
380
- type: "error",
381
- name: "error",
382
- error: { ...serialized, handled: false },
383
- ...this.withDefaults({
384
- context: { source: "window.unhandledrejection" },
385
- }),
386
- });
387
- };
388
- debug(message, payload) {
389
- if (!this.options.debug || typeof console.debug !== "function") {
390
- return;
550
+ if (this.config.pageViews) {
551
+ window.removeEventListener("popstate", this.handlePopState);
552
+ this.restoreHistoryMethod("pushState");
553
+ this.restoreHistoryMethod("replaceState");
554
+ }
555
+ }
556
+ handleWindowError = (event) => {
557
+ const serialized = serializeError(event.error ?? event.message);
558
+ this.onCapture({
559
+ type: "error",
560
+ name: "error",
561
+ error: { ...serialized, handled: false },
562
+ ...this.withDefaults({
563
+ context: {
564
+ source: "window.error",
565
+ url: window.location.href,
566
+ page: window.location.pathname
391
567
  }
392
- const prefix = this.options.label
393
- ? `[emit-vision:${this.options.label}]`
394
- : "[emit-vision]";
395
- if (payload === undefined) {
396
- console.debug(prefix, message);
397
- return;
568
+ })
569
+ });
570
+ };
571
+ handlePopState = () => {
572
+ this.onPageView();
573
+ };
574
+ handleUnhandledRejection = (event) => {
575
+ const serialized = serializeError(event.reason);
576
+ this.onCapture({
577
+ type: "error",
578
+ name: "error",
579
+ error: { ...serialized, handled: false },
580
+ ...this.withDefaults({
581
+ context: {
582
+ source: "window.unhandledrejection",
583
+ url: window.location.href,
584
+ page: window.location.pathname
398
585
  }
399
- console.debug(prefix, message, payload);
586
+ })
587
+ });
588
+ };
589
+ patchHistoryMethod(method) {
590
+ const original = history[method].bind(history);
591
+ const historyWithCustom = history;
592
+ historyWithCustom[`__ev_orig_${method}`] = original;
593
+ historyWithCustom[method] = (...args) => {
594
+ original(...args);
595
+ this.onPageView();
596
+ };
597
+ }
598
+ restoreHistoryMethod(method) {
599
+ const historyWithCustom = history;
600
+ const original = historyWithCustom[`__ev_orig_${method}`];
601
+ if (original) {
602
+ historyWithCustom[method] = original;
400
603
  }
401
- }
402
- let client;
403
- export function init(options) {
404
- const transport = resolveTransport(options);
405
- const autoCapture = resolveAutoCapture(options);
406
- client?.close();
407
- client = new EmitVisionClient({
408
- ...options,
409
- flushIntervalMs: options.flushIntervalMs ?? 5000,
410
- batchSize: options.batchSize ?? 20,
411
- debug: options.debug ?? false,
412
- fetchImpl: options.fetchImpl ?? globalThis.fetch.bind(globalThis),
413
- flushOnCapture: options.flushOnCapture ?? false,
414
- flagEvalTtlMs: options.flagEvalTtlMs ?? 60_000,
415
- retryBackoffInitialMs: options.retryBackoffInitialMs ?? 1_000,
416
- retryBackoffMaxMs: options.retryBackoffMaxMs ?? 30_000,
417
- maxConsecutiveFlushFailures: options.maxConsecutiveFlushFailures ?? 20,
418
- apiKey: transport.apiKey,
419
- endpoint: transport.endpoint ?? options.endpoint ?? "http://localhost:4301",
420
- autoCapture,
604
+ }
605
+ };
606
+
607
+ // src/client.ts
608
+ var EmitVisionClient = class {
609
+ constructor(options) {
610
+ this.options = options;
611
+ this.deployment = options.deployment ? { ...options.deployment } : void 0;
612
+ this.featureFlags = options.featureFlags ? { ...options.featureFlags } : {};
613
+ this.flusher = new FlushManager(options, options.fetchImpl);
614
+ this.flagEval = new FlagEvaluator(
615
+ {
616
+ endpoint: options.endpoint,
617
+ apiKey: options.apiKey,
618
+ ttlMs: options.flagEvalTtlMs,
619
+ flagExposures: options.autoCapture.flagExposures,
620
+ fetchImpl: options.fetchImpl
621
+ },
622
+ (flags) => {
623
+ this.featureFlags = { ...this.featureFlags, ...flags };
624
+ },
625
+ (flagKey, variantKey, variantValue, reason, env) => this.captureExposure(flagKey, variantKey, variantValue, reason, env),
626
+ (msg, data) => debugLog(msg, this.options, data)
627
+ );
628
+ this.autoCapture = new BrowserAutoCapture(
629
+ (payload) => this.flusher.enqueue(payload),
630
+ (opts) => this.withDefaults(opts),
631
+ () => this.capturePageView(),
632
+ options.autoCapture
633
+ );
634
+ this.autoCapture.attach();
635
+ debugLog("initialized", this.options, {
636
+ endpoint: options.endpoint,
637
+ environment: options.environment,
638
+ release: options.release,
639
+ autoCapture: options.autoCapture,
640
+ batchSize: options.batchSize,
641
+ flushIntervalMs: options.flushIntervalMs,
642
+ flushOnCapture: options.flushOnCapture
421
643
  });
422
- return client;
423
- }
424
- export function captureEvent(name, properties, options) {
425
- requireClient().captureEvent(name, properties, options);
426
- }
427
- export function captureError(error, options) {
428
- requireClient().captureError(error, options);
429
- }
430
- export function identify(userIdOrUser, traits) {
431
- requireClient().identify(userIdOrUser, traits);
432
- }
433
- export function setContext(context) {
434
- requireClient().setContext(context);
644
+ }
645
+ options;
646
+ user;
647
+ context = {};
648
+ deployment;
649
+ featureFlags = {};
650
+ tags = {};
651
+ flagEval;
652
+ autoCapture;
653
+ flusher;
654
+ captureEvent(name, properties, options = {}) {
655
+ debugLog("queue event", this.options, { name, properties, options });
656
+ this.flusher.enqueue({
657
+ type: "event",
658
+ name,
659
+ properties,
660
+ ...this.withDefaults(options)
661
+ });
662
+ }
663
+ captureError(error, options = {}) {
664
+ const serialized = serializeError(error);
665
+ debugLog("queue error", this.options, {
666
+ message: serialized.message,
667
+ options
668
+ });
669
+ this.flusher.enqueue({
670
+ type: "error",
671
+ name: "error",
672
+ error: serialized,
673
+ ...this.withDefaults(options)
674
+ });
675
+ }
676
+ identify(userIdOrUser, traits = {}) {
677
+ this.user = typeof userIdOrUser === "string" ? { id: userIdOrUser, ...traits } : { ...userIdOrUser };
678
+ debugLog("identified user", this.options, this.user);
679
+ }
680
+ setContext(context) {
681
+ this.context = { ...this.context, ...context };
682
+ debugLog("updated context", this.options, this.context);
683
+ }
684
+ setDeploymentContext(context) {
685
+ this.deployment = Object.keys(context).length > 0 ? { ...context } : void 0;
686
+ debugLog("updated deployment context", this.options, this.deployment);
687
+ }
688
+ setFeatureFlags(featureFlags) {
689
+ this.featureFlags = { ...featureFlags };
690
+ debugLog("updated feature flags", this.options, this.featureFlags);
691
+ }
692
+ evaluateFlags(opts) {
693
+ return this.flagEval.evaluateFlags({
694
+ ...opts,
695
+ environment: opts.environment ?? this.options.environment
696
+ });
697
+ }
698
+ getFlag(flagKey, fallback, opts) {
699
+ return this.flagEval.getFlag(flagKey, fallback, {
700
+ ...opts,
701
+ environment: opts.environment ?? this.options.environment
702
+ });
703
+ }
704
+ refreshFlags(opts) {
705
+ return this.flagEval.refreshFlags({
706
+ ...opts,
707
+ environment: opts.environment ?? this.options.environment
708
+ });
709
+ }
710
+ capturePageView(path) {
711
+ const page = path ?? (typeof window !== "undefined" ? window.location.pathname : void 0);
712
+ if (!page) return;
713
+ this.captureEvent("$page_view", { page });
714
+ }
715
+ captureExposure(flagKey, variantKey, variantValue, reason, environment) {
716
+ this.captureEvent("$flag_exposure", {
717
+ flagKey,
718
+ variantKey,
719
+ ...variantValue !== void 0 ? { variantValue } : {},
720
+ ...reason ? { reason } : {},
721
+ environment: environment ?? this.options.environment
722
+ });
723
+ }
724
+ setTags(tags) {
725
+ this.tags = { ...this.tags, ...tags };
726
+ debugLog("updated tags", this.options, this.tags);
727
+ }
728
+ flush() {
729
+ return this.flusher.flush();
730
+ }
731
+ close() {
732
+ this.flusher.close();
733
+ this.autoCapture.detach();
734
+ debugLog("closed client", this.options);
735
+ }
736
+ withDefaults(opts) {
737
+ return buildWithDefaults(
738
+ this.user,
739
+ this.context,
740
+ this.deployment,
741
+ this.featureFlags,
742
+ this.tags,
743
+ this.options,
744
+ opts
745
+ );
746
+ }
747
+ };
748
+
749
+ // src/index.ts
750
+ var client;
751
+ function init(options) {
752
+ const transport = resolveTransport(options);
753
+ const autoCapture = resolveAutoCapture(options);
754
+ const resolved = {
755
+ ...options,
756
+ flushIntervalMs: options.flushIntervalMs ?? 5e3,
757
+ batchSize: options.batchSize ?? 20,
758
+ debug: options.debug ?? false,
759
+ fetchImpl: options.fetchImpl ?? globalThis.fetch.bind(globalThis),
760
+ flushOnCapture: options.flushOnCapture ?? false,
761
+ flagEvalTtlMs: options.flagEvalTtlMs ?? 6e4,
762
+ retryBackoffInitialMs: options.retryBackoffInitialMs ?? 1e3,
763
+ retryBackoffMaxMs: options.retryBackoffMaxMs ?? 3e4,
764
+ maxConsecutiveFlushFailures: options.maxConsecutiveFlushFailures ?? 20,
765
+ apiKey: transport.apiKey,
766
+ endpoint: transport.endpoint ?? options.endpoint ?? "http://localhost:4301",
767
+ autoCapture,
768
+ sessionId: options.sessionId ?? getOrCreateAnonymousId()
769
+ };
770
+ client?.close();
771
+ client = new EmitVisionClient(resolved);
772
+ return client;
435
773
  }
436
- export function setDeploymentContext(context) {
437
- requireClient().setDeploymentContext(context);
774
+ function captureEvent(name, properties, options) {
775
+ requireClient().captureEvent(name, properties, options);
438
776
  }
439
- export function setFeatureFlags(featureFlags) {
440
- requireClient().setFeatureFlags(featureFlags);
777
+ function captureError(error, options) {
778
+ requireClient().captureError(error, options);
441
779
  }
442
- export function setTags(tags) {
443
- requireClient().setTags(tags);
780
+ function capturePageView(path) {
781
+ requireClient().capturePageView(path);
444
782
  }
445
- export async function flush() {
446
- await requireClient().flush();
783
+ function identify(userIdOrUser, traits) {
784
+ requireClient().identify(userIdOrUser, traits);
447
785
  }
448
- export async function evaluateFlags(options) {
449
- return requireClient().evaluateFlags(options);
786
+ function setContext(context) {
787
+ requireClient().setContext(context);
450
788
  }
451
- export async function getFlag(flagKey, fallback, options) {
452
- return requireClient().getFlag(flagKey, fallback, options);
789
+ function setDeploymentContext(context) {
790
+ requireClient().setDeploymentContext(context);
453
791
  }
454
- export async function refreshFlags(options) {
455
- return requireClient().refreshFlags(options);
792
+ function setFeatureFlags(featureFlags) {
793
+ requireClient().setFeatureFlags(featureFlags);
456
794
  }
457
- export function captureExposure(flagKey, variantKey, variantValue, reason, environment) {
458
- requireClient().captureExposure(flagKey, variantKey, variantValue, reason, environment);
795
+ function setTags(tags) {
796
+ requireClient().setTags(tags);
459
797
  }
460
- function requireClient() {
461
- if (!client) {
462
- throw new Error("emit-vision SDK has not been initialized");
463
- }
464
- return client;
798
+ async function flush() {
799
+ await requireClient().flush();
465
800
  }
466
- function resolveTransport(options) {
467
- const dsn = options.dsn ? parseDsn(options.dsn) : null;
468
- const apiKey = options.apiKey ?? dsn?.apiKey;
469
- if (!apiKey) {
470
- throw new Error("emit-vision init requires either apiKey or dsn");
471
- }
472
- return {
473
- apiKey,
474
- endpoint: stripTrailingSlash(options.endpoint ?? dsn?.endpoint),
475
- };
801
+ async function evaluateFlags(options) {
802
+ return requireClient().evaluateFlags(options);
476
803
  }
477
- function resolveAutoCapture(options) {
478
- const legacyDefault = options.autoCaptureErrors === undefined ? true : options.autoCaptureErrors;
479
- return {
480
- errors: options.autoCapture?.errors ?? legacyDefault,
481
- unhandledRejections: options.autoCapture?.unhandledRejections ?? legacyDefault,
482
- flagExposures: options.autoCapture?.flagExposures ?? true,
483
- };
804
+ async function getFlag(flagKey, fallback, options) {
805
+ return requireClient().getFlag(flagKey, fallback, options);
484
806
  }
485
- function parseDsn(dsn) {
486
- const parsed = new URL(dsn);
487
- const apiKey = decodeURIComponent(parsed.username);
488
- if (!apiKey) {
489
- throw new Error("emit-vision dsn must include the API key before the @");
490
- }
491
- const pathname = stripTrailingSlash(parsed.pathname) ?? "";
492
- const endpointPath = pathname.endsWith("/v1")
493
- ? pathname.slice(0, -"/v1".length)
494
- : pathname;
495
- return {
496
- apiKey,
497
- endpoint: stripTrailingSlash(`${parsed.origin}${endpointPath}`),
498
- };
807
+ async function refreshFlags(options) {
808
+ return requireClient().refreshFlags(options);
499
809
  }
500
- function stripTrailingSlash(value) {
501
- if (!value) {
502
- return value;
503
- }
504
- return value.endsWith("/") ? value.slice(0, -1) : value;
810
+ function captureExposure(flagKey, variantKey, variantValue, reason, environment) {
811
+ requireClient().captureExposure(
812
+ flagKey,
813
+ variantKey,
814
+ variantValue,
815
+ reason,
816
+ environment
817
+ );
505
818
  }
506
- function serializeError(error) {
507
- if (error instanceof Error) {
508
- return {
509
- message: error.message,
510
- name: error.name,
511
- stack: error.stack,
512
- handled: true,
513
- };
514
- }
515
- return {
516
- message: typeof error === "string" ? error : JSON.stringify(error),
517
- handled: true,
518
- };
819
+ function requireClient() {
820
+ if (!client) {
821
+ throw new Error("emit-vision SDK has not been initialized");
822
+ }
823
+ return client;
519
824
  }
825
+ export {
826
+ captureError,
827
+ captureEvent,
828
+ captureExposure,
829
+ capturePageView,
830
+ evaluateFlags,
831
+ flush,
832
+ getFlag,
833
+ identify,
834
+ init,
835
+ refreshFlags,
836
+ setContext,
837
+ setDeploymentContext,
838
+ setFeatureFlags,
839
+ setTags
840
+ };