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