@kitbase/events 0.1.2 → 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.js CHANGED
@@ -408,6 +408,269 @@ var EventQueue = class {
408
408
  }
409
409
  };
410
410
 
411
+ // src/botDetection.ts
412
+ var AUTOMATION_GLOBALS = [
413
+ "__webdriver_evaluate",
414
+ "__selenium_evaluate",
415
+ "__webdriver_script_function",
416
+ "__webdriver_unwrapped",
417
+ "__fxdriver_evaluate",
418
+ "__driver_evaluate",
419
+ "_Selenium_IDE_Recorder",
420
+ "_selenium",
421
+ "calledSelenium",
422
+ "$cdc_asdjflasutopfhvcZLmcfl_",
423
+ // Chrome DevTools Protocol marker
424
+ "__nightmare",
425
+ "domAutomation",
426
+ "domAutomationController"
427
+ ];
428
+ var HEADLESS_PATTERNS = [
429
+ "headlesschrome",
430
+ "phantomjs",
431
+ "selenium",
432
+ "webdriver",
433
+ "puppeteer",
434
+ "playwright"
435
+ ];
436
+ var HTTP_CLIENT_PATTERNS = [
437
+ "python",
438
+ "curl",
439
+ "wget",
440
+ "java/",
441
+ "go-http",
442
+ "node-fetch",
443
+ "axios",
444
+ "postman",
445
+ "insomnia",
446
+ "httpie",
447
+ "ruby",
448
+ "perl",
449
+ "scrapy",
450
+ "bot",
451
+ "spider",
452
+ "crawler",
453
+ "slurp",
454
+ "googlebot",
455
+ "bingbot",
456
+ "yandexbot",
457
+ "baiduspider",
458
+ "duckduckbot",
459
+ "facebookexternalhit",
460
+ "twitterbot",
461
+ "linkedinbot",
462
+ "whatsapp",
463
+ "telegram",
464
+ "discord",
465
+ "slack"
466
+ ];
467
+ var DEFAULT_BOT_DETECTION_CONFIG = {
468
+ enabled: true,
469
+ checkWebdriver: true,
470
+ checkPhantomJS: true,
471
+ checkNightmare: true,
472
+ checkAutomationGlobals: true,
473
+ checkDocumentAttributes: true,
474
+ checkUserAgentHeadless: true,
475
+ checkUserAgentHttpClient: true
476
+ };
477
+ function isBrowser2() {
478
+ return typeof window !== "undefined" && typeof document !== "undefined";
479
+ }
480
+ function getWindowProperty(key) {
481
+ try {
482
+ return window[key];
483
+ } catch {
484
+ return void 0;
485
+ }
486
+ }
487
+ function checkWebdriver() {
488
+ if (!isBrowser2()) return false;
489
+ try {
490
+ return window.navigator?.webdriver === true;
491
+ } catch {
492
+ return false;
493
+ }
494
+ }
495
+ function checkPhantomJS() {
496
+ if (!isBrowser2()) return false;
497
+ try {
498
+ return !!(getWindowProperty("callPhantom") || getWindowProperty("_phantom") || getWindowProperty("phantom"));
499
+ } catch {
500
+ return false;
501
+ }
502
+ }
503
+ function checkNightmare() {
504
+ if (!isBrowser2()) return false;
505
+ try {
506
+ return !!getWindowProperty("__nightmare");
507
+ } catch {
508
+ return false;
509
+ }
510
+ }
511
+ function checkAutomationGlobals() {
512
+ if (!isBrowser2()) return false;
513
+ try {
514
+ for (const global of AUTOMATION_GLOBALS) {
515
+ if (getWindowProperty(global) !== void 0) {
516
+ return true;
517
+ }
518
+ }
519
+ return false;
520
+ } catch {
521
+ return false;
522
+ }
523
+ }
524
+ function checkDocumentAttributes() {
525
+ if (!isBrowser2()) return false;
526
+ try {
527
+ const docEl = document.documentElement;
528
+ if (!docEl) return false;
529
+ return !!(docEl.getAttribute("webdriver") || docEl.getAttribute("selenium") || docEl.getAttribute("driver"));
530
+ } catch {
531
+ return false;
532
+ }
533
+ }
534
+ function checkUserAgentHeadless() {
535
+ if (!isBrowser2()) return false;
536
+ try {
537
+ const ua = window.navigator?.userAgent?.toLowerCase() || "";
538
+ if (!ua) return false;
539
+ for (const pattern of HEADLESS_PATTERNS) {
540
+ if (ua.includes(pattern)) {
541
+ return true;
542
+ }
543
+ }
544
+ return false;
545
+ } catch {
546
+ return false;
547
+ }
548
+ }
549
+ function checkUserAgentHttpClient(additionalPatterns) {
550
+ if (!isBrowser2()) return false;
551
+ try {
552
+ const ua = window.navigator?.userAgent?.toLowerCase() || "";
553
+ if (!ua) return false;
554
+ for (const pattern of HTTP_CLIENT_PATTERNS) {
555
+ if (ua.includes(pattern)) {
556
+ return true;
557
+ }
558
+ }
559
+ if (additionalPatterns) {
560
+ for (const pattern of additionalPatterns) {
561
+ if (ua.includes(pattern.toLowerCase())) {
562
+ return true;
563
+ }
564
+ }
565
+ }
566
+ return false;
567
+ } catch {
568
+ return false;
569
+ }
570
+ }
571
+ function checkMissingUserAgent() {
572
+ if (!isBrowser2()) return false;
573
+ try {
574
+ const ua = window.navigator?.userAgent;
575
+ return !ua || ua === "" || ua === "undefined" || ua.length < 10;
576
+ } catch {
577
+ return false;
578
+ }
579
+ }
580
+ function checkInvalidEnvironment() {
581
+ if (!isBrowser2()) return false;
582
+ try {
583
+ if (!window.navigator || !window.location || !window.document || typeof window.navigator !== "object" || typeof window.location !== "object" || typeof window.document !== "object") {
584
+ return true;
585
+ }
586
+ return false;
587
+ } catch {
588
+ return true;
589
+ }
590
+ }
591
+ function detectBot(config = {}) {
592
+ const mergedConfig = { ...DEFAULT_BOT_DETECTION_CONFIG, ...config };
593
+ const checks = {
594
+ webdriver: mergedConfig.checkWebdriver ? checkWebdriver() : false,
595
+ phantomjs: mergedConfig.checkPhantomJS ? checkPhantomJS() : false,
596
+ nightmare: mergedConfig.checkNightmare ? checkNightmare() : false,
597
+ automationGlobals: mergedConfig.checkAutomationGlobals ? checkAutomationGlobals() : false,
598
+ documentAttributes: mergedConfig.checkDocumentAttributes ? checkDocumentAttributes() : false,
599
+ userAgentHeadless: mergedConfig.checkUserAgentHeadless ? checkUserAgentHeadless() : false,
600
+ userAgentHttpClient: mergedConfig.checkUserAgentHttpClient ? checkUserAgentHttpClient(config.additionalBotPatterns) : false,
601
+ missingUserAgent: checkMissingUserAgent(),
602
+ invalidEnvironment: checkInvalidEnvironment()
603
+ };
604
+ let reason;
605
+ if (checks.webdriver) {
606
+ reason = "WebDriver detected";
607
+ } else if (checks.phantomjs) {
608
+ reason = "PhantomJS detected";
609
+ } else if (checks.nightmare) {
610
+ reason = "Nightmare.js detected";
611
+ } else if (checks.automationGlobals) {
612
+ reason = "Automation tool globals detected";
613
+ } else if (checks.documentAttributes) {
614
+ reason = "Automation attributes on document element";
615
+ } else if (checks.userAgentHeadless) {
616
+ reason = "Headless browser user agent detected";
617
+ } else if (checks.userAgentHttpClient) {
618
+ reason = "HTTP client/bot user agent detected";
619
+ } else if (checks.missingUserAgent) {
620
+ reason = "Missing or invalid user agent";
621
+ } else if (checks.invalidEnvironment) {
622
+ reason = "Invalid browser environment";
623
+ }
624
+ const isBot2 = Object.values(checks).some(Boolean);
625
+ const result = {
626
+ isBot: isBot2,
627
+ reason,
628
+ checks
629
+ };
630
+ if (isBot2 && config.onBotDetected) {
631
+ try {
632
+ config.onBotDetected(result);
633
+ } catch {
634
+ }
635
+ }
636
+ return result;
637
+ }
638
+ function isBot(config = {}) {
639
+ return detectBot(config).isBot;
640
+ }
641
+ function isUserAgentBot(userAgent, additionalPatterns) {
642
+ if (!userAgent || userAgent.length < 10) {
643
+ return true;
644
+ }
645
+ const ua = userAgent.toLowerCase();
646
+ for (const pattern of HEADLESS_PATTERNS) {
647
+ if (ua.includes(pattern)) {
648
+ return true;
649
+ }
650
+ }
651
+ for (const pattern of HTTP_CLIENT_PATTERNS) {
652
+ if (ua.includes(pattern)) {
653
+ return true;
654
+ }
655
+ }
656
+ if (additionalPatterns) {
657
+ for (const pattern of additionalPatterns) {
658
+ if (ua.includes(pattern.toLowerCase())) {
659
+ return true;
660
+ }
661
+ }
662
+ }
663
+ return false;
664
+ }
665
+ function getUserAgent() {
666
+ if (!isBrowser2()) return null;
667
+ try {
668
+ return window.navigator?.userAgent || null;
669
+ } catch {
670
+ return null;
671
+ }
672
+ }
673
+
411
674
  // src/client.ts
