@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.
Files changed (237) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +361 -0
  4. package/bin/cli.ts +169 -0
  5. package/dist/Analytics.d.ts +558 -0
  6. package/dist/api.d.ts +109 -0
  7. package/dist/batching.d.ts +93 -0
  8. package/dist/chunk-2mx7fq49.js +4 -0
  9. package/dist/chunk-3z29508k.js +204 -0
  10. package/dist/chunk-deephkz6.js +56 -0
  11. package/dist/chunk-j261vgyp.js +305 -0
  12. package/dist/chunk-xga17tz7.js +207 -0
  13. package/dist/config.d.ts +132 -0
  14. package/dist/dashboard/components/index.d.ts +80 -0
  15. package/dist/dashboard/composables/useAnalytics.d.ts +99 -0
  16. package/dist/dashboard/index.d.ts +110 -0
  17. package/dist/dashboard/types/index.d.ts +144 -0
  18. package/dist/dashboard/utils/index.d.ts +81 -0
  19. package/dist/dynamodb.d.ts +86 -0
  20. package/dist/funnels.d.ts +104 -0
  21. package/dist/geolocation.d.ts +96 -0
  22. package/dist/index.d.ts +371 -0
  23. package/dist/index.js +10211 -0
  24. package/dist/infrastructure/cdk.d.ts +60 -0
  25. package/dist/infrastructure/cloudformation.d.ts +44 -0
  26. package/dist/infrastructure/index.d.ts +45 -0
  27. package/dist/infrastructure/setup.d.ts +133 -0
  28. package/dist/integrations/cloudflare.d.ts +76 -0
  29. package/dist/integrations/hono.d.ts +60 -0
  30. package/dist/integrations/index.d.ts +24 -0
  31. package/dist/integrations/nuxt.d.ts +7 -0
  32. package/dist/integrations/nuxt.js +42 -0
  33. package/dist/integrations/runtime/use-ts-analytics.d.ts +18 -0
  34. package/dist/integrations/runtime/use-ts-analytics.js +17 -0
  35. package/dist/integrations/stx.d.ts +88 -0
  36. package/dist/integrations/stx.js +18 -0
  37. package/dist/lib/crypto-random.d.ts +4 -0
  38. package/dist/lib/salt.d.ts +16 -0
  39. package/dist/local.d.ts +56 -0
  40. package/dist/model-connector.d.ts +145 -0
  41. package/dist/models/AggregatedStats.d.ts +9 -0
  42. package/dist/models/CampaignStats.d.ts +9 -0
  43. package/dist/models/Conversion.d.ts +11 -0
  44. package/dist/models/CustomEvent.d.ts +11 -0
  45. package/dist/models/DeviceStats.d.ts +9 -0
  46. package/dist/models/EventStats.d.ts +9 -0
  47. package/dist/models/GeoStats.d.ts +9 -0
  48. package/dist/models/Goal.d.ts +9 -0
  49. package/dist/models/GoalStats.d.ts +9 -0
  50. package/dist/models/PageStats.d.ts +11 -0
  51. package/dist/models/PageView.d.ts +13 -0
  52. package/dist/models/RealtimeStats.d.ts +9 -0
  53. package/dist/models/ReferrerStats.d.ts +9 -0
  54. package/dist/models/Session.d.ts +11 -0
  55. package/dist/models/Site.d.ts +11 -0
  56. package/dist/models/index.d.ts +28 -0
  57. package/dist/models/types.d.ts +60 -0
  58. package/dist/sqs-buffering.d.ts +243 -0
  59. package/dist/stacks-integration.d.ts +159 -0
  60. package/dist/tracking-script.d.ts +71 -0
  61. package/dist/tracking.d.ts +17 -0
  62. package/dist/tracking.js +26 -0
  63. package/dist/types.d.ts +595 -0
  64. package/dist/utils/geolocation.d.ts +111 -0
  65. package/dist/utils/user-agent.d.ts +17 -0
  66. package/dist/version.d.ts +7 -0
  67. package/package.json +119 -0
  68. package/src/Analytics.ts +3349 -0
  69. package/src/api.ts +1286 -0
  70. package/src/assets/crosswind.css +2220 -0
  71. package/src/batching.ts +452 -0
  72. package/src/components/dashboard/index.ts +18 -0
  73. package/src/config.ts +456 -0
  74. package/src/dashboard/Dashboard.stx +1517 -0
  75. package/src/dashboard/components/AlertCard.stx +177 -0
  76. package/src/dashboard/components/AnalyticsDashboard.stx +354 -0
  77. package/src/dashboard/components/AnimatedNumber.stx +86 -0
  78. package/src/dashboard/components/BarChart.stx +220 -0
  79. package/src/dashboard/components/BrowserBreakdown.stx +98 -0
  80. package/src/dashboard/components/BrowsersTable.stx +125 -0
  81. package/src/dashboard/components/CampaignBreakdown.stx +163 -0
  82. package/src/dashboard/components/CampaignTable.stx +238 -0
  83. package/src/dashboard/components/CountryList.stx +101 -0
  84. package/src/dashboard/components/DataTable.stx +226 -0
  85. package/src/dashboard/components/DateRangePicker.stx +77 -0
  86. package/src/dashboard/components/DeviceBreakdown.stx +94 -0
  87. package/src/dashboard/components/DevicesTable.stx +163 -0
  88. package/src/dashboard/components/DonutChart.stories.ts +55 -0
  89. package/src/dashboard/components/DonutChart.stx +157 -0
  90. package/src/dashboard/components/EmptyState.stx +90 -0
  91. package/src/dashboard/components/EngagementMetrics.stx +183 -0
  92. package/src/dashboard/components/EventsSection.stx +192 -0
  93. package/src/dashboard/components/FilterBar.stx +104 -0
  94. package/src/dashboard/components/FullAnalyticsDashboard.stx +455 -0
  95. package/src/dashboard/components/FunnelChart.stx +142 -0
  96. package/src/dashboard/components/GeoTable.stx +337 -0
  97. package/src/dashboard/components/GoalsPanel.stx +109 -0
  98. package/src/dashboard/components/Header.stx +39 -0
  99. package/src/dashboard/components/HeatmapChart.stx +140 -0
  100. package/src/dashboard/components/LiveActivityFeed.stx +172 -0
  101. package/src/dashboard/components/MetricComparison.stx +106 -0
  102. package/src/dashboard/components/MiniStats.stx +96 -0
  103. package/src/dashboard/components/OSBreakdown.stx +124 -0
  104. package/src/dashboard/components/PageDetailCard.stx +102 -0
  105. package/src/dashboard/components/PagesTable.stx +127 -0
  106. package/src/dashboard/components/ProgressRing.stx +89 -0
  107. package/src/dashboard/components/RealtimeCounter.stories.ts +45 -0
  108. package/src/dashboard/components/RealtimeCounter.stx +46 -0
  109. package/src/dashboard/components/ReferrersTable.stx +180 -0
  110. package/src/dashboard/components/SparklineChart.stx +160 -0
  111. package/src/dashboard/components/StatCard.stories.ts +58 -0
  112. package/src/dashboard/components/StatCard.stx +90 -0
  113. package/src/dashboard/components/SummaryStats.stx +81 -0
  114. package/src/dashboard/components/TabNav.stx +66 -0
  115. package/src/dashboard/components/ThemeSwitcher.stx +124 -0
  116. package/src/dashboard/components/TimeSeriesChart.stx +106 -0
  117. package/src/dashboard/components/TopList.stories.ts +58 -0
  118. package/src/dashboard/components/TopList.stx +74 -0
  119. package/src/dashboard/components/TrendIndicator.stx +84 -0
  120. package/src/dashboard/components/heatmap/ClickHeatmap.stx +264 -0
  121. package/src/dashboard/components/heatmap/ElementClickList.stx +125 -0
  122. package/src/dashboard/components/heatmap/HeatmapControls.stx +125 -0
  123. package/src/dashboard/components/heatmap/HeatmapLegend.stx +69 -0
  124. package/src/dashboard/components/heatmap/PageHeatmap.stx +264 -0
  125. package/src/dashboard/components/heatmap/ScrollHeatmap.stx +127 -0
  126. package/src/dashboard/components/heatmap/index.ts +12 -0
  127. package/src/dashboard/components/index.ts +86 -0
  128. package/src/dashboard/composables/useAnalytics.ts +465 -0
  129. package/src/dashboard/demo/DemoApp.stx +370 -0
  130. package/src/dashboard/demo/index.ts +8 -0
  131. package/src/dashboard/demo/mockData.ts +234 -0
  132. package/src/dashboard/index.ts +117 -0
  133. package/src/dashboard/stx-shim.d.ts +4 -0
  134. package/src/dashboard/types/index.ts +203 -0
  135. package/src/dashboard/utils/index.ts +426 -0
  136. package/src/dynamodb.ts +344 -0
  137. package/src/funnels.ts +534 -0
  138. package/src/geolocation.ts +515 -0
  139. package/src/handlers/alerts.ts +328 -0
  140. package/src/handlers/annotations.ts +128 -0
  141. package/src/handlers/api-keys.ts +240 -0
  142. package/src/handlers/auth.ts +1020 -0
  143. package/src/handlers/authz.ts +137 -0
  144. package/src/handlers/collect.ts +724 -0
  145. package/src/handlers/data.ts +625 -0
  146. package/src/handlers/experiments.ts +138 -0
  147. package/src/handlers/funnels.ts +216 -0
  148. package/src/handlers/goals.ts +218 -0
  149. package/src/handlers/heatmaps.ts +272 -0
  150. package/src/handlers/index.ts +56 -0
  151. package/src/handlers/lib/read-cache.ts +63 -0
  152. package/src/handlers/misc.ts +486 -0
  153. package/src/handlers/oauth.ts +233 -0
  154. package/src/handlers/performance.ts +388 -0
  155. package/src/handlers/sessions.ts +413 -0
  156. package/src/handlers/sharing.ts +198 -0
  157. package/src/handlers/stats.ts +1368 -0
  158. package/src/handlers/team.ts +161 -0
  159. package/src/handlers/uptime.ts +283 -0
  160. package/src/handlers/views.ts +390 -0
  161. package/src/handlers/webhooks.ts +226 -0
  162. package/src/heatmap/index.ts +32 -0
  163. package/src/heatmap/tracking-script.ts +452 -0
  164. package/src/heatmap/types.ts +79 -0
  165. package/src/index.ts +387 -0
  166. package/src/infrastructure/cdk.ts +496 -0
  167. package/src/infrastructure/cloudformation.ts +595 -0
  168. package/src/infrastructure/index.ts +48 -0
  169. package/src/infrastructure/setup.ts +611 -0
  170. package/src/integrations/cloudflare.ts +732 -0
  171. package/src/integrations/hono.ts +589 -0
  172. package/src/integrations/index.ts +27 -0
  173. package/src/integrations/nuxt.ts +78 -0
  174. package/src/integrations/runtime/use-ts-analytics.ts +32 -0
  175. package/src/integrations/stx.ts +138 -0
  176. package/src/jobs/index.ts +127 -0
  177. package/src/lib/crypto-random.ts +41 -0
  178. package/src/lib/ddb-errors.ts +20 -0
  179. package/src/lib/dynamodb.ts +216 -0
  180. package/src/lib/email.ts +38 -0
  181. package/src/lib/ga-import.ts +471 -0
  182. package/src/lib/ga4-api.ts +244 -0
  183. package/src/lib/goals.ts +205 -0
  184. package/src/lib/index.ts +6 -0
  185. package/src/lib/ingest-counters.ts +123 -0
  186. package/src/lib/log.ts +36 -0
  187. package/src/lib/membership.ts +98 -0
  188. package/src/lib/plans.ts +90 -0
  189. package/src/lib/quota.ts +46 -0
  190. package/src/lib/rate-limit.ts +27 -0
  191. package/src/lib/rollups.ts +472 -0
  192. package/src/lib/salt.ts +97 -0
  193. package/src/lib/scheduler.ts +97 -0
  194. package/src/lib/significance.ts +66 -0
  195. package/src/lib/site-retention.ts +56 -0
  196. package/src/local.ts +360 -0
  197. package/src/model-connector.ts +616 -0
  198. package/src/models/AggregatedStats.ts +162 -0
  199. package/src/models/CampaignStats.ts +141 -0
  200. package/src/models/Conversion.ts +135 -0
  201. package/src/models/CustomEvent.ts +123 -0
  202. package/src/models/DeviceStats.ts +105 -0
  203. package/src/models/EventStats.ts +108 -0
  204. package/src/models/GeoStats.ts +112 -0
  205. package/src/models/Goal.ts +105 -0
  206. package/src/models/GoalStats.ts +106 -0
  207. package/src/models/PageStats.ts +152 -0
  208. package/src/models/PageView.ts +277 -0
  209. package/src/models/RealtimeStats.ts +96 -0
  210. package/src/models/ReferrerStats.ts +115 -0
  211. package/src/models/Session.ts +235 -0
  212. package/src/models/Site.ts +115 -0
  213. package/src/models/index.ts +50 -0
  214. package/src/models/orm/index.ts +2043 -0
  215. package/src/models/types.ts +71 -0
  216. package/src/router.ts +521 -0
  217. package/src/sqs-buffering.ts +806 -0
  218. package/src/stacks-integration.ts +527 -0
  219. package/src/tracking-script.ts +944 -0
  220. package/src/tracking.ts +27 -0
  221. package/src/types/analytics.ts +219 -0
  222. package/src/types/api.ts +75 -0
  223. package/src/types/bun-router.d.ts +15 -0
  224. package/src/types/dashboard.ts +62 -0
  225. package/src/types/index.ts +66 -0
  226. package/src/types/stx.d.ts +27 -0
  227. package/src/types/window.d.ts +101 -0
  228. package/src/types.ts +911 -0
  229. package/src/utils/cache.ts +148 -0
  230. package/src/utils/date.ts +118 -0
  231. package/src/utils/filters.ts +71 -0
  232. package/src/utils/geolocation.ts +323 -0
  233. package/src/utils/index.ts +9 -0
  234. package/src/utils/response.ts +180 -0
  235. package/src/utils/timezone-country.ts +242 -0
  236. package/src/utils/user-agent.ts +110 -0
  237. 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
+ }