@vibecheckai/cli 3.5.0 → 3.5.2
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/bin/registry.js +214 -237
- package/bin/runners/cli-utils.js +33 -2
- package/bin/runners/context/analyzer.js +52 -1
- package/bin/runners/context/generators/cursor.js +2 -49
- package/bin/runners/context/git-context.js +3 -1
- package/bin/runners/context/team-conventions.js +33 -7
- package/bin/runners/lib/analysis-core.js +25 -5
- package/bin/runners/lib/analyzers.js +431 -481
- package/bin/runners/lib/default-config.js +127 -0
- package/bin/runners/lib/doctor/modules/security.js +3 -1
- package/bin/runners/lib/engine/ast-cache.js +210 -0
- package/bin/runners/lib/engine/auth-extractor.js +211 -0
- package/bin/runners/lib/engine/billing-extractor.js +112 -0
- package/bin/runners/lib/engine/enforcement-extractor.js +100 -0
- package/bin/runners/lib/engine/env-extractor.js +207 -0
- package/bin/runners/lib/engine/express-extractor.js +208 -0
- package/bin/runners/lib/engine/extractors.js +849 -0
- package/bin/runners/lib/engine/index.js +207 -0
- package/bin/runners/lib/engine/repo-index.js +514 -0
- package/bin/runners/lib/engine/types.js +124 -0
- package/bin/runners/lib/engines/accessibility-engine.js +18 -218
- package/bin/runners/lib/engines/api-consistency-engine.js +30 -335
- package/bin/runners/lib/engines/cross-file-analysis-engine.js +27 -292
- package/bin/runners/lib/engines/empty-catch-engine.js +17 -127
- package/bin/runners/lib/engines/mock-data-engine.js +10 -53
- package/bin/runners/lib/engines/performance-issues-engine.js +36 -176
- package/bin/runners/lib/engines/security-vulnerabilities-engine.js +54 -382
- package/bin/runners/lib/engines/type-aware-engine.js +39 -263
- package/bin/runners/lib/engines/vibecheck-engines/index.js +13 -122
- package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +73 -373
- package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
- package/bin/runners/lib/entitlements-v2.js +73 -97
- package/bin/runners/lib/error-handler.js +44 -3
- package/bin/runners/lib/error-messages.js +289 -0
- package/bin/runners/lib/evidence-pack.js +7 -1
- package/bin/runners/lib/finding-id.js +69 -0
- package/bin/runners/lib/finding-sorter.js +89 -0
- package/bin/runners/lib/html-proof-report.js +700 -350
- package/bin/runners/lib/missions/plan.js +6 -46
- package/bin/runners/lib/missions/templates.js +0 -232
- package/bin/runners/lib/next-action.js +560 -0
- package/bin/runners/lib/prerequisites.js +149 -0
- package/bin/runners/lib/route-detection.js +137 -68
- package/bin/runners/lib/scan-output.js +91 -76
- package/bin/runners/lib/scan-runner.js +135 -0
- package/bin/runners/lib/schemas/ajv-validator.js +464 -0
- package/bin/runners/lib/schemas/error-envelope.schema.json +105 -0
- package/bin/runners/lib/schemas/finding-v3.schema.json +151 -0
- package/bin/runners/lib/schemas/report-artifact.schema.json +120 -0
- package/bin/runners/lib/schemas/run-request.schema.json +108 -0
- package/bin/runners/lib/schemas/validator.js +27 -0
- package/bin/runners/lib/schemas/verdict.schema.json +140 -0
- package/bin/runners/lib/ship-output-enterprise.js +23 -23
- package/bin/runners/lib/ship-output.js +75 -31
- package/bin/runners/lib/terminal-ui.js +6 -113
- package/bin/runners/lib/truth.js +351 -10
- package/bin/runners/lib/unified-cli-output.js +430 -603
- package/bin/runners/lib/unified-output.js +13 -9
- package/bin/runners/runAIAgent.js +10 -5
- package/bin/runners/runAgent.js +0 -3
- package/bin/runners/runAllowlist.js +389 -0
- package/bin/runners/runApprove.js +0 -33
- package/bin/runners/runAuth.js +73 -45
- package/bin/runners/runCheckpoint.js +51 -11
- package/bin/runners/runClassify.js +85 -21
- package/bin/runners/runContext.js +0 -3
- package/bin/runners/runDoctor.js +41 -28
- package/bin/runners/runEvidencePack.js +362 -0
- package/bin/runners/runFirewall.js +0 -3
- package/bin/runners/runFirewallHook.js +0 -3
- package/bin/runners/runFix.js +66 -76
- package/bin/runners/runGuard.js +18 -411
- package/bin/runners/runInit.js +113 -30
- package/bin/runners/runLabs.js +424 -0
- package/bin/runners/runMcp.js +19 -25
- package/bin/runners/runPolish.js +64 -240
- package/bin/runners/runPromptFirewall.js +12 -5
- package/bin/runners/runProve.js +57 -22
- package/bin/runners/runQuickstart.js +531 -0
- package/bin/runners/runReality.js +59 -68
- package/bin/runners/runReport.js +38 -33
- package/bin/runners/runRuntime.js +8 -5
- package/bin/runners/runScan.js +1413 -190
- package/bin/runners/runShip.js +113 -719
- package/bin/runners/runTruth.js +0 -3
- package/bin/runners/runValidate.js +13 -9
- package/bin/runners/runWatch.js +23 -14
- package/bin/scan.js +6 -1
- package/bin/vibecheck.js +204 -185
- package/mcp-server/deprecation-middleware.js +282 -0
- package/mcp-server/handlers/index.ts +15 -0
- package/mcp-server/handlers/tool-handler.ts +554 -0
- package/mcp-server/index-v1.js +698 -0
- package/mcp-server/index.js +210 -238
- package/mcp-server/lib/cache-wrapper.cjs +383 -0
- package/mcp-server/lib/error-envelope.js +138 -0
- package/mcp-server/lib/executor.ts +499 -0
- package/mcp-server/lib/index.ts +19 -0
- package/mcp-server/lib/rate-limiter.js +166 -0
- package/mcp-server/lib/sandbox.test.ts +519 -0
- package/mcp-server/lib/sandbox.ts +395 -0
- package/mcp-server/lib/types.ts +267 -0
- package/mcp-server/package.json +12 -3
- package/mcp-server/registry/tool-registry.js +794 -0
- package/mcp-server/registry/tools.json +605 -0
- package/mcp-server/registry.test.ts +334 -0
- package/mcp-server/tests/tier-gating.test.js +297 -0
- package/mcp-server/tier-auth.js +378 -45
- package/mcp-server/tools-v3.js +353 -442
- package/mcp-server/tsconfig.json +37 -0
- package/mcp-server/vibecheck-2.0-tools.js +14 -1
- package/package.json +1 -1
- package/bin/runners/lib/agent-firewall/learning/learning-engine.js +0 -849
- package/bin/runners/lib/audit-logger.js +0 -532
- package/bin/runners/lib/authority/authorities/architecture.js +0 -364
- package/bin/runners/lib/authority/authorities/compliance.js +0 -341
- package/bin/runners/lib/authority/authorities/human.js +0 -343
- package/bin/runners/lib/authority/authorities/quality.js +0 -420
- package/bin/runners/lib/authority/authorities/security.js +0 -228
- package/bin/runners/lib/authority/index.js +0 -293
- package/bin/runners/lib/bundle/bundle-intelligence.js +0 -846
- package/bin/runners/lib/cli-charts.js +0 -368
- package/bin/runners/lib/cli-config-display.js +0 -405
- package/bin/runners/lib/cli-demo.js +0 -275
- package/bin/runners/lib/cli-errors.js +0 -438
- package/bin/runners/lib/cli-help-formatter.js +0 -439
- package/bin/runners/lib/cli-interactive-menu.js +0 -509
- package/bin/runners/lib/cli-prompts.js +0 -441
- package/bin/runners/lib/cli-scan-cards.js +0 -362
- package/bin/runners/lib/compliance-reporter.js +0 -710
- package/bin/runners/lib/conductor/index.js +0 -671
- package/bin/runners/lib/easy/README.md +0 -123
- package/bin/runners/lib/easy/index.js +0 -140
- package/bin/runners/lib/easy/interactive-wizard.js +0 -788
- package/bin/runners/lib/easy/one-click-firewall.js +0 -564
- package/bin/runners/lib/easy/zero-config-reality.js +0 -714
- package/bin/runners/lib/engines/async-patterns-engine.js +0 -444
- package/bin/runners/lib/engines/bundle-size-engine.js +0 -433
- package/bin/runners/lib/engines/confidence-scoring.js +0 -276
- package/bin/runners/lib/engines/context-detection.js +0 -264
- package/bin/runners/lib/engines/database-patterns-engine.js +0 -429
- package/bin/runners/lib/engines/duplicate-code-engine.js +0 -354
- package/bin/runners/lib/engines/env-variables-engine.js +0 -458
- package/bin/runners/lib/engines/error-handling-engine.js +0 -437
- package/bin/runners/lib/engines/false-positive-prevention.js +0 -630
- package/bin/runners/lib/engines/framework-adapters/index.js +0 -607
- package/bin/runners/lib/engines/framework-detection.js +0 -508
- package/bin/runners/lib/engines/import-order-engine.js +0 -429
- package/bin/runners/lib/engines/naming-conventions-engine.js +0 -544
- package/bin/runners/lib/engines/noise-reduction-engine.js +0 -452
- package/bin/runners/lib/engines/orchestrator.js +0 -334
- package/bin/runners/lib/engines/react-patterns-engine.js +0 -457
- package/bin/runners/lib/engines/vibecheck-engines/lib/ai-hallucination-engine.js +0 -806
- package/bin/runners/lib/engines/vibecheck-engines/lib/smart-fix-engine.js +0 -577
- package/bin/runners/lib/engines/vibecheck-engines/lib/vibe-score-engine.js +0 -543
- package/bin/runners/lib/engines/vibecheck-engines.js +0 -514
- package/bin/runners/lib/enhanced-features/index.js +0 -305
- package/bin/runners/lib/enhanced-output.js +0 -631
- package/bin/runners/lib/enterprise.js +0 -300
- package/bin/runners/lib/firewall/command-validator.js +0 -351
- package/bin/runners/lib/firewall/config.js +0 -341
- package/bin/runners/lib/firewall/content-validator.js +0 -519
- package/bin/runners/lib/firewall/index.js +0 -101
- package/bin/runners/lib/firewall/path-validator.js +0 -256
- package/bin/runners/lib/intelligence/cross-repo-intelligence.js +0 -817
- package/bin/runners/lib/mcp-utils.js +0 -425
- package/bin/runners/lib/output/index.js +0 -1022
- package/bin/runners/lib/policy-engine.js +0 -652
- package/bin/runners/lib/polish/autofix/accessibility-fixes.js +0 -333
- package/bin/runners/lib/polish/autofix/async-handlers.js +0 -273
- package/bin/runners/lib/polish/autofix/dead-code.js +0 -280
- package/bin/runners/lib/polish/autofix/imports-optimizer.js +0 -344
- package/bin/runners/lib/polish/autofix/index.js +0 -200
- package/bin/runners/lib/polish/autofix/remove-consoles.js +0 -209
- package/bin/runners/lib/polish/autofix/strengthen-types.js +0 -245
- package/bin/runners/lib/polish/backend-checks.js +0 -148
- package/bin/runners/lib/polish/documentation-checks.js +0 -111
- package/bin/runners/lib/polish/frontend-checks.js +0 -168
- package/bin/runners/lib/polish/index.js +0 -71
- package/bin/runners/lib/polish/infrastructure-checks.js +0 -131
- package/bin/runners/lib/polish/library-detection.js +0 -175
- package/bin/runners/lib/polish/performance-checks.js +0 -100
- package/bin/runners/lib/polish/security-checks.js +0 -148
- package/bin/runners/lib/polish/utils.js +0 -203
- package/bin/runners/lib/prompt-builder.js +0 -540
- package/bin/runners/lib/proof-certificate.js +0 -634
- package/bin/runners/lib/reality/accessibility-audit.js +0 -946
- package/bin/runners/lib/reality/api-contract-validator.js +0 -1012
- package/bin/runners/lib/reality/chaos-engineering.js +0 -1084
- package/bin/runners/lib/reality/performance-tracker.js +0 -1077
- package/bin/runners/lib/reality/scenario-generator.js +0 -1404
- package/bin/runners/lib/reality/visual-regression.js +0 -852
- package/bin/runners/lib/reality-profiler.js +0 -717
- package/bin/runners/lib/replay/flight-recorder-viewer.js +0 -1160
- package/bin/runners/lib/review/ai-code-review.js +0 -832
- package/bin/runners/lib/rules/custom-rule-engine.js +0 -985
- package/bin/runners/lib/sbom-generator.js +0 -641
- package/bin/runners/lib/scan-output-enhanced.js +0 -512
- package/bin/runners/lib/security/owasp-scanner.js +0 -939
- package/bin/runners/lib/validators/contract-validator.js +0 -283
- package/bin/runners/lib/validators/dead-export-detector.js +0 -279
- package/bin/runners/lib/validators/dep-audit.js +0 -245
- package/bin/runners/lib/validators/env-validator.js +0 -319
- package/bin/runners/lib/validators/index.js +0 -120
- package/bin/runners/lib/validators/license-checker.js +0 -252
- package/bin/runners/lib/validators/route-validator.js +0 -290
- package/bin/runners/runAuthority.js +0 -528
- package/bin/runners/runConductor.js +0 -772
- package/bin/runners/runContainer.js +0 -366
- package/bin/runners/runEasy.js +0 -410
- package/bin/runners/runIaC.js +0 -372
- package/bin/runners/runVibe.js +0 -791
- package/mcp-server/tools.js +0 -495
|
@@ -1,1077 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Performance Regression Detection Engine
|
|
3
|
-
*
|
|
4
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
-
* COMPETITIVE MOAT FEATURE - Automatic Performance Regression Detection
|
|
6
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
7
|
-
*
|
|
8
|
-
* This engine tracks Core Web Vitals and performance metrics during Reality Mode
|
|
9
|
-
* runs, comparing against baselines to detect regressions.
|
|
10
|
-
*
|
|
11
|
-
* Metrics Tracked:
|
|
12
|
-
* - LCP (Largest Contentful Paint) - Loading performance
|
|
13
|
-
* - FID (First Input Delay) - Interactivity
|
|
14
|
-
* - CLS (Cumulative Layout Shift) - Visual stability
|
|
15
|
-
* - TTFB (Time to First Byte) - Server response
|
|
16
|
-
* - FCP (First Contentful Paint) - Initial render
|
|
17
|
-
* - TTI (Time to Interactive) - Full interactivity
|
|
18
|
-
* - TBT (Total Blocking Time) - Main thread blocking
|
|
19
|
-
* - Memory usage - JS heap size
|
|
20
|
-
* - Bundle impact - Resource loading
|
|
21
|
-
*
|
|
22
|
-
* Features:
|
|
23
|
-
* - Baseline comparison with configurable thresholds
|
|
24
|
-
* - Trend analysis (degradation over time)
|
|
25
|
-
* - Budget enforcement (fail if exceeding budgets)
|
|
26
|
-
* - Detailed breakdowns (by resource type, domain)
|
|
27
|
-
* - Interaction performance (click-to-response)
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
"use strict";
|
|
31
|
-
|
|
32
|
-
const fs = require("fs");
|
|
33
|
-
const path = require("path");
|
|
34
|
-
|
|
35
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
36
|
-
// PERFORMANCE THRESHOLDS (based on Google's recommendations)
|
|
37
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
38
|
-
|
|
39
|
-
const THRESHOLDS = {
|
|
40
|
-
// Core Web Vitals
|
|
41
|
-
LCP: {
|
|
42
|
-
good: 2500, // ms - Good
|
|
43
|
-
needsImprovement: 4000, // ms - Needs improvement
|
|
44
|
-
poor: Infinity // ms - Poor
|
|
45
|
-
},
|
|
46
|
-
FID: {
|
|
47
|
-
good: 100,
|
|
48
|
-
needsImprovement: 300,
|
|
49
|
-
poor: Infinity
|
|
50
|
-
},
|
|
51
|
-
CLS: {
|
|
52
|
-
good: 0.1,
|
|
53
|
-
needsImprovement: 0.25,
|
|
54
|
-
poor: Infinity
|
|
55
|
-
},
|
|
56
|
-
|
|
57
|
-
// Additional metrics
|
|
58
|
-
TTFB: {
|
|
59
|
-
good: 200,
|
|
60
|
-
needsImprovement: 500,
|
|
61
|
-
poor: Infinity
|
|
62
|
-
},
|
|
63
|
-
FCP: {
|
|
64
|
-
good: 1800,
|
|
65
|
-
needsImprovement: 3000,
|
|
66
|
-
poor: Infinity
|
|
67
|
-
},
|
|
68
|
-
TTI: {
|
|
69
|
-
good: 3800,
|
|
70
|
-
needsImprovement: 7300,
|
|
71
|
-
poor: Infinity
|
|
72
|
-
},
|
|
73
|
-
TBT: {
|
|
74
|
-
good: 200,
|
|
75
|
-
needsImprovement: 600,
|
|
76
|
-
poor: Infinity
|
|
77
|
-
},
|
|
78
|
-
|
|
79
|
-
// Memory
|
|
80
|
-
heapSize: {
|
|
81
|
-
good: 50 * 1024 * 1024, // 50MB
|
|
82
|
-
needsImprovement: 100 * 1024 * 1024, // 100MB
|
|
83
|
-
poor: Infinity
|
|
84
|
-
},
|
|
85
|
-
|
|
86
|
-
// Bundle sizes
|
|
87
|
-
totalTransfer: {
|
|
88
|
-
good: 1024 * 1024, // 1MB
|
|
89
|
-
needsImprovement: 3 * 1024 * 1024, // 3MB
|
|
90
|
-
poor: Infinity
|
|
91
|
-
},
|
|
92
|
-
jsSize: {
|
|
93
|
-
good: 300 * 1024, // 300KB
|
|
94
|
-
needsImprovement: 500 * 1024, // 500KB
|
|
95
|
-
poor: Infinity
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
// Regression thresholds (% increase that triggers alert)
|
|
100
|
-
const REGRESSION_THRESHOLDS = {
|
|
101
|
-
LCP: 0.2, // 20% regression
|
|
102
|
-
FID: 0.25, // 25% regression
|
|
103
|
-
CLS: 0.5, // 50% regression (CLS is more variable)
|
|
104
|
-
TTFB: 0.3, // 30% regression
|
|
105
|
-
FCP: 0.2, // 20% regression
|
|
106
|
-
TTI: 0.2, // 20% regression
|
|
107
|
-
TBT: 0.25, // 25% regression
|
|
108
|
-
heapSize: 0.3, // 30% regression
|
|
109
|
-
totalTransfer: 0.15, // 15% regression
|
|
110
|
-
jsSize: 0.1 // 10% regression (bundle bloat is bad)
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
114
|
-
// PERFORMANCE TRACKER CLASS
|
|
115
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
116
|
-
|
|
117
|
-
class PerformanceTracker {
|
|
118
|
-
constructor(options = {}) {
|
|
119
|
-
this.projectRoot = options.projectRoot || process.cwd();
|
|
120
|
-
this.baselinePath = options.baselinePath ||
|
|
121
|
-
path.join(this.projectRoot, ".vibecheck", "performance-baselines");
|
|
122
|
-
this.historyPath = options.historyPath ||
|
|
123
|
-
path.join(this.projectRoot, ".vibecheck", "performance-history");
|
|
124
|
-
this.thresholds = { ...THRESHOLDS, ...options.thresholds };
|
|
125
|
-
this.regressionThresholds = { ...REGRESSION_THRESHOLDS, ...options.regressionThresholds };
|
|
126
|
-
this.page = null;
|
|
127
|
-
this.cdpSession = null;
|
|
128
|
-
this.metrics = [];
|
|
129
|
-
this.resources = [];
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Set the Playwright page and enable performance tracking
|
|
134
|
-
*/
|
|
135
|
-
async setPage(page) {
|
|
136
|
-
this.page = page;
|
|
137
|
-
|
|
138
|
-
// Create CDP session for detailed metrics
|
|
139
|
-
try {
|
|
140
|
-
this.cdpSession = await page.context().newCDPSession(page);
|
|
141
|
-
await this.cdpSession.send("Performance.enable");
|
|
142
|
-
} catch {
|
|
143
|
-
// CDP not available (e.g., Firefox)
|
|
144
|
-
this.cdpSession = null;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Start tracking for a page load
|
|
150
|
-
*/
|
|
151
|
-
async startTracking() {
|
|
152
|
-
this.metrics = [];
|
|
153
|
-
this.resources = [];
|
|
154
|
-
this.startTime = Date.now();
|
|
155
|
-
|
|
156
|
-
// Track resource loading
|
|
157
|
-
if (this.page) {
|
|
158
|
-
this.page.on("response", this.trackResource.bind(this));
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Track individual resource
|
|
164
|
-
*/
|
|
165
|
-
async trackResource(response) {
|
|
166
|
-
try {
|
|
167
|
-
const request = response.request();
|
|
168
|
-
const timing = await response.timing();
|
|
169
|
-
|
|
170
|
-
this.resources.push({
|
|
171
|
-
url: request.url(),
|
|
172
|
-
method: request.method(),
|
|
173
|
-
resourceType: request.resourceType(),
|
|
174
|
-
status: response.status(),
|
|
175
|
-
transferSize: (await response.headers())["content-length"] || 0,
|
|
176
|
-
timing: {
|
|
177
|
-
dns: timing?.dnsEnd - timing?.dnsStart || 0,
|
|
178
|
-
connect: timing?.connectEnd - timing?.connectStart || 0,
|
|
179
|
-
ssl: timing?.sslEnd - timing?.sslStart || 0,
|
|
180
|
-
ttfb: timing?.responseStart - timing?.requestStart || 0,
|
|
181
|
-
download: timing?.responseEnd - timing?.responseStart || 0,
|
|
182
|
-
total: timing?.responseEnd - timing?.requestStart || 0
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
} catch {
|
|
186
|
-
// Ignore tracking errors
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Collect all performance metrics
|
|
192
|
-
*/
|
|
193
|
-
async collectMetrics() {
|
|
194
|
-
if (!this.page) {
|
|
195
|
-
throw new Error("Page not set. Call setPage() first.");
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const metrics = {
|
|
199
|
-
timestamp: new Date().toISOString(),
|
|
200
|
-
url: this.page.url(),
|
|
201
|
-
|
|
202
|
-
// Core Web Vitals
|
|
203
|
-
coreWebVitals: await this.collectCoreWebVitals(),
|
|
204
|
-
|
|
205
|
-
// Navigation timing
|
|
206
|
-
navigation: await this.collectNavigationTiming(),
|
|
207
|
-
|
|
208
|
-
// Memory
|
|
209
|
-
memory: await this.collectMemoryMetrics(),
|
|
210
|
-
|
|
211
|
-
// Resources
|
|
212
|
-
resources: this.analyzeResources(),
|
|
213
|
-
|
|
214
|
-
// Long tasks (main thread blocking)
|
|
215
|
-
longTasks: await this.collectLongTasks(),
|
|
216
|
-
|
|
217
|
-
// Layout shifts
|
|
218
|
-
layoutShifts: await this.collectLayoutShifts()
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
this.metrics.push(metrics);
|
|
222
|
-
return metrics;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Collect Core Web Vitals
|
|
227
|
-
*/
|
|
228
|
-
async collectCoreWebVitals() {
|
|
229
|
-
return await this.page.evaluate(() => {
|
|
230
|
-
return new Promise((resolve) => {
|
|
231
|
-
const vitals = {
|
|
232
|
-
LCP: null,
|
|
233
|
-
FID: null,
|
|
234
|
-
CLS: 0
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
// LCP
|
|
238
|
-
new PerformanceObserver((list) => {
|
|
239
|
-
const entries = list.getEntries();
|
|
240
|
-
const lastEntry = entries[entries.length - 1];
|
|
241
|
-
vitals.LCP = lastEntry?.startTime || null;
|
|
242
|
-
}).observe({ type: "largest-contentful-paint", buffered: true });
|
|
243
|
-
|
|
244
|
-
// FID (approximated with first-input if available)
|
|
245
|
-
new PerformanceObserver((list) => {
|
|
246
|
-
const entries = list.getEntries();
|
|
247
|
-
if (entries.length > 0) {
|
|
248
|
-
vitals.FID = entries[0].processingStart - entries[0].startTime;
|
|
249
|
-
}
|
|
250
|
-
}).observe({ type: "first-input", buffered: true });
|
|
251
|
-
|
|
252
|
-
// CLS
|
|
253
|
-
new PerformanceObserver((list) => {
|
|
254
|
-
for (const entry of list.getEntries()) {
|
|
255
|
-
if (!entry.hadRecentInput) {
|
|
256
|
-
vitals.CLS += entry.value;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}).observe({ type: "layout-shift", buffered: true });
|
|
260
|
-
|
|
261
|
-
// Wait a bit for metrics to populate
|
|
262
|
-
setTimeout(() => resolve(vitals), 1000);
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Collect navigation timing metrics
|
|
269
|
-
*/
|
|
270
|
-
async collectNavigationTiming() {
|
|
271
|
-
return await this.page.evaluate(() => {
|
|
272
|
-
const nav = performance.getEntriesByType("navigation")[0];
|
|
273
|
-
if (!nav) return null;
|
|
274
|
-
|
|
275
|
-
return {
|
|
276
|
-
TTFB: nav.responseStart - nav.requestStart,
|
|
277
|
-
FCP: performance.getEntriesByType("paint")
|
|
278
|
-
.find(e => e.name === "first-contentful-paint")?.startTime || null,
|
|
279
|
-
domContentLoaded: nav.domContentLoadedEventEnd - nav.startTime,
|
|
280
|
-
load: nav.loadEventEnd - nav.startTime,
|
|
281
|
-
|
|
282
|
-
// Breakdown
|
|
283
|
-
dns: nav.domainLookupEnd - nav.domainLookupStart,
|
|
284
|
-
tcp: nav.connectEnd - nav.connectStart,
|
|
285
|
-
ssl: nav.secureConnectionStart > 0
|
|
286
|
-
? nav.connectEnd - nav.secureConnectionStart
|
|
287
|
-
: 0,
|
|
288
|
-
request: nav.responseStart - nav.requestStart,
|
|
289
|
-
response: nav.responseEnd - nav.responseStart,
|
|
290
|
-
processing: nav.domComplete - nav.responseEnd,
|
|
291
|
-
|
|
292
|
-
// Transfer sizes
|
|
293
|
-
transferSize: nav.transferSize,
|
|
294
|
-
encodedBodySize: nav.encodedBodySize,
|
|
295
|
-
decodedBodySize: nav.decodedBodySize
|
|
296
|
-
};
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Collect memory metrics
|
|
302
|
-
*/
|
|
303
|
-
async collectMemoryMetrics() {
|
|
304
|
-
return await this.page.evaluate(() => {
|
|
305
|
-
if (performance.memory) {
|
|
306
|
-
return {
|
|
307
|
-
usedJSHeapSize: performance.memory.usedJSHeapSize,
|
|
308
|
-
totalJSHeapSize: performance.memory.totalJSHeapSize,
|
|
309
|
-
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
return null;
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Analyze collected resources
|
|
318
|
-
*/
|
|
319
|
-
analyzeResources() {
|
|
320
|
-
const byType = {};
|
|
321
|
-
let totalTransfer = 0;
|
|
322
|
-
let totalRequests = 0;
|
|
323
|
-
|
|
324
|
-
for (const resource of this.resources) {
|
|
325
|
-
const type = resource.resourceType;
|
|
326
|
-
const size = parseInt(resource.transferSize) || 0;
|
|
327
|
-
|
|
328
|
-
if (!byType[type]) {
|
|
329
|
-
byType[type] = { count: 0, size: 0, avgTiming: 0 };
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
byType[type].count++;
|
|
333
|
-
byType[type].size += size;
|
|
334
|
-
byType[type].avgTiming += resource.timing.total;
|
|
335
|
-
|
|
336
|
-
totalTransfer += size;
|
|
337
|
-
totalRequests++;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Calculate averages
|
|
341
|
-
for (const type of Object.keys(byType)) {
|
|
342
|
-
if (byType[type].count > 0) {
|
|
343
|
-
byType[type].avgTiming /= byType[type].count;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
return {
|
|
348
|
-
totalRequests,
|
|
349
|
-
totalTransfer,
|
|
350
|
-
byType,
|
|
351
|
-
jsSize: byType.script?.size || 0,
|
|
352
|
-
cssSize: byType.stylesheet?.size || 0,
|
|
353
|
-
imageSize: byType.image?.size || 0,
|
|
354
|
-
fontSize: byType.font?.size || 0
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Collect long tasks (blocking main thread)
|
|
360
|
-
*/
|
|
361
|
-
async collectLongTasks() {
|
|
362
|
-
return await this.page.evaluate(() => {
|
|
363
|
-
return new Promise((resolve) => {
|
|
364
|
-
const longTasks = [];
|
|
365
|
-
|
|
366
|
-
new PerformanceObserver((list) => {
|
|
367
|
-
for (const entry of list.getEntries()) {
|
|
368
|
-
longTasks.push({
|
|
369
|
-
startTime: entry.startTime,
|
|
370
|
-
duration: entry.duration,
|
|
371
|
-
name: entry.name
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
}).observe({ type: "longtask", buffered: true });
|
|
375
|
-
|
|
376
|
-
setTimeout(() => {
|
|
377
|
-
const totalBlockingTime = longTasks
|
|
378
|
-
.filter(t => t.duration > 50)
|
|
379
|
-
.reduce((sum, t) => sum + (t.duration - 50), 0);
|
|
380
|
-
|
|
381
|
-
resolve({
|
|
382
|
-
count: longTasks.length,
|
|
383
|
-
totalBlockingTime,
|
|
384
|
-
tasks: longTasks.slice(0, 10) // Top 10
|
|
385
|
-
});
|
|
386
|
-
}, 500);
|
|
387
|
-
});
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Collect layout shift details
|
|
393
|
-
*/
|
|
394
|
-
async collectLayoutShifts() {
|
|
395
|
-
return await this.page.evaluate(() => {
|
|
396
|
-
return new Promise((resolve) => {
|
|
397
|
-
const shifts = [];
|
|
398
|
-
let cumulativeScore = 0;
|
|
399
|
-
|
|
400
|
-
new PerformanceObserver((list) => {
|
|
401
|
-
for (const entry of list.getEntries()) {
|
|
402
|
-
if (!entry.hadRecentInput) {
|
|
403
|
-
cumulativeScore += entry.value;
|
|
404
|
-
shifts.push({
|
|
405
|
-
value: entry.value,
|
|
406
|
-
startTime: entry.startTime,
|
|
407
|
-
sources: entry.sources?.map(s => ({
|
|
408
|
-
node: s.node?.nodeName,
|
|
409
|
-
previousRect: s.previousRect,
|
|
410
|
-
currentRect: s.currentRect
|
|
411
|
-
})) || []
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}).observe({ type: "layout-shift", buffered: true });
|
|
416
|
-
|
|
417
|
-
setTimeout(() => {
|
|
418
|
-
resolve({
|
|
419
|
-
cumulativeScore,
|
|
420
|
-
shiftCount: shifts.length,
|
|
421
|
-
largestShift: Math.max(...shifts.map(s => s.value), 0),
|
|
422
|
-
shifts: shifts.slice(0, 5) // Top 5
|
|
423
|
-
});
|
|
424
|
-
}, 500);
|
|
425
|
-
});
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
430
|
-
// BASELINE COMPARISON
|
|
431
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
432
|
-
|
|
433
|
-
/**
|
|
434
|
-
* Compare metrics against baseline
|
|
435
|
-
*/
|
|
436
|
-
async compareWithBaseline(metrics, pageName) {
|
|
437
|
-
const baseline = this.loadBaseline(pageName);
|
|
438
|
-
|
|
439
|
-
if (!baseline) {
|
|
440
|
-
return {
|
|
441
|
-
hasBaseline: false,
|
|
442
|
-
message: "No baseline found. Current metrics will be saved as baseline."
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
const comparison = {
|
|
447
|
-
hasBaseline: true,
|
|
448
|
-
regressions: [],
|
|
449
|
-
improvements: [],
|
|
450
|
-
unchanged: [],
|
|
451
|
-
details: {}
|
|
452
|
-
};
|
|
453
|
-
|
|
454
|
-
// Compare Core Web Vitals
|
|
455
|
-
this.compareMetric(comparison, "LCP",
|
|
456
|
-
metrics.coreWebVitals?.LCP,
|
|
457
|
-
baseline.coreWebVitals?.LCP
|
|
458
|
-
);
|
|
459
|
-
|
|
460
|
-
this.compareMetric(comparison, "FID",
|
|
461
|
-
metrics.coreWebVitals?.FID,
|
|
462
|
-
baseline.coreWebVitals?.FID
|
|
463
|
-
);
|
|
464
|
-
|
|
465
|
-
this.compareMetric(comparison, "CLS",
|
|
466
|
-
metrics.coreWebVitals?.CLS,
|
|
467
|
-
baseline.coreWebVitals?.CLS
|
|
468
|
-
);
|
|
469
|
-
|
|
470
|
-
// Compare navigation timing
|
|
471
|
-
this.compareMetric(comparison, "TTFB",
|
|
472
|
-
metrics.navigation?.TTFB,
|
|
473
|
-
baseline.navigation?.TTFB
|
|
474
|
-
);
|
|
475
|
-
|
|
476
|
-
this.compareMetric(comparison, "FCP",
|
|
477
|
-
metrics.navigation?.FCP,
|
|
478
|
-
baseline.navigation?.FCP
|
|
479
|
-
);
|
|
480
|
-
|
|
481
|
-
// Compare memory
|
|
482
|
-
this.compareMetric(comparison, "heapSize",
|
|
483
|
-
metrics.memory?.usedJSHeapSize,
|
|
484
|
-
baseline.memory?.usedJSHeapSize
|
|
485
|
-
);
|
|
486
|
-
|
|
487
|
-
// Compare bundle sizes
|
|
488
|
-
this.compareMetric(comparison, "totalTransfer",
|
|
489
|
-
metrics.resources?.totalTransfer,
|
|
490
|
-
baseline.resources?.totalTransfer
|
|
491
|
-
);
|
|
492
|
-
|
|
493
|
-
this.compareMetric(comparison, "jsSize",
|
|
494
|
-
metrics.resources?.jsSize,
|
|
495
|
-
baseline.resources?.jsSize
|
|
496
|
-
);
|
|
497
|
-
|
|
498
|
-
// Compare TBT
|
|
499
|
-
this.compareMetric(comparison, "TBT",
|
|
500
|
-
metrics.longTasks?.totalBlockingTime,
|
|
501
|
-
baseline.longTasks?.totalBlockingTime
|
|
502
|
-
);
|
|
503
|
-
|
|
504
|
-
return comparison;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
/**
|
|
508
|
-
* Compare individual metric
|
|
509
|
-
*/
|
|
510
|
-
compareMetric(comparison, name, current, baseline) {
|
|
511
|
-
if (current === null || current === undefined ||
|
|
512
|
-
baseline === null || baseline === undefined) {
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
const threshold = this.regressionThresholds[name] || 0.2;
|
|
517
|
-
const change = (current - baseline) / baseline;
|
|
518
|
-
const changePercent = (change * 100).toFixed(1);
|
|
519
|
-
|
|
520
|
-
const detail = {
|
|
521
|
-
name,
|
|
522
|
-
current,
|
|
523
|
-
baseline,
|
|
524
|
-
change: changePercent + "%",
|
|
525
|
-
status: "unchanged"
|
|
526
|
-
};
|
|
527
|
-
|
|
528
|
-
// Check if regression
|
|
529
|
-
if (change > threshold) {
|
|
530
|
-
detail.status = "regression";
|
|
531
|
-
detail.severity = this.getSeverity(name, current);
|
|
532
|
-
comparison.regressions.push(detail);
|
|
533
|
-
}
|
|
534
|
-
// Check if improvement
|
|
535
|
-
else if (change < -threshold) {
|
|
536
|
-
detail.status = "improvement";
|
|
537
|
-
comparison.improvements.push(detail);
|
|
538
|
-
}
|
|
539
|
-
else {
|
|
540
|
-
comparison.unchanged.push(detail);
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
comparison.details[name] = detail;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
/**
|
|
547
|
-
* Get severity based on absolute value vs thresholds
|
|
548
|
-
*/
|
|
549
|
-
getSeverity(metricName, value) {
|
|
550
|
-
const threshold = this.thresholds[metricName];
|
|
551
|
-
if (!threshold) return "WARN";
|
|
552
|
-
|
|
553
|
-
if (value <= threshold.good) return "INFO";
|
|
554
|
-
if (value <= threshold.needsImprovement) return "WARN";
|
|
555
|
-
return "BLOCK";
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
559
|
-
// BASELINE MANAGEMENT
|
|
560
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* Save metrics as baseline
|
|
564
|
-
*/
|
|
565
|
-
saveBaseline(metrics, pageName) {
|
|
566
|
-
if (!fs.existsSync(this.baselinePath)) {
|
|
567
|
-
fs.mkdirSync(this.baselinePath, { recursive: true });
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
const fileName = this.sanitizeName(pageName) + ".json";
|
|
571
|
-
const filePath = path.join(this.baselinePath, fileName);
|
|
572
|
-
|
|
573
|
-
fs.writeFileSync(filePath, JSON.stringify({
|
|
574
|
-
...metrics,
|
|
575
|
-
savedAt: new Date().toISOString()
|
|
576
|
-
}, null, 2));
|
|
577
|
-
|
|
578
|
-
return filePath;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
/**
|
|
582
|
-
* Load baseline metrics
|
|
583
|
-
*/
|
|
584
|
-
loadBaseline(pageName) {
|
|
585
|
-
const fileName = this.sanitizeName(pageName) + ".json";
|
|
586
|
-
const filePath = path.join(this.baselinePath, fileName);
|
|
587
|
-
|
|
588
|
-
if (!fs.existsSync(filePath)) {
|
|
589
|
-
return null;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
try {
|
|
593
|
-
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
594
|
-
} catch {
|
|
595
|
-
return null;
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
/**
|
|
600
|
-
* Save to history for trend analysis
|
|
601
|
-
*/
|
|
602
|
-
saveToHistory(metrics, pageName) {
|
|
603
|
-
const historyDir = path.join(this.historyPath, this.sanitizeName(pageName));
|
|
604
|
-
|
|
605
|
-
if (!fs.existsSync(historyDir)) {
|
|
606
|
-
fs.mkdirSync(historyDir, { recursive: true });
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
610
|
-
const fileName = `${timestamp}.json`;
|
|
611
|
-
const filePath = path.join(historyDir, fileName);
|
|
612
|
-
|
|
613
|
-
fs.writeFileSync(filePath, JSON.stringify(metrics, null, 2));
|
|
614
|
-
|
|
615
|
-
// Keep only last 30 entries
|
|
616
|
-
this.pruneHistory(historyDir, 30);
|
|
617
|
-
|
|
618
|
-
return filePath;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
/**
|
|
622
|
-
* Prune old history entries
|
|
623
|
-
*/
|
|
624
|
-
pruneHistory(historyDir, keepCount) {
|
|
625
|
-
const files = fs.readdirSync(historyDir)
|
|
626
|
-
.filter(f => f.endsWith(".json"))
|
|
627
|
-
.sort()
|
|
628
|
-
.reverse();
|
|
629
|
-
|
|
630
|
-
for (const file of files.slice(keepCount)) {
|
|
631
|
-
fs.unlinkSync(path.join(historyDir, file));
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
/**
|
|
636
|
-
* Get trend analysis
|
|
637
|
-
*/
|
|
638
|
-
getTrendAnalysis(pageName) {
|
|
639
|
-
const historyDir = path.join(this.historyPath, this.sanitizeName(pageName));
|
|
640
|
-
|
|
641
|
-
if (!fs.existsSync(historyDir)) {
|
|
642
|
-
return null;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
const files = fs.readdirSync(historyDir)
|
|
646
|
-
.filter(f => f.endsWith(".json"))
|
|
647
|
-
.sort()
|
|
648
|
-
.reverse()
|
|
649
|
-
.slice(0, 10); // Last 10 runs
|
|
650
|
-
|
|
651
|
-
if (files.length < 2) {
|
|
652
|
-
return null;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
const history = files.map(f => {
|
|
656
|
-
const data = JSON.parse(fs.readFileSync(path.join(historyDir, f), "utf8"));
|
|
657
|
-
return {
|
|
658
|
-
timestamp: data.timestamp,
|
|
659
|
-
LCP: data.coreWebVitals?.LCP,
|
|
660
|
-
FID: data.coreWebVitals?.FID,
|
|
661
|
-
CLS: data.coreWebVitals?.CLS,
|
|
662
|
-
TTFB: data.navigation?.TTFB,
|
|
663
|
-
jsSize: data.resources?.jsSize,
|
|
664
|
-
heapSize: data.memory?.usedJSHeapSize
|
|
665
|
-
};
|
|
666
|
-
});
|
|
667
|
-
|
|
668
|
-
// Calculate trends
|
|
669
|
-
const trends = {};
|
|
670
|
-
const metrics = ["LCP", "FID", "CLS", "TTFB", "jsSize", "heapSize"];
|
|
671
|
-
|
|
672
|
-
for (const metric of metrics) {
|
|
673
|
-
const values = history.map(h => h[metric]).filter(v => v !== null && v !== undefined);
|
|
674
|
-
if (values.length >= 2) {
|
|
675
|
-
const recent = values.slice(0, 3).reduce((a, b) => a + b, 0) / Math.min(3, values.length);
|
|
676
|
-
const older = values.slice(-3).reduce((a, b) => a + b, 0) / Math.min(3, values.length);
|
|
677
|
-
const change = ((recent - older) / older) * 100;
|
|
678
|
-
|
|
679
|
-
trends[metric] = {
|
|
680
|
-
direction: change > 5 ? "degrading" : change < -5 ? "improving" : "stable",
|
|
681
|
-
changePercent: change.toFixed(1),
|
|
682
|
-
recentAvg: recent,
|
|
683
|
-
historicalAvg: older
|
|
684
|
-
};
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
return {
|
|
689
|
-
sampleCount: history.length,
|
|
690
|
-
timeRange: {
|
|
691
|
-
oldest: history[history.length - 1]?.timestamp,
|
|
692
|
-
newest: history[0]?.timestamp
|
|
693
|
-
},
|
|
694
|
-
trends,
|
|
695
|
-
history: history.slice(0, 5) // Last 5 for display
|
|
696
|
-
};
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
700
|
-
// INTERACTION PERFORMANCE
|
|
701
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
702
|
-
|
|
703
|
-
/**
|
|
704
|
-
* Measure interaction performance (click-to-response)
|
|
705
|
-
*/
|
|
706
|
-
async measureInteraction(selector, action = "click") {
|
|
707
|
-
if (!this.page) {
|
|
708
|
-
throw new Error("Page not set");
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
const startTime = Date.now();
|
|
712
|
-
|
|
713
|
-
// Track network requests during interaction
|
|
714
|
-
const requests = [];
|
|
715
|
-
const requestHandler = (request) => {
|
|
716
|
-
requests.push({
|
|
717
|
-
url: request.url(),
|
|
718
|
-
type: request.resourceType(),
|
|
719
|
-
startTime: Date.now() - startTime
|
|
720
|
-
});
|
|
721
|
-
};
|
|
722
|
-
|
|
723
|
-
this.page.on("request", requestHandler);
|
|
724
|
-
|
|
725
|
-
// Track DOM changes
|
|
726
|
-
const domChangePromise = this.page.evaluate(() => {
|
|
727
|
-
return new Promise((resolve) => {
|
|
728
|
-
const observer = new MutationObserver((mutations) => {
|
|
729
|
-
observer.disconnect();
|
|
730
|
-
resolve({
|
|
731
|
-
changeCount: mutations.length,
|
|
732
|
-
timestamp: performance.now()
|
|
733
|
-
});
|
|
734
|
-
});
|
|
735
|
-
|
|
736
|
-
observer.observe(document.body, {
|
|
737
|
-
childList: true,
|
|
738
|
-
subtree: true,
|
|
739
|
-
attributes: true
|
|
740
|
-
});
|
|
741
|
-
|
|
742
|
-
// Timeout after 5s
|
|
743
|
-
setTimeout(() => {
|
|
744
|
-
observer.disconnect();
|
|
745
|
-
resolve({ changeCount: 0, timeout: true });
|
|
746
|
-
}, 5000);
|
|
747
|
-
});
|
|
748
|
-
});
|
|
749
|
-
|
|
750
|
-
// Perform interaction
|
|
751
|
-
const element = await this.page.$(selector);
|
|
752
|
-
if (!element) {
|
|
753
|
-
throw new Error(`Element not found: ${selector}`);
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
if (action === "click") {
|
|
757
|
-
await element.click();
|
|
758
|
-
} else if (action === "hover") {
|
|
759
|
-
await element.hover();
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
// Wait for response
|
|
763
|
-
const domChange = await domChangePromise;
|
|
764
|
-
const endTime = Date.now();
|
|
765
|
-
|
|
766
|
-
this.page.off("request", requestHandler);
|
|
767
|
-
|
|
768
|
-
return {
|
|
769
|
-
selector,
|
|
770
|
-
action,
|
|
771
|
-
duration: endTime - startTime,
|
|
772
|
-
domChangeTime: domChange.timestamp,
|
|
773
|
-
domChangeCount: domChange.changeCount,
|
|
774
|
-
networkRequests: requests.length,
|
|
775
|
-
requests: requests.slice(0, 5),
|
|
776
|
-
rating: this.rateInteractionSpeed(endTime - startTime)
|
|
777
|
-
};
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
/**
|
|
781
|
-
* Rate interaction speed
|
|
782
|
-
*/
|
|
783
|
-
rateInteractionSpeed(duration) {
|
|
784
|
-
if (duration < 100) return { rating: "instant", color: "green" };
|
|
785
|
-
if (duration < 300) return { rating: "fast", color: "green" };
|
|
786
|
-
if (duration < 1000) return { rating: "acceptable", color: "yellow" };
|
|
787
|
-
if (duration < 3000) return { rating: "slow", color: "orange" };
|
|
788
|
-
return { rating: "very slow", color: "red" };
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
792
|
-
// BUDGETS
|
|
793
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
794
|
-
|
|
795
|
-
/**
|
|
796
|
-
* Check against performance budgets
|
|
797
|
-
*/
|
|
798
|
-
checkBudgets(metrics, budgets = {}) {
|
|
799
|
-
const defaultBudgets = {
|
|
800
|
-
LCP: 2500,
|
|
801
|
-
FID: 100,
|
|
802
|
-
CLS: 0.1,
|
|
803
|
-
TTFB: 200,
|
|
804
|
-
totalTransfer: 1024 * 1024, // 1MB
|
|
805
|
-
jsSize: 300 * 1024, // 300KB
|
|
806
|
-
requestCount: 50
|
|
807
|
-
};
|
|
808
|
-
|
|
809
|
-
const effectiveBudgets = { ...defaultBudgets, ...budgets };
|
|
810
|
-
const violations = [];
|
|
811
|
-
const passing = [];
|
|
812
|
-
|
|
813
|
-
// Check each budget
|
|
814
|
-
if (metrics.coreWebVitals?.LCP > effectiveBudgets.LCP) {
|
|
815
|
-
violations.push({
|
|
816
|
-
metric: "LCP",
|
|
817
|
-
budget: effectiveBudgets.LCP,
|
|
818
|
-
actual: metrics.coreWebVitals.LCP,
|
|
819
|
-
overage: ((metrics.coreWebVitals.LCP / effectiveBudgets.LCP - 1) * 100).toFixed(0) + "%"
|
|
820
|
-
});
|
|
821
|
-
} else {
|
|
822
|
-
passing.push({ metric: "LCP", within: true });
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
if (metrics.coreWebVitals?.CLS > effectiveBudgets.CLS) {
|
|
826
|
-
violations.push({
|
|
827
|
-
metric: "CLS",
|
|
828
|
-
budget: effectiveBudgets.CLS,
|
|
829
|
-
actual: metrics.coreWebVitals.CLS,
|
|
830
|
-
overage: ((metrics.coreWebVitals.CLS / effectiveBudgets.CLS - 1) * 100).toFixed(0) + "%"
|
|
831
|
-
});
|
|
832
|
-
} else {
|
|
833
|
-
passing.push({ metric: "CLS", within: true });
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
if (metrics.resources?.totalTransfer > effectiveBudgets.totalTransfer) {
|
|
837
|
-
violations.push({
|
|
838
|
-
metric: "Total Transfer",
|
|
839
|
-
budget: this.formatBytes(effectiveBudgets.totalTransfer),
|
|
840
|
-
actual: this.formatBytes(metrics.resources.totalTransfer),
|
|
841
|
-
overage: ((metrics.resources.totalTransfer / effectiveBudgets.totalTransfer - 1) * 100).toFixed(0) + "%"
|
|
842
|
-
});
|
|
843
|
-
} else {
|
|
844
|
-
passing.push({ metric: "Total Transfer", within: true });
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
if (metrics.resources?.jsSize > effectiveBudgets.jsSize) {
|
|
848
|
-
violations.push({
|
|
849
|
-
metric: "JS Size",
|
|
850
|
-
budget: this.formatBytes(effectiveBudgets.jsSize),
|
|
851
|
-
actual: this.formatBytes(metrics.resources.jsSize),
|
|
852
|
-
overage: ((metrics.resources.jsSize / effectiveBudgets.jsSize - 1) * 100).toFixed(0) + "%"
|
|
853
|
-
});
|
|
854
|
-
} else {
|
|
855
|
-
passing.push({ metric: "JS Size", within: true });
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
if (metrics.resources?.totalRequests > effectiveBudgets.requestCount) {
|
|
859
|
-
violations.push({
|
|
860
|
-
metric: "Request Count",
|
|
861
|
-
budget: effectiveBudgets.requestCount,
|
|
862
|
-
actual: metrics.resources.totalRequests,
|
|
863
|
-
overage: ((metrics.resources.totalRequests / effectiveBudgets.requestCount - 1) * 100).toFixed(0) + "%"
|
|
864
|
-
});
|
|
865
|
-
} else {
|
|
866
|
-
passing.push({ metric: "Request Count", within: true });
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
return {
|
|
870
|
-
passed: violations.length === 0,
|
|
871
|
-
violations,
|
|
872
|
-
passing,
|
|
873
|
-
summary: `${passing.length}/${passing.length + violations.length} budgets met`
|
|
874
|
-
};
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
878
|
-
// REPORTING
|
|
879
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
880
|
-
|
|
881
|
-
/**
|
|
882
|
-
* Generate performance report
|
|
883
|
-
*/
|
|
884
|
-
generateReport(metrics, comparison, budgetCheck) {
|
|
885
|
-
return {
|
|
886
|
-
timestamp: new Date().toISOString(),
|
|
887
|
-
url: metrics.url,
|
|
888
|
-
|
|
889
|
-
// Scores
|
|
890
|
-
scores: this.calculateScores(metrics),
|
|
891
|
-
|
|
892
|
-
// Core Web Vitals summary
|
|
893
|
-
coreWebVitals: {
|
|
894
|
-
LCP: this.formatMetric("LCP", metrics.coreWebVitals?.LCP),
|
|
895
|
-
FID: this.formatMetric("FID", metrics.coreWebVitals?.FID),
|
|
896
|
-
CLS: this.formatMetric("CLS", metrics.coreWebVitals?.CLS)
|
|
897
|
-
},
|
|
898
|
-
|
|
899
|
-
// Comparison with baseline
|
|
900
|
-
comparison: comparison?.hasBaseline ? {
|
|
901
|
-
regressions: comparison.regressions,
|
|
902
|
-
improvements: comparison.improvements,
|
|
903
|
-
summary: `${comparison.regressions.length} regressions, ${comparison.improvements.length} improvements`
|
|
904
|
-
} : null,
|
|
905
|
-
|
|
906
|
-
// Budget check
|
|
907
|
-
budgets: budgetCheck,
|
|
908
|
-
|
|
909
|
-
// Detailed metrics
|
|
910
|
-
details: {
|
|
911
|
-
navigation: metrics.navigation,
|
|
912
|
-
resources: metrics.resources,
|
|
913
|
-
memory: metrics.memory,
|
|
914
|
-
longTasks: metrics.longTasks,
|
|
915
|
-
layoutShifts: metrics.layoutShifts
|
|
916
|
-
},
|
|
917
|
-
|
|
918
|
-
// Recommendations
|
|
919
|
-
recommendations: this.generateRecommendations(metrics)
|
|
920
|
-
};
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
/**
|
|
924
|
-
* Calculate overall scores
|
|
925
|
-
*/
|
|
926
|
-
calculateScores(metrics) {
|
|
927
|
-
const scores = {};
|
|
928
|
-
|
|
929
|
-
// LCP score (0-100)
|
|
930
|
-
const lcp = metrics.coreWebVitals?.LCP;
|
|
931
|
-
if (lcp !== null) {
|
|
932
|
-
if (lcp <= 2500) scores.LCP = 100;
|
|
933
|
-
else if (lcp <= 4000) scores.LCP = Math.round(100 - ((lcp - 2500) / 1500) * 50);
|
|
934
|
-
else scores.LCP = Math.max(0, Math.round(50 - ((lcp - 4000) / 4000) * 50));
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
// CLS score
|
|
938
|
-
const cls = metrics.coreWebVitals?.CLS;
|
|
939
|
-
if (cls !== null) {
|
|
940
|
-
if (cls <= 0.1) scores.CLS = 100;
|
|
941
|
-
else if (cls <= 0.25) scores.CLS = Math.round(100 - ((cls - 0.1) / 0.15) * 50);
|
|
942
|
-
else scores.CLS = Math.max(0, Math.round(50 - (cls - 0.25) * 100));
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
// Overall score
|
|
946
|
-
const validScores = Object.values(scores).filter(s => s !== undefined);
|
|
947
|
-
scores.overall = validScores.length > 0
|
|
948
|
-
? Math.round(validScores.reduce((a, b) => a + b, 0) / validScores.length)
|
|
949
|
-
: null;
|
|
950
|
-
|
|
951
|
-
return scores;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
/**
|
|
955
|
-
* Format metric for display
|
|
956
|
-
*/
|
|
957
|
-
formatMetric(name, value) {
|
|
958
|
-
if (value === null || value === undefined) {
|
|
959
|
-
return { value: "N/A", rating: "unknown" };
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
const threshold = this.thresholds[name];
|
|
963
|
-
let rating = "unknown";
|
|
964
|
-
|
|
965
|
-
if (threshold) {
|
|
966
|
-
if (value <= threshold.good) rating = "good";
|
|
967
|
-
else if (value <= threshold.needsImprovement) rating = "needs-improvement";
|
|
968
|
-
else rating = "poor";
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
let formatted = value;
|
|
972
|
-
if (name === "CLS") {
|
|
973
|
-
formatted = value.toFixed(3);
|
|
974
|
-
} else if (name === "LCP" || name === "FID" || name === "TTFB" || name === "FCP") {
|
|
975
|
-
formatted = value.toFixed(0) + "ms";
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
return { value: formatted, rating };
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
/**
|
|
982
|
-
* Generate recommendations based on metrics
|
|
983
|
-
*/
|
|
984
|
-
generateRecommendations(metrics) {
|
|
985
|
-
const recommendations = [];
|
|
986
|
-
|
|
987
|
-
// LCP recommendations
|
|
988
|
-
const lcp = metrics.coreWebVitals?.LCP;
|
|
989
|
-
if (lcp > 4000) {
|
|
990
|
-
recommendations.push({
|
|
991
|
-
metric: "LCP",
|
|
992
|
-
severity: "high",
|
|
993
|
-
message: "Largest Contentful Paint is poor",
|
|
994
|
-
suggestions: [
|
|
995
|
-
"Optimize server response time",
|
|
996
|
-
"Remove render-blocking resources",
|
|
997
|
-
"Optimize images with lazy loading",
|
|
998
|
-
"Use a CDN for static assets"
|
|
999
|
-
]
|
|
1000
|
-
});
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
// CLS recommendations
|
|
1004
|
-
const cls = metrics.coreWebVitals?.CLS;
|
|
1005
|
-
if (cls > 0.25) {
|
|
1006
|
-
recommendations.push({
|
|
1007
|
-
metric: "CLS",
|
|
1008
|
-
severity: "high",
|
|
1009
|
-
message: "Cumulative Layout Shift is poor",
|
|
1010
|
-
suggestions: [
|
|
1011
|
-
"Set explicit dimensions on images and videos",
|
|
1012
|
-
"Reserve space for dynamic content",
|
|
1013
|
-
"Avoid inserting content above existing content",
|
|
1014
|
-
"Use CSS transform for animations"
|
|
1015
|
-
]
|
|
1016
|
-
});
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
// JS size recommendations
|
|
1020
|
-
const jsSize = metrics.resources?.jsSize;
|
|
1021
|
-
if (jsSize > 500 * 1024) {
|
|
1022
|
-
recommendations.push({
|
|
1023
|
-
metric: "JS Size",
|
|
1024
|
-
severity: "medium",
|
|
1025
|
-
message: `JavaScript bundle is large (${this.formatBytes(jsSize)})`,
|
|
1026
|
-
suggestions: [
|
|
1027
|
-
"Enable code splitting",
|
|
1028
|
-
"Remove unused dependencies",
|
|
1029
|
-
"Use tree shaking",
|
|
1030
|
-
"Consider lazy loading non-critical JS"
|
|
1031
|
-
]
|
|
1032
|
-
});
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
// TBT recommendations
|
|
1036
|
-
const tbt = metrics.longTasks?.totalBlockingTime;
|
|
1037
|
-
if (tbt > 300) {
|
|
1038
|
-
recommendations.push({
|
|
1039
|
-
metric: "TBT",
|
|
1040
|
-
severity: "medium",
|
|
1041
|
-
message: `High Total Blocking Time (${tbt.toFixed(0)}ms)`,
|
|
1042
|
-
suggestions: [
|
|
1043
|
-
"Break up long tasks",
|
|
1044
|
-
"Defer non-critical JavaScript",
|
|
1045
|
-
"Use web workers for heavy computation",
|
|
1046
|
-
"Optimize third-party scripts"
|
|
1047
|
-
]
|
|
1048
|
-
});
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
return recommendations;
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1055
|
-
// UTILITIES
|
|
1056
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1057
|
-
|
|
1058
|
-
sanitizeName(name) {
|
|
1059
|
-
return name.replace(/[^a-zA-Z0-9-_]/g, "-").toLowerCase();
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
formatBytes(bytes) {
|
|
1063
|
-
if (bytes < 1024) return bytes + " B";
|
|
1064
|
-
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
|
|
1065
|
-
return (bytes / (1024 * 1024)).toFixed(2) + " MB";
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1070
|
-
// EXPORTS
|
|
1071
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1072
|
-
|
|
1073
|
-
module.exports = {
|
|
1074
|
-
PerformanceTracker,
|
|
1075
|
-
THRESHOLDS,
|
|
1076
|
-
REGRESSION_THRESHOLDS
|
|
1077
|
-
};
|