@watchupltd/browser 0.1.9 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,60 +1,671 @@
1
- import { initWatchUp } from "@watchup/core";
2
- export * from "@watchup/core";
3
- let activeClient;
4
- let cleanup;
5
- export function initBrowserWatchUp(options) {
6
- cleanup?.();
7
- const client = initWatchUp({
8
- platform: "browser",
9
- ...options
10
- });
11
- const disposers = [];
12
- if (typeof window !== "undefined" && options.autoCaptureErrors !== false) {
13
- const onError = (event) => {
14
- void client.captureException(event.error ?? event.message, {
15
- service: options.service ?? "frontend",
16
- metadata: {
17
- filename: event.filename,
18
- lineno: event.lineno,
19
- colno: event.colno,
20
- url: window.location.href
21
- },
22
- user_agent: navigator.userAgent
23
- });
24
- };
25
- window.addEventListener("error", onError);
26
- disposers.push(() => window.removeEventListener("error", onError));
27
- }
28
- if (typeof window !== "undefined" && options.autoCaptureUnhandledRejections !== false) {
29
- const onRejection = (event) => {
30
- void client.captureException(event.reason, {
31
- service: options.service ?? "frontend",
32
- metadata: {
33
- type: "unhandledrejection",
34
- url: window.location.href
35
- },
36
- user_agent: navigator.userAgent
37
- });
38
- };
39
- window.addEventListener("unhandledrejection", onRejection);
40
- disposers.push(() => window.removeEventListener("unhandledrejection", onRejection));
41
- }
42
- activeClient = client;
43
- cleanup = () => {
44
- disposers.forEach((dispose) => dispose());
45
- client.close();
46
- activeClient = undefined;
47
- };
48
- return client;
49
- }
50
- export function getWatchUpClient() {
51
- if (!activeClient) {
52
- throw new Error("WatchUp browser client has not been initialized");
53
- }
54
- return activeClient;
55
- }
56
- export function shutdownBrowserWatchUp() {
57
- cleanup?.();
58
- cleanup = undefined;
1
+ 'use strict';
2
+
3
+ // src/transport.ts
4
+ var Transport = class {
5
+ constructor(baseUrl, apiKey, debug = false) {
6
+ const base = baseUrl.replace(/\/$/, "");
7
+ this.url = `${base}/api/v1/ingest/batch`;
8
+ this.webUrl = `${base}/api/v1/ingest/web-batch`;
9
+ this.headers = {
10
+ "Content-Type": "application/json",
11
+ "X-Api-Key": apiKey
12
+ };
13
+ this.debug = debug;
14
+ }
15
+ /**
16
+ * Send via `fetch` with `keepalive: true`.
17
+ * `keepalive` lets the request outlive the current page — it's the
18
+ * browser equivalent of a "fire and forget" POST.
19
+ * Never rejects.
20
+ */
21
+ async send(batch) {
22
+ try {
23
+ const body = JSON.stringify(batch);
24
+ if (body.length > 6e4) {
25
+ this.beacon(batch);
26
+ return;
27
+ }
28
+ const res = await fetch(this.url, {
29
+ method: "POST",
30
+ headers: this.headers,
31
+ body,
32
+ keepalive: true
33
+ });
34
+ if (this.debug && !res.ok) {
35
+ const text = await res.text().catch(() => "");
36
+ console.warn(`[watchup] ingest ${res.status}: ${text}`);
37
+ }
38
+ } catch (err) {
39
+ if (this.debug) console.warn("[watchup] send failed:", err);
40
+ }
41
+ }
42
+ /**
43
+ * Send web analytics batch to the dedicated /web-batch endpoint.
44
+ * Never rejects.
45
+ */
46
+ async sendWeb(batch) {
47
+ try {
48
+ const body = JSON.stringify(batch);
49
+ if (body.length > 6e4) {
50
+ this.beaconWeb(batch);
51
+ return;
52
+ }
53
+ const res = await fetch(this.webUrl, {
54
+ method: "POST",
55
+ headers: this.headers,
56
+ body,
57
+ keepalive: true
58
+ });
59
+ if (this.debug && !res.ok) {
60
+ const text = await res.text().catch(() => "");
61
+ console.warn(`[watchup] web-batch ${res.status}: ${text}`);
62
+ }
63
+ } catch (err) {
64
+ if (this.debug) console.warn("[watchup] sendWeb failed:", err);
65
+ }
66
+ }
67
+ /**
68
+ * Send via `navigator.sendBeacon`.
69
+ * Returns `true` if the browser accepted the request (doesn't guarantee delivery).
70
+ * The server must accept `application/json` from sendBeacon via a Blob.
71
+ */
72
+ beacon(batch) {
73
+ if (typeof navigator === "undefined" || !navigator.sendBeacon) return false;
74
+ try {
75
+ const blob = new Blob([JSON.stringify(batch)], { type: "application/json" });
76
+ return navigator.sendBeacon(this.url, blob);
77
+ } catch {
78
+ return false;
79
+ }
80
+ }
81
+ /**
82
+ * sendBeacon variant for web analytics events.
83
+ */
84
+ beaconWeb(batch) {
85
+ if (typeof navigator === "undefined" || !navigator.sendBeacon) return false;
86
+ try {
87
+ const blob = new Blob([JSON.stringify(batch)], { type: "application/json" });
88
+ return navigator.sendBeacon(this.webUrl, blob);
89
+ } catch {
90
+ return false;
91
+ }
92
+ }
93
+ };
94
+
95
+ // src/batcher.ts
96
+ var Batcher = class {
97
+ constructor(transport, flushInterval, maxBatchSize) {
98
+ this.traces = [];
99
+ this.errors = [];
100
+ this.events = [];
101
+ this.webViews = [];
102
+ this.timer = null;
103
+ this.flushing = false;
104
+ this.transport = transport;
105
+ this.flushInterval = flushInterval;
106
+ this.maxBatchSize = maxBatchSize;
107
+ }
108
+ start() {
109
+ if (this.timer) return;
110
+ this.timer = setInterval(() => this.flush(), this.flushInterval);
111
+ document.addEventListener("visibilitychange", () => {
112
+ if (document.visibilityState === "hidden") this.beaconFlush();
113
+ });
114
+ window.addEventListener("pagehide", () => this.beaconFlush(), { once: true });
115
+ }
116
+ stop() {
117
+ if (this.timer) {
118
+ clearInterval(this.timer);
119
+ this.timer = null;
120
+ }
121
+ }
122
+ // ── Telemetry queue ───────────────────────────────────────────────────────
123
+ addTrace(t) {
124
+ this.traces.push(t);
125
+ if (this.traces.length >= this.maxBatchSize) this.flush();
126
+ }
127
+ addError(e) {
128
+ this.errors.push(e);
129
+ if (this.errors.length >= Math.ceil(this.maxBatchSize / 2)) this.flush();
130
+ }
131
+ addEvent(e) {
132
+ this.events.push(e);
133
+ if (this.events.length >= this.maxBatchSize) this.flush();
134
+ }
135
+ // ── Web analytics queue ───────────────────────────────────────────────────
136
+ addWebView(payload) {
137
+ this.webViews.push(payload);
138
+ if (this.webViews.length >= this.maxBatchSize) this.flushWeb();
139
+ }
140
+ // ── Drain helpers ─────────────────────────────────────────────────────────
141
+ drainTelemetry() {
142
+ const traces = this.traces.splice(0);
143
+ const errors = this.errors.splice(0);
144
+ const events = this.events.splice(0);
145
+ if (!traces.length && !errors.length && !events.length) return null;
146
+ return { traces, errors, events };
147
+ }
148
+ drainWeb() {
149
+ const web = this.webViews.splice(0);
150
+ if (!web.length) return null;
151
+ return { web };
152
+ }
153
+ // ── Flush ─────────────────────────────────────────────────────────────────
154
+ flush() {
155
+ if (!this.flushing) {
156
+ const batch = this.drainTelemetry();
157
+ if (batch) {
158
+ this.flushing = true;
159
+ this.transport.send(batch).finally(() => {
160
+ this.flushing = false;
161
+ });
162
+ }
163
+ }
164
+ this.flushWeb();
165
+ }
166
+ flushWeb() {
167
+ const batch = this.drainWeb();
168
+ if (batch) this.transport.sendWeb(batch);
169
+ }
170
+ beaconFlush() {
171
+ const batch = this.drainTelemetry();
172
+ if (batch) {
173
+ if (!this.transport.beacon(batch)) this.transport.send(batch);
174
+ }
175
+ const webBatch = this.drainWeb();
176
+ if (webBatch) {
177
+ if (!this.transport.beaconWeb(webBatch)) this.transport.sendWeb(webBatch);
178
+ }
179
+ }
180
+ };
181
+
182
+ // src/error-capture.ts
183
+ function captureGlobalErrors(onError, env) {
184
+ const handleError = (event) => {
185
+ var _a, _b, _c, _d;
186
+ onError({
187
+ message: event.message || "Unknown error",
188
+ level: "error",
189
+ route: window.location.pathname,
190
+ stack: (_a = event.error) == null ? void 0 : _a.stack,
191
+ context: {
192
+ url: window.location.href,
193
+ source: (_b = event.filename) != null ? _b : void 0,
194
+ line: (_c = event.lineno) != null ? _c : void 0,
195
+ col: (_d = event.colno) != null ? _d : void 0
196
+ },
197
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
198
+ ...env && { environment: env }
199
+ });
200
+ };
201
+ const handleRejection = (event) => {
202
+ const reason = event.reason;
203
+ const isErr = reason instanceof Error;
204
+ onError({
205
+ message: isErr ? reason.message : String(reason != null ? reason : "Unhandled Promise rejection"),
206
+ level: "error",
207
+ route: window.location.pathname,
208
+ stack: isErr ? reason.stack : void 0,
209
+ context: {
210
+ url: window.location.href,
211
+ type: "unhandledrejection"
212
+ },
213
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
214
+ ...env && { environment: env }
215
+ });
216
+ };
217
+ window.addEventListener("error", handleError);
218
+ window.addEventListener("unhandledrejection", handleRejection);
219
+ return () => {
220
+ window.removeEventListener("error", handleError);
221
+ window.removeEventListener("unhandledrejection", handleRejection);
222
+ };
223
+ }
224
+
225
+ // src/perf.ts
226
+ function rating(ms, good, needsImprovement) {
227
+ if (ms <= good) return "ok";
228
+ if (ms <= needsImprovement) return "warn";
229
+ return "err";
230
+ }
231
+ function statusCode(status) {
232
+ return status === "err" ? 500 : status === "warn" ? 400 : 200;
233
+ }
234
+ function captureFCP(onTrace, env) {
235
+ if (typeof PerformanceObserver === "undefined") return;
236
+ try {
237
+ const po = new PerformanceObserver((list) => {
238
+ for (const entry of list.getEntries()) {
239
+ if (entry.name !== "first-contentful-paint") continue;
240
+ const ms = Math.round(entry.startTime);
241
+ const status = rating(ms, 1800, 3e3);
242
+ onTrace({
243
+ span: "web-vital fcp",
244
+ ms,
245
+ status_code: statusCode(status),
246
+ status,
247
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
248
+ ...env && { environment: env }
249
+ });
250
+ po.disconnect();
251
+ }
252
+ });
253
+ po.observe({ type: "paint", buffered: true });
254
+ } catch {
255
+ }
256
+ }
257
+ function captureLCP(onTrace, env) {
258
+ if (typeof PerformanceObserver === "undefined") return;
259
+ let last = null;
260
+ let reported = false;
261
+ const report = () => {
262
+ if (reported || !last) return;
263
+ reported = true;
264
+ try {
265
+ po.disconnect();
266
+ } catch {
267
+ }
268
+ const ms = Math.round(last.startTime);
269
+ const status = rating(ms, 2500, 4e3);
270
+ onTrace({
271
+ span: "web-vital lcp",
272
+ ms,
273
+ status_code: statusCode(status),
274
+ status,
275
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
276
+ ...env && { environment: env }
277
+ });
278
+ };
279
+ let po;
280
+ try {
281
+ po = new PerformanceObserver((list) => {
282
+ var _a;
283
+ const entries = list.getEntries();
284
+ if (entries.length) last = (_a = entries[entries.length - 1]) != null ? _a : null;
285
+ });
286
+ po.observe({ type: "largest-contentful-paint", buffered: true });
287
+ } catch {
288
+ return;
289
+ }
290
+ document.addEventListener("visibilitychange", report, { once: true });
291
+ document.addEventListener("keydown", report, { once: true, capture: true });
292
+ document.addEventListener("pointerdown", report, { once: true, capture: true });
293
+ }
294
+ function capturePageLoad(onTrace, env) {
295
+ const report = () => {
296
+ const nav = performance.getEntriesByType("navigation")[0];
297
+ if (!nav || nav.loadEventEnd <= 0) return;
298
+ const ms = Math.round(nav.loadEventEnd - nav.startTime);
299
+ const ttfb = Math.round(nav.responseStart - nav.requestStart);
300
+ const status = rating(ms, 2e3, 4e3);
301
+ onTrace({
302
+ span: "pageload",
303
+ ms,
304
+ status_code: statusCode(status),
305
+ status,
306
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
307
+ meta: { ttfb },
308
+ ...env && { environment: env }
309
+ });
310
+ };
311
+ if (document.readyState === "complete") {
312
+ setTimeout(report, 0);
313
+ } else {
314
+ window.addEventListener("load", () => setTimeout(report, 100), { once: true });
315
+ }
316
+ }
317
+
318
+ // src/watchup.ts
319
+ function flagBucket(flagKey, userId) {
320
+ const str = `${flagKey}:${userId}`;
321
+ let hash = 5381;
322
+ for (let i = 0; i < str.length; i++) {
323
+ hash = hash * 33 ^ str.charCodeAt(i) | 0;
324
+ }
325
+ return (hash >>> 0) % 100;
59
326
  }
327
+ function matchesTargeting(flag, ctx) {
328
+ var _a;
329
+ if (!((_a = flag.targeting_rules) == null ? void 0 : _a.length)) return true;
330
+ return flag.targeting_rules.every((rule) => {
331
+ var _a2;
332
+ const val = String((_a2 = ctx[rule.attribute]) != null ? _a2 : "");
333
+ switch (rule.operator) {
334
+ case "in":
335
+ return rule.values.includes(val);
336
+ case "not_in":
337
+ return !rule.values.includes(val);
338
+ case "contains":
339
+ return rule.values.some((v) => val.includes(v));
340
+ case "equals":
341
+ return rule.values[0] === val;
342
+ default:
343
+ return true;
344
+ }
345
+ });
346
+ }
347
+ var DEFAULTS = {
348
+ baseUrl: "https://api.watchup.site",
349
+ flushInterval: 5e3,
350
+ maxBatchSize: 100,
351
+ debug: false,
352
+ environment: "production",
353
+ release: "",
354
+ sampleRate: 1,
355
+ autoCapture: {
356
+ errors: true,
357
+ performance: true,
358
+ pageViews: true
359
+ }
360
+ };
361
+ var VISITOR_KEY = "__wup_vid";
362
+ var SESSION_KEY = "__wup_sid";
363
+ var Watchup = class {
364
+ constructor(options) {
365
+ this.cleanup = [];
366
+ this._user = null;
367
+ /**
368
+ * A random UUID generated on init. Stable for the lifetime of the page —
369
+ * useful for correlating all events from one user session.
370
+ */
371
+ this.sessionId = crypto.randomUUID();
372
+ // Feature flags
373
+ this._flags = /* @__PURE__ */ new Map();
374
+ this._flagTimer = null;
375
+ if (!options.apiKey) {
376
+ throw new Error("[watchup] apiKey is required.");
377
+ }
378
+ this.cfg = {
379
+ ...DEFAULTS,
380
+ autoCapture: { ...DEFAULTS.autoCapture, ...options.autoCapture },
381
+ ...options
382
+ };
383
+ const transport = new Transport(this.cfg.baseUrl, this.cfg.apiKey, this.cfg.debug);
384
+ this.batcher = new Batcher(transport, this.cfg.flushInterval, this.cfg.maxBatchSize);
385
+ this.batcher.start();
386
+ this.visitorId = this._getOrCreateVisitorId();
387
+ this.webSessionId = this._getOrCreateSessionId();
388
+ this._setupAutoCapture();
389
+ this._fetchFlags();
390
+ this._flagTimer = setInterval(() => this._fetchFlags(), 3e4);
391
+ this.cleanup.push(() => {
392
+ if (this._flagTimer) clearInterval(this._flagTimer);
393
+ });
394
+ }
395
+ async _fetchFlags() {
396
+ var _a;
397
+ try {
398
+ const res = await fetch(`${this.cfg.baseUrl}/api/v1/flags`, {
399
+ headers: { "X-Api-Key": this.cfg.apiKey }
400
+ });
401
+ if (!res.ok) return;
402
+ const json = await res.json();
403
+ if (json.ok && ((_a = json.data) == null ? void 0 : _a.flags)) {
404
+ this._flags.clear();
405
+ for (const flag of json.data.flags) {
406
+ this._flags.set(flag.key, flag);
407
+ }
408
+ }
409
+ } catch {
410
+ }
411
+ }
412
+ // ── Visitor / session identity helpers ─────────────────────────────────────
413
+ _getOrCreateVisitorId() {
414
+ try {
415
+ let id = localStorage.getItem(VISITOR_KEY);
416
+ if (!id) {
417
+ id = crypto.randomUUID();
418
+ localStorage.setItem(VISITOR_KEY, id);
419
+ }
420
+ return id;
421
+ } catch {
422
+ return crypto.randomUUID();
423
+ }
424
+ }
425
+ _getOrCreateSessionId() {
426
+ try {
427
+ let id = sessionStorage.getItem(SESSION_KEY);
428
+ if (!id) {
429
+ id = crypto.randomUUID();
430
+ sessionStorage.setItem(SESSION_KEY, id);
431
+ }
432
+ return id;
433
+ } catch {
434
+ return this.sessionId;
435
+ }
436
+ }
437
+ // ── User identification ───────────────────────────────────────────────────
438
+ /**
439
+ * Attach a user to all subsequent errors, traces, and events.
440
+ * Call this after login; the context persists until `clearUser()` or page reload.
441
+ *
442
+ * @example
443
+ * watchup.setUser({ id: '42', email: 'ada@example.com', name: 'Ada Lovelace' });
444
+ */
445
+ setUser(user) {
446
+ this._user = { ...user };
447
+ }
448
+ /**
449
+ * Remove the current user context (e.g. after logout).
450
+ */
451
+ clearUser() {
452
+ this._user = null;
453
+ }
454
+ // ── Public API ────────────────────────────────────────────────────────────
455
+ /**
456
+ * Track a custom analytics event.
457
+ *
458
+ * @example
459
+ * watchup.track('button.clicked', { label: 'Sign Up', variant: 'A' });
460
+ */
461
+ track(name, properties) {
462
+ if (!name) return;
463
+ const event = {
464
+ name,
465
+ ...properties && Object.keys(properties).length && { properties },
466
+ occurred_at: (/* @__PURE__ */ new Date()).toISOString()
467
+ };
468
+ this.batcher.addEvent(event);
469
+ }
470
+ /**
471
+ * Track a web analytics page view (or custom web event).
472
+ * Enriches the payload with visitor context, UTM params, and device info.
473
+ *
474
+ * Normally called automatically. Call manually when you need custom event_name.
475
+ *
476
+ * @example
477
+ * watchup.trackWebView({ event_name: 'conversion', path: '/checkout/success' });
478
+ */
479
+ trackWebView(overrides = {}) {
480
+ var _a, _b;
481
+ const url = new URL(window.location.href);
482
+ const params = url.searchParams;
483
+ const payload = {
484
+ path: url.pathname + (url.search || ""),
485
+ hostname: url.hostname,
486
+ referrer: document.referrer || void 0,
487
+ title: document.title || void 0,
488
+ screen_w: (_a = window.screen) == null ? void 0 : _a.width,
489
+ screen_h: (_b = window.screen) == null ? void 0 : _b.height,
490
+ lang: navigator.language || void 0,
491
+ timezone: this._timezone(),
492
+ // UTM parameters
493
+ utm_source: params.get("utm_source") || void 0,
494
+ utm_medium: params.get("utm_medium") || void 0,
495
+ utm_campaign: params.get("utm_campaign") || void 0,
496
+ utm_term: params.get("utm_term") || void 0,
497
+ utm_content: params.get("utm_content") || void 0,
498
+ // Identity (raw; the server hashes before storing)
499
+ visitor_id: this.visitorId,
500
+ session_id: this.webSessionId,
501
+ event_name: "pageview",
502
+ occurred_at: (/* @__PURE__ */ new Date()).toISOString(),
503
+ // Apply caller overrides last
504
+ ...overrides
505
+ };
506
+ this.batcher.addWebView(payload);
507
+ }
508
+ /**
509
+ * Manually capture an error.
510
+ *
511
+ * @example
512
+ * try { ... } catch (err) {
513
+ * watchup.captureError(err, { component: 'CheckoutForm' });
514
+ * }
515
+ */
516
+ captureError(error, context) {
517
+ const { route, level = "error", ...rest } = context != null ? context : {};
518
+ const err = error instanceof Error ? error : new Error(String(error));
519
+ const payload = {
520
+ message: err.message,
521
+ level,
522
+ ...err.stack !== void 0 && { stack: err.stack },
523
+ route: route != null ? route : window.location.pathname,
524
+ ...Object.keys(rest).length && {
525
+ context: { ...rest, url: window.location.href }
526
+ },
527
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
528
+ environment: this.cfg.environment,
529
+ ...this.cfg.release && { release: this.cfg.release },
530
+ ...this._user && { user: this._user }
531
+ };
532
+ this.batcher.addError(payload);
533
+ }
534
+ /**
535
+ * Time any async operation and record it as a trace.
536
+ * Returns an `end()` function — call it when the operation finishes.
537
+ *
538
+ * @example
539
+ * const end = watchup.startTrace('fetch /api/cart');
540
+ * const cart = await fetch('/api/cart');
541
+ * end({ status: cart.ok ? 'ok' : 'err' });
542
+ */
543
+ startTrace(span) {
544
+ const start = Date.now();
545
+ return (opts = {}) => {
546
+ var _a;
547
+ const status = (_a = opts.status) != null ? _a : "ok";
548
+ this.batcher.addTrace({
549
+ span,
550
+ ms: Date.now() - start,
551
+ status_code: status === "err" ? 500 : status === "warn" ? 400 : 200,
552
+ status,
553
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
554
+ environment: this.cfg.environment,
555
+ ...this.cfg.release && { release: this.cfg.release },
556
+ ...opts.meta && { meta: opts.meta },
557
+ ...this._user && { user: this._user }
558
+ });
559
+ };
560
+ }
561
+ // ── Feature flags ─────────────────────────────────────────────────────────
562
+ /**
563
+ * Check whether a feature flag is enabled. Evaluated locally — zero
564
+ * network latency. Flag rules refresh every 30 seconds in the background.
565
+ * Falls back to the identified user (via `setUser`) when no context is given.
566
+ *
567
+ * @example
568
+ * if (watchup.isEnabled('new-checkout')) {
569
+ * renderNewCheckout();
570
+ * }
571
+ */
572
+ isEnabled(key, ctx = {}) {
573
+ var _a, _b, _c, _d;
574
+ const flag = this._flags.get(key);
575
+ if (!flag || !flag.enabled) return false;
576
+ const mergedCtx = { userId: (_a = this._user) == null ? void 0 : _a.id, email: (_b = this._user) == null ? void 0 : _b.email, ...ctx };
577
+ if (!matchesTargeting(flag, mergedCtx)) return false;
578
+ if (flag.rollout_percentage >= 100) return true;
579
+ if (flag.rollout_percentage <= 0) return false;
580
+ const userId = String((_d = (_c = mergedCtx.userId) != null ? _c : mergedCtx.email) != null ? _d : this.visitorId);
581
+ return flagBucket(key, userId) < flag.rollout_percentage;
582
+ }
583
+ /**
584
+ * Get the variant key for a multivariate (A/B) flag.
585
+ * Returns `"control"` if the flag is off or the visitor isn't in the rollout.
586
+ *
587
+ * @example
588
+ * const variant = watchup.getVariant('pricing-layout');
589
+ * // → "control" | "variant-a" | "variant-b"
590
+ */
591
+ getVariant(key, ctx = {}) {
592
+ var _a, _b, _c, _d, _e;
593
+ if (!this.isEnabled(key, ctx)) return "control";
594
+ const flag = this._flags.get(key);
595
+ if (!((_a = flag.variants) == null ? void 0 : _a.length)) return "on";
596
+ const mergedCtx = { userId: (_b = this._user) == null ? void 0 : _b.id, email: (_c = this._user) == null ? void 0 : _c.email, ...ctx };
597
+ const userId = String((_e = (_d = mergedCtx.userId) != null ? _d : mergedCtx.email) != null ? _e : this.visitorId);
598
+ const bucket = flagBucket(key, userId);
599
+ let cumulative = 0;
600
+ for (const variant of flag.variants) {
601
+ cumulative += variant.weight;
602
+ if (bucket < cumulative) return variant.key;
603
+ }
604
+ return flag.variants[flag.variants.length - 1].key;
605
+ }
606
+ /** Immediately flush all queued items (both telemetry and web analytics). */
607
+ flush() {
608
+ this.batcher.flush();
609
+ }
610
+ /** Stop the flush timer and release all listeners. */
611
+ shutdown() {
612
+ this.batcher.stop();
613
+ this.batcher.flush();
614
+ this.cleanup.forEach((fn) => fn());
615
+ }
616
+ // ── Auto-capture setup ────────────────────────────────────────────────────
617
+ _setupAutoCapture() {
618
+ const { autoCapture, environment } = this.cfg;
619
+ if (autoCapture.errors) {
620
+ this.cleanup.push(
621
+ captureGlobalErrors((e) => this.batcher.addError(e), environment)
622
+ );
623
+ }
624
+ if (autoCapture.performance) {
625
+ captureFCP((t) => this.batcher.addTrace(t), environment);
626
+ captureLCP((t) => this.batcher.addTrace(t), environment);
627
+ capturePageLoad((t) => this.batcher.addTrace(t), environment);
628
+ }
629
+ if (autoCapture.pageViews) {
630
+ this._setupPageViewTracking();
631
+ }
632
+ }
633
+ _setupPageViewTracking() {
634
+ const trackView = () => {
635
+ setTimeout(() => this.trackWebView(), 0);
636
+ };
637
+ if (document.readyState === "loading") {
638
+ document.addEventListener("DOMContentLoaded", trackView, { once: true });
639
+ } else {
640
+ setTimeout(() => this.trackWebView(), 0);
641
+ }
642
+ const origPush = history.pushState.bind(history);
643
+ const origReplace = history.replaceState.bind(history);
644
+ history.pushState = (...args) => {
645
+ origPush(...args);
646
+ trackView();
647
+ };
648
+ history.replaceState = (...args) => {
649
+ origReplace(...args);
650
+ };
651
+ const onPopState = () => trackView();
652
+ window.addEventListener("popstate", onPopState);
653
+ this.cleanup.push(() => {
654
+ history.pushState = origPush;
655
+ history.replaceState = origReplace;
656
+ window.removeEventListener("popstate", onPopState);
657
+ });
658
+ }
659
+ // ── Helpers ───────────────────────────────────────────────────────────────
660
+ _timezone() {
661
+ try {
662
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || void 0;
663
+ } catch {
664
+ return void 0;
665
+ }
666
+ }
667
+ };
668
+
669
+ exports.Watchup = Watchup;
670
+ //# sourceMappingURL=index.js.map
60
671
  //# sourceMappingURL=index.js.map