@stacksjs/ts-analytics 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -0
- package/LICENSE.md +21 -0
- package/README.md +361 -0
- package/bin/cli.ts +169 -0
- package/dist/Analytics.d.ts +558 -0
- package/dist/api.d.ts +109 -0
- package/dist/batching.d.ts +93 -0
- package/dist/chunk-2mx7fq49.js +4 -0
- package/dist/chunk-3z29508k.js +204 -0
- package/dist/chunk-deephkz6.js +56 -0
- package/dist/chunk-j261vgyp.js +305 -0
- package/dist/chunk-xga17tz7.js +207 -0
- package/dist/config.d.ts +132 -0
- package/dist/dashboard/components/index.d.ts +80 -0
- package/dist/dashboard/composables/useAnalytics.d.ts +99 -0
- package/dist/dashboard/index.d.ts +110 -0
- package/dist/dashboard/types/index.d.ts +144 -0
- package/dist/dashboard/utils/index.d.ts +81 -0
- package/dist/dynamodb.d.ts +86 -0
- package/dist/funnels.d.ts +104 -0
- package/dist/geolocation.d.ts +96 -0
- package/dist/index.d.ts +371 -0
- package/dist/index.js +10211 -0
- package/dist/infrastructure/cdk.d.ts +60 -0
- package/dist/infrastructure/cloudformation.d.ts +44 -0
- package/dist/infrastructure/index.d.ts +45 -0
- package/dist/infrastructure/setup.d.ts +133 -0
- package/dist/integrations/cloudflare.d.ts +76 -0
- package/dist/integrations/hono.d.ts +60 -0
- package/dist/integrations/index.d.ts +24 -0
- package/dist/integrations/nuxt.d.ts +7 -0
- package/dist/integrations/nuxt.js +42 -0
- package/dist/integrations/runtime/use-ts-analytics.d.ts +18 -0
- package/dist/integrations/runtime/use-ts-analytics.js +17 -0
- package/dist/integrations/stx.d.ts +88 -0
- package/dist/integrations/stx.js +18 -0
- package/dist/lib/crypto-random.d.ts +4 -0
- package/dist/lib/salt.d.ts +16 -0
- package/dist/local.d.ts +56 -0
- package/dist/model-connector.d.ts +145 -0
- package/dist/models/AggregatedStats.d.ts +9 -0
- package/dist/models/CampaignStats.d.ts +9 -0
- package/dist/models/Conversion.d.ts +11 -0
- package/dist/models/CustomEvent.d.ts +11 -0
- package/dist/models/DeviceStats.d.ts +9 -0
- package/dist/models/EventStats.d.ts +9 -0
- package/dist/models/GeoStats.d.ts +9 -0
- package/dist/models/Goal.d.ts +9 -0
- package/dist/models/GoalStats.d.ts +9 -0
- package/dist/models/PageStats.d.ts +11 -0
- package/dist/models/PageView.d.ts +13 -0
- package/dist/models/RealtimeStats.d.ts +9 -0
- package/dist/models/ReferrerStats.d.ts +9 -0
- package/dist/models/Session.d.ts +11 -0
- package/dist/models/Site.d.ts +11 -0
- package/dist/models/index.d.ts +28 -0
- package/dist/models/types.d.ts +60 -0
- package/dist/sqs-buffering.d.ts +243 -0
- package/dist/stacks-integration.d.ts +159 -0
- package/dist/tracking-script.d.ts +71 -0
- package/dist/tracking.d.ts +17 -0
- package/dist/tracking.js +26 -0
- package/dist/types.d.ts +595 -0
- package/dist/utils/geolocation.d.ts +111 -0
- package/dist/utils/user-agent.d.ts +17 -0
- package/dist/version.d.ts +7 -0
- package/package.json +119 -0
- package/src/Analytics.ts +3349 -0
- package/src/api.ts +1286 -0
- package/src/assets/crosswind.css +2220 -0
- package/src/batching.ts +452 -0
- package/src/components/dashboard/index.ts +18 -0
- package/src/config.ts +456 -0
- package/src/dashboard/Dashboard.stx +1517 -0
- package/src/dashboard/components/AlertCard.stx +177 -0
- package/src/dashboard/components/AnalyticsDashboard.stx +354 -0
- package/src/dashboard/components/AnimatedNumber.stx +86 -0
- package/src/dashboard/components/BarChart.stx +220 -0
- package/src/dashboard/components/BrowserBreakdown.stx +98 -0
- package/src/dashboard/components/BrowsersTable.stx +125 -0
- package/src/dashboard/components/CampaignBreakdown.stx +163 -0
- package/src/dashboard/components/CampaignTable.stx +238 -0
- package/src/dashboard/components/CountryList.stx +101 -0
- package/src/dashboard/components/DataTable.stx +226 -0
- package/src/dashboard/components/DateRangePicker.stx +77 -0
- package/src/dashboard/components/DeviceBreakdown.stx +94 -0
- package/src/dashboard/components/DevicesTable.stx +163 -0
- package/src/dashboard/components/DonutChart.stories.ts +55 -0
- package/src/dashboard/components/DonutChart.stx +157 -0
- package/src/dashboard/components/EmptyState.stx +90 -0
- package/src/dashboard/components/EngagementMetrics.stx +183 -0
- package/src/dashboard/components/EventsSection.stx +192 -0
- package/src/dashboard/components/FilterBar.stx +104 -0
- package/src/dashboard/components/FullAnalyticsDashboard.stx +455 -0
- package/src/dashboard/components/FunnelChart.stx +142 -0
- package/src/dashboard/components/GeoTable.stx +337 -0
- package/src/dashboard/components/GoalsPanel.stx +109 -0
- package/src/dashboard/components/Header.stx +39 -0
- package/src/dashboard/components/HeatmapChart.stx +140 -0
- package/src/dashboard/components/LiveActivityFeed.stx +172 -0
- package/src/dashboard/components/MetricComparison.stx +106 -0
- package/src/dashboard/components/MiniStats.stx +96 -0
- package/src/dashboard/components/OSBreakdown.stx +124 -0
- package/src/dashboard/components/PageDetailCard.stx +102 -0
- package/src/dashboard/components/PagesTable.stx +127 -0
- package/src/dashboard/components/ProgressRing.stx +89 -0
- package/src/dashboard/components/RealtimeCounter.stories.ts +45 -0
- package/src/dashboard/components/RealtimeCounter.stx +46 -0
- package/src/dashboard/components/ReferrersTable.stx +180 -0
- package/src/dashboard/components/SparklineChart.stx +160 -0
- package/src/dashboard/components/StatCard.stories.ts +58 -0
- package/src/dashboard/components/StatCard.stx +90 -0
- package/src/dashboard/components/SummaryStats.stx +81 -0
- package/src/dashboard/components/TabNav.stx +66 -0
- package/src/dashboard/components/ThemeSwitcher.stx +124 -0
- package/src/dashboard/components/TimeSeriesChart.stx +106 -0
- package/src/dashboard/components/TopList.stories.ts +58 -0
- package/src/dashboard/components/TopList.stx +74 -0
- package/src/dashboard/components/TrendIndicator.stx +84 -0
- package/src/dashboard/components/heatmap/ClickHeatmap.stx +264 -0
- package/src/dashboard/components/heatmap/ElementClickList.stx +125 -0
- package/src/dashboard/components/heatmap/HeatmapControls.stx +125 -0
- package/src/dashboard/components/heatmap/HeatmapLegend.stx +69 -0
- package/src/dashboard/components/heatmap/PageHeatmap.stx +264 -0
- package/src/dashboard/components/heatmap/ScrollHeatmap.stx +127 -0
- package/src/dashboard/components/heatmap/index.ts +12 -0
- package/src/dashboard/components/index.ts +86 -0
- package/src/dashboard/composables/useAnalytics.ts +465 -0
- package/src/dashboard/demo/DemoApp.stx +370 -0
- package/src/dashboard/demo/index.ts +8 -0
- package/src/dashboard/demo/mockData.ts +234 -0
- package/src/dashboard/index.ts +117 -0
- package/src/dashboard/stx-shim.d.ts +4 -0
- package/src/dashboard/types/index.ts +203 -0
- package/src/dashboard/utils/index.ts +426 -0
- package/src/dynamodb.ts +344 -0
- package/src/funnels.ts +534 -0
- package/src/geolocation.ts +515 -0
- package/src/handlers/alerts.ts +328 -0
- package/src/handlers/annotations.ts +128 -0
- package/src/handlers/api-keys.ts +240 -0
- package/src/handlers/auth.ts +1020 -0
- package/src/handlers/authz.ts +137 -0
- package/src/handlers/collect.ts +724 -0
- package/src/handlers/data.ts +625 -0
- package/src/handlers/experiments.ts +138 -0
- package/src/handlers/funnels.ts +216 -0
- package/src/handlers/goals.ts +218 -0
- package/src/handlers/heatmaps.ts +272 -0
- package/src/handlers/index.ts +56 -0
- package/src/handlers/lib/read-cache.ts +63 -0
- package/src/handlers/misc.ts +486 -0
- package/src/handlers/oauth.ts +233 -0
- package/src/handlers/performance.ts +388 -0
- package/src/handlers/sessions.ts +413 -0
- package/src/handlers/sharing.ts +198 -0
- package/src/handlers/stats.ts +1368 -0
- package/src/handlers/team.ts +161 -0
- package/src/handlers/uptime.ts +283 -0
- package/src/handlers/views.ts +390 -0
- package/src/handlers/webhooks.ts +226 -0
- package/src/heatmap/index.ts +32 -0
- package/src/heatmap/tracking-script.ts +452 -0
- package/src/heatmap/types.ts +79 -0
- package/src/index.ts +387 -0
- package/src/infrastructure/cdk.ts +496 -0
- package/src/infrastructure/cloudformation.ts +595 -0
- package/src/infrastructure/index.ts +48 -0
- package/src/infrastructure/setup.ts +611 -0
- package/src/integrations/cloudflare.ts +732 -0
- package/src/integrations/hono.ts +589 -0
- package/src/integrations/index.ts +27 -0
- package/src/integrations/nuxt.ts +78 -0
- package/src/integrations/runtime/use-ts-analytics.ts +32 -0
- package/src/integrations/stx.ts +138 -0
- package/src/jobs/index.ts +127 -0
- package/src/lib/crypto-random.ts +41 -0
- package/src/lib/ddb-errors.ts +20 -0
- package/src/lib/dynamodb.ts +216 -0
- package/src/lib/email.ts +38 -0
- package/src/lib/ga-import.ts +471 -0
- package/src/lib/ga4-api.ts +244 -0
- package/src/lib/goals.ts +205 -0
- package/src/lib/index.ts +6 -0
- package/src/lib/ingest-counters.ts +123 -0
- package/src/lib/log.ts +36 -0
- package/src/lib/membership.ts +98 -0
- package/src/lib/plans.ts +90 -0
- package/src/lib/quota.ts +46 -0
- package/src/lib/rate-limit.ts +27 -0
- package/src/lib/rollups.ts +472 -0
- package/src/lib/salt.ts +97 -0
- package/src/lib/scheduler.ts +97 -0
- package/src/lib/significance.ts +66 -0
- package/src/lib/site-retention.ts +56 -0
- package/src/local.ts +360 -0
- package/src/model-connector.ts +616 -0
- package/src/models/AggregatedStats.ts +162 -0
- package/src/models/CampaignStats.ts +141 -0
- package/src/models/Conversion.ts +135 -0
- package/src/models/CustomEvent.ts +123 -0
- package/src/models/DeviceStats.ts +105 -0
- package/src/models/EventStats.ts +108 -0
- package/src/models/GeoStats.ts +112 -0
- package/src/models/Goal.ts +105 -0
- package/src/models/GoalStats.ts +106 -0
- package/src/models/PageStats.ts +152 -0
- package/src/models/PageView.ts +277 -0
- package/src/models/RealtimeStats.ts +96 -0
- package/src/models/ReferrerStats.ts +115 -0
- package/src/models/Session.ts +235 -0
- package/src/models/Site.ts +115 -0
- package/src/models/index.ts +50 -0
- package/src/models/orm/index.ts +2043 -0
- package/src/models/types.ts +71 -0
- package/src/router.ts +521 -0
- package/src/sqs-buffering.ts +806 -0
- package/src/stacks-integration.ts +527 -0
- package/src/tracking-script.ts +944 -0
- package/src/tracking.ts +27 -0
- package/src/types/analytics.ts +219 -0
- package/src/types/api.ts +75 -0
- package/src/types/bun-router.d.ts +15 -0
- package/src/types/dashboard.ts +62 -0
- package/src/types/index.ts +66 -0
- package/src/types/stx.d.ts +27 -0
- package/src/types/window.d.ts +101 -0
- package/src/types.ts +911 -0
- package/src/utils/cache.ts +148 -0
- package/src/utils/date.ts +118 -0
- package/src/utils/filters.ts +71 -0
- package/src/utils/geolocation.ts +323 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/response.ts +180 -0
- package/src/utils/timezone-country.ts +242 -0
- package/src/utils/user-agent.ts +110 -0
- package/src/version.ts +7 -0
|
@@ -0,0 +1,944 @@
|
|
|
1
|
+
/* eslint-disable prefer-const */
|
|
2
|
+
/**
|
|
3
|
+
* Tracking Script Generator
|
|
4
|
+
*
|
|
5
|
+
* Generates a comprehensive client-side tracking script for web analytics.
|
|
6
|
+
* Supports page views, custom events, scroll tracking, outbound links, and more.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Types
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
export interface TrackingScriptConfig {
|
|
14
|
+
/** Site ID */
|
|
15
|
+
siteId: string
|
|
16
|
+
/** API endpoint for collecting events */
|
|
17
|
+
apiEndpoint: string
|
|
18
|
+
/** Enable automatic page view tracking */
|
|
19
|
+
autoPageView?: boolean
|
|
20
|
+
/** Track hash changes as page views */
|
|
21
|
+
trackHashChanges?: boolean
|
|
22
|
+
/** Track outbound link clicks */
|
|
23
|
+
trackOutboundLinks?: boolean
|
|
24
|
+
/** Track scroll depth (percentages) */
|
|
25
|
+
trackScrollDepth?: number[]
|
|
26
|
+
/** Track time on page (intervals in seconds) */
|
|
27
|
+
trackTimeOnPage?: number[]
|
|
28
|
+
/** Respect Do Not Track header */
|
|
29
|
+
honorDnt?: boolean
|
|
30
|
+
/** Cookie domain (for cross-subdomain tracking) */
|
|
31
|
+
cookieDomain?: string
|
|
32
|
+
/** Session timeout in minutes */
|
|
33
|
+
sessionTimeout?: number
|
|
34
|
+
/** Custom data attributes prefix */
|
|
35
|
+
dataAttributePrefix?: string
|
|
36
|
+
/** Debug mode */
|
|
37
|
+
debug?: boolean
|
|
38
|
+
/** Callback for errors */
|
|
39
|
+
onError?: string // Function name
|
|
40
|
+
/** Excluded paths (regex patterns) */
|
|
41
|
+
excludePaths?: string[]
|
|
42
|
+
/** Exclude query params from tracking */
|
|
43
|
+
excludeQueryParams?: boolean
|
|
44
|
+
/** Minify the output */
|
|
45
|
+
minify?: boolean
|
|
46
|
+
/** Heatmap tracking configuration */
|
|
47
|
+
heatmap?: HeatmapTrackingConfig
|
|
48
|
+
/** Use stealth mode (shorter endpoint paths, less identifiable) */
|
|
49
|
+
stealthMode?: boolean
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface HeatmapTrackingConfig {
|
|
53
|
+
/** Enable click tracking for heatmaps */
|
|
54
|
+
trackClicks?: boolean
|
|
55
|
+
/** Enable mouse movement tracking for heatmaps */
|
|
56
|
+
trackMovements?: boolean
|
|
57
|
+
/** Movement sampling interval in ms (default: 100) */
|
|
58
|
+
movementSampleInterval?: number
|
|
59
|
+
/** Maximum movement points per page before flush (default: 500) */
|
|
60
|
+
maxMovementPoints?: number
|
|
61
|
+
/** Enable scroll position tracking for heatmaps */
|
|
62
|
+
trackScrollPositions?: boolean
|
|
63
|
+
/** Flush interval for batched data in ms (default: 5000) */
|
|
64
|
+
flushInterval?: number
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// Main Generator
|
|
69
|
+
// ============================================================================
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Generate a comprehensive tracking script
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* const script = generateFullTrackingScript({
|
|
77
|
+
* siteId: 'my-site',
|
|
78
|
+
* apiEndpoint: 'https://analytics.example.com/api/analytics',
|
|
79
|
+
* trackScrollDepth: [25, 50, 75, 100],
|
|
80
|
+
* trackOutboundLinks: true,
|
|
81
|
+
* })
|
|
82
|
+
*
|
|
83
|
+
* // Add to HTML
|
|
84
|
+
* res.send(`<script>${script}</script>`)
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export function generateFullTrackingScript(config: TrackingScriptConfig): string {
|
|
88
|
+
const script = buildScript(config)
|
|
89
|
+
return config.minify ? minifyScript(script) : script
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Generate just the tracking snippet (loader)
|
|
94
|
+
*/
|
|
95
|
+
export function generateTrackingSnippet(config: Pick<TrackingScriptConfig, 'siteId' | 'apiEndpoint'>): string {
|
|
96
|
+
return `<script>
|
|
97
|
+
(function(w,d,s,a){
|
|
98
|
+
w.sa=w.sa||function(){(w.sa.q=w.sa.q||[]).push(arguments)};
|
|
99
|
+
var f=d.getElementsByTagName(s)[0],j=d.createElement(s);
|
|
100
|
+
j.async=1;j.src=${JSON.stringify(`${config.apiEndpoint}/sites/${config.siteId}/tracker.js`)};
|
|
101
|
+
f.parentNode.insertBefore(j,f);
|
|
102
|
+
})(window,document,'script');
|
|
103
|
+
sa('init',${JSON.stringify(config.siteId)});
|
|
104
|
+
</script>`
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Generate inline tracking script (no external file needed)
|
|
109
|
+
*/
|
|
110
|
+
export function generateInlineTrackingScript(config: TrackingScriptConfig): string {
|
|
111
|
+
return `<script>
|
|
112
|
+
${generateFullTrackingScript(config)}
|
|
113
|
+
sa('init',${JSON.stringify(config.siteId)});
|
|
114
|
+
</script>`
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ============================================================================
|
|
118
|
+
// Script Builder
|
|
119
|
+
// ============================================================================
|
|
120
|
+
|
|
121
|
+
function buildScript(config: TrackingScriptConfig): string {
|
|
122
|
+
const parts: string[] = []
|
|
123
|
+
|
|
124
|
+
// IIFE wrapper
|
|
125
|
+
parts.push('(function(window, document) {')
|
|
126
|
+
parts.push('"use strict";')
|
|
127
|
+
parts.push('')
|
|
128
|
+
|
|
129
|
+
// Configuration
|
|
130
|
+
parts.push(buildConfigSection(config))
|
|
131
|
+
parts.push('')
|
|
132
|
+
|
|
133
|
+
// Utilities
|
|
134
|
+
parts.push(buildUtilities())
|
|
135
|
+
parts.push('')
|
|
136
|
+
|
|
137
|
+
// Session management
|
|
138
|
+
parts.push(buildSessionManagement(config))
|
|
139
|
+
parts.push('')
|
|
140
|
+
|
|
141
|
+
// Core tracking
|
|
142
|
+
parts.push(buildCoreTracking(config))
|
|
143
|
+
parts.push('')
|
|
144
|
+
|
|
145
|
+
// Page view tracking
|
|
146
|
+
if (config.autoPageView !== false) {
|
|
147
|
+
parts.push(buildPageViewTracking(config))
|
|
148
|
+
parts.push('')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Scroll tracking
|
|
152
|
+
if (config.trackScrollDepth?.length) {
|
|
153
|
+
parts.push(buildScrollTracking(config.trackScrollDepth))
|
|
154
|
+
parts.push('')
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Time on page tracking
|
|
158
|
+
if (config.trackTimeOnPage?.length) {
|
|
159
|
+
parts.push(buildTimeOnPageTracking(config.trackTimeOnPage))
|
|
160
|
+
parts.push('')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Outbound link tracking
|
|
164
|
+
if (config.trackOutboundLinks) {
|
|
165
|
+
parts.push(buildOutboundLinkTracking(config.dataAttributePrefix))
|
|
166
|
+
parts.push('')
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Heatmap tracking
|
|
170
|
+
if (config.heatmap && (config.heatmap.trackClicks || config.heatmap.trackMovements || config.heatmap.trackScrollPositions)) {
|
|
171
|
+
parts.push(buildHeatmapTrackingSection(config.heatmap, config.debug))
|
|
172
|
+
parts.push('')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Public API
|
|
176
|
+
parts.push(buildPublicAPI())
|
|
177
|
+
parts.push('')
|
|
178
|
+
|
|
179
|
+
// Initialization
|
|
180
|
+
parts.push(buildInitialization(config))
|
|
181
|
+
parts.push('')
|
|
182
|
+
|
|
183
|
+
// Close IIFE
|
|
184
|
+
parts.push('})(window, document);')
|
|
185
|
+
|
|
186
|
+
return parts.join('\n')
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function buildConfigSection(config: TrackingScriptConfig): string {
|
|
190
|
+
// JSON.stringify the string values (matching excludePaths below) so a quote,
|
|
191
|
+
// backslash, or newline in siteId/apiEndpoint can't break out of the literal
|
|
192
|
+
// and corrupt the whole inline tracker.
|
|
193
|
+
return `// Configuration
|
|
194
|
+
var CONFIG = {
|
|
195
|
+
siteId: ${JSON.stringify(config.siteId)},
|
|
196
|
+
endpoint: ${JSON.stringify(`${config.apiEndpoint}/collect`)},
|
|
197
|
+
honorDnt: ${config.honorDnt ?? true},
|
|
198
|
+
sessionTimeout: ${(config.sessionTimeout ?? 30) * 60 * 1000},
|
|
199
|
+
debug: ${config.debug ?? false},
|
|
200
|
+
excludePaths: ${JSON.stringify(config.excludePaths ?? [])},
|
|
201
|
+
excludeQueryParams: ${config.excludeQueryParams ?? false}
|
|
202
|
+
};`
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function buildUtilities(): string {
|
|
206
|
+
return `// Utilities
|
|
207
|
+
function generateId() {
|
|
208
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
209
|
+
var r = Math.random() * 16 | 0;
|
|
210
|
+
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function getCookie(name) {
|
|
215
|
+
var value = '; ' + document.cookie;
|
|
216
|
+
var parts = value.split('; ' + name + '=');
|
|
217
|
+
if (parts.length === 2) return parts.pop().split(';').shift();
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function setCookie(name, value, days) {
|
|
222
|
+
var expires = '';
|
|
223
|
+
if (days) {
|
|
224
|
+
var date = new Date();
|
|
225
|
+
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
|
226
|
+
expires = '; expires=' + date.toUTCString();
|
|
227
|
+
}
|
|
228
|
+
document.cookie = name + '=' + value + expires + '; path=/; SameSite=Lax';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function log() {
|
|
232
|
+
if (CONFIG.debug && console && console.log) {
|
|
233
|
+
console.log.apply(console, ['[Analytics]'].concat(Array.prototype.slice.call(arguments)));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function shouldTrack() {
|
|
238
|
+
if (CONFIG.honorDnt && (navigator.doNotTrack === '1' || navigator.globalPrivacyControl === true)) {
|
|
239
|
+
log('DNT/GPC enabled, not tracking');
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
var path = window.location.pathname;
|
|
244
|
+
for (var i = 0; i < CONFIG.excludePaths.length; i++) {
|
|
245
|
+
if (new RegExp(CONFIG.excludePaths[i]).test(path)) {
|
|
246
|
+
log('Path excluded:', path);
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function sendBeacon(data) {
|
|
255
|
+
var payload = JSON.stringify(data);
|
|
256
|
+
|
|
257
|
+
if (navigator.sendBeacon) {
|
|
258
|
+
navigator.sendBeacon(CONFIG.endpoint, payload);
|
|
259
|
+
log('Sent via beacon:', data);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
var xhr = new XMLHttpRequest();
|
|
263
|
+
xhr.open('POST', CONFIG.endpoint, true);
|
|
264
|
+
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
265
|
+
xhr.send(payload);
|
|
266
|
+
log('Sent via XHR:', data);
|
|
267
|
+
}
|
|
268
|
+
}`
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function buildSessionManagement(_config: TrackingScriptConfig): string {
|
|
272
|
+
return `// Session Management
|
|
273
|
+
var SESSION_KEY = 'sa_session';
|
|
274
|
+
var VISITOR_KEY = 'sa_visitor';
|
|
275
|
+
var session = null;
|
|
276
|
+
|
|
277
|
+
function getOrCreateVisitor() {
|
|
278
|
+
var visitorId = getCookie(VISITOR_KEY);
|
|
279
|
+
if (!visitorId) {
|
|
280
|
+
visitorId = generateId();
|
|
281
|
+
setCookie(VISITOR_KEY, visitorId, 365);
|
|
282
|
+
log('Created new visitor:', visitorId);
|
|
283
|
+
}
|
|
284
|
+
return visitorId;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function getOrCreateSession() {
|
|
288
|
+
var now = Date.now();
|
|
289
|
+
var sessionData = null;
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
var stored = sessionStorage.getItem(SESSION_KEY);
|
|
293
|
+
if (stored) {
|
|
294
|
+
sessionData = JSON.parse(stored);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch (e) {}
|
|
298
|
+
|
|
299
|
+
// Check if session is expired
|
|
300
|
+
if (sessionData && (now - sessionData.lastActivity) < CONFIG.sessionTimeout) {
|
|
301
|
+
sessionData.lastActivity = now;
|
|
302
|
+
sessionData.pageCount++;
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
// Create new session
|
|
306
|
+
sessionData = {
|
|
307
|
+
id: generateId(),
|
|
308
|
+
startedAt: now,
|
|
309
|
+
lastActivity: now,
|
|
310
|
+
pageCount: 1,
|
|
311
|
+
referrer: document.referrer || null
|
|
312
|
+
};
|
|
313
|
+
log('Created new session:', sessionData.id);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
sessionStorage.setItem(SESSION_KEY, JSON.stringify(sessionData));
|
|
318
|
+
}
|
|
319
|
+
catch (e) {}
|
|
320
|
+
|
|
321
|
+
return sessionData;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function updateSessionActivity() {
|
|
325
|
+
session.lastActivity = Date.now();
|
|
326
|
+
try {
|
|
327
|
+
sessionStorage.setItem(SESSION_KEY, JSON.stringify(session));
|
|
328
|
+
}
|
|
329
|
+
catch (e) {}
|
|
330
|
+
}`
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function buildCoreTracking(_config: TrackingScriptConfig): string {
|
|
334
|
+
return `// Core Tracking
|
|
335
|
+
function track(eventType, eventData) {
|
|
336
|
+
if (!shouldTrack()) return;
|
|
337
|
+
|
|
338
|
+
eventData = eventData || {};
|
|
339
|
+
|
|
340
|
+
var url = window.location.href;
|
|
341
|
+
if (CONFIG.excludeQueryParams) {
|
|
342
|
+
url = window.location.origin + window.location.pathname;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
var payload = {
|
|
346
|
+
s: CONFIG.siteId,
|
|
347
|
+
sid: session.id,
|
|
348
|
+
e: eventType,
|
|
349
|
+
u: url,
|
|
350
|
+
r: session.referrer,
|
|
351
|
+
t: document.title,
|
|
352
|
+
sw: window.screen.width,
|
|
353
|
+
sh: window.screen.height,
|
|
354
|
+
ts: Date.now()
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// Add event properties
|
|
358
|
+
if (eventData.name) payload.en = eventData.name;
|
|
359
|
+
if (eventData.category) payload.ec = eventData.category;
|
|
360
|
+
if (eventData.value !== undefined) payload.ev = eventData.value;
|
|
361
|
+
if (eventData.properties) payload.p = eventData.properties;
|
|
362
|
+
|
|
363
|
+
sendBeacon(payload);
|
|
364
|
+
updateSessionActivity();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function trackPageView(path, title) {
|
|
368
|
+
track('pageview', {
|
|
369
|
+
properties: {
|
|
370
|
+
path: path || window.location.pathname,
|
|
371
|
+
title: title || document.title
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function trackEvent(name, properties, category, value) {
|
|
377
|
+
track('event', {
|
|
378
|
+
name: name,
|
|
379
|
+
category: category,
|
|
380
|
+
value: value,
|
|
381
|
+
properties: properties
|
|
382
|
+
});
|
|
383
|
+
}`
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function buildPageViewTracking(config: TrackingScriptConfig): string {
|
|
387
|
+
const hashTracking = config.trackHashChanges
|
|
388
|
+
? `
|
|
389
|
+
// Track hash changes
|
|
390
|
+
window.addEventListener('hashchange', function() {
|
|
391
|
+
trackPageView();
|
|
392
|
+
});`
|
|
393
|
+
: ''
|
|
394
|
+
|
|
395
|
+
return `// Auto Page View Tracking
|
|
396
|
+
var initialPageViewSent = false;
|
|
397
|
+
|
|
398
|
+
function sendInitialPageView() {
|
|
399
|
+
if (!initialPageViewSent) {
|
|
400
|
+
initialPageViewSent = true;
|
|
401
|
+
trackPageView();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
${hashTracking}
|
|
405
|
+
|
|
406
|
+
// Track SPA navigation
|
|
407
|
+
var pushState = history.pushState;
|
|
408
|
+
history.pushState = function() {
|
|
409
|
+
pushState.apply(history, arguments);
|
|
410
|
+
trackPageView();
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
var replaceState = history.replaceState;
|
|
414
|
+
history.replaceState = function() {
|
|
415
|
+
replaceState.apply(history, arguments);
|
|
416
|
+
trackPageView();
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
window.addEventListener('popstate', function() {
|
|
420
|
+
trackPageView();
|
|
421
|
+
});`
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function buildScrollTracking(depths: number[]): string {
|
|
425
|
+
return `// Scroll Depth Tracking
|
|
426
|
+
var scrollDepths = ${JSON.stringify(depths)};
|
|
427
|
+
var scrollTracked = {};
|
|
428
|
+
|
|
429
|
+
function trackScrollDepth() {
|
|
430
|
+
var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
431
|
+
var docHeight = Math.max(
|
|
432
|
+
document.body.scrollHeight,
|
|
433
|
+
document.documentElement.scrollHeight
|
|
434
|
+
) - window.innerHeight;
|
|
435
|
+
|
|
436
|
+
if (docHeight <= 0) return;
|
|
437
|
+
|
|
438
|
+
var scrollPercent = Math.round((scrollTop / docHeight) * 100);
|
|
439
|
+
|
|
440
|
+
for (var i = 0; i < scrollDepths.length; i++) {
|
|
441
|
+
var depth = scrollDepths[i];
|
|
442
|
+
if (scrollPercent >= depth && !scrollTracked[depth]) {
|
|
443
|
+
scrollTracked[depth] = true;
|
|
444
|
+
trackEvent('scroll_depth', { depth: depth }, 'engagement', depth);
|
|
445
|
+
log('Scroll depth reached:', depth + '%');
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
var scrollThrottle = null;
|
|
451
|
+
window.addEventListener('scroll', function() {
|
|
452
|
+
if (scrollThrottle) return;
|
|
453
|
+
scrollThrottle = setTimeout(function() {
|
|
454
|
+
scrollThrottle = null;
|
|
455
|
+
trackScrollDepth();
|
|
456
|
+
}, 100);
|
|
457
|
+
}, { passive: true });`
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function buildTimeOnPageTracking(intervals: number[]): string {
|
|
461
|
+
return `// Time on Page Tracking
|
|
462
|
+
var timeIntervals = ${JSON.stringify(intervals)};
|
|
463
|
+
var timeTracked = {};
|
|
464
|
+
var pageStartTime = Date.now();
|
|
465
|
+
|
|
466
|
+
function checkTimeOnPage() {
|
|
467
|
+
var elapsed = Math.floor((Date.now() - pageStartTime) / 1000);
|
|
468
|
+
|
|
469
|
+
for (var i = 0; i < timeIntervals.length; i++) {
|
|
470
|
+
var interval = timeIntervals[i];
|
|
471
|
+
if (elapsed >= interval && !timeTracked[interval]) {
|
|
472
|
+
timeTracked[interval] = true;
|
|
473
|
+
trackEvent('time_on_page', { seconds: interval }, 'engagement', interval);
|
|
474
|
+
log('Time on page reached:', interval + 's');
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
setInterval(checkTimeOnPage, 1000);`
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function buildOutboundLinkTracking(prefix?: string): string {
|
|
483
|
+
const dataAttr = prefix ? `data-${prefix}-track` : 'data-sa-track'
|
|
484
|
+
return `// Outbound Link Tracking
|
|
485
|
+
function isOutboundLink(link) {
|
|
486
|
+
return link.hostname !== window.location.hostname;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function trackOutboundLink(event) {
|
|
490
|
+
var link = event.target.closest('a');
|
|
491
|
+
if (!link) return;
|
|
492
|
+
|
|
493
|
+
if (isOutboundLink(link)) {
|
|
494
|
+
var url = link.href;
|
|
495
|
+
var text = link.textContent || link.innerText || '';
|
|
496
|
+
|
|
497
|
+
trackEvent('outbound_click', {
|
|
498
|
+
url: url,
|
|
499
|
+
text: text.trim().substring(0, 100),
|
|
500
|
+
hostname: link.hostname
|
|
501
|
+
}, 'outbound');
|
|
502
|
+
|
|
503
|
+
log('Outbound link clicked:', url);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
document.addEventListener('click', trackOutboundLink);
|
|
508
|
+
|
|
509
|
+
// Track custom elements with data attribute
|
|
510
|
+
function trackDataElements() {
|
|
511
|
+
var elements = document.querySelectorAll('[${dataAttr}]');
|
|
512
|
+
elements.forEach(function(el) {
|
|
513
|
+
if (el._saTracked) return;
|
|
514
|
+
el._saTracked = true;
|
|
515
|
+
|
|
516
|
+
el.addEventListener('click', function() {
|
|
517
|
+
var eventName = el.getAttribute('${dataAttr}');
|
|
518
|
+
var category = el.getAttribute('${dataAttr}-category') || 'interaction';
|
|
519
|
+
var value = el.getAttribute('${dataAttr}-value');
|
|
520
|
+
trackEvent(eventName, null, category, value ? Number(value) : undefined);
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Observe DOM changes for new elements
|
|
526
|
+
var observer = new MutationObserver(trackDataElements);
|
|
527
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
528
|
+
trackDataElements();`
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function buildPublicAPI(): string {
|
|
532
|
+
return `// Public API
|
|
533
|
+
window.sa = function(command) {
|
|
534
|
+
var args = Array.prototype.slice.call(arguments, 1);
|
|
535
|
+
|
|
536
|
+
switch (command) {
|
|
537
|
+
case 'init':
|
|
538
|
+
// Already initialized via config
|
|
539
|
+
log('Initialized with site:', args[0]);
|
|
540
|
+
break;
|
|
541
|
+
|
|
542
|
+
case 'track':
|
|
543
|
+
case 'trackPageView':
|
|
544
|
+
trackPageView(args[0], args[1]);
|
|
545
|
+
break;
|
|
546
|
+
|
|
547
|
+
case 'event':
|
|
548
|
+
case 'trackEvent':
|
|
549
|
+
trackEvent(args[0], args[1], args[2], args[3]);
|
|
550
|
+
break;
|
|
551
|
+
|
|
552
|
+
case 'identify':
|
|
553
|
+
// Store user ID for future events
|
|
554
|
+
session.userId = args[0];
|
|
555
|
+
log('User identified:', args[0]);
|
|
556
|
+
break;
|
|
557
|
+
|
|
558
|
+
case 'setProperty':
|
|
559
|
+
session.properties = session.properties || {};
|
|
560
|
+
session.properties[args[0]] = args[1];
|
|
561
|
+
break;
|
|
562
|
+
|
|
563
|
+
case 'debug':
|
|
564
|
+
CONFIG.debug = args[0] !== false;
|
|
565
|
+
break;
|
|
566
|
+
|
|
567
|
+
default:
|
|
568
|
+
log('Unknown command:', command);
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
// Process any queued commands
|
|
573
|
+
if (window.sa && window.sa.q) {
|
|
574
|
+
var queue = window.sa.q;
|
|
575
|
+
window.sa.q = [];
|
|
576
|
+
for (var i = 0; i < queue.length; i++) {
|
|
577
|
+
window.sa.apply(null, queue[i]);
|
|
578
|
+
}
|
|
579
|
+
}`
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function buildInitialization(_config: TrackingScriptConfig): string {
|
|
583
|
+
return `// Initialization
|
|
584
|
+
function init() {
|
|
585
|
+
log('Initializing analytics...');
|
|
586
|
+
|
|
587
|
+
// Set up session
|
|
588
|
+
getOrCreateVisitor();
|
|
589
|
+
session = getOrCreateSession();
|
|
590
|
+
|
|
591
|
+
// Send initial page view
|
|
592
|
+
if (document.readyState === 'complete') {
|
|
593
|
+
sendInitialPageView();
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
window.addEventListener('load', sendInitialPageView);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Track page unload for session duration
|
|
600
|
+
window.addEventListener('beforeunload', function() {
|
|
601
|
+
if (session) {
|
|
602
|
+
var duration = Date.now() - session.startedAt;
|
|
603
|
+
trackEvent('session_end', { duration: duration }, 'session', Math.floor(duration / 1000));
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
log('Analytics initialized');
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Wait for DOM ready
|
|
611
|
+
if (document.readyState === 'loading') {
|
|
612
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
init();
|
|
616
|
+
}`
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// ============================================================================
|
|
620
|
+
// Heatmap Tracking
|
|
621
|
+
// ============================================================================
|
|
622
|
+
|
|
623
|
+
function buildHeatmapTrackingSection(config: HeatmapTrackingConfig, debug?: boolean): string {
|
|
624
|
+
const parts: string[] = []
|
|
625
|
+
|
|
626
|
+
parts.push(`// Heatmap Tracking
|
|
627
|
+
var HEATMAP = {
|
|
628
|
+
trackClicks: ${config.trackClicks ?? true},
|
|
629
|
+
trackMovements: ${config.trackMovements ?? false},
|
|
630
|
+
trackScrollPositions: ${config.trackScrollPositions ?? true},
|
|
631
|
+
movementSampleInterval: ${config.movementSampleInterval ?? 100},
|
|
632
|
+
maxMovementPoints: ${config.maxMovementPoints ?? 500},
|
|
633
|
+
flushInterval: ${config.flushInterval ?? 5000}
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
function hmLog() {
|
|
637
|
+
if (${debug ?? false} && console && console.log) {
|
|
638
|
+
console.log.apply(console, ['[Heatmap]'].concat(Array.prototype.slice.call(arguments)));
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// CSS Selector Generator
|
|
643
|
+
function getSelector(el, maxDepth) {
|
|
644
|
+
maxDepth = maxDepth || 5;
|
|
645
|
+
var path = [];
|
|
646
|
+
var depth = 0;
|
|
647
|
+
|
|
648
|
+
while (el && el.nodeType === 1 && depth < maxDepth) {
|
|
649
|
+
var selector = el.nodeName.toLowerCase();
|
|
650
|
+
|
|
651
|
+
if (el.id) {
|
|
652
|
+
path.unshift('#' + el.id);
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
var testId = el.getAttribute('data-testid') || el.getAttribute('data-analytics-id');
|
|
657
|
+
if (testId) {
|
|
658
|
+
path.unshift('[data-testid="' + testId + '"]');
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
var classes = el.className;
|
|
663
|
+
if (typeof classes === 'string' && classes.trim()) {
|
|
664
|
+
var classList = classes.trim().split(/\\s+/).slice(0, 2);
|
|
665
|
+
selector += '.' + classList.join('.');
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
var parent = el.parentNode;
|
|
669
|
+
if (parent && parent.children) {
|
|
670
|
+
var siblings = parent.children;
|
|
671
|
+
var index = 1;
|
|
672
|
+
for (var i = 0; i < siblings.length; i++) {
|
|
673
|
+
if (siblings[i] === el) break;
|
|
674
|
+
if (siblings[i].nodeName === el.nodeName) index++;
|
|
675
|
+
}
|
|
676
|
+
if (siblings.length > 1) {
|
|
677
|
+
selector += ':nth-of-type(' + index + ')';
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
path.unshift(selector);
|
|
682
|
+
el = el.parentNode;
|
|
683
|
+
depth++;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
return path.join(' > ');
|
|
687
|
+
}`)
|
|
688
|
+
|
|
689
|
+
// Click tracking
|
|
690
|
+
if (config.trackClicks !== false) {
|
|
691
|
+
parts.push(`
|
|
692
|
+
|
|
693
|
+
// Click Tracking
|
|
694
|
+
document.addEventListener('click', function(event) {
|
|
695
|
+
var el = event.target;
|
|
696
|
+
if (!el || el.nodeType !== 1) return;
|
|
697
|
+
|
|
698
|
+
var clickData = {
|
|
699
|
+
vx: event.clientX,
|
|
700
|
+
vy: event.clientY,
|
|
701
|
+
dx: event.pageX,
|
|
702
|
+
dy: event.pageY,
|
|
703
|
+
selector: getSelector(el),
|
|
704
|
+
tag: el.tagName.toLowerCase(),
|
|
705
|
+
vw: window.innerWidth,
|
|
706
|
+
vh: window.innerHeight
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
var text = (el.textContent || el.innerText || '').trim().substring(0, 50);
|
|
710
|
+
if (text) clickData.text = text;
|
|
711
|
+
|
|
712
|
+
sendBeacon({
|
|
713
|
+
s: CONFIG.siteId,
|
|
714
|
+
sid: session.id,
|
|
715
|
+
e: 'hm_click',
|
|
716
|
+
u: window.location.pathname,
|
|
717
|
+
p: clickData,
|
|
718
|
+
ts: Date.now()
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
hmLog('Click:', clickData.selector);
|
|
722
|
+
}, true);`)
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Movement tracking
|
|
726
|
+
if (config.trackMovements) {
|
|
727
|
+
parts.push(`
|
|
728
|
+
|
|
729
|
+
// Movement Tracking
|
|
730
|
+
var movementBuffer = [];
|
|
731
|
+
var lastMoveTime = 0;
|
|
732
|
+
var moveFlushTimer = null;
|
|
733
|
+
|
|
734
|
+
function flushMovements() {
|
|
735
|
+
if (movementBuffer.length === 0) return;
|
|
736
|
+
|
|
737
|
+
sendBeacon({
|
|
738
|
+
s: CONFIG.siteId,
|
|
739
|
+
sid: session.id,
|
|
740
|
+
e: 'hm_move',
|
|
741
|
+
u: window.location.pathname,
|
|
742
|
+
p: {
|
|
743
|
+
points: movementBuffer.slice(),
|
|
744
|
+
vw: window.innerWidth,
|
|
745
|
+
vh: window.innerHeight
|
|
746
|
+
},
|
|
747
|
+
ts: Date.now()
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
hmLog('Flushed', movementBuffer.length, 'movement points');
|
|
751
|
+
movementBuffer = [];
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
document.addEventListener('mousemove', function(event) {
|
|
755
|
+
var now = Date.now();
|
|
756
|
+
if (now - lastMoveTime < HEATMAP.movementSampleInterval) return;
|
|
757
|
+
lastMoveTime = now;
|
|
758
|
+
|
|
759
|
+
var x = Math.round((event.clientX / window.innerWidth) * 100);
|
|
760
|
+
var y = Math.round((event.clientY / window.innerHeight) * 100);
|
|
761
|
+
movementBuffer.push([x, y, now]);
|
|
762
|
+
|
|
763
|
+
if (movementBuffer.length >= HEATMAP.maxMovementPoints) {
|
|
764
|
+
flushMovements();
|
|
765
|
+
}
|
|
766
|
+
else if (!moveFlushTimer) {
|
|
767
|
+
moveFlushTimer = setTimeout(function() {
|
|
768
|
+
moveFlushTimer = null;
|
|
769
|
+
flushMovements();
|
|
770
|
+
}, HEATMAP.flushInterval);
|
|
771
|
+
}
|
|
772
|
+
}, { passive: true });`)
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Scroll position tracking
|
|
776
|
+
if (config.trackScrollPositions) {
|
|
777
|
+
parts.push(`
|
|
778
|
+
|
|
779
|
+
// Scroll Position Tracking
|
|
780
|
+
var scrollDepthTimes = {};
|
|
781
|
+
var maxScrollDepth = 0;
|
|
782
|
+
var scrollStartTime = Date.now();
|
|
783
|
+
var lastScrollDepth = 0;
|
|
784
|
+
|
|
785
|
+
function updateHeatmapScroll() {
|
|
786
|
+
var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
787
|
+
var docHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight) - window.innerHeight;
|
|
788
|
+
if (docHeight <= 0) return;
|
|
789
|
+
|
|
790
|
+
var depth = Math.min(100, Math.round((scrollTop / docHeight) * 100));
|
|
791
|
+
var bucket = Math.floor(depth / 10) * 10;
|
|
792
|
+
|
|
793
|
+
if (bucket > 0 && lastScrollDepth > 0) {
|
|
794
|
+
var prevBucket = Math.floor(lastScrollDepth / 10) * 10;
|
|
795
|
+
scrollDepthTimes[prevBucket] = (scrollDepthTimes[prevBucket] || 0) + (Date.now() - scrollStartTime);
|
|
796
|
+
scrollStartTime = Date.now();
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
lastScrollDepth = depth;
|
|
800
|
+
if (depth > maxScrollDepth) maxScrollDepth = depth;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
var hmScrollThrottle = null;
|
|
804
|
+
window.addEventListener('scroll', function() {
|
|
805
|
+
if (hmScrollThrottle) return;
|
|
806
|
+
hmScrollThrottle = setTimeout(function() {
|
|
807
|
+
hmScrollThrottle = null;
|
|
808
|
+
updateHeatmapScroll();
|
|
809
|
+
}, 100);
|
|
810
|
+
}, { passive: true });
|
|
811
|
+
|
|
812
|
+
// Flush scroll data on unload
|
|
813
|
+
window.addEventListener('beforeunload', function() {
|
|
814
|
+
if (maxScrollDepth > 0) {
|
|
815
|
+
if (lastScrollDepth > 0) {
|
|
816
|
+
var bucket = Math.floor(lastScrollDepth / 10) * 10;
|
|
817
|
+
scrollDepthTimes[bucket] = (scrollDepthTimes[bucket] || 0) + (Date.now() - scrollStartTime);
|
|
818
|
+
}
|
|
819
|
+
sendBeacon({
|
|
820
|
+
s: CONFIG.siteId,
|
|
821
|
+
sid: session.id,
|
|
822
|
+
e: 'hm_scroll',
|
|
823
|
+
u: window.location.pathname,
|
|
824
|
+
p: {
|
|
825
|
+
depths: scrollDepthTimes,
|
|
826
|
+
maxDepth: maxScrollDepth,
|
|
827
|
+
docHeight: Math.max(document.body.scrollHeight, document.documentElement.scrollHeight),
|
|
828
|
+
vh: window.innerHeight
|
|
829
|
+
},
|
|
830
|
+
ts: Date.now()
|
|
831
|
+
});
|
|
832
|
+
hmLog('Scroll data sent:', maxScrollDepth + '%');
|
|
833
|
+
}
|
|
834
|
+
});`)
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Movement flush on unload
|
|
838
|
+
if (config.trackMovements) {
|
|
839
|
+
parts.push(`
|
|
840
|
+
|
|
841
|
+
window.addEventListener('beforeunload', function() {
|
|
842
|
+
if (movementBuffer.length > 0) flushMovements();
|
|
843
|
+
});`)
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
parts.push(`
|
|
847
|
+
|
|
848
|
+
hmLog('Heatmap tracking initialized');`)
|
|
849
|
+
|
|
850
|
+
return parts.join('')
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// ============================================================================
|
|
854
|
+
// Minification
|
|
855
|
+
// ============================================================================
|
|
856
|
+
|
|
857
|
+
function minifyScript(script: string): string {
|
|
858
|
+
return script
|
|
859
|
+
// Remove comments
|
|
860
|
+
.replace(/\/\/.*$/gm, '')
|
|
861
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
862
|
+
// Remove extra whitespace
|
|
863
|
+
.replace(/\n\s*\n/g, '\n')
|
|
864
|
+
.replace(/^\s+/gm, '')
|
|
865
|
+
.replace(/\s+$/gm, '')
|
|
866
|
+
// Compact lines
|
|
867
|
+
.split('\n')
|
|
868
|
+
.filter(line => line.trim())
|
|
869
|
+
.join('')
|
|
870
|
+
// Add semicolons where needed
|
|
871
|
+
.replace(/}\s*function/g, '};function')
|
|
872
|
+
.replace(/}\s*var/g, '};var')
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// ============================================================================
|
|
876
|
+
// Script Variants
|
|
877
|
+
// ============================================================================
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Generate a minimal tracking script (page views only)
|
|
881
|
+
*/
|
|
882
|
+
export function generateMinimalTrackingScript(config: Pick<TrackingScriptConfig, 'siteId' | 'apiEndpoint' | 'honorDnt' | 'stealthMode'>): string {
|
|
883
|
+
const endpoint = config.stealthMode ? '/t' : '/collect'
|
|
884
|
+
const parts: string[] = []
|
|
885
|
+
parts.push('(function(){')
|
|
886
|
+
// JSON.stringify so a quote/backslash/newline in siteId/apiEndpoint can't
|
|
887
|
+
// break out of the string literal and inject JS into every tracked page.
|
|
888
|
+
parts.push('var s=' + JSON.stringify(config.siteId) + ',e=' + JSON.stringify(config.apiEndpoint + endpoint) + ';')
|
|
889
|
+
if (config.honorDnt) {
|
|
890
|
+
parts.push('if(navigator.doNotTrack===\'1\'||navigator.globalPrivacyControl===true)return;')
|
|
891
|
+
}
|
|
892
|
+
parts.push('var sid=sessionStorage.getItem(\'sa_s\')||Math.random().toString(36).slice(2);')
|
|
893
|
+
parts.push('sessionStorage.setItem(\'sa_s\',sid);')
|
|
894
|
+
parts.push('var tz=\'\';try{tz=Intl.DateTimeFormat().resolvedOptions().timeZone||\'\'}catch(_e){}')
|
|
895
|
+
parts.push('navigator.sendBeacon(e,JSON.stringify({')
|
|
896
|
+
parts.push('s:s,sid:sid,e:\'pageview\',')
|
|
897
|
+
parts.push('u:location.href,r:document.referrer,t:document.title,')
|
|
898
|
+
parts.push('sw:screen.width,sh:screen.height,tz:tz')
|
|
899
|
+
parts.push('}));')
|
|
900
|
+
parts.push('})();')
|
|
901
|
+
return parts.join('\n')
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Generate a Google Analytics 4-style tracking script
|
|
906
|
+
*/
|
|
907
|
+
export function generateGA4StyleScript(config: TrackingScriptConfig): string {
|
|
908
|
+
return `window.dataLayer = window.dataLayer || [];
|
|
909
|
+
function gtag(){dataLayer.push(arguments);}
|
|
910
|
+
gtag('js', new Date());
|
|
911
|
+
gtag('config', ${JSON.stringify(config.siteId)}, {
|
|
912
|
+
send_page_view: ${config.autoPageView !== false},
|
|
913
|
+
cookie_domain: ${JSON.stringify(config.cookieDomain || 'auto')}
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
// Custom analytics layer
|
|
917
|
+
(function(){
|
|
918
|
+
var endpoint = ${JSON.stringify(`${config.apiEndpoint}/collect`)};
|
|
919
|
+
var siteId = ${JSON.stringify(config.siteId)};
|
|
920
|
+
|
|
921
|
+
function processDataLayer() {
|
|
922
|
+
while (dataLayer.length > 0) {
|
|
923
|
+
var item = dataLayer.shift();
|
|
924
|
+
if (typeof item === 'function') {
|
|
925
|
+
item();
|
|
926
|
+
}
|
|
927
|
+
else if (item[0] === 'event') {
|
|
928
|
+
navigator.sendBeacon && navigator.sendBeacon(endpoint, JSON.stringify({
|
|
929
|
+
s: siteId,
|
|
930
|
+
e: 'event',
|
|
931
|
+
en: item[1],
|
|
932
|
+
p: item[2]
|
|
933
|
+
}));
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
var push = dataLayer.push;
|
|
939
|
+
dataLayer.push = function() {
|
|
940
|
+
push.apply(dataLayer, arguments);
|
|
941
|
+
processDataLayer();
|
|
942
|
+
};
|
|
943
|
+
})();`
|
|
944
|
+
}
|