412
675
  var DEFAULT_BASE_URL = "https://api.kitbase.dev";
413
676
  var TIMEOUT = 3e4;
@@ -454,14 +717,19 @@ var Kitbase = class {
454
717
  sessionStorageKey;
455
718
  analyticsEnabled;
456
719
  autoTrackPageViews;
720
+ autoTrackOutboundLinks;
457
721
  userId = null;
458
722
  unloadListenerAdded = false;
723
+ clickListenerAdded = false;
724
+ // Bot detection
725
+ botDetectionConfig;
726
+ botDetectionResult = null;
459
727
  constructor(config) {
460
728
  if (!config.token) {
461
729
  throw new ValidationError("API token is required", "token");
462
730
  }
463
731
  this.token = config.token;
464
- this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
732
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
465
733
  this.storageKey = config.storageKey ?? DEFAULT_STORAGE_KEY;
466
734
  this.debugMode = config.debug ?? false;
467
735
  if (config.storage === null) {
@@ -474,6 +742,7 @@ var Kitbase = class {
474
742
  this.sessionStorageKey = config.analytics?.sessionStorageKey ?? DEFAULT_SESSION_STORAGE_KEY;
475
743
  this.analyticsEnabled = config.analytics?.autoTrackSessions ?? true;
476
744
  this.autoTrackPageViews = config.analytics?.autoTrackPageViews ?? false;
745
+ this.autoTrackOutboundLinks = config.analytics?.autoTrackOutboundLinks ?? true;
477
746
  if (this.analyticsEnabled) {
478
747
  this.loadSession();
479
748
  this.setupUnloadListener();
@@ -481,6 +750,9 @@ var Kitbase = class {
481
750
  this.enableAutoPageViews();
482
751
  }
483
752
  }
753
+ if (this.autoTrackOutboundLinks && typeof window !== "undefined") {
754
+ this.setupOutboundLinkTracking();
755
+ }
484
756
  this.offlineEnabled = config.offline?.enabled ?? false;
485
757
  if (this.offlineEnabled) {
486
758
  this.queue = new EventQueue(config.offline);
@@ -491,6 +763,21 @@ var Kitbase = class {
491
763
  storageType: this.queue.getStorageType()
492
764
  });
493
765
  }
766
+ this.botDetectionConfig = {
767
+ ...DEFAULT_BOT_DETECTION_CONFIG,
768
+ ...config.botDetection
769
+ };
770
+ if (this.botDetectionConfig.enabled) {
771
+ this.botDetectionResult = detectBot(this.botDetectionConfig);
772
+ if (this.botDetectionResult.isBot) {
773
+ this.log("Bot detected", {
774
+ reason: this.botDetectionResult.reason,
775
+ checks: this.botDetectionResult.checks
776
+ });
777
+ } else {
778
+ this.log("Bot detection enabled, no bot detected");
779
+ }
780
+ }
494
781
  }
495
782
  /**
496
783
  * Initialize the anonymous ID from storage or generate a new one
@@ -711,6 +998,82 @@ var Kitbase = class {
711
998
  return (Date.now() - startTime) / 1e3;
712
999
  }
713
1000
  // ============================================================
1001
+ // Bot Detection
1002
+ // ============================================================
1003
+ /**
1004
+ * Check if the current visitor is detected as a bot
1005
+ *
1006
+ * @returns true if bot detected, false otherwise
1007
+ *
1008
+ * @example
1009
+ * ```typescript
1010
+ * if (kitbase.isBot()) {
1011
+ * console.log('Bot detected, tracking disabled');
1012
+ * }
1013
+ * ```
1014
+ */
1015
+ isBot() {
1016
+ if (!this.botDetectionConfig.enabled) {
1017
+ return false;
1018
+ }
1019
+ if (!this.botDetectionResult) {
1020
+ this.botDetectionResult = detectBot(this.botDetectionConfig);
1021
+ }
1022
+ return this.botDetectionResult.isBot;
1023
+ }
1024
+ /**
1025
+ * Get detailed bot detection result
1026
+ *
1027
+ * @returns Bot detection result with detailed checks, or null if detection not enabled
1028
+ *
1029
+ * @example
1030
+ * ```typescript
1031
+ * const result = kitbase.getBotDetectionResult();
1032
+ * if (result?.isBot) {
1033
+ * console.log('Bot detected:', result.reason);
1034
+ * console.log('Checks:', result.checks);
1035
+ * }
1036
+ * ```
1037
+ */
1038
+ getBotDetectionResult() {
1039
+ if (!this.botDetectionConfig.enabled) {
1040
+ return null;
1041
+ }
1042
+ if (!this.botDetectionResult) {
1043
+ this.botDetectionResult = detectBot(this.botDetectionConfig);
1044
+ }
1045
+ return this.botDetectionResult;
1046
+ }
1047
+ /**
1048
+ * Force re-run bot detection
1049
+ * Useful if you want to check again after page state changes
1050
+ *
1051
+ * @returns Updated bot detection result
1052
+ *
1053
+ * @example
1054
+ * ```typescript
1055
+ * const result = kitbase.redetectBot();
1056
+ * console.log('Is bot:', result.isBot);
1057
+ * ```
1058
+ */
1059
+ redetectBot() {
1060
+ this.botDetectionResult = detectBot(this.botDetectionConfig);
1061
+ this.log("Bot detection re-run", {
1062
+ isBot: this.botDetectionResult.isBot,
1063
+ reason: this.botDetectionResult.reason
1064
+ });
1065
+ return this.botDetectionResult;
1066
+ }
1067
+ /**
1068
+ * Check if bot blocking is currently active
1069
+ * When bot detection is enabled and a bot is detected, all events are blocked.
1070
+ *
1071
+ * @returns true if bots are being blocked from tracking
1072
+ */
1073
+ isBotBlockingActive() {
1074
+ return this.botDetectionConfig.enabled === true && this.isBot();
1075
+ }
1076
+ // ============================================================
714
1077
  // Offline Queue
715
1078
  // ============================================================
716
1079
  /**
@@ -788,6 +1151,14 @@ var Kitbase = class {
788
1151
  */
789
1152
  async track(options) {
790
1153
  this.validateTrackOptions(options);
1154
+ if (this.isBotBlockingActive()) {
1155
+ this.log("Event blocked - bot detected", { event: options.event });
1156
+ return {
1157
+ id: `blocked-bot-${Date.now()}`,
1158
+ event: options.event,
1159
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1160
+ };
1161
+ }
791
1162
  let duration;
792
1163
  const startTime = this.timedEvents.get(options.event);
793
1164
  if (startTime !== void 0) {
@@ -1012,6 +1383,97 @@ var Kitbase = class {
1012
1383
  this.unloadListenerAdded = true;
1013
1384
  this.log("Session lifecycle listeners added");
1014
1385
  }
1386
+ /**
1387
+ * Setup outbound link click tracking
1388
+ */
1389
+ setupOutboundLinkTracking() {
1390
+ if (typeof window === "undefined" || this.clickListenerAdded) return;
1391
+ const handleClick = (event) => {
1392
+ const link = event.target?.closest?.("a");
1393
+ if (link) {
1394
+ this.handleLinkClick(link);
1395
+ }
1396
+ };
1397
+ const handleKeydown = (event) => {
1398
+ if (event.key === "Enter" || event.key === " ") {
1399
+ const link = event.target?.closest?.("a");
1400
+ if (link) {
1401
+ this.handleLinkClick(link);
1402
+ }
1403
+ }
1404
+ };
1405
+ document.addEventListener("click", handleClick);
1406
+ document.addEventListener("keydown", handleKeydown);
1407
+ this.clickListenerAdded = true;
1408
+ this.log("Outbound link tracking enabled");
1409
+ }
1410
+ /**
1411
+ * Handle link click for outbound tracking
1412
+ */
1413
+ handleLinkClick(link) {
1414
+ if (!link.href) return;
1415
+ try {
1416
+ const linkUrl = new URL(link.href);
1417
+ if (linkUrl.protocol !== "http:" && linkUrl.protocol !== "https:") {
1418
+ return;
1419
+ }
1420
+ const currentHost = window.location.hostname;
1421
+ const linkHost = linkUrl.hostname;
1422
+ if (linkHost === currentHost) {
1423
+ return;
1424
+ }
1425
+ if (this.isSameRootDomain(currentHost, linkHost)) {
1426
+ return;
1427
+ }
1428
+ this.trackOutboundLink({
1429
+ url: link.href,
1430
+ text: link.textContent?.trim() || ""
1431
+ }).catch((err) => this.log("Failed to track outbound link", err));
1432
+ } catch {
1433
+ }
1434
+ }
1435
+ /**
1436
+ * Get root domain from hostname (e.g., blog.example.com -> example.com)
1437
+ */
1438
+ getRootDomain(hostname) {
1439
+ const parts = hostname.replace(/^www\./, "").split(".");
1440
+ if (parts.length >= 2) {
1441
+ return parts.slice(-2).join(".");
1442
+ }
1443
+ return hostname;
1444
+ }
1445
+ /**
1446
+ * Check if two hostnames share the same root domain
1447
+ */
1448
+ isSameRootDomain(host1, host2) {
1449
+ return this.getRootDomain(host1) === this.getRootDomain(host2);
1450
+ }
1451
+ /**
1452
+ * Track an outbound link click
1453
+ *
1454
+ * @param options - Outbound link options
1455
+ * @returns Promise resolving to the track response
1456
+ *
1457
+ * @example
1458
+ * ```typescript
1459
+ * await kitbase.trackOutboundLink({
1460
+ * url: 'https://example.com',
1461
+ * text: 'Visit Example',
1462
+ * });
1463
+ * ```
1464
+ */
1465
+ async trackOutboundLink(options) {
1466
+ const session = this.getOrCreateSession();
1467
+ return this.track({
1468
+ channel: ANALYTICS_CHANNEL,
1469
+ event: "outbound_link",
1470
+ tags: {
1471
+ __session_id: session.id,
1472
+ __url: options.url,
1473
+ __text: options.text || ""
1474
+ }
1475
+ });
1476
+ }
1015
1477
  /**
1016
1478
  * Get UTM parameters from URL
1017
1479
  */
@@ -1216,6 +1678,10 @@ export {
1216
1678
  Kitbase,
1217
1679
  KitbaseError,
1218
1680
  TimeoutError,
1219
- ValidationError
1681
+ ValidationError,
1682
+ detectBot,
1683
+ getUserAgent,
1684
+ isBot,
1685
+ isUserAgentBot
1220
1686
  };
1221
1687
  //# sourceMappingURL=index.js.map