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