@stacksjs/ts-cloud-core 0.1.7 → 0.1.9
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/package.json +7 -6
- package/src/advanced-features.test.ts +465 -0
- package/src/aws/cloudformation.ts +421 -0
- package/src/aws/cloudfront.ts +158 -0
- package/src/aws/credentials.test.ts +132 -0
- package/src/aws/credentials.ts +545 -0
- package/src/aws/index.ts +87 -0
- package/src/aws/s3.test.ts +188 -0
- package/src/aws/s3.ts +1088 -0
- package/src/aws/signature.test.ts +670 -0
- package/src/aws/signature.ts +1155 -0
- package/src/backup/disaster-recovery.test.ts +726 -0
- package/src/backup/disaster-recovery.ts +500 -0
- package/src/backup/index.ts +34 -0
- package/src/backup/manager.test.ts +498 -0
- package/src/backup/manager.ts +432 -0
- package/src/cicd/circleci.ts +430 -0
- package/src/cicd/github-actions.ts +424 -0
- package/src/cicd/gitlab-ci.ts +255 -0
- package/src/cicd/index.ts +8 -0
- package/src/cli/history.ts +396 -0
- package/src/cli/index.ts +10 -0
- package/src/cli/progress.ts +458 -0
- package/src/cli/repl.ts +454 -0
- package/src/cli/suggestions.ts +327 -0
- package/src/cli/table.test.ts +319 -0
- package/src/cli/table.ts +332 -0
- package/src/cloudformation/builder.test.ts +327 -0
- package/src/cloudformation/builder.ts +378 -0
- package/src/cloudformation/builders/api-gateway.ts +449 -0
- package/src/cloudformation/builders/cache.ts +334 -0
- package/src/cloudformation/builders/cdn.ts +278 -0
- package/src/cloudformation/builders/compute.ts +485 -0
- package/src/cloudformation/builders/database.ts +392 -0
- package/src/cloudformation/builders/functions.ts +343 -0
- package/src/cloudformation/builders/messaging.ts +140 -0
- package/src/cloudformation/builders/monitoring.ts +300 -0
- package/src/cloudformation/builders/network.ts +264 -0
- package/src/cloudformation/builders/queue.ts +147 -0
- package/src/cloudformation/builders/security.ts +399 -0
- package/src/cloudformation/builders/storage.ts +285 -0
- package/src/cloudformation/index.ts +30 -0
- package/src/cloudformation/types.ts +173 -0
- package/src/compliance/aws-config.ts +543 -0
- package/src/compliance/cloudtrail.ts +376 -0
- package/src/compliance/compliance.test.ts +423 -0
- package/src/compliance/guardduty.ts +446 -0
- package/src/compliance/index.ts +66 -0
- package/src/compliance/security-hub.ts +456 -0
- package/src/containers/build-optimization.ts +416 -0
- package/src/containers/containers.test.ts +508 -0
- package/src/containers/image-scanning.ts +360 -0
- package/src/containers/index.ts +9 -0
- package/src/containers/registry.ts +293 -0
- package/src/containers/service-mesh.ts +520 -0
- package/src/database/database.test.ts +762 -0
- package/src/database/index.ts +9 -0
- package/src/database/migrations.ts +444 -0
- package/src/database/performance.ts +528 -0
- package/src/database/replicas.ts +534 -0
- package/src/database/users.ts +494 -0
- package/src/dependency-graph.ts +143 -0
- package/src/deployment/ab-testing.ts +582 -0
- package/src/deployment/blue-green.ts +452 -0
- package/src/deployment/canary.ts +500 -0
- package/src/deployment/deployment.test.ts +526 -0
- package/src/deployment/index.ts +61 -0
- package/src/deployment/progressive.ts +62 -0
- package/src/dns/dns.test.ts +641 -0
- package/src/dns/dnssec.ts +315 -0
- package/src/dns/index.ts +8 -0
- package/src/dns/resolver.ts +496 -0
- package/src/dns/routing.ts +593 -0
- package/src/email/advanced/analytics.ts +445 -0
- package/src/email/advanced/index.ts +11 -0
- package/src/email/advanced/rules.ts +465 -0
- package/src/email/advanced/scheduling.ts +352 -0
- package/src/email/advanced/search.ts +412 -0
- package/src/email/advanced/shared-mailboxes.ts +404 -0
- package/src/email/advanced/templates.ts +455 -0
- package/src/email/advanced/threading.ts +281 -0
- package/src/email/analytics.ts +467 -0
- package/src/email/bounce-handling.ts +425 -0
- package/src/email/email.test.ts +431 -0
- package/src/email/handlers/__tests__/inbound.test.ts +38 -0
- package/src/email/handlers/__tests__/outbound.test.ts +37 -0
- package/src/email/handlers/converter.ts +227 -0
- package/src/email/handlers/feedback.ts +228 -0
- package/src/email/handlers/inbound.ts +169 -0
- package/src/email/handlers/outbound.ts +178 -0
- package/src/email/index.ts +15 -0
- package/src/email/reputation.ts +303 -0
- package/src/email/templates.ts +352 -0
- package/src/errors/index.test.ts +434 -0
- package/src/errors/index.ts +416 -0
- package/src/health-checks/index.ts +40 -0
- package/src/index.ts +360 -0
- package/src/intrinsic-functions.ts +118 -0
- package/src/lambda/concurrency.ts +330 -0
- package/src/lambda/destinations.ts +345 -0
- package/src/lambda/dlq.ts +425 -0
- package/src/lambda/index.ts +11 -0
- package/src/lambda/lambda.test.ts +840 -0
- package/src/lambda/layers.ts +263 -0
- package/src/lambda/versions.ts +376 -0
- package/src/lambda/vpc.ts +399 -0
- package/src/local/config.ts +114 -0
- package/src/local/index.ts +6 -0
- package/src/local/mock-aws.ts +351 -0
- package/src/modules/ai.ts +340 -0
- package/src/modules/api.ts +478 -0
- package/src/modules/auth.ts +805 -0
- package/src/modules/cache.ts +417 -0
- package/src/modules/cdn.ts +1062 -0
- package/src/modules/communication.ts +1094 -0
- package/src/modules/compute.ts +3348 -0
- package/src/modules/database.ts +554 -0
- package/src/modules/deployment.ts +1079 -0
- package/src/modules/dns.ts +337 -0
- package/src/modules/email.ts +1538 -0
- package/src/modules/filesystem.ts +515 -0
- package/src/modules/index.ts +32 -0
- package/src/modules/messaging.ts +486 -0
- package/src/modules/monitoring.ts +2086 -0
- package/src/modules/network.ts +664 -0
- package/src/modules/parameter-store.ts +325 -0
- package/src/modules/permissions.ts +1081 -0
- package/src/modules/phone.ts +494 -0
- package/src/modules/queue.ts +1260 -0
- package/src/modules/redirects.ts +464 -0
- package/src/modules/registry.ts +699 -0
- package/src/modules/search.ts +401 -0
- package/src/modules/secrets.ts +416 -0
- package/src/modules/security.ts +731 -0
- package/src/modules/sms.ts +389 -0
- package/src/modules/storage.ts +1120 -0
- package/src/modules/workflow.ts +680 -0
- package/src/multi-account/config.ts +521 -0
- package/src/multi-account/index.ts +7 -0
- package/src/multi-account/manager.ts +427 -0
- package/src/multi-region/cross-region.ts +410 -0
- package/src/multi-region/index.ts +8 -0
- package/src/multi-region/manager.ts +483 -0
- package/src/multi-region/regions.ts +435 -0
- package/src/network-security/index.ts +48 -0
- package/src/observability/index.ts +9 -0
- package/src/observability/logs.ts +522 -0
- package/src/observability/metrics.ts +460 -0
- package/src/observability/observability.test.ts +782 -0
- package/src/observability/synthetics.ts +568 -0
- package/src/observability/xray.ts +358 -0
- package/src/phone/advanced/analytics.ts +349 -0
- package/src/phone/advanced/callbacks.ts +428 -0
- package/src/phone/advanced/index.ts +8 -0
- package/src/phone/advanced/ivr-builder.ts +504 -0
- package/src/phone/advanced/recording.ts +310 -0
- package/src/phone/handlers/__tests__/incoming-call.test.ts +40 -0
- package/src/phone/handlers/incoming-call.ts +117 -0
- package/src/phone/handlers/missed-call.ts +116 -0
- package/src/phone/handlers/voicemail.ts +179 -0
- package/src/phone/index.ts +9 -0
- package/src/presets/api-backend.ts +134 -0
- package/src/presets/data-pipeline.ts +204 -0
- package/src/presets/extend.test.ts +295 -0
- package/src/presets/extend.ts +297 -0
- package/src/presets/fullstack-app.ts +144 -0
- package/src/presets/index.ts +27 -0
- package/src/presets/jamstack.ts +135 -0
- package/src/presets/microservices.ts +167 -0
- package/src/presets/ml-api.ts +208 -0
- package/src/presets/nodejs-server.ts +104 -0
- package/src/presets/nodejs-serverless.ts +114 -0
- package/src/presets/realtime-app.ts +184 -0
- package/src/presets/static-site.ts +64 -0
- package/src/presets/traditional-web-app.ts +339 -0
- package/src/presets/wordpress.ts +138 -0
- package/src/preview/github.test.ts +249 -0
- package/src/preview/github.ts +297 -0
- package/src/preview/index.ts +37 -0
- package/src/preview/manager.test.ts +440 -0
- package/src/preview/manager.ts +326 -0
- package/src/preview/notifications.test.ts +582 -0
- package/src/preview/notifications.ts +341 -0
- package/src/queue/batch-processing.ts +402 -0
- package/src/queue/dlq-monitoring.ts +402 -0
- package/src/queue/fifo.ts +342 -0
- package/src/queue/index.ts +9 -0
- package/src/queue/management.ts +428 -0
- package/src/queue/queue.test.ts +429 -0
- package/src/resource-mgmt/index.ts +39 -0
- package/src/resource-naming.ts +62 -0
- package/src/s3/index.ts +523 -0
- package/src/schema/cloud-config.schema.json +554 -0
- package/src/schema/index.ts +68 -0
- package/src/security/certificate-manager.ts +492 -0
- package/src/security/index.ts +9 -0
- package/src/security/scanning.ts +545 -0
- package/src/security/secrets-manager.ts +476 -0
- package/src/security/secrets-rotation.ts +456 -0
- package/src/security/security.test.ts +738 -0
- package/src/sms/advanced/ab-testing.ts +389 -0
- package/src/sms/advanced/analytics.ts +336 -0
- package/src/sms/advanced/campaigns.ts +523 -0
- package/src/sms/advanced/chatbot.ts +224 -0
- package/src/sms/advanced/index.ts +10 -0
- package/src/sms/advanced/link-tracking.ts +248 -0
- package/src/sms/advanced/mms.ts +308 -0
- package/src/sms/handlers/__tests__/send.test.ts +40 -0
- package/src/sms/handlers/delivery-status.ts +133 -0
- package/src/sms/handlers/receive.ts +162 -0
- package/src/sms/handlers/send.ts +174 -0
- package/src/sms/index.ts +9 -0
- package/src/stack-diff.ts +389 -0
- package/src/static-site/index.ts +85 -0
- package/src/template-builder.ts +110 -0
- package/src/template-validator.ts +574 -0
- package/src/utils/cache.ts +291 -0
- package/src/utils/diff.ts +269 -0
- package/src/utils/hash.ts +227 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/parallel.ts +294 -0
- package/src/validators/credentials.test.ts +274 -0
- package/src/validators/credentials.ts +233 -0
- package/src/validators/quotas.test.ts +434 -0
- package/src/validators/quotas.ts +217 -0
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A/B Testing Infrastructure
|
|
3
|
+
* Traffic splitting based on user attributes, headers, or cookies
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ABTest {
|
|
7
|
+
id: string
|
|
8
|
+
name: string
|
|
9
|
+
description?: string
|
|
10
|
+
variants: ABVariant[]
|
|
11
|
+
routingStrategy: RoutingStrategy
|
|
12
|
+
startTime: Date
|
|
13
|
+
endTime?: Date
|
|
14
|
+
status: 'draft' | 'active' | 'paused' | 'completed'
|
|
15
|
+
metrics?: ABMetrics
|
|
16
|
+
winner?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ABVariant {
|
|
20
|
+
id: string
|
|
21
|
+
name: string
|
|
22
|
+
description?: string
|
|
23
|
+
trafficPercentage: number
|
|
24
|
+
targetGroupArn?: string
|
|
25
|
+
functionVersionArn?: string
|
|
26
|
+
originId?: string // For CloudFront
|
|
27
|
+
weight: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RoutingStrategy {
|
|
31
|
+
type: 'random' | 'cookie' | 'header' | 'geo' | 'device' | 'user-attribute'
|
|
32
|
+
cookieName?: string
|
|
33
|
+
headerName?: string
|
|
34
|
+
attributeName?: string
|
|
35
|
+
stickySession?: boolean
|
|
36
|
+
sessionDuration?: number // minutes
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ABMetrics {
|
|
40
|
+
variants: Record<string, VariantMetrics>
|
|
41
|
+
totalRequests: number
|
|
42
|
+
startTime: Date
|
|
43
|
+
lastUpdated: Date
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface VariantMetrics {
|
|
47
|
+
requests: number
|
|
48
|
+
conversions: number
|
|
49
|
+
conversionRate: number
|
|
50
|
+
averageLatency: number
|
|
51
|
+
errorRate: number
|
|
52
|
+
revenue?: number
|
|
53
|
+
customMetrics?: Record<string, number>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ABTestResult {
|
|
57
|
+
testId: string
|
|
58
|
+
winningVariant: string
|
|
59
|
+
confidence: number // 0-100%
|
|
60
|
+
improvement: number // Percentage improvement over control
|
|
61
|
+
statisticalSignificance: boolean
|
|
62
|
+
metrics: ABMetrics
|
|
63
|
+
recommendation: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A/B testing manager
|
|
68
|
+
*/
|
|
69
|
+
export class ABTestManager {
|
|
70
|
+
private tests: Map<string, ABTest> = new Map()
|
|
71
|
+
private testCounter = 0
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create A/B test
|
|
75
|
+
*/
|
|
76
|
+
createTest(test: Omit<ABTest, 'id'>): ABTest {
|
|
77
|
+
const id = `abtest-${Date.now()}-${this.testCounter++}`
|
|
78
|
+
|
|
79
|
+
const abTest: ABTest = {
|
|
80
|
+
id,
|
|
81
|
+
...test,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.tests.set(id, abTest)
|
|
85
|
+
|
|
86
|
+
return abTest
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Create simple A/B test (control vs variant)
|
|
91
|
+
*/
|
|
92
|
+
createSimpleABTest(options: {
|
|
93
|
+
name: string
|
|
94
|
+
description?: string
|
|
95
|
+
controlTargetGroupArn: string
|
|
96
|
+
variantTargetGroupArn: string
|
|
97
|
+
variantTrafficPercentage?: number
|
|
98
|
+
stickySession?: boolean
|
|
99
|
+
}): ABTest {
|
|
100
|
+
const variantPercentage = options.variantTrafficPercentage || 50
|
|
101
|
+
|
|
102
|
+
return this.createTest({
|
|
103
|
+
name: options.name,
|
|
104
|
+
description: options.description,
|
|
105
|
+
variants: [
|
|
106
|
+
{
|
|
107
|
+
id: 'control',
|
|
108
|
+
name: 'Control',
|
|
109
|
+
description: 'Original version',
|
|
110
|
+
trafficPercentage: 100 - variantPercentage,
|
|
111
|
+
targetGroupArn: options.controlTargetGroupArn,
|
|
112
|
+
weight: 100 - variantPercentage,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: 'variant-a',
|
|
116
|
+
name: 'Variant A',
|
|
117
|
+
description: 'Test version',
|
|
118
|
+
trafficPercentage: variantPercentage,
|
|
119
|
+
targetGroupArn: options.variantTargetGroupArn,
|
|
120
|
+
weight: variantPercentage,
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
routingStrategy: {
|
|
124
|
+
type: options.stickySession ? 'cookie' : 'random',
|
|
125
|
+
cookieName: options.stickySession ? 'ab_variant' : undefined,
|
|
126
|
+
stickySession: options.stickySession ?? false,
|
|
127
|
+
sessionDuration: 1440, // 24 hours
|
|
128
|
+
},
|
|
129
|
+
startTime: new Date(),
|
|
130
|
+
status: 'draft',
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Create multivariate test
|
|
136
|
+
*/
|
|
137
|
+
createMultivariateTest(options: {
|
|
138
|
+
name: string
|
|
139
|
+
description?: string
|
|
140
|
+
variants: Array<{
|
|
141
|
+
name: string
|
|
142
|
+
description?: string
|
|
143
|
+
targetGroupArn: string
|
|
144
|
+
trafficPercentage: number
|
|
145
|
+
}>
|
|
146
|
+
routingStrategy?: RoutingStrategy
|
|
147
|
+
}): ABTest {
|
|
148
|
+
// Validate percentages sum to 100
|
|
149
|
+
const totalPercentage = options.variants.reduce((sum, v) => sum + v.trafficPercentage, 0)
|
|
150
|
+
if (totalPercentage !== 100) {
|
|
151
|
+
throw new Error(`Traffic percentages must sum to 100, got ${totalPercentage}`)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return this.createTest({
|
|
155
|
+
name: options.name,
|
|
156
|
+
description: options.description,
|
|
157
|
+
variants: options.variants.map((v, index) => ({
|
|
158
|
+
id: `variant-${index}`,
|
|
159
|
+
name: v.name,
|
|
160
|
+
description: v.description,
|
|
161
|
+
trafficPercentage: v.trafficPercentage,
|
|
162
|
+
targetGroupArn: v.targetGroupArn,
|
|
163
|
+
weight: v.trafficPercentage,
|
|
164
|
+
})),
|
|
165
|
+
routingStrategy: options.routingStrategy || {
|
|
166
|
+
type: 'random',
|
|
167
|
+
stickySession: false,
|
|
168
|
+
},
|
|
169
|
+
startTime: new Date(),
|
|
170
|
+
status: 'draft',
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Create header-based A/B test
|
|
176
|
+
*/
|
|
177
|
+
createHeaderBasedTest(options: {
|
|
178
|
+
name: string
|
|
179
|
+
controlTargetGroupArn: string
|
|
180
|
+
variantTargetGroupArn: string
|
|
181
|
+
headerName: string
|
|
182
|
+
headerValue: string
|
|
183
|
+
}): ABTest {
|
|
184
|
+
return this.createTest({
|
|
185
|
+
name: options.name,
|
|
186
|
+
description: `Route based on ${options.headerName} header`,
|
|
187
|
+
variants: [
|
|
188
|
+
{
|
|
189
|
+
id: 'control',
|
|
190
|
+
name: 'Control',
|
|
191
|
+
trafficPercentage: 50,
|
|
192
|
+
targetGroupArn: options.controlTargetGroupArn,
|
|
193
|
+
weight: 50,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: 'variant-a',
|
|
197
|
+
name: 'Variant A',
|
|
198
|
+
trafficPercentage: 50,
|
|
199
|
+
targetGroupArn: options.variantTargetGroupArn,
|
|
200
|
+
weight: 50,
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
routingStrategy: {
|
|
204
|
+
type: 'header',
|
|
205
|
+
headerName: options.headerName,
|
|
206
|
+
stickySession: false,
|
|
207
|
+
},
|
|
208
|
+
startTime: new Date(),
|
|
209
|
+
status: 'draft',
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Create geo-based A/B test
|
|
215
|
+
*/
|
|
216
|
+
createGeoBasedTest(options: {
|
|
217
|
+
name: string
|
|
218
|
+
controlTargetGroupArn: string
|
|
219
|
+
variantTargetGroupArn: string
|
|
220
|
+
regions: string[] // For variant (e.g., ['US', 'CA'])
|
|
221
|
+
}): ABTest {
|
|
222
|
+
return this.createTest({
|
|
223
|
+
name: options.name,
|
|
224
|
+
description: `Route based on geographic location`,
|
|
225
|
+
variants: [
|
|
226
|
+
{
|
|
227
|
+
id: 'control',
|
|
228
|
+
name: 'Control (Rest of World)',
|
|
229
|
+
trafficPercentage: 50,
|
|
230
|
+
targetGroupArn: options.controlTargetGroupArn,
|
|
231
|
+
weight: 50,
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
id: 'variant-a',
|
|
235
|
+
name: `Variant A (${options.regions.join(', ')})`,
|
|
236
|
+
trafficPercentage: 50,
|
|
237
|
+
targetGroupArn: options.variantTargetGroupArn,
|
|
238
|
+
weight: 50,
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
routingStrategy: {
|
|
242
|
+
type: 'geo',
|
|
243
|
+
stickySession: true,
|
|
244
|
+
sessionDuration: 1440,
|
|
245
|
+
},
|
|
246
|
+
startTime: new Date(),
|
|
247
|
+
status: 'draft',
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Start A/B test
|
|
253
|
+
*/
|
|
254
|
+
startTest(testId: string): void {
|
|
255
|
+
const test = this.tests.get(testId)
|
|
256
|
+
|
|
257
|
+
if (!test) {
|
|
258
|
+
throw new Error(`Test not found: ${testId}`)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (test.status !== 'draft' && test.status !== 'paused') {
|
|
262
|
+
throw new Error(`Cannot start test in ${test.status} status`)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
test.status = 'active'
|
|
266
|
+
test.startTime = new Date()
|
|
267
|
+
|
|
268
|
+
console.log(`Started A/B test: ${test.name}`)
|
|
269
|
+
console.log(` Variants: ${test.variants.length}`)
|
|
270
|
+
test.variants.forEach((v) => {
|
|
271
|
+
console.log(` - ${v.name}: ${v.trafficPercentage}%`)
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Pause A/B test
|
|
277
|
+
*/
|
|
278
|
+
pauseTest(testId: string): void {
|
|
279
|
+
const test = this.tests.get(testId)
|
|
280
|
+
|
|
281
|
+
if (!test) {
|
|
282
|
+
throw new Error(`Test not found: ${testId}`)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
test.status = 'paused'
|
|
286
|
+
console.log(`Paused A/B test: ${test.name}`)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Update traffic split
|
|
291
|
+
*/
|
|
292
|
+
updateTrafficSplit(testId: string, variantId: string, newPercentage: number): void {
|
|
293
|
+
const test = this.tests.get(testId)
|
|
294
|
+
|
|
295
|
+
if (!test) {
|
|
296
|
+
throw new Error(`Test not found: ${testId}`)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const variant = test.variants.find(v => v.id === variantId)
|
|
300
|
+
|
|
301
|
+
if (!variant) {
|
|
302
|
+
throw new Error(`Variant not found: ${variantId}`)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const oldPercentage = variant.trafficPercentage
|
|
306
|
+
variant.trafficPercentage = newPercentage
|
|
307
|
+
variant.weight = newPercentage
|
|
308
|
+
|
|
309
|
+
console.log(`Updated ${variant.name} traffic: ${oldPercentage}% → ${newPercentage}%`)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Analyze test results
|
|
314
|
+
*/
|
|
315
|
+
analyzeResults(testId: string): ABTestResult {
|
|
316
|
+
const test = this.tests.get(testId)
|
|
317
|
+
|
|
318
|
+
if (!test) {
|
|
319
|
+
throw new Error(`Test not found: ${testId}`)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (!test.metrics) {
|
|
323
|
+
// Simulate metrics collection
|
|
324
|
+
test.metrics = this.collectMetrics(test)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Find winning variant (highest conversion rate)
|
|
328
|
+
let winningVariant = test.variants[0]
|
|
329
|
+
let highestConversionRate = 0
|
|
330
|
+
|
|
331
|
+
for (const variant of test.variants) {
|
|
332
|
+
const metrics = test.metrics.variants[variant.id]
|
|
333
|
+
if (metrics && metrics.conversionRate > highestConversionRate) {
|
|
334
|
+
highestConversionRate = metrics.conversionRate
|
|
335
|
+
winningVariant = variant
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const controlMetrics = test.metrics.variants['control'] || test.metrics.variants[test.variants[0].id]
|
|
340
|
+
const winnerMetrics = test.metrics.variants[winningVariant.id]
|
|
341
|
+
|
|
342
|
+
const improvement
|
|
343
|
+
= ((winnerMetrics.conversionRate - controlMetrics.conversionRate) / controlMetrics.conversionRate) * 100
|
|
344
|
+
|
|
345
|
+
// Simple statistical significance check (would use proper chi-square test in production)
|
|
346
|
+
const minSampleSize = 100
|
|
347
|
+
const statisticalSignificance
|
|
348
|
+
= winnerMetrics.requests > minSampleSize && controlMetrics.requests > minSampleSize && Math.abs(improvement) > 10
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
testId,
|
|
352
|
+
winningVariant: winningVariant.name,
|
|
353
|
+
confidence: statisticalSignificance ? 95 : 75,
|
|
354
|
+
improvement,
|
|
355
|
+
statisticalSignificance,
|
|
356
|
+
metrics: test.metrics,
|
|
357
|
+
recommendation: this.generateRecommendation(improvement, statisticalSignificance, winningVariant.name),
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Declare winner and route all traffic
|
|
363
|
+
*/
|
|
364
|
+
declareWinner(testId: string, variantId: string): void {
|
|
365
|
+
const test = this.tests.get(testId)
|
|
366
|
+
|
|
367
|
+
if (!test) {
|
|
368
|
+
throw new Error(`Test not found: ${testId}`)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const winner = test.variants.find(v => v.id === variantId)
|
|
372
|
+
|
|
373
|
+
if (!winner) {
|
|
374
|
+
throw new Error(`Variant not found: ${variantId}`)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Route all traffic to winner
|
|
378
|
+
test.variants.forEach((v) => {
|
|
379
|
+
if (v.id === variantId) {
|
|
380
|
+
v.trafficPercentage = 100
|
|
381
|
+
v.weight = 100
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
v.trafficPercentage = 0
|
|
385
|
+
v.weight = 0
|
|
386
|
+
}
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
test.status = 'completed'
|
|
390
|
+
test.endTime = new Date()
|
|
391
|
+
test.winner = variantId
|
|
392
|
+
|
|
393
|
+
console.log(`Declared winner: ${winner.name}`)
|
|
394
|
+
console.log(` All traffic now routed to ${winner.name}`)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Collect metrics for test
|
|
399
|
+
*/
|
|
400
|
+
private collectMetrics(test: ABTest): ABMetrics {
|
|
401
|
+
const variantMetrics: Record<string, VariantMetrics> = {}
|
|
402
|
+
|
|
403
|
+
// Simulate metric collection
|
|
404
|
+
let totalRequests = 0
|
|
405
|
+
|
|
406
|
+
for (const variant of test.variants) {
|
|
407
|
+
const requests = Math.floor(Math.random() * 1000) + 500
|
|
408
|
+
const conversions = Math.floor(requests * (Math.random() * 0.1 + 0.05)) // 5-15% conversion
|
|
409
|
+
const conversionRate = (conversions / requests) * 100
|
|
410
|
+
|
|
411
|
+
variantMetrics[variant.id] = {
|
|
412
|
+
requests,
|
|
413
|
+
conversions,
|
|
414
|
+
conversionRate,
|
|
415
|
+
averageLatency: 150 + Math.random() * 100,
|
|
416
|
+
errorRate: Math.random() * 0.5,
|
|
417
|
+
revenue: conversions * (Math.random() * 50 + 100), // $100-150 per conversion
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
totalRequests += requests
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
variants: variantMetrics,
|
|
425
|
+
totalRequests,
|
|
426
|
+
startTime: test.startTime,
|
|
427
|
+
lastUpdated: new Date(),
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Generate recommendation
|
|
433
|
+
*/
|
|
434
|
+
private generateRecommendation(
|
|
435
|
+
improvement: number,
|
|
436
|
+
significant: boolean,
|
|
437
|
+
winnerName: string,
|
|
438
|
+
): string {
|
|
439
|
+
if (!significant) {
|
|
440
|
+
return 'Continue test - sample size too small or no significant difference detected'
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (improvement > 20) {
|
|
444
|
+
return `Strong winner detected - ${winnerName} shows ${improvement.toFixed(1)}% improvement. Recommend deploying to all traffic.`
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (improvement > 10) {
|
|
448
|
+
return `Moderate improvement - ${winnerName} shows ${improvement.toFixed(1)}% improvement. Consider deploying.`
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (improvement > 0) {
|
|
452
|
+
return `Minor improvement - ${winnerName} shows ${improvement.toFixed(1)}% improvement. May not be worth the complexity.`
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return `No improvement detected - consider reverting to control variant`
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Get test
|
|
460
|
+
*/
|
|
461
|
+
getTest(id: string): ABTest | undefined {
|
|
462
|
+
return this.tests.get(id)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* List tests
|
|
467
|
+
*/
|
|
468
|
+
listTests(): ABTest[] {
|
|
469
|
+
return Array.from(this.tests.values())
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Generate CloudFormation for ALB weighted routing
|
|
474
|
+
*/
|
|
475
|
+
generateALBListenerRuleCF(test: ABTest): any {
|
|
476
|
+
return {
|
|
477
|
+
Type: 'AWS::ElasticLoadBalancingV2::ListenerRule',
|
|
478
|
+
Properties: {
|
|
479
|
+
ListenerArn: { Ref: 'LoadBalancerListener' },
|
|
480
|
+
Priority: 1,
|
|
481
|
+
Conditions: [
|
|
482
|
+
{
|
|
483
|
+
Field: 'path-pattern',
|
|
484
|
+
Values: ['/*'],
|
|
485
|
+
},
|
|
486
|
+
],
|
|
487
|
+
Actions: [
|
|
488
|
+
{
|
|
489
|
+
Type: 'forward',
|
|
490
|
+
ForwardConfig: {
|
|
491
|
+
TargetGroups: test.variants.map(variant => ({
|
|
492
|
+
TargetGroupArn: variant.targetGroupArn,
|
|
493
|
+
Weight: variant.weight,
|
|
494
|
+
})),
|
|
495
|
+
TargetGroupStickinessConfig: {
|
|
496
|
+
Enabled: test.routingStrategy.stickySession || false,
|
|
497
|
+
DurationSeconds: test.routingStrategy.sessionDuration
|
|
498
|
+
? test.routingStrategy.sessionDuration * 60
|
|
499
|
+
: undefined,
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
],
|
|
504
|
+
},
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Generate Lambda@Edge function for A/B testing
|
|
510
|
+
*/
|
|
511
|
+
generateLambdaEdgeFunction(test: ABTest): string {
|
|
512
|
+
return `'use strict';
|
|
513
|
+
|
|
514
|
+
exports.handler = (event, context, callback) => {
|
|
515
|
+
const request = event.Records[0].cf.request;
|
|
516
|
+
const headers = request.headers;
|
|
517
|
+
|
|
518
|
+
// Check for existing variant cookie
|
|
519
|
+
let variant = null;
|
|
520
|
+
if (headers.cookie) {
|
|
521
|
+
const cookies = headers.cookie[0].value.split(';');
|
|
522
|
+
for (const cookie of cookies) {
|
|
523
|
+
const [key, value] = cookie.trim().split('=');
|
|
524
|
+
if (key === '${test.routingStrategy.cookieName || 'ab_variant'}') {
|
|
525
|
+
variant = value;
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Assign variant if not already assigned
|
|
532
|
+
if (!variant) {
|
|
533
|
+
const random = Math.random() * 100;
|
|
534
|
+
let cumulative = 0;
|
|
535
|
+
|
|
536
|
+
${test.variants
|
|
537
|
+
.map((v, i) => {
|
|
538
|
+
return `if (random < ${v.trafficPercentage + (i > 0 ? test.variants.slice(0, i).reduce((sum, v) => sum + v.trafficPercentage, 0) : 0)}) {
|
|
539
|
+
variant = '${v.id}';
|
|
540
|
+
}`
|
|
541
|
+
})
|
|
542
|
+
.join(' else ')}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Set variant cookie
|
|
546
|
+
const response = {
|
|
547
|
+
status: '200',
|
|
548
|
+
statusDescription: 'OK',
|
|
549
|
+
headers: {
|
|
550
|
+
'set-cookie': [{
|
|
551
|
+
key: 'Set-Cookie',
|
|
552
|
+
value: \`${test.routingStrategy.cookieName || 'ab_variant'}=\${variant}; Path=/; Max-Age=${(test.routingStrategy.sessionDuration || 1440) * 60}\`
|
|
553
|
+
}]
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
// Route to appropriate origin based on variant
|
|
558
|
+
${test.variants
|
|
559
|
+
.map(
|
|
560
|
+
v => `if (variant === '${v.id}') {
|
|
561
|
+
request.origin.custom.domainName = '${v.originId}';
|
|
562
|
+
}`,
|
|
563
|
+
)
|
|
564
|
+
.join(' else ')}
|
|
565
|
+
|
|
566
|
+
callback(null, request);
|
|
567
|
+
};`
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Clear all data
|
|
572
|
+
*/
|
|
573
|
+
clear(): void {
|
|
574
|
+
this.tests.clear()
|
|
575
|
+
this.testCounter = 0
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Global A/B testing manager instance
|
|
581
|
+
*/
|
|
582
|
+
export const abTestManager: ABTestManager = new ABTestManager()
|