@kitbase/analytics 0.1.6

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 ADDED
@@ -0,0 +1,2374 @@
1
+ // src/errors.ts
2
+ var KitbaseError = class _KitbaseError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "KitbaseError";
6
+ Object.setPrototypeOf(this, _KitbaseError.prototype);
7
+ }
8
+ };
9
+ var AuthenticationError = class _AuthenticationError extends KitbaseError {
10
+ constructor(message = "Invalid API key") {
11
+ super(message);
12
+ this.name = "AuthenticationError";
13
+ Object.setPrototypeOf(this, _AuthenticationError.prototype);
14
+ }
15
+ };
16
+ var ApiError = class _ApiError extends KitbaseError {
17
+ statusCode;
18
+ response;
19
+ constructor(message, statusCode, response) {
20
+ super(message);
21
+ this.name = "ApiError";
22
+ this.statusCode = statusCode;
23
+ this.response = response;
24
+ Object.setPrototypeOf(this, _ApiError.prototype);
25
+ }
26
+ };
27
+ var ValidationError = class _ValidationError extends KitbaseError {
28
+ field;
29
+ constructor(message, field) {
30
+ super(message);
31
+ this.name = "ValidationError";
32
+ this.field = field;
33
+ Object.setPrototypeOf(this, _ValidationError.prototype);
34
+ }
35
+ };
36
+ var TimeoutError = class _TimeoutError extends KitbaseError {
37
+ constructor(message = "Request timed out") {
38
+ super(message);
39
+ this.name = "TimeoutError";
40
+ Object.setPrototypeOf(this, _TimeoutError.prototype);
41
+ }
42
+ };
43
+
44
+ // src/botDetection.ts
45
+ var AUTOMATION_GLOBALS = [
46
+ "__webdriver_evaluate",
47
+ "__selenium_evaluate",
48
+ "__webdriver_script_function",
49
+ "__webdriver_unwrapped",
50
+ "__fxdriver_evaluate",
51
+ "__driver_evaluate",
52
+ "_Selenium_IDE_Recorder",
53
+ "_selenium",
54
+ "calledSelenium",
55
+ "$cdc_asdjflasutopfhvcZLmcfl_",
56
+ // Chrome DevTools Protocol marker
57
+ "__nightmare",
58
+ "domAutomation",
59
+ "domAutomationController"
60
+ ];
61
+ var HEADLESS_PATTERNS = [
62
+ "headlesschrome",
63
+ "phantomjs",
64
+ "selenium",
65
+ "webdriver",
66
+ "puppeteer",
67
+ "playwright"
68
+ ];
69
+ var HTTP_CLIENT_PATTERNS = [
70
+ "python",
71
+ "curl",
72
+ "wget",
73
+ "java/",
74
+ "go-http",
75
+ "node-fetch",
76
+ "axios",
77
+ "postman",
78
+ "insomnia",
79
+ "httpie",
80
+ "ruby",
81
+ "perl",
82
+ "scrapy",
83
+ "bot",
84
+ "spider",
85
+ "crawler",
86
+ "slurp",
87
+ "googlebot",
88
+ "bingbot",
89
+ "yandexbot",
90
+ "baiduspider",
91
+ "duckduckbot",
92
+ "facebookexternalhit",
93
+ "twitterbot",
94
+ "linkedinbot",
95
+ "whatsapp",
96
+ "telegram",
97
+ "discord",
98
+ "slack"
99
+ ];
100
+ var DEFAULT_BOT_DETECTION_CONFIG = {
101
+ enabled: true,
102
+ checkWebdriver: true,
103
+ checkPhantomJS: true,
104
+ checkNightmare: true,
105
+ checkAutomationGlobals: true,
106
+ checkDocumentAttributes: true,
107
+ checkUserAgentHeadless: true,
108
+ checkUserAgentHttpClient: true
109
+ };
110
+ function isBrowser() {
111
+ return typeof window !== "undefined" && typeof document !== "undefined";
112
+ }
113
+ function getWindowProperty(key) {
114
+ try {
115
+ return window[key];
116
+ } catch {
117
+ return void 0;
118
+ }
119
+ }
120
+ function checkWebdriver() {
121
+ if (!isBrowser()) return false;
122
+ try {
123
+ return window.navigator?.webdriver === true;
124
+ } catch {
125
+ return false;
126
+ }
127
+ }
128
+ function checkPhantomJS() {
129
+ if (!isBrowser()) return false;
130
+ try {
131
+ return !!(getWindowProperty("callPhantom") || getWindowProperty("_phantom") || getWindowProperty("phantom"));
132
+ } catch {
133
+ return false;
134
+ }
135
+ }
136
+ function checkNightmare() {
137
+ if (!isBrowser()) return false;
138
+ try {
139
+ return !!getWindowProperty("__nightmare");
140
+ } catch {
141
+ return false;
142
+ }
143
+ }
144
+ function checkAutomationGlobals() {
145
+ if (!isBrowser()) return false;
146
+ try {
147
+ for (const global of AUTOMATION_GLOBALS) {
148
+ if (getWindowProperty(global) !== void 0) {
149
+ return true;
150
+ }
151
+ }
152
+ return false;
153
+ } catch {
154
+ return false;
155
+ }
156
+ }
157
+ function checkDocumentAttributes() {
158
+ if (!isBrowser()) return false;
159
+ try {
160
+ const docEl = document.documentElement;
161
+ if (!docEl) return false;
162
+ return !!(docEl.getAttribute("webdriver") || docEl.getAttribute("selenium") || docEl.getAttribute("driver"));
163
+ } catch {
164
+ return false;
165
+ }
166
+ }
167
+ function checkUserAgentHeadless() {
168
+ if (!isBrowser()) return false;
169
+ try {
170
+ const ua = window.navigator?.userAgent?.toLowerCase() || "";
171
+ if (!ua) return false;
172
+ for (const pattern of HEADLESS_PATTERNS) {
173
+ if (ua.includes(pattern)) {
174
+ return true;
175
+ }
176
+ }
177
+ return false;
178
+ } catch {
179
+ return false;
180
+ }
181
+ }
182
+ function checkUserAgentHttpClient(additionalPatterns) {
183
+ if (!isBrowser()) return false;
184
+ try {
185
+ const ua = window.navigator?.userAgent?.toLowerCase() || "";
186
+ if (!ua) return false;
187
+ for (const pattern of HTTP_CLIENT_PATTERNS) {
188
+ if (ua.includes(pattern)) {
189
+ return true;
190
+ }
191
+ }
192
+ if (additionalPatterns) {
193
+ for (const pattern of additionalPatterns) {
194
+ if (ua.includes(pattern.toLowerCase())) {
195
+ return true;
196
+ }
197
+ }
198
+ }
199
+ return false;
200
+ } catch {
201
+ return false;
202
+ }
203
+ }
204
+ function checkMissingUserAgent() {
205
+ if (!isBrowser()) return false;
206
+ try {
207
+ const ua = window.navigator?.userAgent;
208
+ return !ua || ua === "" || ua === "undefined" || ua.length < 10;
209
+ } catch {
210
+ return false;
211
+ }
212
+ }
213
+ function checkInvalidEnvironment() {
214
+ if (!isBrowser()) return false;
215
+ try {
216
+ if (!window.navigator || !window.location || !window.document || typeof window.navigator !== "object" || typeof window.location !== "object" || typeof window.document !== "object") {
217
+ return true;
218
+ }
219
+ return false;
220
+ } catch {
221
+ return true;
222
+ }
223
+ }
224
+ function detectBot(config = {}) {
225
+ const mergedConfig = { ...DEFAULT_BOT_DETECTION_CONFIG, ...config };
226
+ const checks = {
227
+ webdriver: mergedConfig.checkWebdriver ? checkWebdriver() : false,
228
+ phantomjs: mergedConfig.checkPhantomJS ? checkPhantomJS() : false,
229
+ nightmare: mergedConfig.checkNightmare ? checkNightmare() : false,
230
+ automationGlobals: mergedConfig.checkAutomationGlobals ? checkAutomationGlobals() : false,
231
+ documentAttributes: mergedConfig.checkDocumentAttributes ? checkDocumentAttributes() : false,
232
+ userAgentHeadless: mergedConfig.checkUserAgentHeadless ? checkUserAgentHeadless() : false,
233
+ userAgentHttpClient: mergedConfig.checkUserAgentHttpClient ? checkUserAgentHttpClient(config.additionalBotPatterns) : false,
234
+ missingUserAgent: checkMissingUserAgent(),
235
+ invalidEnvironment: checkInvalidEnvironment()
236
+ };
237
+ let reason;
238
+ if (checks.webdriver) {
239
+ reason = "WebDriver detected";
240
+ } else if (checks.phantomjs) {
241
+ reason = "PhantomJS detected";
242
+ } else if (checks.nightmare) {
243
+ reason = "Nightmare.js detected";
244
+ } else if (checks.automationGlobals) {
245
+ reason = "Automation tool globals detected";
246
+ } else if (checks.documentAttributes) {
247
+ reason = "Automation attributes on document element";
248
+ } else if (checks.userAgentHeadless) {
249
+ reason = "Headless browser user agent detected";
250
+ } else if (checks.userAgentHttpClient) {
251
+ reason = "HTTP client/bot user agent detected";
252
+ } else if (checks.missingUserAgent) {
253
+ reason = "Missing or invalid user agent";
254
+ } else if (checks.invalidEnvironment) {
255
+ reason = "Invalid browser environment";
256
+ }
257
+ const isBot2 = Object.values(checks).some(Boolean);
258
+ const result = {
259
+ isBot: isBot2,
260
+ reason,
261
+ checks
262
+ };
263
+ if (isBot2 && config.onBotDetected) {
264
+ try {
265
+ config.onBotDetected(result);
266
+ } catch {
267
+ }
268
+ }
269
+ return result;
270
+ }
271
+ function isBot(config = {}) {
272
+ return detectBot(config).isBot;
273
+ }
274
+ function isUserAgentBot(userAgent, additionalPatterns) {
275
+ if (!userAgent || userAgent.length < 10) {
276
+ return true;
277
+ }
278
+ const ua = userAgent.toLowerCase();
279
+ for (const pattern of HEADLESS_PATTERNS) {
280
+ if (ua.includes(pattern)) {
281
+ return true;
282
+ }
283
+ }
284
+ for (const pattern of HTTP_CLIENT_PATTERNS) {
285
+ if (ua.includes(pattern)) {
286
+ return true;
287
+ }
288
+ }
289
+ if (additionalPatterns) {
290
+ for (const pattern of additionalPatterns) {
291
+ if (ua.includes(pattern.toLowerCase())) {
292
+ return true;
293
+ }
294
+ }
295
+ }
296
+ return false;
297
+ }
298
+ function getUserAgent() {
299
+ if (!isBrowser()) return null;
300
+ try {
301
+ return window.navigator?.userAgent || null;
302
+ } catch {
303
+ return null;
304
+ }
305
+ }
306
+
307
+ // src/plugins/utils.ts
308
+ var CLICKABLE_SELECTOR = [
309
+ "a",
310
+ "button",
311
+ "input",
312
+ "select",
313
+ "textarea",
314
+ '[role="button"]',
315
+ '[role="link"]',
316
+ '[role="menuitem"]',
317
+ '[role="tab"]'
318
+ ].join(", ");
319
+ function findClickableElement(event) {
320
+ const path = event.composedPath?.();
321
+ if (path) {
322
+ for (const node of path) {
323
+ if (!(node instanceof Element)) continue;
324
+ if (node === document.documentElement) break;
325
+ if (node.matches(CLICKABLE_SELECTOR)) {
326
+ const root = node.getRootNode();
327
+ if (root instanceof ShadowRoot && root.host instanceof Element) {
328
+ return root.host;
329
+ }
330
+ return node;
331
+ }
332
+ if (node.tagName.includes("-")) {
333
+ return node;
334
+ }
335
+ }
336
+ }
337
+ const target = event.target;
338
+ if (!target?.closest) return null;
339
+ return target.closest(CLICKABLE_SELECTOR);
340
+ }
341
+ function buildCssSelector(el) {
342
+ if (el.id) return `#${el.id}`;
343
+ const tag = el.tagName.toLowerCase();
344
+ const classes = el.className && typeof el.className === "string" ? "." + el.className.trim().split(/\s+/).slice(0, 2).join(".") : "";
345
+ if (classes) return `${tag}${classes}`;
346
+ return tag;
347
+ }
348
+ function getRootDomain(hostname) {
349
+ const parts = hostname.replace(/^www\./, "").split(".");
350
+ if (parts.length >= 2) {
351
+ return parts.slice(-2).join(".");
352
+ }
353
+ return hostname;
354
+ }
355
+ function isSameRootDomain(host1, host2) {
356
+ return getRootDomain(host1) === getRootDomain(host2);
357
+ }
358
+ function getUtmParams() {
359
+ if (typeof window === "undefined") return {};
360
+ const params = new URLSearchParams(window.location.search);
361
+ const utmParams = {};
362
+ const utmKeys = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
363
+ for (const key of utmKeys) {
364
+ const value = params.get(key);
365
+ if (value) {
366
+ utmParams[`__${key}`] = value;
367
+ }
368
+ }
369
+ return utmParams;
370
+ }
371
+
372
+ // src/client-base.ts
373
+ var DEFAULT_BASE_URL = "https://api.kitbase.dev";
374
+ var TIMEOUT = 3e4;
375
+ var ANALYTICS_CHANNEL = "__analytics";
376
+ var KitbaseAnalytics = class _KitbaseAnalytics {
377
+ sdkKey;
378
+ baseUrl;
379
+ // Super properties (memory-only, merged into all events)
380
+ superProperties = {};
381
+ // Time event tracking
382
+ timedEvents = /* @__PURE__ */ new Map();
383
+ // Debug mode
384
+ debugMode;
385
+ // Analytics config (stored for PluginContext)
386
+ analyticsConfig;
387
+ userId = null;
388
+ // Bot detection
389
+ botDetectionConfig;
390
+ botDetectionResult = null;
391
+ // Client-side session tracking
392
+ clientSessionId = null;
393
+ lastActivityAt = 0;
394
+ static SESSION_TIMEOUT_MS = 30 * 60 * 1e3;
395
+ // 30 minutes
396
+ // Plugin system
397
+ _plugins = /* @__PURE__ */ new Map();
398
+ _pluginContext = null;
399
+ constructor(config, defaultPlugins) {
400
+ if (!config.sdkKey) {
401
+ throw new ValidationError("SDK key is required", "sdkKey");
402
+ }
403
+ this.sdkKey = config.sdkKey;
404
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
405
+ this.debugMode = config.debug ?? false;
406
+ this.analyticsConfig = config.analytics;
407
+ this.botDetectionConfig = {
408
+ ...DEFAULT_BOT_DETECTION_CONFIG,
409
+ ...config.botDetection
410
+ };
411
+ if (this.botDetectionConfig.enabled) {
412
+ this.botDetectionResult = detectBot(this.botDetectionConfig);
413
+ if (this.botDetectionResult.isBot) {
414
+ this.log("Bot detected", {
415
+ reason: this.botDetectionResult.reason,
416
+ checks: this.botDetectionResult.checks
417
+ });
418
+ } else {
419
+ this.log("Bot detection enabled, no bot detected");
420
+ }
421
+ }
422
+ if (defaultPlugins) {
423
+ for (const plugin of defaultPlugins) {
424
+ this.use(plugin);
425
+ }
426
+ }
427
+ }
428
+ // ============================================================
429
+ // Plugin System
430
+ // ============================================================
431
+ /**
432
+ * Register a plugin
433
+ *
434
+ * @param plugin - The plugin instance to register
435
+ *
436
+ * @example
437
+ * ```typescript
438
+ * kitbase.use(new WebVitalsPlugin());
439
+ * ```
440
+ */
441
+ use(plugin) {
442
+ if (this._plugins.has(plugin.name)) {
443
+ this.log(`Plugin "${plugin.name}" already registered`);
444
+ return;
445
+ }
446
+ const ctx = this.getPluginContext();
447
+ const result = plugin.setup(ctx);
448
+ if (result === false) {
449
+ this.log(`Plugin "${plugin.name}" declined to activate`);
450
+ return;
451
+ }
452
+ this._plugins.set(plugin.name, plugin);
453
+ const methods = plugin.methods;
454
+ if (methods) {
455
+ for (const [name, fn] of Object.entries(methods)) {
456
+ this[name] = fn;
457
+ }
458
+ }
459
+ this.log(`Plugin "${plugin.name}" registered`);
460
+ }
461
+ /**
462
+ * Get the names of all registered plugins
463
+ */
464
+ getPlugins() {
465
+ return Array.from(this._plugins.keys());
466
+ }
467
+ getPluginContext() {
468
+ if (this._pluginContext) return this._pluginContext;
469
+ this._pluginContext = {
470
+ track: (options) => this.track(options),
471
+ config: Object.freeze({
472
+ autoTrackPageViews: this.analyticsConfig?.autoTrackPageViews,
473
+ autoTrackOutboundLinks: this.analyticsConfig?.autoTrackOutboundLinks,
474
+ autoTrackClicks: this.analyticsConfig?.autoTrackClicks,
475
+ autoTrackScrollDepth: this.analyticsConfig?.autoTrackScrollDepth,
476
+ autoTrackVisibility: this.analyticsConfig?.autoTrackVisibility,
477
+ autoTrackWebVitals: this.analyticsConfig?.autoTrackWebVitals,
478
+ autoDetectFrustration: this.analyticsConfig?.autoDetectFrustration
479
+ }),
480
+ debug: this.debugMode,
481
+ log: (message, data) => this.log(message, data),
482
+ isBotBlockingActive: () => this.isBotBlockingActive(),
483
+ findClickableElement,
484
+ CLICKABLE_SELECTOR,
485
+ getRootDomain,
486
+ isSameRootDomain,
487
+ getUtmParams
488
+ };
489
+ return this._pluginContext;
490
+ }
491
+ // ============================================================
492
+ // Debug Mode
493
+ // ============================================================
494
+ /**
495
+ * Enable or disable debug mode
496
+ * When enabled, all SDK operations are logged to the console
497
+ *
498
+ * @param enabled - Whether to enable debug mode
499
+ *
500
+ * @example
501
+ * ```typescript
502
+ * kitbase.setDebugMode(true);
503
+ * // All events and operations will now be logged
504
+ * ```
505
+ */
506
+ setDebugMode(enabled) {
507
+ this.debugMode = enabled;
508
+ this.log(`Debug mode ${enabled ? "enabled" : "disabled"}`);
509
+ }
510
+ /**
511
+ * Check if debug mode is enabled
512
+ */
513
+ isDebugMode() {
514
+ return this.debugMode;
515
+ }
516
+ /**
517
+ * Internal logging function
518
+ */
519
+ log(message, data) {
520
+ if (!this.debugMode) return;
521
+ const prefix = "[Kitbase]";
522
+ if (data !== void 0) {
523
+ console.log(prefix, message, data);
524
+ } else {
525
+ console.log(prefix, message);
526
+ }
527
+ }
528
+ // ============================================================
529
+ // Super Properties
530
+ // ============================================================
531
+ /**
532
+ * Register super properties that will be included with every event
533
+ * These properties are stored in memory only and reset on page reload
534
+ *
535
+ * @param properties - Properties to register
536
+ *
537
+ * @example
538
+ * ```typescript
539
+ * kitbase.register({
540
+ * app_version: '2.1.0',
541
+ * platform: 'web',
542
+ * environment: 'production',
543
+ * });
544
+ * ```
545
+ */
546
+ register(properties) {
547
+ this.superProperties = { ...this.superProperties, ...properties };
548
+ this.log("Super properties registered", properties);
549
+ }
550
+ /**
551
+ * Register super properties only if they haven't been set yet
552
+ * Useful for setting default values that shouldn't override existing ones
553
+ *
554
+ * @param properties - Properties to register if not already set
555
+ *
556
+ * @example
557
+ * ```typescript
558
+ * kitbase.registerOnce({ first_visit: new Date().toISOString() });
559
+ * ```
560
+ */
561
+ registerOnce(properties) {
562
+ const newProps = {};
563
+ for (const [key, value] of Object.entries(properties)) {
564
+ if (!(key in this.superProperties)) {
565
+ newProps[key] = value;
566
+ }
567
+ }
568
+ if (Object.keys(newProps).length > 0) {
569
+ this.superProperties = { ...this.superProperties, ...newProps };
570
+ this.log("Super properties registered (once)", newProps);
571
+ }
572
+ }
573
+ /**
574
+ * Remove a super property
575
+ *
576
+ * @param key - The property key to remove
577
+ *
578
+ * @example
579
+ * ```typescript
580
+ * kitbase.unregister('platform');
581
+ * ```
582
+ */
583
+ unregister(key) {
584
+ if (key in this.superProperties) {
585
+ delete this.superProperties[key];
586
+ this.log("Super property removed", { key });
587
+ }
588
+ }
589
+ /**
590
+ * Get all registered super properties
591
+ *
592
+ * @returns A copy of the current super properties
593
+ *
594
+ * @example
595
+ * ```typescript
596
+ * const props = kitbase.getSuperProperties();
597
+ * console.log(props); // { app_version: '2.1.0', platform: 'web' }
598
+ * ```
599
+ */
600
+ getSuperProperties() {
601
+ return { ...this.superProperties };
602
+ }
603
+ /**
604
+ * Clear all super properties
605
+ *
606
+ * @example
607
+ * ```typescript
608
+ * kitbase.clearSuperProperties();
609
+ * ```
610
+ */
611
+ clearSuperProperties() {
612
+ this.superProperties = {};
613
+ this.log("Super properties cleared");
614
+ }
615
+ // ============================================================
616
+ // Time Events (Duration Tracking)
617
+ // ============================================================
618
+ /**
619
+ * Start timing an event
620
+ * When the same event is tracked later, a $duration property (in seconds)
621
+ * will automatically be included
622
+ *
623
+ * @param eventName - The name of the event to time
624
+ *
625
+ * @example
626
+ * ```typescript
627
+ * kitbase.timeEvent('Video Watched');
628
+ * // ... user watches video ...
629
+ * await kitbase.track({
630
+ * channel: 'engagement',
631
+ * event: 'Video Watched',
632
+ * tags: { video_id: '123' }
633
+ * });
634
+ * // Event will include $duration: 45.2 (seconds)
635
+ * ```
636
+ */
637
+ timeEvent(eventName) {
638
+ this.timedEvents.set(eventName, Date.now());
639
+ this.log("Timer started", { event: eventName });
640
+ }
641
+ /**
642
+ * Cancel a timed event without tracking it
643
+ *
644
+ * @param eventName - The name of the event to cancel timing for
645
+ *
646
+ * @example
647
+ * ```typescript
648
+ * kitbase.timeEvent('Checkout Flow');
649
+ * // User abandons checkout
650
+ * kitbase.cancelTimeEvent('Checkout Flow');
651
+ * ```
652
+ */
653
+ cancelTimeEvent(eventName) {
654
+ if (this.timedEvents.has(eventName)) {
655
+ this.timedEvents.delete(eventName);
656
+ this.log("Timer cancelled", { event: eventName });
657
+ }
658
+ }
659
+ /**
660
+ * Get all currently timed events
661
+ *
662
+ * @returns Array of event names that are currently being timed
663
+ *
664
+ * @example
665
+ * ```typescript
666
+ * const timedEvents = kitbase.getTimedEvents();
667
+ * console.log(timedEvents); // ['Video Watched', 'Checkout Flow']
668
+ * ```
669
+ */
670
+ getTimedEvents() {
671
+ return Array.from(this.timedEvents.keys());
672
+ }
673
+ /**
674
+ * Get the duration of a timed event (without stopping it)
675
+ * @internal
676
+ *
677
+ * @param eventName - The name of the event
678
+ * @returns Duration in seconds, or null if not being timed
679
+ */
680
+ getEventDuration(eventName) {
681
+ const startTime = this.timedEvents.get(eventName);
682
+ if (startTime === void 0) return null;
683
+ return (Date.now() - startTime) / 1e3;
684
+ }
685
+ // ============================================================
686
+ // Bot Detection
687
+ // ============================================================
688
+ /**
689
+ * Check if the current visitor is detected as a bot
690
+ *
691
+ * @internal This is an internal API and may change without notice.
692
+ * @returns true if bot detected, false otherwise
693
+ *
694
+ * @example
695
+ * ```typescript
696
+ * if (kitbase.isBot()) {
697
+ * console.log('Bot detected, tracking disabled');
698
+ * }
699
+ * ```
700
+ */
701
+ isBot() {
702
+ if (!this.botDetectionConfig?.enabled) {
703
+ return false;
704
+ }
705
+ if (!this.botDetectionResult) {
706
+ this.botDetectionResult = detectBot(this.botDetectionConfig);
707
+ }
708
+ return this.botDetectionResult.isBot;
709
+ }
710
+ /**
711
+ * Get detailed bot detection result
712
+ *
713
+ * @internal This is an internal API and may change without notice.
714
+ * @returns Bot detection result with detailed checks, or null if detection not enabled
715
+ *
716
+ * @example
717
+ * ```typescript
718
+ * const result = kitbase.getBotDetectionResult();
719
+ * if (result?.isBot) {
720
+ * console.log('Bot detected:', result.reason);
721
+ * console.log('Checks:', result.checks);
722
+ * }
723
+ * ```
724
+ */
725
+ getBotDetectionResult() {
726
+ if (!this.botDetectionConfig.enabled) {
727
+ return null;
728
+ }
729
+ if (!this.botDetectionResult) {
730
+ this.botDetectionResult = detectBot(this.botDetectionConfig);
731
+ }
732
+ return this.botDetectionResult;
733
+ }
734
+ /**
735
+ * Force re-run bot detection
736
+ * Useful if you want to check again after page state changes
737
+ *
738
+ * @internal This is an internal API and may change without notice.
739
+ * @returns Updated bot detection result
740
+ *
741
+ * @example
742
+ * ```typescript
743
+ * const result = kitbase.redetectBot();
744
+ * console.log('Is bot:', result.isBot);
745
+ * ```
746
+ */
747
+ redetectBot() {
748
+ this.botDetectionResult = detectBot(this.botDetectionConfig);
749
+ this.log("Bot detection re-run", {
750
+ isBot: this.botDetectionResult.isBot,
751
+ reason: this.botDetectionResult.reason
752
+ });
753
+ return this.botDetectionResult;
754
+ }
755
+ /**
756
+ * Check if bot blocking is currently active
757
+ * When bot detection is enabled and a bot is detected, all events are blocked.
758
+ *
759
+ * @internal This is an internal API and may change without notice.
760
+ * @returns true if bots are being blocked from tracking
761
+ */
762
+ isBotBlockingActive() {
763
+ return this.botDetectionConfig?.enabled === true && this.isBot();
764
+ }
765
+ // ============================================================
766
+ // Client Session Tracking
767
+ // ============================================================
768
+ /**
769
+ * Get or create a client-side session ID.
770
+ * Rotates the session after 30 minutes of inactivity.
771
+ * @internal
772
+ */
773
+ getClientSessionId() {
774
+ const now = Date.now();
775
+ if (!this.clientSessionId || this.lastActivityAt > 0 && now - this.lastActivityAt > _KitbaseAnalytics.SESSION_TIMEOUT_MS) {
776
+ this.clientSessionId = _KitbaseAnalytics.generateUUID();
777
+ this.log("New client session started", { sessionId: this.clientSessionId });
778
+ }
779
+ this.lastActivityAt = now;
780
+ return this.clientSessionId;
781
+ }
782
+ /**
783
+ * Generate a UUID v4, with fallback for environments where
784
+ * crypto.randomUUID() is not available (older WebViews, Ionic).
785
+ */
786
+ static generateUUID() {
787
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
788
+ return crypto.randomUUID();
789
+ }
790
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
791
+ const bytes = new Uint8Array(16);
792
+ crypto.getRandomValues(bytes);
793
+ bytes[6] = bytes[6] & 15 | 64;
794
+ bytes[8] = bytes[8] & 63 | 128;
795
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
796
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
797
+ }
798
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
799
+ const r = Math.random() * 16 | 0;
800
+ const v = c === "x" ? r : r & 3 | 8;
801
+ return v.toString(16);
802
+ });
803
+ }
804
+ // ============================================================
805
+ // Track Event
806
+ // ============================================================
807
+ /**
808
+ * Track an event
809
+ *
810
+ * Events are sent directly to the API. For offline queueing support,
811
+ * use the full Kitbase client instead.
812
+ *
813
+ * @param options - Event tracking options
814
+ * @returns Promise resolving to the track response, or void if tracking is blocked
815
+ * @throws {ValidationError} When required fields are missing
816
+ * @throws {AuthenticationError} When the API key is invalid
817
+ * @throws {ApiError} When the API returns an error
818
+ * @throws {TimeoutError} When the request times out
819
+ */
820
+ async track(options) {
821
+ this.validateTrackOptions(options);
822
+ if (this.isBotBlockingActive()) {
823
+ this.log("Event skipped - bot detected", { event: options.event });
824
+ return;
825
+ }
826
+ let duration;
827
+ const startTime = this.timedEvents.get(options.event);
828
+ if (startTime !== void 0) {
829
+ duration = (Date.now() - startTime) / 1e3;
830
+ this.timedEvents.delete(options.event);
831
+ this.log("Timer stopped", { event: options.event, duration });
832
+ }
833
+ const mergedTags = {
834
+ ...this.superProperties,
835
+ ...options.tags ?? {},
836
+ ...duration !== void 0 ? { $duration: duration } : {}
837
+ };
838
+ const payload = {
839
+ channel: options.channel,
840
+ event: options.event,
841
+ client_timestamp: Date.now(),
842
+ client_session_id: this.getClientSessionId(),
843
+ ...options.user_id && { user_id: options.user_id },
844
+ ...options.icon && { icon: options.icon },
845
+ ...options.notify !== void 0 && { notify: options.notify },
846
+ ...options.description && { description: options.description },
847
+ ...Object.keys(mergedTags).length > 0 && { tags: mergedTags }
848
+ };
849
+ this.log("Track", { event: options.event, payload });
850
+ const response = await this.sendRequest("/sdk/v1/logs", payload);
851
+ this.log("Event sent successfully", { id: response.id });
852
+ return response;
853
+ }
854
+ validateTrackOptions(options) {
855
+ if (!options.event) {
856
+ throw new ValidationError("Event is required", "event");
857
+ }
858
+ }
859
+ /**
860
+ * Send a request to the API
861
+ */
862
+ async sendRequest(endpoint, body) {
863
+ const url = `${this.baseUrl}${endpoint}`;
864
+ const controller = new AbortController();
865
+ const timeoutId = setTimeout(() => controller.abort(), TIMEOUT);
866
+ try {
867
+ const response = await fetch(url, {
868
+ method: "POST",
869
+ headers: {
870
+ "Content-Type": "application/json",
871
+ "x-sdk-key": `${this.sdkKey}`
872
+ },
873
+ body: JSON.stringify(body),
874
+ signal: controller.signal
875
+ });
876
+ clearTimeout(timeoutId);
877
+ if (!response.ok) {
878
+ const errorBody = await this.parseResponseBody(response);
879
+ if (response.status === 401) {
880
+ throw new AuthenticationError();
881
+ }
882
+ throw new ApiError(
883
+ this.getErrorMessage(errorBody, response.statusText),
884
+ response.status,
885
+ errorBody
886
+ );
887
+ }
888
+ return await response.json();
889
+ } catch (error) {
890
+ clearTimeout(timeoutId);
891
+ if (error instanceof Error && error.name === "AbortError") {
892
+ throw new TimeoutError();
893
+ }
894
+ throw error;
895
+ }
896
+ }
897
+ async parseResponseBody(response) {
898
+ try {
899
+ return await response.json();
900
+ } catch {
901
+ return null;
902
+ }
903
+ }
904
+ getErrorMessage(body, fallback) {
905
+ if (body && typeof body === "object" && "message" in body) {
906
+ return String(body.message);
907
+ }
908
+ if (body && typeof body === "object" && "error" in body) {
909
+ return String(body.error);
910
+ }
911
+ return fallback;
912
+ }
913
+ // ============================================================
914
+ // Analytics — Stub methods (overridden by plugins via methods)
915
+ // ============================================================
916
+ /**
917
+ * Track a page view
918
+ *
919
+ * @param options - Page view options
920
+ * @returns Promise resolving to the track response
921
+ *
922
+ * @example
923
+ * ```typescript
924
+ * // Track current page
925
+ * await kitbase.trackPageView();
926
+ *
927
+ * // Track with custom path
928
+ * await kitbase.trackPageView({ path: '/products/123', title: 'Product Details' });
929
+ * ```
930
+ */
931
+ async trackPageView(options) {
932
+ this.log("trackPageView() called but page-view plugin is not registered");
933
+ }
934
+ /**
935
+ * Track a click on an interactive element
936
+ */
937
+ async trackClick(tags) {
938
+ this.log("trackClick() called but click-tracking plugin is not registered");
939
+ }
940
+ /**
941
+ * Track an outbound link click
942
+ *
943
+ * @param options - Outbound link options
944
+ * @returns Promise resolving to the track response
945
+ *
946
+ * @example
947
+ * ```typescript
948
+ * await kitbase.trackOutboundLink({
949
+ * url: 'https://example.com',
950
+ * text: 'Visit Example',
951
+ * });
952
+ * ```
953
+ */
954
+ async trackOutboundLink(options) {
955
+ this.log("trackOutboundLink() called but outbound-links plugin is not registered");
956
+ }
957
+ // ============================================================
958
+ // Analytics — Revenue & Identity (non-plugin)
959
+ // ============================================================
960
+ /**
961
+ * Track a revenue event
962
+ *
963
+ * @param options - Revenue options
964
+ * @returns Promise resolving to the track response
965
+ *
966
+ * @example
967
+ * ```typescript
968
+ * // Track a $19.99 purchase
969
+ * await kitbase.trackRevenue({
970
+ * amount: 1999,
971
+ * currency: 'USD',
972
+ * tags: { product_id: 'prod_123', plan: 'premium' },
973
+ * });
974
+ * ```
975
+ */
976
+ async trackRevenue(options) {
977
+ return this.track({
978
+ channel: ANALYTICS_CHANNEL,
979
+ event: "revenue",
980
+ user_id: options.user_id ?? this.userId ?? void 0,
981
+ tags: {
982
+ __revenue: options.amount,
983
+ __currency: options.currency ?? "USD",
984
+ ...options.tags ?? {}
985
+ }
986
+ });
987
+ }
988
+ /**
989
+ * Identify a user
990
+ * Sets the user identity on the server.
991
+ * Call this when a user signs up or logs in.
992
+ *
993
+ * @param options - Identify options
994
+ * @returns Promise that resolves when the identity is set
995
+ *
996
+ * @example
997
+ * ```typescript
998
+ * await kitbase.identify({
999
+ * userId: 'user_123',
1000
+ * traits: { email: 'user@example.com', plan: 'premium' },
1001
+ * });
1002
+ * ```
1003
+ */
1004
+ async identify(options) {
1005
+ this.userId = options.userId;
1006
+ if (options.traits) {
1007
+ this.register({
1008
+ __user_id: options.userId,
1009
+ ...options.traits
1010
+ });
1011
+ } else {
1012
+ this.register({ __user_id: options.userId });
1013
+ }
1014
+ try {
1015
+ const response = await fetch(`${this.baseUrl}/sdk/v1/identify`, {
1016
+ method: "POST",
1017
+ headers: {
1018
+ "Content-Type": "application/json",
1019
+ "x-sdk-key": this.sdkKey
1020
+ },
1021
+ body: JSON.stringify({
1022
+ user_id: options.userId,
1023
+ traits: options.traits
1024
+ })
1025
+ });
1026
+ if (!response.ok) {
1027
+ this.log("Identify API call failed", { status: response.status });
1028
+ } else {
1029
+ this.log("Identity set on server", {
1030
+ userId: options.userId
1031
+ });
1032
+ }
1033
+ } catch (err) {
1034
+ this.log("Failed to call identify endpoint", err);
1035
+ }
1036
+ this.log("User identified", { userId: options.userId });
1037
+ }
1038
+ /**
1039
+ * Get the current user ID (set via identify)
1040
+ */
1041
+ getUserId() {
1042
+ return this.userId;
1043
+ }
1044
+ /**
1045
+ * Reset the user identity
1046
+ * Call this when a user logs out
1047
+ *
1048
+ * @example
1049
+ * ```typescript
1050
+ * kitbase.reset();
1051
+ * ```
1052
+ */
1053
+ reset() {
1054
+ this.userId = null;
1055
+ this.clientSessionId = null;
1056
+ this.lastActivityAt = 0;
1057
+ this.clearSuperProperties();
1058
+ this.log("User reset complete");
1059
+ }
1060
+ // ============================================================
1061
+ // Cleanup
1062
+ // ============================================================
1063
+ /**
1064
+ * Shutdown the client and cleanup resources
1065
+ * Call this when you're done using the client to stop timers and close connections
1066
+ *
1067
+ * @example
1068
+ * ```typescript
1069
+ * kitbase.shutdown();
1070
+ * ```
1071
+ */
1072
+ shutdown() {
1073
+ this.log("Shutting down");
1074
+ this.timedEvents.clear();
1075
+ const pluginNames = Array.from(this._plugins.keys()).reverse();
1076
+ for (const name of pluginNames) {
1077
+ const plugin = this._plugins.get(name);
1078
+ try {
1079
+ plugin.teardown();
1080
+ this.log(`Plugin "${name}" torn down`);
1081
+ } catch (err) {
1082
+ this.log(`Plugin "${name}" teardown failed`, err);
1083
+ }
1084
+ }
1085
+ this._plugins.clear();
1086
+ this._pluginContext = null;
1087
+ this.log("Shutdown complete");
1088
+ }
1089
+ };
1090
+
1091
+ // src/queue/index.ts
1092
+ import Dexie from "dexie";
1093
+ var DEFAULT_CONFIG = {
1094
+ enabled: false,
1095
+ maxQueueSize: 1e3,
1096
+ flushInterval: 3e4,
1097
+ flushBatchSize: 50,
1098
+ maxRetries: 3,
1099
+ retryBaseDelay: 1e3
1100
+ };
1101
+ function isIndexedDBAvailable() {
1102
+ try {
1103
+ return typeof window !== "undefined" && typeof window.indexedDB !== "undefined" && window.indexedDB !== null;
1104
+ } catch {
1105
+ return false;
1106
+ }
1107
+ }
1108
+ function isBrowser2() {
1109
+ return typeof window !== "undefined" && typeof document !== "undefined";
1110
+ }
1111
+ var KitbaseQueueDB = class extends Dexie {
1112
+ events;
1113
+ constructor(dbName) {
1114
+ super(dbName);
1115
+ this.version(1).stores({
1116
+ events: "++id, timestamp, retries, lastAttempt"
1117
+ });
1118
+ }
1119
+ };
1120
+ var MemoryQueue = class {
1121
+ queue = [];
1122
+ idCounter = 1;
1123
+ async enqueue(payload) {
1124
+ const event = {
1125
+ id: this.idCounter++,
1126
+ payload,
1127
+ timestamp: Date.now(),
1128
+ retries: 0
1129
+ };
1130
+ this.queue.push(event);
1131
+ return event.id;
1132
+ }
1133
+ async dequeue(count) {
1134
+ this.queue.sort((a, b) => a.timestamp - b.timestamp);
1135
+ return this.queue.slice(0, count);
1136
+ }
1137
+ async delete(ids) {
1138
+ this.queue = this.queue.filter((e) => !ids.includes(e.id));
1139
+ }
1140
+ async updateRetries(ids) {
1141
+ const now = Date.now();
1142
+ for (const event of this.queue) {
1143
+ if (ids.includes(event.id)) {
1144
+ event.retries++;
1145
+ event.lastAttempt = now;
1146
+ }
1147
+ }
1148
+ }
1149
+ async getStats() {
1150
+ const size = this.queue.length;
1151
+ const oldestEvent = size > 0 ? Math.min(...this.queue.map((e) => e.timestamp)) : void 0;
1152
+ return { size, oldestEvent };
1153
+ }
1154
+ async clear() {
1155
+ this.queue = [];
1156
+ }
1157
+ async enforceMaxSize(maxSize) {
1158
+ if (this.queue.length > maxSize) {
1159
+ this.queue.sort((a, b) => a.timestamp - b.timestamp);
1160
+ this.queue = this.queue.slice(-maxSize);
1161
+ }
1162
+ }
1163
+ async getEventsExceedingRetries(maxRetries) {
1164
+ return this.queue.filter((e) => e.retries >= maxRetries).map((e) => e.id);
1165
+ }
1166
+ };
1167
+ var EventQueue = class {
1168
+ config;
1169
+ dbName;
1170
+ db = null;
1171
+ memoryQueue = null;
1172
+ flushTimer = null;
1173
+ isFlushing = false;
1174
+ sendEvents = null;
1175
+ useIndexedDB;
1176
+ debugMode = false;
1177
+ debugLogger = null;
1178
+ constructor(config = {}, dbName = "_ka_events") {
1179
+ this.config = { ...DEFAULT_CONFIG, ...config };
1180
+ this.dbName = dbName;
1181
+ this.useIndexedDB = isIndexedDBAvailable();
1182
+ if (this.useIndexedDB) {
1183
+ this.db = new KitbaseQueueDB(this.dbName);
1184
+ } else {
1185
+ this.memoryQueue = new MemoryQueue();
1186
+ }
1187
+ }
1188
+ /**
1189
+ * Set debug mode and logger
1190
+ */
1191
+ setDebugMode(enabled, logger) {
1192
+ this.debugMode = enabled;
1193
+ this.debugLogger = logger ?? null;
1194
+ }
1195
+ log(message, data) {
1196
+ if (this.debugMode && this.debugLogger) {
1197
+ this.debugLogger(message, data);
1198
+ }
1199
+ }
1200
+ /**
1201
+ * Set the callback for sending events
1202
+ */
1203
+ setSendCallback(callback) {
1204
+ this.sendEvents = callback;
1205
+ }
1206
+ /**
1207
+ * Check if the queue storage is available
1208
+ */
1209
+ isAvailable() {
1210
+ return this.useIndexedDB || this.memoryQueue !== null;
1211
+ }
1212
+ /**
1213
+ * Get the storage type being used
1214
+ */
1215
+ getStorageType() {
1216
+ return this.useIndexedDB ? "indexeddb" : "memory";
1217
+ }
1218
+ /**
1219
+ * Add an event to the queue
1220
+ */
1221
+ async enqueue(payload) {
1222
+ const event = {
1223
+ payload,
1224
+ timestamp: Date.now(),
1225
+ retries: 0
1226
+ };
1227
+ if (this.useIndexedDB && this.db) {
1228
+ await this.db.events.add(event);
1229
+ this.log("Event queued to IndexedDB", payload);
1230
+ } else if (this.memoryQueue) {
1231
+ await this.memoryQueue.enqueue(payload);
1232
+ this.log("Event queued to memory", payload);
1233
+ }
1234
+ await this.enforceMaxQueueSize();
1235
+ }
1236
+ /**
1237
+ * Get and remove the next batch of events to send
1238
+ */
1239
+ async dequeue(count) {
1240
+ if (this.useIndexedDB && this.db) {
1241
+ return this.db.events.where("retries").below(this.config.maxRetries).sortBy("timestamp").then((events) => events.slice(0, count));
1242
+ } else if (this.memoryQueue) {
1243
+ const events = await this.memoryQueue.dequeue(count);
1244
+ return events.filter((e) => e.retries < this.config.maxRetries);
1245
+ }
1246
+ return [];
1247
+ }
1248
+ /**
1249
+ * Mark events as successfully sent (remove from queue)
1250
+ */
1251
+ async markSent(ids) {
1252
+ if (ids.length === 0) return;
1253
+ if (this.useIndexedDB && this.db) {
1254
+ await this.db.events.bulkDelete(ids);
1255
+ this.log(`Removed ${ids.length} sent events from queue`);
1256
+ } else if (this.memoryQueue) {
1257
+ await this.memoryQueue.delete(ids);
1258
+ this.log(`Removed ${ids.length} sent events from memory queue`);
1259
+ }
1260
+ }
1261
+ /**
1262
+ * Mark events as failed and increment retry count
1263
+ */
1264
+ async markFailed(ids) {
1265
+ if (ids.length === 0) return;
1266
+ const now = Date.now();
1267
+ if (this.useIndexedDB && this.db) {
1268
+ await this.db.transaction("rw", this.db.events, async () => {
1269
+ for (const id of ids) {
1270
+ const event = await this.db.events.get(id);
1271
+ if (event) {
1272
+ await this.db.events.update(id, {
1273
+ retries: event.retries + 1,
1274
+ lastAttempt: now
1275
+ });
1276
+ }
1277
+ }
1278
+ });
1279
+ this.log(`Marked ${ids.length} events as failed`);
1280
+ } else if (this.memoryQueue) {
1281
+ await this.memoryQueue.updateRetries(ids);
1282
+ this.log(`Marked ${ids.length} events as failed in memory queue`);
1283
+ }
1284
+ await this.removeExpiredRetries();
1285
+ }
1286
+ /**
1287
+ * Remove events that have exceeded max retry attempts
1288
+ */
1289
+ async removeExpiredRetries() {
1290
+ if (this.useIndexedDB && this.db) {
1291
+ const expiredIds = await this.db.events.where("retries").aboveOrEqual(this.config.maxRetries).primaryKeys();
1292
+ if (expiredIds.length > 0) {
1293
+ await this.db.events.bulkDelete(expiredIds);
1294
+ this.log(`Removed ${expiredIds.length} events that exceeded max retries`);
1295
+ }
1296
+ } else if (this.memoryQueue) {
1297
+ const expiredIds = await this.memoryQueue.getEventsExceedingRetries(
1298
+ this.config.maxRetries
1299
+ );
1300
+ if (expiredIds.length > 0) {
1301
+ await this.memoryQueue.delete(expiredIds);
1302
+ this.log(`Removed ${expiredIds.length} events that exceeded max retries`);
1303
+ }
1304
+ }
1305
+ }
1306
+ /**
1307
+ * Enforce the maximum queue size by removing oldest events
1308
+ */
1309
+ async enforceMaxQueueSize() {
1310
+ if (this.useIndexedDB && this.db) {
1311
+ const count = await this.db.events.count();
1312
+ if (count > this.config.maxQueueSize) {
1313
+ const excess = count - this.config.maxQueueSize;
1314
+ const oldestEvents = await this.db.events.orderBy("timestamp").limit(excess).primaryKeys();
1315
+ await this.db.events.bulkDelete(oldestEvents);
1316
+ this.log(`Removed ${excess} oldest events to enforce queue size limit`);
1317
+ }
1318
+ } else if (this.memoryQueue) {
1319
+ await this.memoryQueue.enforceMaxSize(this.config.maxQueueSize);
1320
+ }
1321
+ }
1322
+ /**
1323
+ * Get queue statistics
1324
+ */
1325
+ async getStats() {
1326
+ if (this.useIndexedDB && this.db) {
1327
+ const size = await this.db.events.count();
1328
+ const oldestEvent = await this.db.events.orderBy("timestamp").first().then((e) => e?.timestamp);
1329
+ return { size, oldestEvent, isFlushing: this.isFlushing };
1330
+ } else if (this.memoryQueue) {
1331
+ const stats = await this.memoryQueue.getStats();
1332
+ return { ...stats, isFlushing: this.isFlushing };
1333
+ }
1334
+ return { size: 0, isFlushing: this.isFlushing };
1335
+ }
1336
+ /**
1337
+ * Clear all events from the queue
1338
+ */
1339
+ async clear() {
1340
+ if (this.useIndexedDB && this.db) {
1341
+ await this.db.events.clear();
1342
+ this.log("Queue cleared (IndexedDB)");
1343
+ } else if (this.memoryQueue) {
1344
+ await this.memoryQueue.clear();
1345
+ this.log("Queue cleared (memory)");
1346
+ }
1347
+ }
1348
+ /**
1349
+ * Start the automatic flush timer
1350
+ */
1351
+ startFlushTimer() {
1352
+ if (this.flushTimer) return;
1353
+ this.flushTimer = setInterval(() => {
1354
+ this.flush().catch((err) => {
1355
+ this.log("Flush timer error", err);
1356
+ });
1357
+ }, this.config.flushInterval);
1358
+ if (isBrowser2()) {
1359
+ window.addEventListener("online", this.handleOnline);
1360
+ }
1361
+ this.log(`Flush timer started (interval: ${this.config.flushInterval}ms)`);
1362
+ }
1363
+ /**
1364
+ * Stop the automatic flush timer
1365
+ */
1366
+ stopFlushTimer() {
1367
+ if (this.flushTimer) {
1368
+ clearInterval(this.flushTimer);
1369
+ this.flushTimer = null;
1370
+ }
1371
+ if (isBrowser2()) {
1372
+ window.removeEventListener("online", this.handleOnline);
1373
+ }
1374
+ this.log("Flush timer stopped");
1375
+ }
1376
+ /**
1377
+ * Handle coming back online
1378
+ */
1379
+ handleOnline = () => {
1380
+ this.log("Browser came online, triggering flush");
1381
+ this.flush().catch((err) => {
1382
+ this.log("Online flush error", err);
1383
+ });
1384
+ };
1385
+ /**
1386
+ * Check if we're currently online
1387
+ */
1388
+ isOnline() {
1389
+ if (isBrowser2()) {
1390
+ return navigator.onLine;
1391
+ }
1392
+ return true;
1393
+ }
1394
+ /**
1395
+ * Manually trigger a flush of queued events
1396
+ */
1397
+ async flush() {
1398
+ if (this.isFlushing) {
1399
+ this.log("Flush already in progress, skipping");
1400
+ return;
1401
+ }
1402
+ if (!this.isOnline()) {
1403
+ this.log("Offline, skipping flush");
1404
+ return;
1405
+ }
1406
+ if (!this.sendEvents) {
1407
+ this.log("No send callback configured, skipping flush");
1408
+ return;
1409
+ }
1410
+ this.isFlushing = true;
1411
+ try {
1412
+ const stats = await this.getStats();
1413
+ if (stats.size === 0) {
1414
+ this.log("Queue is empty, nothing to flush");
1415
+ return;
1416
+ }
1417
+ this.log(`Flushing queue (${stats.size} events)`);
1418
+ let processed = 0;
1419
+ while (true) {
1420
+ const events = await this.dequeue(this.config.flushBatchSize);
1421
+ if (events.length === 0) break;
1422
+ this.log(`Sending batch of ${events.length} events`);
1423
+ try {
1424
+ const sentIds = await this.sendEvents(events);
1425
+ await this.markSent(sentIds);
1426
+ const failedIds = events.filter((e) => !sentIds.includes(e.id)).map((e) => e.id);
1427
+ if (failedIds.length > 0) {
1428
+ await this.markFailed(failedIds);
1429
+ }
1430
+ processed += sentIds.length;
1431
+ } catch (error) {
1432
+ const allIds = events.map((e) => e.id);
1433
+ await this.markFailed(allIds);
1434
+ this.log("Batch send failed", error);
1435
+ break;
1436
+ }
1437
+ }
1438
+ this.log(`Flush complete, sent ${processed} events`);
1439
+ } finally {
1440
+ this.isFlushing = false;
1441
+ }
1442
+ }
1443
+ /**
1444
+ * Close the database connection
1445
+ */
1446
+ async close() {
1447
+ this.stopFlushTimer();
1448
+ if (this.db) {
1449
+ this.db.close();
1450
+ this.log("Database connection closed");
1451
+ }
1452
+ }
1453
+ };
1454
+
1455
+ // src/plugins/page-view.ts
1456
+ var ANALYTICS_CHANNEL2 = "__analytics";
1457
+ var PageViewPlugin = class {
1458
+ name = "page-view";
1459
+ ctx;
1460
+ active = false;
1461
+ popstateListener = null;
1462
+ setup(ctx) {
1463
+ if (typeof window === "undefined") return false;
1464
+ this.ctx = ctx;
1465
+ this.active = true;
1466
+ Promise.resolve().then(() => {
1467
+ if (this.active) {
1468
+ this.trackPageView().catch((err) => ctx.log("Failed to track initial page view", err));
1469
+ }
1470
+ });
1471
+ const originalPushState = history.pushState.bind(history);
1472
+ history.pushState = (...args) => {
1473
+ originalPushState(...args);
1474
+ if (this.active) {
1475
+ this.trackPageView().catch((err) => ctx.log("Failed to track page view (pushState)", err));
1476
+ }
1477
+ };
1478
+ const originalReplaceState = history.replaceState.bind(history);
1479
+ history.replaceState = (...args) => {
1480
+ originalReplaceState(...args);
1481
+ };
1482
+ this.popstateListener = () => {
1483
+ if (this.active) {
1484
+ this.trackPageView().catch((err) => ctx.log("Failed to track page view (popstate)", err));
1485
+ }
1486
+ };
1487
+ window.addEventListener("popstate", this.popstateListener);
1488
+ ctx.log("Auto page view tracking enabled");
1489
+ }
1490
+ teardown() {
1491
+ this.active = false;
1492
+ if (this.popstateListener) {
1493
+ window.removeEventListener("popstate", this.popstateListener);
1494
+ this.popstateListener = null;
1495
+ }
1496
+ }
1497
+ get methods() {
1498
+ return {
1499
+ trackPageView: (options) => this.trackPageView(options)
1500
+ };
1501
+ }
1502
+ async trackPageView(options = {}) {
1503
+ const path = options.path ?? (typeof window !== "undefined" ? window.location.pathname : "");
1504
+ const title = options.title ?? (typeof document !== "undefined" ? document.title : "");
1505
+ const referrer = options.referrer ?? (typeof document !== "undefined" ? document.referrer : "");
1506
+ return this.ctx.track({
1507
+ channel: ANALYTICS_CHANNEL2,
1508
+ event: "screen_view",
1509
+ tags: {
1510
+ __path: path,
1511
+ __title: title,
1512
+ __referrer: referrer,
1513
+ ...getUtmParams(),
1514
+ ...options.tags ?? {}
1515
+ }
1516
+ });
1517
+ }
1518
+ };
1519
+
1520
+ // src/plugins/outbound-links.ts
1521
+ var ANALYTICS_CHANNEL3 = "__analytics";
1522
+ var OutboundLinksPlugin = class {
1523
+ name = "outbound-links";
1524
+ ctx;
1525
+ clickListener = null;
1526
+ keydownListener = null;
1527
+ setup(ctx) {
1528
+ if (typeof window === "undefined") return false;
1529
+ this.ctx = ctx;
1530
+ this.clickListener = (event) => {
1531
+ const link = event.target?.closest?.("a");
1532
+ if (link) {
1533
+ this.handleLinkClick(link);
1534
+ }
1535
+ };
1536
+ this.keydownListener = (event) => {
1537
+ if (event.key === "Enter" || event.key === " ") {
1538
+ const link = event.target?.closest?.("a");
1539
+ if (link) {
1540
+ this.handleLinkClick(link);
1541
+ }
1542
+ }
1543
+ };
1544
+ document.addEventListener("click", this.clickListener);
1545
+ document.addEventListener("keydown", this.keydownListener);
1546
+ ctx.log("Outbound link tracking enabled");
1547
+ }
1548
+ teardown() {
1549
+ if (this.clickListener) {
1550
+ document.removeEventListener("click", this.clickListener);
1551
+ this.clickListener = null;
1552
+ }
1553
+ if (this.keydownListener) {
1554
+ document.removeEventListener("keydown", this.keydownListener);
1555
+ this.keydownListener = null;
1556
+ }
1557
+ }
1558
+ get methods() {
1559
+ return {
1560
+ trackOutboundLink: (options) => this.trackOutboundLink(options)
1561
+ };
1562
+ }
1563
+ handleLinkClick(link) {
1564
+ if (!link.href) return;
1565
+ try {
1566
+ const linkUrl = new URL(link.href);
1567
+ if (linkUrl.protocol !== "http:" && linkUrl.protocol !== "https:") {
1568
+ return;
1569
+ }
1570
+ const currentHost = window.location.hostname;
1571
+ const linkHost = linkUrl.hostname;
1572
+ if (linkHost === currentHost) return;
1573
+ if (this.ctx.isSameRootDomain(currentHost, linkHost)) return;
1574
+ this.trackOutboundLink({
1575
+ url: link.href,
1576
+ text: link.textContent?.trim() || ""
1577
+ }).catch((err) => this.ctx.log("Failed to track outbound link", err));
1578
+ } catch {
1579
+ }
1580
+ }
1581
+ async trackOutboundLink(options) {
1582
+ return this.ctx.track({
1583
+ channel: ANALYTICS_CHANNEL3,
1584
+ event: "outbound_link",
1585
+ tags: {
1586
+ __url: options.url,
1587
+ __text: options.text || ""
1588
+ }
1589
+ });
1590
+ }
1591
+ };
1592
+
1593
+ // src/plugins/click-tracking.ts
1594
+ var ANALYTICS_CHANNEL4 = "__analytics";
1595
+ var ClickTrackingPlugin = class {
1596
+ name = "click-tracking";
1597
+ ctx;
1598
+ clickListener = null;
1599
+ setup(ctx) {
1600
+ if (typeof window === "undefined") return false;
1601
+ this.ctx = ctx;
1602
+ this.clickListener = (event) => {
1603
+ const target = event.target;
1604
+ const annotated = target?.closest?.("[data-kb-track-click]");
1605
+ if (annotated) {
1606
+ const eventName = annotated.getAttribute("data-kb-track-click");
1607
+ if (eventName) {
1608
+ const channel = annotated.getAttribute("data-kb-click-channel") || "engagement";
1609
+ ctx.track({
1610
+ channel,
1611
+ event: eventName,
1612
+ tags: {
1613
+ __path: window.location.pathname
1614
+ }
1615
+ }).catch((err) => ctx.log("Failed to track data-attribute click", err));
1616
+ return;
1617
+ }
1618
+ }
1619
+ const element = ctx.findClickableElement(event);
1620
+ if (!element) return;
1621
+ if (ctx.config.autoTrackOutboundLinks !== false) {
1622
+ const elHref = element.href || element.getAttribute("href") || "";
1623
+ if (elHref) {
1624
+ try {
1625
+ const linkUrl = new URL(elHref, window.location.origin);
1626
+ if ((linkUrl.protocol === "http:" || linkUrl.protocol === "https:") && linkUrl.hostname !== window.location.hostname && !ctx.isSameRootDomain(window.location.hostname, linkUrl.hostname)) {
1627
+ return;
1628
+ }
1629
+ } catch {
1630
+ }
1631
+ }
1632
+ }
1633
+ const tag = element.tagName.toLowerCase();
1634
+ const id = element.id || "";
1635
+ const className = element.className && typeof element.className === "string" ? element.className : "";
1636
+ const text = (element.textContent || "").trim().slice(0, 100);
1637
+ const href = element.href || element.getAttribute("href") || "";
1638
+ const path = window.location.pathname;
1639
+ this.trackClick({ __tag: tag, __id: id, __class: className, __text: text, __href: href, __path: path }).catch(
1640
+ (err) => ctx.log("Failed to track click", err)
1641
+ );
1642
+ };
1643
+ document.addEventListener("click", this.clickListener);
1644
+ ctx.log("Click tracking enabled");
1645
+ }
1646
+ teardown() {
1647
+ if (this.clickListener) {
1648
+ document.removeEventListener("click", this.clickListener);
1649
+ this.clickListener = null;
1650
+ }
1651
+ }
1652
+ get methods() {
1653
+ return {
1654
+ trackClick: (tags) => this.trackClick(tags)
1655
+ };
1656
+ }
1657
+ async trackClick(tags) {
1658
+ return this.ctx.track({
1659
+ channel: ANALYTICS_CHANNEL4,
1660
+ event: "click",
1661
+ tags
1662
+ });
1663
+ }
1664
+ };
1665
+
1666
+ // src/plugins/scroll-depth.ts
1667
+ var ANALYTICS_CHANNEL5 = "__analytics";
1668
+ var ScrollDepthPlugin = class {
1669
+ name = "scroll-depth";
1670
+ ctx;
1671
+ active = false;
1672
+ maxScrollDepth = 0;
1673
+ scrollListener = null;
1674
+ beforeUnloadListener = null;
1675
+ popstateListener = null;
1676
+ scrollRafScheduled = false;
1677
+ setup(ctx) {
1678
+ if (typeof window === "undefined") return false;
1679
+ this.ctx = ctx;
1680
+ this.active = true;
1681
+ this.scrollListener = () => {
1682
+ if (this.scrollRafScheduled) return;
1683
+ this.scrollRafScheduled = true;
1684
+ requestAnimationFrame(() => {
1685
+ this.scrollRafScheduled = false;
1686
+ const scrollTop = window.scrollY || document.documentElement.scrollTop;
1687
+ const viewportHeight = window.innerHeight;
1688
+ const documentHeight = Math.max(
1689
+ document.body.scrollHeight,
1690
+ document.documentElement.scrollHeight
1691
+ );
1692
+ if (documentHeight <= 0) return;
1693
+ const depth = Math.min(100, Math.round((scrollTop + viewportHeight) / documentHeight * 100));
1694
+ if (depth > this.maxScrollDepth) {
1695
+ this.maxScrollDepth = depth;
1696
+ }
1697
+ });
1698
+ };
1699
+ this.beforeUnloadListener = () => {
1700
+ this.flushScrollDepth();
1701
+ };
1702
+ window.addEventListener("scroll", this.scrollListener, { passive: true });
1703
+ window.addEventListener("beforeunload", this.beforeUnloadListener);
1704
+ const originalPushState = history.pushState;
1705
+ const self = this;
1706
+ history.pushState = function(...args) {
1707
+ if (self.active) self.flushScrollDepth();
1708
+ return originalPushState.apply(this, args);
1709
+ };
1710
+ this.popstateListener = () => {
1711
+ if (this.active) this.flushScrollDepth();
1712
+ };
1713
+ window.addEventListener("popstate", this.popstateListener);
1714
+ ctx.log("Scroll depth tracking enabled");
1715
+ }
1716
+ teardown() {
1717
+ this.active = false;
1718
+ this.flushScrollDepth();
1719
+ if (this.scrollListener) {
1720
+ window.removeEventListener("scroll", this.scrollListener);
1721
+ this.scrollListener = null;
1722
+ }
1723
+ if (this.beforeUnloadListener) {
1724
+ window.removeEventListener("beforeunload", this.beforeUnloadListener);
1725
+ this.beforeUnloadListener = null;
1726
+ }
1727
+ if (this.popstateListener) {
1728
+ window.removeEventListener("popstate", this.popstateListener);
1729
+ this.popstateListener = null;
1730
+ }
1731
+ }
1732
+ flushScrollDepth() {
1733
+ if (this.maxScrollDepth > 0) {
1734
+ const path = typeof window !== "undefined" ? window.location.pathname : "";
1735
+ this.ctx.track({
1736
+ channel: ANALYTICS_CHANNEL5,
1737
+ event: "scroll_depth",
1738
+ tags: {
1739
+ __depth: this.maxScrollDepth,
1740
+ __path: path
1741
+ }
1742
+ }).catch((err) => this.ctx.log("Failed to track scroll depth", err));
1743
+ this.maxScrollDepth = 0;
1744
+ }
1745
+ }
1746
+ };
1747
+
1748
+ // src/plugins/visibility.ts
1749
+ var VisibilityPlugin = class {
1750
+ name = "visibility";
1751
+ ctx;
1752
+ active = false;
1753
+ visibilityObservers = /* @__PURE__ */ new Map();
1754
+ visibilityMutationObserver = null;
1755
+ visibilityData = /* @__PURE__ */ new Map();
1756
+ beforeUnloadListener = null;
1757
+ popstateListener = null;
1758
+ setup(ctx) {
1759
+ if (typeof window === "undefined") return false;
1760
+ if (typeof IntersectionObserver === "undefined" || typeof MutationObserver === "undefined") return false;
1761
+ this.ctx = ctx;
1762
+ this.active = true;
1763
+ this.scanForVisibilityElements();
1764
+ this.visibilityMutationObserver = new MutationObserver((mutations) => {
1765
+ for (const mutation of mutations) {
1766
+ for (const node of Array.from(mutation.addedNodes)) {
1767
+ if (node instanceof Element) {
1768
+ this.observeVisibilityElement(node);
1769
+ for (const el of Array.from(node.querySelectorAll("[data-kb-track-visibility]"))) {
1770
+ this.observeVisibilityElement(el);
1771
+ }
1772
+ }
1773
+ }
1774
+ for (const node of Array.from(mutation.removedNodes)) {
1775
+ if (node instanceof Element) {
1776
+ this.flushVisibilityForElement(node);
1777
+ for (const el of Array.from(node.querySelectorAll("[data-kb-track-visibility]"))) {
1778
+ this.flushVisibilityForElement(el);
1779
+ }
1780
+ }
1781
+ }
1782
+ }
1783
+ });
1784
+ this.visibilityMutationObserver.observe(document.body, {
1785
+ childList: true,
1786
+ subtree: true
1787
+ });
1788
+ this.beforeUnloadListener = () => {
1789
+ this.flushAllVisibilityEvents();
1790
+ };
1791
+ window.addEventListener("beforeunload", this.beforeUnloadListener);
1792
+ const originalPushState = history.pushState;
1793
+ const self = this;
1794
+ history.pushState = function(...args) {
1795
+ if (self.active) self.flushAllVisibilityEvents();
1796
+ return originalPushState.apply(this, args);
1797
+ };
1798
+ this.popstateListener = () => {
1799
+ if (this.active) this.flushAllVisibilityEvents();
1800
+ };
1801
+ window.addEventListener("popstate", this.popstateListener);
1802
+ ctx.log("Visibility tracking enabled");
1803
+ }
1804
+ teardown() {
1805
+ this.active = false;
1806
+ this.flushAllVisibilityEvents();
1807
+ for (const observer of this.visibilityObservers.values()) {
1808
+ observer.disconnect();
1809
+ }
1810
+ this.visibilityObservers.clear();
1811
+ if (this.visibilityMutationObserver) {
1812
+ this.visibilityMutationObserver.disconnect();
1813
+ this.visibilityMutationObserver = null;
1814
+ }
1815
+ if (this.beforeUnloadListener) {
1816
+ window.removeEventListener("beforeunload", this.beforeUnloadListener);
1817
+ this.beforeUnloadListener = null;
1818
+ }
1819
+ if (this.popstateListener) {
1820
+ window.removeEventListener("popstate", this.popstateListener);
1821
+ this.popstateListener = null;
1822
+ }
1823
+ this.visibilityData.clear();
1824
+ }
1825
+ scanForVisibilityElements() {
1826
+ for (const el of Array.from(document.querySelectorAll("[data-kb-track-visibility]"))) {
1827
+ this.observeVisibilityElement(el);
1828
+ }
1829
+ }
1830
+ observeVisibilityElement(el) {
1831
+ const eventName = el.getAttribute("data-kb-track-visibility");
1832
+ if (!eventName || this.visibilityData.has(el)) return;
1833
+ const channel = el.getAttribute("data-kb-visibility-channel") || "engagement";
1834
+ const threshold = parseFloat(el.getAttribute("data-kb-visibility-threshold") || "0.5");
1835
+ const clampedThreshold = Math.max(0, Math.min(1, isNaN(threshold) ? 0.5 : threshold));
1836
+ this.visibilityData.set(el, {
1837
+ visibleSince: null,
1838
+ totalMs: 0,
1839
+ event: eventName,
1840
+ channel
1841
+ });
1842
+ const observer = this.getOrCreateObserver(clampedThreshold);
1843
+ observer.observe(el);
1844
+ }
1845
+ getOrCreateObserver(threshold) {
1846
+ const key = Math.round(threshold * 100);
1847
+ let observer = this.visibilityObservers.get(key);
1848
+ if (observer) return observer;
1849
+ observer = new IntersectionObserver(
1850
+ (entries) => {
1851
+ const now = Date.now();
1852
+ for (const entry of entries) {
1853
+ const data = this.visibilityData.get(entry.target);
1854
+ if (!data) continue;
1855
+ if (entry.isIntersecting) {
1856
+ data.visibleSince = now;
1857
+ } else if (data.visibleSince !== null) {
1858
+ data.totalMs += now - data.visibleSince;
1859
+ data.visibleSince = null;
1860
+ }
1861
+ }
1862
+ },
1863
+ { threshold }
1864
+ );
1865
+ this.visibilityObservers.set(key, observer);
1866
+ return observer;
1867
+ }
1868
+ flushVisibilityForElement(el) {
1869
+ const data = this.visibilityData.get(el);
1870
+ if (!data) return;
1871
+ if (data.visibleSince !== null) {
1872
+ data.totalMs += Date.now() - data.visibleSince;
1873
+ data.visibleSince = null;
1874
+ }
1875
+ if (data.totalMs > 0) {
1876
+ const durationMs = Math.round(data.totalMs);
1877
+ const durationSeconds = Math.round(durationMs / 1e3);
1878
+ this.ctx.track({
1879
+ channel: data.channel,
1880
+ event: "element_visible",
1881
+ tags: {
1882
+ __element_name: data.event,
1883
+ __duration_seconds: durationSeconds,
1884
+ __duration_ms: durationMs
1885
+ }
1886
+ }).catch((err) => this.ctx.log("Failed to track visibility event", err));
1887
+ }
1888
+ for (const observer of this.visibilityObservers.values()) {
1889
+ observer.unobserve(el);
1890
+ }
1891
+ this.visibilityData.delete(el);
1892
+ }
1893
+ flushAllVisibilityEvents() {
1894
+ for (const [, data] of this.visibilityData.entries()) {
1895
+ if (data.visibleSince !== null) {
1896
+ data.totalMs += Date.now() - data.visibleSince;
1897
+ data.visibleSince = null;
1898
+ }
1899
+ if (data.totalMs > 0) {
1900
+ const durationMs = Math.round(data.totalMs);
1901
+ const durationSeconds = Math.round(durationMs / 1e3);
1902
+ this.ctx.track({
1903
+ channel: data.channel,
1904
+ event: "element_visible",
1905
+ tags: {
1906
+ element_name: data.event,
1907
+ duration_seconds: durationSeconds,
1908
+ duration_ms: durationMs
1909
+ }
1910
+ }).catch((err) => this.ctx.log("Failed to track visibility event", err));
1911
+ }
1912
+ data.totalMs = 0;
1913
+ data.visibleSince = null;
1914
+ }
1915
+ }
1916
+ };
1917
+
1918
+ // src/plugins/web-vitals.ts
1919
+ import { onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals";
1920
+ var ANALYTICS_CHANNEL6 = "__analytics";
1921
+ var WebVitalsPlugin = class {
1922
+ name = "web-vitals";
1923
+ ctx;
1924
+ sent = false;
1925
+ timeout = null;
1926
+ beforeUnloadListener = null;
1927
+ data = {
1928
+ lcp: null,
1929
+ cls: null,
1930
+ inp: null,
1931
+ fcp: null,
1932
+ ttfb: null
1933
+ };
1934
+ setup(ctx) {
1935
+ if (typeof window === "undefined") return false;
1936
+ this.ctx = ctx;
1937
+ const checkAndSend = () => {
1938
+ const { lcp, cls, inp, fcp, ttfb } = this.data;
1939
+ if (lcp !== null && cls !== null && inp !== null && fcp !== null && ttfb !== null) {
1940
+ this.sendWebVitals();
1941
+ }
1942
+ };
1943
+ onLCP((metric) => {
1944
+ this.data.lcp = metric.value;
1945
+ ctx.log("Web Vital collected", { name: "LCP", value: metric.value });
1946
+ checkAndSend();
1947
+ });
1948
+ onCLS((metric) => {
1949
+ this.data.cls = metric.value;
1950
+ ctx.log("Web Vital collected", { name: "CLS", value: metric.value });
1951
+ checkAndSend();
1952
+ });
1953
+ onINP((metric) => {
1954
+ this.data.inp = metric.value;
1955
+ ctx.log("Web Vital collected", { name: "INP", value: metric.value });
1956
+ checkAndSend();
1957
+ });
1958
+ onFCP((metric) => {
1959
+ this.data.fcp = metric.value;
1960
+ ctx.log("Web Vital collected", { name: "FCP", value: metric.value });
1961
+ checkAndSend();
1962
+ });
1963
+ onTTFB((metric) => {
1964
+ this.data.ttfb = metric.value;
1965
+ ctx.log("Web Vital collected", { name: "TTFB", value: metric.value });
1966
+ checkAndSend();
1967
+ });
1968
+ this.timeout = setTimeout(() => {
1969
+ this.timeout = null;
1970
+ this.sendWebVitals();
1971
+ }, 3e4);
1972
+ this.beforeUnloadListener = () => {
1973
+ this.sendWebVitals();
1974
+ };
1975
+ window.addEventListener("beforeunload", this.beforeUnloadListener);
1976
+ ctx.log("Web Vitals tracking enabled");
1977
+ }
1978
+ teardown() {
1979
+ if (this.timeout !== null) {
1980
+ clearTimeout(this.timeout);
1981
+ this.timeout = null;
1982
+ }
1983
+ this.sendWebVitals();
1984
+ if (this.beforeUnloadListener) {
1985
+ window.removeEventListener("beforeunload", this.beforeUnloadListener);
1986
+ this.beforeUnloadListener = null;
1987
+ }
1988
+ }
1989
+ sendWebVitals() {
1990
+ if (this.sent) return;
1991
+ const { lcp, cls, inp, fcp, ttfb } = this.data;
1992
+ if (lcp === null && cls === null && inp === null && fcp === null && ttfb === null) return;
1993
+ this.sent = true;
1994
+ if (this.timeout !== null) {
1995
+ clearTimeout(this.timeout);
1996
+ this.timeout = null;
1997
+ }
1998
+ const tags = {};
1999
+ if (lcp !== null) tags.__lcp = lcp;
2000
+ if (cls !== null) tags.__cls = cls;
2001
+ if (inp !== null) tags.__inp = inp;
2002
+ if (fcp !== null) tags.__fcp = fcp;
2003
+ if (ttfb !== null) tags.__ttfb = ttfb;
2004
+ this.ctx.log("Sending Web Vitals", tags);
2005
+ this.ctx.track({
2006
+ channel: ANALYTICS_CHANNEL6,
2007
+ event: "web_vitals",
2008
+ tags
2009
+ }).catch((err) => this.ctx.log("Failed to track web vitals", err));
2010
+ }
2011
+ };
2012
+
2013
+ // src/plugins/frustration.ts
2014
+ var ANALYTICS_CHANNEL7 = "__analytics";
2015
+ var RAGE_CLICK_THRESHOLD = 3;
2016
+ var RAGE_CLICK_WINDOW_MS = 1e3;
2017
+ var RAGE_CLICK_RADIUS_PX = 30;
2018
+ var DEAD_CLICK_TIMEOUT_MS = 1e3;
2019
+ var FrustrationPlugin = class {
2020
+ name = "frustration";
2021
+ ctx;
2022
+ rageClickBuffer = [];
2023
+ deadClickObserver = null;
2024
+ deadClickTimeout = null;
2025
+ clickListener = null;
2026
+ setup(ctx) {
2027
+ if (typeof window === "undefined") return false;
2028
+ this.ctx = ctx;
2029
+ this.clickListener = (event) => {
2030
+ const target = event.target;
2031
+ if (!target) return;
2032
+ const now = Date.now();
2033
+ this.rageClickBuffer.push({ time: now, x: event.clientX, y: event.clientY, target });
2034
+ this.rageClickBuffer = this.rageClickBuffer.filter(
2035
+ (c) => now - c.time < RAGE_CLICK_WINDOW_MS
2036
+ );
2037
+ if (this.rageClickBuffer.length >= RAGE_CLICK_THRESHOLD) {
2038
+ const first = this.rageClickBuffer[0];
2039
+ const allNearby = this.rageClickBuffer.every(
2040
+ (c) => Math.hypot(c.x - first.x, c.y - first.y) < RAGE_CLICK_RADIUS_PX
2041
+ );
2042
+ if (allNearby) {
2043
+ const element = findClickableElement(event) || target;
2044
+ const clickCount = this.rageClickBuffer.length;
2045
+ this.rageClickBuffer = [];
2046
+ ctx.track({
2047
+ channel: ANALYTICS_CHANNEL7,
2048
+ event: "rage_click",
2049
+ tags: {
2050
+ __path: window.location.pathname,
2051
+ __tag: element.tagName.toLowerCase(),
2052
+ __id: element.id || "",
2053
+ __class: element.className && typeof element.className === "string" ? element.className : "",
2054
+ __text: (element.textContent || "").trim().slice(0, 100),
2055
+ __selector: buildCssSelector(element),
2056
+ __click_count: clickCount
2057
+ }
2058
+ }).catch((err) => ctx.log("Failed to track rage click", err));
2059
+ return;
2060
+ }
2061
+ }
2062
+ const clickedElement = findClickableElement(event);
2063
+ if (!clickedElement) return;
2064
+ if (target.closest?.("a[href]")) return;
2065
+ if (clickedElement.tagName === "SELECT" || target.closest?.("select")) return;
2066
+ if (this.deadClickTimeout !== null) {
2067
+ clearTimeout(this.deadClickTimeout);
2068
+ this.deadClickTimeout = null;
2069
+ }
2070
+ if (this.deadClickObserver) {
2071
+ this.deadClickObserver.disconnect();
2072
+ }
2073
+ let mutationDetected = false;
2074
+ this.deadClickObserver = new MutationObserver(() => {
2075
+ mutationDetected = true;
2076
+ });
2077
+ this.deadClickObserver.observe(document.body, {
2078
+ childList: true,
2079
+ subtree: true,
2080
+ attributes: true,
2081
+ characterData: true
2082
+ });
2083
+ this.deadClickTimeout = setTimeout(() => {
2084
+ if (this.deadClickObserver) {
2085
+ this.deadClickObserver.disconnect();
2086
+ this.deadClickObserver = null;
2087
+ }
2088
+ if (!mutationDetected) {
2089
+ ctx.track({
2090
+ channel: ANALYTICS_CHANNEL7,
2091
+ event: "dead_click",
2092
+ tags: {
2093
+ __path: window.location.pathname,
2094
+ __tag: clickedElement.tagName.toLowerCase(),
2095
+ __id: clickedElement.id || "",
2096
+ __class: clickedElement.className && typeof clickedElement.className === "string" ? clickedElement.className : "",
2097
+ __text: (clickedElement.textContent || "").trim().slice(0, 100),
2098
+ __selector: buildCssSelector(clickedElement)
2099
+ }
2100
+ }).catch((err) => ctx.log("Failed to track dead click", err));
2101
+ }
2102
+ }, DEAD_CLICK_TIMEOUT_MS);
2103
+ };
2104
+ document.addEventListener("click", this.clickListener, true);
2105
+ ctx.log("Frustration signal detection enabled");
2106
+ }
2107
+ teardown() {
2108
+ if (this.clickListener) {
2109
+ document.removeEventListener("click", this.clickListener, true);
2110
+ this.clickListener = null;
2111
+ }
2112
+ if (this.deadClickObserver) {
2113
+ this.deadClickObserver.disconnect();
2114
+ this.deadClickObserver = null;
2115
+ }
2116
+ if (this.deadClickTimeout !== null) {
2117
+ clearTimeout(this.deadClickTimeout);
2118
+ this.deadClickTimeout = null;
2119
+ }
2120
+ this.rageClickBuffer = [];
2121
+ }
2122
+ };
2123
+
2124
+ // src/plugins/defaults.ts
2125
+ function createDefaultPlugins(config) {
2126
+ const plugins = [];
2127
+ if (config?.autoTrackPageViews !== false) {
2128
+ plugins.push(new PageViewPlugin());
2129
+ }
2130
+ if (config?.autoTrackOutboundLinks !== false) {
2131
+ plugins.push(new OutboundLinksPlugin());
2132
+ }
2133
+ if (config?.autoTrackClicks !== false) {
2134
+ plugins.push(new ClickTrackingPlugin());
2135
+ }
2136
+ if (config?.autoTrackScrollDepth !== false) {
2137
+ plugins.push(new ScrollDepthPlugin());
2138
+ }
2139
+ if (config?.autoTrackVisibility !== false) {
2140
+ plugins.push(new VisibilityPlugin());
2141
+ }
2142
+ if (config?.autoTrackWebVitals === true) {
2143
+ plugins.push(new WebVitalsPlugin());
2144
+ }
2145
+ if (config?.autoDetectFrustration !== false) {
2146
+ plugins.push(new FrustrationPlugin());
2147
+ }
2148
+ return plugins;
2149
+ }
2150
+
2151
+ // src/client.ts
2152
+ var _instance = null;
2153
+ function init(config) {
2154
+ if (_instance) {
2155
+ _instance.shutdown();
2156
+ }
2157
+ _instance = new KitbaseAnalytics2(config);
2158
+ return _instance;
2159
+ }
2160
+ function getInstance() {
2161
+ return _instance;
2162
+ }
2163
+ var KitbaseAnalytics2 = class extends KitbaseAnalytics {
2164
+ // Offline queue
2165
+ queue = null;
2166
+ offlineEnabled;
2167
+ constructor(config) {
2168
+ super(config, createDefaultPlugins(config.analytics));
2169
+ this.offlineEnabled = config.offline?.enabled ?? false;
2170
+ if (this.offlineEnabled) {
2171
+ this.queue = new EventQueue(config.offline);
2172
+ this.queue.setDebugMode(this.debugMode, this.log.bind(this));
2173
+ this.queue.setSendCallback(this.sendQueuedEvents.bind(this));
2174
+ this.queue.startFlushTimer();
2175
+ this.log("Offline queueing enabled", {
2176
+ storageType: this.queue.getStorageType()
2177
+ });
2178
+ }
2179
+ }
2180
+ // ============================================================
2181
+ // Debug Mode Override
2182
+ // ============================================================
2183
+ /**
2184
+ * Enable or disable debug mode
2185
+ * When enabled, all SDK operations are logged to the console
2186
+ *
2187
+ * @param enabled - Whether to enable debug mode
2188
+ *
2189
+ * @example
2190
+ * ```typescript
2191
+ * kitbase.setDebugMode(true);
2192
+ * // All events and operations will now be logged
2193
+ * ```
2194
+ */
2195
+ setDebugMode(enabled) {
2196
+ super.setDebugMode(enabled);
2197
+ if (this.queue) {
2198
+ this.queue.setDebugMode(enabled, this.log.bind(this));
2199
+ }
2200
+ }
2201
+ // ============================================================
2202
+ // Offline Queue
2203
+ // ============================================================
2204
+ /**
2205
+ * Get offline queue statistics
2206
+ *
2207
+ * @returns Queue statistics including size and flush status
2208
+ *
2209
+ * @example
2210
+ * ```typescript
2211
+ * const stats = await kitbase.getQueueStats();
2212
+ * console.log(stats); // { size: 5, isFlushing: false }
2213
+ * ```
2214
+ */
2215
+ async getQueueStats() {
2216
+ if (!this.queue) return null;
2217
+ return this.queue.getStats();
2218
+ }
2219
+ /**
2220
+ * Manually flush the offline queue
2221
+ * Events are automatically flushed on interval and when coming back online,
2222
+ * but this method can be used to trigger an immediate flush
2223
+ *
2224
+ * @example
2225
+ * ```typescript
2226
+ * await kitbase.flushQueue();
2227
+ * ```
2228
+ */
2229
+ async flushQueue() {
2230
+ if (!this.queue) return;
2231
+ await this.queue.flush();
2232
+ }
2233
+ /**
2234
+ * Clear all events from the offline queue
2235
+ *
2236
+ * @example
2237
+ * ```typescript
2238
+ * await kitbase.clearQueue();
2239
+ * ```
2240
+ */
2241
+ async clearQueue() {
2242
+ if (!this.queue) return;
2243
+ await this.queue.clear();
2244
+ }
2245
+ /**
2246
+ * Callback for the queue to send batched events via the batch endpoint.
2247
+ * Sends all events in a single HTTP request instead of individual POSTs.
2248
+ */
2249
+ async sendQueuedEvents(events) {
2250
+ try {
2251
+ await this.sendRequest("/sdk/v1/logs/batch", {
2252
+ events: events.map((e) => e.payload)
2253
+ });
2254
+ return events.map((e) => e.id);
2255
+ } catch (error) {
2256
+ this.log("Batch send failed", { count: events.length, error });
2257
+ return [];
2258
+ }
2259
+ }
2260
+ // ============================================================
2261
+ // Track Event Override
2262
+ // ============================================================
2263
+ /**
2264
+ * Track an event
2265
+ *
2266
+ * When offline queueing is enabled, events are always written to the local
2267
+ * database first (write-ahead), then sent to the server. This ensures no
2268
+ * events are lost if the browser crashes or the network fails.
2269
+ *
2270
+ * @param options - Event tracking options
2271
+ * @returns Promise resolving to the track response, or void if tracking is blocked
2272
+ * @throws {ValidationError} When required fields are missing
2273
+ * @throws {AuthenticationError} When the API key is invalid (only when offline disabled)
2274
+ * @throws {ApiError} When the API returns an error (only when offline disabled)
2275
+ * @throws {TimeoutError} When the request times out (only when offline disabled)
2276
+ */
2277
+ async track(options) {
2278
+ this.validateTrackOptions(options);
2279
+ if (this.isBotBlockingActive()) {
2280
+ this.log("Event skipped - bot detected", { event: options.event });
2281
+ return;
2282
+ }
2283
+ let duration;
2284
+ const startTime = this.timedEvents.get(options.event);
2285
+ if (startTime !== void 0) {
2286
+ duration = (Date.now() - startTime) / 1e3;
2287
+ this.timedEvents.delete(options.event);
2288
+ this.log("Timer stopped", { event: options.event, duration });
2289
+ }
2290
+ const mergedTags = {
2291
+ ...this.superProperties,
2292
+ ...options.tags ?? {},
2293
+ ...duration !== void 0 ? { $duration: duration } : {}
2294
+ };
2295
+ const payload = {
2296
+ channel: options.channel,
2297
+ event: options.event,
2298
+ client_timestamp: Date.now(),
2299
+ client_session_id: this.getClientSessionId(),
2300
+ ...options.user_id && { user_id: options.user_id },
2301
+ ...options.icon && { icon: options.icon },
2302
+ ...options.notify !== void 0 && { notify: options.notify },
2303
+ ...options.description && { description: options.description },
2304
+ ...Object.keys(mergedTags).length > 0 && { tags: mergedTags }
2305
+ };
2306
+ this.log("Track", { event: options.event, payload });
2307
+ if (this.queue) {
2308
+ await this.queue.enqueue(payload);
2309
+ this.log("Event persisted to queue");
2310
+ this.queue.flush().catch((err) => {
2311
+ this.log("Background flush failed", err);
2312
+ });
2313
+ return {
2314
+ id: `queued-${Date.now()}`,
2315
+ event: options.event,
2316
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2317
+ };
2318
+ }
2319
+ const response = await this.sendRequest("/sdk/v1/logs", payload);
2320
+ this.log("Event sent successfully", { id: response.id });
2321
+ return response;
2322
+ }
2323
+ validateTrackOptions(options) {
2324
+ if (!options.event) {
2325
+ throw new ValidationError("Event is required", "event");
2326
+ }
2327
+ }
2328
+ // ============================================================
2329
+ // Cleanup
2330
+ // ============================================================
2331
+ /**
2332
+ * Shutdown the client and cleanup resources
2333
+ * Call this when you're done using the client to stop timers and close connections
2334
+ *
2335
+ * @example
2336
+ * ```typescript
2337
+ * await kitbase.shutdown();
2338
+ * ```
2339
+ */
2340
+ async shutdown() {
2341
+ if (this.queue) {
2342
+ await this.queue.flush();
2343
+ }
2344
+ super.shutdown();
2345
+ if (this.queue) {
2346
+ await this.queue.flush();
2347
+ await this.queue.close();
2348
+ this.queue = null;
2349
+ }
2350
+ }
2351
+ };
2352
+ export {
2353
+ ApiError,
2354
+ AuthenticationError,
2355
+ ClickTrackingPlugin,
2356
+ FrustrationPlugin,
2357
+ KitbaseAnalytics2 as KitbaseAnalytics,
2358
+ KitbaseError,
2359
+ OutboundLinksPlugin,
2360
+ PageViewPlugin,
2361
+ ScrollDepthPlugin,
2362
+ TimeoutError,
2363
+ ValidationError,
2364
+ VisibilityPlugin,
2365
+ WebVitalsPlugin,
2366
+ createDefaultPlugins,
2367
+ detectBot,
2368
+ getInstance,
2369
+ getUserAgent,
2370
+ init,
2371
+ isBot,
2372
+ isUserAgentBot
2373
+ };
2374
+ //# sourceMappingURL=index.js.map