@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,527 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// LEGACY -- NOT the live request path (#95)
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Built on the in-memory legacy layer (src/api.ts). Kept for backwards
|
|
5
|
+
// compatibility of the published library API only. The live API is
|
|
6
|
+
// src/router.ts + src/handlers/* -- add new functionality there.
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Stacks Framework Integration
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Integration with the Stacks framework analytics system
|
|
11
|
+
// This provides a DynamoDB-backed analytics driver for Stacks
|
|
12
|
+
|
|
13
|
+
import type { AnalyticsAPIConfig, HandlerContext } from './index'
|
|
14
|
+
import {
|
|
15
|
+
AggregationPipeline,
|
|
16
|
+
AnalyticsAPI,
|
|
17
|
+
|
|
18
|
+
AnalyticsQueryAPI,
|
|
19
|
+
AnalyticsStore,
|
|
20
|
+
generateTrackingScript,
|
|
21
|
+
|
|
22
|
+
} from './index'
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Stacks Analytics Driver Configuration
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* DynamoDB analytics driver configuration for Stacks
|
|
30
|
+
*/
|
|
31
|
+
export interface DynamoDBAnalyticsConfig {
|
|
32
|
+
/** DynamoDB table name */
|
|
33
|
+
tableName: string
|
|
34
|
+
/** AWS region */
|
|
35
|
+
region?: string
|
|
36
|
+
/** Site ID for this installation */
|
|
37
|
+
siteId: string
|
|
38
|
+
/** API endpoint URL */
|
|
39
|
+
apiEndpoint?: string
|
|
40
|
+
/** Whether to use TTL for data retention */
|
|
41
|
+
useTtl?: boolean
|
|
42
|
+
/** Raw event retention in days */
|
|
43
|
+
rawEventRetentionDays?: number
|
|
44
|
+
/** Hourly aggregate retention in days */
|
|
45
|
+
hourlyRetentionDays?: number
|
|
46
|
+
/** Daily aggregate retention in years */
|
|
47
|
+
dailyRetentionYears?: number
|
|
48
|
+
/** Privacy settings */
|
|
49
|
+
privacy?: {
|
|
50
|
+
/** Respect Do Not Track header */
|
|
51
|
+
honorDnt?: boolean
|
|
52
|
+
/** Hash visitor IPs */
|
|
53
|
+
hashIps?: boolean
|
|
54
|
+
/** Collect geolocation data */
|
|
55
|
+
collectGeolocation?: boolean
|
|
56
|
+
}
|
|
57
|
+
/** Tracking options */
|
|
58
|
+
tracking?: {
|
|
59
|
+
/** Track hash changes (for SPAs) */
|
|
60
|
+
trackHashChanges?: boolean
|
|
61
|
+
/** Track outbound links */
|
|
62
|
+
trackOutboundLinks?: boolean
|
|
63
|
+
/** Excluded paths (regex patterns) */
|
|
64
|
+
excludedPaths?: string[]
|
|
65
|
+
/** Excluded IPs */
|
|
66
|
+
excludedIps?: string[]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Stacks analytics options interface (matches Stacks pattern)
|
|
72
|
+
*/
|
|
73
|
+
export interface StacksAnalyticsOptions {
|
|
74
|
+
driver: 'google-analytics' | 'fathom' | 'dynamodb'
|
|
75
|
+
|
|
76
|
+
drivers: {
|
|
77
|
+
googleAnalytics?: {
|
|
78
|
+
trackingId: string
|
|
79
|
+
}
|
|
80
|
+
fathom?: {
|
|
81
|
+
siteId: string
|
|
82
|
+
}
|
|
83
|
+
dynamodb?: DynamoDBAnalyticsConfig
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ============================================================================
|
|
88
|
+
// Stacks Analytics Driver
|
|
89
|
+
// ============================================================================
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* DynamoDB Analytics Driver for Stacks
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```typescript
|
|
96
|
+
* // config/analytics.ts
|
|
97
|
+
* export default {
|
|
98
|
+
* driver: 'dynamodb',
|
|
99
|
+
* drivers: {
|
|
100
|
+
* dynamodb: {
|
|
101
|
+
* tableName: 'analytics',
|
|
102
|
+
* siteId: 'my-site-id',
|
|
103
|
+
* region: 'us-east-1',
|
|
104
|
+
* privacy: {
|
|
105
|
+
* honorDnt: true,
|
|
106
|
+
* hashIps: true,
|
|
107
|
+
* },
|
|
108
|
+
* },
|
|
109
|
+
* },
|
|
110
|
+
* } satisfies StacksAnalyticsOptions
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export class DynamoDBAnalyticsDriver {
|
|
114
|
+
private config: DynamoDBAnalyticsConfig
|
|
115
|
+
private api: AnalyticsAPI
|
|
116
|
+
private store: AnalyticsStore
|
|
117
|
+
private queryApi: AnalyticsQueryAPI
|
|
118
|
+
private pipeline: AggregationPipeline
|
|
119
|
+
private executeCommand: HandlerContext['executeCommand']
|
|
120
|
+
|
|
121
|
+
constructor(
|
|
122
|
+
config: DynamoDBAnalyticsConfig,
|
|
123
|
+
executeCommand: HandlerContext['executeCommand'],
|
|
124
|
+
) {
|
|
125
|
+
this.config = {
|
|
126
|
+
region: 'us-east-1',
|
|
127
|
+
useTtl: true,
|
|
128
|
+
rawEventRetentionDays: 30,
|
|
129
|
+
hourlyRetentionDays: 90,
|
|
130
|
+
dailyRetentionYears: 2,
|
|
131
|
+
privacy: {
|
|
132
|
+
honorDnt: true,
|
|
133
|
+
hashIps: true,
|
|
134
|
+
collectGeolocation: false,
|
|
135
|
+
},
|
|
136
|
+
tracking: {
|
|
137
|
+
trackHashChanges: true,
|
|
138
|
+
trackOutboundLinks: true,
|
|
139
|
+
excludedPaths: [],
|
|
140
|
+
excludedIps: [],
|
|
141
|
+
},
|
|
142
|
+
...config,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
this.executeCommand = executeCommand
|
|
146
|
+
|
|
147
|
+
const apiConfig: AnalyticsAPIConfig = {
|
|
148
|
+
tableName: this.config.tableName,
|
|
149
|
+
useTtl: this.config.useTtl,
|
|
150
|
+
rawEventTtl: (this.config.rawEventRetentionDays || 30) * 24 * 60 * 60,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this.api = new AnalyticsAPI(apiConfig)
|
|
154
|
+
this.store = this.api.getStore()
|
|
155
|
+
this.queryApi = this.api.getQueryAPI()
|
|
156
|
+
this.pipeline = this.api.getPipeline()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get the tracking script HTML to inject into pages
|
|
161
|
+
*/
|
|
162
|
+
getTrackingScript(): string {
|
|
163
|
+
return generateTrackingScript({
|
|
164
|
+
siteId: this.config.siteId,
|
|
165
|
+
apiEndpoint: this.config.apiEndpoint || '/api/analytics',
|
|
166
|
+
honorDnt: this.config.privacy?.honorDnt,
|
|
167
|
+
trackHashChanges: this.config.tracking?.trackHashChanges,
|
|
168
|
+
trackOutboundLinks: this.config.tracking?.trackOutboundLinks,
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Track a page view (server-side)
|
|
174
|
+
*/
|
|
175
|
+
async trackPageView(options: {
|
|
176
|
+
path: string
|
|
177
|
+
title?: string
|
|
178
|
+
referrer?: string
|
|
179
|
+
visitorId: string
|
|
180
|
+
sessionId: string
|
|
181
|
+
userAgent?: string
|
|
182
|
+
ip?: string
|
|
183
|
+
}): Promise<void> {
|
|
184
|
+
const deviceInfo = AnalyticsStore.parseUserAgent(options.userAgent || '')
|
|
185
|
+
const referrerSource = AnalyticsStore.parseReferrerSource(options.referrer)
|
|
186
|
+
const timestamp = new Date()
|
|
187
|
+
|
|
188
|
+
const pageView = {
|
|
189
|
+
id: AnalyticsStore.generateId(),
|
|
190
|
+
siteId: this.config.siteId,
|
|
191
|
+
visitorId: options.visitorId,
|
|
192
|
+
sessionId: options.sessionId,
|
|
193
|
+
path: options.path,
|
|
194
|
+
hostname: '',
|
|
195
|
+
title: options.title,
|
|
196
|
+
referrer: options.referrer,
|
|
197
|
+
referrerSource,
|
|
198
|
+
deviceType: deviceInfo.deviceType,
|
|
199
|
+
browser: deviceInfo.browser,
|
|
200
|
+
browserVersion: deviceInfo.browserVersion,
|
|
201
|
+
os: deviceInfo.os,
|
|
202
|
+
osVersion: deviceInfo.osVersion,
|
|
203
|
+
isUnique: true,
|
|
204
|
+
isBounce: true,
|
|
205
|
+
timestamp,
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const command = this.store.recordPageViewCommand(pageView)
|
|
209
|
+
await this.executeCommand(command)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Track a custom event (server-side)
|
|
214
|
+
*/
|
|
215
|
+
async trackEvent(options: {
|
|
216
|
+
name: string
|
|
217
|
+
value?: number
|
|
218
|
+
properties?: Record<string, string | number | boolean>
|
|
219
|
+
path: string
|
|
220
|
+
visitorId: string
|
|
221
|
+
sessionId: string
|
|
222
|
+
}): Promise<void> {
|
|
223
|
+
const event = {
|
|
224
|
+
id: AnalyticsStore.generateId(),
|
|
225
|
+
siteId: this.config.siteId,
|
|
226
|
+
visitorId: options.visitorId,
|
|
227
|
+
sessionId: options.sessionId,
|
|
228
|
+
name: options.name,
|
|
229
|
+
value: options.value,
|
|
230
|
+
properties: options.properties,
|
|
231
|
+
path: options.path,
|
|
232
|
+
timestamp: new Date(),
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const command = this.store.recordCustomEventCommand(event)
|
|
236
|
+
await this.executeCommand(command)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Get dashboard summary stats
|
|
241
|
+
*/
|
|
242
|
+
async getDashboardStats(options?: {
|
|
243
|
+
startDate?: Date
|
|
244
|
+
endDate?: Date
|
|
245
|
+
includeComparison?: boolean
|
|
246
|
+
}): Promise<{
|
|
247
|
+
summary: ReturnType<typeof AnalyticsQueryAPI.processSummary>
|
|
248
|
+
timeSeries: ReturnType<typeof AnalyticsQueryAPI.processTimeSeries>
|
|
249
|
+
}> {
|
|
250
|
+
const startDate = options?.startDate || new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
|
|
251
|
+
const endDate = options?.endDate || new Date()
|
|
252
|
+
|
|
253
|
+
const queries = this.queryApi.generateDashboardQueries({
|
|
254
|
+
siteId: this.config.siteId,
|
|
255
|
+
dateRange: { start: startDate, end: endDate },
|
|
256
|
+
includeComparison: options?.includeComparison,
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
const [statsResult, previousResult] = await Promise.all([
|
|
260
|
+
this.executeCommand(queries.aggregatedStats),
|
|
261
|
+
queries.previousPeriodStats ? this.executeCommand(queries.previousPeriodStats) : null,
|
|
262
|
+
])
|
|
263
|
+
|
|
264
|
+
const stats = (statsResult as { Items?: unknown[] })?.Items || []
|
|
265
|
+
const previousStats = previousResult
|
|
266
|
+
? (previousResult as { Items?: unknown[] })?.Items || []
|
|
267
|
+
: undefined
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
summary: AnalyticsQueryAPI.processSummary(
|
|
271
|
+
stats as Parameters<typeof AnalyticsQueryAPI.processSummary>[0],
|
|
272
|
+
previousStats as Parameters<typeof AnalyticsQueryAPI.processSummary>[1],
|
|
273
|
+
),
|
|
274
|
+
timeSeries: AnalyticsQueryAPI.processTimeSeries(
|
|
275
|
+
stats as Parameters<typeof AnalyticsQueryAPI.processTimeSeries>[0],
|
|
276
|
+
),
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Get realtime visitor count
|
|
282
|
+
*/
|
|
283
|
+
async getRealtimeVisitors(minutes = 5): Promise<{
|
|
284
|
+
currentVisitors: number
|
|
285
|
+
pageViewsLastHour: number
|
|
286
|
+
topActivePages: Array<{ name: string, value: number, percentage: number }>
|
|
287
|
+
}> {
|
|
288
|
+
const command = this.store.getRealtimeStatsCommand(this.config.siteId, minutes)
|
|
289
|
+
const result = await this.executeCommand(command) as { Items?: unknown[] }
|
|
290
|
+
|
|
291
|
+
return AnalyticsQueryAPI.processRealtimeData(
|
|
292
|
+
(result.Items || []) as Parameters<typeof AnalyticsQueryAPI.processRealtimeData>[0],
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Get top pages
|
|
298
|
+
*/
|
|
299
|
+
async getTopPages(options?: {
|
|
300
|
+
startDate?: Date
|
|
301
|
+
endDate?: Date
|
|
302
|
+
limit?: number
|
|
303
|
+
}): Promise<unknown[]> {
|
|
304
|
+
const endDate = options?.endDate || new Date()
|
|
305
|
+
const dateRange = {
|
|
306
|
+
start: options?.startDate || new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
|
|
307
|
+
end: endDate,
|
|
308
|
+
}
|
|
309
|
+
const period = AnalyticsQueryAPI.determinePeriod(dateRange)
|
|
310
|
+
const periodStart = AnalyticsStore.getPeriodStart(endDate, period)
|
|
311
|
+
|
|
312
|
+
const command = this.store.getTopPagesCommand(
|
|
313
|
+
this.config.siteId,
|
|
314
|
+
period,
|
|
315
|
+
periodStart,
|
|
316
|
+
options?.limit || 10,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
const result = await this.executeCommand(command) as { Items?: unknown[] }
|
|
320
|
+
return result.Items || []
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Run aggregation for a time period
|
|
325
|
+
*/
|
|
326
|
+
async runAggregation(period: 'hour' | 'day' | 'month'): Promise<{
|
|
327
|
+
success: boolean
|
|
328
|
+
config: ReturnType<typeof AggregationPipeline.createJobConfig>
|
|
329
|
+
}> {
|
|
330
|
+
const config = AggregationPipeline.createJobConfig(this.config.siteId, period)
|
|
331
|
+
|
|
332
|
+
// Note: In a real implementation, you would:
|
|
333
|
+
// 1. Query raw page views and sessions for the window
|
|
334
|
+
// 2. Run the aggregation job
|
|
335
|
+
// 3. Execute the generated commands
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
success: true,
|
|
339
|
+
config,
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Get the underlying API instance for advanced usage
|
|
345
|
+
*/
|
|
346
|
+
getAPI(): AnalyticsAPI {
|
|
347
|
+
return this.api
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Get the underlying store for advanced queries
|
|
352
|
+
*/
|
|
353
|
+
getStore(): AnalyticsStore {
|
|
354
|
+
return this.store
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ============================================================================
|
|
359
|
+
// Stacks Middleware
|
|
360
|
+
// ============================================================================
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Create Stacks analytics middleware
|
|
364
|
+
* Injects tracking script into HTML responses
|
|
365
|
+
*/
|
|
366
|
+
export function createAnalyticsMiddleware(driver: DynamoDBAnalyticsDriver): (ctx: {
|
|
367
|
+
request: Request
|
|
368
|
+
response?: Response
|
|
369
|
+
html?: string
|
|
370
|
+
}, next: () => Promise<void>) => Promise<void> {
|
|
371
|
+
const script = driver.getTrackingScript()
|
|
372
|
+
|
|
373
|
+
return async (ctx: {
|
|
374
|
+
request: Request
|
|
375
|
+
response?: Response
|
|
376
|
+
html?: string
|
|
377
|
+
}, next: () => Promise<void>): Promise<void> => {
|
|
378
|
+
await next()
|
|
379
|
+
|
|
380
|
+
// Inject script into HTML responses
|
|
381
|
+
if (ctx.html && ctx.html.includes('</head>')) {
|
|
382
|
+
ctx.html = ctx.html.replace('</head>', `${script}\n</head>`)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Create server-side tracking middleware
|
|
389
|
+
* Tracks page views automatically for server-rendered pages
|
|
390
|
+
*/
|
|
391
|
+
export function createServerTrackingMiddleware(
|
|
392
|
+
driver: DynamoDBAnalyticsDriver,
|
|
393
|
+
options?: {
|
|
394
|
+
excludedPaths?: RegExp[]
|
|
395
|
+
getVisitorId?: (request: Request) => string
|
|
396
|
+
getSessionId?: (request: Request) => string
|
|
397
|
+
},
|
|
398
|
+
): (ctx: { request: Request, url: URL }, next: () => Promise<void>) => Promise<void> {
|
|
399
|
+
return async (ctx: {
|
|
400
|
+
request: Request
|
|
401
|
+
url: URL
|
|
402
|
+
}, next: () => Promise<void>): Promise<void> => {
|
|
403
|
+
const { request, url } = ctx
|
|
404
|
+
|
|
405
|
+
// Check excluded paths
|
|
406
|
+
if (options?.excludedPaths?.some(pattern => pattern.test(url.pathname))) {
|
|
407
|
+
return next()
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Get or generate visitor/session IDs
|
|
411
|
+
const visitorId = options?.getVisitorId?.(request)
|
|
412
|
+
|| request.headers.get('x-visitor-id')
|
|
413
|
+
|| AnalyticsStore.generateId()
|
|
414
|
+
|
|
415
|
+
const sessionId = options?.getSessionId?.(request)
|
|
416
|
+
|| request.headers.get('x-session-id')
|
|
417
|
+
|| AnalyticsStore.generateId()
|
|
418
|
+
|
|
419
|
+
// Track page view asynchronously (don't block response)
|
|
420
|
+
driver.trackPageView({
|
|
421
|
+
path: url.pathname,
|
|
422
|
+
referrer: request.headers.get('referer') || undefined,
|
|
423
|
+
visitorId,
|
|
424
|
+
sessionId,
|
|
425
|
+
userAgent: request.headers.get('user-agent') || undefined,
|
|
426
|
+
ip: request.headers.get('x-forwarded-for') || undefined,
|
|
427
|
+
}).catch(err => console.error('Failed to track page view:', err))
|
|
428
|
+
|
|
429
|
+
return next()
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ============================================================================
|
|
434
|
+
// Stacks Actions (Dashboard API)
|
|
435
|
+
// ============================================================================
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Create Stacks dashboard actions for analytics
|
|
439
|
+
*/
|
|
440
|
+
export function createDashboardActions(driver: DynamoDBAnalyticsDriver): {
|
|
441
|
+
getDashboardStats: (params: { startDate?: string, endDate?: string, compare?: boolean }) => Promise<unknown>
|
|
442
|
+
getRealtimeStats: (params?: { minutes?: number }) => Promise<unknown>
|
|
443
|
+
getTopPages: (params?: { startDate?: string, endDate?: string, limit?: number }) => Promise<unknown>
|
|
444
|
+
runAggregation: (params: { period: 'hour' | 'day' | 'month' }) => Promise<unknown>
|
|
445
|
+
} {
|
|
446
|
+
return {
|
|
447
|
+
/**
|
|
448
|
+
* Get dashboard stats action
|
|
449
|
+
*/
|
|
450
|
+
async getDashboardStats(params: {
|
|
451
|
+
startDate?: string
|
|
452
|
+
endDate?: string
|
|
453
|
+
compare?: boolean
|
|
454
|
+
}): Promise<unknown> {
|
|
455
|
+
return driver.getDashboardStats({
|
|
456
|
+
startDate: params.startDate ? new Date(params.startDate) : undefined,
|
|
457
|
+
endDate: params.endDate ? new Date(params.endDate) : undefined,
|
|
458
|
+
includeComparison: params.compare,
|
|
459
|
+
})
|
|
460
|
+
},
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Get realtime stats action
|
|
464
|
+
*/
|
|
465
|
+
async getRealtimeStats(params?: { minutes?: number }): Promise<unknown> {
|
|
466
|
+
return driver.getRealtimeVisitors(params?.minutes)
|
|
467
|
+
},
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Get top pages action
|
|
471
|
+
*/
|
|
472
|
+
async getTopPages(params?: {
|
|
473
|
+
startDate?: string
|
|
474
|
+
endDate?: string
|
|
475
|
+
limit?: number
|
|
476
|
+
}): Promise<unknown> {
|
|
477
|
+
return driver.getTopPages({
|
|
478
|
+
startDate: params?.startDate ? new Date(params.startDate) : undefined,
|
|
479
|
+
endDate: params?.endDate ? new Date(params.endDate) : undefined,
|
|
480
|
+
limit: params?.limit,
|
|
481
|
+
})
|
|
482
|
+
},
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Trigger aggregation action
|
|
486
|
+
*/
|
|
487
|
+
async runAggregation(params: { period: 'hour' | 'day' | 'month' }): Promise<unknown> {
|
|
488
|
+
return driver.runAggregation(params.period)
|
|
489
|
+
},
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ============================================================================
|
|
494
|
+
// Factory Function
|
|
495
|
+
// ============================================================================
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Create a DynamoDB analytics driver from Stacks config
|
|
499
|
+
*
|
|
500
|
+
* @example
|
|
501
|
+
* ```typescript
|
|
502
|
+
* import { createAnalyticsDriver } from '@stacksjs/dynamodb-tooling'
|
|
503
|
+
* import config from './config/analytics'
|
|
504
|
+
* import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
|
|
505
|
+
*
|
|
506
|
+
* const client = new DynamoDBClient({ region: 'us-east-1' })
|
|
507
|
+
* const driver = createAnalyticsDriver(config.drivers.dynamodb!, async (cmd) => {
|
|
508
|
+
* // Execute DynamoDB command
|
|
509
|
+
* return client.send(new cmd.command(cmd.input))
|
|
510
|
+
* })
|
|
511
|
+
* ```
|
|
512
|
+
*/
|
|
513
|
+
export function createAnalyticsDriver(
|
|
514
|
+
config: DynamoDBAnalyticsConfig,
|
|
515
|
+
executeCommand: HandlerContext['executeCommand'],
|
|
516
|
+
): DynamoDBAnalyticsDriver {
|
|
517
|
+
return new DynamoDBAnalyticsDriver(config, executeCommand)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ============================================================================
|
|
521
|
+
// Type Exports for Stacks Integration
|
|
522
|
+
// ============================================================================
|
|
523
|
+
|
|
524
|
+
export type {
|
|
525
|
+
AnalyticsAPIConfig,
|
|
526
|
+
HandlerContext,
|
|
527
|
+
}
|