@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,66 @@
1
+ /**
2
+ * A/B test statistical significance (two-proportion z-test vs the control).
3
+ *
4
+ * The first variant is treated as the control. Each other variant gets a
5
+ * conversion rate, uplift, two-tailed p-value, confidence, and a significance
6
+ * flag (p < 0.05) so the dashboard can call a winner instead of showing raw
7
+ * counts.
8
+ */
9
+
10
+ /** Error function approximation (Abramowitz & Stegun 7.1.26). */
11
+ function erf(x: number): number {
12
+ const t = 1 / (1 + 0.3275911 * Math.abs(x))
13
+ const y = 1 - (((((1.061405429 * t - 1.453152027) * t) + 1.421413741) * t - 0.284496736) * t + 0.254829592) * t * Math.exp(-x * x)
14
+ return x >= 0 ? y : -y
15
+ }
16
+
17
+ /** Standard normal CDF. */
18
+ function normalCdf(z: number): number {
19
+ return 0.5 * (1 + erf(z / Math.SQRT2))
20
+ }
21
+
22
+ export interface VariantStat {
23
+ visitors: number
24
+ conversions: number
25
+ conversionRate: number
26
+ upliftPct: number | null
27
+ pValue: number | null
28
+ confidence: number | null
29
+ significant: boolean
30
+ isControl: boolean
31
+ }
32
+
33
+ export function analyzeVariants(variants: { visitors?: number; conversions?: number }[]): VariantStat[] {
34
+ if (!variants.length) return []
35
+
36
+ const control = variants[0]
37
+ const n1 = control.visitors || 0
38
+ const c1 = control.conversions || 0
39
+ const p1 = n1 > 0 ? c1 / n1 : 0
40
+
41
+ return variants.map((v, i) => {
42
+ const n2 = v.visitors || 0
43
+ const c2 = v.conversions || 0
44
+ const p2 = n2 > 0 ? c2 / n2 : 0
45
+ const isControl = i === 0
46
+
47
+ let upliftPct: number | null = null
48
+ let pValue: number | null = null
49
+ let confidence: number | null = null
50
+ let significant = false
51
+
52
+ if (!isControl && n1 > 0 && n2 > 0) {
53
+ upliftPct = p1 > 0 ? ((p2 - p1) / p1) * 100 : null
54
+ const pooled = (c1 + c2) / (n1 + n2)
55
+ const se = Math.sqrt(pooled * (1 - pooled) * (1 / n1 + 1 / n2))
56
+ if (se > 0) {
57
+ const z = (p2 - p1) / se
58
+ pValue = 2 * (1 - normalCdf(Math.abs(z)))
59
+ confidence = (1 - pValue) * 100
60
+ significant = pValue < 0.05
61
+ }
62
+ }
63
+
64
+ return { visitors: n2, conversions: c2, conversionRate: p2 * 100, upliftPct, pValue, confidence, significant, isControl }
65
+ })
66
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Per-site raw-data retention, honored at ingest (#178).
3
+ *
4
+ * The Settings UI has always written a RETENTION_SETTINGS item (plan-clamped),
5
+ * but nothing ever read it — every raw row got the global config TTL, making
6
+ * the setting decorative. Collect now loads the site's setting (cached
7
+ * in-process for 5 minutes) and every raw write stamps its TTL from it.
8
+ *
9
+ * Honest limitation: TTLs are stamped at WRITE time. Changing retention
10
+ * affects rows written after the change; existing rows keep their original
11
+ * expiry (a rewrite sweep would cost a full-partition scan per change).
12
+ */
13
+ import { getConfig } from '../config'
14
+
15
+ const CACHE_MS = 5 * 60 * 1000
16
+ const cache = new Map<string, { seconds: number | null, at: number }>()
17
+
18
+ /**
19
+ * Load (cached) the site's retention override in seconds, or null when the
20
+ * site has none configured.
21
+ */
22
+ export async function ensureSiteRetentionLoaded(siteId: string): Promise<void> {
23
+ const hit = cache.get(siteId)
24
+ if (hit && Date.now() - hit.at < CACHE_MS)
25
+ return
26
+ try {
27
+ // Lazy import: the ORM imports this module for rawTtlForSite, and
28
+ // lib/dynamodb imports the ORM — a static import here would recreate the
29
+ // circular-import TDZ crash (#177).
30
+ const { dynamodb, TABLE_NAME, unmarshall } = await import('./dynamodb')
31
+ const res = await dynamodb.getItem({
32
+ TableName: TABLE_NAME,
33
+ Key: { pk: { S: `SITE#${siteId}` }, sk: { S: 'RETENTION_SETTINGS' } },
34
+ })
35
+ const days = res.Item ? Number(unmarshall(res.Item).retentionDays) : Number.NaN
36
+ cache.set(siteId, { seconds: Number.isFinite(days) && days > 0 ? days * 24 * 60 * 60 : null, at: Date.now() })
37
+ }
38
+ catch {
39
+ // Unreachable settings must never drop events — fall back to global TTL.
40
+ cache.set(siteId, { seconds: null, at: Date.now() })
41
+ }
42
+ }
43
+
44
+ /** Raw-row TTL epoch for a site: its configured retention, else global config. */
45
+ export function rawTtlForSite(siteId: string): number {
46
+ const now = Math.floor(Date.now() / 1000)
47
+ const override = cache.get(siteId)?.seconds
48
+ if (override)
49
+ return now + override
50
+ return now + getConfig().retention.rawEventTtl
51
+ }
52
+
53
+ /** Test hook. */
54
+ export function clearSiteRetentionCache(): void {
55
+ cache.clear()
56
+ }
package/src/local.ts ADDED
@@ -0,0 +1,360 @@
1
+ /**
2
+ * Local Development Utilities
3
+ *
4
+ * Helpers for running analytics locally with DynamoDB Local.
5
+ * Uses dynamodb-tooling for DynamoDB Local management.
6
+ */
7
+
8
+ import type { AnalyticsConfig } from './config'
9
+ import { getConfig } from './config'
10
+
11
+ // ============================================================================
12
+ // Local DynamoDB Configuration
13
+ // ============================================================================
14
+
15
+ export interface LocalDynamoDBConfig {
16
+ /** Port for DynamoDB Local (default: 8000) */
17
+ port: number
18
+ /** Region to use (default: us-east-1) */
19
+ region: string
20
+ /** Endpoint URL */
21
+ endpoint: string
22
+ /** Whether to use shared database mode */
23
+ sharedDb: boolean
24
+ /** Path to store data (empty for in-memory) */
25
+ dbPath: string
26
+ }
27
+
28
+ export const defaultLocalConfig: LocalDynamoDBConfig = {
29
+ port: 8000,
30
+ region: 'us-east-1',
31
+ endpoint: 'http://localhost:8000',
32
+ sharedDb: true,
33
+ dbPath: '',
34
+ }
35
+
36
+ // ============================================================================
37
+ // Table Creation
38
+ // ============================================================================
39
+
40
+ /**
41
+ * Generate CreateTable input for analytics table (local development)
42
+ */
43
+ export function generateLocalCreateTableInput(config?: AnalyticsConfig): {
44
+ TableName: string
45
+ KeySchema: Array<{ AttributeName: string, KeyType: 'HASH' | 'RANGE' }>
46
+ AttributeDefinitions: Array<{ AttributeName: string, AttributeType: 'S' | 'N' | 'B' }>
47
+ BillingMode: string
48
+ GlobalSecondaryIndexes?: Array<{
49
+ IndexName: string
50
+ KeySchema: Array<{ AttributeName: string, KeyType: 'HASH' | 'RANGE' }>
51
+ Projection: { ProjectionType: string }
52
+ }>
53
+ } {
54
+ const cfg = config ?? getConfig()
55
+ const { singleTable } = cfg.table
56
+
57
+ const input: ReturnType<typeof generateLocalCreateTableInput> = {
58
+ TableName: cfg.table.tableName,
59
+ KeySchema: [
60
+ { AttributeName: singleTable.partitionKeyName, KeyType: 'HASH' },
61
+ { AttributeName: singleTable.sortKeyName, KeyType: 'RANGE' },
62
+ ],
63
+ AttributeDefinitions: [
64
+ { AttributeName: singleTable.partitionKeyName, AttributeType: 'S' },
65
+ { AttributeName: singleTable.sortKeyName, AttributeType: 'S' },
66
+ { AttributeName: singleTable.gsi1pkName, AttributeType: 'S' },
67
+ { AttributeName: singleTable.gsi1skName, AttributeType: 'S' },
68
+ { AttributeName: singleTable.gsi2pkName, AttributeType: 'S' },
69
+ { AttributeName: singleTable.gsi2skName, AttributeType: 'S' },
70
+ ],
71
+ BillingMode: cfg.table.billingMode,
72
+ GlobalSecondaryIndexes: [
73
+ {
74
+ IndexName: 'gsi1',
75
+ KeySchema: [
76
+ { AttributeName: singleTable.gsi1pkName, KeyType: 'HASH' },
77
+ { AttributeName: singleTable.gsi1skName, KeyType: 'RANGE' },
78
+ ],
79
+ Projection: { ProjectionType: 'ALL' },
80
+ },
81
+ {
82
+ IndexName: 'gsi2',
83
+ KeySchema: [
84
+ { AttributeName: singleTable.gsi2pkName, KeyType: 'HASH' },
85
+ { AttributeName: singleTable.gsi2skName, KeyType: 'RANGE' },
86
+ ],
87
+ Projection: { ProjectionType: 'ALL' },
88
+ },
89
+ ],
90
+ }
91
+
92
+ return input
93
+ }
94
+
95
+ /**
96
+ * Generate AWS CLI command to create the table
97
+ */
98
+ export function generateAwsCliCommand(config?: AnalyticsConfig): string {
99
+ const input = generateLocalCreateTableInput(config)
100
+
101
+ const command = [
102
+ 'aws dynamodb create-table',
103
+ `--table-name ${input.TableName}`,
104
+ `--attribute-definitions '${JSON.stringify(input.AttributeDefinitions)}'`,
105
+ `--key-schema '${JSON.stringify(input.KeySchema)}'`,
106
+ `--billing-mode ${input.BillingMode}`,
107
+ `--global-secondary-indexes '${JSON.stringify(input.GlobalSecondaryIndexes)}'`,
108
+ '--endpoint-url http://localhost:8000',
109
+ '--region us-east-1',
110
+ ]
111
+
112
+ return command.join(' \\\n ')
113
+ }
114
+
115
+ // ============================================================================
116
+ // Docker Compose
117
+ // ============================================================================
118
+
119
+ /**
120
+ * Generate docker-compose.yml for local development
121
+ */
122
+ export function generateDockerCompose(localConfig: Partial<LocalDynamoDBConfig> = {}): string {
123
+ const cfg = { ...defaultLocalConfig, ...localConfig }
124
+
125
+ return `version: '3.8'
126
+
127
+ services:
128
+ dynamodb-local:
129
+ image: amazon/dynamodb-local:latest
130
+ container_name: analytics-dynamodb
131
+ ports:
132
+ - "${cfg.port}:8000"
133
+ command: ["-jar", "DynamoDBLocal.jar"${cfg.sharedDb ? ', "-sharedDb"' : ''}${cfg.dbPath ? `, "-dbPath", "/data"` : ''}]
134
+ ${cfg.dbPath
135
+ ? `volumes:
136
+ - ./data/dynamodb:/data`
137
+ : ''}
138
+ networks:
139
+ - analytics-network
140
+
141
+ networks:
142
+ analytics-network:
143
+ driver: bridge
144
+ `
145
+ }
146
+
147
+ // ============================================================================
148
+ // Seed Data
149
+ // ============================================================================
150
+
151
+ export interface SeedDataOptions {
152
+ /** Number of sites to create */
153
+ sites: number
154
+ /** Number of page views per site */
155
+ pageViewsPerSite: number
156
+ /** Number of sessions per site */
157
+ sessionsPerSite: number
158
+ /** Number of days of historical data */
159
+ daysOfHistory: number
160
+ }
161
+
162
+ export const defaultSeedOptions: SeedDataOptions = {
163
+ sites: 1,
164
+ pageViewsPerSite: 100,
165
+ sessionsPerSite: 50,
166
+ daysOfHistory: 7,
167
+ }
168
+
169
+ /**
170
+ * Generate seed data for local development
171
+ */
172
+ export function generateSeedData(options: Partial<SeedDataOptions> = {}): {
173
+ sites: Array<Record<string, unknown>>
174
+ pageViews: Array<Record<string, unknown>>
175
+ sessions: Array<Record<string, unknown>>
176
+ } {
177
+ const opts = { ...defaultSeedOptions, ...options }
178
+ const sites: Array<Record<string, unknown>> = []
179
+ const pageViews: Array<Record<string, unknown>> = []
180
+ const sessions: Array<Record<string, unknown>> = []
181
+
182
+ const paths = [
183
+ '/',
184
+ '/about',
185
+ '/blog',
186
+ '/blog/post-1',
187
+ '/blog/post-2',
188
+ '/contact',
189
+ '/pricing',
190
+ '/features',
191
+ ]
192
+
193
+ const referrers = [
194
+ { source: 'google', url: 'https://google.com' },
195
+ { source: 'twitter', url: 'https://twitter.com' },
196
+ { source: 'direct', url: null },
197
+ { source: 'facebook', url: 'https://facebook.com' },
198
+ { source: 'linkedin', url: 'https://linkedin.com' },
199
+ ]
200
+
201
+ const devices = ['desktop', 'mobile', 'tablet']
202
+ const browsers = ['Chrome', 'Firefox', 'Safari', 'Edge']
203
+ const oses = ['Windows', 'macOS', 'iOS', 'Android', 'Linux']
204
+
205
+ for (let s = 0; s < opts.sites; s++) {
206
+ const siteId = `site_${String(s + 1).padStart(3, '0')}`
207
+
208
+ // Create site
209
+ sites.push({
210
+ pk: `SITE#${siteId}`,
211
+ sk: `SITE#${siteId}`,
212
+ gsi1pk: `OWNER#user_001`,
213
+ gsi1sk: `SITE#${siteId}`,
214
+ id: siteId,
215
+ name: `Test Site ${s + 1}`,
216
+ domains: [`site${s + 1}.example.com`],
217
+ timezone: 'UTC',
218
+ isActive: true,
219
+ ownerId: 'user_001',
220
+ settings: JSON.stringify({
221
+ collectGeolocation: false,
222
+ trackReferrers: true,
223
+ trackUtmParams: true,
224
+ trackDeviceType: true,
225
+ publicDashboard: false,
226
+ excludedPaths: [],
227
+ excludedIps: [],
228
+ dataRetentionDays: 365,
229
+ }),
230
+ createdAt: new Date().toISOString(),
231
+ updatedAt: new Date().toISOString(),
232
+ })
233
+
234
+ // Create sessions
235
+ for (let sess = 0; sess < opts.sessionsPerSite; sess++) {
236
+ const sessionId = `sess_${siteId}_${String(sess + 1).padStart(4, '0')}`
237
+ const visitorId = `visitor_${String(Math.floor(Math.random() * opts.sessionsPerSite / 2) + 1).padStart(4, '0')}`
238
+ const daysAgo = Math.floor(Math.random() * opts.daysOfHistory)
239
+ const startTime = new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000)
240
+ const duration = Math.floor(Math.random() * 300000) // 0-5 minutes
241
+ const pageCount = Math.floor(Math.random() * 5) + 1
242
+ const referrer = referrers[Math.floor(Math.random() * referrers.length)]
243
+ const device = devices[Math.floor(Math.random() * devices.length)]
244
+
245
+ sessions.push({
246
+ pk: `SITE#${siteId}`,
247
+ sk: `SESSION#${sessionId}`,
248
+ id: sessionId,
249
+ siteId,
250
+ visitorId,
251
+ entryPath: paths[Math.floor(Math.random() * paths.length)],
252
+ exitPath: paths[Math.floor(Math.random() * paths.length)],
253
+ referrer: referrer.url,
254
+ referrerSource: referrer.source,
255
+ deviceType: device,
256
+ browser: browsers[Math.floor(Math.random() * browsers.length)],
257
+ os: oses[Math.floor(Math.random() * oses.length)],
258
+ pageViewCount: pageCount,
259
+ eventCount: Math.floor(Math.random() * 3),
260
+ isBounce: pageCount === 1,
261
+ duration,
262
+ startedAt: startTime.toISOString(),
263
+ endedAt: new Date(startTime.getTime() + duration).toISOString(),
264
+ })
265
+ }
266
+
267
+ // Create page views
268
+ for (let pv = 0; pv < opts.pageViewsPerSite; pv++) {
269
+ const pageViewId = `pv_${siteId}_${String(pv + 1).padStart(5, '0')}`
270
+ const sessionIdx = Math.floor(Math.random() * opts.sessionsPerSite)
271
+ const sessionId = `sess_${siteId}_${String(sessionIdx + 1).padStart(4, '0')}`
272
+ const visitorId = `visitor_${String(Math.floor(Math.random() * opts.sessionsPerSite / 2) + 1).padStart(4, '0')}`
273
+ const daysAgo = Math.floor(Math.random() * opts.daysOfHistory)
274
+ const timestamp = new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000 - Math.random() * 24 * 60 * 60 * 1000)
275
+ const path = paths[Math.floor(Math.random() * paths.length)]
276
+ const referrer = referrers[Math.floor(Math.random() * referrers.length)]
277
+
278
+ pageViews.push({
279
+ pk: `SITE#${siteId}`,
280
+ sk: `PV#${timestamp.toISOString()}#${pageViewId}`,
281
+ gsi1pk: `SITE#${siteId}#DATE#${timestamp.toISOString().slice(0, 10)}`,
282
+ gsi1sk: `PATH#${path}#${pageViewId}`,
283
+ id: pageViewId,
284
+ siteId,
285
+ visitorId,
286
+ sessionId,
287
+ path,
288
+ hostname: `site${s + 1}.example.com`,
289
+ title: `Page: ${path}`,
290
+ referrer: referrer.url,
291
+ referrerSource: referrer.source,
292
+ deviceType: devices[Math.floor(Math.random() * devices.length)],
293
+ browser: browsers[Math.floor(Math.random() * browsers.length)],
294
+ os: oses[Math.floor(Math.random() * oses.length)],
295
+ screenWidth: [1920, 1440, 1366, 375, 768][Math.floor(Math.random() * 5)],
296
+ screenHeight: [1080, 900, 768, 667, 1024][Math.floor(Math.random() * 5)],
297
+ isUnique: pv % 3 === 0,
298
+ isBounce: false,
299
+ timestamp: timestamp.toISOString(),
300
+ ttl: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
301
+ })
302
+ }
303
+ }
304
+
305
+ return { sites, pageViews, sessions }
306
+ }
307
+
308
+ // ============================================================================
309
+ // Setup Script
310
+ // ============================================================================
311
+
312
+ /**
313
+ * Print setup instructions for local development
314
+ */
315
+ export function printLocalSetupInstructions(): void {
316
+ console.log(`
317
+ ╔═══════════════════════════════════════════════════════════════════╗
318
+ ║ Analytics Local Development Setup ║
319
+ ╚═══════════════════════════════════════════════════════════════════╝
320
+
321
+ 1. Start DynamoDB Local:
322
+
323
+ Option A: Using Docker
324
+ ────────────────────────
325
+ docker run -p 8000:8000 amazon/dynamodb-local
326
+
327
+ Option B: Using docker-compose
328
+ ────────────────────────
329
+ Save the docker-compose.yml and run:
330
+ docker-compose up -d
331
+
332
+ Option C: Using dynamodb-tooling (if installed globally)
333
+ ────────────────────────
334
+ dbtooling local start
335
+
336
+ 2. Create the analytics table:
337
+
338
+ ${generateAwsCliCommand()}
339
+
340
+ 3. Verify the table was created:
341
+
342
+ aws dynamodb list-tables --endpoint-url http://localhost:8000
343
+
344
+ 4. Configure your analytics:
345
+
346
+ import { setConfig } from '@stacksjs/analytics'
347
+
348
+ setConfig({
349
+ table: { tableName: 'AnalyticsTable' },
350
+ endpoint: 'http://localhost:8000',
351
+ region: 'us-east-1',
352
+ })
353
+
354
+ 5. Run the seed script (optional):
355
+
356
+ bun run examples/seed-local.ts
357
+
358
+ Happy developing! 🚀
359
+ `)
360
+ }