@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,272 @@
1
+ /**
2
+ * Heatmap handlers
3
+ */
4
+
5
+ import { dynamodb, TABLE_NAME, unmarshall } from '../lib/dynamodb'
6
+ import { parseDateRange } from '../utils/date'
7
+ import { jsonResponse, errorResponse } from '../utils/response'
8
+ import { getQueryParams } from '../../deploy/lambda-adapter'
9
+
10
+ /**
11
+ * GET /api/sites/{siteId}/heatmap/clicks
12
+ */
13
+ export async function handleGetHeatmapClicks(request: Request, siteId: string): Promise<Response> {
14
+ try {
15
+ const query = getQueryParams(request)
16
+ const { startDate, endDate } = parseDateRange(query)
17
+ const path = query.path
18
+ const device = query.device
19
+ const limit = Math.min(Number(query.limit) || 1000, 10000)
20
+
21
+ const result = await dynamodb.query({
22
+ TableName: TABLE_NAME,
23
+ KeyConditionExpression: 'pk = :pk AND sk BETWEEN :start AND :end',
24
+ ExpressionAttributeValues: {
25
+ ':pk': { S: `SITE#${siteId}` },
26
+ ':start': { S: `HMCLICK#${startDate.toISOString()}` },
27
+ ':end': { S: `HMCLICK#${endDate.toISOString()}` },
28
+ },
29
+ Limit: limit,
30
+ }) as { Items?: any[] }
31
+
32
+ let clicks = (result.Items || []).map(unmarshall)
33
+
34
+ if (path) {
35
+ clicks = clicks.filter(c => c.path === path || c.path === decodeURIComponent(path))
36
+ }
37
+
38
+ if (device && device !== 'all') {
39
+ clicks = clicks.filter(c => c.deviceType === device)
40
+ }
41
+
42
+ const gridSize = Number(query.gridSize) || 20
43
+ const clickGrid: Record<string, { count: number; elements: Record<string, number> }> = {}
44
+
45
+ for (const click of clicks) {
46
+ const gridX = Math.floor(click.documentX / gridSize) * gridSize
47
+ const gridY = Math.floor(click.documentY / gridSize) * gridSize
48
+ const key = `${gridX},${gridY}`
49
+
50
+ if (!clickGrid[key]) {
51
+ clickGrid[key] = { count: 0, elements: {} }
52
+ }
53
+ clickGrid[key].count++
54
+
55
+ if (click.selector) {
56
+ clickGrid[key].elements[click.selector] = (clickGrid[key].elements[click.selector] || 0) + 1
57
+ }
58
+ }
59
+
60
+ const heatmapData = Object.entries(clickGrid).map(([key, data]) => {
61
+ const [x, y] = key.split(',').map(Number)
62
+ const topElement = Object.entries(data.elements)
63
+ .sort((a, b) => b[1] - a[1])[0]
64
+
65
+ return {
66
+ x,
67
+ y,
68
+ count: data.count,
69
+ topElement: topElement ? { selector: topElement[0], count: topElement[1] } : null,
70
+ }
71
+ })
72
+
73
+ const elementStats: Record<string, { count: number; tag: string; text?: string }> = {}
74
+ for (const click of clicks) {
75
+ if (click.selector) {
76
+ if (!elementStats[click.selector]) {
77
+ elementStats[click.selector] = { count: 0, tag: click.elementTag || '', text: click.elementText }
78
+ }
79
+ elementStats[click.selector].count++
80
+ }
81
+ }
82
+
83
+ const topElements = Object.entries(elementStats)
84
+ .map(([selector, stats]) => ({
85
+ selector,
86
+ tag: stats.tag,
87
+ text: stats.text,
88
+ count: stats.count,
89
+ }))
90
+ .sort((a, b) => b.count - a.count)
91
+ .slice(0, 20)
92
+
93
+ return jsonResponse({
94
+ clicks: heatmapData,
95
+ topElements,
96
+ totalClicks: clicks.length,
97
+ dateRange: {
98
+ start: startDate.toISOString(),
99
+ end: endDate.toISOString(),
100
+ },
101
+ })
102
+ }
103
+ catch (error) {
104
+ console.error('Heatmap clicks error:', error)
105
+ return errorResponse('Failed to fetch heatmap clicks')
106
+ }
107
+ }
108
+
109
+ /**
110
+ * GET /api/sites/{siteId}/heatmap/scroll
111
+ */
112
+ export async function handleGetHeatmapScroll(request: Request, siteId: string): Promise<Response> {
113
+ try {
114
+ const query = getQueryParams(request)
115
+ const { startDate, endDate } = parseDateRange(query)
116
+ const path = query.path
117
+ const device = query.device
118
+
119
+ const result = await dynamodb.query({
120
+ TableName: TABLE_NAME,
121
+ KeyConditionExpression: 'pk = :pk AND begins_with(sk, :prefix)',
122
+ ExpressionAttributeValues: {
123
+ ':pk': { S: `SITE#${siteId}` },
124
+ ':prefix': { S: 'HMSCROLL#' },
125
+ },
126
+ }) as { Items?: any[] }
127
+
128
+ let scrollData = (result.Items || []).map(unmarshall).filter(s => {
129
+ const timestamp = new Date(s.timestamp)
130
+ return timestamp >= startDate && timestamp <= endDate
131
+ })
132
+
133
+ if (path) {
134
+ scrollData = scrollData.filter(s => s.path === path || s.path === decodeURIComponent(path))
135
+ }
136
+
137
+ if (device && device !== 'all') {
138
+ scrollData = scrollData.filter(s => s.deviceType === device)
139
+ }
140
+
141
+ const depthBuckets: Record<number, { sessions: number; totalTime: number }> = {}
142
+ let totalSessions = 0
143
+ let totalMaxDepth = 0
144
+
145
+ for (const scroll of scrollData) {
146
+ totalSessions++
147
+ totalMaxDepth += scroll.maxScrollDepth || 0
148
+
149
+ const depths = scroll.scrollDepths || {}
150
+ for (const [depth, time] of Object.entries(depths)) {
151
+ const depthNum = Number(depth)
152
+ if (!depthBuckets[depthNum]) {
153
+ depthBuckets[depthNum] = { sessions: 0, totalTime: 0 }
154
+ }
155
+ depthBuckets[depthNum].sessions++
156
+ depthBuckets[depthNum].totalTime += time as number
157
+ }
158
+ }
159
+
160
+ const scrollDepths = Object.entries(depthBuckets)
161
+ .map(([depth, data]) => ({
162
+ depth: Number(depth),
163
+ reachPercentage: totalSessions > 0 ? Math.round((data.sessions / totalSessions) * 100) : 0,
164
+ avgTimeMs: data.sessions > 0 ? Math.round(data.totalTime / data.sessions) : 0,
165
+ sessions: data.sessions,
166
+ }))
167
+ .sort((a, b) => a.depth - b.depth)
168
+
169
+ const avgMaxDepth = totalSessions > 0 ? Math.round(totalMaxDepth / totalSessions) : 0
170
+
171
+ return jsonResponse({
172
+ scrollDepths,
173
+ avgMaxDepth,
174
+ totalSessions,
175
+ dateRange: {
176
+ start: startDate.toISOString(),
177
+ end: endDate.toISOString(),
178
+ },
179
+ })
180
+ }
181
+ catch (error) {
182
+ console.error('Heatmap scroll error:', error)
183
+ return errorResponse('Failed to fetch heatmap scroll data')
184
+ }
185
+ }
186
+
187
+ /**
188
+ * GET /api/sites/{siteId}/heatmap/pages
189
+ */
190
+ export async function handleGetHeatmapPages(request: Request, siteId: string): Promise<Response> {
191
+ try {
192
+ const query = getQueryParams(request)
193
+ const { startDate, endDate } = parseDateRange(query)
194
+ const limit = Math.min(Number(query.limit) || 20, 100)
195
+
196
+ const clicksResult = await dynamodb.query({
197
+ TableName: TABLE_NAME,
198
+ KeyConditionExpression: 'pk = :pk AND sk BETWEEN :start AND :end',
199
+ ExpressionAttributeValues: {
200
+ ':pk': { S: `SITE#${siteId}` },
201
+ ':start': { S: `HMCLICK#${startDate.toISOString()}` },
202
+ ':end': { S: `HMCLICK#${endDate.toISOString()}` },
203
+ },
204
+ }) as { Items?: any[] }
205
+
206
+ const scrollResult = await dynamodb.query({
207
+ TableName: TABLE_NAME,
208
+ KeyConditionExpression: 'pk = :pk AND begins_with(sk, :prefix)',
209
+ ExpressionAttributeValues: {
210
+ ':pk': { S: `SITE#${siteId}` },
211
+ ':prefix': { S: 'HMSCROLL#' },
212
+ },
213
+ }) as { Items?: any[] }
214
+
215
+ const clicks = (clicksResult.Items || []).map(unmarshall)
216
+ const scrolls = (scrollResult.Items || []).map(unmarshall).filter(s => {
217
+ const timestamp = new Date(s.timestamp)
218
+ return timestamp >= startDate && timestamp <= endDate
219
+ })
220
+
221
+ const pageStats: Record<string, {
222
+ clicks: number
223
+ scrollSessions: number
224
+ avgScrollDepth: number
225
+ totalScrollDepth: number
226
+ }> = {}
227
+
228
+ for (const click of clicks) {
229
+ const path = click.path
230
+ if (!pageStats[path]) {
231
+ pageStats[path] = { clicks: 0, scrollSessions: 0, avgScrollDepth: 0, totalScrollDepth: 0 }
232
+ }
233
+ pageStats[path].clicks++
234
+ }
235
+
236
+ for (const scroll of scrolls) {
237
+ const path = scroll.path
238
+ if (!pageStats[path]) {
239
+ pageStats[path] = { clicks: 0, scrollSessions: 0, avgScrollDepth: 0, totalScrollDepth: 0 }
240
+ }
241
+ pageStats[path].scrollSessions++
242
+ pageStats[path].totalScrollDepth += scroll.maxScrollDepth || 0
243
+ }
244
+
245
+ const pages = Object.entries(pageStats)
246
+ .map(([path, stats]) => ({
247
+ path,
248
+ clicks: stats.clicks,
249
+ scrollSessions: stats.scrollSessions,
250
+ avgScrollDepth: stats.scrollSessions > 0
251
+ ? Math.round(stats.totalScrollDepth / stats.scrollSessions)
252
+ : 0,
253
+ hasClickData: stats.clicks > 0,
254
+ hasScrollData: stats.scrollSessions > 0,
255
+ }))
256
+ .sort((a, b) => (b.clicks + b.scrollSessions) - (a.clicks + a.scrollSessions))
257
+ .slice(0, limit)
258
+
259
+ return jsonResponse({
260
+ pages,
261
+ totalPages: Object.keys(pageStats).length,
262
+ dateRange: {
263
+ start: startDate.toISOString(),
264
+ end: endDate.toISOString(),
265
+ },
266
+ })
267
+ }
268
+ catch (error) {
269
+ console.error('Heatmap pages error:', error)
270
+ return errorResponse('Failed to fetch heatmap pages')
271
+ }
272
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Handlers barrel export
3
+ */
4
+
5
+ // Stats & Analytics
6
+ export * from './stats'
7
+
8
+ // Goals
9
+ export * from './goals'
10
+
11
+ // Sessions
12
+ export * from './sessions'
13
+
14
+ // Heatmaps
15
+ export * from './heatmaps'
16
+
17
+ // Errors
18
+
19
+ // Performance & Vitals
20
+ export * from './performance'
21
+
22
+ // Funnels
23
+ export * from './funnels'
24
+
25
+ // Annotations
26
+ export * from './annotations'
27
+
28
+ // Experiments
29
+ export * from './experiments'
30
+
31
+ // Alerts
32
+ export * from './alerts'
33
+
34
+ // API Keys
35
+ export * from './api-keys'
36
+
37
+ // Uptime
38
+ export * from './uptime'
39
+
40
+ // Webhooks
41
+ export * from './webhooks'
42
+
43
+ // Team
44
+ export * from './team'
45
+
46
+ // Data (export, GDPR, retention, insights)
47
+ export * from './data'
48
+
49
+ // Sharing
50
+ export * from './sharing'
51
+
52
+ // Collection
53
+ export * from './collect'
54
+
55
+ // Misc (health, sites, revenue)
56
+ export * from './misc'
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Read-through cache for expensive dashboard read endpoints.
3
+ *
4
+ * Each cached endpoint currently re-scans raw DynamoDB events and aggregates
5
+ * them in memory on every request. This wraps such a handler so the computed
6
+ * JSON response is cached for a short TTL keyed by (path + query), letting
7
+ * repeated polls (e.g. the summary card every ~15s) reuse the result instead of
8
+ * re-scanning.
9
+ *
10
+ * Safety: on by default with short TTLs (#96) — set ANALYTICS_READ_CACHE=false
11
+ * to opt out. Only caches successful (200) responses, and ALWAYS falls back to
12
+ * the real handler on a cache miss — a missing/expired entry never yields wrong
13
+ * or empty data. The cache is the existing in-memory generic cache (per warm
14
+ * process; resets on cold start). Freshness-critical endpoints (realtime,
15
+ * live view, error triage statuses) are deliberately not wrapped.
16
+ */
17
+
18
+ import { getFromCache, setInCache } from '../../utils/cache'
19
+
20
+ interface CachedResponse {
21
+ body: string
22
+ status: number
23
+ headers: Record<string, string>
24
+ }
25
+
26
+ /** Whether the read-through cache is enabled (default on; opt out with ANALYTICS_READ_CACHE=false). */
27
+ export function readCacheEnabled(): boolean {
28
+ return process.env.ANALYTICS_READ_CACHE !== 'false'
29
+ }
30
+
31
+ /**
32
+ * Wrap a read handler with the read-through cache. When disabled, this is a
33
+ * transparent pass-through to `compute`.
34
+ */
35
+ export async function withReadCache(
36
+ request: Request,
37
+ name: string,
38
+ ttlMs: number,
39
+ compute: () => Promise<Response>,
40
+ ): Promise<Response> {
41
+ if (!readCacheEnabled()) return compute()
42
+
43
+ const url = new URL(request.url)
44
+ const key = `read:${name}:${url.pathname}:${url.search}`
45
+
46
+ const hit = getFromCache<CachedResponse>(key)
47
+ if (hit) {
48
+ return new Response(hit.body, { status: hit.status, headers: hit.headers })
49
+ }
50
+
51
+ const res = await compute()
52
+ // Only cache successful responses; errors must re-run next time.
53
+ if (res.status === 200) {
54
+ try {
55
+ const body = await res.clone().text()
56
+ const headers = Object.fromEntries(res.headers.entries())
57
+ setInCache<CachedResponse>(key, { body, status: res.status, headers }, ttlMs)
58
+ } catch {
59
+ // Caching is best-effort; never fail the request over a cache write.
60
+ }
61
+ }
62
+ return res
63
+ }