@grainql/analytics-web 2.7.1 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +36 -3
  2. package/dist/cjs/consent.d.ts +38 -7
  3. package/dist/cjs/consent.d.ts.map +1 -1
  4. package/dist/cjs/consent.js +82 -23
  5. package/dist/cjs/consent.js.map +1 -1
  6. package/dist/cjs/debug-agent.d.ts +171 -0
  7. package/dist/cjs/debug-agent.d.ts.map +1 -0
  8. package/dist/cjs/debug-agent.js +1219 -0
  9. package/dist/cjs/debug-agent.js.map +1 -0
  10. package/dist/cjs/id-manager.d.ts +66 -0
  11. package/dist/cjs/id-manager.d.ts.map +1 -0
  12. package/dist/cjs/id-manager.js +212 -0
  13. package/dist/cjs/id-manager.js.map +1 -0
  14. package/dist/cjs/index.d.ts +26 -8
  15. package/dist/cjs/index.d.ts.map +1 -1
  16. package/dist/cjs/index.js.map +1 -1
  17. package/dist/cjs/interaction-tracking.d.ts +6 -0
  18. package/dist/cjs/interaction-tracking.d.ts.map +1 -1
  19. package/dist/cjs/interaction-tracking.js +55 -5
  20. package/dist/cjs/interaction-tracking.js.map +1 -1
  21. package/dist/cjs/page-tracking.d.ts +6 -0
  22. package/dist/cjs/page-tracking.d.ts.map +1 -1
  23. package/dist/cjs/page-tracking.js +23 -2
  24. package/dist/cjs/page-tracking.js.map +1 -1
  25. package/dist/cjs/react/hooks/useConsent.d.ts +18 -2
  26. package/dist/cjs/react/hooks/useConsent.d.ts.map +1 -1
  27. package/dist/cjs/react/hooks/useConsent.js +52 -1
  28. package/dist/cjs/react/hooks/useConsent.js.map +1 -1
  29. package/dist/consent.d.ts +38 -7
  30. package/dist/consent.d.ts.map +1 -1
  31. package/dist/consent.js +82 -23
  32. package/dist/debug-agent.d.ts +171 -0
  33. package/dist/debug-agent.d.ts.map +1 -0
  34. package/dist/debug-agent.js +1219 -0
  35. package/dist/esm/consent.d.ts +38 -7
  36. package/dist/esm/consent.d.ts.map +1 -1
  37. package/dist/esm/consent.js +82 -23
  38. package/dist/esm/consent.js.map +1 -1
  39. package/dist/esm/debug-agent.d.ts +171 -0
  40. package/dist/esm/debug-agent.d.ts.map +1 -0
  41. package/dist/esm/debug-agent.js +1215 -0
  42. package/dist/esm/debug-agent.js.map +1 -0
  43. package/dist/esm/id-manager.d.ts +66 -0
  44. package/dist/esm/id-manager.d.ts.map +1 -0
  45. package/dist/esm/id-manager.js +208 -0
  46. package/dist/esm/id-manager.js.map +1 -0
  47. package/dist/esm/index.d.ts +26 -8
  48. package/dist/esm/index.d.ts.map +1 -1
  49. package/dist/esm/index.js.map +1 -1
  50. package/dist/esm/interaction-tracking.d.ts +6 -0
  51. package/dist/esm/interaction-tracking.d.ts.map +1 -1
  52. package/dist/esm/interaction-tracking.js +55 -5
  53. package/dist/esm/interaction-tracking.js.map +1 -1
  54. package/dist/esm/page-tracking.d.ts +6 -0
  55. package/dist/esm/page-tracking.d.ts.map +1 -1
  56. package/dist/esm/page-tracking.js +23 -2
  57. package/dist/esm/page-tracking.js.map +1 -1
  58. package/dist/esm/react/hooks/useConsent.d.ts +18 -2
  59. package/dist/esm/react/hooks/useConsent.d.ts.map +1 -1
  60. package/dist/esm/react/hooks/useConsent.js +49 -1
  61. package/dist/esm/react/hooks/useConsent.js.map +1 -1
  62. package/dist/id-manager.d.ts +66 -0
  63. package/dist/id-manager.d.ts.map +1 -0
  64. package/dist/id-manager.js +212 -0
  65. package/dist/index.d.ts +26 -8
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.global.dev.js +1635 -86
  68. package/dist/index.global.dev.js.map +4 -4
  69. package/dist/index.global.js +506 -2
  70. package/dist/index.global.js.map +4 -4
  71. package/dist/index.js +171 -44
  72. package/dist/index.mjs +172 -45
  73. package/dist/interaction-tracking.d.ts +6 -0
  74. package/dist/interaction-tracking.d.ts.map +1 -1
  75. package/dist/interaction-tracking.js +55 -5
  76. package/dist/page-tracking.d.ts +6 -0
  77. package/dist/page-tracking.d.ts.map +1 -1
  78. package/dist/page-tracking.js +23 -2
  79. package/dist/react/hooks/useConsent.d.ts +18 -2
  80. package/dist/react/hooks/useConsent.d.ts.map +1 -1
  81. package/dist/react/hooks/useConsent.js +52 -1
  82. package/dist/react/hooks/useConsent.mjs +49 -1
  83. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- /* Grain Analytics Web SDK v2.7.1 | MIT License | Development Build */
1
+ /* Grain Analytics Web SDK v2.9.0 | MIT License | Development Build */
2
2
  "use strict";
