@emit-vision/sdk-js 0.4.1 → 0.4.3
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/README.md +4 -0
- package/dist/index.d.ts +67 -1
- package/dist/index.js +251 -11
- package/package.json +16 -3
package/README.md
CHANGED
|
@@ -218,3 +218,7 @@ init({
|
|
|
218
218
|
- Do not use `email` or `username` as the `evaluationKey` — use an opaque user ID or session ID.
|
|
219
219
|
- Use the helper packages when they reduce boilerplate, but keep direct browser
|
|
220
220
|
SDK usage for the simplest client-side apps.
|
|
221
|
+
|
|
222
|
+
## License
|
|
223
|
+
|
|
224
|
+
MIT — see [LICENSE](./LICENSE).
|
package/dist/index.d.ts
CHANGED
|
@@ -38,6 +38,7 @@ type CaptureOptions = {
|
|
|
38
38
|
release?: string;
|
|
39
39
|
sessionId?: string;
|
|
40
40
|
user?: EmitVisionUser;
|
|
41
|
+
groupId?: string;
|
|
41
42
|
context?: Record<string, unknown>;
|
|
42
43
|
deployment?: DeploymentContext;
|
|
43
44
|
featureFlags?: FeatureFlagsContext;
|
|
@@ -60,6 +61,10 @@ type EmitVisionOptions = {
|
|
|
60
61
|
flushIntervalMs?: number;
|
|
61
62
|
batchSize?: number;
|
|
62
63
|
fetchImpl?: typeof fetch;
|
|
64
|
+
/** Project ID — passed through to the DevTools debug channel so the extension can build dashboard deep links. Not required for event capture. */
|
|
65
|
+
projectId?: string;
|
|
66
|
+
/** Initial group ID to associate with all events. Can be overridden per-event or updated via `group()`. */
|
|
67
|
+
groupId?: string;
|
|
63
68
|
/** TTL in milliseconds for in-memory flag evaluation cache. Default: 60 000 (1 minute). */
|
|
64
69
|
flagEvalTtlMs?: number;
|
|
65
70
|
/** Initial delay (ms) before retrying after a flush failure. Default: 1000. Doubles each consecutive failure up to {@link retryBackoffMaxMs}. Set to 0 to retry immediately on every tick. */
|
|
@@ -68,17 +73,72 @@ type EmitVisionOptions = {
|
|
|
68
73
|
retryBackoffMaxMs?: number;
|
|
69
74
|
/** Disable the SDK after this many consecutive flush failures: the queue is dropped, future captures become no-ops, and a single warning is logged. Default: 20. Set to 0 to disable the cap and retry forever. */
|
|
70
75
|
maxConsecutiveFlushFailures?: number;
|
|
76
|
+
/** When true, events are queued in memory but not sent until `consent()` is called. Default: false. */
|
|
77
|
+
consentRequired?: boolean;
|
|
78
|
+
/** Set to true when `consentRequired` is true but consent is already known at init time (e.g. read from a cookie server-side). Skips the consent gate. Default: false. */
|
|
79
|
+
consentGiven?: boolean;
|
|
71
80
|
};
|
|
72
81
|
type RequiredEmitVisionOptions = Required<Pick<EmitVisionOptions, "apiKey" | "endpoint" | "flushIntervalMs" | "batchSize" | "fetchImpl" | "flushOnCapture" | "flagEvalTtlMs" | "retryBackoffInitialMs" | "retryBackoffMaxMs" | "maxConsecutiveFlushFailures">> & Pick<EmitVisionOptions, "environment" | "release" | "sessionId" | "label" | "deployment" | "featureFlags" | "debug"> & {
|
|
73
82
|
autoCapture: Required<AutoCaptureOptions> & {
|
|
74
83
|
flagExposures: boolean;
|
|
75
84
|
pageViews: boolean;
|
|
76
85
|
};
|
|
86
|
+
projectId?: string;
|
|
87
|
+
groupId?: string;
|
|
88
|
+
consentRequired?: boolean;
|
|
89
|
+
consentGiven?: boolean;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
type DevToolsMessage = {
|
|
93
|
+
type: "init";
|
|
94
|
+
payload: {
|
|
95
|
+
sdkVersion: string;
|
|
96
|
+
endpoint: string;
|
|
97
|
+
environment?: string;
|
|
98
|
+
release?: string;
|
|
99
|
+
sessionId: string;
|
|
100
|
+
autoCapture: AutoCaptureOptions;
|
|
101
|
+
projectId?: string;
|
|
102
|
+
consentRequired?: boolean;
|
|
103
|
+
};
|
|
104
|
+
} | {
|
|
105
|
+
type: "event";
|
|
106
|
+
payload: {
|
|
107
|
+
name: string;
|
|
108
|
+
properties?: Record<string, unknown>;
|
|
109
|
+
eventId?: string;
|
|
110
|
+
timestamp?: string;
|
|
111
|
+
environment?: string;
|
|
112
|
+
sessionId?: string;
|
|
113
|
+
};
|
|
114
|
+
} | {
|
|
115
|
+
type: "identify";
|
|
116
|
+
payload: {
|
|
117
|
+
userId?: string;
|
|
118
|
+
traits?: Record<string, unknown>;
|
|
119
|
+
};
|
|
120
|
+
} | {
|
|
121
|
+
type: "flush";
|
|
122
|
+
payload: {
|
|
123
|
+
count: number;
|
|
124
|
+
};
|
|
125
|
+
} | {
|
|
126
|
+
type: "error";
|
|
127
|
+
payload: {
|
|
128
|
+
message: string;
|
|
129
|
+
};
|
|
130
|
+
} | {
|
|
131
|
+
type: "group";
|
|
132
|
+
payload: {
|
|
133
|
+
groupId: string;
|
|
134
|
+
traits?: Record<string, unknown>;
|
|
135
|
+
};
|
|
77
136
|
};
|
|
78
137
|
|
|
79
138
|
declare class EmitVisionClient {
|
|
80
139
|
private readonly options;
|
|
81
140
|
private user;
|
|
141
|
+
private groupId;
|
|
82
142
|
private context;
|
|
83
143
|
private deployment;
|
|
84
144
|
private featureFlags;
|
|
@@ -86,10 +146,13 @@ declare class EmitVisionClient {
|
|
|
86
146
|
private readonly flagEval;
|
|
87
147
|
private readonly autoCapture;
|
|
88
148
|
private readonly flusher;
|
|
149
|
+
private readonly persistOnHide;
|
|
150
|
+
private readonly persistOnHidden;
|
|
89
151
|
constructor(options: RequiredEmitVisionOptions);
|
|
90
152
|
captureEvent(name: string, properties?: Record<string, unknown>, options?: CaptureOptions): void;
|
|
91
153
|
captureError(error: unknown, options?: CaptureOptions): void;
|
|
92
154
|
identify(userIdOrUser: string | EmitVisionUser, traits?: EmitVisionTraits): void;
|
|
155
|
+
group(groupId: string, traits?: Record<string, unknown>): void;
|
|
93
156
|
setContext(context: Record<string, unknown>): void;
|
|
94
157
|
setDeploymentContext(context: DeploymentContext): void;
|
|
95
158
|
setFeatureFlags(featureFlags: FeatureFlagsContext): void;
|
|
@@ -104,6 +167,7 @@ declare class EmitVisionClient {
|
|
|
104
167
|
}, options?: CaptureOptions): void;
|
|
105
168
|
captureExposure(flagKey: string, variantKey: string, variantValue?: string | number | boolean | null, reason?: string, environment?: string): void;
|
|
106
169
|
setTags(tags: Record<string, string>): void;
|
|
170
|
+
consent(): Promise<void>;
|
|
107
171
|
flush(): Promise<void>;
|
|
108
172
|
close(): void;
|
|
109
173
|
private withDefaults;
|
|
@@ -125,9 +189,11 @@ declare function setDeploymentContext(context: DeploymentContext): void;
|
|
|
125
189
|
declare function setFeatureFlags(featureFlags: FeatureFlagsContext): void;
|
|
126
190
|
declare function setTags(tags: Record<string, string>): void;
|
|
127
191
|
declare function flush(): Promise<void>;
|
|
192
|
+
declare function consent(): Promise<void>;
|
|
193
|
+
declare function group(groupId: string, traits?: Record<string, unknown>): void;
|
|
128
194
|
declare function evaluateFlags(options: FlagEvaluationOptions): Promise<FeatureFlagsContext>;
|
|
129
195
|
declare function getFlag<T extends string | number | boolean | null>(flagKey: string, fallback: T, options: FlagEvaluationOptions): Promise<T>;
|
|
130
196
|
declare function refreshFlags(options: FlagEvaluationOptions): Promise<FeatureFlagsContext>;
|
|
131
197
|
declare function captureExposure(flagKey: string, variantKey: string, variantValue?: string | number | boolean | null, reason?: string, environment?: string): void;
|
|
132
198
|
|
|
133
|
-
export { type AutoCaptureOptions, type CaptureOptions, type DeploymentContext, type EmitVisionOptions, type EmitVisionTraits, type EmitVisionUser, type FeatureFlagsContext, type FlagEvaluationOptions, captureError, captureEvent, captureExposure, captureFeedback, capturePageView, evaluateFlags, flush, getFlag, identify, init, refreshFlags, setContext, setDeploymentContext, setFeatureFlags, setTags };
|
|
199
|
+
export { type AutoCaptureOptions, type CaptureOptions, type DeploymentContext, type DevToolsMessage, type EmitVisionOptions, type EmitVisionTraits, type EmitVisionUser, type FeatureFlagsContext, type FlagEvaluationOptions, captureError, captureEvent, captureExposure, captureFeedback, capturePageView, consent, evaluateFlags, flush, getFlag, group, identify, init, refreshFlags, setContext, setDeploymentContext, setFeatureFlags, setTags };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
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
|
+
}
|
|
2
6
|
var FlagEvaluator = class {
|
|
3
7
|
constructor(cfg, onFlagsLoaded, captureExposure2, debug) {
|
|
4
8
|
this.cfg = cfg;
|
|
@@ -14,7 +18,7 @@ var FlagEvaluator = class {
|
|
|
14
18
|
async evaluateFlags(opts) {
|
|
15
19
|
const env = opts.environment ?? "";
|
|
16
20
|
const ttl = opts.ttlMs ?? this.cfg.ttlMs;
|
|
17
|
-
const key =
|
|
21
|
+
const key = cacheKeyFor(env, opts);
|
|
18
22
|
const cached = this.cache.get(key);
|
|
19
23
|
if (cached && Date.now() < cached.expiresAt) {
|
|
20
24
|
this.debug("flag eval cache hit", { key });
|
|
@@ -24,7 +28,9 @@ var FlagEvaluator = class {
|
|
|
24
28
|
}
|
|
25
29
|
async getFlag(flagKey, fallback, opts) {
|
|
26
30
|
try {
|
|
27
|
-
const flags = await this.evaluateFlags(
|
|
31
|
+
const flags = await this.evaluateFlags(
|
|
32
|
+
opts.flagKeys?.length ? opts : { ...opts, flagKeys: [flagKey] }
|
|
33
|
+
);
|
|
28
34
|
const value = flags[flagKey];
|
|
29
35
|
return value !== void 0 ? value : fallback;
|
|
30
36
|
} catch {
|
|
@@ -34,7 +40,7 @@ var FlagEvaluator = class {
|
|
|
34
40
|
async refreshFlags(opts) {
|
|
35
41
|
const env = opts.environment ?? "";
|
|
36
42
|
const ttl = opts.ttlMs ?? this.cfg.ttlMs;
|
|
37
|
-
const key =
|
|
43
|
+
const key = cacheKeyFor(env, opts);
|
|
38
44
|
this.cache.delete(key);
|
|
39
45
|
return this.fetchAndCacheFlags(opts, env, ttl, key);
|
|
40
46
|
}
|
|
@@ -157,6 +163,15 @@ function resolveAutoCapture(options) {
|
|
|
157
163
|
};
|
|
158
164
|
}
|
|
159
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
|
+
|
|
160
175
|
// src/client-helpers.ts
|
|
161
176
|
var SDK_NAME = "emit-vision-js";
|
|
162
177
|
var SDK_VERSION = "0.4.0";
|
|
@@ -276,15 +291,51 @@ function mergeFeatureFlagsFn(clientFlags, overrideFlags) {
|
|
|
276
291
|
function buildFlushDisabledMessage(label, consecutiveFailures, dropped) {
|
|
277
292
|
return `[emit-vision${label ? `:${label}` : ""}] disabled after ${consecutiveFailures} consecutive flush failures; dropping ${dropped} queued events. Re-init the SDK to resume.`;
|
|
278
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
|
+
};
|
|
279
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;
|
|
280
312
|
const response = await fetchImpl(`${endpoint}/v1/batch`, {
|
|
281
313
|
method: "POST",
|
|
282
|
-
headers: {
|
|
283
|
-
|
|
284
|
-
|
|
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 }
|
|
285
325
|
});
|
|
286
326
|
if (!response.ok) {
|
|
287
|
-
|
|
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
|
+
);
|
|
288
339
|
}
|
|
289
340
|
}
|
|
290
341
|
|
|
@@ -307,10 +358,20 @@ var FlushRetryController = class {
|
|
|
307
358
|
if (this.disabled) return Infinity;
|
|
308
359
|
return Math.max(0, this.backoffUntil - now);
|
|
309
360
|
}
|
|
361
|
+
forceDisable() {
|
|
362
|
+
this.disabled = true;
|
|
363
|
+
this.backoffUntil = 0;
|
|
364
|
+
}
|
|
310
365
|
reset() {
|
|
311
366
|
this.consecutiveFailures = 0;
|
|
312
367
|
this.backoffUntil = 0;
|
|
313
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
|
+
}
|
|
314
375
|
recordFailure(now = Date.now()) {
|
|
315
376
|
this.consecutiveFailures += 1;
|
|
316
377
|
if (this.opts.failureCap > 0 && this.consecutiveFailures >= this.opts.failureCap) {
|
|
@@ -346,11 +407,77 @@ var FlushRetryController = class {
|
|
|
346
407
|
}
|
|
347
408
|
};
|
|
348
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
|
+
|
|
349
474
|
// src/client-flush.ts
|
|
350
475
|
var FlushManager = class {
|
|
351
476
|
constructor(options, fetchImpl) {
|
|
352
477
|
this.options = options;
|
|
353
478
|
this.fetchImpl = fetchImpl;
|
|
479
|
+
this.consentGiven = !options.consentRequired || (options.consentGiven ?? false);
|
|
480
|
+
this.storageKey = persistenceKey(options.apiKey, options.projectId);
|
|
354
481
|
this.retry = new FlushRetryController({
|
|
355
482
|
initialMs: options.retryBackoffInitialMs,
|
|
356
483
|
maxMs: options.retryBackoffMaxMs,
|
|
@@ -369,6 +496,9 @@ var FlushManager = class {
|
|
|
369
496
|
timer;
|
|
370
497
|
flushScheduled = false;
|
|
371
498
|
retry;
|
|
499
|
+
storageKey;
|
|
500
|
+
consentGiven;
|
|
501
|
+
authFailureCount = 0;
|
|
372
502
|
enqueue(payload) {
|
|
373
503
|
if (this.retry.isDisabled()) return;
|
|
374
504
|
const {
|
|
@@ -407,8 +537,27 @@ var FlushManager = class {
|
|
|
407
537
|
);
|
|
408
538
|
}
|
|
409
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
|
+
}
|
|
410
558
|
async flush() {
|
|
411
559
|
if (this.retry.isDisabled()) return;
|
|
560
|
+
if (!this.consentGiven) return;
|
|
412
561
|
const batch = this.queue.splice(0, this.options.batchSize);
|
|
413
562
|
if (batch.length === 0) return;
|
|
414
563
|
debugLog("flushing batch", this.options, { count: batch.length });
|
|
@@ -446,19 +595,50 @@ var FlushManager = class {
|
|
|
446
595
|
this.fetchImpl
|
|
447
596
|
);
|
|
448
597
|
} catch (err) {
|
|
449
|
-
const m = err instanceof Error ? /flush failed with (\d+)/.exec(err.message) : null;
|
|
450
598
|
this.handleFlushFailure(
|
|
451
599
|
chunk,
|
|
452
600
|
tailCount,
|
|
453
|
-
|
|
601
|
+
err instanceof FlushHttpError ? {
|
|
602
|
+
reason: "status",
|
|
603
|
+
status: err.status,
|
|
604
|
+
retryAfterSeconds: err.retryAfterSeconds,
|
|
605
|
+
serverMessage: err.serverMessage
|
|
606
|
+
} : { reason: "network", error: err }
|
|
454
607
|
);
|
|
455
608
|
throw err;
|
|
456
609
|
}
|
|
457
610
|
this.retry.reset();
|
|
611
|
+
this.authFailureCount = 0;
|
|
612
|
+
clearPersistedQueue(this.storageKey);
|
|
458
613
|
debugLog("flush complete", this.options, { count: chunk.length });
|
|
614
|
+
dispatchDevToolsEvent("flush", { count: chunk.length });
|
|
459
615
|
}
|
|
460
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
|
+
}
|
|
461
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
|
+
}
|
|
462
642
|
if (result.justDisabled) {
|
|
463
643
|
const dropped = this.queue.length + batch.length + tailCount;
|
|
464
644
|
this.queue = [];
|
|
@@ -476,6 +656,7 @@ var FlushManager = class {
|
|
|
476
656
|
return;
|
|
477
657
|
}
|
|
478
658
|
this.queue.unshift(...batch);
|
|
659
|
+
this.persistNow();
|
|
479
660
|
const base = {
|
|
480
661
|
restoredCount: batch.length,
|
|
481
662
|
consecutiveFailures: result.consecutiveFailures,
|
|
@@ -489,6 +670,7 @@ var FlushManager = class {
|
|
|
489
670
|
} else {
|
|
490
671
|
debugLog("flush failed", this.options, {
|
|
491
672
|
status: detail.status,
|
|
673
|
+
serverMessage: detail.serverMessage,
|
|
492
674
|
...base
|
|
493
675
|
});
|
|
494
676
|
}
|
|
@@ -608,9 +790,17 @@ var BrowserAutoCapture = class {
|
|
|
608
790
|
var EmitVisionClient = class {
|
|
609
791
|
constructor(options) {
|
|
610
792
|
this.options = options;
|
|
793
|
+
this.groupId = options.groupId;
|
|
611
794
|
this.deployment = options.deployment ? { ...options.deployment } : void 0;
|
|
612
795
|
this.featureFlags = options.featureFlags ? { ...options.featureFlags } : {};
|
|
613
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
|
+
}
|
|
614
804
|
this.flagEval = new FlagEvaluator(
|
|
615
805
|
{
|
|
616
806
|
endpoint: options.endpoint,
|
|
@@ -641,9 +831,20 @@ var EmitVisionClient = class {
|
|
|
641
831
|
flushIntervalMs: options.flushIntervalMs,
|
|
642
832
|
flushOnCapture: options.flushOnCapture
|
|
643
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
|
+
});
|
|
644
844
|
}
|
|
645
845
|
options;
|
|
646
846
|
user;
|
|
847
|
+
groupId;
|
|
647
848
|
context = {};
|
|
648
849
|
deployment;
|
|
649
850
|
featureFlags = {};
|
|
@@ -651,13 +852,28 @@ var EmitVisionClient = class {
|
|
|
651
852
|
flagEval;
|
|
652
853
|
autoCapture;
|
|
653
854
|
flusher;
|
|
855
|
+
persistOnHide = () => this.flusher.persistNow();
|
|
856
|
+
persistOnHidden = () => {
|
|
857
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
858
|
+
this.flusher.persistNow();
|
|
859
|
+
}
|
|
860
|
+
};
|
|
654
861
|
captureEvent(name, properties, options = {}) {
|
|
862
|
+
const eventId = options.eventId ?? (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : void 0);
|
|
655
863
|
debugLog("queue event", this.options, { name, properties, options });
|
|
656
864
|
this.flusher.enqueue({
|
|
657
865
|
type: "event",
|
|
658
866
|
name,
|
|
659
867
|
properties,
|
|
660
|
-
...this.withDefaults(options)
|
|
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
|
|
661
877
|
});
|
|
662
878
|
}
|
|
663
879
|
captureError(error, options = {}) {
|
|
@@ -676,6 +892,13 @@ var EmitVisionClient = class {
|
|
|
676
892
|
identify(userIdOrUser, traits = {}) {
|
|
677
893
|
this.user = typeof userIdOrUser === "string" ? { id: userIdOrUser, ...traits } : { ...userIdOrUser };
|
|
678
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 });
|
|
679
902
|
}
|
|
680
903
|
setContext(context) {
|
|
681
904
|
this.context = { ...this.context, ...context };
|
|
@@ -733,10 +956,19 @@ var EmitVisionClient = class {
|
|
|
733
956
|
this.tags = { ...this.tags, ...tags };
|
|
734
957
|
debugLog("updated tags", this.options, this.tags);
|
|
735
958
|
}
|
|
959
|
+
consent() {
|
|
960
|
+
return this.flusher.giveConsent();
|
|
961
|
+
}
|
|
736
962
|
flush() {
|
|
737
963
|
return this.flusher.flush();
|
|
738
964
|
}
|
|
739
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
|
+
}
|
|
740
972
|
this.flusher.close();
|
|
741
973
|
this.autoCapture.detach();
|
|
742
974
|
debugLog("closed client", this.options);
|
|
@@ -749,7 +981,7 @@ var EmitVisionClient = class {
|
|
|
749
981
|
this.featureFlags,
|
|
750
982
|
this.tags,
|
|
751
983
|
this.options,
|
|
752
|
-
opts
|
|
984
|
+
{ groupId: this.groupId, ...opts }
|
|
753
985
|
);
|
|
754
986
|
}
|
|
755
987
|
};
|
|
@@ -809,6 +1041,12 @@ function setTags(tags) {
|
|
|
809
1041
|
async function flush() {
|
|
810
1042
|
await requireClient().flush();
|
|
811
1043
|
}
|
|
1044
|
+
async function consent() {
|
|
1045
|
+
await requireClient().consent();
|
|
1046
|
+
}
|
|
1047
|
+
function group(groupId, traits) {
|
|
1048
|
+
requireClient().group(groupId, traits);
|
|
1049
|
+
}
|
|
812
1050
|
async function evaluateFlags(options) {
|
|
813
1051
|
return requireClient().evaluateFlags(options);
|
|
814
1052
|
}
|
|
@@ -839,9 +1077,11 @@ export {
|
|
|
839
1077
|
captureExposure,
|
|
840
1078
|
captureFeedback,
|
|
841
1079
|
capturePageView,
|
|
1080
|
+
consent,
|
|
842
1081
|
evaluateFlags,
|
|
843
1082
|
flush,
|
|
844
1083
|
getFlag,
|
|
1084
|
+
group,
|
|
845
1085
|
identify,
|
|
846
1086
|
init,
|
|
847
1087
|
refreshFlags,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emit-vision/sdk-js",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Browser SDK for self-hosted emit-vision analytics and error tracking.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,6 +34,19 @@
|
|
|
34
34
|
}
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
|
-
"build": "tsup --config tsup.config.ts"
|
|
37
|
+
"build": "tsup --config tsup.config.ts",
|
|
38
|
+
"size": "size-limit",
|
|
39
|
+
"prepack": "pnpm run build"
|
|
40
|
+
},
|
|
41
|
+
"size-limit": [
|
|
42
|
+
{
|
|
43
|
+
"name": "@emit-vision/sdk-js (gzip)",
|
|
44
|
+
"path": "dist/index.js",
|
|
45
|
+
"limit": "9 kB"
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@size-limit/file": "^11.1.6",
|
|
50
|
+
"size-limit": "^11.1.6"
|
|
38
51
|
}
|
|
39
|
-
}
|
|
52
|
+
}
|