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