@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,527 @@
1
+ // ============================================================================
2
+ // LEGACY -- NOT the live request path (#95)
3
+ // ============================================================================
4
+ // Built on the in-memory legacy layer (src/api.ts). Kept for backwards
5
+ // compatibility of the published library API only. The live API is
6
+ // src/router.ts + src/handlers/* -- add new functionality there.
7
+ // ============================================================================
8
+ // Stacks Framework Integration
9
+ // ============================================================================
10
+ // Integration with the Stacks framework analytics system
11
+ // This provides a DynamoDB-backed analytics driver for Stacks
12
+
13
+ import type { AnalyticsAPIConfig, HandlerContext } from './index'
14
+ import {
15
+ AggregationPipeline,
16
+ AnalyticsAPI,
17
+
18
+ AnalyticsQueryAPI,
19
+ AnalyticsStore,
20
+ generateTrackingScript,
21
+
22
+ } from './index'
23
+
24
+ // ============================================================================
25
+ // Stacks Analytics Driver Configuration
26
+ // ============================================================================
27
+
28
+ /**
29
+ * DynamoDB analytics driver configuration for Stacks
30
+ */
31
+ export interface DynamoDBAnalyticsConfig {
32
+ /** DynamoDB table name */
33
+ tableName: string
34
+ /** AWS region */
35
+ region?: string
36
+ /** Site ID for this installation */
37
+ siteId: string
38
+ /** API endpoint URL */
39
+ apiEndpoint?: string
40
+ /** Whether to use TTL for data retention */
41
+ useTtl?: boolean
42
+ /** Raw event retention in days */
43
+ rawEventRetentionDays?: number
44
+ /** Hourly aggregate retention in days */
45
+ hourlyRetentionDays?: number
46
+ /** Daily aggregate retention in years */
47
+ dailyRetentionYears?: number
48
+ /** Privacy settings */
49
+ privacy?: {
50
+ /** Respect Do Not Track header */
51
+ honorDnt?: boolean
52
+ /** Hash visitor IPs */
53
+ hashIps?: boolean
54
+ /** Collect geolocation data */
55
+ collectGeolocation?: boolean
56
+ }
57
+ /** Tracking options */
58
+ tracking?: {
59
+ /** Track hash changes (for SPAs) */
60
+ trackHashChanges?: boolean
61
+ /** Track outbound links */
62
+ trackOutboundLinks?: boolean
63
+ /** Excluded paths (regex patterns) */
64
+ excludedPaths?: string[]
65
+ /** Excluded IPs */
66
+ excludedIps?: string[]
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Stacks analytics options interface (matches Stacks pattern)
72
+ */
73
+ export interface StacksAnalyticsOptions {
74
+ driver: 'google-analytics' | 'fathom' | 'dynamodb'
75
+
76
+ drivers: {
77
+ googleAnalytics?: {
78
+ trackingId: string
79
+ }
80
+ fathom?: {
81
+ siteId: string
82
+ }
83
+ dynamodb?: DynamoDBAnalyticsConfig
84
+ }
85
+ }
86
+
87
+ // ============================================================================
88
+ // Stacks Analytics Driver
89
+ // ============================================================================
90
+
91
+ /**
92
+ * DynamoDB Analytics Driver for Stacks
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * // config/analytics.ts
97
+ * export default {
98
+ * driver: 'dynamodb',
99
+ * drivers: {
100
+ * dynamodb: {
101
+ * tableName: 'analytics',
102
+ * siteId: 'my-site-id',
103
+ * region: 'us-east-1',
104
+ * privacy: {
105
+ * honorDnt: true,
106
+ * hashIps: true,
107
+ * },
108
+ * },
109
+ * },
110
+ * } satisfies StacksAnalyticsOptions
111
+ * ```
112
+ */
113
+ export class DynamoDBAnalyticsDriver {
114
+ private config: DynamoDBAnalyticsConfig
115
+ private api: AnalyticsAPI
116
+ private store: AnalyticsStore
117
+ private queryApi: AnalyticsQueryAPI
118
+ private pipeline: AggregationPipeline
119
+ private executeCommand: HandlerContext['executeCommand']
120
+
121
+ constructor(
122
+ config: DynamoDBAnalyticsConfig,
123
+ executeCommand: HandlerContext['executeCommand'],
124
+ ) {
125
+ this.config = {
126
+ region: 'us-east-1',
127
+ useTtl: true,
128
+ rawEventRetentionDays: 30,
129
+ hourlyRetentionDays: 90,
130
+ dailyRetentionYears: 2,
131
+ privacy: {
132
+ honorDnt: true,
133
+ hashIps: true,
134
+ collectGeolocation: false,
135
+ },
136
+ tracking: {
137
+ trackHashChanges: true,
138
+ trackOutboundLinks: true,
139
+ excludedPaths: [],
140
+ excludedIps: [],
141
+ },
142
+ ...config,
143
+ }
144
+
145
+ this.executeCommand = executeCommand
146
+
147
+ const apiConfig: AnalyticsAPIConfig = {
148
+ tableName: this.config.tableName,
149
+ useTtl: this.config.useTtl,
150
+ rawEventTtl: (this.config.rawEventRetentionDays || 30) * 24 * 60 * 60,
151
+ }
152
+
153
+ this.api = new AnalyticsAPI(apiConfig)
154
+ this.store = this.api.getStore()
155
+ this.queryApi = this.api.getQueryAPI()
156
+ this.pipeline = this.api.getPipeline()
157
+ }
158
+
159
+ /**
160
+ * Get the tracking script HTML to inject into pages
161
+ */
162
+ getTrackingScript(): string {
163
+ return generateTrackingScript({
164
+ siteId: this.config.siteId,
165
+ apiEndpoint: this.config.apiEndpoint || '/api/analytics',
166
+ honorDnt: this.config.privacy?.honorDnt,
167
+ trackHashChanges: this.config.tracking?.trackHashChanges,
168
+ trackOutboundLinks: this.config.tracking?.trackOutboundLinks,
169
+ })
170
+ }
171
+
172
+ /**
173
+ * Track a page view (server-side)
174
+ */
175
+ async trackPageView(options: {
176
+ path: string
177
+ title?: string
178
+ referrer?: string
179
+ visitorId: string
180
+ sessionId: string
181
+ userAgent?: string
182
+ ip?: string
183
+ }): Promise<void> {
184
+ const deviceInfo = AnalyticsStore.parseUserAgent(options.userAgent || '')
185
+ const referrerSource = AnalyticsStore.parseReferrerSource(options.referrer)
186
+ const timestamp = new Date()
187
+
188
+ const pageView = {
189
+ id: AnalyticsStore.generateId(),
190
+ siteId: this.config.siteId,
191
+ visitorId: options.visitorId,
192
+ sessionId: options.sessionId,
193
+ path: options.path,
194
+ hostname: '',
195
+ title: options.title,
196
+ referrer: options.referrer,
197
+ referrerSource,
198
+ deviceType: deviceInfo.deviceType,
199
+ browser: deviceInfo.browser,
200
+ browserVersion: deviceInfo.browserVersion,
201
+ os: deviceInfo.os,
202
+ osVersion: deviceInfo.osVersion,
203
+ isUnique: true,
204
+ isBounce: true,
205
+ timestamp,
206
+ }
207
+
208
+ const command = this.store.recordPageViewCommand(pageView)
209
+ await this.executeCommand(command)
210
+ }
211
+
212
+ /**
213
+ * Track a custom event (server-side)
214
+ */
215
+ async trackEvent(options: {
216
+ name: string
217
+ value?: number
218
+ properties?: Record<string, string | number | boolean>
219
+ path: string
220
+ visitorId: string
221
+ sessionId: string
222
+ }): Promise<void> {
223
+ const event = {
224
+ id: AnalyticsStore.generateId(),
225
+ siteId: this.config.siteId,
226
+ visitorId: options.visitorId,
227
+ sessionId: options.sessionId,
228
+ name: options.name,
229
+ value: options.value,
230
+ properties: options.properties,
231
+ path: options.path,
232
+ timestamp: new Date(),
233
+ }
234
+
235
+ const command = this.store.recordCustomEventCommand(event)
236
+ await this.executeCommand(command)
237
+ }
238
+
239
+ /**
240
+ * Get dashboard summary stats
241
+ */
242
+ async getDashboardStats(options?: {
243
+ startDate?: Date
244
+ endDate?: Date
245
+ includeComparison?: boolean
246
+ }): Promise<{
247
+ summary: ReturnType<typeof AnalyticsQueryAPI.processSummary>
248
+ timeSeries: ReturnType<typeof AnalyticsQueryAPI.processTimeSeries>
249
+ }> {
250
+ const startDate = options?.startDate || new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
251
+ const endDate = options?.endDate || new Date()
252
+
253
+ const queries = this.queryApi.generateDashboardQueries({
254
+ siteId: this.config.siteId,
255
+ dateRange: { start: startDate, end: endDate },
256
+ includeComparison: options?.includeComparison,
257
+ })
258
+
259
+ const [statsResult, previousResult] = await Promise.all([
260
+ this.executeCommand(queries.aggregatedStats),
261
+ queries.previousPeriodStats ? this.executeCommand(queries.previousPeriodStats) : null,
262
+ ])
263
+
264
+ const stats = (statsResult as { Items?: unknown[] })?.Items || []
265
+ const previousStats = previousResult
266
+ ? (previousResult as { Items?: unknown[] })?.Items || []
267
+ : undefined
268
+
269
+ return {
270
+ summary: AnalyticsQueryAPI.processSummary(
271
+ stats as Parameters<typeof AnalyticsQueryAPI.processSummary>[0],
272
+ previousStats as Parameters<typeof AnalyticsQueryAPI.processSummary>[1],
273
+ ),
274
+ timeSeries: AnalyticsQueryAPI.processTimeSeries(
275
+ stats as Parameters<typeof AnalyticsQueryAPI.processTimeSeries>[0],
276
+ ),
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Get realtime visitor count
282
+ */
283
+ async getRealtimeVisitors(minutes = 5): Promise<{
284
+ currentVisitors: number
285
+ pageViewsLastHour: number
286
+ topActivePages: Array<{ name: string, value: number, percentage: number }>
287
+ }> {
288
+ const command = this.store.getRealtimeStatsCommand(this.config.siteId, minutes)
289
+ const result = await this.executeCommand(command) as { Items?: unknown[] }
290
+
291
+ return AnalyticsQueryAPI.processRealtimeData(
292
+ (result.Items || []) as Parameters<typeof AnalyticsQueryAPI.processRealtimeData>[0],
293
+ )
294
+ }
295
+
296
+ /**
297
+ * Get top pages
298
+ */
299
+ async getTopPages(options?: {
300
+ startDate?: Date
301
+ endDate?: Date
302
+ limit?: number
303
+ }): Promise<unknown[]> {
304
+ const endDate = options?.endDate || new Date()
305
+ const dateRange = {
306
+ start: options?.startDate || new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
307
+ end: endDate,
308
+ }
309
+ const period = AnalyticsQueryAPI.determinePeriod(dateRange)
310
+ const periodStart = AnalyticsStore.getPeriodStart(endDate, period)
311
+
312
+ const command = this.store.getTopPagesCommand(
313
+ this.config.siteId,
314
+ period,
315
+ periodStart,
316
+ options?.limit || 10,
317
+ )
318
+
319
+ const result = await this.executeCommand(command) as { Items?: unknown[] }
320
+ return result.Items || []
321
+ }
322
+
323
+ /**
324
+ * Run aggregation for a time period
325
+ */
326
+ async runAggregation(period: 'hour' | 'day' | 'month'): Promise<{
327
+ success: boolean
328
+ config: ReturnType<typeof AggregationPipeline.createJobConfig>
329
+ }> {
330
+ const config = AggregationPipeline.createJobConfig(this.config.siteId, period)
331
+
332
+ // Note: In a real implementation, you would:
333
+ // 1. Query raw page views and sessions for the window
334
+ // 2. Run the aggregation job
335
+ // 3. Execute the generated commands
336
+
337
+ return {
338
+ success: true,
339
+ config,
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Get the underlying API instance for advanced usage
345
+ */
346
+ getAPI(): AnalyticsAPI {
347
+ return this.api
348
+ }
349
+
350
+ /**
351
+ * Get the underlying store for advanced queries
352
+ */
353
+ getStore(): AnalyticsStore {
354
+ return this.store
355
+ }
356
+ }
357
+
358
+ // ============================================================================
359
+ // Stacks Middleware
360
+ // ============================================================================
361
+
362
+ /**
363
+ * Create Stacks analytics middleware
364
+ * Injects tracking script into HTML responses
365
+ */
366
+ export function createAnalyticsMiddleware(driver: DynamoDBAnalyticsDriver): (ctx: {
367
+ request: Request
368
+ response?: Response
369
+ html?: string
370
+ }, next: () => Promise<void>) => Promise<void> {
371
+ const script = driver.getTrackingScript()
372
+
373
+ return async (ctx: {
374
+ request: Request
375
+ response?: Response
376
+ html?: string
377
+ }, next: () => Promise<void>): Promise<void> => {
378
+ await next()
379
+
380
+ // Inject script into HTML responses
381
+ if (ctx.html && ctx.html.includes('</head>')) {
382
+ ctx.html = ctx.html.replace('</head>', `${script}\n</head>`)
383
+ }
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Create server-side tracking middleware
389
+ * Tracks page views automatically for server-rendered pages
390
+ */
391
+ export function createServerTrackingMiddleware(
392
+ driver: DynamoDBAnalyticsDriver,
393
+ options?: {
394
+ excludedPaths?: RegExp[]
395
+ getVisitorId?: (request: Request) => string
396
+ getSessionId?: (request: Request) => string
397
+ },
398
+ ): (ctx: { request: Request, url: URL }, next: () => Promise<void>) => Promise<void> {
399
+ return async (ctx: {
400
+ request: Request
401
+ url: URL
402
+ }, next: () => Promise<void>): Promise<void> => {
403
+ const { request, url } = ctx
404
+
405
+ // Check excluded paths
406
+ if (options?.excludedPaths?.some(pattern => pattern.test(url.pathname))) {
407
+ return next()
408
+ }
409
+
410
+ // Get or generate visitor/session IDs
411
+ const visitorId = options?.getVisitorId?.(request)
412
+ || request.headers.get('x-visitor-id')
413
+ || AnalyticsStore.generateId()
414
+
415
+ const sessionId = options?.getSessionId?.(request)
416
+ || request.headers.get('x-session-id')
417
+ || AnalyticsStore.generateId()
418
+
419
+ // Track page view asynchronously (don't block response)
420
+ driver.trackPageView({
421
+ path: url.pathname,
422
+ referrer: request.headers.get('referer') || undefined,
423
+ visitorId,
424
+ sessionId,
425
+ userAgent: request.headers.get('user-agent') || undefined,
426
+ ip: request.headers.get('x-forwarded-for') || undefined,
427
+ }).catch(err => console.error('Failed to track page view:', err))
428
+
429
+ return next()
430
+ }
431
+ }
432
+
433
+ // ============================================================================
434
+ // Stacks Actions (Dashboard API)
435
+ // ============================================================================
436
+
437
+ /**
438
+ * Create Stacks dashboard actions for analytics
439
+ */
440
+ export function createDashboardActions(driver: DynamoDBAnalyticsDriver): {
441
+ getDashboardStats: (params: { startDate?: string, endDate?: string, compare?: boolean }) => Promise<unknown>
442
+ getRealtimeStats: (params?: { minutes?: number }) => Promise<unknown>
443
+ getTopPages: (params?: { startDate?: string, endDate?: string, limit?: number }) => Promise<unknown>
444
+ runAggregation: (params: { period: 'hour' | 'day' | 'month' }) => Promise<unknown>
445
+ } {
446
+ return {
447
+ /**
448
+ * Get dashboard stats action
449
+ */
450
+ async getDashboardStats(params: {
451
+ startDate?: string
452
+ endDate?: string
453
+ compare?: boolean
454
+ }): Promise<unknown> {
455
+ return driver.getDashboardStats({
456
+ startDate: params.startDate ? new Date(params.startDate) : undefined,
457
+ endDate: params.endDate ? new Date(params.endDate) : undefined,
458
+ includeComparison: params.compare,
459
+ })
460
+ },
461
+
462
+ /**
463
+ * Get realtime stats action
464
+ */
465
+ async getRealtimeStats(params?: { minutes?: number }): Promise<unknown> {
466
+ return driver.getRealtimeVisitors(params?.minutes)
467
+ },
468
+
469
+ /**
470
+ * Get top pages action
471
+ */
472
+ async getTopPages(params?: {
473
+ startDate?: string
474
+ endDate?: string
475
+ limit?: number
476
+ }): Promise<unknown> {
477
+ return driver.getTopPages({
478
+ startDate: params?.startDate ? new Date(params.startDate) : undefined,
479
+ endDate: params?.endDate ? new Date(params.endDate) : undefined,
480
+ limit: params?.limit,
481
+ })
482
+ },
483
+
484
+ /**
485
+ * Trigger aggregation action
486
+ */
487
+ async runAggregation(params: { period: 'hour' | 'day' | 'month' }): Promise<unknown> {
488
+ return driver.runAggregation(params.period)
489
+ },
490
+ }
491
+ }
492
+
493
+ // ============================================================================
494
+ // Factory Function
495
+ // ============================================================================
496
+
497
+ /**
498
+ * Create a DynamoDB analytics driver from Stacks config
499
+ *
500
+ * @example
501
+ * ```typescript
502
+ * import { createAnalyticsDriver } from '@stacksjs/dynamodb-tooling'
503
+ * import config from './config/analytics'
504
+ * import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
505
+ *
506
+ * const client = new DynamoDBClient({ region: 'us-east-1' })
507
+ * const driver = createAnalyticsDriver(config.drivers.dynamodb!, async (cmd) => {
508
+ * // Execute DynamoDB command
509
+ * return client.send(new cmd.command(cmd.input))
510
+ * })
511
+ * ```
512
+ */
513
+ export function createAnalyticsDriver(
514
+ config: DynamoDBAnalyticsConfig,
515
+ executeCommand: HandlerContext['executeCommand'],
516
+ ): DynamoDBAnalyticsDriver {
517
+ return new DynamoDBAnalyticsDriver(config, executeCommand)
518
+ }
519
+
520
+ // ============================================================================
521
+ // Type Exports for Stacks Integration
522
+ // ============================================================================
523
+
524
+ export type {
525
+ AnalyticsAPIConfig,
526
+ HandlerContext,
527
+ }