@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,496 @@
1
+ /**
2
+ * AWS CDK Construct for Analytics DynamoDB Table
3
+ *
4
+ * This module provides CDK constructs for deploying the analytics
5
+ * infrastructure to AWS.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { AnalyticsTable } from 'dynamodb-tooling/analytics/infrastructure/cdk'
10
+ *
11
+ * const analytics = new AnalyticsTable(this, 'Analytics', {
12
+ * tableName: 'my-analytics',
13
+ * billingMode: BillingMode.PAY_PER_REQUEST,
14
+ * })
15
+ *
16
+ * // Access the table
17
+ * analytics.table.grantReadWriteData(myLambda)
18
+ * ```
19
+ *
20
+ * Note: This module exports construct props and a helper function
21
+ * to generate the CDK code since we can't import CDK directly
22
+ * (it's a peer dependency).
23
+ */
24
+
25
+ // ============================================================================
26
+ // CDK Props Types
27
+ // ============================================================================
28
+
29
+ export interface AnalyticsTableProps {
30
+ /** Table name (default: 'analytics') */
31
+ tableName?: string
32
+ /** Billing mode */
33
+ billingMode?: 'PAY_PER_REQUEST' | 'PROVISIONED'
34
+ /** Read capacity units (if PROVISIONED) */
35
+ readCapacity?: number
36
+ /** Write capacity units (if PROVISIONED) */
37
+ writeCapacity?: number
38
+ /** Enable Point-in-Time Recovery */
39
+ pointInTimeRecovery?: boolean
40
+ /** Enable DynamoDB Streams */
41
+ stream?: 'KEYS_ONLY' | 'NEW_IMAGE' | 'OLD_IMAGE' | 'NEW_AND_OLD_IMAGES'
42
+ /** TTL attribute name */
43
+ timeToLiveAttribute?: string
44
+ /** Removal policy */
45
+ removalPolicy?: 'DESTROY' | 'RETAIN' | 'SNAPSHOT'
46
+ /** Enable encryption */
47
+ encryption?: 'AWS_MANAGED' | 'CUSTOMER_MANAGED' | 'DEFAULT'
48
+ /** KMS key for encryption (if CUSTOMER_MANAGED) */
49
+ encryptionKey?: string
50
+ /** Enable deletion protection */
51
+ deletionProtection?: boolean
52
+ /** Tags */
53
+ tags?: Record<string, string>
54
+ }
55
+
56
+ export interface AnalyticsApiProps extends AnalyticsTableProps {
57
+ /** API stage name */
58
+ stageName?: string
59
+ /** CORS allowed origins */
60
+ corsOrigins?: string[]
61
+ /** Lambda memory size */
62
+ memorySize?: number
63
+ /** Lambda timeout (seconds) */
64
+ timeout?: number
65
+ /** Lambda code path */
66
+ codePath?: string
67
+ /** Enable aggregation scheduled functions */
68
+ enableAggregation?: boolean
69
+ }
70
+
71
+ // ============================================================================
72
+ // CDK Code Generator
73
+ // ============================================================================
74
+
75
+ /**
76
+ * Generate CDK TypeScript code for analytics infrastructure
77
+ */
78
+ export function generateCdkCode(props: AnalyticsApiProps = {}): string {
79
+ const config = {
80
+ tableName: props.tableName || 'analytics',
81
+ billingMode: props.billingMode || 'PAY_PER_REQUEST',
82
+ readCapacity: props.readCapacity || 5,
83
+ writeCapacity: props.writeCapacity || 5,
84
+ pointInTimeRecovery: props.pointInTimeRecovery ?? true,
85
+ stream: props.stream,
86
+ timeToLiveAttribute: props.timeToLiveAttribute || 'ttl',
87
+ removalPolicy: props.removalPolicy || 'RETAIN',
88
+ encryption: props.encryption || 'AWS_MANAGED',
89
+ deletionProtection: props.deletionProtection ?? false,
90
+ stageName: props.stageName || 'prod',
91
+ corsOrigins: props.corsOrigins || ['*'],
92
+ memorySize: props.memorySize || 256,
93
+ timeout: props.timeout || 30,
94
+ codePath: props.codePath || 'dist/lambda',
95
+ enableAggregation: props.enableAggregation ?? true,
96
+ tags: props.tags || {},
97
+ }
98
+
99
+ return `import * as cdk from 'aws-cdk-lib'
100
+ import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'
101
+ import * as lambda from 'aws-cdk-lib/aws-lambda'
102
+ import * as apigateway from 'aws-cdk-lib/aws-apigatewayv2'
103
+ import * as apigatewayIntegrations from 'aws-cdk-lib/aws-apigatewayv2-integrations'
104
+ import * as events from 'aws-cdk-lib/aws-events'
105
+ import * as targets from 'aws-cdk-lib/aws-events-targets'
106
+ import { Construct } from 'constructs'
107
+
108
+ export interface AnalyticsStackProps extends cdk.StackProps {
109
+ tableName?: string
110
+ stageName?: string
111
+ corsOrigins?: string[]
112
+ }
113
+
114
+ export class AnalyticsStack extends cdk.Stack {
115
+ public readonly table: dynamodb.Table
116
+ public readonly api: apigateway.HttpApi
117
+
118
+ constructor(scope: Construct, id: string, props: AnalyticsStackProps = {}) {
119
+ super(scope, id, props)
120
+
121
+ const tableName = props.tableName || '${config.tableName}'
122
+ const stageName = props.stageName || '${config.stageName}'
123
+ const corsOrigins = props.corsOrigins || ${JSON.stringify(config.corsOrigins)}
124
+
125
+ // ========================================================================
126
+ // DynamoDB Table
127
+ // ========================================================================
128
+
129
+ this.table = new dynamodb.Table(this, 'AnalyticsTable', {
130
+ tableName,
131
+ partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING },
132
+ sortKey: { name: 'sk', type: dynamodb.AttributeType.STRING },
133
+ billingMode: dynamodb.BillingMode.${config.billingMode},
134
+ ${config.billingMode === 'PROVISIONED' ? `readCapacity: ${config.readCapacity},\n writeCapacity: ${config.writeCapacity},` : ''}
135
+ pointInTimeRecovery: ${config.pointInTimeRecovery},
136
+ ${config.stream ? `stream: dynamodb.StreamViewType.${config.stream},` : ''}
137
+ timeToLiveAttribute: '${config.timeToLiveAttribute}',
138
+ removalPolicy: cdk.RemovalPolicy.${config.removalPolicy},
139
+ encryption: dynamodb.TableEncryption.${config.encryption},
140
+ deletionProtection: ${config.deletionProtection},
141
+ })
142
+
143
+ // GSI1: Owner lookups, date-based queries
144
+ this.table.addGlobalSecondaryIndex({
145
+ indexName: 'GSI1',
146
+ partitionKey: { name: 'gsi1pk', type: dynamodb.AttributeType.STRING },
147
+ sortKey: { name: 'gsi1sk', type: dynamodb.AttributeType.STRING },
148
+ projectionType: dynamodb.ProjectionType.ALL,
149
+ })
150
+
151
+ // GSI2: Visitor lookups
152
+ this.table.addGlobalSecondaryIndex({
153
+ indexName: 'GSI2',
154
+ partitionKey: { name: 'gsi2pk', type: dynamodb.AttributeType.STRING },
155
+ sortKey: { name: 'gsi2sk', type: dynamodb.AttributeType.STRING },
156
+ projectionType: dynamodb.ProjectionType.ALL,
157
+ })
158
+
159
+ // ========================================================================
160
+ // Lambda Functions
161
+ // ========================================================================
162
+
163
+ const commonLambdaProps: Partial<lambda.FunctionProps> = {
164
+ runtime: lambda.Runtime.NODEJS_20_X,
165
+ architecture: lambda.Architecture.ARM_64,
166
+ memorySize: ${config.memorySize},
167
+ timeout: cdk.Duration.seconds(${config.timeout}),
168
+ environment: {
169
+ ANALYTICS_TABLE: tableName,
170
+ NODE_ENV: stageName,
171
+ },
172
+ }
173
+
174
+ const collectFn = new lambda.Function(this, 'CollectFunction', {
175
+ ...commonLambdaProps,
176
+ handler: 'index.collectHandler',
177
+ code: lambda.Code.fromAsset('${config.codePath}'),
178
+ description: 'Collect analytics events',
179
+ })
180
+
181
+ const statsFn = new lambda.Function(this, 'StatsFunction', {
182
+ ...commonLambdaProps,
183
+ handler: 'index.statsHandler',
184
+ code: lambda.Code.fromAsset('${config.codePath}'),
185
+ description: 'Get analytics statistics',
186
+ })
187
+
188
+ const realtimeFn = new lambda.Function(this, 'RealtimeFunction', {
189
+ ...commonLambdaProps,
190
+ handler: 'index.realtimeHandler',
191
+ code: lambda.Code.fromAsset('${config.codePath}'),
192
+ description: 'Get realtime analytics',
193
+ })
194
+
195
+ const sitesFn = new lambda.Function(this, 'SitesFunction', {
196
+ ...commonLambdaProps,
197
+ handler: 'index.sitesHandler',
198
+ code: lambda.Code.fromAsset('${config.codePath}'),
199
+ description: 'Manage analytics sites',
200
+ })
201
+
202
+ const goalsFn = new lambda.Function(this, 'GoalsFunction', {
203
+ ...commonLambdaProps,
204
+ handler: 'index.goalsHandler',
205
+ code: lambda.Code.fromAsset('${config.codePath}'),
206
+ description: 'Manage analytics goals',
207
+ })
208
+
209
+ const scriptFn = new lambda.Function(this, 'ScriptFunction', {
210
+ ...commonLambdaProps,
211
+ handler: 'index.scriptHandler',
212
+ code: lambda.Code.fromAsset('${config.codePath}'),
213
+ description: 'Generate tracking script',
214
+ })
215
+
216
+ // Grant permissions
217
+ this.table.grantReadWriteData(collectFn)
218
+ this.table.grantReadData(statsFn)
219
+ this.table.grantReadData(realtimeFn)
220
+ this.table.grantReadWriteData(sitesFn)
221
+ this.table.grantReadWriteData(goalsFn)
222
+
223
+ // ========================================================================
224
+ // API Gateway
225
+ // ========================================================================
226
+
227
+ this.api = new apigateway.HttpApi(this, 'AnalyticsApi', {
228
+ apiName: \`\${tableName}-api\`,
229
+ corsPreflight: {
230
+ allowOrigins: corsOrigins,
231
+ allowMethods: [
232
+ apigateway.CorsHttpMethod.GET,
233
+ apigateway.CorsHttpMethod.POST,
234
+ apigateway.CorsHttpMethod.OPTIONS,
235
+ ],
236
+ allowHeaders: ['Content-Type', 'Authorization'],
237
+ },
238
+ })
239
+
240
+ // Routes
241
+ this.api.addRoutes({
242
+ path: '/api/analytics/collect',
243
+ methods: [apigateway.HttpMethod.POST],
244
+ integration: new apigatewayIntegrations.HttpLambdaIntegration('CollectIntegration', collectFn),
245
+ })
246
+
247
+ this.api.addRoutes({
248
+ path: '/api/analytics/sites/{siteId}/stats',
249
+ methods: [apigateway.HttpMethod.GET],
250
+ integration: new apigatewayIntegrations.HttpLambdaIntegration('StatsIntegration', statsFn),
251
+ })
252
+
253
+ this.api.addRoutes({
254
+ path: '/api/analytics/sites/{siteId}/realtime',
255
+ methods: [apigateway.HttpMethod.GET],
256
+ integration: new apigatewayIntegrations.HttpLambdaIntegration('RealtimeIntegration', realtimeFn),
257
+ })
258
+
259
+ this.api.addRoutes({
260
+ path: '/api/analytics/sites',
261
+ methods: [apigateway.HttpMethod.GET, apigateway.HttpMethod.POST],
262
+ integration: new apigatewayIntegrations.HttpLambdaIntegration('SitesIntegration', sitesFn),
263
+ })
264
+
265
+ this.api.addRoutes({
266
+ path: '/api/analytics/sites/{siteId}',
267
+ methods: [apigateway.HttpMethod.GET],
268
+ integration: new apigatewayIntegrations.HttpLambdaIntegration('SiteIntegration', sitesFn),
269
+ })
270
+
271
+ this.api.addRoutes({
272
+ path: '/api/analytics/sites/{siteId}/goals',
273
+ methods: [apigateway.HttpMethod.GET, apigateway.HttpMethod.POST],
274
+ integration: new apigatewayIntegrations.HttpLambdaIntegration('GoalsIntegration', goalsFn),
275
+ })
276
+
277
+ this.api.addRoutes({
278
+ path: '/api/analytics/sites/{siteId}/script',
279
+ methods: [apigateway.HttpMethod.GET],
280
+ integration: new apigatewayIntegrations.HttpLambdaIntegration('ScriptIntegration', scriptFn),
281
+ })
282
+
283
+ ${config.enableAggregation
284
+ ? `
285
+ // ========================================================================
286
+ // Aggregation Scheduled Functions
287
+ // ========================================================================
288
+
289
+ // Background jobs (rollups/alerts) are driven by POST /api/jobs/tick on
290
+ // the API function via an EventBridge rule — there is no separate
291
+ // aggregation handler artifact (the old index.aggregationHandler never
292
+ // existed and broke deploys, #168).
293
+
294
+ // Hourly aggregation
295
+ new events.Rule(this, 'HourlyAggregation', {
296
+ schedule: events.Schedule.rate(cdk.Duration.hours(1)),
297
+ targets: [
298
+ new targets.LambdaFunction(aggregationFn, {
299
+ event: events.RuleTargetInput.fromObject({ period: 'hour' }),
300
+ }),
301
+ ],
302
+ })
303
+
304
+ // Daily aggregation (midnight UTC)
305
+ new events.Rule(this, 'DailyAggregation', {
306
+ schedule: events.Schedule.cron({ hour: '0', minute: '0' }),
307
+ targets: [
308
+ new targets.LambdaFunction(aggregationFn, {
309
+ event: events.RuleTargetInput.fromObject({ period: 'day' }),
310
+ }),
311
+ ],
312
+ })
313
+
314
+ // Monthly aggregation (1st of month, midnight UTC)
315
+ new events.Rule(this, 'MonthlyAggregation', {
316
+ schedule: events.Schedule.cron({ day: '1', hour: '0', minute: '0' }),
317
+ targets: [
318
+ new targets.LambdaFunction(aggregationFn, {
319
+ event: events.RuleTargetInput.fromObject({ period: 'month' }),
320
+ }),
321
+ ],
322
+ })
323
+ `
324
+ : ''}
325
+
326
+ // ========================================================================
327
+ // Outputs
328
+ // ========================================================================
329
+
330
+ new cdk.CfnOutput(this, 'TableName', {
331
+ value: this.table.tableName,
332
+ description: 'Analytics DynamoDB table name',
333
+ })
334
+
335
+ new cdk.CfnOutput(this, 'TableArn', {
336
+ value: this.table.tableArn,
337
+ description: 'Analytics DynamoDB table ARN',
338
+ })
339
+
340
+ new cdk.CfnOutput(this, 'ApiEndpoint', {
341
+ value: this.api.apiEndpoint,
342
+ description: 'Analytics API endpoint',
343
+ })
344
+ }
345
+ }
346
+
347
+ // ========================================================================
348
+ // App Entry Point
349
+ // ========================================================================
350
+
351
+ const app = new cdk.App()
352
+
353
+ new AnalyticsStack(app, 'AnalyticsStack', {
354
+ env: {
355
+ account: process.env.CDK_DEFAULT_ACCOUNT,
356
+ region: process.env.CDK_DEFAULT_REGION || 'us-east-1',
357
+ },
358
+ ${Object.keys(config.tags).length > 0 ? `tags: ${JSON.stringify(config.tags)},` : ''}
359
+ })
360
+
361
+ app.synth()
362
+ `
363
+ }
364
+
365
+ /**
366
+ * Generate minimal CDK code for just the table (no API)
367
+ */
368
+ export function generateCdkTableCode(props: AnalyticsTableProps = {}): string {
369
+ const config = {
370
+ tableName: props.tableName || 'analytics',
371
+ billingMode: props.billingMode || 'PAY_PER_REQUEST',
372
+ readCapacity: props.readCapacity || 5,
373
+ writeCapacity: props.writeCapacity || 5,
374
+ pointInTimeRecovery: props.pointInTimeRecovery ?? true,
375
+ stream: props.stream,
376
+ timeToLiveAttribute: props.timeToLiveAttribute || 'ttl',
377
+ removalPolicy: props.removalPolicy || 'RETAIN',
378
+ encryption: props.encryption || 'AWS_MANAGED',
379
+ deletionProtection: props.deletionProtection ?? false,
380
+ }
381
+
382
+ return `import * as cdk from 'aws-cdk-lib'
383
+ import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'
384
+ import { Construct } from 'constructs'
385
+
386
+ export class AnalyticsTableConstruct extends Construct {
387
+ public readonly table: dynamodb.Table
388
+
389
+ constructor(scope: Construct, id: string) {
390
+ super(scope, id)
391
+
392
+ this.table = new dynamodb.Table(this, 'Table', {
393
+ tableName: '${config.tableName}',
394
+ partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING },
395
+ sortKey: { name: 'sk', type: dynamodb.AttributeType.STRING },
396
+ billingMode: dynamodb.BillingMode.${config.billingMode},
397
+ ${config.billingMode === 'PROVISIONED' ? `readCapacity: ${config.readCapacity},\n writeCapacity: ${config.writeCapacity},` : ''}
398
+ pointInTimeRecovery: ${config.pointInTimeRecovery},
399
+ ${config.stream ? `stream: dynamodb.StreamViewType.${config.stream},` : ''}
400
+ timeToLiveAttribute: '${config.timeToLiveAttribute}',
401
+ removalPolicy: cdk.RemovalPolicy.${config.removalPolicy},
402
+ encryption: dynamodb.TableEncryption.${config.encryption},
403
+ deletionProtection: ${config.deletionProtection},
404
+ })
405
+
406
+ // GSI1: Owner lookups, date-based queries
407
+ this.table.addGlobalSecondaryIndex({
408
+ indexName: 'GSI1',
409
+ partitionKey: { name: 'gsi1pk', type: dynamodb.AttributeType.STRING },
410
+ sortKey: { name: 'gsi1sk', type: dynamodb.AttributeType.STRING },
411
+ projectionType: dynamodb.ProjectionType.ALL,
412
+ })
413
+
414
+ // GSI2: Visitor lookups
415
+ this.table.addGlobalSecondaryIndex({
416
+ indexName: 'GSI2',
417
+ partitionKey: { name: 'gsi2pk', type: dynamodb.AttributeType.STRING },
418
+ sortKey: { name: 'gsi2sk', type: dynamodb.AttributeType.STRING },
419
+ projectionType: dynamodb.ProjectionType.ALL,
420
+ })
421
+ }
422
+ }
423
+ `
424
+ }
425
+
426
+ // ============================================================================
427
+ // CreateTable Input Generator (for AWS SDK)
428
+ // ============================================================================
429
+
430
+ /**
431
+ * Generate CreateTableInput for AWS SDK
432
+ */
433
+ export function generateCreateTableInput(props: AnalyticsTableProps = {}): Record<string, unknown> {
434
+ const config = {
435
+ tableName: props.tableName || 'analytics',
436
+ billingMode: props.billingMode || 'PAY_PER_REQUEST',
437
+ readCapacity: props.readCapacity || 5,
438
+ writeCapacity: props.writeCapacity || 5,
439
+ }
440
+
441
+ const input: Record<string, unknown> = {
442
+ TableName: config.tableName,
443
+ AttributeDefinitions: [
444
+ { AttributeName: 'pk', AttributeType: 'S' },
445
+ { AttributeName: 'sk', AttributeType: 'S' },
446
+ { AttributeName: 'gsi1pk', AttributeType: 'S' },
447
+ { AttributeName: 'gsi1sk', AttributeType: 'S' },
448
+ { AttributeName: 'gsi2pk', AttributeType: 'S' },
449
+ { AttributeName: 'gsi2sk', AttributeType: 'S' },
450
+ ],
451
+ KeySchema: [
452
+ { AttributeName: 'pk', KeyType: 'HASH' },
453
+ { AttributeName: 'sk', KeyType: 'RANGE' },
454
+ ],
455
+ BillingMode: config.billingMode,
456
+ GlobalSecondaryIndexes: [
457
+ {
458
+ IndexName: 'GSI1',
459
+ KeySchema: [
460
+ { AttributeName: 'gsi1pk', KeyType: 'HASH' },
461
+ { AttributeName: 'gsi1sk', KeyType: 'RANGE' },
462
+ ],
463
+ Projection: { ProjectionType: 'ALL' },
464
+ ...(config.billingMode === 'PROVISIONED' && {
465
+ ProvisionedThroughput: {
466
+ ReadCapacityUnits: config.readCapacity,
467
+ WriteCapacityUnits: config.writeCapacity,
468
+ },
469
+ }),
470
+ },
471
+ {
472
+ IndexName: 'GSI2',
473
+ KeySchema: [
474
+ { AttributeName: 'gsi2pk', KeyType: 'HASH' },
475
+ { AttributeName: 'gsi2sk', KeyType: 'RANGE' },
476
+ ],
477
+ Projection: { ProjectionType: 'ALL' },
478
+ ...(config.billingMode === 'PROVISIONED' && {
479
+ ProvisionedThroughput: {
480
+ ReadCapacityUnits: config.readCapacity,
481
+ WriteCapacityUnits: config.writeCapacity,
482
+ },
483
+ }),
484
+ },
485
+ ],
486
+ }
487
+
488
+ if (config.billingMode === 'PROVISIONED') {
489
+ input.ProvisionedThroughput = {
490
+ ReadCapacityUnits: config.readCapacity,
491
+ WriteCapacityUnits: config.writeCapacity,
492
+ }
493
+ }
494
+
495
+ return input
496
+ }