3
3
  var Grain = (() => {
4
4
  var __defProp = Object.defineProperty;
@@ -755,19 +755,65 @@ var Grain = (() => {
755
755
  this.config = {
756
756
  debug: config.debug ?? false,
757
757
  enableMutationObserver: config.enableMutationObserver ?? true,
758
- mutationDebounceDelay: config.mutationDebounceDelay ?? 500
758
+ mutationDebounceDelay: config.mutationDebounceDelay ?? 500,
759
+ tenantId: config.tenantId,
760
+ apiUrl: config.apiUrl
759
761
  };
760
762
  if (typeof window !== "undefined" && typeof document !== "undefined") {
761
- if (document.readyState === "loading") {
762
- document.addEventListener("DOMContentLoaded", () => this.attachAllListeners());
763
+ if (this.config.tenantId && this.config.apiUrl) {
764
+ this.fetchAndMergeTrackers().then(() => {
765
+ this.attachAllListeners();
766
+ });
763
767
  } else {
764
- setTimeout(() => this.attachAllListeners(), 0);
768
+ if (document.readyState === "loading") {
769
+ document.addEventListener("DOMContentLoaded", () => this.attachAllListeners());
770
+ } else {
771
+ setTimeout(() => this.attachAllListeners(), 0);
772
+ }
765
773
  }
766
774
  if (this.config.enableMutationObserver) {
767
775
  this.setupMutationObserver();
768
776
  }
769
777
  }
770
778
  }
779
+ /**
780
+ * Fetch trackers from API and merge with existing interactions
781
+ */
782
+ async fetchAndMergeTrackers() {
783
+ if (!this.config.tenantId || !this.config.apiUrl)
784
+ return;
785
+ try {
786
+ const currentUrl = typeof window !== "undefined" ? window.location.href : "";
787
+ const url = `${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/trackers?url=${encodeURIComponent(currentUrl)}`;
788
+ this.log("Fetching trackers from:", url);
789
+ const response = await fetch(url, {
790
+ method: "GET",
791
+ headers: {
792
+ "Content-Type": "application/json"
793
+ }
794
+ });
795
+ if (!response.ok) {
796
+ this.log("Failed to fetch trackers:", response.status);
797
+ return;
798
+ }
799
+ const result = await response.json();
800
+ if (result.trackers && Array.isArray(result.trackers)) {
801
+ this.log("Fetched", result.trackers.length, "trackers");
802
+ const trackerInteractions = result.trackers.map((tracker) => ({
803
+ eventName: tracker.eventName,
804
+ selector: tracker.selector,
805
+ priority: 5,
806
+ // High priority for manually created trackers
807
+ label: tracker.eventName,
808
+ description: `Tracker: ${tracker.eventName}`
809
+ }));
810
+ this.interactions = [...trackerInteractions, ...this.interactions];
811
+ this.log("Merged trackers, total interactions:", this.interactions.length);
812
+ }
813
+ } catch (error) {
814
+ this.log("Error fetching trackers:", error);
815
+ }
816
+ }
771
817
  /**
772
818
  * Attach listeners to all configured interactions
773
819
  */
@@ -1458,6 +1504,1185 @@ var Grain = (() => {
1458
1504
  }
1459
1505
  });
1460
1506
 
1507
+ // src/debug-agent.ts
1508
+ var debug_agent_exports = {};
1509
+ __export(debug_agent_exports, {
1510
+ DebugAgent: () => DebugAgent
1511
+ });
1512
+ var DebugAgent;
1513
+ var init_debug_agent = __esm({
1514
+ "src/debug-agent.ts"() {
1515
+ "use strict";
1516
+ DebugAgent = class {
1517
+ constructor(tracker, sessionId, tenantId, apiUrl, config = {}) {
1518
+ this.isDestroyed = false;
1519
+ // UI state
1520
+ this.isInspectMode = false;
1521
+ this.showTrackers = false;
1522
+ this.selectedElement = null;
1523
+ this.toolbarElement = null;
1524
+ this.panelElement = null;
1525
+ this.highlightElement = null;
1526
+ this.existingTrackers = [];
1527
+ this.trackerHighlights = [];
1528
+ // Dragging state
1529
+ this.isDragging = false;
1530
+ this.dragStartX = 0;
1531
+ this.dragStartY = 0;
1532
+ this.toolbarStartX = 0;
1533
+ this.toolbarStartY = 0;
1534
+ // Event listeners
1535
+ this.mouseMoveListener = null;
1536
+ this.clickListener = null;
1537
+ this.dragMoveListener = null;
1538
+ this.dragEndListener = null;
1539
+ /**
1540
+ * Handle ESC key to exit inspect mode
1541
+ */
1542
+ this.handleEscapeKey = (e) => {
1543
+ if (e.key === "Escape" && this.isInspectMode) {
1544
+ this.disableInspectMode();
1545
+ }
1546
+ };
1547
+ this.tracker = tracker;
1548
+ this.sessionId = sessionId;
1549
+ this.tenantId = tenantId;
1550
+ this.apiUrl = apiUrl;
1551
+ this.config = {
1552
+ debug: config.debug ?? false
1553
+ };
1554
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
1555
+ this.initialize();
1556
+ }
1557
+ }
1558
+ /**
1559
+ * Initialize the debug agent
1560
+ */
1561
+ async initialize() {
1562
+ this.log("Initializing debug agent");
1563
+ await this.loadExistingTrackers();
1564
+ this.showToolbar();
1565
+ this.createHighlightElement();
1566
+ this.showTrackers = true;
1567
+ this.showTrackerHighlights();
1568
+ this.showTrackersList();
1569
+ }
1570
+ /**
1571
+ * Load existing trackers from API
1572
+ */
1573
+ async loadExistingTrackers() {
1574
+ try {
1575
+ const url = `${this.apiUrl}/v1/tenant/${encodeURIComponent(this.tenantId)}/trackers`;
1576
+ const response = await fetch(url);
1577
+ if (response.ok) {
1578
+ this.existingTrackers = await response.json();
1579
+ this.log("Loaded trackers:", this.existingTrackers);
1580
+ }
1581
+ } catch (error) {
1582
+ this.log("Failed to load trackers:", error);
1583
+ this.existingTrackers = [];
1584
+ }
1585
+ }
1586
+ /**
1587
+ * Show the debug toolbar
1588
+ */
1589
+ showToolbar() {
1590
+ if (this.toolbarElement)
1591
+ return;
1592
+ const toolbar = document.createElement("div");
1593
+ toolbar.id = "grain-debug-toolbar";
1594
+ toolbar.innerHTML = `
1595
+ <style>
1596
+ #grain-debug-toolbar {
1597
+ position: fixed;
1598
+ bottom: 20px;
1599
+ right: 20px;
1600
+ background: repeating-linear-gradient(
1601
+ 45deg,
1602
+ #fbbf24,
1603
+ #fbbf24 10px,
1604
+ #1e293b 10px,
1605
+ #1e293b 20px
1606
+ );
1607
+ border: 2px solid #1e293b;
1608
+ border-radius: 12px;
1609
+ padding: 6px;
1610
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2), 0 2px 8px rgba(0, 0, 0, 0.1);
1611
+ z-index: 999999;
1612
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1613
+ font-size: 13px;
1614
+ }
1615
+
1616
+ .grain-toolbar-inner {
1617
+ display: flex;
1618
+ align-items: center;
1619
+ gap: 12px;
1620
+ background: white;
1621
+ border-radius: 6px;
1622
+ padding: 8px 12px;
1623
+ }
1624
+
1625
+ #grain-debug-toolbar.dragging {
1626
+ cursor: move;
1627
+ user-select: none;
1628
+ }
1629
+
1630
+ .grain-toolbar-header {
1631
+ display: flex;
1632
+ align-items: center;
1633
+ cursor: move;
1634
+ user-select: none;
1635
+ padding: 6px 10px;
1636
+ background: #1e293b;
1637
+ border-radius: 4px;
1638
+ margin-right: 4px;
1639
+ }
1640
+
1641
+ .grain-toolbar-title {
1642
+ font-size: 11px;
1643
+ font-weight: 700;
1644
+ letter-spacing: 1.2px;
1645
+ text-transform: uppercase;
1646
+ color: #fbbf24;
1647
+ }
1648
+
1649
+ .grain-toolbar-body {
1650
+ display: flex;
1651
+ align-items: center;
1652
+ gap: 10px;
1653
+ flex: 1;
1654
+ }
1655
+
1656
+ .grain-toolbar-stats {
1657
+ display: flex;
1658
+ gap: 12px;
1659
+ padding: 6px 10px;
1660
+ background: #f8fafc;
1661
+ border-radius: 4px;
1662
+ margin-right: 4px;
1663
+ }
1664
+
1665
+ .grain-stat {
1666
+ display: flex;
1667
+ align-items: baseline;
1668
+ gap: 6px;
1669
+ }
1670
+
1671
+ .grain-stat-value {
1672
+ font-size: 18px;
1673
+ font-weight: 700;
1674
+ color: #1e293b;
1675
+ }
1676
+
1677
+ .grain-stat-label {
1678
+ font-size: 11px;
1679
+ color: #64748b;
1680
+ font-weight: 500;
1681
+ }
1682
+
1683
+ .grain-toolbar-actions {
1684
+ display: flex;
1685
+ gap: 8px;
1686
+ align-items: center;
1687
+ }
1688
+
1689
+ #grain-debug-toolbar button {
1690
+ background: white;
1691
+ border: 1.5px solid #e2e8f0;
1692
+ color: #475569;
1693
+ padding: 8px 14px;
1694
+ border-radius: 8px;
1695
+ cursor: pointer;
1696
+ font-size: 12px;
1697
+ font-weight: 600;
1698
+ transition: all 0.2s;
1699
+ display: flex;
1700
+ align-items: center;
1701
+ justify-content: center;
1702
+ gap: 6px;
1703
+ white-space: nowrap;
1704
+ }
1705
+
1706
+ #grain-debug-toolbar button:hover {
1707
+ background: #f8fafc;
1708
+ border-color: #cbd5e1;
1709
+ transform: translateY(-1px);
1710
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
1711
+ }
1712
+
1713
+ #grain-debug-toolbar button.active {
1714
+ background: #fbbf24;
1715
+ color: #1e293b;
1716
+ border-color: #fbbf24;
1717
+ box-shadow: 0 2px 8px rgba(251, 191, 36, 0.3);
1718
+ }
1719
+
1720
+ #grain-debug-toolbar button.danger {
1721
+ background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
1722
+ border-color: #ef4444;
1723
+ color: white;
1724
+ }
1725
+
1726
+ #grain-debug-toolbar button.danger:hover {
1727
+ transform: translateY(-1px);
1728
+ box-shadow: 0 2px 8px rgba(239, 68, 68, 0.25);
1729
+ }
1730
+
1731
+ .grain-debug-highlight {
1732
+ position: absolute;
1733
+ pointer-events: none;
1734
+ border: 2px solid #10b981;
1735
+ background: rgba(16, 185, 129, 0.1);
1736
+ z-index: 999998;
1737
+ transition: all 0.1s;
1738
+ }
1739
+
1740
+ .grain-tracker-highlight {
1741
+ position: absolute;
1742
+ pointer-events: none;
1743
+ border: 2px solid #6366f1;
1744
+ background: rgba(99, 102, 241, 0.08);
1745
+ z-index: 999997;
1746
+ transition: opacity 0.2s;
1747
+ }
1748
+
1749
+ .grain-tracker-label {
1750
+ position: absolute;
1751
+ top: -28px;
1752
+ left: 0;
1753
+ background: #6366f1;
1754
+ color: white;
1755
+ padding: 4px 10px;
1756
+ border-radius: 6px;
1757
+ font-size: 11px;
1758
+ font-weight: 600;
1759
+ white-space: nowrap;
1760
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
1761
+ pointer-events: none;
1762
+ }
1763
+
1764
+ .grain-tracker-label::after {
1765
+ content: '';
1766
+ position: absolute;
1767
+ bottom: -4px;
1768
+ left: 10px;
1769
+ width: 0;
1770
+ height: 0;
1771
+ border-left: 4px solid transparent;
1772
+ border-right: 4px solid transparent;
1773
+ border-top: 4px solid #6366f1;
1774
+ }
1775
+
1776
+ .grain-trackers-list {
1777
+ position: fixed;
1778
+ bottom: 80px;
1779
+ right: 20px;
1780
+ background: white;
1781
+ border: 1.5px solid #e2e8f0;
1782
+ border-radius: 10px;
1783
+ padding: 12px;
1784
+ max-height: 400px;
1785
+ width: 320px;
1786
+ overflow-y: auto;
1787
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1), 0 2px 8px rgba(0, 0, 0, 0.06);
1788
+ z-index: 999998;
1789
+ }
1790
+
1791
+ .grain-tracker-item {
1792
+ padding: 10px;
1793
+ background: #f8fafc;
1794
+ border-radius: 8px;
1795
+ margin-bottom: 8px;
1796
+ cursor: pointer;
1797
+ transition: all 0.2s;
1798
+ }
1799
+
1800
+ .grain-tracker-item:hover {
1801
+ background: #f1f5f9;
1802
+ transform: translateX(4px);
1803
+ }
1804
+
1805
+ .grain-tracker-item:last-child {
1806
+ margin-bottom: 0;
1807
+ }
1808
+
1809
+ .grain-tracker-name {
1810
+ font-weight: 600;
1811
+ color: #1e293b;
1812
+ font-size: 13px;
1813
+ margin-bottom: 4px;
1814
+ }
1815
+
1816
+ .grain-tracker-details {
1817
+ font-size: 11px;
1818
+ color: #64748b;
1819
+ display: flex;
1820
+ gap: 8px;
1821
+ align-items: center;
1822
+ }
1823
+
1824
+ .grain-tracker-type {
1825
+ background: #dbeafe;
1826
+ color: #1e40af;
1827
+ padding: 2px 6px;
1828
+ border-radius: 4px;
1829
+ font-weight: 600;
1830
+ text-transform: uppercase;
1831
+ font-size: 9px;
1832
+ letter-spacing: 0.5px;
1833
+ }
1834
+ </style>
1835
+ <div class="grain-toolbar-inner">
1836
+ <div class="grain-toolbar-header" id="grain-toolbar-handle">
1837
+ <div class="grain-toolbar-title">Grain Debug</div>
1838
+ </div>
1839
+ <div class="grain-toolbar-body">
1840
+ <div class="grain-toolbar-stats">
1841
+ <div class="grain-stat">
1842
+ <div class="grain-stat-value">${this.existingTrackers.length}</div>
1843
+ <div class="grain-stat-label">trackers</div>
1844
+ </div>
1845
+ <div class="grain-stat">
1846
+ <div class="grain-stat-value">${this.existingTrackers.filter((t) => t.isEnabled).length}</div>
1847
+ <div class="grain-stat-label">active</div>
1848
+ </div>
1849
+ </div>
1850
+ <div class="grain-toolbar-actions">
1851
+ <button id="grain-debug-inspect" type="button">
1852
+ + New
1853
+ </button>
1854
+ <button id="grain-debug-trackers" class="active" type="button">
1855
+ Hide
1856
+ </button>
1857
+ <button id="grain-debug-end" class="danger" type="button">
1858
+ End Session
1859
+ </button>
1860
+ </div>
1861
+ </div>
1862
+ </div>
1863
+ <div id="grain-trackers-list-container"></div>
1864
+ `;
1865
+ document.body.appendChild(toolbar);
1866
+ this.toolbarElement = toolbar;
1867
+ const handle = toolbar.querySelector("#grain-toolbar-handle");
1868
+ if (handle) {
1869
+ handle.addEventListener("mousedown", (e) => this.startDrag(e));
1870
+ }
1871
+ const inspectBtn = toolbar.querySelector("#grain-debug-inspect");
1872
+ const trackersBtn = toolbar.querySelector("#grain-debug-trackers");
1873
+ const endBtn = toolbar.querySelector("#grain-debug-end");
1874
+ if (inspectBtn) {
1875
+ inspectBtn.addEventListener("click", () => this.toggleInspectMode());
1876
+ }
1877
+ if (trackersBtn) {
1878
+ trackersBtn.addEventListener("click", () => this.toggleTrackerView());
1879
+ }
1880
+ if (endBtn) {
1881
+ endBtn.addEventListener("click", () => this.endDebug());
1882
+ }
1883
+ this.log("Toolbar shown");
1884
+ }
1885
+ /**
1886
+ * Create highlight element for hovering
1887
+ */
1888
+ createHighlightElement() {
1889
+ if (this.highlightElement)
1890
+ return;
1891
+ const highlight = document.createElement("div");
1892
+ highlight.className = "grain-debug-highlight";
1893
+ highlight.style.display = "none";
1894
+ document.body.appendChild(highlight);
1895
+ this.highlightElement = highlight;
1896
+ }
1897
+ /**
1898
+ * Start dragging the toolbar
1899
+ */
1900
+ startDrag(e) {
1901
+ if (!this.toolbarElement)
1902
+ return;
1903
+ this.isDragging = true;
1904
+ this.dragStartX = e.clientX;
1905
+ this.dragStartY = e.clientY;
1906
+ const rect = this.toolbarElement.getBoundingClientRect();
1907
+ this.toolbarStartX = rect.left;
1908
+ this.toolbarStartY = rect.top;
1909
+ this.toolbarElement.classList.add("dragging");
1910
+ this.dragMoveListener = (e2) => this.onDrag(e2);
1911
+ this.dragEndListener = () => this.endDrag();
1912
+ document.addEventListener("mousemove", this.dragMoveListener);
1913
+ document.addEventListener("mouseup", this.dragEndListener);
1914
+ }
1915
+ /**
1916
+ * Handle drag movement
1917
+ */
1918
+ onDrag(e) {
1919
+ if (!this.isDragging || !this.toolbarElement)
1920
+ return;
1921
+ const deltaX = e.clientX - this.dragStartX;
1922
+ const deltaY = e.clientY - this.dragStartY;
1923
+ const newX = this.toolbarStartX + deltaX;
1924
+ const newY = this.toolbarStartY + deltaY;
1925
+ const maxX = window.innerWidth - this.toolbarElement.offsetWidth;
1926
+ const maxY = window.innerHeight - this.toolbarElement.offsetHeight;
1927
+ const clampedX = Math.max(0, Math.min(newX, maxX));
1928
+ const clampedY = Math.max(0, Math.min(newY, maxY));
1929
+ this.toolbarElement.style.left = `${clampedX}px`;
1930
+ this.toolbarElement.style.top = `${clampedY}px`;
1931
+ this.toolbarElement.style.right = "auto";
1932
+ this.toolbarElement.style.bottom = "auto";
1933
+ }
1934
+ /**
1935
+ * End dragging
1936
+ */
1937
+ endDrag() {
1938
+ if (!this.isDragging)
1939
+ return;
1940
+ this.isDragging = false;
1941
+ if (this.toolbarElement) {
1942
+ this.toolbarElement.classList.remove("dragging");
1943
+ }
1944
+ if (this.dragMoveListener) {
1945
+ document.removeEventListener("mousemove", this.dragMoveListener);
1946
+ this.dragMoveListener = null;
1947
+ }
1948
+ if (this.dragEndListener) {
1949
+ document.removeEventListener("mouseup", this.dragEndListener);
1950
+ this.dragEndListener = null;
1951
+ }
1952
+ }
1953
+ /**
1954
+ * Toggle tracker view
1955
+ */
1956
+ toggleTrackerView() {
1957
+ this.showTrackers = !this.showTrackers;
1958
+ const trackersBtn = document.querySelector("#grain-debug-trackers");
1959
+ if (trackersBtn) {
1960
+ trackersBtn.textContent = this.showTrackers ? "Hide" : "View";
1961
+ trackersBtn.classList.toggle("active", this.showTrackers);
1962
+ }
1963
+ if (this.showTrackers) {
1964
+ this.showTrackerHighlights();
1965
+ this.showTrackersList();
1966
+ } else {
1967
+ this.hideTrackerHighlights();
1968
+ this.hideTrackersList();
1969
+ }
1970
+ }
1971
+ /**
1972
+ * Show tracker highlights on page
1973
+ */
1974
+ showTrackerHighlights() {
1975
+ this.hideTrackerHighlights();
1976
+ for (const tracker of this.existingTrackers) {
1977
+ if (!tracker.isEnabled)
1978
+ continue;
1979
+ try {
1980
+ const element = this.findElementBySelector(tracker.selector);
1981
+ if (!element)
1982
+ continue;
1983
+ const rect = element.getBoundingClientRect();
1984
+ const highlight = document.createElement("div");
1985
+ highlight.className = "grain-tracker-highlight";
1986
+ const label = document.createElement("div");
1987
+ label.className = "grain-tracker-label";
1988
+ label.textContent = tracker.name;
1989
+ highlight.style.top = `${rect.top + window.scrollY}px`;
1990
+ highlight.style.left = `${rect.left + window.scrollX}px`;
1991
+ highlight.style.width = `${rect.width}px`;
1992
+ highlight.style.height = `${rect.height}px`;
1993
+ highlight.appendChild(label);
1994
+ document.body.appendChild(highlight);
1995
+ this.trackerHighlights.push(highlight);
1996
+ } catch (error) {
1997
+ this.log("Failed to highlight tracker:", tracker.name, error);
1998
+ }
1999
+ }
2000
+ }
2001
+ /**
2002
+ * Hide tracker highlights
2003
+ */
2004
+ hideTrackerHighlights() {
2005
+ for (const highlight of this.trackerHighlights) {
2006
+ highlight.remove();
2007
+ }
2008
+ this.trackerHighlights = [];
2009
+ }
2010
+ /**
2011
+ * Show trackers list
2012
+ */
2013
+ showTrackersList() {
2014
+ let list = document.querySelector(".grain-trackers-list");
2015
+ if (this.existingTrackers.length === 0) {
2016
+ if (list)
2017
+ list.remove();
2018
+ return;
2019
+ }
2020
+ if (!list) {
2021
+ list = document.createElement("div");
2022
+ list.className = "grain-trackers-list";
2023
+ document.body.appendChild(list);
2024
+ }
2025
+ list.innerHTML = `
2026
+ ${this.existingTrackers.map((tracker) => `
2027
+ <div class="grain-tracker-item" data-tracker-id="${tracker.trackerId}">
2028
+ <div class="grain-tracker-name">${tracker.name}</div>
2029
+ <div class="grain-tracker-details">
2030
+ <span class="grain-tracker-type">${tracker.type}</span>
2031
+ <span>${tracker.urlScope}</span>
2032
+ ${!tracker.isEnabled ? '<span style="color: #ef4444;">\u2022 Disabled</span>' : ""}
2033
+ </div>
2034
+ </div>
2035
+ `).join("")}
2036
+ `;
2037
+ list.querySelectorAll(".grain-tracker-item").forEach((item) => {
2038
+ item.addEventListener("click", () => {
2039
+ const trackerId = item.getAttribute("data-tracker-id");
2040
+ const tracker = this.existingTrackers.find((t) => t.trackerId === trackerId);
2041
+ if (tracker) {
2042
+ this.scrollToTracker(tracker);
2043
+ }
2044
+ });
2045
+ });
2046
+ }
2047
+ /**
2048
+ * Hide trackers list
2049
+ */
2050
+ hideTrackersList() {
2051
+ const list = document.querySelector(".grain-trackers-list");
2052
+ if (list) {
2053
+ list.remove();
2054
+ }
2055
+ }
2056
+ /**
2057
+ * Scroll to and highlight a tracker element
2058
+ */
2059
+ scrollToTracker(tracker) {
2060
+ try {
2061
+ const element = this.findElementBySelector(tracker.selector);
2062
+ if (element) {
2063
+ element.scrollIntoView({ behavior: "smooth", block: "center" });
2064
+ const highlight = this.trackerHighlights.find((h) => {
2065
+ const rect = element.getBoundingClientRect();
2066
+ const hRect = h.getBoundingClientRect();
2067
+ return Math.abs(hRect.top - rect.top) < 5;
2068
+ });
2069
+ if (highlight) {
2070
+ highlight.style.opacity = "0";
2071
+ setTimeout(() => {
2072
+ highlight.style.opacity = "1";
2073
+ }, 100);
2074
+ setTimeout(() => {
2075
+ highlight.style.opacity = "0";
2076
+ }, 300);
2077
+ setTimeout(() => {
2078
+ highlight.style.opacity = "1";
2079
+ }, 500);
2080
+ }
2081
+ }
2082
+ } catch (error) {
2083
+ this.log("Failed to scroll to tracker:", error);
2084
+ }
2085
+ }
2086
+ /**
2087
+ * Find element by XPath selector
2088
+ */
2089
+ findElementBySelector(selector) {
2090
+ try {
2091
+ const result = document.evaluate(
2092
+ selector,
2093
+ document,
2094
+ null,
2095
+ XPathResult.FIRST_ORDERED_NODE_TYPE,
2096
+ null
2097
+ );
2098
+ return result.singleNodeValue;
2099
+ } catch (error) {
2100
+ this.log("Failed to find element:", error);
2101
+ return null;
2102
+ }
2103
+ }
2104
+ /**
2105
+ * Toggle inspect mode
2106
+ */
2107
+ toggleInspectMode() {
2108
+ if (this.isInspectMode) {
2109
+ this.disableInspectMode();
2110
+ } else {
2111
+ this.enableInspectMode();
2112
+ }
2113
+ }
2114
+ /**
2115
+ * Enable element inspection mode
2116
+ */
2117
+ enableInspectMode() {
2118
+ if (this.isInspectMode)
2119
+ return;
2120
+ this.log("Enabling inspect mode");
2121
+ this.isInspectMode = true;
2122
+ const inspectBtn = document.querySelector("#grain-debug-inspect");
2123
+ if (inspectBtn) {
2124
+ inspectBtn.classList.add("active");
2125
+ inspectBtn.textContent = "Click Element";
2126
+ }
2127
+ this.mouseMoveListener = (e) => this.handleMouseMove(e);
2128
+ this.clickListener = (e) => this.handleElementClick(e);
2129
+ document.addEventListener("mousemove", this.mouseMoveListener, true);
2130
+ document.addEventListener("click", this.clickListener, true);
2131
+ document.addEventListener("keydown", this.handleEscapeKey);
2132
+ }
2133
+ /**
2134
+ * Disable element inspection mode
2135
+ */
2136
+ disableInspectMode() {
2137
+ if (!this.isInspectMode)
2138
+ return;
2139
+ this.log("Disabling inspect mode");
2140
+ this.isInspectMode = false;
2141
+ const inspectBtn = document.querySelector("#grain-debug-inspect");
2142
+ if (inspectBtn) {
2143
+ inspectBtn.classList.remove("active");
2144
+ inspectBtn.textContent = "+ New";
2145
+ }
2146
+ if (this.mouseMoveListener) {
2147
+ document.removeEventListener("mousemove", this.mouseMoveListener, true);
2148
+ this.mouseMoveListener = null;
2149
+ }
2150
+ if (this.clickListener) {
2151
+ document.removeEventListener("click", this.clickListener, true);
2152
+ this.clickListener = null;
2153
+ }
2154
+ document.removeEventListener("keydown", this.handleEscapeKey);
2155
+ if (this.highlightElement) {
2156
+ this.highlightElement.style.display = "none";
2157
+ }
2158
+ }
2159
+ /**
2160
+ * Handle mouse move to highlight hovered element
2161
+ */
2162
+ handleMouseMove(e) {
2163
+ if (!this.isInspectMode || !this.highlightElement)
2164
+ return;
2165
+ const target = e.target;
2166
+ if (target.closest("#grain-debug-toolbar") || target.closest("#grain-debug-panel") || target.closest(".grain-trackers-list")) {
2167
+ this.highlightElement.style.display = "none";
2168
+ return;
2169
+ }
2170
+ const element = e.target;
2171
+ const rect = element.getBoundingClientRect();
2172
+ this.highlightElement.style.display = "block";
2173
+ this.highlightElement.style.top = `${rect.top + window.scrollY}px`;
2174
+ this.highlightElement.style.left = `${rect.left + window.scrollX}px`;
2175
+ this.highlightElement.style.width = `${rect.width}px`;
2176
+ this.highlightElement.style.height = `${rect.height}px`;
2177
+ }
2178
+ /**
2179
+ * Handle element click to show creation panel
2180
+ */
2181
+ handleElementClick(e) {
2182
+ if (!this.isInspectMode)
2183
+ return;
2184
+ const target = e.target;
2185
+ if (target.closest("#grain-debug-toolbar") || target.closest(".grain-trackers-list")) {
2186
+ this.disableInspectMode();
2187
+ return;
2188
+ }
2189
+ if (target.closest("#grain-debug-panel")) {
2190
+ e.preventDefault();
2191
+ e.stopPropagation();
2192
+ return;
2193
+ }
2194
+ e.preventDefault();
2195
+ e.stopPropagation();
2196
+ this.selectedElement = target;
2197
+ this.disableInspectMode();
2198
+ this.showCreationPanel(target);
2199
+ }
2200
+ /**
2201
+ * Show tracker creation panel
2202
+ */
2203
+ showCreationPanel(element) {
2204
+ if (this.panelElement) {
2205
+ this.panelElement.remove();
2206
+ }
2207
+ const panel = document.createElement("div");
2208
+ panel.id = "grain-debug-panel";
2209
+ const tagName = element.tagName.toLowerCase();
2210
+ const elementId = element.id;
2211
+ const elementText = element.textContent?.trim().substring(0, 50) || "";
2212
+ const xpath = this.getXPathForElement(element);
2213
+ panel.innerHTML = `
2214
+ <style>
2215
+ #grain-debug-panel {
2216
+ position: fixed;
2217
+ top: 50%;
2218
+ left: 50%;
2219
+ transform: translate(-50%, -50%);
2220
+ background: repeating-linear-gradient(
2221
+ 45deg,
2222
+ #fbbf24,
2223
+ #fbbf24 10px,
2224
+ #1e293b 10px,
2225
+ #1e293b 20px
2226
+ );
2227
+ border: 2px solid #1e293b;
2228
+ border-radius: 16px;
2229
+ padding: 6px;
2230
+ width: 420px;
2231
+ max-width: 90vw;
2232
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25), 0 8px 24px rgba(0, 0, 0, 0.15);
2233
+ z-index: 1000000;
2234
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2235
+ }
2236
+
2237
+ .grain-panel-inner {
2238
+ background: white;
2239
+ border-radius: 10px;
2240
+ overflow: hidden;
2241
+ }
2242
+
2243
+ .grain-panel-header {
2244
+ background: #1e293b;
2245
+ padding: 14px 18px;
2246
+ color: white;
2247
+ }
2248
+
2249
+ .grain-panel-header h3 {
2250
+ margin: 0 0 4px 0;
2251
+ font-size: 16px;
2252
+ font-weight: 700;
2253
+ letter-spacing: 0.5px;
2254
+ color: #fbbf24;
2255
+ text-transform: uppercase;
2256
+ }
2257
+
2258
+ .grain-panel-header p {
2259
+ margin: 0;
2260
+ font-size: 12px;
2261
+ opacity: 0.85;
2262
+ color: white;
2263
+ }
2264
+
2265
+ .grain-panel-body {
2266
+ padding: 18px;
2267
+ }
2268
+
2269
+ #grain-debug-panel .element-preview {
2270
+ background: #f8fafc;
2271
+ border: 1.5px solid #e2e8f0;
2272
+ border-radius: 8px;
2273
+ padding: 10px 12px;
2274
+ margin-bottom: 16px;
2275
+ font-size: 11px;
2276
+ color: #475569;
2277
+ }
2278
+
2279
+ #grain-debug-panel .element-preview div {
2280
+ margin-bottom: 4px;
2281
+ }
2282
+
2283
+ #grain-debug-panel .element-preview div:last-child {
2284
+ margin-bottom: 0;
2285
+ }
2286
+
2287
+ #grain-debug-panel .element-preview strong {
2288
+ color: #1e293b;
2289
+ font-weight: 600;
2290
+ }
2291
+
2292
+ #grain-debug-panel label {
2293
+ display: block;
2294
+ color: #1e293b;
2295
+ font-size: 12px;
2296
+ font-weight: 600;
2297
+ margin-bottom: 6px;
2298
+ }
2299
+
2300
+ #grain-debug-panel input,
2301
+ #grain-debug-panel select {
2302
+ width: 100%;
2303
+ background: white;
2304
+ border: 1.5px solid #e2e8f0;
2305
+ border-radius: 8px;
2306
+ padding: 9px 12px;
2307
+ color: #1e293b;
2308
+ font-size: 13px;
2309
+ margin-bottom: 14px;
2310
+ box-sizing: border-box;
2311
+ transition: all 0.2s;
2312
+ }
2313
+
2314
+ #grain-debug-panel input:focus,
2315
+ #grain-debug-panel select:focus {
2316
+ outline: none;
2317
+ border-color: #fbbf24;
2318
+ box-shadow: 0 0 0 3px rgba(251, 191, 36, 0.1);
2319
+ }
2320
+
2321
+ #grain-debug-panel .button-group {
2322
+ display: flex;
2323
+ gap: 8px;
2324
+ margin-top: 18px;
2325
+ }
2326
+
2327
+ #grain-debug-panel button {
2328
+ flex: 1;
2329
+ padding: 10px 16px;
2330
+ border: none;
2331
+ border-radius: 8px;
2332
+ font-size: 13px;
2333
+ font-weight: 600;
2334
+ cursor: pointer;
2335
+ transition: all 0.2s;
2336
+ }
2337
+
2338
+ #grain-debug-panel button.primary {
2339
+ background: #fbbf24;
2340
+ color: #1e293b;
2341
+ box-shadow: 0 2px 8px rgba(251, 191, 36, 0.3);
2342
+ }
2343
+
2344
+ #grain-debug-panel button.primary:hover {
2345
+ background: #f59e0b;
2346
+ transform: translateY(-1px);
2347
+ box-shadow: 0 4px 12px rgba(251, 191, 36, 0.4);
2348
+ }
2349
+
2350
+ #grain-debug-panel button.secondary {
2351
+ background: white;
2352
+ border: 1.5px solid #e2e8f0;
2353
+ color: #475569;
2354
+ }
2355
+
2356
+ #grain-debug-panel button.secondary:hover {
2357
+ background: #f8fafc;
2358
+ border-color: #cbd5e1;
2359
+ }
2360
+
2361
+ #grain-debug-panel .url-pattern-input {
2362
+ display: none;
2363
+ }
2364
+
2365
+ #grain-debug-panel .url-pattern-input.visible {
2366
+ display: block;
2367
+ }
2368
+ </style>
2369
+ <div class="grain-panel-inner">
2370
+ <div class="grain-panel-header">
2371
+ <h3>Create Tracker</h3>
2372
+ <p>Set up automatic tracking for this element</p>
2373
+ </div>
2374
+ <div class="grain-panel-body">
2375
+ <div class="element-preview">
2376
+ <div><strong>Element:</strong> ${tagName}${elementId ? `#${elementId}` : ""}</div>
2377
+ ${elementText ? `<div><strong>Text:</strong> ${elementText}</div>` : ""}
2378
+ </div>
2379
+ <div>
2380
+ <label>Event Name</label>
2381
+ <input type="text" id="grain-event-name" placeholder="e.g., signup_button_click" value="" />
2382
+ </div>
2383
+ <div>
2384
+ <label>Type</label>
2385
+ <select id="grain-event-type">
2386
+ <option value="metric">Metric</option>
2387
+ <option value="conversion">Conversion</option>
2388
+ </select>
2389
+ </div>
2390
+ <div>
2391
+ <label>URL Scope</label>
2392
+ <select id="grain-url-scope">
2393
+ <option value="all">All Pages</option>
2394
+ <option value="contains" selected>This Page</option>
2395
+ <option value="equals">Exact URL</option>
2396
+ </select>
2397
+ </div>
2398
+ <div class="url-pattern-input visible" id="grain-url-pattern-container">
2399
+ <label>URL Pattern</label>
2400
+ <input type="text" id="grain-url-pattern" placeholder="e.g., /pricing" value="${window.location.pathname}" />
2401
+ </div>
2402
+ <div class="button-group">
2403
+ <button type="button" class="secondary" id="grain-cancel">Cancel</button>
2404
+ <button type="button" class="primary" id="grain-create">\u2713 Create</button>
2405
+ </div>
2406
+ </div>
2407
+ </div>
2408
+ `;
2409
+ document.body.appendChild(panel);
2410
+ this.panelElement = panel;
2411
+ const eventNameInput = panel.querySelector("#grain-event-name");
2412
+ if (eventNameInput) {
2413
+ const suggestedName = this.generateEventName(element);
2414
+ eventNameInput.value = suggestedName;
2415
+ eventNameInput.select();
2416
+ }
2417
+ const urlScopeSelect = panel.querySelector("#grain-url-scope");
2418
+ const urlPatternContainer = panel.querySelector("#grain-url-pattern-container");
2419
+ const urlPatternInput = panel.querySelector("#grain-url-pattern");
2420
+ if (urlScopeSelect && urlPatternContainer) {
2421
+ urlScopeSelect.addEventListener("change", () => {
2422
+ if (urlScopeSelect.value === "all") {
2423
+ urlPatternContainer.classList.remove("visible");
2424
+ } else {
2425
+ urlPatternContainer.classList.add("visible");
2426
+ if (urlPatternInput && !urlPatternInput.value) {
2427
+ urlPatternInput.value = window.location.pathname;
2428
+ }
2429
+ }
2430
+ });
2431
+ }
2432
+ const cancelBtn = panel.querySelector("#grain-cancel");
2433
+ const createBtn = panel.querySelector("#grain-create");
2434
+ if (cancelBtn) {
2435
+ cancelBtn.addEventListener("click", () => this.hideCreationPanel());
2436
+ }
2437
+ if (createBtn) {
2438
+ createBtn.addEventListener("click", () => this.handleCreateTracker(xpath));
2439
+ }
2440
+ }
2441
+ /**
2442
+ * Generate suggested event name from element
2443
+ */
2444
+ generateEventName(element) {
2445
+ const tagName = element.tagName.toLowerCase();
2446
+ const elementId = element.id;
2447
+ const elementText = element.textContent?.trim().toLowerCase().replace(/\s+/g, "_").substring(0, 30) || "";
2448
+ if (elementId) {
2449
+ return `${elementId}_click`;
2450
+ } else if (elementText) {
2451
+ return `${elementText}_click`;
2452
+ } else if (tagName === "button") {
2453
+ return "button_click";
2454
+ } else if (tagName === "a") {
2455
+ return "link_click";
2456
+ } else {
2457
+ return `${tagName}_click`;
2458
+ }
2459
+ }
2460
+ /**
2461
+ * Handle tracker creation
2462
+ */
2463
+ async handleCreateTracker(selector) {
2464
+ if (!this.panelElement)
2465
+ return;
2466
+ const eventNameInput = this.panelElement.querySelector("#grain-event-name");
2467
+ const eventTypeSelect = this.panelElement.querySelector("#grain-event-type");
2468
+ const urlScopeSelect = this.panelElement.querySelector("#grain-url-scope");
2469
+ const urlPatternInput = this.panelElement.querySelector("#grain-url-pattern");
2470
+ if (!eventNameInput || !eventTypeSelect || !urlScopeSelect)
2471
+ return;
2472
+ const eventName = eventNameInput.value.trim();
2473
+ const eventType = eventTypeSelect.value;
2474
+ const urlScope = urlScopeSelect.value;
2475
+ const urlPattern = urlPatternInput?.value.trim() || void 0;
2476
+ if (!eventName) {
2477
+ alert("Please enter an event name");
2478
+ return;
2479
+ }
2480
+ if (!eventName.match(/^[a-zA-Z0-9_-]+$/)) {
2481
+ alert("Event name can only contain letters, numbers, underscores, and hyphens");
2482
+ return;
2483
+ }
2484
+ if ((urlScope === "contains" || urlScope === "equals") && !urlPattern) {
2485
+ alert("Please enter a URL pattern");
2486
+ return;
2487
+ }
2488
+ try {
2489
+ const createBtn = this.panelElement.querySelector("#grain-create");
2490
+ if (createBtn) {
2491
+ createBtn.textContent = "Creating...";
2492
+ createBtn.disabled = true;
2493
+ }
2494
+ await this.createTracker(eventName, eventType, selector, urlScope, urlPattern);
2495
+ this.hideCreationPanel();
2496
+ this.showSuccessMessage(`Tracker "${eventName}" created successfully!`);
2497
+ await this.loadExistingTrackers();
2498
+ this.updateToolbarStats();
2499
+ if (this.showTrackers) {
2500
+ this.showTrackerHighlights();
2501
+ this.showTrackersList();
2502
+ }
2503
+ this.log("Tracker created:", eventName);
2504
+ } catch (error) {
2505
+ alert("Failed to create tracker. Please try again.");
2506
+ this.log("Failed to create tracker:", error);
2507
+ const createBtn = this.panelElement.querySelector("#grain-create");
2508
+ if (createBtn) {
2509
+ createBtn.textContent = "Create Tracker";
2510
+ createBtn.disabled = false;
2511
+ }
2512
+ }
2513
+ }
2514
+ /**
2515
+ * Create tracker via API
2516
+ */
2517
+ async createTracker(name, type, selector, urlScope, urlPattern) {
2518
+ const url = `${this.apiUrl}/v1/tenant/${encodeURIComponent(this.tenantId)}/debug-sessions/${this.sessionId}/trackers`;
2519
+ const response = await fetch(url, {
2520
+ method: "POST",
2521
+ headers: {
2522
+ "Content-Type": "application/json"
2523
+ },
2524
+ body: JSON.stringify({
2525
+ name,
2526
+ type,
2527
+ selector,
2528
+ urlScope,
2529
+ urlPattern
2530
+ })
2531
+ });
2532
+ if (!response.ok) {
2533
+ throw new Error(`Failed to create tracker: ${response.status}`);
2534
+ }
2535
+ }
2536
+ /**
2537
+ * Hide creation panel
2538
+ */
2539
+ hideCreationPanel() {
2540
+ if (this.panelElement) {
2541
+ this.panelElement.remove();
2542
+ this.panelElement = null;
2543
+ }
2544
+ this.selectedElement = null;
2545
+ }
2546
+ /**
2547
+ * Show success message
2548
+ */
2549
+ showSuccessMessage(message) {
2550
+ const toast = document.createElement("div");
2551
+ toast.style.cssText = `
2552
+ position: fixed;
2553
+ top: 20px;
2554
+ left: 50%;
2555
+ transform: translateX(-50%) translateY(-20px);
2556
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
2557
+ color: white;
2558
+ padding: 14px 24px;
2559
+ border-radius: 12px;
2560
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2561
+ font-size: 14px;
2562
+ font-weight: 600;
2563
+ box-shadow: 0 12px 32px rgba(16, 185, 129, 0.3), 0 4px 12px rgba(0, 0, 0, 0.15);
2564
+ z-index: 1000000;
2565
+ display: flex;
2566
+ align-items: center;
2567
+ gap: 10px;
2568
+ animation: slideDown 0.3s ease-out forwards;
2569
+ `;
2570
+ toast.innerHTML = `
2571
+ <style>
2572
+ @keyframes slideDown {
2573
+ to {
2574
+ transform: translateX(-50%) translateY(0);
2575
+ }
2576
+ }
2577
+ </style>
2578
+ <span style="font-size: 18px;">\u2713</span>
2579
+ <span>${message}</span>
2580
+ `;
2581
+ document.body.appendChild(toast);
2582
+ setTimeout(() => {
2583
+ toast.style.animation = "slideDown 0.3s ease-in reverse";
2584
+ setTimeout(() => {
2585
+ toast.remove();
2586
+ }, 300);
2587
+ }, 2700);
2588
+ }
2589
+ /**
2590
+ * End debug session
2591
+ */
2592
+ async endDebug() {
2593
+ try {
2594
+ const url2 = `${this.apiUrl}/v1/tenant/${encodeURIComponent(this.tenantId)}/debug-sessions/${this.sessionId}/end`;
2595
+ await fetch(url2, {
2596
+ method: "POST",
2597
+ headers: {
2598
+ "Content-Type": "application/json"
2599
+ }
2600
+ });
2601
+ } catch (error) {
2602
+ this.log("Failed to end debug session:", error);
2603
+ }
2604
+ this.destroy();
2605
+ const url = new URL(window.location.href);
2606
+ url.searchParams.delete("grain_debug");
2607
+ url.searchParams.delete("grain_session");
2608
+ window.location.href = url.toString();
2609
+ }
2610
+ /**
2611
+ * Get XPath for element
2612
+ */
2613
+ getXPathForElement(element) {
2614
+ if (element.id) {
2615
+ return `//*[@id="${element.id}"]`;
2616
+ }
2617
+ const parts = [];
2618
+ let current = element;
2619
+ while (current && current.nodeType === Node.ELEMENT_NODE) {
2620
+ let index = 0;
2621
+ let sibling = current;
2622
+ while (sibling) {
2623
+ if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === current.tagName) {
2624
+ index++;
2625
+ }
2626
+ sibling = sibling.previousElementSibling;
2627
+ }
2628
+ const tagName = current.tagName.toLowerCase();
2629
+ const pathIndex = index > 1 ? `[${index}]` : "";
2630
+ parts.unshift(`${tagName}${pathIndex}`);
2631
+ current = current.parentElement;
2632
+ }
2633
+ return parts.length ? `/${parts.join("/")}` : "";
2634
+ }
2635
+ /**
2636
+ * Log debug messages
2637
+ */
2638
+ log(...args) {
2639
+ if (this.config.debug) {
2640
+ console.log("[DebugAgent]", ...args);
2641
+ }
2642
+ }
2643
+ /**
2644
+ * Update toolbar stats
2645
+ */
2646
+ updateToolbarStats() {
2647
+ if (!this.toolbarElement)
2648
+ return;
2649
+ const totalStat = this.toolbarElement.querySelector(".grain-stat:nth-child(1) .grain-stat-value");
2650
+ const activeStat = this.toolbarElement.querySelector(".grain-stat:nth-child(2) .grain-stat-value");
2651
+ if (totalStat) {
2652
+ totalStat.textContent = String(this.existingTrackers.length);
2653
+ }
2654
+ if (activeStat) {
2655
+ activeStat.textContent = String(this.existingTrackers.filter((t) => t.isEnabled).length);
2656
+ }
2657
+ }
2658
+ /**
2659
+ * Destroy the debug agent
2660
+ */
2661
+ destroy() {
2662
+ if (this.isDestroyed)
2663
+ return;
2664
+ this.log("Destroying debug agent");
2665
+ this.isDestroyed = true;
2666
+ this.disableInspectMode();
2667
+ this.hideTrackerHighlights();
2668
+ this.endDrag();
2669
+ if (this.toolbarElement) {
2670
+ this.toolbarElement.remove();
2671
+ this.toolbarElement = null;
2672
+ }
2673
+ if (this.panelElement) {
2674
+ this.panelElement.remove();
2675
+ this.panelElement = null;
2676
+ }
2677
+ if (this.highlightElement) {
2678
+ this.highlightElement.remove();
2679
+ this.highlightElement = null;
2680
+ }
2681
+ }
2682
+ };
2683
+ }
2684
+ });
2685
+
1461
2686
  // src/index.ts
