@kitbase/events 0.1.3 → 0.1.4

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 CHANGED
@@ -35,7 +35,11 @@ __export(index_exports, {
35
35
  Kitbase: () => Kitbase,
36
36
  KitbaseError: () => KitbaseError,
37
37
  TimeoutError: () => TimeoutError,
38
- ValidationError: () => ValidationError
38
+ ValidationError: () => ValidationError,
39
+ detectBot: () => detectBot,
40
+ getUserAgent: () => getUserAgent,
41
+ isBot: () => isBot,
42
+ isUserAgentBot: () => isUserAgentBot
39
43
  });
40
44
  module.exports = __toCommonJS(index_exports);
41
45
 
@@ -449,6 +453,269 @@ var EventQueue = class {
449
453
  }
450
454
  };
451
455
 
456
+ // src/botDetection.ts
457
+ var AUTOMATION_GLOBALS = [
458
+ "__webdriver_evaluate",
459
+ "__selenium_evaluate",
460
+ "__webdriver_script_function",
461
+ "__webdriver_unwrapped",
462
+ "__fxdriver_evaluate",
463
+ "__driver_evaluate",
464
+ "_Selenium_IDE_Recorder",
465
+ "_selenium",
466
+ "calledSelenium",
467
+ "$cdc_asdjflasutopfhvcZLmcfl_",
468
+ // Chrome DevTools Protocol marker
469
+ "__nightmare",
470
+ "domAutomation",
471
+ "domAutomationController"
472
+ ];
473
+ var HEADLESS_PATTERNS = [
474
+ "headlesschrome",
475
+ "phantomjs",
476
+ "selenium",
477
+ "webdriver",
478
+ "puppeteer",
479
+ "playwright"
480
+ ];
481
+ var HTTP_CLIENT_PATTERNS = [
482
+ "python",
483
+ "curl",
484
+ "wget",
485
+ "java/",
486
+ "go-http",
487
+ "node-fetch",
488
+ "axios",
489
+ "postman",
490
+ "insomnia",
491
+ "httpie",
492
+ "ruby",
493
+ "perl",
494
+ "scrapy",
495
+ "bot",
496
+ "spider",
497
+ "crawler",
498
+ "slurp",
499
+ "googlebot",
500
+ "bingbot",
501
+ "yandexbot",
502
+ "baiduspider",
503
+ "duckduckbot",
504
+ "facebookexternalhit",
505
+ "twitterbot",
506
+ "linkedinbot",
507
+ "whatsapp",
508
+ "telegram",
509
+ "discord",
510
+ "slack"
511
+ ];
512
+ var DEFAULT_BOT_DETECTION_CONFIG = {
513
+ enabled: true,
514
+ checkWebdriver: true,
515
+ checkPhantomJS: true,
516
+ checkNightmare: true,
517
+ checkAutomationGlobals: true,
518
+ checkDocumentAttributes: true,
519
+ checkUserAgentHeadless: true,
520
+ checkUserAgentHttpClient: true
521
+ };
522
+ function isBrowser2() {
523
+ return typeof window !== "undefined" && typeof document !== "undefined";
524
+ }
525
+ function getWindowProperty(key) {
526
+ try {
527
+ return window[key];
528
+ } catch {
529
+ return void 0;
530
+ }
531
+ }
532
+ function checkWebdriver() {
533
+ if (!isBrowser2()) return false;
534
+ try {
535
+ return window.navigator?.webdriver === true;
536
+ } catch {
537
+ return false;
538
+ }
539
+ }
540
+ function checkPhantomJS() {
541
+ if (!isBrowser2()) return false;
542
+ try {
543
+ return !!(getWindowProperty("callPhantom") || getWindowProperty("_phantom") || getWindowProperty("phantom"));
544
+ } catch {
545
+ return false;
546
+ }
547
+ }
548
+ function checkNightmare() {
549
+ if (!isBrowser2()) return false;
550
+ try {
551
+ return !!getWindowProperty("__nightmare");
552
+ } catch {
553
+ return false;
554
+ }
555
+ }
556
+ function checkAutomationGlobals() {
557
+ if (!isBrowser2()) return false;
558
+ try {
559
+ for (const global of AUTOMATION_GLOBALS) {
560
+ if (getWindowProperty(global) !== void 0) {
561
+ return true;
562
+ }
563
+ }
564
+ return false;
565
+ } catch {
566
+ return false;
567
+ }
568
+ }
569
+ function checkDocumentAttributes() {
570
+ if (!isBrowser2()) return false;
571
+ try {
572
+ const docEl = document.documentElement;
573
+ if (!docEl) return false;
574
+ return !!(docEl.getAttribute("webdriver") || docEl.getAttribute("selenium") || docEl.getAttribute("driver"));
575
+ } catch {
576
+ return false;
577
+ }
578
+ }
579
+ function checkUserAgentHeadless() {
580
+ if (!isBrowser2()) return false;
581
+ try {
582
+ const ua = window.navigator?.userAgent?.toLowerCase() || "";
583
+ if (!ua) return false;
584
+ for (const pattern of HEADLESS_PATTERNS) {
585
+ if (ua.includes(pattern)) {
586
+ return true;
587
+ }
588
+ }
589
+ return false;
590
+ } catch {
591
+ return false;
592
+ }
593
+ }
594
+ function checkUserAgentHttpClient(additionalPatterns) {
595
+ if (!isBrowser2()) return false;
596
+ try {
597
+ const ua = window.navigator?.userAgent?.toLowerCase() || "";
598
+ if (!ua) return false;
599
+ for (const pattern of HTTP_CLIENT_PATTERNS) {
600
+ if (ua.includes(pattern)) {
601
+ return true;
602
+ }
603
+ }
604
+ if (additionalPatterns) {
605
+ for (const pattern of additionalPatterns) {
606
+ if (ua.includes(pattern.toLowerCase())) {
607
+ return true;
608
+ }
609
+ }
610
+ }
611
+ return false;
612
+ } catch {
613
+ return false;
614
+ }
615
+ }
616
+ function checkMissingUserAgent() {
617
+ if (!isBrowser2()) return false;
618
+ try {
619
+ const ua = window.navigator?.userAgent;
620
+ return !ua || ua === "" || ua === "undefined" || ua.length < 10;
621
+ } catch {
622
+ return false;
623
+ }
624
+ }
625
+ function checkInvalidEnvironment() {
626
+ if (!isBrowser2()) return false;
627
+ try {
628
+ if (!window.navigator || !window.location || !window.document || typeof window.navigator !== "object" || typeof window.location !== "object" || typeof window.document !== "object") {
629
+ return true;
630
+ }
631
+ return false;
632
+ } catch {
633
+ return true;
634
+ }
635
+ }
636
+ function detectBot(config = {}) {
637
+ const mergedConfig = { ...DEFAULT_BOT_DETECTION_CONFIG, ...config };
638
+ const checks = {
639
+ webdriver: mergedConfig.checkWebdriver ? checkWebdriver() : false,
640
+ phantomjs: mergedConfig.checkPhantomJS ? checkPhantomJS() : false,
641
+ nightmare: mergedConfig.checkNightmare ? checkNightmare() : false,
642
+ automationGlobals: mergedConfig.checkAutomationGlobals ? checkAutomationGlobals() : false,
643
+ documentAttributes: mergedConfig.checkDocumentAttributes ? checkDocumentAttributes() : false,
644
+ userAgentHeadless: mergedConfig.checkUserAgentHeadless ? checkUserAgentHeadless() : false,
645
+ userAgentHttpClient: mergedConfig.checkUserAgentHttpClient ? checkUserAgentHttpClient(config.additionalBotPatterns) : false,
646
+ missingUserAgent: checkMissingUserAgent(),
647
+ invalidEnvironment: checkInvalidEnvironment()
648
+ };
649
+ let reason;
650
+ if (checks.webdriver) {
651
+ reason = "WebDriver detected";
652
+ } else if (checks.phantomjs) {
653
+ reason = "PhantomJS detected";
654
+ } else if (checks.nightmare) {
655
+ reason = "Nightmare.js detected";
656
+ } else if (checks.automationGlobals) {
657
+ reason = "Automation tool globals detected";
658
+ } else if (checks.documentAttributes) {
659
+ reason = "Automation attributes on document element";
660
+ } else if (checks.userAgentHeadless) {
661
+ reason = "Headless browser user agent detected";
662
+ } else if (checks.userAgentHttpClient) {
663
+ reason = "HTTP client/bot user agent detected";
664
+ } else if (checks.missingUserAgent) {
665
+ reason = "Missing or invalid user agent";
666
+ } else if (checks.invalidEnvironment) {
667
+ reason = "Invalid browser environment";
668
+ }
669
+ const isBot2 = Object.values(checks).some(Boolean);
670
+ const result = {
671
+ isBot: isBot2,
672
+ reason,
673
+ checks
674
+ };
675
+ if (isBot2 && config.onBotDetected) {
676
+ try {
677
+ config.onBotDetected(result);
678
+ } catch {
679
+ }
680
+ }
681
+ return result;
682
+ }
683
+ function isBot(config = {}) {
684
+ return detectBot(config).isBot;
685
+ }
686
+ function isUserAgentBot(userAgent, additionalPatterns) {
687
+ if (!userAgent || userAgent.length < 10) {
688
+ return true;
689
+ }
690
+ const ua = userAgent.toLowerCase();
691
+ for (const pattern of HEADLESS_PATTERNS) {
692
+ if (ua.includes(pattern)) {
693
+ return true;
694
+ }
695
+ }
696
+ for (const pattern of HTTP_CLIENT_PATTERNS) {
697
+ if (ua.includes(pattern)) {
698
+ return true;
699
+ }
700
+ }
701
+ if (additionalPatterns) {
702
+ for (const pattern of additionalPatterns) {
703
+ if (ua.includes(pattern.toLowerCase())) {
704
+ return true;
705
+ }
706
+ }
707
+ }
708
+ return false;
709
+ }
710
+ function getUserAgent() {
711
+ if (!isBrowser2()) return null;
712
+ try {
713
+ return window.navigator?.userAgent || null;
714
+ } catch {
715
+ return null;
716
+ }
717
+ }
718
+
452
719
  // src/client.ts
