@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.
- package/README.md +36 -3
- package/dist/cjs/consent.d.ts +38 -7
- package/dist/cjs/consent.d.ts.map +1 -1
- package/dist/cjs/consent.js +82 -23
- package/dist/cjs/consent.js.map +1 -1
- package/dist/cjs/debug-agent.d.ts +171 -0
- package/dist/cjs/debug-agent.d.ts.map +1 -0
- package/dist/cjs/debug-agent.js +1219 -0
- package/dist/cjs/debug-agent.js.map +1 -0
- package/dist/cjs/id-manager.d.ts +66 -0
- package/dist/cjs/id-manager.d.ts.map +1 -0
- package/dist/cjs/id-manager.js +212 -0
- package/dist/cjs/id-manager.js.map +1 -0
- package/dist/cjs/index.d.ts +26 -8
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/interaction-tracking.d.ts +6 -0
- package/dist/cjs/interaction-tracking.d.ts.map +1 -1
- package/dist/cjs/interaction-tracking.js +55 -5
- package/dist/cjs/interaction-tracking.js.map +1 -1
- package/dist/cjs/page-tracking.d.ts +6 -0
- package/dist/cjs/page-tracking.d.ts.map +1 -1
- package/dist/cjs/page-tracking.js +23 -2
- package/dist/cjs/page-tracking.js.map +1 -1
- package/dist/cjs/react/hooks/useConsent.d.ts +18 -2
- package/dist/cjs/react/hooks/useConsent.d.ts.map +1 -1
- package/dist/cjs/react/hooks/useConsent.js +52 -1
- package/dist/cjs/react/hooks/useConsent.js.map +1 -1
- package/dist/consent.d.ts +38 -7
- package/dist/consent.d.ts.map +1 -1
- package/dist/consent.js +82 -23
- package/dist/debug-agent.d.ts +171 -0
- package/dist/debug-agent.d.ts.map +1 -0
- package/dist/debug-agent.js +1219 -0
- package/dist/esm/consent.d.ts +38 -7
- package/dist/esm/consent.d.ts.map +1 -1
- package/dist/esm/consent.js +82 -23
- package/dist/esm/consent.js.map +1 -1
- package/dist/esm/debug-agent.d.ts +171 -0
- package/dist/esm/debug-agent.d.ts.map +1 -0
- package/dist/esm/debug-agent.js +1215 -0
- package/dist/esm/debug-agent.js.map +1 -0
- package/dist/esm/id-manager.d.ts +66 -0
- package/dist/esm/id-manager.d.ts.map +1 -0
- package/dist/esm/id-manager.js +208 -0
- package/dist/esm/id-manager.js.map +1 -0
- package/dist/esm/index.d.ts +26 -8
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/interaction-tracking.d.ts +6 -0
- package/dist/esm/interaction-tracking.d.ts.map +1 -1
- package/dist/esm/interaction-tracking.js +55 -5
- package/dist/esm/interaction-tracking.js.map +1 -1
- package/dist/esm/page-tracking.d.ts +6 -0
- package/dist/esm/page-tracking.d.ts.map +1 -1
- package/dist/esm/page-tracking.js +23 -2
- package/dist/esm/page-tracking.js.map +1 -1
- package/dist/esm/react/hooks/useConsent.d.ts +18 -2
- package/dist/esm/react/hooks/useConsent.d.ts.map +1 -1
- package/dist/esm/react/hooks/useConsent.js +49 -1
- package/dist/esm/react/hooks/useConsent.js.map +1 -1
- package/dist/id-manager.d.ts +66 -0
- package/dist/id-manager.d.ts.map +1 -0
- package/dist/id-manager.js +212 -0
- package/dist/index.d.ts +26 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.global.dev.js +1635 -86
- package/dist/index.global.dev.js.map +4 -4
- package/dist/index.global.js +506 -2
- package/dist/index.global.js.map +4 -4
- package/dist/index.js +171 -44
- package/dist/index.mjs +172 -45
- package/dist/interaction-tracking.d.ts +6 -0
- package/dist/interaction-tracking.d.ts.map +1 -1
- package/dist/interaction-tracking.js +55 -5
- package/dist/page-tracking.d.ts +6 -0
- package/dist/page-tracking.d.ts.map +1 -1
- package/dist/page-tracking.js +23 -2
- package/dist/react/hooks/useConsent.d.ts +18 -2
- package/dist/react/hooks/useConsent.d.ts.map +1 -1
- package/dist/react/hooks/useConsent.js +52 -1
- package/dist/react/hooks/useConsent.mjs +49 -1
- package/package.json +1 -1
package/dist/index.global.dev.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Grain Analytics Web SDK v2.
|
|
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 (
|
|
762
|
-
|
|
763
|
+
if (this.config.tenantId && this.config.apiUrl) {
|
|
764
|
+
this.fetchAndMergeTrackers().then(() => {
|
|
765
|
+
this.attachAllListeners();
|
|
766
|
+
});
|
|
763
767
|
} else {
|
|
764
|
-
|
|
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 = "
|
|
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:
|
|
1489
|
-
*
|
|
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"
|
|
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 === "
|
|
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 (
|
|
1586
|
-
|
|
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 === "
|
|
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
|
|
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: "
|
|
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
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
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
|
|
6847
|
+
const isCookieless = this.config.consentMode === "cookieless";
|
|
5409
6848
|
const userExplicitlyIdentified = !!this.globalUserId;
|
|
5410
6849
|
const isJWTAuth = this.config.authStrategy === "JWT";
|
|
5411
|
-
|
|
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 (
|
|
6935
|
+
* Get the effective user ID (v2.0)
|
|
5495
6936
|
*
|
|
5496
|
-
*
|
|
5497
|
-
*
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
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.
|
|
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 (
|
|
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.
|
|
6882
|
-
this.
|
|
6883
|
-
this.
|
|
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 = [];
|