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