453
720
  var DEFAULT_BASE_URL = "https://api.kitbase.dev";
454
721
  var TIMEOUT = 3e4;
@@ -495,8 +762,13 @@ var Kitbase = class {
495
762
  sessionStorageKey;
496
763
  analyticsEnabled;
497
764
  autoTrackPageViews;
765
+ autoTrackOutboundLinks;
498
766
  userId = null;
499
767
  unloadListenerAdded = false;
768
+ clickListenerAdded = false;
769
+ // Bot detection
770
+ botDetectionConfig;
771
+ botDetectionResult = null;
500
772
  constructor(config) {
501
773
  if (!config.token) {
502
774
  throw new ValidationError("API token is required", "token");
@@ -515,6 +787,7 @@ var Kitbase = class {
515
787
  this.sessionStorageKey = config.analytics?.sessionStorageKey ?? DEFAULT_SESSION_STORAGE_KEY;
516
788
  this.analyticsEnabled = config.analytics?.autoTrackSessions ?? true;
517
789
  this.autoTrackPageViews = config.analytics?.autoTrackPageViews ?? false;
790
+ this.autoTrackOutboundLinks = config.analytics?.autoTrackOutboundLinks ?? true;
518
791
  if (this.analyticsEnabled) {
519
792
  this.loadSession();
520
793
  this.setupUnloadListener();
@@ -522,6 +795,9 @@ var Kitbase = class {
522
795
  this.enableAutoPageViews();
523
796
  }
524
797
  }
798
+ if (this.autoTrackOutboundLinks && typeof window !== "undefined") {
799
+ this.setupOutboundLinkTracking();
800
+ }
525
801
  this.offlineEnabled = config.offline?.enabled ?? false;
526
802
  if (this.offlineEnabled) {
527
803
  this.queue = new EventQueue(config.offline);
@@ -532,6 +808,21 @@ var Kitbase = class {
532
808
  storageType: this.queue.getStorageType()
533
809
  });
534
810
  }
811
+ this.botDetectionConfig = {
812
+ ...DEFAULT_BOT_DETECTION_CONFIG,
813
+ ...config.botDetection
814
+ };
815
+ if (this.botDetectionConfig.enabled) {
816
+ this.botDetectionResult = detectBot(this.botDetectionConfig);
817
+ if (this.botDetectionResult.isBot) {
818
+ this.log("Bot detected", {
819
+ reason: this.botDetectionResult.reason,
820
+ checks: this.botDetectionResult.checks
821
+ });
822
+ } else {
823
+ this.log("Bot detection enabled, no bot detected");
824
+ }
825
+ }
535
826
  }
536
827
  /**
537
828
  * Initialize the anonymous ID from storage or generate a new one
@@ -752,6 +1043,82 @@ var Kitbase = class {
752
1043
  return (Date.now() - startTime) / 1e3;
753
1044
  }
754
1045
  // ============================================================
1046
+ // Bot Detection
1047
+ // ============================================================
1048
+ /**
1049
+ * Check if the current visitor is detected as a bot
1050
+ *
1051
+ * @returns true if bot detected, false otherwise
1052
+ *
1053
+ * @example
1054
+ * ```typescript
1055
+ * if (kitbase.isBot()) {
1056
+ * console.log('Bot detected, tracking disabled');
1057
+ * }
1058
+ * ```
1059
+ */
1060
+ isBot() {
1061
+ if (!this.botDetectionConfig.enabled) {
1062
+ return false;
1063
+ }
1064
+ if (!this.botDetectionResult) {
1065
+ this.botDetectionResult = detectBot(this.botDetectionConfig);
1066
+ }
1067
+ return this.botDetectionResult.isBot;
1068
+ }
1069
+ /**
1070
+ * Get detailed bot detection result
1071
+ *
1072
+ * @returns Bot detection result with detailed checks, or null if detection not enabled
1073
+ *
1074
+ * @example
1075
+ * ```typescript
1076
+ * const result = kitbase.getBotDetectionResult();
1077
+ * if (result?.isBot) {
1078
+ * console.log('Bot detected:', result.reason);
1079
+ * console.log('Checks:', result.checks);
1080
+ * }
1081
+ * ```
1082
+ */
1083
+ getBotDetectionResult() {
1084
+ if (!this.botDetectionConfig.enabled) {
1085
+ return null;
1086
+ }
1087
+ if (!this.botDetectionResult) {
1088
+ this.botDetectionResult = detectBot(this.botDetectionConfig);
1089
+ }
1090
+ return this.botDetectionResult;
1091
+ }
1092
+ /**
1093
+ * Force re-run bot detection
1094
+ * Useful if you want to check again after page state changes
1095
+ *
1096
+ * @returns Updated bot detection result
1097
+ *
1098
+ * @example
1099
+ * ```typescript
1100
+ * const result = kitbase.redetectBot();
1101
+ * console.log('Is bot:', result.isBot);
1102
+ * ```
1103
+ */
1104
+ redetectBot() {
1105
+ this.botDetectionResult = detectBot(this.botDetectionConfig);
1106
+ this.log("Bot detection re-run", {
1107
+ isBot: this.botDetectionResult.isBot,
1108
+ reason: this.botDetectionResult.reason
1109
+ });
1110
+ return this.botDetectionResult;
1111
+ }
1112
+ /**
1113
+ * Check if bot blocking is currently active
1114
+ * When bot detection is enabled and a bot is detected, all events are blocked.
1115
+ *
1116
+ * @returns true if bots are being blocked from tracking
1117
+ */
1118
+ isBotBlockingActive() {
1119
+ return this.botDetectionConfig.enabled === true && this.isBot();
1120
+ }
1121
+ // ============================================================
755
1122
  // Offline Queue
756
1123
  // ============================================================
757
1124
  /**
@@ -829,6 +1196,14 @@ var Kitbase = class {
829
1196
  */
830
1197
  async track(options) {
831
1198
  this.validateTrackOptions(options);
1199
+ if (this.isBotBlockingActive()) {
1200
+ this.log("Event blocked - bot detected", { event: options.event });
1201
+ return {
1202
+ id: `blocked-bot-${Date.now()}`,
1203
+ event: options.event,
1204
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1205
+ };
1206
+ }
832
1207
  let duration;
833
1208
  const startTime = this.timedEvents.get(options.event);
834
1209
  if (startTime !== void 0) {
@@ -1053,6 +1428,97 @@ var Kitbase = class {
1053
1428
  this.unloadListenerAdded = true;
1054
1429
  this.log("Session lifecycle listeners added");
1055
1430
  }
1431
+ /**
1432
+ * Setup outbound link click tracking
1433
+ */
1434
+ setupOutboundLinkTracking() {
1435
+ if (typeof window === "undefined" || this.clickListenerAdded) return;
1436
+ const handleClick = (event) => {
1437
+ const link = event.target?.closest?.("a");
1438
+ if (link) {
1439
+ this.handleLinkClick(link);
1440
+ }
1441
+ };
1442
+ const handleKeydown = (event) => {
1443
+ if (event.key === "Enter" || event.key === " ") {
1444
+ const link = event.target?.closest?.("a");
1445
+ if (link) {
1446
+ this.handleLinkClick(link);
1447
+ }
1448
+ }
1449
+ };
1450
+ document.addEventListener("click", handleClick);
1451
+ document.addEventListener("keydown", handleKeydown);
1452
+ this.clickListenerAdded = true;
1453
+ this.log("Outbound link tracking enabled");
1454
+ }
1455
+ /**
1456
+ * Handle link click for outbound tracking
1457
+ */
1458
+ handleLinkClick(link) {
1459
+ if (!link.href) return;
1460
+ try {
1461
+ const linkUrl = new URL(link.href);
1462
+ if (linkUrl.protocol !== "http:" && linkUrl.protocol !== "https:") {
1463
+ return;
1464
+ }
1465
+ const currentHost = window.location.hostname;
1466
+ const linkHost = linkUrl.hostname;
1467
+ if (linkHost === currentHost) {
1468
+ return;
1469
+ }
1470
+ if (this.isSameRootDomain(currentHost, linkHost)) {
1471
+ return;
1472
+ }
1473
+ this.trackOutboundLink({
1474
+ url: link.href,
1475
+ text: link.textContent?.trim() || ""
1476
+ }).catch((err) => this.log("Failed to track outbound link", err));
1477
+ } catch {
1478
+ }
1479
+ }
1480
+ /**
1481
+ * Get root domain from hostname (e.g., blog.example.com -> example.com)
1482
+ */
1483
+ getRootDomain(hostname) {
1484
+ const parts = hostname.replace(/^www\./, "").split(".");
1485
+ if (parts.length >= 2) {
1486
+ return parts.slice(-2).join(".");
1487
+ }
1488
+ return hostname;
1489
+ }
1490
+ /**
1491
+ * Check if two hostnames share the same root domain
1492
+ */
1493
+ isSameRootDomain(host1, host2) {
1494
+ return this.getRootDomain(host1) === this.getRootDomain(host2);
1495
+ }
1496
+ /**
1497
+ * Track an outbound link click
1498
+ *
1499
+ * @param options - Outbound link options
1500
+ * @returns Promise resolving to the track response
1501
+ *
1502
+ * @example
1503
+ * ```typescript
1504
+ * await kitbase.trackOutboundLink({
1505
+ * url: 'https://example.com',
1506
+ * text: 'Visit Example',
1507
+ * });
1508
+ * ```
1509
+ */
1510
+ async trackOutboundLink(options) {
1511
+ const session = this.getOrCreateSession();
1512
+ return this.track({
1513
+ channel: ANALYTICS_CHANNEL,
1514
+ event: "outbound_link",
1515
+ tags: {
1516
+ __session_id: session.id,
1517
+ __url: options.url,
1518
+ __text: options.text || ""
1519
+ }
1520
+ });
1521
+ }
1056
1522
  /**
1057
1523
  * Get UTM parameters from URL
1058
1524
  */
@@ -1258,6 +1724,10 @@ var Kitbase = class {
1258
1724
  Kitbase,
1259
1725
  KitbaseError,
1260
1726
  TimeoutError,
1261
- ValidationError
1727
+ ValidationError,
1728
+ detectBot,
1729
+ getUserAgent,
1730
+ isBot,
1731
+ isUserAgentBot
1262
1732
  });
1263
1733
  //# sourceMappingURL=index.cjs.map