@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,344 @@
1
+ /**
2
+ * DynamoDB Utilities for Analytics
3
+ *
4
+ * Integrates with dynamodb-tooling for DynamoDB operations.
5
+ * Provides analytics-specific helpers for single-table design.
6
+ */
7
+
8
+ import type { AnalyticsConfig } from './config'
9
+ import { getConfig } from './config'
10
+ import { randomToken } from './lib/crypto-random'
11
+ import type { AggregationPeriod, AnalyticsEntityType, AnalyticsKeyPatterns } from './types'
12
+
13
+ // Re-export dynamodb-tooling utilities when available
14
+ // Note: The npm package (0.3.2) has limited exports. When updated,
15
+ // uncomment and use the full utilities:
16
+ // export { config as dynamodbConfig } from 'dynamodb-tooling'
17
+
18
+ // ============================================================================
19
+ // Key Builders
20
+ // ============================================================================
21
+
22
+ /**
23
+ * Build a partition key for an analytics entity
24
+ */
25
+ export function buildPK(entityType: AnalyticsEntityType, id: string): string {
26
+ return `${entityType}#${id}`
27
+ }
28
+
29
+ /**
30
+ * Build a sort key for an analytics entity
31
+ */
32
+ export function buildSK(entityType: AnalyticsEntityType, ...parts: string[]): string {
33
+ return [entityType, ...parts].join('#')
34
+ }
35
+
36
+ /**
37
+ * Build a composite key with timestamp
38
+ */
39
+ export function buildTimestampKey(
40
+ prefix: string,
41
+ timestamp: Date,
42
+ id?: string,
43
+ ): string {
44
+ const ts = timestamp.toISOString()
45
+ return id ? `${prefix}#${ts}#${id}` : `${prefix}#${ts}`
46
+ }
47
+
48
+ /**
49
+ * Build GSI1 partition key
50
+ */
51
+ export function buildGSI1PK(siteId: string, dimension: string, value: string): string {
52
+ return `SITE#${siteId}#${dimension}#${value}`
53
+ }
54
+
55
+ /**
56
+ * Build period-based sort key for aggregates
57
+ */
58
+ export function buildPeriodSK(
59
+ prefix: string,
60
+ period: AggregationPeriod,
61
+ periodStart: string,
62
+ ...additionalParts: string[]
63
+ ): string {
64
+ const parts = [prefix, period.toUpperCase(), periodStart, ...additionalParts]
65
+ return parts.join('#')
66
+ }
67
+
68
+ // ============================================================================
69
+ // Key Patterns (matching Analytics.ts patterns)
70
+ // ============================================================================
71
+
72
+ /**
73
+ * Analytics key patterns for single-table design
74
+ */
75
+ export const KeyPatterns = {
76
+ site: {
77
+ pk: (siteId: string): string => `SITE#${siteId}`,
78
+ sk: (siteId: string): string => `SITE#${siteId}`,
79
+ gsi1pk: (ownerId: string): string => `OWNER#${ownerId}`,
80
+ gsi1sk: (siteId: string): string => `SITE#${siteId}`,
81
+ },
82
+
83
+ pageView: {
84
+ pk: (siteId: string): string => `SITE#${siteId}`,
85
+ sk: (timestamp: Date, id: string): string => `PV#${timestamp.toISOString()}#${id}`,
86
+ gsi1pk: (siteId: string, date: string): string => `SITE#${siteId}#DATE#${date}`,
87
+ gsi1sk: (path: string, id: string): string => `PATH#${path}#${id}`,
88
+ },
89
+
90
+ session: {
91
+ pk: (siteId: string): string => `SITE#${siteId}`,
92
+ sk: (sessionId: string): string => `SESSION#${sessionId}`,
93
+ },
94
+
95
+ event: {
96
+ pk: (siteId: string): string => `SITE#${siteId}`,
97
+ sk: (timestamp: Date, id: string): string => `EVENT#${timestamp.toISOString()}#${id}`,
98
+ },
99
+
100
+ goal: {
101
+ pk: (siteId: string): string => `SITE#${siteId}`,
102
+ sk: (goalId: string): string => `GOAL#${goalId}`,
103
+ },
104
+
105
+ stats: {
106
+ pk: (siteId: string): string => `SITE#${siteId}`,
107
+ sk: (period: AggregationPeriod, periodStart: string): string =>
108
+ `STATS#${period.toUpperCase()}#${periodStart}`,
109
+ },
110
+
111
+ pageStats: {
112
+ pk: (siteId: string): string => `SITE#${siteId}`,
113
+ sk: (period: AggregationPeriod, periodStart: string, path: string): string =>
114
+ `PAGESTATS#${period.toUpperCase()}#${periodStart}#${encodeURIComponent(path)}`,
115
+ },
116
+
117
+ realtime: {
118
+ pk: (siteId: string): string => `SITE#${siteId}`,
119
+ sk: (minute: string): string => `REALTIME#${minute}`,
120
+ },
121
+ } as const
122
+
123
+ // ============================================================================
124
+ // DynamoDB Item Helpers
125
+ // ============================================================================
126
+
127
+ /**
128
+ * Convert a JavaScript value to DynamoDB AttributeValue
129
+ */
130
+ export function toDynamoValue(value: unknown): Record<string, unknown> {
131
+ if (value === null || value === undefined) {
132
+ return { NULL: true }
133
+ }
134
+ if (typeof value === 'string') {
135
+ return { S: value }
136
+ }
137
+ if (typeof value === 'number') {
138
+ return { N: String(value) }
139
+ }
140
+ if (typeof value === 'boolean') {
141
+ return { BOOL: value }
142
+ }
143
+ if (value instanceof Date) {
144
+ return { S: value.toISOString() }
145
+ }
146
+ if (Array.isArray(value)) {
147
+ return { L: value.map(toDynamoValue) }
148
+ }
149
+ if (typeof value === 'object') {
150
+ const map: Record<string, unknown> = {}
151
+ for (const [k, v] of Object.entries(value)) {
152
+ map[k] = toDynamoValue(v)
153
+ }
154
+ return { M: map }
155
+ }
156
+ return { S: String(value) }
157
+ }
158
+
159
+ /**
160
+ * Convert DynamoDB AttributeValue to JavaScript value
161
+ */
162
+ export function fromDynamoValue(attr: Record<string, unknown>): unknown {
163
+ if ('S' in attr) return attr.S
164
+ if ('N' in attr) return Number(attr.N)
165
+ if ('BOOL' in attr) return attr.BOOL
166
+ if ('NULL' in attr) return null
167
+ if ('L' in attr) return (attr.L as Record<string, unknown>[]).map(fromDynamoValue)
168
+ if ('M' in attr) {
169
+ const result: Record<string, unknown> = {}
170
+ for (const [k, v] of Object.entries(attr.M as Record<string, unknown>)) {
171
+ result[k] = fromDynamoValue(v as Record<string, unknown>)
172
+ }
173
+ return result
174
+ }
175
+ if ('SS' in attr) return attr.SS
176
+ if ('NS' in attr) return (attr.NS as string[]).map(Number)
177
+ return attr
178
+ }
179
+
180
+ /**
181
+ * Marshal a JavaScript object to DynamoDB Item
182
+ */
183
+ export function marshal(obj: Record<string, unknown>): Record<string, Record<string, unknown>> {
184
+ const item: Record<string, Record<string, unknown>> = {}
185
+ for (const [key, value] of Object.entries(obj)) {
186
+ if (value !== undefined) {
187
+ item[key] = toDynamoValue(value)
188
+ }
189
+ }
190
+ return item
191
+ }
192
+
193
+ /**
194
+ * Unmarshal a DynamoDB Item to JavaScript object
195
+ */
196
+ export function unmarshal(item: Record<string, Record<string, unknown>>): Record<string, unknown> {
197
+ const result: Record<string, unknown> = {}
198
+ for (const [key, value] of Object.entries(item)) {
199
+ result[key] = fromDynamoValue(value)
200
+ }
201
+ return result
202
+ }
203
+
204
+ // ============================================================================
205
+ // Query Helpers
206
+ // ============================================================================
207
+
208
+ /**
209
+ * Build a query for items by site and sort key prefix
210
+ */
211
+ export function buildSiteQuery(
212
+ siteId: string,
213
+ skPrefix: string,
214
+ config?: AnalyticsConfig,
215
+ ): {
216
+ TableName: string
217
+ KeyConditionExpression: string
218
+ ExpressionAttributeValues: Record<string, Record<string, string>>
219
+ } {
220
+ const cfg = config ?? getConfig()
221
+
222
+ return {
223
+ TableName: cfg.table.tableName,
224
+ KeyConditionExpression: 'pk = :pk AND begins_with(sk, :skPrefix)',
225
+ ExpressionAttributeValues: {
226
+ ':pk': { S: KeyPatterns.site.pk(siteId) },
227
+ ':skPrefix': { S: skPrefix },
228
+ },
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Build a query for items in a time range
234
+ */
235
+ export function buildTimeRangeQuery(
236
+ siteId: string,
237
+ skPrefix: string,
238
+ startTime: Date,
239
+ endTime: Date,
240
+ config?: AnalyticsConfig,
241
+ ): {
242
+ TableName: string
243
+ KeyConditionExpression: string
244
+ ExpressionAttributeValues: Record<string, Record<string, string>>
245
+ } {
246
+ const cfg = config ?? getConfig()
247
+
248
+ return {
249
+ TableName: cfg.table.tableName,
250
+ KeyConditionExpression: 'pk = :pk AND sk BETWEEN :skStart AND :skEnd',
251
+ ExpressionAttributeValues: {
252
+ ':pk': { S: KeyPatterns.site.pk(siteId) },
253
+ ':skStart': { S: `${skPrefix}#${startTime.toISOString()}` },
254
+ ':skEnd': { S: `${skPrefix}#${endTime.toISOString()}` },
255
+ },
256
+ }
257
+ }
258
+
259
+ // ============================================================================
260
+ // ID Generation
261
+ // ============================================================================
262
+
263
+ /**
264
+ * Generate a unique ID (ULID-like)
265
+ */
266
+ export function generateId(): string {
267
+ const timestamp = Date.now().toString(36)
268
+ // CSPRNG suffix so ids aren't predictable (#130); still time-prefixed + unique.
269
+ const random = randomToken(8, '0123456789abcdefghijklmnopqrstuvwxyz')
270
+ return `${timestamp}${random}`
271
+ }
272
+
273
+ /**
274
+ * Generate a session ID
275
+ */
276
+ export function generateSessionId(): string {
277
+ return `sess_${generateId()}`
278
+ }
279
+
280
+ /**
281
+ * Hash a visitor ID for privacy
282
+ */
283
+ export async function hashVisitorId(
284
+ ip: string,
285
+ userAgent: string,
286
+ siteId: string,
287
+ salt: string,
288
+ ): Promise<string> {
289
+ const data = `${ip}|${userAgent}|${siteId}|${salt}`
290
+
291
+ // Use Web Crypto API
292
+ const encoder = new TextEncoder()
293
+ const dataBuffer = encoder.encode(data)
294
+ const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer)
295
+ const hashArray = Array.from(new Uint8Array(hashBuffer))
296
+ const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
297
+
298
+ return hashHex.substring(0, 16) // Return first 16 chars
299
+ }
300
+
301
+ // ============================================================================
302
+ // Period Utilities
303
+ // ============================================================================
304
+
305
+ /**
306
+ * Get the start of a period
307
+ */
308
+ export function getPeriodStart(date: Date, period: AggregationPeriod): string {
309
+ const d = new Date(date)
310
+
311
+ switch (period) {
312
+ case 'hour':
313
+ d.setMinutes(0, 0, 0)
314
+ return d.toISOString().slice(0, 13) // YYYY-MM-DDTHH
315
+ case 'day':
316
+ d.setHours(0, 0, 0, 0)
317
+ return d.toISOString().slice(0, 10) // YYYY-MM-DD
318
+ case 'month':
319
+ d.setDate(1)
320
+ d.setHours(0, 0, 0, 0)
321
+ return d.toISOString().slice(0, 7) // YYYY-MM
322
+ default:
323
+ return d.toISOString().slice(0, 10)
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Get the appropriate period for a date range
329
+ */
330
+ export function determinePeriod(startDate: Date, endDate: Date): AggregationPeriod {
331
+ const diffMs = endDate.getTime() - startDate.getTime()
332
+ const diffDays = diffMs / (1000 * 60 * 60 * 24)
333
+
334
+ if (diffDays <= 2) return 'hour'
335
+ if (diffDays <= 90) return 'day'
336
+ return 'month'
337
+ }
338
+
339
+ /**
340
+ * Get daily salt for visitor ID hashing (rotates daily for privacy).
341
+ * Secret-seeded so visitor hashes cannot be reproduced without the server
342
+ * secret (#88) — see src/lib/salt.ts.
343
+ */
344
+ export { getDailySalt, getSharedDailySalt } from './lib/salt'