1462
2687
  var src_exports = {};
1463
2688
  __export(src_exports, {
@@ -1475,7 +2700,7 @@ var Grain = (() => {
1475
2700
  var DEFAULT_CONSENT_CATEGORIES = ["necessary", "analytics", "functional"];
1476
2701
  var CONSENT_VERSION = "1.0.0";
1477
2702
  var ConsentManager = class {
1478
- constructor(tenantId, consentMode = "opt-out") {
2703
+ constructor(tenantId, consentMode = "cookieless") {
1479
2704
  this.consentState = null;
1480
2705
  this.listeners = [];
1481
2706
  this.consentMode = consentMode;
@@ -1485,9 +2710,8 @@ var Grain = (() => {
1485
2710
  /**
1486
2711
  * Load consent state from localStorage
1487
2712
  *
1488
- * GDPR Compliance: In opt-in mode, we can use localStorage for consent preferences
1489
- * since storing consent choices is a legitimate interest and necessary for compliance.
1490
- * The consent preference itself is not tracking data.
2713
+ * GDPR Compliance: localStorage only used for storing consent preferences
2714
+ * (not for tracking), which is a legitimate interest for compliance.
1491
2715
  */
1492
2716
  loadConsentState() {
1493
2717
  if (typeof window === "undefined")
@@ -1500,7 +2724,7 @@ var Grain = (() => {
1500
2724
  ...parsed,
1501
2725
  timestamp: new Date(parsed.timestamp)
1502
2726
  };
1503
- } else if (this.consentMode === "opt-out" || this.consentMode === "disabled") {
2727
+ } else if (this.consentMode === "gdpr-opt-out") {
1504
2728
  this.consentState = {
1505
2729
  granted: true,
1506
2730
  categories: DEFAULT_CONSENT_CATEGORIES,
@@ -1573,28 +2797,67 @@ var Grain = (() => {
1573
2797
  return this.consentState ? { ...this.consentState } : null;
1574
2798
  }
1575
2799
  /**
1576
- * Check if user has granted consent
2800
+ * Check if user has granted consent for permanent IDs
1577
2801
  */
1578
2802
  hasConsent(category) {
1579
- if (this.consentMode === "disabled") {
1580
- return true;
1581
- }
1582
- if (this.consentMode === "opt-in" && !this.consentState) {
2803
+ if (this.consentMode === "cookieless") {
1583
2804
  return false;
1584
2805
  }
1585
- if (!this.consentState?.granted) {
1586
- return false;
2806
+ if (this.consentMode === "gdpr-strict") {
2807
+ if (!this.consentState?.granted) {
2808
+ return false;
2809
+ }
2810
+ }
2811
+ if (this.consentMode === "gdpr-opt-out") {
2812
+ if (!this.consentState) {
2813
+ return true;
2814
+ }
2815
+ if (!this.consentState.granted) {
2816
+ return false;
2817
+ }
1587
2818
  }
1588
- if (category) {
2819
+ if (category && this.consentState) {
1589
2820
  return this.consentState.categories.includes(category);
1590
2821
  }
2822
+ return this.consentState?.granted ?? this.consentMode === "gdpr-opt-out";
2823
+ }
2824
+ /**
2825
+ * Check if permanent IDs are allowed
2826
+ */
2827
+ shouldUsePermanentId() {
2828
+ return this.hasConsent();
2829
+ }
2830
+ /**
2831
+ * Check if we should strip query parameters from URLs
2832
+ * Query params stripped unless:
2833
+ * - Mode is gdpr-opt-out, OR
2834
+ * - Mode is gdpr-strict AND consent given
2835
+ */
2836
+ shouldStripQueryParams() {
2837
+ if (this.consentMode === "cookieless") {
2838
+ return true;
2839
+ }
2840
+ if (this.consentMode === "gdpr-strict") {
2841
+ return !this.hasConsent();
2842
+ }
2843
+ if (this.consentMode === "gdpr-opt-out") {
2844
+ return false;
2845
+ }
2846
+ return true;
2847
+ }
2848
+ /**
2849
+ * Check if we can track events (always true in v2.0)
2850
+ * Even cookieless mode allows basic analytics with daily IDs
2851
+ */
2852
+ canTrack() {
1591
2853
  return true;
1592
2854
  }
1593
2855
  /**
1594
2856
  * Check if we should wait for consent before tracking
2857
+ * Only relevant for GDPR Strict mode
1595
2858
  */
1596
2859
  shouldWaitForConsent() {
1597
- return this.consentMode === "opt-in" && !this.consentState?.granted;
2860
+ return this.consentMode === "gdpr-strict" && !this.consentState?.granted;
1598
2861
  }
1599
2862
  /**
1600
2863
  * Add consent change listener
@@ -1636,6 +2899,19 @@ var Grain = (() => {
1636
2899
  } catch (error) {
1637
2900
  }
1638
2901
  }
2902
+ /**
2903
+ * Get current consent mode
2904
+ */
2905
+ getConsentMode() {
2906
+ return this.consentMode;
2907
+ }
2908
+ /**
2909
+ * Get ID mode based on consent state
2910
+ * Returns 'cookieless' or 'permanent'
2911
+ */
2912
+ getIdMode() {
2913
+ return this.shouldUsePermanentId() ? "permanent" : "cookieless";
2914
+ }
1639
2915
  };
1640
2916
 
1641
2917
  // src/cookies.ts
@@ -1678,36 +2954,6 @@ var Grain = (() => {
1678
2954
  }
1679
2955
  return null;
1680
2956
  }
1681
- function deleteCookie(name, config) {
1682
- if (typeof document === "undefined")
1683
- return;
1684
- const parts = [
1685
- `${encodeURIComponent(name)}=`,
1686
- "max-age=0"
1687
- ];
1688
- if (config?.domain) {
1689
- parts.push(`domain=${config.domain}`);
1690
- }
1691
- if (config?.path) {
1692
- parts.push(`path=${config.path}`);
1693
- } else {
1694
- parts.push("path=/");
1695
- }
1696
- document.cookie = parts.join("; ");
1697
- }
1698
- function areCookiesEnabled() {
1699
- if (typeof document === "undefined")
1700
- return false;
1701
- try {
1702
- const testCookie = "_grain_cookie_test";
1703
- setCookie(testCookie, "test", { maxAge: 1 });
1704
- const result = getCookie(testCookie) === "test";
1705
- deleteCookie(testCookie);
1706
- return result;
1707
- } catch {
1708
- return false;
1709
- }
1710
- }
1711
2957
 
1712
2958
  // src/activity.ts
1713
2959
  var ActivityDetector = class {
@@ -5087,7 +6333,7 @@ var Grain = (() => {
5087
6333
  };
5088
6334
  if (hasConsent) {
5089
6335
  properties.title = document.title || "";
5090
- properties.full_url = currentUrl;
6336
+ properties.full_url = this.cleanUrl(currentUrl);
5091
6337
  properties.session_id = this.tracker.getSessionId();
5092
6338
  if (referrer) {
5093
6339
  properties.referrer = referrer;
@@ -5192,14 +6438,18 @@ var Grain = (() => {
5192
6438
  }
5193
6439
  /**
5194
6440
  * Extract path from URL, optionally stripping query parameters
6441
+ * Privacy-first: strips query params by default
5195
6442
  */
5196
6443
  extractPath(url) {
5197
6444
  try {
5198
6445
  const urlObj = new URL(url);
5199
- let path = urlObj.pathname + urlObj.hash;
6446
+ let path = urlObj.pathname;
5200
6447
  if (!this.config.stripQueryParams && urlObj.search) {
5201
6448
  path += urlObj.search;
5202
6449
  }
6450
+ if (!this.config.stripHash && urlObj.hash) {
6451
+ path += urlObj.hash;
6452
+ }
5203
6453
  return path;
5204
6454
  } catch (error) {
5205
6455
  if (this.config.debug) {
@@ -5208,6 +6458,20 @@ var Grain = (() => {
5208
6458
  return url;
5209
6459
  }
5210
6460
  }
6461
+ /**
6462
+ * Clean URL for privacy (strip query params based on config)
6463
+ */
6464
+ cleanUrl(url) {
6465
+ if (!this.config.stripQueryParams) {
6466
+ return url;
6467
+ }
6468
+ try {
6469
+ const urlObj = new URL(url);
6470
+ return `${urlObj.origin}${urlObj.pathname}${this.config.stripHash ? "" : urlObj.hash}`;
6471
+ } catch (error) {
6472
+ return url;
6473
+ }
6474
+ }
5211
6475
  /**
5212
6476
  * Get the current page path
5213
6477
  */
@@ -5276,6 +6540,172 @@ var Grain = (() => {
5276
6540
  }
5277
6541
  };
5278
6542
 
6543
+ // src/id-manager.ts
6544
+ function simpleHash(str) {
6545
+ let hash = 0;
6546
+ for (let i = 0; i < str.length; i++) {
6547
+ const char = str.charCodeAt(i);
6548
+ hash = (hash << 5) - hash + char;
6549
+ hash = hash & hash;
6550
+ }
6551
+ return Math.abs(hash).toString(36);
6552
+ }
6553
+ function generateUUID() {
6554
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
6555
+ return crypto.randomUUID();
6556
+ }
6557
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
6558
+ const r = Math.random() * 16 | 0;
6559
+ const v = c === "x" ? r : r & 3 | 8;
6560
+ return v.toString(16);
6561
+ });
6562
+ }
6563
+ function getBrowserFingerprint() {
6564
+ if (typeof window === "undefined")
6565
+ return "server";
6566
+ const components = [
6567
+ screen.width?.toString() || "",
6568
+ screen.height?.toString() || "",
6569
+ navigator.language || "",
6570
+ Intl.DateTimeFormat().resolvedOptions().timeZone || ""
6571
+ ];
6572
+ return simpleHash(components.join("|"));
6573
+ }
6574
+ function getLocalDateString() {
6575
+ const now = /* @__PURE__ */ new Date();
6576
+ const year = now.getFullYear();
6577
+ const month = String(now.getMonth() + 1).padStart(2, "0");
6578
+ const day = String(now.getDate()).padStart(2, "0");
6579
+ return `${year}-${month}-${day}`;
6580
+ }
6581
+ var IdManager = class {
6582
+ constructor(config) {
6583
+ this.cachedDailyId = null;
6584
+ this.dailyIdDate = null;
6585
+ this.permanentId = null;
6586
+ this.config = config;
6587
+ if (config.mode === "permanent" && config.useLocalStorage) {
6588
+ this.loadPermanentId();
6589
+ }
6590
+ }
6591
+ /**
6592
+ * Generate a daily rotating ID
6593
+ * Rotates at midnight in user's local timezone
6594
+ * Provides same-day continuity without persistent tracking
6595
+ */
6596
+ generateDailyRotatingId() {
6597
+ const currentDate = getLocalDateString();
6598
+ if (this.cachedDailyId && this.dailyIdDate === currentDate) {
6599
+ return this.cachedDailyId;
6600
+ }
6601
+ const fingerprint = getBrowserFingerprint();
6602
+ const seed = `${this.config.tenantId}|${currentDate}|${fingerprint}`;
6603
+ const dailyId = `daily_${simpleHash(seed)}_${simpleHash(Date.now().toString())}`;
6604
+ this.cachedDailyId = dailyId;
6605
+ this.dailyIdDate = currentDate;
6606
+ return dailyId;
6607
+ }
6608
+ /**
6609
+ * Generate or retrieve permanent user ID
6610
+ * Only used when consent is given
6611
+ */
6612
+ generatePermanentId() {
6613
+ if (this.permanentId) {
6614
+ return this.permanentId;
6615
+ }
6616
+ if (this.config.useLocalStorage) {
6617
+ const stored = this.loadPermanentId();
6618
+ if (stored) {
6619
+ return stored;
6620
+ }
6621
+ }
6622
+ const newId = generateUUID();
6623
+ this.permanentId = newId;
6624
+ if (this.config.useLocalStorage) {
6625
+ this.savePermanentId(newId);
6626
+ }
6627
+ return newId;
6628
+ }
6629
+ /**
6630
+ * Get the current user ID based on mode
6631
+ */
6632
+ getCurrentUserId() {
6633
+ if (this.config.mode === "cookieless") {
6634
+ return this.generateDailyRotatingId();
6635
+ } else {
6636
+ return this.generatePermanentId();
6637
+ }
6638
+ }
6639
+ /**
6640
+ * Switch ID mode (e.g., when consent is granted/revoked)
6641
+ */
6642
+ setMode(mode) {
6643
+ this.config.mode = mode;
6644
+ if (mode === "permanent") {
6645
+ this.cachedDailyId = null;
6646
+ this.dailyIdDate = null;
6647
+ }
6648
+ if (mode === "cookieless") {
6649
+ this.permanentId = null;
6650
+ if (this.config.useLocalStorage) {
6651
+ this.clearPermanentId();
6652
+ }
6653
+ }
6654
+ }
6655
+ /**
6656
+ * Load permanent ID from localStorage
6657
+ */
6658
+ loadPermanentId() {
6659
+ if (typeof window === "undefined")
6660
+ return null;
6661
+ try {
6662
+ const storageKey = `grain_anonymous_user_id_${this.config.tenantId}`;
6663
+ const stored = localStorage.getItem(storageKey);
6664
+ if (stored) {
6665
+ this.permanentId = stored;
6666
+ return stored;
6667
+ }
6668
+ } catch (error) {
6669
+ }
6670
+ return null;
6671
+ }
6672
+ /**
6673
+ * Save permanent ID to localStorage
6674
+ */
6675
+ savePermanentId(id) {
6676
+ if (typeof window === "undefined")
6677
+ return;
6678
+ try {
6679
+ const storageKey = `grain_anonymous_user_id_${this.config.tenantId}`;
6680
+ localStorage.setItem(storageKey, id);
6681
+ } catch (error) {
6682
+ }
6683
+ }
6684
+ /**
6685
+ * Clear permanent ID from localStorage
6686
+ */
6687
+ clearPermanentId() {
6688
+ if (typeof window === "undefined")
6689
+ return;
6690
+ try {
6691
+ const storageKey = `grain_anonymous_user_id_${this.config.tenantId}`;
6692
+ localStorage.removeItem(storageKey);
6693
+ } catch (error) {
6694
+ }
6695
+ }
6696
+ /**
6697
+ * Get info about current ID for debugging
6698
+ */
6699
+ getIdInfo() {
6700
+ const id = this.getCurrentUserId();
6701
+ return {
6702
+ mode: this.config.mode,
6703
+ id,
6704
+ isDailyRotating: id.startsWith("daily_")
6705
+ };
6706
+ }
6707
+ };
6708
+
5279
6709
  // src/index.ts
5280
6710
  var GrainAnalytics = class {
5281
6711
  constructor(config) {
@@ -5285,12 +6715,14 @@ var Grain = (() => {
5285
6715
  this.isDestroyed = false;
5286
6716
  this.globalUserId = null;
5287
6717
  this.persistentAnonymousUserId = null;
6718
+ // Deprecated: use idManager instead
5288
6719
  // Remote Config properties
5289
6720
  this.configCache = null;
5290
6721
  this.configRefreshTimer = null;
5291
6722
  this.configChangeListeners = [];
5292
6723
  this.configFetchPromise = null;
5293
6724
  this.cookiesEnabled = false;
6725
+ // Deprecated: cookies no longer used for IDs
5294
6726
  // Automatic Tracking properties
5295
6727
  this.activityDetector = null;
5296
6728
  this.heartbeatManager = null;
@@ -5304,6 +6736,9 @@ var Grain = (() => {
5304
6736
  // Session tracking
5305
6737
  this.sessionStartTime = Date.now();
5306
6738
  this.sessionEventCount = 0;
6739
+ // Debug mode properties
6740
+ this.debugAgent = null;
6741
+ this.isDebugMode = false;
5307
6742
  this.config = {
5308
6743
  apiUrl: "https://api.grainql.com",
5309
6744
  authStrategy: "NONE",
@@ -5322,11 +6757,10 @@ var Grain = (() => {
5322
6757
  configRefreshInterval: 3e5,
5323
6758
  // 5 minutes
5324
6759
  enableConfigCache: true,
5325
- // Privacy defaults
5326
- consentMode: "opt-out",
6760
+ // Privacy defaults (v2.0)
6761
+ consentMode: "cookieless",
6762
+ // Default: privacy-first, no permanent tracking
5327
6763
  waitForConsent: false,
5328
- enableCookies: false,
5329
- anonymizeIP: false,
5330
6764
  disableAutoProperties: false,
5331
6765
  // Automatic Tracking defaults
5332
6766
  enableHeartbeat: true,
@@ -5336,28 +6770,31 @@ var Grain = (() => {
5336
6770
  // 5 minutes
5337
6771
  enableAutoPageView: true,
5338
6772
  stripQueryParams: true,
6773
+ // Privacy-first: strip by default
6774
+ stripHash: false,
5339
6775
  // Heatmap Tracking defaults
5340
6776
  enableHeatmapTracking: true,
5341
6777
  ...config,
5342
6778
  tenantId: config.tenantId
5343
6779
  };
5344
6780
  this.consentManager = new ConsentManager(this.config.tenantId, this.config.consentMode);
5345
- if (this.config.enableCookies) {
5346
- this.cookiesEnabled = areCookiesEnabled();
5347
- if (!this.cookiesEnabled && this.config.debug) {
5348
- console.warn("[Grain Analytics] Cookies are not available, falling back to localStorage");
5349
- }
5350
- }
6781
+ const idMode = this.consentManager.getIdMode();
6782
+ this.idManager = new IdManager({
6783
+ mode: idMode,
6784
+ tenantId: this.config.tenantId,
6785
+ useLocalStorage: true
6786
+ // For permanent IDs when consented
6787
+ });
5351
6788
  if (config.userId) {
5352
6789
  this.globalUserId = config.userId;
5353
6790
  }
5354
6791
  this.validateConfig();
5355
- this.initializePersistentAnonymousUserId();
5356
6792
  this.setupBeforeUnload();
5357
6793
  this.startFlushTimer();
5358
6794
  this.initializeConfigCache();
5359
6795
  this.ephemeralSessionId = this.generateUUID();
5360
6796
  if (typeof window !== "undefined") {
6797
+ this.checkAndInitializeDebugMode();
5361
6798
  this.initializeAutomaticTracking();
5362
6799
  this.trackSessionStart();
5363
6800
  if (this.config.enableHeatmapTracking) {
@@ -5365,6 +6802,8 @@ var Grain = (() => {
5365
6802
  }
5366
6803
  }
5367
6804
  this.consentManager.addListener((state) => {
6805
+ const idMode2 = this.consentManager.getIdMode();
6806
+ this.idManager.setMode(idMode2);
5368
6807
  if (state.granted) {
5369
6808
  this.handleConsentGranted();
5370
6809
  }
@@ -5405,10 +6844,12 @@ var Grain = (() => {
5405
6844
  */
5406
6845
  shouldAllowPersistentStorage() {
5407
6846
  const hasConsent = this.consentManager.hasConsent("analytics");
5408
- const isOptInMode = this.config.consentMode === "opt-in";
6847
+ const isCookieless = this.config.consentMode === "cookieless";
5409
6848
  const userExplicitlyIdentified = !!this.globalUserId;
5410
6849
  const isJWTAuth = this.config.authStrategy === "JWT";
5411
- return hasConsent || !isOptInMode || userExplicitlyIdentified || isJWTAuth;
6850
+ if (isCookieless)
6851
+ return false;
6852
+ return hasConsent || userExplicitlyIdentified || isJWTAuth;
5412
6853
  }
5413
6854
  /**
5414
6855
  * Generate a proper UUIDv4 identifier for anonymous user ID
@@ -5491,21 +6932,19 @@ var Grain = (() => {
5491
6932
  }
5492
6933
  }
5493
6934
  /**
5494
- * Get the effective user ID (global userId or persistent anonymous ID)
6935
+ * Get the effective user ID (v2.0)
5495
6936
  *
5496
- * GDPR Compliance: In opt-in mode without consent and no explicit user identification,
5497
- * this should not be called. Use getEphemeralSessionId() instead.
6937
+ * Privacy-first implementation:
6938
+ * - Returns global userId if explicitly set (via identify/login)
6939
+ * - Otherwise uses IdManager to generate:
6940
+ * - Daily rotating ID (cookieless mode)
6941
+ * - Permanent ID (with consent)
5498
6942
  */
5499
6943
  getEffectiveUserIdInternal() {
5500
6944
  if (this.globalUserId) {
5501
6945
  return this.globalUserId;
5502
6946
  }
5503
- if (this.persistentAnonymousUserId) {
5504
- return this.persistentAnonymousUserId;
5505
- }
5506
- this.persistentAnonymousUserId = this.generateAnonymousUserId();
5507
- this.savePersistentAnonymousUserId(this.persistentAnonymousUserId);
5508
- return this.persistentAnonymousUserId;
6947
+ return this.idManager.getCurrentUserId();
5509
6948
  }
5510
6949
  log(...args) {
5511
6950
  if (this.config.debug) {
@@ -5843,6 +7282,7 @@ var Grain = (() => {
5843
7282
  this,
5844
7283
  {
5845
7284
  stripQueryParams: this.config.stripQueryParams,
7285
+ stripHash: this.config.stripHash,
5846
7286
  debug: this.config.debug,
5847
7287
  tenantId: this.config.tenantId
5848
7288
  }
@@ -5933,7 +7373,9 @@ var Grain = (() => {
5933
7373
  {
5934
7374
  debug: this.config.debug,
5935
7375
  enableMutationObserver: true,
5936
- mutationDebounceDelay: 500
7376
+ mutationDebounceDelay: 500,
7377
+ tenantId: this.config.tenantId,
7378
+ apiUrl: this.config.apiUrl
5937
7379
  }
5938
7380
  );
5939
7381
  this.log("Interaction tracking initialized");
@@ -6134,11 +7576,12 @@ var Grain = (() => {
6134
7576
  const hasConsent = this.consentManager.hasConsent("analytics");
6135
7577
  const event = {
6136
7578
  eventName,
6137
- userId: hasConsent ? this.getEffectiveUserId() : this.getEphemeralSessionId(),
7579
+ userId: this.getEffectiveUserId(),
7580
+ // IdManager handles daily vs permanent based on consent
6138
7581
  properties: {
6139
7582
  ...properties,
6140
7583
  _minimal: !hasConsent,
6141
- // Flag to indicate minimal tracking
7584
+ // Flag to indicate minimal tracking (daily rotating ID)
6142
7585
  _consent_status: hasConsent ? "granted" : "pending"
6143
7586
  }
6144
7587
  };
@@ -6236,10 +7679,13 @@ var Grain = (() => {
6236
7679
  this.log(`Event waiting for consent: ${event.eventName}`, event.properties);
6237
7680
  return;
6238
7681
  }
6239
- if (!this.consentManager.hasConsent("analytics")) {
6240
- this.log(`Event blocked by consent: ${event.eventName}`);
6241
- return;
6242
- }
7682
+ const hasConsent = this.consentManager.hasConsent("analytics");
7683
+ formattedEvent.properties = {
7684
+ ...formattedEvent.properties,
7685
+ _minimal: !hasConsent,
7686
+ // Flag: true = daily rotating ID, false = permanent ID
7687
+ _consent_status: hasConsent ? "granted" : "pending"
7688
+ };
6243
7689
  this.eventQueue.push(formattedEvent);
6244
7690
  this.eventCountSinceLastHeartbeat++;
6245
7691
  this.sessionEventCount++;
@@ -6859,28 +8305,42 @@ var Grain = (() => {
6859
8305
  }
6860
8306
  // Privacy & Consent Methods
6861
8307
  /**
6862
- * Grant consent for tracking
8308
+ * Grant consent for tracking (v2.0)
8309
+ * Switches from cookie-less mode to permanent IDs
6863
8310
  * @param categories - Optional array of consent categories (e.g., ['analytics', 'functional'])
6864
8311
  */
6865
8312
  grantConsent(categories) {
6866
8313
  try {
6867
8314
  this.consentManager.grantConsent(categories);
6868
- this.log("Consent granted", categories);
8315
+ const idMode = this.consentManager.getIdMode();
8316
+ this.idManager.setMode(idMode);
8317
+ this.log("Consent granted, switched to permanent IDs", categories);
8318
+ if (this.waitingForConsentQueue.length > 0) {
8319
+ this.log(`Processing ${this.waitingForConsentQueue.length} queued events`);
8320
+ this.eventQueue.push(...this.waitingForConsentQueue);
8321
+ this.waitingForConsentQueue = [];
8322
+ this.flush();
8323
+ }
6869
8324
  } catch (error) {
6870
8325
  const formattedError = this.formatError(error, "grantConsent");
6871
8326
  this.logError(formattedError);
6872
8327
  }
6873
8328
  }
6874
8329
  /**
6875
- * Revoke consent for tracking (opt-out)
8330
+ * Revoke consent for tracking (v2.0)
8331
+ * Switches from permanent IDs to cookie-less mode
6876
8332
  * @param categories - Optional array of categories to revoke (if not provided, revokes all)
6877
8333
  */
6878
8334
  revokeConsent(categories) {
6879
8335
  try {
6880
8336
  this.consentManager.revokeConsent(categories);
6881
- this.log("Consent revoked", categories);
6882
- this.eventQueue = [];
6883
- this.waitingForConsentQueue = [];
8337
+ const idMode = this.consentManager.getIdMode();
8338
+ this.idManager.setMode(idMode);
8339
+ this.log("Consent revoked, switched to cookie-less mode", categories);
8340
+ if (!this.consentManager.hasConsent()) {
8341
+ this.eventQueue = [];
8342
+ this.waitingForConsentQueue = [];
8343
+ }
6884
8344
  } catch (error) {
6885
8345
  const formattedError = this.formatError(error, "revokeConsent");
6886
8346
  this.logError(formattedError);
@@ -6911,6 +8371,91 @@ var Grain = (() => {
6911
8371
  offConsentChange(listener) {
6912
8372
  this.consentManager.removeListener(listener);
6913
8373
  }
8374
+ /**
8375
+ * Check for debug mode parameters and initialize debug agent if valid
8376
+ */
8377
+ checkAndInitializeDebugMode() {
8378
+ if (typeof window === "undefined")
8379
+ return;
8380
+ try {
8381
+ const urlParams = new URLSearchParams(window.location.search);
8382
+ const isDebug = urlParams.get("grain_debug") === "1";
8383
+ const sessionId = urlParams.get("grain_session");
8384
+ if (!isDebug || !sessionId) {
8385
+ return;
8386
+ }
8387
+ this.log("Debug mode detected, verifying session:", sessionId);
8388
+ this.verifyDebugSession(sessionId, window.location.hostname).then((valid) => {
8389
+ if (valid) {
8390
+ this.log("Debug session verified, initializing debug agent");
8391
+ this.isDebugMode = true;
8392
+ this.initializeDebugAgent(sessionId);
8393
+ } else {
8394
+ this.log("Debug session verification failed");
8395
+ }
8396
+ }).catch((error) => {
8397
+ this.log("Failed to verify debug session:", error);
8398
+ });
8399
+ } catch (error) {
8400
+ this.log("Error checking debug mode:", error);
8401
+ }
8402
+ }
8403
+ /**
8404
+ * Verify debug session with API
8405
+ */
8406
+ async verifyDebugSession(sessionId, domain) {
8407
+ try {
8408
+ const url = `${this.config.apiUrl}/v1/tenant/${encodeURIComponent(this.config.tenantId)}/debug-sessions/verify`;
8409
+ const response = await fetch(url, {
8410
+ method: "POST",
8411
+ headers: {
8412
+ "Content-Type": "application/json"
8413
+ },
8414
+ body: JSON.stringify({
8415
+ sessionId,
8416
+ domain
8417
+ })
8418
+ });
8419
+ if (!response.ok) {
8420
+ return false;
8421
+ }
8422
+ const result = await response.json();
8423
+ return result.valid === true;
8424
+ } catch (error) {
8425
+ this.log("Debug session verification error:", error);
8426
+ return false;
8427
+ }
8428
+ }
8429
+ /**
8430
+ * Initialize debug agent
8431
+ */
8432
+ initializeDebugAgent(sessionId) {
8433
+ if (typeof window === "undefined")
8434
+ return;
8435
+ try {
8436
+ this.log("Loading debug agent module");
8437
+ Promise.resolve().then(() => (init_debug_agent(), debug_agent_exports)).then(({ DebugAgent: DebugAgent2 }) => {
8438
+ try {
8439
+ this.debugAgent = new DebugAgent2(
8440
+ this,
8441
+ sessionId,
8442
+ this.config.tenantId,
8443
+ this.config.apiUrl,
8444
+ {
8445
+ debug: this.config.debug
8446
+ }
8447
+ );
8448
+ this.log("Debug agent initialized");
8449
+ } catch (error) {
8450
+ this.log("Failed to initialize debug agent:", error);
8451
+ }
8452
+ }).catch((error) => {
8453
+ this.log("Failed to load debug agent module:", error);
8454
+ });
8455
+ } catch (error) {
8456
+ this.log("Error initializing debug agent:", error);
8457
+ }
8458
+ }
6914
8459
  /**
6915
8460
  * Destroy the client and clean up resources
6916
8461
  */
@@ -6946,6 +8491,10 @@ var Grain = (() => {
6946
8491
  this.heatmapTrackingManager.destroy();
6947
8492
  this.heatmapTrackingManager = null;
6948
8493
  }
8494
+ if (this.debugAgent) {
8495
+ this.debugAgent.destroy();
8496
+ this.debugAgent = null;
8497
+ }
6949
8498
  if (this.eventQueue.length > 0) {
6950
8499
  const eventsToSend = [...this.eventQueue];
6951
8500
  this.eventQueue = [];