@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/batching.ts
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event Batching Utility
|
|
3
|
+
*
|
|
4
|
+
* High-performance event batching for handling large volumes of analytics events.
|
|
5
|
+
* Uses DynamoDB BatchWriteItem for efficient bulk writes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CustomEvent, PageView, Session } from './types'
|
|
9
|
+
import { getConfig } from './config'
|
|
10
|
+
import { generateId, KeyPatterns } from './dynamodb'
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Types
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
export interface BatchItem {
|
|
17
|
+
type: 'pageview' | 'session' | 'event'
|
|
18
|
+
data: PageView | Session | CustomEvent
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface BatchWriteResult {
|
|
22
|
+
successful: number
|
|
23
|
+
failed: number
|
|
24
|
+
unprocessedItems: BatchItem[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface BatchQueueOptions {
|
|
28
|
+
/** Maximum items per batch (DynamoDB limit is 25) */
|
|
29
|
+
maxBatchSize?: number
|
|
30
|
+
/** Flush interval in milliseconds */
|
|
31
|
+
flushIntervalMs?: number
|
|
32
|
+
/** Maximum queue size before auto-flush */
|
|
33
|
+
maxQueueSize?: number
|
|
34
|
+
/** Callback for batch write errors */
|
|
35
|
+
onError?: (error: Error, items: BatchItem[]) => void
|
|
36
|
+
/** Callback when batch is flushed */
|
|
37
|
+
onFlush?: (result: BatchWriteResult) => void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface DynamoDBBatchClient {
|
|
41
|
+
batchWriteItem: (input: BatchWriteInput) => Promise<BatchWriteOutput>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface BatchWriteInput {
|
|
45
|
+
RequestItems: Record<string, WriteRequest[]>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface BatchWriteOutput {
|
|
49
|
+
UnprocessedItems?: Record<string, WriteRequest[]>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface WriteRequest {
|
|
53
|
+
PutRequest?: {
|
|
54
|
+
Item: Record<string, AttributeValue>
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface AttributeValue {
|
|
59
|
+
S?: string
|
|
60
|
+
N?: string
|
|
61
|
+
BOOL?: boolean
|
|
62
|
+
L?: AttributeValue[]
|
|
63
|
+
M?: Record<string, AttributeValue>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// Event Batch Queue
|
|
68
|
+
// ============================================================================
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* High-performance event batch queue
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* const queue = new EventBatchQueue(dynamoClient, {
|
|
76
|
+
* maxBatchSize: 25,
|
|
77
|
+
* flushIntervalMs: 5000,
|
|
78
|
+
* onFlush: (result) => console.log(`Flushed ${result.successful} items`),
|
|
79
|
+
* })
|
|
80
|
+
*
|
|
81
|
+
* // Add events (auto-batched)
|
|
82
|
+
* queue.addPageView(pageView)
|
|
83
|
+
* queue.addSession(session)
|
|
84
|
+
*
|
|
85
|
+
* // Manual flush if needed
|
|
86
|
+
* await queue.flush()
|
|
87
|
+
*
|
|
88
|
+
* // Shutdown gracefully
|
|
89
|
+
* await queue.close()
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export class EventBatchQueue {
|
|
93
|
+
private queue: BatchItem[] = []
|
|
94
|
+
private flushTimer: ReturnType<typeof setInterval> | null = null
|
|
95
|
+
private isClosing = false
|
|
96
|
+
private flushPromise: Promise<void> | null = null
|
|
97
|
+
|
|
98
|
+
private readonly client: DynamoDBBatchClient
|
|
99
|
+
private readonly tableName: string
|
|
100
|
+
private readonly maxBatchSize: number
|
|
101
|
+
private readonly flushIntervalMs: number
|
|
102
|
+
private readonly maxQueueSize: number
|
|
103
|
+
private readonly onError?: (error: Error, items: BatchItem[]) => void
|
|
104
|
+
private readonly onFlush?: (result: BatchWriteResult) => void
|
|
105
|
+
|
|
106
|
+
constructor(client: DynamoDBBatchClient, options: BatchQueueOptions = {}) {
|
|
107
|
+
this.client = client
|
|
108
|
+
this.tableName = getConfig().table.tableName
|
|
109
|
+
this.maxBatchSize = Math.min(options.maxBatchSize ?? 25, 25) // DynamoDB limit
|
|
110
|
+
this.flushIntervalMs = options.flushIntervalMs ?? 5000
|
|
111
|
+
this.maxQueueSize = options.maxQueueSize ?? 100
|
|
112
|
+
this.onError = options.onError
|
|
113
|
+
this.onFlush = options.onFlush
|
|
114
|
+
|
|
115
|
+
// Start periodic flush
|
|
116
|
+
this.startFlushTimer()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Add a page view to the queue
|
|
121
|
+
*/
|
|
122
|
+
addPageView(pageView: PageView): void {
|
|
123
|
+
this.add({ type: 'pageview', data: pageView })
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Add a session to the queue
|
|
128
|
+
*/
|
|
129
|
+
addSession(session: Session): void {
|
|
130
|
+
this.add({ type: 'session', data: session })
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Add a custom event to the queue
|
|
135
|
+
*/
|
|
136
|
+
addEvent(event: CustomEvent): void {
|
|
137
|
+
this.add({ type: 'event', data: event })
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Add a batch item to the queue
|
|
142
|
+
*/
|
|
143
|
+
add(item: BatchItem): void {
|
|
144
|
+
if (this.isClosing) {
|
|
145
|
+
throw new Error('Cannot add items to a closing queue')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.queue.push(item)
|
|
149
|
+
|
|
150
|
+
// Auto-flush if queue is full
|
|
151
|
+
if (this.queue.length >= this.maxQueueSize) {
|
|
152
|
+
this.flush()
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Flush all queued items to DynamoDB
|
|
158
|
+
*/
|
|
159
|
+
async flush(): Promise<BatchWriteResult> {
|
|
160
|
+
// Wait for any in-progress flush
|
|
161
|
+
if (this.flushPromise) {
|
|
162
|
+
await this.flushPromise
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (this.queue.length === 0) {
|
|
166
|
+
return { successful: 0, failed: 0, unprocessedItems: [] }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const itemsToFlush = this.queue.splice(0, this.queue.length)
|
|
170
|
+
const totalResult: BatchWriteResult = { successful: 0, failed: 0, unprocessedItems: [] }
|
|
171
|
+
|
|
172
|
+
this.flushPromise = (async () => {
|
|
173
|
+
// Process in batches of maxBatchSize
|
|
174
|
+
for (let i = 0; i < itemsToFlush.length; i += this.maxBatchSize) {
|
|
175
|
+
const batch = itemsToFlush.slice(i, i + this.maxBatchSize)
|
|
176
|
+
const result = await this.writeBatch(batch)
|
|
177
|
+
|
|
178
|
+
totalResult.successful += result.successful
|
|
179
|
+
totalResult.failed += result.failed
|
|
180
|
+
totalResult.unprocessedItems.push(...result.unprocessedItems)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
this.onFlush?.(totalResult)
|
|
184
|
+
})()
|
|
185
|
+
|
|
186
|
+
await this.flushPromise
|
|
187
|
+
this.flushPromise = null
|
|
188
|
+
|
|
189
|
+
return totalResult
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Close the queue and flush remaining items
|
|
194
|
+
*/
|
|
195
|
+
async close(): Promise<void> {
|
|
196
|
+
this.isClosing = true
|
|
197
|
+
this.stopFlushTimer()
|
|
198
|
+
await this.flush()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get current queue size
|
|
203
|
+
*/
|
|
204
|
+
get size(): number {
|
|
205
|
+
return this.queue.length
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private startFlushTimer(): void {
|
|
209
|
+
this.flushTimer = setInterval(() => {
|
|
210
|
+
if (this.queue.length > 0) {
|
|
211
|
+
this.flush()
|
|
212
|
+
}
|
|
213
|
+
}, this.flushIntervalMs)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private stopFlushTimer(): void {
|
|
217
|
+
if (this.flushTimer) {
|
|
218
|
+
clearInterval(this.flushTimer)
|
|
219
|
+
this.flushTimer = null
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private async writeBatch(items: BatchItem[]): Promise<BatchWriteResult> {
|
|
224
|
+
const writeRequests = items.map(item => this.toWriteRequest(item))
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const result = await this.client.batchWriteItem({
|
|
228
|
+
RequestItems: {
|
|
229
|
+
[this.tableName]: writeRequests,
|
|
230
|
+
},
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
const unprocessedCount = result.UnprocessedItems?.[this.tableName]?.length ?? 0
|
|
234
|
+
const successCount = items.length - unprocessedCount
|
|
235
|
+
|
|
236
|
+
// Convert unprocessed items back to BatchItem format
|
|
237
|
+
const unprocessedItems: BatchItem[] = []
|
|
238
|
+
if (unprocessedCount > 0) {
|
|
239
|
+
// Note: DynamoDB doesn't preserve order, so we return all as failed
|
|
240
|
+
unprocessedItems.push(...items.slice(-unprocessedCount))
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
successful: successCount,
|
|
245
|
+
failed: unprocessedCount,
|
|
246
|
+
unprocessedItems,
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
this.onError?.(error as Error, items)
|
|
251
|
+
return {
|
|
252
|
+
successful: 0,
|
|
253
|
+
failed: items.length,
|
|
254
|
+
unprocessedItems: items,
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private toWriteRequest(item: BatchItem): WriteRequest {
|
|
260
|
+
switch (item.type) {
|
|
261
|
+
case 'pageview':
|
|
262
|
+
return { PutRequest: { Item: this.marshalPageView(item.data as PageView) } }
|
|
263
|
+
case 'session':
|
|
264
|
+
return { PutRequest: { Item: this.marshalSession(item.data as Session) } }
|
|
265
|
+
case 'event':
|
|
266
|
+
return { PutRequest: { Item: this.marshalCustomEvent(item.data as CustomEvent) } }
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private marshalPageView(pv: PageView): Record<string, AttributeValue> {
|
|
271
|
+
return {
|
|
272
|
+
pk: { S: KeyPatterns.pageView.pk(pv.siteId) },
|
|
273
|
+
sk: { S: KeyPatterns.pageView.sk(pv.timestamp, pv.id) },
|
|
274
|
+
id: { S: pv.id },
|
|
275
|
+
siteId: { S: pv.siteId },
|
|
276
|
+
visitorId: { S: pv.visitorId },
|
|
277
|
+
sessionId: { S: pv.sessionId },
|
|
278
|
+
path: { S: pv.path },
|
|
279
|
+
hostname: { S: pv.hostname },
|
|
280
|
+
...(pv.title && { title: { S: pv.title } }),
|
|
281
|
+
...(pv.referrer && { referrer: { S: pv.referrer } }),
|
|
282
|
+
...(pv.referrerSource && { referrerSource: { S: pv.referrerSource } }),
|
|
283
|
+
...(pv.deviceType && { deviceType: { S: pv.deviceType } }),
|
|
284
|
+
...(pv.browser && { browser: { S: pv.browser } }),
|
|
285
|
+
...(pv.os && { os: { S: pv.os } }),
|
|
286
|
+
isUnique: { BOOL: pv.isUnique },
|
|
287
|
+
isBounce: { BOOL: pv.isBounce },
|
|
288
|
+
timestamp: { S: pv.timestamp.toISOString() },
|
|
289
|
+
entityType: { S: 'pageview' },
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private marshalSession(s: Session): Record<string, AttributeValue> {
|
|
294
|
+
return {
|
|
295
|
+
pk: { S: KeyPatterns.session.pk(s.siteId) },
|
|
296
|
+
sk: { S: KeyPatterns.session.sk(s.id) },
|
|
297
|
+
id: { S: s.id },
|
|
298
|
+
siteId: { S: s.siteId },
|
|
299
|
+
visitorId: { S: s.visitorId },
|
|
300
|
+
entryPath: { S: s.entryPath },
|
|
301
|
+
exitPath: { S: s.exitPath },
|
|
302
|
+
...(s.referrer && { referrer: { S: s.referrer } }),
|
|
303
|
+
...(s.referrerSource && { referrerSource: { S: s.referrerSource } }),
|
|
304
|
+
...(s.deviceType && { deviceType: { S: s.deviceType } }),
|
|
305
|
+
...(s.browser && { browser: { S: s.browser } }),
|
|
306
|
+
...(s.os && { os: { S: s.os } }),
|
|
307
|
+
pageViewCount: { N: String(s.pageViewCount) },
|
|
308
|
+
eventCount: { N: String(s.eventCount) },
|
|
309
|
+
isBounce: { BOOL: s.isBounce },
|
|
310
|
+
duration: { N: String(s.duration) },
|
|
311
|
+
startedAt: { S: s.startedAt.toISOString() },
|
|
312
|
+
endedAt: { S: s.endedAt.toISOString() },
|
|
313
|
+
entityType: { S: 'session' },
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private marshalCustomEvent(e: CustomEvent): Record<string, AttributeValue> {
|
|
318
|
+
return {
|
|
319
|
+
pk: { S: KeyPatterns.event.pk(e.siteId) },
|
|
320
|
+
sk: { S: KeyPatterns.event.sk(e.timestamp, e.id) },
|
|
321
|
+
id: { S: e.id },
|
|
322
|
+
siteId: { S: e.siteId },
|
|
323
|
+
visitorId: { S: e.visitorId },
|
|
324
|
+
sessionId: { S: e.sessionId },
|
|
325
|
+
name: { S: e.name },
|
|
326
|
+
...(e.category && { category: { S: e.category } }),
|
|
327
|
+
...(e.value !== undefined && { value: { N: String(e.value) } }),
|
|
328
|
+
...(e.properties && { properties: { S: JSON.stringify(e.properties) } }),
|
|
329
|
+
path: { S: e.path },
|
|
330
|
+
timestamp: { S: e.timestamp.toISOString() },
|
|
331
|
+
entityType: { S: 'event' },
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ============================================================================
|
|
337
|
+
// Batch Processing Utilities
|
|
338
|
+
// ============================================================================
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Create a batch processor for processing events in bulk
|
|
342
|
+
*/
|
|
343
|
+
export function createBatchProcessor<T>(
|
|
344
|
+
processor: (batch: T[]) => Promise<void>,
|
|
345
|
+
options: { batchSize?: number, delayMs?: number } = {},
|
|
346
|
+
): {
|
|
347
|
+
add: (item: T) => void
|
|
348
|
+
flush: () => Promise<void>
|
|
349
|
+
size: () => number
|
|
350
|
+
} {
|
|
351
|
+
const batchSize = options.batchSize ?? 100
|
|
352
|
+
const delayMs = options.delayMs ?? 0
|
|
353
|
+
let queue: T[] = []
|
|
354
|
+
let processing = false
|
|
355
|
+
|
|
356
|
+
async function processQueue(): Promise<void> {
|
|
357
|
+
if (processing || queue.length === 0) return
|
|
358
|
+
processing = true
|
|
359
|
+
|
|
360
|
+
while (queue.length > 0) {
|
|
361
|
+
const batch = queue.splice(0, batchSize)
|
|
362
|
+
|
|
363
|
+
if (delayMs > 0) {
|
|
364
|
+
await new Promise(resolve => setTimeout(resolve, delayMs))
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
await processor(batch)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
processing = false
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
add(item: T): void {
|
|
375
|
+
queue.push(item)
|
|
376
|
+
if (queue.length >= batchSize) {
|
|
377
|
+
processQueue()
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
async flush(): Promise<void> {
|
|
381
|
+
await processQueue()
|
|
382
|
+
},
|
|
383
|
+
size(): number {
|
|
384
|
+
return queue.length
|
|
385
|
+
},
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Retry helper with exponential backoff
|
|
391
|
+
*/
|
|
392
|
+
export async function withRetry<T>(
|
|
393
|
+
fn: () => Promise<T>,
|
|
394
|
+
options: { maxRetries?: number, baseDelayMs?: number } = {},
|
|
395
|
+
): Promise<T> {
|
|
396
|
+
const maxRetries = options.maxRetries ?? 3
|
|
397
|
+
const baseDelayMs = options.baseDelayMs ?? 100
|
|
398
|
+
let lastError: Error | undefined
|
|
399
|
+
|
|
400
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
401
|
+
try {
|
|
402
|
+
return await fn()
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
lastError = error as Error
|
|
406
|
+
|
|
407
|
+
if (attempt < maxRetries - 1) {
|
|
408
|
+
const delay = baseDelayMs * 2 ** attempt
|
|
409
|
+
await new Promise(resolve => setTimeout(resolve, delay))
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
throw lastError
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Chunk an array into smaller arrays
|
|
419
|
+
*/
|
|
420
|
+
export function chunk<T>(array: T[], size: number): T[][] {
|
|
421
|
+
const chunks: T[][] = []
|
|
422
|
+
for (let i = 0; i < array.length; i += size) {
|
|
423
|
+
chunks.push(array.slice(i, i + size))
|
|
424
|
+
}
|
|
425
|
+
return chunks
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Process items in parallel with concurrency limit
|
|
430
|
+
*/
|
|
431
|
+
export async function parallelProcess<T, R>(
|
|
432
|
+
items: T[],
|
|
433
|
+
processor: (item: T) => Promise<R>,
|
|
434
|
+
concurrency: number = 5,
|
|
435
|
+
): Promise<R[]> {
|
|
436
|
+
const results: R[] = []
|
|
437
|
+
const queue = [...items]
|
|
438
|
+
|
|
439
|
+
async function worker(): Promise<void> {
|
|
440
|
+
while (queue.length > 0) {
|
|
441
|
+
const item = queue.shift()
|
|
442
|
+
if (item !== undefined) {
|
|
443
|
+
results.push(await processor(item))
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker())
|
|
449
|
+
await Promise.all(workers)
|
|
450
|
+
|
|
451
|
+
return results
|
|
452
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard Components Index
|
|
3
|
+
*
|
|
4
|
+
* Re-exports all dashboard components for easy importing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export * from '../../../components/dashboard/SiteSelector.stx'
|
|
8
|
+
export * from '../../../components/dashboard/DashboardHeader.stx'
|
|
9
|
+
export * from '../../../components/dashboard/ControlsBar.stx'
|
|
10
|
+
export * from '../../../components/dashboard/FiltersBar.stx'
|
|
11
|
+
export * from '../../../components/dashboard/StatsRow.stx'
|
|
12
|
+
export * from '../../../components/dashboard/ChartSection.stx'
|
|
13
|
+
export * from '../../../components/dashboard/DataPanel.stx'
|
|
14
|
+
export * from '../../../components/dashboard/NoDataMessage.stx'
|
|
15
|
+
export * from '../../../components/dashboard/EventsSection.stx'
|
|
16
|
+
export * from '../../../components/dashboard/GoalsSection.stx'
|
|
17
|
+
export * from '../../../components/dashboard/GoalModal.stx'
|
|
18
|
+
export * from '../../../components/dashboard/DashboardStyles.stx'
|