lytx 0.3.0
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/.env.example +37 -0
- package/README.md +486 -0
- package/alchemy.run.ts +155 -0
- package/cli/bootstrap-admin.ts +284 -0
- package/cli/deploy-staging.ts +692 -0
- package/cli/import-events.ts +628 -0
- package/cli/import-sites.ts +518 -0
- package/cli/index.ts +609 -0
- package/cli/init-db.ts +269 -0
- package/cli/migrate-to-durable-objects.ts +564 -0
- package/cli/migration-worker.ts +300 -0
- package/cli/performance-test.ts +588 -0
- package/cli/pg/client.ts +4 -0
- package/cli/pg/new-site.ts +153 -0
- package/cli/rollback-durable-objects.ts +622 -0
- package/cli/seed-data.ts +459 -0
- package/cli/setup.js +18 -0
- package/cli/setup.ts +463 -0
- package/cli/validate-migration.ts +200 -0
- package/cli/wrangler-migration.jsonc +28 -0
- package/db/adapter.ts +166 -0
- package/db/analytics_engine/client.ts +0 -0
- package/db/analytics_engine/sites.ts +0 -0
- package/db/client.ts +16 -0
- package/db/d1/client.ts +8 -0
- package/db/d1/drizzle.config.ts +35 -0
- package/db/d1/migrations/0000_true_maelstrom.sql +165 -0
- package/db/d1/migrations/0001_wonderful_bloodaxe.sql +12 -0
- package/db/d1/migrations/0002_late_frightful_four.sql +1 -0
- package/db/d1/migrations/0003_cuddly_obadiah_stane.sql +16 -0
- package/db/d1/migrations/0004_mute_stardust.sql +1 -0
- package/db/d1/migrations/0005_awesome_silvermane.sql +3 -0
- package/db/d1/migrations/0006_volatile_shriek.sql +2 -0
- package/db/d1/migrations/0007_superb_lila_cheney.sql +1 -0
- package/db/d1/migrations/0008_bitter_longshot.sql +17 -0
- package/db/d1/migrations/0009_wonderful_madame_masque.sql +28 -0
- package/db/d1/migrations/meta/0000_snapshot.json +1112 -0
- package/db/d1/migrations/meta/0001_snapshot.json +1187 -0
- package/db/d1/migrations/meta/0002_snapshot.json +1194 -0
- package/db/d1/migrations/meta/0003_snapshot.json +1296 -0
- package/db/d1/migrations/meta/0004_snapshot.json +1303 -0
- package/db/d1/migrations/meta/0005_snapshot.json +1325 -0
- package/db/d1/migrations/meta/0006_snapshot.json +1339 -0
- package/db/d1/migrations/meta/0007_snapshot.json +1347 -0
- package/db/d1/migrations/meta/0008_snapshot.json +1464 -0
- package/db/d1/migrations/meta/0009_snapshot.json +1648 -0
- package/db/d1/migrations/meta/_journal.json +76 -0
- package/db/d1/schema.ts +407 -0
- package/db/d1/sites.ts +374 -0
- package/db/d1/teamAiUsage.ts +101 -0
- package/db/d1/teams.ts +127 -0
- package/db/durable/drizzle.config.ts +8 -0
- package/db/durable/durableObjectClient.ts +480 -0
- package/db/durable/events.ts +100 -0
- package/db/durable/migrations/0000_fair_bucky.sql +38 -0
- package/db/durable/migrations/meta/0000_snapshot.json +278 -0
- package/db/durable/migrations/meta/_journal.json +13 -0
- package/db/durable/migrations/migrations.js +10 -0
- package/db/durable/schema.ts +5 -0
- package/db/durable/siteDurableObject.ts +1352 -0
- package/db/durable/types.ts +53 -0
- package/db/postgres/client.ts +13 -0
- package/db/postgres/drizzle.config.ts +12 -0
- package/db/postgres/migrations/0000_brainy_sprite.sql +116 -0
- package/db/postgres/migrations/meta/0000_snapshot.json +681 -0
- package/db/postgres/migrations/meta/_journal.json +13 -0
- package/db/postgres/schema.ts +145 -0
- package/db/postgres/sites.ts +118 -0
- package/db/tranformReports.ts +595 -0
- package/db/types.ts +55 -0
- package/endpoints/api_worker.tsx +1854 -0
- package/endpoints/site_do_worker.ts +11 -0
- package/index.d.ts +63 -0
- package/index.ts +83 -0
- package/lib/auth.ts +279 -0
- package/lib/geojson/world_countries.json +45307 -0
- package/lib/random_name.ts +41 -0
- package/lib/sendMail.ts +252 -0
- package/package.json +142 -0
- package/public/favicon.ico +0 -0
- package/public/images/android-chrome-192x192.png +0 -0
- package/public/images/android-chrome-512x512.png +0 -0
- package/public/images/apple-touch-icon.png +0 -0
- package/public/images/favicon-16x16.png +0 -0
- package/public/images/favicon-32x32.png +0 -0
- package/public/images/lytx_dark_dashboard.png +0 -0
- package/public/images/lytx_light_dashboard.png +0 -0
- package/public/images/safari-pinned-tab.svg +4 -0
- package/public/logo.png +0 -0
- package/public/site.webmanifest +26 -0
- package/public/sw.js +107 -0
- package/src/Document.tsx +86 -0
- package/src/api/ai_api.ts +1156 -0
- package/src/api/authMiddleware.ts +45 -0
- package/src/api/auth_api.ts +465 -0
- package/src/api/event_labels_api.ts +193 -0
- package/src/api/events_api.ts +210 -0
- package/src/api/queueWorker.ts +303 -0
- package/src/api/reports_api.ts +278 -0
- package/src/api/seed_api.ts +288 -0
- package/src/api/sites_api.ts +904 -0
- package/src/api/tag_api.ts +458 -0
- package/src/api/tag_api_v2.ts +289 -0
- package/src/api/team_api.ts +456 -0
- package/src/app/Dashboard.tsx +1339 -0
- package/src/app/Events.tsx +974 -0
- package/src/app/Explore.tsx +312 -0
- package/src/app/Layout.tsx +58 -0
- package/src/app/Settings.tsx +1302 -0
- package/src/app/components/DashboardCard.tsx +118 -0
- package/src/app/components/EditableCell.tsx +123 -0
- package/src/app/components/EventForm.tsx +93 -0
- package/src/app/components/MarketingFooter.tsx +49 -0
- package/src/app/components/MarketingNav.tsx +150 -0
- package/src/app/components/Nav.tsx +755 -0
- package/src/app/components/NewSiteSetup.tsx +298 -0
- package/src/app/components/SQLEditor.tsx +740 -0
- package/src/app/components/SiteSelector.tsx +126 -0
- package/src/app/components/SiteTag.tsx +42 -0
- package/src/app/components/SiteTagInstallCard.tsx +241 -0
- package/src/app/components/WorldMapCard.tsx +337 -0
- package/src/app/components/charts/ChartComponents.tsx +1481 -0
- package/src/app/components/charts/EventFunnel.tsx +45 -0
- package/src/app/components/charts/EventSummary.tsx +194 -0
- package/src/app/components/charts/SankeyFlows.tsx +72 -0
- package/src/app/components/marketing/CheckIcon.tsx +16 -0
- package/src/app/components/marketing/MarketingLayout.tsx +23 -0
- package/src/app/components/marketing/SectionHeading.tsx +35 -0
- package/src/app/components/reports/AskAiWorkspace.tsx +371 -0
- package/src/app/components/reports/CreateReportStarter.tsx +74 -0
- package/src/app/components/reports/DashboardRouteFiltersContext.tsx +14 -0
- package/src/app/components/reports/DashboardToolbar.tsx +154 -0
- package/src/app/components/reports/DashboardWorkspaceLayout.tsx +63 -0
- package/src/app/components/reports/DashboardWorkspaceShell.tsx +118 -0
- package/src/app/components/reports/ReportBuilderWorkspace.tsx +76 -0
- package/src/app/components/reports/custom/CustomReportBuilderPage.tsx +1667 -0
- package/src/app/components/reports/custom/ReportWidgetChart.tsx +297 -0
- package/src/app/components/reports/custom/buildWidgetSql.ts +151 -0
- package/src/app/components/reports/custom/chartPalettes.ts +18 -0
- package/src/app/components/reports/custom/types.ts +50 -0
- package/src/app/components/reports/reportBuilderMenuItems.ts +17 -0
- package/src/app/components/reports/useDashboardToolbarControls.tsx +235 -0
- package/src/app/components/ui/AlertBanner.tsx +101 -0
- package/src/app/components/ui/Button.tsx +55 -0
- package/src/app/components/ui/Card.tsx +80 -0
- package/src/app/components/ui/Input.tsx +72 -0
- package/src/app/components/ui/Link.tsx +23 -0
- package/src/app/components/ui/ReportBuilderMenu.tsx +246 -0
- package/src/app/components/ui/ThemeToggle.tsx +54 -0
- package/src/app/constants.ts +6 -0
- package/src/app/headers.ts +33 -0
- package/src/app/providers/AuthProvider.tsx +189 -0
- package/src/app/providers/ClientProviders.tsx +18 -0
- package/src/app/providers/QueryProvider.tsx +23 -0
- package/src/app/providers/ThemeProvider.tsx +88 -0
- package/src/app/utils/chartThemes.ts +146 -0
- package/src/app/utils/keybinds.ts +96 -0
- package/src/app/utils/media.tsx +24 -0
- package/src/client.tsx +114 -0
- package/src/config/createLytxAppConfig.ts +252 -0
- package/src/config/resourceNames.ts +88 -0
- package/src/db/index.ts +67 -0
- package/src/index.css +285 -0
- package/src/lib/featureFlags.ts +69 -0
- package/src/pages/GetStarted.tsx +290 -0
- package/src/pages/Home.tsx +268 -0
- package/src/pages/Login.tsx +283 -0
- package/src/pages/PrivacyPolicy.tsx +120 -0
- package/src/pages/Signup.tsx +267 -0
- package/src/pages/TermsOfService.tsx +126 -0
- package/src/pages/VerifyEmail.tsx +56 -0
- package/src/session/durableObject.ts +7 -0
- package/src/session/siteSchema.ts +86 -0
- package/src/session/types.ts +36 -0
- package/src/templates/README.md +80 -0
- package/src/templates/cleanFunctions.js +44 -0
- package/src/templates/embedFunctions.js +52 -0
- package/src/templates/lytx-shared.ts +662 -0
- package/src/templates/lytxpixel-core.ts +144 -0
- package/src/templates/lytxpixel.ts +267 -0
- package/src/templates/lytxpixelBrowser.js +634 -0
- package/src/templates/lytxpixelBrowser.mjs +634 -0
- package/src/templates/parseData.js +12 -0
- package/src/templates/script.ts +31 -0
- package/src/templates/template.tsx +50 -0
- package/src/templates/test.js +3 -0
- package/src/templates/trackWebEvents.ts +177 -0
- package/src/templates/vendors/clickcease.ts +8 -0
- package/src/templates/vendors/google.ts +174 -0
- package/src/templates/vendors/linkedin.ts +23 -0
- package/src/templates/vendors/meta.ts +56 -0
- package/src/templates/vendors/quantcast.ts +22 -0
- package/src/templates/vendors/simplfi.ts +7 -0
- package/src/types/app-context.ts +16 -0
- package/src/utilities/dashboardParams.ts +188 -0
- package/src/utilities/dashboardQueries.ts +537 -0
- package/src/utilities/dashboardTransforms.ts +167 -0
- package/src/utilities/dataValidation.ts +414 -0
- package/src/utilities/detector.ts +73 -0
- package/src/utilities/encrypt.ts +103 -0
- package/src/utilities/index.ts +13 -0
- package/src/utilities/parser.ts +117 -0
- package/src/utilities/performanceMonitoring.ts +570 -0
- package/src/utilities/route_interuptors.ts +24 -0
- package/src/worker.tsx +675 -0
- package/tsconfig.json +78 -0
- package/types/env.d.ts +16 -0
- package/types/rw.d.ts +7 -0
- package/types/shims.d.ts +53 -0
- package/types/vite.d.ts +19 -0
- package/vite/vite-plugin-pixel-bundle.ts +126 -0
- package/vite.config.ts +53 -0
- package/worker-configuration.d.ts +8401 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Performance Testing Suite for Durable Objects
|
|
5
|
+
*
|
|
6
|
+
* This script conducts comprehensive performance testing of the new durable object system,
|
|
7
|
+
* measuring dashboard load times, concurrent user capacity, and event ingestion rates.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* npx tsx cli/performance-test.ts --dashboard-load
|
|
11
|
+
* npx tsx cli/performance-test.ts --event-ingestion
|
|
12
|
+
* npx tsx cli/performance-test.ts --concurrent-users
|
|
13
|
+
* npx tsx cli/performance-test.ts --all
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { performance } from 'perf_hooks';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Performance test configuration
|
|
20
|
+
*/
|
|
21
|
+
interface PerformanceConfig {
|
|
22
|
+
// Dashboard load testing
|
|
23
|
+
dashboardConcurrency: number;
|
|
24
|
+
dashboardIterations: number;
|
|
25
|
+
dashboardTimeout: number;
|
|
26
|
+
|
|
27
|
+
// Event ingestion testing
|
|
28
|
+
eventBatchSizes: number[];
|
|
29
|
+
eventConcurrency: number;
|
|
30
|
+
eventIterations: number;
|
|
31
|
+
|
|
32
|
+
// Concurrent user testing
|
|
33
|
+
maxConcurrentUsers: number;
|
|
34
|
+
userRampUpTime: number;
|
|
35
|
+
testDuration: number;
|
|
36
|
+
|
|
37
|
+
// Target performance metrics
|
|
38
|
+
targets: {
|
|
39
|
+
dashboardLoadTime: number; // ms
|
|
40
|
+
eventIngestionRate: number; // events/second
|
|
41
|
+
concurrentUsers: number;
|
|
42
|
+
errorRate: number; // percentage
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const DEFAULT_CONFIG: PerformanceConfig = {
|
|
47
|
+
dashboardConcurrency: 10,
|
|
48
|
+
dashboardIterations: 100,
|
|
49
|
+
dashboardTimeout: 5000,
|
|
50
|
+
|
|
51
|
+
eventBatchSizes: [1, 10, 50, 100, 500],
|
|
52
|
+
eventConcurrency: 5,
|
|
53
|
+
eventIterations: 50,
|
|
54
|
+
|
|
55
|
+
maxConcurrentUsers: 100,
|
|
56
|
+
userRampUpTime: 30000, // 30 seconds
|
|
57
|
+
testDuration: 120000, // 2 minutes
|
|
58
|
+
|
|
59
|
+
targets: {
|
|
60
|
+
dashboardLoadTime: 100, // Sub-100ms target
|
|
61
|
+
eventIngestionRate: 1000, // 1000 events/second
|
|
62
|
+
concurrentUsers: 50,
|
|
63
|
+
errorRate: 1 // Less than 1% errors
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Performance test results
|
|
69
|
+
*/
|
|
70
|
+
interface TestResults {
|
|
71
|
+
testName: string;
|
|
72
|
+
duration: number;
|
|
73
|
+
iterations: number;
|
|
74
|
+
successCount: number;
|
|
75
|
+
errorCount: number;
|
|
76
|
+
averageTime: number;
|
|
77
|
+
medianTime: number;
|
|
78
|
+
p95Time: number;
|
|
79
|
+
p99Time: number;
|
|
80
|
+
minTime: number;
|
|
81
|
+
maxTime: number;
|
|
82
|
+
throughput: number;
|
|
83
|
+
errorRate: number;
|
|
84
|
+
passed: boolean;
|
|
85
|
+
details?: any;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Mock HTTP client for testing (replace with actual fetch in real environment)
|
|
90
|
+
*/
|
|
91
|
+
class MockHttpClient {
|
|
92
|
+
private baseUrl: string;
|
|
93
|
+
private latencyRange: [number, number];
|
|
94
|
+
private errorRate: number;
|
|
95
|
+
|
|
96
|
+
constructor(baseUrl: string = 'http://localhost:8787', latencyRange: [number, number] = [10, 200], errorRate: number = 0.01) {
|
|
97
|
+
this.baseUrl = baseUrl;
|
|
98
|
+
this.latencyRange = latencyRange;
|
|
99
|
+
this.errorRate = errorRate;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async request(path: string, options: RequestInit = {}): Promise<{ status: number; data: any; time: number }> {
|
|
103
|
+
const startTime = performance.now();
|
|
104
|
+
|
|
105
|
+
// Simulate network latency
|
|
106
|
+
const latency = Math.random() * (this.latencyRange[1] - this.latencyRange[0]) + this.latencyRange[0];
|
|
107
|
+
await new Promise(resolve => setTimeout(resolve, latency));
|
|
108
|
+
|
|
109
|
+
// Simulate occasional errors
|
|
110
|
+
if (Math.random() < this.errorRate) {
|
|
111
|
+
throw new Error(`HTTP ${Math.random() > 0.5 ? '500' : '503'} - Simulated error`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const endTime = performance.now();
|
|
115
|
+
|
|
116
|
+
// Mock response based on path
|
|
117
|
+
let mockData = {};
|
|
118
|
+
if (path.includes('/events/stats')) {
|
|
119
|
+
mockData = {
|
|
120
|
+
totalEvents: Math.floor(Math.random() * 10000),
|
|
121
|
+
uniqueVisitors: Math.floor(Math.random() * 1000),
|
|
122
|
+
topEventTypes: [
|
|
123
|
+
{ label: 'page_view', value: Math.floor(Math.random() * 5000), percentage: 50 },
|
|
124
|
+
{ label: 'click', value: Math.floor(Math.random() * 2000), percentage: 20 }
|
|
125
|
+
]
|
|
126
|
+
};
|
|
127
|
+
} else if (path.includes('/events/timeseries')) {
|
|
128
|
+
mockData = {
|
|
129
|
+
data: Array.from({ length: 30 }, (_, i) => ({
|
|
130
|
+
date: `2024-01-${i + 1}`,
|
|
131
|
+
count: Math.floor(Math.random() * 1000)
|
|
132
|
+
}))
|
|
133
|
+
};
|
|
134
|
+
} else if (path.includes('/events') && options.method === 'POST') {
|
|
135
|
+
mockData = { success: true, inserted: 100 };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
status: 200,
|
|
140
|
+
data: mockData,
|
|
141
|
+
time: endTime - startTime
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Dashboard load performance test
|
|
148
|
+
*/
|
|
149
|
+
async function testDashboardLoad(config: PerformanceConfig): Promise<TestResults> {
|
|
150
|
+
console.log('🔄 Running Dashboard Load Performance Test...');
|
|
151
|
+
|
|
152
|
+
const client = new MockHttpClient();
|
|
153
|
+
const results: number[] = [];
|
|
154
|
+
const errors: string[] = [];
|
|
155
|
+
const startTime = performance.now();
|
|
156
|
+
|
|
157
|
+
// Test different dashboard endpoints
|
|
158
|
+
const endpoints = [
|
|
159
|
+
'/api/events/123/stats',
|
|
160
|
+
'/api/events/123/timeseries?granularity=day',
|
|
161
|
+
'/api/events/123/metrics?type=events&limit=10',
|
|
162
|
+
'/api/events/123/metrics?type=countries&limit=20'
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
const promises = [];
|
|
166
|
+
|
|
167
|
+
for (let i = 0; i < config.dashboardIterations; i++) {
|
|
168
|
+
const promise = (async () => {
|
|
169
|
+
try {
|
|
170
|
+
const endpoint = endpoints[i % endpoints.length];
|
|
171
|
+
const result = await client.request(endpoint);
|
|
172
|
+
results.push(result.time);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
175
|
+
}
|
|
176
|
+
})();
|
|
177
|
+
|
|
178
|
+
promises.push(promise);
|
|
179
|
+
|
|
180
|
+
// Control concurrency
|
|
181
|
+
if (promises.length >= config.dashboardConcurrency) {
|
|
182
|
+
await Promise.all(promises);
|
|
183
|
+
promises.length = 0;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Wait for remaining promises
|
|
188
|
+
if (promises.length > 0) {
|
|
189
|
+
await Promise.all(promises);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const endTime = performance.now();
|
|
193
|
+
const duration = endTime - startTime;
|
|
194
|
+
|
|
195
|
+
// Calculate statistics
|
|
196
|
+
results.sort((a, b) => a - b);
|
|
197
|
+
const successCount = results.length;
|
|
198
|
+
const errorCount = errors.length;
|
|
199
|
+
const averageTime = results.reduce((sum, time) => sum + time, 0) / results.length || 0;
|
|
200
|
+
const medianTime = results[Math.floor(results.length / 2)] || 0;
|
|
201
|
+
const p95Time = results[Math.floor(results.length * 0.95)] || 0;
|
|
202
|
+
const p99Time = results[Math.floor(results.length * 0.99)] || 0;
|
|
203
|
+
const minTime = results[0] || 0;
|
|
204
|
+
const maxTime = results[results.length - 1] || 0;
|
|
205
|
+
const throughput = (successCount / duration) * 1000; // requests per second
|
|
206
|
+
const errorRate = (errorCount / (successCount + errorCount)) * 100;
|
|
207
|
+
|
|
208
|
+
const passed = averageTime <= config.targets.dashboardLoadTime && errorRate <= config.targets.errorRate;
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
testName: 'Dashboard Load Performance',
|
|
212
|
+
duration,
|
|
213
|
+
iterations: config.dashboardIterations,
|
|
214
|
+
successCount,
|
|
215
|
+
errorCount,
|
|
216
|
+
averageTime,
|
|
217
|
+
medianTime,
|
|
218
|
+
p95Time,
|
|
219
|
+
p99Time,
|
|
220
|
+
minTime,
|
|
221
|
+
maxTime,
|
|
222
|
+
throughput,
|
|
223
|
+
errorRate,
|
|
224
|
+
passed,
|
|
225
|
+
details: {
|
|
226
|
+
target: `${config.targets.dashboardLoadTime}ms`,
|
|
227
|
+
endpoints: endpoints.length,
|
|
228
|
+
concurrency: config.dashboardConcurrency
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Event ingestion performance test
|
|
235
|
+
*/
|
|
236
|
+
async function testEventIngestion(config: PerformanceConfig): Promise<TestResults> {
|
|
237
|
+
console.log('🔄 Running Event Ingestion Performance Test...');
|
|
238
|
+
|
|
239
|
+
const client = new MockHttpClient();
|
|
240
|
+
const results: number[] = [];
|
|
241
|
+
const errors: string[] = [];
|
|
242
|
+
const startTime = performance.now();
|
|
243
|
+
let totalEventsProcessed = 0;
|
|
244
|
+
|
|
245
|
+
const promises = [];
|
|
246
|
+
|
|
247
|
+
for (const batchSize of config.eventBatchSizes) {
|
|
248
|
+
console.log(` Testing batch size: ${batchSize} events`);
|
|
249
|
+
|
|
250
|
+
for (let i = 0; i < config.eventIterations; i++) {
|
|
251
|
+
const promise = (async () => {
|
|
252
|
+
try {
|
|
253
|
+
// Generate mock event batch
|
|
254
|
+
const events = Array.from({ length: batchSize }, (_, index) => ({
|
|
255
|
+
event: 'page_view',
|
|
256
|
+
tag_id: 'test-tag-123',
|
|
257
|
+
browser: 'Chrome',
|
|
258
|
+
country: 'US',
|
|
259
|
+
device_type: 'desktop',
|
|
260
|
+
createdAt: new Date().toISOString()
|
|
261
|
+
}));
|
|
262
|
+
|
|
263
|
+
const result = await client.request('/api/events/123', {
|
|
264
|
+
method: 'POST',
|
|
265
|
+
headers: { 'Content-Type': 'application/json' },
|
|
266
|
+
body: JSON.stringify(events)
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
results.push(result.time);
|
|
270
|
+
totalEventsProcessed += batchSize;
|
|
271
|
+
} catch (error) {
|
|
272
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
273
|
+
}
|
|
274
|
+
})();
|
|
275
|
+
|
|
276
|
+
promises.push(promise);
|
|
277
|
+
|
|
278
|
+
// Control concurrency
|
|
279
|
+
if (promises.length >= config.eventConcurrency) {
|
|
280
|
+
await Promise.all(promises);
|
|
281
|
+
promises.length = 0;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Wait for remaining promises
|
|
287
|
+
if (promises.length > 0) {
|
|
288
|
+
await Promise.all(promises);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const endTime = performance.now();
|
|
292
|
+
const duration = endTime - startTime;
|
|
293
|
+
|
|
294
|
+
// Calculate statistics
|
|
295
|
+
results.sort((a, b) => a - b);
|
|
296
|
+
const successCount = results.length;
|
|
297
|
+
const errorCount = errors.length;
|
|
298
|
+
const averageTime = results.reduce((sum, time) => sum + time, 0) / results.length || 0;
|
|
299
|
+
const medianTime = results[Math.floor(results.length / 2)] || 0;
|
|
300
|
+
const p95Time = results[Math.floor(results.length * 0.95)] || 0;
|
|
301
|
+
const p99Time = results[Math.floor(results.length * 0.99)] || 0;
|
|
302
|
+
const minTime = results[0] || 0;
|
|
303
|
+
const maxTime = results[results.length - 1] || 0;
|
|
304
|
+
const throughput = (totalEventsProcessed / duration) * 1000; // events per second
|
|
305
|
+
const errorRate = (errorCount / (successCount + errorCount)) * 100;
|
|
306
|
+
|
|
307
|
+
const passed = throughput >= config.targets.eventIngestionRate && errorRate <= config.targets.errorRate;
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
testName: 'Event Ingestion Performance',
|
|
311
|
+
duration,
|
|
312
|
+
iterations: successCount + errorCount,
|
|
313
|
+
successCount,
|
|
314
|
+
errorCount,
|
|
315
|
+
averageTime,
|
|
316
|
+
medianTime,
|
|
317
|
+
p95Time,
|
|
318
|
+
p99Time,
|
|
319
|
+
minTime,
|
|
320
|
+
maxTime,
|
|
321
|
+
throughput,
|
|
322
|
+
errorRate,
|
|
323
|
+
passed,
|
|
324
|
+
details: {
|
|
325
|
+
target: `${config.targets.eventIngestionRate} events/sec`,
|
|
326
|
+
totalEventsProcessed,
|
|
327
|
+
batchSizes: config.eventBatchSizes,
|
|
328
|
+
concurrency: config.eventConcurrency
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Concurrent users performance test
|
|
335
|
+
*/
|
|
336
|
+
async function testConcurrentUsers(config: PerformanceConfig): Promise<TestResults> {
|
|
337
|
+
console.log('🔄 Running Concurrent Users Performance Test...');
|
|
338
|
+
|
|
339
|
+
const client = new MockHttpClient();
|
|
340
|
+
const results: number[] = [];
|
|
341
|
+
const errors: string[] = [];
|
|
342
|
+
const startTime = performance.now();
|
|
343
|
+
|
|
344
|
+
// Simulate gradual user ramp-up
|
|
345
|
+
const userPromises: Promise<void>[] = [];
|
|
346
|
+
const rampUpInterval = config.userRampUpTime / config.maxConcurrentUsers;
|
|
347
|
+
|
|
348
|
+
for (let userId = 0; userId < config.maxConcurrentUsers; userId++) {
|
|
349
|
+
const userPromise = (async () => {
|
|
350
|
+
// Wait for ramp-up time
|
|
351
|
+
await new Promise(resolve => setTimeout(resolve, userId * rampUpInterval));
|
|
352
|
+
|
|
353
|
+
const userStartTime = performance.now();
|
|
354
|
+
const userEndTime = userStartTime + config.testDuration;
|
|
355
|
+
|
|
356
|
+
// Simulate user activity
|
|
357
|
+
while (performance.now() < userEndTime) {
|
|
358
|
+
try {
|
|
359
|
+
// Random dashboard activity
|
|
360
|
+
const endpoints = [
|
|
361
|
+
'/api/events/123/stats',
|
|
362
|
+
'/api/events/123/timeseries?granularity=day',
|
|
363
|
+
'/api/events/123/metrics?type=events'
|
|
364
|
+
];
|
|
365
|
+
|
|
366
|
+
const endpoint = endpoints[Math.floor(Math.random() * endpoints.length)];
|
|
367
|
+
const result = await client.request(endpoint);
|
|
368
|
+
results.push(result.time);
|
|
369
|
+
|
|
370
|
+
// Wait between requests (simulate user think time)
|
|
371
|
+
await new Promise(resolve => setTimeout(resolve, Math.random() * 2000 + 1000));
|
|
372
|
+
} catch (error) {
|
|
373
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
})();
|
|
377
|
+
|
|
378
|
+
userPromises.push(userPromise);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Wait for all users to complete
|
|
382
|
+
await Promise.all(userPromises);
|
|
383
|
+
|
|
384
|
+
const endTime = performance.now();
|
|
385
|
+
const duration = endTime - startTime;
|
|
386
|
+
|
|
387
|
+
// Calculate statistics
|
|
388
|
+
results.sort((a, b) => a - b);
|
|
389
|
+
const successCount = results.length;
|
|
390
|
+
const errorCount = errors.length;
|
|
391
|
+
const averageTime = results.reduce((sum, time) => sum + time, 0) / results.length || 0;
|
|
392
|
+
const medianTime = results[Math.floor(results.length / 2)] || 0;
|
|
393
|
+
const p95Time = results[Math.floor(results.length * 0.95)] || 0;
|
|
394
|
+
const p99Time = results[Math.floor(results.length * 0.99)] || 0;
|
|
395
|
+
const minTime = results[0] || 0;
|
|
396
|
+
const maxTime = results[results.length - 1] || 0;
|
|
397
|
+
const throughput = (successCount / duration) * 1000; // requests per second
|
|
398
|
+
const errorRate = (errorCount / (successCount + errorCount)) * 100;
|
|
399
|
+
|
|
400
|
+
const passed = config.maxConcurrentUsers >= config.targets.concurrentUsers && errorRate <= config.targets.errorRate;
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
testName: 'Concurrent Users Performance',
|
|
404
|
+
duration,
|
|
405
|
+
iterations: successCount + errorCount,
|
|
406
|
+
successCount,
|
|
407
|
+
errorCount,
|
|
408
|
+
averageTime,
|
|
409
|
+
medianTime,
|
|
410
|
+
p95Time,
|
|
411
|
+
p99Time,
|
|
412
|
+
minTime,
|
|
413
|
+
maxTime,
|
|
414
|
+
throughput,
|
|
415
|
+
errorRate,
|
|
416
|
+
passed,
|
|
417
|
+
details: {
|
|
418
|
+
target: `${config.targets.concurrentUsers} concurrent users`,
|
|
419
|
+
maxConcurrentUsers: config.maxConcurrentUsers,
|
|
420
|
+
rampUpTime: config.userRampUpTime,
|
|
421
|
+
testDuration: config.testDuration
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Generate performance report
|
|
428
|
+
*/
|
|
429
|
+
function generateReport(results: TestResults[]): string {
|
|
430
|
+
const lines: string[] = [];
|
|
431
|
+
|
|
432
|
+
lines.push('🚀 Performance Test Report');
|
|
433
|
+
lines.push('=' .repeat(50));
|
|
434
|
+
lines.push('');
|
|
435
|
+
|
|
436
|
+
let allPassed = true;
|
|
437
|
+
|
|
438
|
+
for (const result of results) {
|
|
439
|
+
const status = result.passed ? '✅ PASSED' : '❌ FAILED';
|
|
440
|
+
allPassed = allPassed && result.passed;
|
|
441
|
+
|
|
442
|
+
lines.push(`${status} ${result.testName}`);
|
|
443
|
+
lines.push(`Duration: ${(result.duration / 1000).toFixed(2)}s`);
|
|
444
|
+
lines.push(`Iterations: ${result.iterations}`);
|
|
445
|
+
lines.push(`Success Rate: ${((result.successCount / result.iterations) * 100).toFixed(1)}%`);
|
|
446
|
+
lines.push(`Error Rate: ${result.errorRate.toFixed(2)}%`);
|
|
447
|
+
lines.push(`Average Time: ${result.averageTime.toFixed(2)}ms`);
|
|
448
|
+
lines.push(`Median Time: ${result.medianTime.toFixed(2)}ms`);
|
|
449
|
+
lines.push(`95th Percentile: ${result.p95Time.toFixed(2)}ms`);
|
|
450
|
+
lines.push(`99th Percentile: ${result.p99Time.toFixed(2)}ms`);
|
|
451
|
+
lines.push(`Throughput: ${result.throughput.toFixed(2)} req/sec`);
|
|
452
|
+
|
|
453
|
+
if (result.details) {
|
|
454
|
+
lines.push(`Target: ${result.details.target}`);
|
|
455
|
+
if (result.details.totalEventsProcessed) {
|
|
456
|
+
lines.push(`Events Processed: ${result.details.totalEventsProcessed}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
lines.push('');
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
lines.push('=' .repeat(50));
|
|
464
|
+
lines.push(`Overall Status: ${allPassed ? '✅ ALL TESTS PASSED' : '❌ SOME TESTS FAILED'}`);
|
|
465
|
+
lines.push('');
|
|
466
|
+
|
|
467
|
+
// Performance recommendations
|
|
468
|
+
lines.push('📊 Performance Analysis:');
|
|
469
|
+
lines.push('');
|
|
470
|
+
|
|
471
|
+
const dashboardTest = results.find(r => r.testName.includes('Dashboard'));
|
|
472
|
+
if (dashboardTest) {
|
|
473
|
+
if (dashboardTest.averageTime <= 100) {
|
|
474
|
+
lines.push('✅ Dashboard load times meet sub-100ms target');
|
|
475
|
+
} else {
|
|
476
|
+
lines.push('⚠️ Dashboard load times exceed target - consider caching optimizations');
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const ingestionTest = results.find(r => r.testName.includes('Ingestion'));
|
|
481
|
+
if (ingestionTest) {
|
|
482
|
+
if (ingestionTest.throughput >= 1000) {
|
|
483
|
+
lines.push('✅ Event ingestion meets 1000+ events/sec target');
|
|
484
|
+
} else {
|
|
485
|
+
lines.push('⚠️ Event ingestion below target - consider batch size optimization');
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const concurrentTest = results.find(r => r.testName.includes('Concurrent'));
|
|
490
|
+
if (concurrentTest) {
|
|
491
|
+
if (concurrentTest.passed) {
|
|
492
|
+
lines.push('✅ Concurrent user capacity meets requirements');
|
|
493
|
+
} else {
|
|
494
|
+
lines.push('⚠️ Concurrent user capacity needs improvement - check resource limits');
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return lines.join('\n');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Main performance testing function
|
|
503
|
+
*/
|
|
504
|
+
async function main() {
|
|
505
|
+
const args = process.argv.slice(2);
|
|
506
|
+
|
|
507
|
+
let runDashboard = false;
|
|
508
|
+
let runIngestion = false;
|
|
509
|
+
let runConcurrent = false;
|
|
510
|
+
|
|
511
|
+
// Parse command line arguments
|
|
512
|
+
for (const arg of args) {
|
|
513
|
+
if (arg === '--dashboard-load') runDashboard = true;
|
|
514
|
+
else if (arg === '--event-ingestion') runIngestion = true;
|
|
515
|
+
else if (arg === '--concurrent-users') runConcurrent = true;
|
|
516
|
+
else if (arg === '--all') {
|
|
517
|
+
runDashboard = runIngestion = runConcurrent = true;
|
|
518
|
+
} else if (arg === '--help') {
|
|
519
|
+
console.log(`
|
|
520
|
+
Performance Testing Suite
|
|
521
|
+
|
|
522
|
+
Usage:
|
|
523
|
+
npx tsx cli/performance-test.ts --dashboard-load # Test dashboard load performance
|
|
524
|
+
npx tsx cli/performance-test.ts --event-ingestion # Test event ingestion performance
|
|
525
|
+
npx tsx cli/performance-test.ts --concurrent-users # Test concurrent user capacity
|
|
526
|
+
npx tsx cli/performance-test.ts --all # Run all performance tests
|
|
527
|
+
npx tsx cli/performance-test.ts --help # Show this help message
|
|
528
|
+
|
|
529
|
+
Target Metrics:
|
|
530
|
+
- Dashboard Load Time: < 100ms average
|
|
531
|
+
- Event Ingestion Rate: > 1000 events/second
|
|
532
|
+
- Concurrent Users: 50+ simultaneous users
|
|
533
|
+
- Error Rate: < 1%
|
|
534
|
+
`);
|
|
535
|
+
process.exit(0);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (!runDashboard && !runIngestion && !runConcurrent) {
|
|
540
|
+
console.error('Error: Must specify at least one test type. Use --help for usage information.');
|
|
541
|
+
process.exit(1);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
console.log('🚀 Starting Performance Test Suite...');
|
|
545
|
+
console.log('');
|
|
546
|
+
|
|
547
|
+
const config = DEFAULT_CONFIG;
|
|
548
|
+
const results: TestResults[] = [];
|
|
549
|
+
|
|
550
|
+
try {
|
|
551
|
+
if (runDashboard) {
|
|
552
|
+
const dashboardResult = await testDashboardLoad(config);
|
|
553
|
+
results.push(dashboardResult);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (runIngestion) {
|
|
557
|
+
const ingestionResult = await testEventIngestion(config);
|
|
558
|
+
results.push(ingestionResult);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (runConcurrent) {
|
|
562
|
+
const concurrentResult = await testConcurrentUsers(config);
|
|
563
|
+
results.push(concurrentResult);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Generate and display report
|
|
567
|
+
const report = generateReport(results);
|
|
568
|
+
console.log('\n' + report);
|
|
569
|
+
|
|
570
|
+
// Exit with appropriate code
|
|
571
|
+
const allPassed = results.every(r => r.passed);
|
|
572
|
+
process.exit(allPassed ? 0 : 1);
|
|
573
|
+
|
|
574
|
+
} catch (error) {
|
|
575
|
+
console.error('Fatal error during performance testing:', error);
|
|
576
|
+
process.exit(1);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Run the performance tests
|
|
581
|
+
if (require.main === module) {
|
|
582
|
+
main().catch(error => {
|
|
583
|
+
console.error('Unhandled error:', error);
|
|
584
|
+
process.exit(1);
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export { testDashboardLoad, testEventIngestion, testConcurrentUsers, generateReport };
|
package/cli/pg/client.ts
ADDED