@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.
- package/CHANGELOG.md +68 -0
- package/LICENSE.md +21 -0
- package/README.md +361 -0
- package/bin/cli.ts +169 -0
- package/dist/Analytics.d.ts +558 -0
- package/dist/api.d.ts +109 -0
- package/dist/batching.d.ts +93 -0
- package/dist/chunk-2mx7fq49.js +4 -0
- package/dist/chunk-3z29508k.js +204 -0
- package/dist/chunk-deephkz6.js +56 -0
- package/dist/chunk-j261vgyp.js +305 -0
- package/dist/chunk-xga17tz7.js +207 -0
- package/dist/config.d.ts +132 -0
- package/dist/dashboard/components/index.d.ts +80 -0
- package/dist/dashboard/composables/useAnalytics.d.ts +99 -0
- package/dist/dashboard/index.d.ts +110 -0
- package/dist/dashboard/types/index.d.ts +144 -0
- package/dist/dashboard/utils/index.d.ts +81 -0
- package/dist/dynamodb.d.ts +86 -0
- package/dist/funnels.d.ts +104 -0
- package/dist/geolocation.d.ts +96 -0
- package/dist/index.d.ts +371 -0
- package/dist/index.js +10211 -0
- package/dist/infrastructure/cdk.d.ts +60 -0
- package/dist/infrastructure/cloudformation.d.ts +44 -0
- package/dist/infrastructure/index.d.ts +45 -0
- package/dist/infrastructure/setup.d.ts +133 -0
- package/dist/integrations/cloudflare.d.ts +76 -0
- package/dist/integrations/hono.d.ts +60 -0
- package/dist/integrations/index.d.ts +24 -0
- package/dist/integrations/nuxt.d.ts +7 -0
- package/dist/integrations/nuxt.js +42 -0
- package/dist/integrations/runtime/use-ts-analytics.d.ts +18 -0
- package/dist/integrations/runtime/use-ts-analytics.js +17 -0
- package/dist/integrations/stx.d.ts +88 -0
- package/dist/integrations/stx.js +18 -0
- package/dist/lib/crypto-random.d.ts +4 -0
- package/dist/lib/salt.d.ts +16 -0
- package/dist/local.d.ts +56 -0
- package/dist/model-connector.d.ts +145 -0
- package/dist/models/AggregatedStats.d.ts +9 -0
- package/dist/models/CampaignStats.d.ts +9 -0
- package/dist/models/Conversion.d.ts +11 -0
- package/dist/models/CustomEvent.d.ts +11 -0
- package/dist/models/DeviceStats.d.ts +9 -0
- package/dist/models/EventStats.d.ts +9 -0
- package/dist/models/GeoStats.d.ts +9 -0
- package/dist/models/Goal.d.ts +9 -0
- package/dist/models/GoalStats.d.ts +9 -0
- package/dist/models/PageStats.d.ts +11 -0
- package/dist/models/PageView.d.ts +13 -0
- package/dist/models/RealtimeStats.d.ts +9 -0
- package/dist/models/ReferrerStats.d.ts +9 -0
- package/dist/models/Session.d.ts +11 -0
- package/dist/models/Site.d.ts +11 -0
- package/dist/models/index.d.ts +28 -0
- package/dist/models/types.d.ts +60 -0
- package/dist/sqs-buffering.d.ts +243 -0
- package/dist/stacks-integration.d.ts +159 -0
- package/dist/tracking-script.d.ts +71 -0
- package/dist/tracking.d.ts +17 -0
- package/dist/tracking.js +26 -0
- package/dist/types.d.ts +595 -0
- package/dist/utils/geolocation.d.ts +111 -0
- package/dist/utils/user-agent.d.ts +17 -0
- package/dist/version.d.ts +7 -0
- package/package.json +119 -0
- package/src/Analytics.ts +3349 -0
- package/src/api.ts +1286 -0
- package/src/assets/crosswind.css +2220 -0
- package/src/batching.ts +452 -0
- package/src/components/dashboard/index.ts +18 -0
- package/src/config.ts +456 -0
- package/src/dashboard/Dashboard.stx +1517 -0
- package/src/dashboard/components/AlertCard.stx +177 -0
- package/src/dashboard/components/AnalyticsDashboard.stx +354 -0
- package/src/dashboard/components/AnimatedNumber.stx +86 -0
- package/src/dashboard/components/BarChart.stx +220 -0
- package/src/dashboard/components/BrowserBreakdown.stx +98 -0
- package/src/dashboard/components/BrowsersTable.stx +125 -0
- package/src/dashboard/components/CampaignBreakdown.stx +163 -0
- package/src/dashboard/components/CampaignTable.stx +238 -0
- package/src/dashboard/components/CountryList.stx +101 -0
- package/src/dashboard/components/DataTable.stx +226 -0
- package/src/dashboard/components/DateRangePicker.stx +77 -0
- package/src/dashboard/components/DeviceBreakdown.stx +94 -0
- package/src/dashboard/components/DevicesTable.stx +163 -0
- package/src/dashboard/components/DonutChart.stories.ts +55 -0
- package/src/dashboard/components/DonutChart.stx +157 -0
- package/src/dashboard/components/EmptyState.stx +90 -0
- package/src/dashboard/components/EngagementMetrics.stx +183 -0
- package/src/dashboard/components/EventsSection.stx +192 -0
- package/src/dashboard/components/FilterBar.stx +104 -0
- package/src/dashboard/components/FullAnalyticsDashboard.stx +455 -0
- package/src/dashboard/components/FunnelChart.stx +142 -0
- package/src/dashboard/components/GeoTable.stx +337 -0
- package/src/dashboard/components/GoalsPanel.stx +109 -0
- package/src/dashboard/components/Header.stx +39 -0
- package/src/dashboard/components/HeatmapChart.stx +140 -0
- package/src/dashboard/components/LiveActivityFeed.stx +172 -0
- package/src/dashboard/components/MetricComparison.stx +106 -0
- package/src/dashboard/components/MiniStats.stx +96 -0
- package/src/dashboard/components/OSBreakdown.stx +124 -0
- package/src/dashboard/components/PageDetailCard.stx +102 -0
- package/src/dashboard/components/PagesTable.stx +127 -0
- package/src/dashboard/components/ProgressRing.stx +89 -0
- package/src/dashboard/components/RealtimeCounter.stories.ts +45 -0
- package/src/dashboard/components/RealtimeCounter.stx +46 -0
- package/src/dashboard/components/ReferrersTable.stx +180 -0
- package/src/dashboard/components/SparklineChart.stx +160 -0
- package/src/dashboard/components/StatCard.stories.ts +58 -0
- package/src/dashboard/components/StatCard.stx +90 -0
- package/src/dashboard/components/SummaryStats.stx +81 -0
- package/src/dashboard/components/TabNav.stx +66 -0
- package/src/dashboard/components/ThemeSwitcher.stx +124 -0
- package/src/dashboard/components/TimeSeriesChart.stx +106 -0
- package/src/dashboard/components/TopList.stories.ts +58 -0
- package/src/dashboard/components/TopList.stx +74 -0
- package/src/dashboard/components/TrendIndicator.stx +84 -0
- package/src/dashboard/components/heatmap/ClickHeatmap.stx +264 -0
- package/src/dashboard/components/heatmap/ElementClickList.stx +125 -0
- package/src/dashboard/components/heatmap/HeatmapControls.stx +125 -0
- package/src/dashboard/components/heatmap/HeatmapLegend.stx +69 -0
- package/src/dashboard/components/heatmap/PageHeatmap.stx +264 -0
- package/src/dashboard/components/heatmap/ScrollHeatmap.stx +127 -0
- package/src/dashboard/components/heatmap/index.ts +12 -0
- package/src/dashboard/components/index.ts +86 -0
- package/src/dashboard/composables/useAnalytics.ts +465 -0
- package/src/dashboard/demo/DemoApp.stx +370 -0
- package/src/dashboard/demo/index.ts +8 -0
- package/src/dashboard/demo/mockData.ts +234 -0
- package/src/dashboard/index.ts +117 -0
- package/src/dashboard/stx-shim.d.ts +4 -0
- package/src/dashboard/types/index.ts +203 -0
- package/src/dashboard/utils/index.ts +426 -0
- package/src/dynamodb.ts +344 -0
- package/src/funnels.ts +534 -0
- package/src/geolocation.ts +515 -0
- package/src/handlers/alerts.ts +328 -0
- package/src/handlers/annotations.ts +128 -0
- package/src/handlers/api-keys.ts +240 -0
- package/src/handlers/auth.ts +1020 -0
- package/src/handlers/authz.ts +137 -0
- package/src/handlers/collect.ts +724 -0
- package/src/handlers/data.ts +625 -0
- package/src/handlers/experiments.ts +138 -0
- package/src/handlers/funnels.ts +216 -0
- package/src/handlers/goals.ts +218 -0
- package/src/handlers/heatmaps.ts +272 -0
- package/src/handlers/index.ts +56 -0
- package/src/handlers/lib/read-cache.ts +63 -0
- package/src/handlers/misc.ts +486 -0
- package/src/handlers/oauth.ts +233 -0
- package/src/handlers/performance.ts +388 -0
- package/src/handlers/sessions.ts +413 -0
- package/src/handlers/sharing.ts +198 -0
- package/src/handlers/stats.ts +1368 -0
- package/src/handlers/team.ts +161 -0
- package/src/handlers/uptime.ts +283 -0
- package/src/handlers/views.ts +390 -0
- package/src/handlers/webhooks.ts +226 -0
- package/src/heatmap/index.ts +32 -0
- package/src/heatmap/tracking-script.ts +452 -0
- package/src/heatmap/types.ts +79 -0
- package/src/index.ts +387 -0
- package/src/infrastructure/cdk.ts +496 -0
- package/src/infrastructure/cloudformation.ts +595 -0
- package/src/infrastructure/index.ts +48 -0
- package/src/infrastructure/setup.ts +611 -0
- package/src/integrations/cloudflare.ts +732 -0
- package/src/integrations/hono.ts +589 -0
- package/src/integrations/index.ts +27 -0
- package/src/integrations/nuxt.ts +78 -0
- package/src/integrations/runtime/use-ts-analytics.ts +32 -0
- package/src/integrations/stx.ts +138 -0
- package/src/jobs/index.ts +127 -0
- package/src/lib/crypto-random.ts +41 -0
- package/src/lib/ddb-errors.ts +20 -0
- package/src/lib/dynamodb.ts +216 -0
- package/src/lib/email.ts +38 -0
- package/src/lib/ga-import.ts +471 -0
- package/src/lib/ga4-api.ts +244 -0
- package/src/lib/goals.ts +205 -0
- package/src/lib/index.ts +6 -0
- package/src/lib/ingest-counters.ts +123 -0
- package/src/lib/log.ts +36 -0
- package/src/lib/membership.ts +98 -0
- package/src/lib/plans.ts +90 -0
- package/src/lib/quota.ts +46 -0
- package/src/lib/rate-limit.ts +27 -0
- package/src/lib/rollups.ts +472 -0
- package/src/lib/salt.ts +97 -0
- package/src/lib/scheduler.ts +97 -0
- package/src/lib/significance.ts +66 -0
- package/src/lib/site-retention.ts +56 -0
- package/src/local.ts +360 -0
- package/src/model-connector.ts +616 -0
- package/src/models/AggregatedStats.ts +162 -0
- package/src/models/CampaignStats.ts +141 -0
- package/src/models/Conversion.ts +135 -0
- package/src/models/CustomEvent.ts +123 -0
- package/src/models/DeviceStats.ts +105 -0
- package/src/models/EventStats.ts +108 -0
- package/src/models/GeoStats.ts +112 -0
- package/src/models/Goal.ts +105 -0
- package/src/models/GoalStats.ts +106 -0
- package/src/models/PageStats.ts +152 -0
- package/src/models/PageView.ts +277 -0
- package/src/models/RealtimeStats.ts +96 -0
- package/src/models/ReferrerStats.ts +115 -0
- package/src/models/Session.ts +235 -0
- package/src/models/Site.ts +115 -0
- package/src/models/index.ts +50 -0
- package/src/models/orm/index.ts +2043 -0
- package/src/models/types.ts +71 -0
- package/src/router.ts +521 -0
- package/src/sqs-buffering.ts +806 -0
- package/src/stacks-integration.ts +527 -0
- package/src/tracking-script.ts +944 -0
- package/src/tracking.ts +27 -0
- package/src/types/analytics.ts +219 -0
- package/src/types/api.ts +75 -0
- package/src/types/bun-router.d.ts +15 -0
- package/src/types/dashboard.ts +62 -0
- package/src/types/index.ts +66 -0
- package/src/types/stx.d.ts +27 -0
- package/src/types/window.d.ts +101 -0
- package/src/types.ts +911 -0
- package/src/utils/cache.ts +148 -0
- package/src/utils/date.ts +118 -0
- package/src/utils/filters.ts +71 -0
- package/src/utils/geolocation.ts +323 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/response.ts +180 -0
- package/src/utils/timezone-country.ts +242 -0
- package/src/utils/user-agent.ts +110 -0
- package/src/version.ts +7 -0
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CloudFormation Template Generator for Analytics DynamoDB Table
|
|
3
|
+
*
|
|
4
|
+
* Generates CloudFormation JSON/YAML for deploying the analytics
|
|
5
|
+
* single-table design to AWS.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface CloudFormationConfig {
|
|
9
|
+
/** Stack name */
|
|
10
|
+
stackName?: string
|
|
11
|
+
/** Table name */
|
|
12
|
+
tableName?: string
|
|
13
|
+
/** Billing mode */
|
|
14
|
+
billingMode?: 'PAY_PER_REQUEST' | 'PROVISIONED'
|
|
15
|
+
/** Provisioned capacity (if billingMode is PROVISIONED) */
|
|
16
|
+
provisionedCapacity?: {
|
|
17
|
+
readCapacityUnits: number
|
|
18
|
+
writeCapacityUnits: number
|
|
19
|
+
}
|
|
20
|
+
/** Enable Point-in-Time Recovery */
|
|
21
|
+
enablePitr?: boolean
|
|
22
|
+
/** Enable DynamoDB Streams */
|
|
23
|
+
enableStreams?: boolean
|
|
24
|
+
/** Stream view type */
|
|
25
|
+
streamViewType?: 'KEYS_ONLY' | 'NEW_IMAGE' | 'OLD_IMAGE' | 'NEW_AND_OLD_IMAGES'
|
|
26
|
+
/** TTL attribute name */
|
|
27
|
+
ttlAttributeName?: string
|
|
28
|
+
/** Enable server-side encryption */
|
|
29
|
+
enableEncryption?: boolean
|
|
30
|
+
/** KMS key ARN for encryption (optional, uses AWS managed key if not specified) */
|
|
31
|
+
kmsKeyArn?: string
|
|
32
|
+
/** Tags to apply to resources */
|
|
33
|
+
tags?: Record<string, string>
|
|
34
|
+
/** Enable deletion protection */
|
|
35
|
+
deletionProtection?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const defaultConfig: Required<CloudFormationConfig> = {
|
|
39
|
+
stackName: 'analytics-dynamodb',
|
|
40
|
+
tableName: 'analytics',
|
|
41
|
+
billingMode: 'PAY_PER_REQUEST',
|
|
42
|
+
provisionedCapacity: {
|
|
43
|
+
readCapacityUnits: 5,
|
|
44
|
+
writeCapacityUnits: 5,
|
|
45
|
+
},
|
|
46
|
+
enablePitr: true,
|
|
47
|
+
enableStreams: false,
|
|
48
|
+
streamViewType: 'NEW_AND_OLD_IMAGES',
|
|
49
|
+
ttlAttributeName: 'ttl',
|
|
50
|
+
enableEncryption: true,
|
|
51
|
+
kmsKeyArn: '',
|
|
52
|
+
tags: {},
|
|
53
|
+
deletionProtection: false,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Analytics table attribute definitions
|
|
58
|
+
*/
|
|
59
|
+
const attributeDefinitions = [
|
|
60
|
+
{ AttributeName: 'pk', AttributeType: 'S' },
|
|
61
|
+
{ AttributeName: 'sk', AttributeType: 'S' },
|
|
62
|
+
{ AttributeName: 'gsi1pk', AttributeType: 'S' },
|
|
63
|
+
{ AttributeName: 'gsi1sk', AttributeType: 'S' },
|
|
64
|
+
{ AttributeName: 'gsi2pk', AttributeType: 'S' },
|
|
65
|
+
{ AttributeName: 'gsi2sk', AttributeType: 'S' },
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Analytics table key schema
|
|
70
|
+
*/
|
|
71
|
+
const keySchema = [
|
|
72
|
+
{ AttributeName: 'pk', KeyType: 'HASH' },
|
|
73
|
+
{ AttributeName: 'sk', KeyType: 'RANGE' },
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Global Secondary Index definitions for analytics
|
|
78
|
+
*
|
|
79
|
+
* GSI1: Owner lookups, date-based queries
|
|
80
|
+
* - Sites by owner: gsi1pk=OWNER#{ownerId}, gsi1sk=SITE#{siteId}
|
|
81
|
+
* - PageViews by date: gsi1pk=SITE#{id}#DATE#{date}, gsi1sk=PATH#{path}
|
|
82
|
+
* - Sessions by date: gsi1pk=SITE#{id}#SESSIONS#{date}
|
|
83
|
+
* - Events by name: gsi1pk=SITE#{id}#EVENTNAME#{name}
|
|
84
|
+
* - Goals by site: gsi1pk=SITE#{id}#GOAL#{goalId}
|
|
85
|
+
* - Page stats by period: gsi1pk=SITE#{id}#PAGESTATS#{period}#{start}
|
|
86
|
+
*
|
|
87
|
+
* GSI2: Visitor lookups
|
|
88
|
+
* - PageViews by visitor: gsi2pk=SITE#{id}#VISITOR#{visitorId}
|
|
89
|
+
*/
|
|
90
|
+
function getGsiDefinitions(config: Required<CloudFormationConfig>) {
|
|
91
|
+
const projection = { ProjectionType: 'ALL' }
|
|
92
|
+
|
|
93
|
+
const gsis = [
|
|
94
|
+
{
|
|
95
|
+
IndexName: 'GSI1',
|
|
96
|
+
KeySchema: [
|
|
97
|
+
{ AttributeName: 'gsi1pk', KeyType: 'HASH' },
|
|
98
|
+
{ AttributeName: 'gsi1sk', KeyType: 'RANGE' },
|
|
99
|
+
],
|
|
100
|
+
Projection: projection,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
IndexName: 'GSI2',
|
|
104
|
+
KeySchema: [
|
|
105
|
+
{ AttributeName: 'gsi2pk', KeyType: 'HASH' },
|
|
106
|
+
{ AttributeName: 'gsi2sk', KeyType: 'RANGE' },
|
|
107
|
+
],
|
|
108
|
+
Projection: projection,
|
|
109
|
+
},
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
// Add provisioned capacity if not on-demand
|
|
113
|
+
if (config.billingMode === 'PROVISIONED') {
|
|
114
|
+
return gsis.map(gsi => ({
|
|
115
|
+
...gsi,
|
|
116
|
+
ProvisionedThroughput: {
|
|
117
|
+
ReadCapacityUnits: config.provisionedCapacity.readCapacityUnits,
|
|
118
|
+
WriteCapacityUnits: config.provisionedCapacity.writeCapacityUnits,
|
|
119
|
+
},
|
|
120
|
+
}))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return gsis
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Generate CloudFormation template for analytics table
|
|
128
|
+
*/
|
|
129
|
+
export function generateCloudFormationTemplate(
|
|
130
|
+
userConfig: CloudFormationConfig = {},
|
|
131
|
+
): Record<string, unknown> {
|
|
132
|
+
const config = { ...defaultConfig, ...userConfig }
|
|
133
|
+
|
|
134
|
+
const tableProperties: Record<string, unknown> = {
|
|
135
|
+
TableName: config.tableName,
|
|
136
|
+
AttributeDefinitions: attributeDefinitions,
|
|
137
|
+
KeySchema: keySchema,
|
|
138
|
+
GlobalSecondaryIndexes: getGsiDefinitions(config),
|
|
139
|
+
BillingMode: config.billingMode,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Add provisioned capacity if not on-demand
|
|
143
|
+
if (config.billingMode === 'PROVISIONED') {
|
|
144
|
+
tableProperties.ProvisionedThroughput = {
|
|
145
|
+
ReadCapacityUnits: config.provisionedCapacity.readCapacityUnits,
|
|
146
|
+
WriteCapacityUnits: config.provisionedCapacity.writeCapacityUnits,
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// TTL configuration
|
|
151
|
+
if (config.ttlAttributeName) {
|
|
152
|
+
tableProperties.TimeToLiveSpecification = {
|
|
153
|
+
AttributeName: config.ttlAttributeName,
|
|
154
|
+
Enabled: true,
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Stream configuration
|
|
159
|
+
if (config.enableStreams) {
|
|
160
|
+
tableProperties.StreamSpecification = {
|
|
161
|
+
StreamViewType: config.streamViewType,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Server-side encryption
|
|
166
|
+
if (config.enableEncryption) {
|
|
167
|
+
tableProperties.SSESpecification = {
|
|
168
|
+
SSEEnabled: true,
|
|
169
|
+
...(config.kmsKeyArn && {
|
|
170
|
+
SSEType: 'KMS',
|
|
171
|
+
KMSMasterKeyId: config.kmsKeyArn,
|
|
172
|
+
}),
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Point-in-time recovery
|
|
177
|
+
if (config.enablePitr) {
|
|
178
|
+
tableProperties.PointInTimeRecoverySpecification = {
|
|
179
|
+
PointInTimeRecoveryEnabled: true,
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Deletion protection
|
|
184
|
+
if (config.deletionProtection) {
|
|
185
|
+
tableProperties.DeletionProtectionEnabled = true
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Tags
|
|
189
|
+
if (Object.keys(config.tags).length > 0) {
|
|
190
|
+
tableProperties.Tags = Object.entries(config.tags).map(([Key, Value]) => ({
|
|
191
|
+
Key,
|
|
192
|
+
Value,
|
|
193
|
+
}))
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
AWSTemplateFormatVersion: '2010-09-09',
|
|
198
|
+
Description: 'DynamoDB table for privacy-focused web analytics (single-table design)',
|
|
199
|
+
|
|
200
|
+
Parameters: {
|
|
201
|
+
Environment: {
|
|
202
|
+
Type: 'String',
|
|
203
|
+
Default: 'production',
|
|
204
|
+
AllowedValues: ['development', 'staging', 'production'],
|
|
205
|
+
Description: 'Deployment environment',
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
Resources: {
|
|
210
|
+
AnalyticsTable: {
|
|
211
|
+
Type: 'AWS::DynamoDB::Table',
|
|
212
|
+
Properties: tableProperties,
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
Outputs: {
|
|
217
|
+
TableName: {
|
|
218
|
+
Description: 'Analytics DynamoDB table name',
|
|
219
|
+
Value: { Ref: 'AnalyticsTable' },
|
|
220
|
+
Export: {
|
|
221
|
+
Name: { 'Fn::Sub': '${AWS::StackName}-TableName' },
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
TableArn: {
|
|
225
|
+
Description: 'Analytics DynamoDB table ARN',
|
|
226
|
+
Value: { 'Fn::GetAtt': ['AnalyticsTable', 'Arn'] },
|
|
227
|
+
Export: {
|
|
228
|
+
Name: { 'Fn::Sub': '${AWS::StackName}-TableArn' },
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
...(config.enableStreams && {
|
|
232
|
+
StreamArn: {
|
|
233
|
+
Description: 'DynamoDB Stream ARN',
|
|
234
|
+
Value: { 'Fn::GetAtt': ['AnalyticsTable', 'StreamArn'] },
|
|
235
|
+
Export: {
|
|
236
|
+
Name: { 'Fn::Sub': '${AWS::StackName}-StreamArn' },
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
}),
|
|
240
|
+
},
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Generate CloudFormation template as JSON string
|
|
246
|
+
*/
|
|
247
|
+
export function generateCloudFormationJson(
|
|
248
|
+
config: CloudFormationConfig = {},
|
|
249
|
+
pretty = true,
|
|
250
|
+
): string {
|
|
251
|
+
const template = generateCloudFormationTemplate(config)
|
|
252
|
+
return pretty ? JSON.stringify(template, null, 2) : JSON.stringify(template)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Generate CloudFormation template as YAML string
|
|
257
|
+
*/
|
|
258
|
+
export function generateCloudFormationYaml(
|
|
259
|
+
config: CloudFormationConfig = {},
|
|
260
|
+
): string {
|
|
261
|
+
const template = generateCloudFormationTemplate(config)
|
|
262
|
+
return jsonToYaml(template)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Simple JSON to YAML converter (handles CloudFormation intrinsic functions)
|
|
267
|
+
*/
|
|
268
|
+
// eslint-disable-next-line pickier/no-unused-vars
|
|
269
|
+
function jsonToYaml(obj: unknown, indent = 0): string {
|
|
270
|
+
const spaces = ' '.repeat(indent)
|
|
271
|
+
|
|
272
|
+
if (obj === null || obj === undefined) {
|
|
273
|
+
return 'null'
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (typeof obj === 'string') {
|
|
277
|
+
// Handle multi-line strings or strings with special characters
|
|
278
|
+
if (obj.includes('\n') || obj.includes(':') || obj.includes('#')) {
|
|
279
|
+
return `"${obj.replace(/"/g, '\\"')}"`
|
|
280
|
+
}
|
|
281
|
+
return obj
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (typeof obj === 'number' || typeof obj === 'boolean') {
|
|
285
|
+
return String(obj)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (Array.isArray(obj)) {
|
|
289
|
+
if (obj.length === 0)
|
|
290
|
+
return '[]'
|
|
291
|
+
return obj
|
|
292
|
+
.map((item) => {
|
|
293
|
+
const value = jsonToYaml(item, indent + 1)
|
|
294
|
+
if (typeof item === 'object' && item !== null) {
|
|
295
|
+
return `${spaces}- ${value.trim().replace(/^\s+/gm, match => `${spaces} ${match.slice(spaces.length + 2)}`)}`
|
|
296
|
+
}
|
|
297
|
+
return `${spaces}- ${value}`
|
|
298
|
+
})
|
|
299
|
+
.join('\n')
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (typeof obj === 'object') {
|
|
303
|
+
const entries = Object.entries(obj as Record<string, unknown>)
|
|
304
|
+
if (entries.length === 0)
|
|
305
|
+
return '{}'
|
|
306
|
+
|
|
307
|
+
// Handle CloudFormation intrinsic functions
|
|
308
|
+
if (entries.length === 1) {
|
|
309
|
+
const [key, value] = entries[0]
|
|
310
|
+
if (key === 'Ref') {
|
|
311
|
+
return `!Ref ${value}`
|
|
312
|
+
}
|
|
313
|
+
if (key === 'Fn::Sub') {
|
|
314
|
+
return `!Sub "${value}"`
|
|
315
|
+
}
|
|
316
|
+
if (key === 'Fn::GetAtt') {
|
|
317
|
+
if (Array.isArray(value)) {
|
|
318
|
+
return `!GetAtt ${(value as string[]).join('.')}`
|
|
319
|
+
}
|
|
320
|
+
return `!GetAtt ${value}`
|
|
321
|
+
}
|
|
322
|
+
if (key === 'Fn::Join') {
|
|
323
|
+
const [delimiter, parts] = value as [string, unknown[]]
|
|
324
|
+
return `!Join\n${spaces} - "${delimiter}"\n${spaces} - ${jsonToYaml(parts, indent + 2)}`
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return entries
|
|
329
|
+
.map(([key, value]) => {
|
|
330
|
+
const yamlValue = jsonToYaml(value, indent + 1)
|
|
331
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
332
|
+
return `${spaces}${key}:\n${yamlValue}`
|
|
333
|
+
}
|
|
334
|
+
if (Array.isArray(value)) {
|
|
335
|
+
return `${spaces}${key}:\n${yamlValue}`
|
|
336
|
+
}
|
|
337
|
+
return `${spaces}${key}: ${yamlValue}`
|
|
338
|
+
})
|
|
339
|
+
.join('\n')
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return String(obj)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Generate SAM (Serverless Application Model) template
|
|
347
|
+
* Includes Lambda functions for API endpoints
|
|
348
|
+
*/
|
|
349
|
+
export function generateSamTemplate(config: CloudFormationConfig = {}): Record<string, unknown> {
|
|
350
|
+
const cfnTemplate = generateCloudFormationTemplate(config)
|
|
351
|
+
const _mergedConfig = { ...defaultConfig, ...config }
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
AWSTemplateFormatVersion: '2010-09-09',
|
|
355
|
+
Transform: 'AWS::Serverless-2016-10-31',
|
|
356
|
+
Description: 'Privacy-focused web analytics with DynamoDB and Lambda',
|
|
357
|
+
|
|
358
|
+
Globals: {
|
|
359
|
+
Function: {
|
|
360
|
+
Runtime: 'nodejs20.x',
|
|
361
|
+
Timeout: 30,
|
|
362
|
+
MemorySize: 256,
|
|
363
|
+
Environment: {
|
|
364
|
+
Variables: {
|
|
365
|
+
ANALYTICS_TABLE: { Ref: 'AnalyticsTable' },
|
|
366
|
+
NODE_ENV: { Ref: 'Environment' },
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
Architectures: ['arm64'],
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
Parameters: {
|
|
374
|
+
...((cfnTemplate as Record<string, unknown>).Parameters as Record<string, unknown> || {}),
|
|
375
|
+
CorsOrigins: {
|
|
376
|
+
Type: 'String',
|
|
377
|
+
Default: '*',
|
|
378
|
+
Description: 'CORS allowed origins (comma-separated)',
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
|
|
382
|
+
Resources: {
|
|
383
|
+
// DynamoDB Table
|
|
384
|
+
AnalyticsTable: (cfnTemplate as Record<string, unknown>).Resources
|
|
385
|
+
? ((cfnTemplate as Record<string, unknown>).Resources as Record<string, unknown>).AnalyticsTable
|
|
386
|
+
: {},
|
|
387
|
+
|
|
388
|
+
// API Gateway
|
|
389
|
+
AnalyticsApi: {
|
|
390
|
+
Type: 'AWS::Serverless::HttpApi',
|
|
391
|
+
Properties: {
|
|
392
|
+
StageName: { Ref: 'Environment' },
|
|
393
|
+
CorsConfiguration: {
|
|
394
|
+
AllowOrigins: [{ Ref: 'CorsOrigins' }],
|
|
395
|
+
AllowMethods: ['GET', 'POST', 'OPTIONS'],
|
|
396
|
+
AllowHeaders: ['Content-Type', 'Authorization'],
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
// Lambda Functions
|
|
402
|
+
CollectFunction: {
|
|
403
|
+
Type: 'AWS::Serverless::Function',
|
|
404
|
+
Properties: {
|
|
405
|
+
Handler: 'index.collectHandler',
|
|
406
|
+
CodeUri: './dist/lambda',
|
|
407
|
+
Description: 'Collect analytics events',
|
|
408
|
+
Events: {
|
|
409
|
+
Api: {
|
|
410
|
+
Type: 'HttpApi',
|
|
411
|
+
Properties: {
|
|
412
|
+
ApiId: { Ref: 'AnalyticsApi' },
|
|
413
|
+
Path: '/api/analytics/collect',
|
|
414
|
+
Method: 'POST',
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
Policies: [
|
|
419
|
+
{
|
|
420
|
+
DynamoDBCrudPolicy: {
|
|
421
|
+
TableName: { Ref: 'AnalyticsTable' },
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
StatsFunction: {
|
|
429
|
+
Type: 'AWS::Serverless::Function',
|
|
430
|
+
Properties: {
|
|
431
|
+
Handler: 'index.statsHandler',
|
|
432
|
+
CodeUri: './dist/lambda',
|
|
433
|
+
Description: 'Get analytics statistics',
|
|
434
|
+
Events: {
|
|
435
|
+
Api: {
|
|
436
|
+
Type: 'HttpApi',
|
|
437
|
+
Properties: {
|
|
438
|
+
ApiId: { Ref: 'AnalyticsApi' },
|
|
439
|
+
Path: '/api/analytics/sites/{siteId}/stats',
|
|
440
|
+
Method: 'GET',
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
Policies: [
|
|
445
|
+
{
|
|
446
|
+
DynamoDBReadPolicy: {
|
|
447
|
+
TableName: { Ref: 'AnalyticsTable' },
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
|
|
454
|
+
RealtimeFunction: {
|
|
455
|
+
Type: 'AWS::Serverless::Function',
|
|
456
|
+
Properties: {
|
|
457
|
+
Handler: 'index.realtimeHandler',
|
|
458
|
+
CodeUri: './dist/lambda',
|
|
459
|
+
Description: 'Get realtime analytics',
|
|
460
|
+
Events: {
|
|
461
|
+
Api: {
|
|
462
|
+
Type: 'HttpApi',
|
|
463
|
+
Properties: {
|
|
464
|
+
ApiId: { Ref: 'AnalyticsApi' },
|
|
465
|
+
Path: '/api/analytics/sites/{siteId}/realtime',
|
|
466
|
+
Method: 'GET',
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
Policies: [
|
|
471
|
+
{
|
|
472
|
+
DynamoDBReadPolicy: {
|
|
473
|
+
TableName: { Ref: 'AnalyticsTable' },
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
],
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
|
|
480
|
+
SitesFunction: {
|
|
481
|
+
Type: 'AWS::Serverless::Function',
|
|
482
|
+
Properties: {
|
|
483
|
+
Handler: 'index.sitesHandler',
|
|
484
|
+
CodeUri: './dist/lambda',
|
|
485
|
+
Description: 'Manage analytics sites',
|
|
486
|
+
Events: {
|
|
487
|
+
List: {
|
|
488
|
+
Type: 'HttpApi',
|
|
489
|
+
Properties: {
|
|
490
|
+
ApiId: { Ref: 'AnalyticsApi' },
|
|
491
|
+
Path: '/api/analytics/sites',
|
|
492
|
+
Method: 'GET',
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
Create: {
|
|
496
|
+
Type: 'HttpApi',
|
|
497
|
+
Properties: {
|
|
498
|
+
ApiId: { Ref: 'AnalyticsApi' },
|
|
499
|
+
Path: '/api/analytics/sites',
|
|
500
|
+
Method: 'POST',
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
Get: {
|
|
504
|
+
Type: 'HttpApi',
|
|
505
|
+
Properties: {
|
|
506
|
+
ApiId: { Ref: 'AnalyticsApi' },
|
|
507
|
+
Path: '/api/analytics/sites/{siteId}',
|
|
508
|
+
Method: 'GET',
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
Policies: [
|
|
513
|
+
{
|
|
514
|
+
DynamoDBCrudPolicy: {
|
|
515
|
+
TableName: { Ref: 'AnalyticsTable' },
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
],
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
|
|
522
|
+
GoalsFunction: {
|
|
523
|
+
Type: 'AWS::Serverless::Function',
|
|
524
|
+
Properties: {
|
|
525
|
+
Handler: 'index.goalsHandler',
|
|
526
|
+
CodeUri: './dist/lambda',
|
|
527
|
+
Description: 'Manage analytics goals',
|
|
528
|
+
Events: {
|
|
529
|
+
List: {
|
|
530
|
+
Type: 'HttpApi',
|
|
531
|
+
Properties: {
|
|
532
|
+
ApiId: { Ref: 'AnalyticsApi' },
|
|
533
|
+
Path: '/api/analytics/sites/{siteId}/goals',
|
|
534
|
+
Method: 'GET',
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
Create: {
|
|
538
|
+
Type: 'HttpApi',
|
|
539
|
+
Properties: {
|
|
540
|
+
ApiId: { Ref: 'AnalyticsApi' },
|
|
541
|
+
Path: '/api/analytics/sites/{siteId}/goals',
|
|
542
|
+
Method: 'POST',
|
|
543
|
+
},
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
Policies: [
|
|
547
|
+
{
|
|
548
|
+
DynamoDBCrudPolicy: {
|
|
549
|
+
TableName: { Ref: 'AnalyticsTable' },
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
],
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
|
|
556
|
+
ScriptFunction: {
|
|
557
|
+
Type: 'AWS::Serverless::Function',
|
|
558
|
+
Properties: {
|
|
559
|
+
Handler: 'index.scriptHandler',
|
|
560
|
+
CodeUri: './dist/lambda',
|
|
561
|
+
Description: 'Generate tracking script',
|
|
562
|
+
Events: {
|
|
563
|
+
Api: {
|
|
564
|
+
Type: 'HttpApi',
|
|
565
|
+
Properties: {
|
|
566
|
+
ApiId: { Ref: 'AnalyticsApi' },
|
|
567
|
+
Path: '/api/analytics/sites/{siteId}/script',
|
|
568
|
+
Method: 'GET',
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
|
|
575
|
+
},
|
|
576
|
+
|
|
577
|
+
Outputs: {
|
|
578
|
+
...((cfnTemplate as Record<string, unknown>).Outputs || {}),
|
|
579
|
+
ApiEndpoint: {
|
|
580
|
+
Description: 'Analytics API endpoint',
|
|
581
|
+
Value: {
|
|
582
|
+
'Fn::Sub': 'https://${AnalyticsApi}.execute-api.${AWS::Region}.amazonaws.com/${Environment}',
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Generate SAM template as YAML string
|
|
591
|
+
*/
|
|
592
|
+
export function generateSamYaml(config: CloudFormationConfig = {}): string {
|
|
593
|
+
const template = generateSamTemplate(config)
|
|
594
|
+
return jsonToYaml(template)
|
|
595
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics Infrastructure Module
|
|
3
|
+
*
|
|
4
|
+
* Provides tools for deploying the analytics DynamoDB table:
|
|
5
|
+
* - CloudFormation templates (JSON/YAML)
|
|
6
|
+
* - SAM templates for serverless deployment
|
|
7
|
+
* - CDK code generation
|
|
8
|
+
* - AWS SDK CreateTable input
|
|
9
|
+
* - Migration utilities
|
|
10
|
+
* - Setup scripts
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// CDK
|
|
14
|
+
export {
|
|
15
|
+
type AnalyticsApiProps,
|
|
16
|
+
type AnalyticsTableProps,
|
|
17
|
+
generateCdkCode,
|
|
18
|
+
generateCdkTableCode,
|
|
19
|
+
generateCreateTableInput,
|
|
20
|
+
} from './cdk'
|
|
21
|
+
|
|
22
|
+
// CloudFormation
|
|
23
|
+
export {
|
|
24
|
+
type CloudFormationConfig,
|
|
25
|
+
generateCloudFormationJson,
|
|
26
|
+
generateCloudFormationTemplate,
|
|
27
|
+
generateCloudFormationYaml,
|
|
28
|
+
generateSamTemplate,
|
|
29
|
+
generateSamYaml,
|
|
30
|
+
} from './cloudformation'
|
|
31
|
+
|
|
32
|
+
// Setup & Migrations
|
|
33
|
+
export {
|
|
34
|
+
checkTableStatus,
|
|
35
|
+
createAnalyticsTable,
|
|
36
|
+
createPitrMigration,
|
|
37
|
+
createStreamsMigration,
|
|
38
|
+
createTimeBasedGsiMigration,
|
|
39
|
+
type DynamoDBClientLike,
|
|
40
|
+
enableTtl,
|
|
41
|
+
generateAwsCliCommands,
|
|
42
|
+
type MigrationResult,
|
|
43
|
+
type MigrationStep,
|
|
44
|
+
printSetupInstructions,
|
|
45
|
+
runMigrations,
|
|
46
|
+
type SetupConfig,
|
|
47
|
+
type SetupResult,
|
|
48
|
+
} from './setup'
|