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