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