@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,452 @@
1
+ /**
2
+ * Event Batching Utility
3
+ *
4
+ * High-performance event batching for handling large volumes of analytics events.
5
+ * Uses DynamoDB BatchWriteItem for efficient bulk writes.
6
+ */
7
+
8
+ import type { CustomEvent, PageView, Session } from './types'
9
+ import { getConfig } from './config'
10
+ import { generateId, KeyPatterns } from './dynamodb'
11
+
12
+ // ============================================================================
13
+ // Types
14
+ // ============================================================================
15
+
16
+ export interface BatchItem {
17
+ type: 'pageview' | 'session' | 'event'
18
+ data: PageView | Session | CustomEvent
19
+ }
20
+
21
+ export interface BatchWriteResult {
22
+ successful: number
23
+ failed: number
24
+ unprocessedItems: BatchItem[]
25
+ }
26
+
27
+ export interface BatchQueueOptions {
28
+ /** Maximum items per batch (DynamoDB limit is 25) */
29
+ maxBatchSize?: number
30
+ /** Flush interval in milliseconds */
31
+ flushIntervalMs?: number
32
+ /** Maximum queue size before auto-flush */
33
+ maxQueueSize?: number
34
+ /** Callback for batch write errors */
35
+ onError?: (error: Error, items: BatchItem[]) => void
36
+ /** Callback when batch is flushed */
37
+ onFlush?: (result: BatchWriteResult) => void
38
+ }
39
+
40
+ export interface DynamoDBBatchClient {
41
+ batchWriteItem: (input: BatchWriteInput) => Promise<BatchWriteOutput>
42
+ }
43
+
44
+ interface BatchWriteInput {
45
+ RequestItems: Record<string, WriteRequest[]>
46
+ }
47
+
48
+ interface BatchWriteOutput {
49
+ UnprocessedItems?: Record<string, WriteRequest[]>
50
+ }
51
+
52
+ interface WriteRequest {
53
+ PutRequest?: {
54
+ Item: Record<string, AttributeValue>
55
+ }
56
+ }
57
+
58
+ interface AttributeValue {
59
+ S?: string
60
+ N?: string
61
+ BOOL?: boolean
62
+ L?: AttributeValue[]
63
+ M?: Record<string, AttributeValue>
64
+ }
65
+
66
+ // ============================================================================
67
+ // Event Batch Queue
68
+ // ============================================================================
69
+
70
+ /**
71
+ * High-performance event batch queue
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * const queue = new EventBatchQueue(dynamoClient, {
76
+ * maxBatchSize: 25,
77
+ * flushIntervalMs: 5000,
78
+ * onFlush: (result) => console.log(`Flushed ${result.successful} items`),
79
+ * })
80
+ *
81
+ * // Add events (auto-batched)
82
+ * queue.addPageView(pageView)
83
+ * queue.addSession(session)
84
+ *
85
+ * // Manual flush if needed
86
+ * await queue.flush()
87
+ *
88
+ * // Shutdown gracefully
89
+ * await queue.close()
90
+ * ```
91
+ */
92
+ export class EventBatchQueue {
93
+ private queue: BatchItem[] = []
94
+ private flushTimer: ReturnType<typeof setInterval> | null = null
95
+ private isClosing = false
96
+ private flushPromise: Promise<void> | null = null
97
+
98
+ private readonly client: DynamoDBBatchClient
99
+ private readonly tableName: string
100
+ private readonly maxBatchSize: number
101
+ private readonly flushIntervalMs: number
102
+ private readonly maxQueueSize: number
103
+ private readonly onError?: (error: Error, items: BatchItem[]) => void
104
+ private readonly onFlush?: (result: BatchWriteResult) => void
105
+
106
+ constructor(client: DynamoDBBatchClient, options: BatchQueueOptions = {}) {
107
+ this.client = client
108
+ this.tableName = getConfig().table.tableName
109
+ this.maxBatchSize = Math.min(options.maxBatchSize ?? 25, 25) // DynamoDB limit
110
+ this.flushIntervalMs = options.flushIntervalMs ?? 5000
111
+ this.maxQueueSize = options.maxQueueSize ?? 100
112
+ this.onError = options.onError
113
+ this.onFlush = options.onFlush
114
+
115
+ // Start periodic flush
116
+ this.startFlushTimer()
117
+ }
118
+
119
+ /**
120
+ * Add a page view to the queue
121
+ */
122
+ addPageView(pageView: PageView): void {
123
+ this.add({ type: 'pageview', data: pageView })
124
+ }
125
+
126
+ /**
127
+ * Add a session to the queue
128
+ */
129
+ addSession(session: Session): void {
130
+ this.add({ type: 'session', data: session })
131
+ }
132
+
133
+ /**
134
+ * Add a custom event to the queue
135
+ */
136
+ addEvent(event: CustomEvent): void {
137
+ this.add({ type: 'event', data: event })
138
+ }
139
+
140
+ /**
141
+ * Add a batch item to the queue
142
+ */
143
+ add(item: BatchItem): void {
144
+ if (this.isClosing) {
145
+ throw new Error('Cannot add items to a closing queue')
146
+ }
147
+
148
+ this.queue.push(item)
149
+
150
+ // Auto-flush if queue is full
151
+ if (this.queue.length >= this.maxQueueSize) {
152
+ this.flush()
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Flush all queued items to DynamoDB
158
+ */
159
+ async flush(): Promise<BatchWriteResult> {
160
+ // Wait for any in-progress flush
161
+ if (this.flushPromise) {
162
+ await this.flushPromise
163
+ }
164
+
165
+ if (this.queue.length === 0) {
166
+ return { successful: 0, failed: 0, unprocessedItems: [] }
167
+ }
168
+
169
+ const itemsToFlush = this.queue.splice(0, this.queue.length)
170
+ const totalResult: BatchWriteResult = { successful: 0, failed: 0, unprocessedItems: [] }
171
+
172
+ this.flushPromise = (async () => {
173
+ // Process in batches of maxBatchSize
174
+ for (let i = 0; i < itemsToFlush.length; i += this.maxBatchSize) {
175
+ const batch = itemsToFlush.slice(i, i + this.maxBatchSize)
176
+ const result = await this.writeBatch(batch)
177
+
178
+ totalResult.successful += result.successful
179
+ totalResult.failed += result.failed
180
+ totalResult.unprocessedItems.push(...result.unprocessedItems)
181
+ }
182
+
183
+ this.onFlush?.(totalResult)
184
+ })()
185
+
186
+ await this.flushPromise
187
+ this.flushPromise = null
188
+
189
+ return totalResult
190
+ }
191
+
192
+ /**
193
+ * Close the queue and flush remaining items
194
+ */
195
+ async close(): Promise<void> {
196
+ this.isClosing = true
197
+ this.stopFlushTimer()
198
+ await this.flush()
199
+ }
200
+
201
+ /**
202
+ * Get current queue size
203
+ */
204
+ get size(): number {
205
+ return this.queue.length
206
+ }
207
+
208
+ private startFlushTimer(): void {
209
+ this.flushTimer = setInterval(() => {
210
+ if (this.queue.length > 0) {
211
+ this.flush()
212
+ }
213
+ }, this.flushIntervalMs)
214
+ }
215
+
216
+ private stopFlushTimer(): void {
217
+ if (this.flushTimer) {
218
+ clearInterval(this.flushTimer)
219
+ this.flushTimer = null
220
+ }
221
+ }
222
+
223
+ private async writeBatch(items: BatchItem[]): Promise<BatchWriteResult> {
224
+ const writeRequests = items.map(item => this.toWriteRequest(item))
225
+
226
+ try {
227
+ const result = await this.client.batchWriteItem({
228
+ RequestItems: {
229
+ [this.tableName]: writeRequests,
230
+ },
231
+ })
232
+
233
+ const unprocessedCount = result.UnprocessedItems?.[this.tableName]?.length ?? 0
234
+ const successCount = items.length - unprocessedCount
235
+
236
+ // Convert unprocessed items back to BatchItem format
237
+ const unprocessedItems: BatchItem[] = []
238
+ if (unprocessedCount > 0) {
239
+ // Note: DynamoDB doesn't preserve order, so we return all as failed
240
+ unprocessedItems.push(...items.slice(-unprocessedCount))
241
+ }
242
+
243
+ return {
244
+ successful: successCount,
245
+ failed: unprocessedCount,
246
+ unprocessedItems,
247
+ }
248
+ }
249
+ catch (error) {
250
+ this.onError?.(error as Error, items)
251
+ return {
252
+ successful: 0,
253
+ failed: items.length,
254
+ unprocessedItems: items,
255
+ }
256
+ }
257
+ }
258
+
259
+ private toWriteRequest(item: BatchItem): WriteRequest {
260
+ switch (item.type) {
261
+ case 'pageview':
262
+ return { PutRequest: { Item: this.marshalPageView(item.data as PageView) } }
263
+ case 'session':
264
+ return { PutRequest: { Item: this.marshalSession(item.data as Session) } }
265
+ case 'event':
266
+ return { PutRequest: { Item: this.marshalCustomEvent(item.data as CustomEvent) } }
267
+ }
268
+ }
269
+
270
+ private marshalPageView(pv: PageView): Record<string, AttributeValue> {
271
+ return {
272
+ pk: { S: KeyPatterns.pageView.pk(pv.siteId) },
273
+ sk: { S: KeyPatterns.pageView.sk(pv.timestamp, pv.id) },
274
+ id: { S: pv.id },
275
+ siteId: { S: pv.siteId },
276
+ visitorId: { S: pv.visitorId },
277
+ sessionId: { S: pv.sessionId },
278
+ path: { S: pv.path },
279
+ hostname: { S: pv.hostname },
280
+ ...(pv.title && { title: { S: pv.title } }),
281
+ ...(pv.referrer && { referrer: { S: pv.referrer } }),
282
+ ...(pv.referrerSource && { referrerSource: { S: pv.referrerSource } }),
283
+ ...(pv.deviceType && { deviceType: { S: pv.deviceType } }),
284
+ ...(pv.browser && { browser: { S: pv.browser } }),
285
+ ...(pv.os && { os: { S: pv.os } }),
286
+ isUnique: { BOOL: pv.isUnique },
287
+ isBounce: { BOOL: pv.isBounce },
288
+ timestamp: { S: pv.timestamp.toISOString() },
289
+ entityType: { S: 'pageview' },
290
+ }
291
+ }
292
+
293
+ private marshalSession(s: Session): Record<string, AttributeValue> {
294
+ return {
295
+ pk: { S: KeyPatterns.session.pk(s.siteId) },
296
+ sk: { S: KeyPatterns.session.sk(s.id) },
297
+ id: { S: s.id },
298
+ siteId: { S: s.siteId },
299
+ visitorId: { S: s.visitorId },
300
+ entryPath: { S: s.entryPath },
301
+ exitPath: { S: s.exitPath },
302
+ ...(s.referrer && { referrer: { S: s.referrer } }),
303
+ ...(s.referrerSource && { referrerSource: { S: s.referrerSource } }),
304
+ ...(s.deviceType && { deviceType: { S: s.deviceType } }),
305
+ ...(s.browser && { browser: { S: s.browser } }),
306
+ ...(s.os && { os: { S: s.os } }),
307
+ pageViewCount: { N: String(s.pageViewCount) },
308
+ eventCount: { N: String(s.eventCount) },
309
+ isBounce: { BOOL: s.isBounce },
310
+ duration: { N: String(s.duration) },
311
+ startedAt: { S: s.startedAt.toISOString() },
312
+ endedAt: { S: s.endedAt.toISOString() },
313
+ entityType: { S: 'session' },
314
+ }
315
+ }
316
+
317
+ private marshalCustomEvent(e: CustomEvent): Record<string, AttributeValue> {
318
+ return {
319
+ pk: { S: KeyPatterns.event.pk(e.siteId) },
320
+ sk: { S: KeyPatterns.event.sk(e.timestamp, e.id) },
321
+ id: { S: e.id },
322
+ siteId: { S: e.siteId },
323
+ visitorId: { S: e.visitorId },
324
+ sessionId: { S: e.sessionId },
325
+ name: { S: e.name },
326
+ ...(e.category && { category: { S: e.category } }),
327
+ ...(e.value !== undefined && { value: { N: String(e.value) } }),
328
+ ...(e.properties && { properties: { S: JSON.stringify(e.properties) } }),
329
+ path: { S: e.path },
330
+ timestamp: { S: e.timestamp.toISOString() },
331
+ entityType: { S: 'event' },
332
+ }
333
+ }
334
+ }
335
+
336
+ // ============================================================================
337
+ // Batch Processing Utilities
338
+ // ============================================================================
339
+
340
+ /**
341
+ * Create a batch processor for processing events in bulk
342
+ */
343
+ export function createBatchProcessor<T>(
344
+ processor: (batch: T[]) => Promise<void>,
345
+ options: { batchSize?: number, delayMs?: number } = {},
346
+ ): {
347
+ add: (item: T) => void
348
+ flush: () => Promise<void>
349
+ size: () => number
350
+ } {
351
+ const batchSize = options.batchSize ?? 100
352
+ const delayMs = options.delayMs ?? 0
353
+ let queue: T[] = []
354
+ let processing = false
355
+
356
+ async function processQueue(): Promise<void> {
357
+ if (processing || queue.length === 0) return
358
+ processing = true
359
+
360
+ while (queue.length > 0) {
361
+ const batch = queue.splice(0, batchSize)
362
+
363
+ if (delayMs > 0) {
364
+ await new Promise(resolve => setTimeout(resolve, delayMs))
365
+ }
366
+
367
+ await processor(batch)
368
+ }
369
+
370
+ processing = false
371
+ }
372
+
373
+ return {
374
+ add(item: T): void {
375
+ queue.push(item)
376
+ if (queue.length >= batchSize) {
377
+ processQueue()
378
+ }
379
+ },
380
+ async flush(): Promise<void> {
381
+ await processQueue()
382
+ },
383
+ size(): number {
384
+ return queue.length
385
+ },
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Retry helper with exponential backoff
391
+ */
392
+ export async function withRetry<T>(
393
+ fn: () => Promise<T>,
394
+ options: { maxRetries?: number, baseDelayMs?: number } = {},
395
+ ): Promise<T> {
396
+ const maxRetries = options.maxRetries ?? 3
397
+ const baseDelayMs = options.baseDelayMs ?? 100
398
+ let lastError: Error | undefined
399
+
400
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
401
+ try {
402
+ return await fn()
403
+ }
404
+ catch (error) {
405
+ lastError = error as Error
406
+
407
+ if (attempt < maxRetries - 1) {
408
+ const delay = baseDelayMs * 2 ** attempt
409
+ await new Promise(resolve => setTimeout(resolve, delay))
410
+ }
411
+ }
412
+ }
413
+
414
+ throw lastError
415
+ }
416
+
417
+ /**
418
+ * Chunk an array into smaller arrays
419
+ */
420
+ export function chunk<T>(array: T[], size: number): T[][] {
421
+ const chunks: T[][] = []
422
+ for (let i = 0; i < array.length; i += size) {
423
+ chunks.push(array.slice(i, i + size))
424
+ }
425
+ return chunks
426
+ }
427
+
428
+ /**
429
+ * Process items in parallel with concurrency limit
430
+ */
431
+ export async function parallelProcess<T, R>(
432
+ items: T[],
433
+ processor: (item: T) => Promise<R>,
434
+ concurrency: number = 5,
435
+ ): Promise<R[]> {
436
+ const results: R[] = []
437
+ const queue = [...items]
438
+
439
+ async function worker(): Promise<void> {
440
+ while (queue.length > 0) {
441
+ const item = queue.shift()
442
+ if (item !== undefined) {
443
+ results.push(await processor(item))
444
+ }
445
+ }
446
+ }
447
+
448
+ const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker())
449
+ await Promise.all(workers)
450
+
451
+ return results
452
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Dashboard Components Index
3
+ *
4
+ * Re-exports all dashboard components for easy importing.
5
+ */
6
+
7
+ export * from '../../../components/dashboard/SiteSelector.stx'
8
+ export * from '../../../components/dashboard/DashboardHeader.stx'
9
+ export * from '../../../components/dashboard/ControlsBar.stx'
10
+ export * from '../../../components/dashboard/FiltersBar.stx'
11
+ export * from '../../../components/dashboard/StatsRow.stx'
12
+ export * from '../../../components/dashboard/ChartSection.stx'
13
+ export * from '../../../components/dashboard/DataPanel.stx'
14
+ export * from '../../../components/dashboard/NoDataMessage.stx'
15
+ export * from '../../../components/dashboard/EventsSection.stx'
16
+ export * from '../../../components/dashboard/GoalsSection.stx'
17
+ export * from '../../../components/dashboard/GoalModal.stx'
18
+ export * from '../../../components/dashboard/DashboardStyles.stx'