@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,806 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQS-Based Write Buffering
|
|
3
|
+
*
|
|
4
|
+
* High-throughput event ingestion using SQS as a buffer between
|
|
5
|
+
* the /collect endpoint and DynamoDB writes. This architecture:
|
|
6
|
+
*
|
|
7
|
+
* 1. Handles traffic spikes by queuing events
|
|
8
|
+
* 2. Enables batch writes to DynamoDB (25 items per batch)
|
|
9
|
+
* 3. Provides automatic retry with exponential backoff
|
|
10
|
+
* 4. Reduces Lambda execution time for /collect (faster responses)
|
|
11
|
+
* 5. Decouples ingestion from storage
|
|
12
|
+
*
|
|
13
|
+
* Architecture:
|
|
14
|
+
* [Browser] -> [Lambda /collect] -> [SQS Queue] -> [Lambda Consumer] -> [DynamoDB]
|
|
15
|
+
*
|
|
16
|
+
* Based on learnings from Fathom Analytics about scaling DynamoDB:
|
|
17
|
+
* - Avoid per-request connection overhead
|
|
18
|
+
* - Use batching for efficient writes
|
|
19
|
+
* - Buffer writes during traffic spikes
|
|
20
|
+
*
|
|
21
|
+
* Extends ts-cloud for infrastructure and AWS clients.
|
|
22
|
+
* @see cloud.config.ts for infrastructure definitions
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { CustomEvent, PageView, Session } from './types'
|
|
26
|
+
|
|
27
|
+
// Re-export ts-cloud compatible types for convenience
|
|
28
|
+
export interface SQSRawMessage {
|
|
29
|
+
MessageId: string
|
|
30
|
+
ReceiptHandle: string
|
|
31
|
+
Body: string
|
|
32
|
+
Attributes?: Record<string, string>
|
|
33
|
+
MessageAttributes?: Record<string, unknown>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface QueueAttributes {
|
|
37
|
+
QueueUrl: string
|
|
38
|
+
QueueArn?: string
|
|
39
|
+
ApproximateNumberOfMessages?: string
|
|
40
|
+
ApproximateNumberOfMessagesNotVisible?: string
|
|
41
|
+
VisibilityTimeout?: string
|
|
42
|
+
MessageRetentionPeriod?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// Types
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
export interface SQSConfig {
|
|
50
|
+
/** SQS Queue URL for event buffering */
|
|
51
|
+
queueUrl?: string
|
|
52
|
+
/** Enable SQS buffering (default: false for backward compatibility) */
|
|
53
|
+
enabled: boolean
|
|
54
|
+
/** Batch size for SQS messages (max 10) */
|
|
55
|
+
batchSize: number
|
|
56
|
+
/** Maximum messages per Lambda invocation */
|
|
57
|
+
maxMessagesPerInvocation: number
|
|
58
|
+
/** Visibility timeout in seconds */
|
|
59
|
+
visibilityTimeout: number
|
|
60
|
+
/** Message retention in seconds (max 14 days) */
|
|
61
|
+
messageRetention: number
|
|
62
|
+
/** Dead letter queue URL for failed messages */
|
|
63
|
+
deadLetterQueueUrl?: string
|
|
64
|
+
/** Max receive count before sending to DLQ */
|
|
65
|
+
maxReceiveCount: number
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface AnalyticsEvent {
|
|
69
|
+
type: 'pageview' | 'session' | 'event' | 'realtime'
|
|
70
|
+
siteId: string
|
|
71
|
+
timestamp: string
|
|
72
|
+
data: PageView | Session | CustomEvent | RealtimeUpdate
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface RealtimeUpdate {
|
|
76
|
+
siteId: string
|
|
77
|
+
minute: string
|
|
78
|
+
path: string
|
|
79
|
+
visitorId: string
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface SQSMessage {
|
|
83
|
+
events: AnalyticsEvent[]
|
|
84
|
+
batchId: string
|
|
85
|
+
timestamp: string
|
|
86
|
+
retryCount?: number
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface SQSSendResult {
|
|
90
|
+
successful: number
|
|
91
|
+
failed: number
|
|
92
|
+
messageId?: string
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface SQSClient {
|
|
96
|
+
sendMessage: (input: SQSSendMessageInput) => Promise<SQSSendMessageOutput>
|
|
97
|
+
sendMessageBatch: (input: SQSSendMessageBatchInput) => Promise<SQSSendMessageBatchOutput>
|
|
98
|
+
receiveMessage: (input: SQSReceiveMessageInput) => Promise<SQSReceiveMessageOutput>
|
|
99
|
+
deleteMessage: (input: SQSDeleteMessageInput) => Promise<void>
|
|
100
|
+
deleteMessageBatch: (input: SQSDeleteMessageBatchInput) => Promise<SQSDeleteMessageBatchOutput>
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface SQSSendMessageInput {
|
|
104
|
+
QueueUrl: string
|
|
105
|
+
MessageBody: string
|
|
106
|
+
MessageGroupId?: string
|
|
107
|
+
MessageDeduplicationId?: string
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface SQSSendMessageOutput {
|
|
111
|
+
MessageId?: string
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface SQSSendMessageBatchInput {
|
|
115
|
+
QueueUrl: string
|
|
116
|
+
Entries: Array<{
|
|
117
|
+
Id: string
|
|
118
|
+
MessageBody: string
|
|
119
|
+
MessageGroupId?: string
|
|
120
|
+
MessageDeduplicationId?: string
|
|
121
|
+
}>
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface SQSSendMessageBatchOutput {
|
|
125
|
+
Successful?: Array<{ Id: string, MessageId: string }>
|
|
126
|
+
Failed?: Array<{ Id: string, Code: string, Message: string }>
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface SQSReceiveMessageInput {
|
|
130
|
+
QueueUrl: string
|
|
131
|
+
MaxNumberOfMessages?: number
|
|
132
|
+
VisibilityTimeout?: number
|
|
133
|
+
WaitTimeSeconds?: number
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface SQSReceiveMessageOutput {
|
|
137
|
+
Messages?: Array<{
|
|
138
|
+
MessageId: string
|
|
139
|
+
ReceiptHandle: string
|
|
140
|
+
Body: string
|
|
141
|
+
}>
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
interface SQSDeleteMessageInput {
|
|
145
|
+
QueueUrl: string
|
|
146
|
+
ReceiptHandle: string
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
interface SQSDeleteMessageBatchInput {
|
|
150
|
+
QueueUrl: string
|
|
151
|
+
Entries: Array<{
|
|
152
|
+
Id: string
|
|
153
|
+
ReceiptHandle: string
|
|
154
|
+
}>
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
interface SQSDeleteMessageBatchOutput {
|
|
158
|
+
Successful?: Array<{ Id: string }>
|
|
159
|
+
Failed?: Array<{ Id: string, Code: string }>
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ============================================================================
|
|
163
|
+
// Default Configuration
|
|
164
|
+
// ============================================================================
|
|
165
|
+
|
|
166
|
+
export const defaultSQSConfig: SQSConfig = {
|
|
167
|
+
enabled: false,
|
|
168
|
+
batchSize: 10, // SQS max batch size
|
|
169
|
+
maxMessagesPerInvocation: 100,
|
|
170
|
+
visibilityTimeout: 30,
|
|
171
|
+
messageRetention: 86400, // 1 day
|
|
172
|
+
maxReceiveCount: 3,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ============================================================================
|
|
176
|
+
// Event Buffer (in-memory for Lambda warm starts)
|
|
177
|
+
// ============================================================================
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* In-memory event buffer for batching before SQS send
|
|
181
|
+
*/
|
|
182
|
+
export class EventBuffer {
|
|
183
|
+
private buffer: AnalyticsEvent[] = []
|
|
184
|
+
private flushTimer: ReturnType<typeof setTimeout> | null = null
|
|
185
|
+
private readonly maxSize: number
|
|
186
|
+
private readonly flushIntervalMs: number
|
|
187
|
+
private readonly onFlush: (events: AnalyticsEvent[]) => Promise<void>
|
|
188
|
+
|
|
189
|
+
constructor(options: {
|
|
190
|
+
maxSize?: number
|
|
191
|
+
flushIntervalMs?: number
|
|
192
|
+
onFlush: (events: AnalyticsEvent[]) => Promise<void>
|
|
193
|
+
}) {
|
|
194
|
+
this.maxSize = options.maxSize ?? 100
|
|
195
|
+
this.flushIntervalMs = options.flushIntervalMs ?? 1000
|
|
196
|
+
this.onFlush = options.onFlush
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
add(event: AnalyticsEvent): void {
|
|
200
|
+
this.buffer.push(event)
|
|
201
|
+
|
|
202
|
+
if (this.buffer.length >= this.maxSize) {
|
|
203
|
+
this.flush()
|
|
204
|
+
}
|
|
205
|
+
else if (!this.flushTimer) {
|
|
206
|
+
this.flushTimer = setTimeout(() => this.flush(), this.flushIntervalMs)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async flush(): Promise<void> {
|
|
211
|
+
if (this.flushTimer) {
|
|
212
|
+
clearTimeout(this.flushTimer)
|
|
213
|
+
this.flushTimer = null
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (this.buffer.length === 0) return
|
|
217
|
+
|
|
218
|
+
const events = this.buffer.splice(0, this.buffer.length)
|
|
219
|
+
await this.onFlush(events)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
get size(): number {
|
|
223
|
+
return this.buffer.length
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ============================================================================
|
|
228
|
+
// SQS Event Producer
|
|
229
|
+
// ============================================================================
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Sends analytics events to SQS queue
|
|
233
|
+
*/
|
|
234
|
+
export class SQSEventProducer {
|
|
235
|
+
private readonly client: SQSClient
|
|
236
|
+
private readonly queueUrl: string
|
|
237
|
+
private readonly batchSize: number
|
|
238
|
+
|
|
239
|
+
constructor(client: SQSClient, queueUrl: string, batchSize: number = 10) {
|
|
240
|
+
this.client = client
|
|
241
|
+
this.queueUrl = queueUrl
|
|
242
|
+
this.batchSize = Math.min(batchSize, 10) // SQS limit
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Send a single event to SQS
|
|
247
|
+
*/
|
|
248
|
+
async sendEvent(event: AnalyticsEvent): Promise<SQSSendResult> {
|
|
249
|
+
return this.sendEvents([event])
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Send multiple events to SQS (batched)
|
|
254
|
+
*/
|
|
255
|
+
async sendEvents(events: AnalyticsEvent[]): Promise<SQSSendResult> {
|
|
256
|
+
if (events.length === 0) {
|
|
257
|
+
return { successful: 0, failed: 0 }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const message: SQSMessage = {
|
|
261
|
+
events,
|
|
262
|
+
batchId: generateBatchId(),
|
|
263
|
+
timestamp: new Date().toISOString(),
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const result = await this.client.sendMessage({
|
|
268
|
+
QueueUrl: this.queueUrl,
|
|
269
|
+
MessageBody: JSON.stringify(message),
|
|
270
|
+
// Use FIFO queue with site-based grouping for ordering
|
|
271
|
+
MessageGroupId: events[0]?.siteId || 'default',
|
|
272
|
+
MessageDeduplicationId: message.batchId,
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
successful: events.length,
|
|
277
|
+
failed: 0,
|
|
278
|
+
messageId: result.MessageId,
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
console.error('[SQS] Failed to send events:', error)
|
|
283
|
+
return {
|
|
284
|
+
successful: 0,
|
|
285
|
+
failed: events.length,
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Send events in batches using SQS batch API
|
|
292
|
+
*/
|
|
293
|
+
async sendEventsBatch(events: AnalyticsEvent[]): Promise<SQSSendResult> {
|
|
294
|
+
let successful = 0
|
|
295
|
+
let failed = 0
|
|
296
|
+
|
|
297
|
+
// Group events by site for better batching
|
|
298
|
+
const bySite = groupEventsBySite(events)
|
|
299
|
+
|
|
300
|
+
for (const [siteId, siteEvents] of Object.entries(bySite)) {
|
|
301
|
+
// Chunk into SQS batch size
|
|
302
|
+
for (let i = 0; i < siteEvents.length; i += this.batchSize) {
|
|
303
|
+
const chunk = siteEvents.slice(i, i + this.batchSize)
|
|
304
|
+
const entries = chunk.map((event, idx) => ({
|
|
305
|
+
Id: `${i + idx}`,
|
|
306
|
+
MessageBody: JSON.stringify({
|
|
307
|
+
events: [event],
|
|
308
|
+
batchId: generateBatchId(),
|
|
309
|
+
timestamp: new Date().toISOString(),
|
|
310
|
+
} as SQSMessage),
|
|
311
|
+
MessageGroupId: siteId,
|
|
312
|
+
MessageDeduplicationId: generateBatchId(),
|
|
313
|
+
}))
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
const result = await this.client.sendMessageBatch({
|
|
317
|
+
QueueUrl: this.queueUrl,
|
|
318
|
+
Entries: entries,
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
successful += result.Successful?.length ?? 0
|
|
322
|
+
failed += result.Failed?.length ?? 0
|
|
323
|
+
|
|
324
|
+
if (result.Failed && result.Failed.length > 0) {
|
|
325
|
+
console.error('[SQS] Batch send failures:', result.Failed)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
console.error('[SQS] Batch send error:', error)
|
|
330
|
+
failed += chunk.length
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return { successful, failed }
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ============================================================================
|
|
340
|
+
// SQS Event Consumer
|
|
341
|
+
// ============================================================================
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Processes analytics events from SQS queue
|
|
345
|
+
*/
|
|
346
|
+
export class SQSEventConsumer {
|
|
347
|
+
private readonly client: SQSClient
|
|
348
|
+
private readonly queueUrl: string
|
|
349
|
+
private readonly processor: EventProcessor
|
|
350
|
+
|
|
351
|
+
constructor(
|
|
352
|
+
client: SQSClient,
|
|
353
|
+
queueUrl: string,
|
|
354
|
+
processor: EventProcessor,
|
|
355
|
+
) {
|
|
356
|
+
this.client = client
|
|
357
|
+
this.queueUrl = queueUrl
|
|
358
|
+
this.processor = processor
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Process a batch of SQS messages (called by Lambda trigger)
|
|
363
|
+
*/
|
|
364
|
+
async processBatch(messages: SQSLambdaMessage[]): Promise<ProcessResult> {
|
|
365
|
+
const results: ProcessResult = {
|
|
366
|
+
successful: 0,
|
|
367
|
+
failed: 0,
|
|
368
|
+
errors: [],
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
for (const message of messages) {
|
|
372
|
+
try {
|
|
373
|
+
const sqsMessage: SQSMessage = JSON.parse(message.body)
|
|
374
|
+
await this.processor.processEvents(sqsMessage.events)
|
|
375
|
+
results.successful++
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
console.error('[SQS Consumer] Failed to process message:', error)
|
|
379
|
+
results.failed++
|
|
380
|
+
results.errors.push({
|
|
381
|
+
messageId: message.messageId,
|
|
382
|
+
error: error instanceof Error ? error.message : String(error),
|
|
383
|
+
})
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return results
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Poll for messages (for non-Lambda environments)
|
|
392
|
+
*/
|
|
393
|
+
async poll(maxMessages: number = 10): Promise<ProcessResult> {
|
|
394
|
+
const results: ProcessResult = {
|
|
395
|
+
successful: 0,
|
|
396
|
+
failed: 0,
|
|
397
|
+
errors: [],
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
const response = await this.client.receiveMessage({
|
|
402
|
+
QueueUrl: this.queueUrl,
|
|
403
|
+
MaxNumberOfMessages: Math.min(maxMessages, 10),
|
|
404
|
+
VisibilityTimeout: 30,
|
|
405
|
+
WaitTimeSeconds: 20, // Long polling
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
if (!response.Messages || response.Messages.length === 0) {
|
|
409
|
+
return results
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const deleteEntries: Array<{ Id: string, ReceiptHandle: string }> = []
|
|
413
|
+
|
|
414
|
+
for (const message of response.Messages) {
|
|
415
|
+
try {
|
|
416
|
+
const sqsMessage: SQSMessage = JSON.parse(message.Body)
|
|
417
|
+
await this.processor.processEvents(sqsMessage.events)
|
|
418
|
+
results.successful++
|
|
419
|
+
deleteEntries.push({
|
|
420
|
+
Id: message.MessageId,
|
|
421
|
+
ReceiptHandle: message.ReceiptHandle,
|
|
422
|
+
})
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
console.error('[SQS Consumer] Processing error:', error)
|
|
426
|
+
results.failed++
|
|
427
|
+
results.errors.push({
|
|
428
|
+
messageId: message.MessageId,
|
|
429
|
+
error: error instanceof Error ? error.message : String(error),
|
|
430
|
+
})
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Delete successfully processed messages
|
|
435
|
+
if (deleteEntries.length > 0) {
|
|
436
|
+
await this.client.deleteMessageBatch({
|
|
437
|
+
QueueUrl: this.queueUrl,
|
|
438
|
+
Entries: deleteEntries,
|
|
439
|
+
})
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
console.error('[SQS Consumer] Poll error:', error)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return results
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ============================================================================
|
|
451
|
+
// Event Processor Interface
|
|
452
|
+
// ============================================================================
|
|
453
|
+
|
|
454
|
+
export interface EventProcessor {
|
|
455
|
+
processEvents(events: AnalyticsEvent[]): Promise<void>
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export interface SQSLambdaMessage {
|
|
459
|
+
messageId: string
|
|
460
|
+
receiptHandle: string
|
|
461
|
+
body: string
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export interface ProcessResult {
|
|
465
|
+
successful: number
|
|
466
|
+
failed: number
|
|
467
|
+
errors: Array<{ messageId: string, error: string }>
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ============================================================================
|
|
471
|
+
// Partition Key Strategy for Scale
|
|
472
|
+
// ============================================================================
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Generate a partition key with time-based sharding to avoid hot partitions
|
|
476
|
+
*
|
|
477
|
+
* For high-volume sites, we add a shard suffix based on the current minute
|
|
478
|
+
* to distribute writes across multiple partitions.
|
|
479
|
+
*
|
|
480
|
+
* Pattern: SITE#{siteId}#SHARD#{shardId}
|
|
481
|
+
*
|
|
482
|
+
* This helps with:
|
|
483
|
+
* - Distributing write throughput across partitions
|
|
484
|
+
* - Avoiding hot partition throttling
|
|
485
|
+
* - Supporting burst traffic patterns
|
|
486
|
+
*/
|
|
487
|
+
export function getShardedPartitionKey(
|
|
488
|
+
siteId: string,
|
|
489
|
+
shardCount: number = 10,
|
|
490
|
+
): string {
|
|
491
|
+
// Use current minute for deterministic sharding
|
|
492
|
+
const minute = new Date().getMinutes()
|
|
493
|
+
const shardId = minute % shardCount
|
|
494
|
+
return `SITE#${siteId}#SHARD#${shardId}`
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Generate partition key with random sharding for even distribution
|
|
499
|
+
*/
|
|
500
|
+
export function getRandomShardedPartitionKey(
|
|
501
|
+
siteId: string,
|
|
502
|
+
shardCount: number = 10,
|
|
503
|
+
): string {
|
|
504
|
+
const shardId = Math.floor(Math.random() * shardCount)
|
|
505
|
+
return `SITE#${siteId}#SHARD#${shardId}`
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Get all shard keys for a site (used for queries)
|
|
510
|
+
*/
|
|
511
|
+
export function getAllShardKeys(
|
|
512
|
+
siteId: string,
|
|
513
|
+
shardCount: number = 10,
|
|
514
|
+
): string[] {
|
|
515
|
+
return Array.from({ length: shardCount }, (_, i) => `SITE#${siteId}#SHARD#${i}`)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ============================================================================
|
|
519
|
+
// Write Coalescing
|
|
520
|
+
// ============================================================================
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Coalesce multiple writes to the same key within a time window
|
|
524
|
+
* This reduces write costs and avoids unnecessary overwrites
|
|
525
|
+
*/
|
|
526
|
+
export class WriteCoalescer<T> {
|
|
527
|
+
private pending: Map<string, { data: T, timer: ReturnType<typeof setTimeout> }> = new Map()
|
|
528
|
+
private readonly windowMs: number
|
|
529
|
+
private readonly onWrite: (key: string, data: T) => Promise<void>
|
|
530
|
+
|
|
531
|
+
constructor(options: {
|
|
532
|
+
windowMs?: number
|
|
533
|
+
onWrite: (key: string, data: T) => Promise<void>
|
|
534
|
+
}) {
|
|
535
|
+
this.windowMs = options.windowMs ?? 100
|
|
536
|
+
this.onWrite = options.onWrite
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Queue a write - if a write for the same key is pending, it will be replaced
|
|
541
|
+
*/
|
|
542
|
+
write(key: string, data: T): void {
|
|
543
|
+
const existing = this.pending.get(key)
|
|
544
|
+
if (existing) {
|
|
545
|
+
clearTimeout(existing.timer)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const timer = setTimeout(async () => {
|
|
549
|
+
this.pending.delete(key)
|
|
550
|
+
try {
|
|
551
|
+
await this.onWrite(key, data)
|
|
552
|
+
}
|
|
553
|
+
catch (error) {
|
|
554
|
+
console.error('[WriteCoalescer] Write failed:', key, error)
|
|
555
|
+
}
|
|
556
|
+
}, this.windowMs)
|
|
557
|
+
|
|
558
|
+
this.pending.set(key, { data, timer })
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Flush all pending writes immediately
|
|
563
|
+
*/
|
|
564
|
+
async flush(): Promise<void> {
|
|
565
|
+
const entries = Array.from(this.pending.entries())
|
|
566
|
+
this.pending.clear()
|
|
567
|
+
|
|
568
|
+
for (const [key, { data, timer }] of entries) {
|
|
569
|
+
clearTimeout(timer)
|
|
570
|
+
try {
|
|
571
|
+
await this.onWrite(key, data)
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
console.error('[WriteCoalescer] Flush write failed:', key, error)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
get size(): number {
|
|
580
|
+
return this.pending.size
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// ============================================================================
|
|
585
|
+
// Utilities
|
|
586
|
+
// ============================================================================
|
|
587
|
+
|
|
588
|
+
function generateBatchId(): string {
|
|
589
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function groupEventsBySite(events: AnalyticsEvent[]): Record<string, AnalyticsEvent[]> {
|
|
593
|
+
const grouped: Record<string, AnalyticsEvent[]> = {}
|
|
594
|
+
for (const event of events) {
|
|
595
|
+
const siteId = event.siteId || 'unknown'
|
|
596
|
+
if (!grouped[siteId]) {
|
|
597
|
+
grouped[siteId] = []
|
|
598
|
+
}
|
|
599
|
+
grouped[siteId].push(event)
|
|
600
|
+
}
|
|
601
|
+
return grouped
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// ============================================================================
|
|
605
|
+
// CloudFormation/SAM Template Helper
|
|
606
|
+
// ============================================================================
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Generate CloudFormation resources for SQS-based buffering
|
|
610
|
+
*/
|
|
611
|
+
export function generateSQSResources(config: {
|
|
612
|
+
queueName: string
|
|
613
|
+
deadLetterQueueName?: string
|
|
614
|
+
visibilityTimeout?: number
|
|
615
|
+
messageRetention?: number
|
|
616
|
+
maxReceiveCount?: number
|
|
617
|
+
fifo?: boolean
|
|
618
|
+
}): Record<string, unknown> {
|
|
619
|
+
const resources: Record<string, unknown> = {}
|
|
620
|
+
const fifoSuffix = config.fifo ? '.fifo' : ''
|
|
621
|
+
|
|
622
|
+
// Dead Letter Queue
|
|
623
|
+
if (config.deadLetterQueueName) {
|
|
624
|
+
resources.AnalyticsDeadLetterQueue = {
|
|
625
|
+
Type: 'AWS::SQS::Queue',
|
|
626
|
+
Properties: {
|
|
627
|
+
QueueName: `${config.deadLetterQueueName}${fifoSuffix}`,
|
|
628
|
+
MessageRetentionPeriod: 1209600, // 14 days
|
|
629
|
+
...(config.fifo && {
|
|
630
|
+
FifoQueue: true,
|
|
631
|
+
ContentBasedDeduplication: true,
|
|
632
|
+
}),
|
|
633
|
+
},
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Main Queue
|
|
638
|
+
resources.AnalyticsEventQueue = {
|
|
639
|
+
Type: 'AWS::SQS::Queue',
|
|
640
|
+
Properties: {
|
|
641
|
+
QueueName: `${config.queueName}${fifoSuffix}`,
|
|
642
|
+
VisibilityTimeout: config.visibilityTimeout ?? 30,
|
|
643
|
+
MessageRetentionPeriod: config.messageRetention ?? 86400,
|
|
644
|
+
...(config.fifo && {
|
|
645
|
+
FifoQueue: true,
|
|
646
|
+
ContentBasedDeduplication: true,
|
|
647
|
+
}),
|
|
648
|
+
...(config.deadLetterQueueName && {
|
|
649
|
+
RedrivePolicy: {
|
|
650
|
+
deadLetterTargetArn: { 'Fn::GetAtt': ['AnalyticsDeadLetterQueue', 'Arn'] },
|
|
651
|
+
maxReceiveCount: config.maxReceiveCount ?? 3,
|
|
652
|
+
},
|
|
653
|
+
}),
|
|
654
|
+
},
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return resources
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// ============================================================================
|
|
661
|
+
// ts-cloud Integration
|
|
662
|
+
// ============================================================================
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Create an SQS event producer using ts-cloud configuration
|
|
666
|
+
*
|
|
667
|
+
* @example
|
|
668
|
+
* ```ts
|
|
669
|
+
* import { createAnalyticsProducer } from 'ts-analytics'
|
|
670
|
+
*
|
|
671
|
+
* // Uses queue URL from environment or cloud.config.ts
|
|
672
|
+
* const producer = await createAnalyticsProducer()
|
|
673
|
+
*
|
|
674
|
+
* // Or specify queue URL directly
|
|
675
|
+
* const producer = await createAnalyticsProducer({
|
|
676
|
+
* queueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789/analytics-events',
|
|
677
|
+
* region: 'us-east-1',
|
|
678
|
+
* })
|
|
679
|
+
*
|
|
680
|
+
* // Send events
|
|
681
|
+
* await producer.sendEvent({
|
|
682
|
+
* type: 'pageview',
|
|
683
|
+
* siteId: 'my-site',
|
|
684
|
+
* timestamp: new Date().toISOString(),
|
|
685
|
+
* data: pageViewData,
|
|
686
|
+
* })
|
|
687
|
+
* ```
|
|
688
|
+
*/
|
|
689
|
+
export async function createAnalyticsProducer(options?: {
|
|
690
|
+
queueUrl?: string
|
|
691
|
+
region?: string
|
|
692
|
+
}): Promise<SQSEventProducer> {
|
|
693
|
+
// Try to load from environment or cloud.config.ts
|
|
694
|
+
const queueUrl = options?.queueUrl
|
|
695
|
+
|| process.env.SQS_QUEUE_URL
|
|
696
|
+
|| process.env.ANALYTICS_SQS_QUEUE_URL
|
|
697
|
+
|
|
698
|
+
if (!queueUrl) {
|
|
699
|
+
throw new Error(
|
|
700
|
+
'SQS queue URL not configured. Set SQS_QUEUE_URL environment variable or pass queueUrl option.',
|
|
701
|
+
)
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const region = options?.region || process.env.AWS_REGION || 'us-east-1'
|
|
705
|
+
|
|
706
|
+
// Dynamic import to avoid bundling ts-cloud in browser builds
|
|
707
|
+
// Falls back to a minimal implementation if ts-cloud is not available
|
|
708
|
+
let clientAdapter: SQSClient
|
|
709
|
+
|
|
710
|
+
try {
|
|
711
|
+
const tsCloud = await (import('ts-cloud' as string) as Promise<any>)
|
|
712
|
+
const sqsClient = new tsCloud.SQSClient(region)
|
|
713
|
+
|
|
714
|
+
// Create adapter to match our interface using ts-cloud client
|
|
715
|
+
clientAdapter = {
|
|
716
|
+
sendMessage: async (input) => {
|
|
717
|
+
const result = await sqsClient.sendMessage({
|
|
718
|
+
queueUrl: input.QueueUrl,
|
|
719
|
+
messageBody: input.MessageBody,
|
|
720
|
+
messageGroupId: input.MessageGroupId,
|
|
721
|
+
messageDeduplicationId: input.MessageDeduplicationId,
|
|
722
|
+
})
|
|
723
|
+
return { MessageId: result.MessageId }
|
|
724
|
+
},
|
|
725
|
+
sendMessageBatch: async (input) => {
|
|
726
|
+
// ts-cloud may not have sendMessageBatch, fall back to individual sends
|
|
727
|
+
const results = await Promise.allSettled(
|
|
728
|
+
input.Entries.map(e =>
|
|
729
|
+
sqsClient.sendMessage({
|
|
730
|
+
queueUrl: input.QueueUrl,
|
|
731
|
+
messageBody: e.MessageBody,
|
|
732
|
+
messageGroupId: e.MessageGroupId,
|
|
733
|
+
messageDeduplicationId: e.MessageDeduplicationId,
|
|
734
|
+
}) .then((r: any) => ({ Id: e.Id, MessageId: r.MessageId })),
|
|
735
|
+
),
|
|
736
|
+
)
|
|
737
|
+
return {
|
|
738
|
+
Successful: results
|
|
739
|
+
.filter((r): r is PromiseFulfilledResult<{ Id: string, MessageId: string }> => r.status === 'fulfilled')
|
|
740
|
+
.map(r => r.value),
|
|
741
|
+
Failed: results
|
|
742
|
+
.filter((r): r is PromiseRejectedResult => r.status === 'rejected')
|
|
743
|
+
.map((r, i) => ({ Id: input.Entries[i].Id, Code: 'SendFailed', Message: String(r.reason) })),
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
receiveMessage: async (input) => {
|
|
747
|
+
const result = await sqsClient.receiveMessages({
|
|
748
|
+
queueUrl: input.QueueUrl,
|
|
749
|
+
maxMessages: input.MaxNumberOfMessages,
|
|
750
|
+
visibilityTimeout: input.VisibilityTimeout,
|
|
751
|
+
waitTimeSeconds: input.WaitTimeSeconds,
|
|
752
|
+
})
|
|
753
|
+
return {
|
|
754
|
+
Messages: result.Messages?.map((m: any) => ({
|
|
755
|
+
MessageId: m.MessageId || '',
|
|
756
|
+
ReceiptHandle: m.ReceiptHandle || '',
|
|
757
|
+
Body: m.Body || '',
|
|
758
|
+
})),
|
|
759
|
+
}
|
|
760
|
+
},
|
|
761
|
+
deleteMessage: async (input) => {
|
|
762
|
+
await sqsClient.deleteMessage(input.QueueUrl, input.ReceiptHandle)
|
|
763
|
+
},
|
|
764
|
+
deleteMessageBatch: async (input) => {
|
|
765
|
+
// ts-cloud may not have deleteMessageBatch, fall back to individual deletes
|
|
766
|
+
const results = await Promise.allSettled(
|
|
767
|
+
input.Entries.map(e =>
|
|
768
|
+
sqsClient.deleteMessage(input.QueueUrl, e.ReceiptHandle).then(() => ({ Id: e.Id })),
|
|
769
|
+
),
|
|
770
|
+
)
|
|
771
|
+
return {
|
|
772
|
+
Successful: results
|
|
773
|
+
.filter((r): r is PromiseFulfilledResult<{ Id: string }> => r.status === 'fulfilled')
|
|
774
|
+
.map(r => r.value),
|
|
775
|
+
Failed: results
|
|
776
|
+
.filter((r): r is PromiseRejectedResult => r.status === 'rejected')
|
|
777
|
+
.map((_r, i) => ({ Id: input.Entries[i].Id, Code: 'DeleteFailed' })),
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
catch {
|
|
783
|
+
throw new Error(
|
|
784
|
+
'ts-cloud package is required for SQS integration. Install with: bun add ts-cloud',
|
|
785
|
+
)
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return new SQSEventProducer(clientAdapter, queueUrl)
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Check if SQS buffering is enabled based on configuration
|
|
793
|
+
*/
|
|
794
|
+
export function isSQSBufferingEnabled(): boolean {
|
|
795
|
+
return (
|
|
796
|
+
process.env.SQS_BUFFERING_ENABLED === 'true'
|
|
797
|
+
|| process.env.ANALYTICS_SQS_ENABLED === 'true'
|
|
798
|
+
|| Boolean(process.env.SQS_QUEUE_URL)
|
|
799
|
+
)
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// ============================================================================
|
|
803
|
+
// Export Default Config
|
|
804
|
+
// ============================================================================
|
|
805
|
+
|
|
806
|
+
export { defaultSQSConfig as sqsConfig }
|