@testsmith/perfornium 0.1.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/README.md +360 -0
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.js +192 -0
- package/dist/cli/commands/distributed.d.ts +11 -0
- package/dist/cli/commands/distributed.js +179 -0
- package/dist/cli/commands/import.d.ts +23 -0
- package/dist/cli/commands/import.js +461 -0
- package/dist/cli/commands/init.d.ts +7 -0
- package/dist/cli/commands/init.js +923 -0
- package/dist/cli/commands/mock.d.ts +7 -0
- package/dist/cli/commands/mock.js +281 -0
- package/dist/cli/commands/report.d.ts +5 -0
- package/dist/cli/commands/report.js +70 -0
- package/dist/cli/commands/run.d.ts +12 -0
- package/dist/cli/commands/run.js +260 -0
- package/dist/cli/commands/validate.d.ts +3 -0
- package/dist/cli/commands/validate.js +35 -0
- package/dist/cli/commands/worker.d.ts +27 -0
- package/dist/cli/commands/worker.js +320 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.js +20 -0
- package/dist/config/parser.d.ts +19 -0
- package/dist/config/parser.js +330 -0
- package/dist/config/types/global-config.d.ts +74 -0
- package/dist/config/types/global-config.js +2 -0
- package/dist/config/types/hooks.d.ts +58 -0
- package/dist/config/types/hooks.js +3 -0
- package/dist/config/types/import-types.d.ts +33 -0
- package/dist/config/types/import-types.js +2 -0
- package/dist/config/types/index.d.ts +11 -0
- package/dist/config/types/index.js +27 -0
- package/dist/config/types/load-config.d.ts +32 -0
- package/dist/config/types/load-config.js +9 -0
- package/dist/config/types/output-config.d.ts +10 -0
- package/dist/config/types/output-config.js +2 -0
- package/dist/config/types/report-config.d.ts +10 -0
- package/dist/config/types/report-config.js +2 -0
- package/dist/config/types/runtime-types.d.ts +6 -0
- package/dist/config/types/runtime-types.js +2 -0
- package/dist/config/types/scenario-config.d.ts +30 -0
- package/dist/config/types/scenario-config.js +2 -0
- package/dist/config/types/step-types.d.ts +139 -0
- package/dist/config/types/step-types.js +2 -0
- package/dist/config/types/test-configuration.d.ts +18 -0
- package/dist/config/types/test-configuration.js +2 -0
- package/dist/config/types/worker-config.d.ts +12 -0
- package/dist/config/types/worker-config.js +2 -0
- package/dist/config/validator.d.ts +19 -0
- package/dist/config/validator.js +198 -0
- package/dist/core/csv-data-provider.d.ts +47 -0
- package/dist/core/csv-data-provider.js +265 -0
- package/dist/core/hooks-manager.d.ts +33 -0
- package/dist/core/hooks-manager.js +129 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +11 -0
- package/dist/core/script-executor.d.ts +14 -0
- package/dist/core/script-executor.js +290 -0
- package/dist/core/step-executor.d.ts +41 -0
- package/dist/core/step-executor.js +680 -0
- package/dist/core/test-runner.d.ts +34 -0
- package/dist/core/test-runner.js +465 -0
- package/dist/core/threshold-evaluator.d.ts +43 -0
- package/dist/core/threshold-evaluator.js +170 -0
- package/dist/core/virtual-user-pool.d.ts +42 -0
- package/dist/core/virtual-user-pool.js +136 -0
- package/dist/core/virtual-user.d.ts +51 -0
- package/dist/core/virtual-user.js +488 -0
- package/dist/distributed/coordinator.d.ts +34 -0
- package/dist/distributed/coordinator.js +158 -0
- package/dist/distributed/health-monitor.d.ts +18 -0
- package/dist/distributed/health-monitor.js +72 -0
- package/dist/distributed/load-distributor.d.ts +17 -0
- package/dist/distributed/load-distributor.js +106 -0
- package/dist/distributed/remote-worker.d.ts +37 -0
- package/dist/distributed/remote-worker.js +241 -0
- package/dist/distributed/result-aggregator.d.ts +43 -0
- package/dist/distributed/result-aggregator.js +146 -0
- package/dist/dsl/index.d.ts +3 -0
- package/dist/dsl/index.js +11 -0
- package/dist/dsl/test-builder.d.ts +111 -0
- package/dist/dsl/test-builder.js +514 -0
- package/dist/importers/har-importer.d.ts +17 -0
- package/dist/importers/har-importer.js +172 -0
- package/dist/importers/open-api-importer.d.ts +23 -0
- package/dist/importers/open-api-importer.js +181 -0
- package/dist/importers/wsdl-importer.d.ts +42 -0
- package/dist/importers/wsdl-importer.js +440 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +17 -0
- package/dist/load-patterns/arrivals.d.ts +7 -0
- package/dist/load-patterns/arrivals.js +118 -0
- package/dist/load-patterns/base.d.ts +9 -0
- package/dist/load-patterns/base.js +2 -0
- package/dist/load-patterns/basic.d.ts +7 -0
- package/dist/load-patterns/basic.js +117 -0
- package/dist/load-patterns/stepping.d.ts +6 -0
- package/dist/load-patterns/stepping.js +122 -0
- package/dist/metrics/collector.d.ts +72 -0
- package/dist/metrics/collector.js +662 -0
- package/dist/metrics/types.d.ts +135 -0
- package/dist/metrics/types.js +2 -0
- package/dist/outputs/base.d.ts +7 -0
- package/dist/outputs/base.js +2 -0
- package/dist/outputs/csv.d.ts +13 -0
- package/dist/outputs/csv.js +163 -0
- package/dist/outputs/graphite.d.ts +13 -0
- package/dist/outputs/graphite.js +126 -0
- package/dist/outputs/influxdb.d.ts +12 -0
- package/dist/outputs/influxdb.js +82 -0
- package/dist/outputs/json.d.ts +14 -0
- package/dist/outputs/json.js +107 -0
- package/dist/outputs/streaming-csv.d.ts +37 -0
- package/dist/outputs/streaming-csv.js +254 -0
- package/dist/outputs/streaming-json.d.ts +43 -0
- package/dist/outputs/streaming-json.js +353 -0
- package/dist/outputs/webhook.d.ts +16 -0
- package/dist/outputs/webhook.js +96 -0
- package/dist/protocols/base.d.ts +33 -0
- package/dist/protocols/base.js +2 -0
- package/dist/protocols/rest/handler.d.ts +67 -0
- package/dist/protocols/rest/handler.js +776 -0
- package/dist/protocols/soap/handler.d.ts +12 -0
- package/dist/protocols/soap/handler.js +165 -0
- package/dist/protocols/web/core-web-vitals.d.ts +121 -0
- package/dist/protocols/web/core-web-vitals.js +373 -0
- package/dist/protocols/web/handler.d.ts +50 -0
- package/dist/protocols/web/handler.js +706 -0
- package/dist/recorder/native-recorder.d.ts +14 -0
- package/dist/recorder/native-recorder.js +533 -0
- package/dist/recorder/scenario-recorder.d.ts +55 -0
- package/dist/recorder/scenario-recorder.js +296 -0
- package/dist/reporting/constants.d.ts +94 -0
- package/dist/reporting/constants.js +82 -0
- package/dist/reporting/enhanced-html-generator.d.ts +55 -0
- package/dist/reporting/enhanced-html-generator.js +965 -0
- package/dist/reporting/generator.d.ts +42 -0
- package/dist/reporting/generator.js +1217 -0
- package/dist/reporting/statistics.d.ts +144 -0
- package/dist/reporting/statistics.js +742 -0
- package/dist/reporting/templates/enhanced-report.hbs +2812 -0
- package/dist/reporting/templates/html.hbs +2453 -0
- package/dist/utils/faker-manager.d.ts +55 -0
- package/dist/utils/faker-manager.js +166 -0
- package/dist/utils/file-manager.d.ts +33 -0
- package/dist/utils/file-manager.js +154 -0
- package/dist/utils/handlebars-manager.d.ts +42 -0
- package/dist/utils/handlebars-manager.js +172 -0
- package/dist/utils/logger.d.ts +16 -0
- package/dist/utils/logger.js +46 -0
- package/dist/utils/template.d.ts +80 -0
- package/dist/utils/template.js +513 -0
- package/dist/utils/test-output-writer.d.ts +56 -0
- package/dist/utils/test-output-writer.js +643 -0
- package/dist/utils/time.d.ts +3 -0
- package/dist/utils/time.js +23 -0
- package/dist/utils/timestamp-helper.d.ts +17 -0
- package/dist/utils/timestamp-helper.js +53 -0
- package/dist/workers/manager.d.ts +18 -0
- package/dist/workers/manager.js +95 -0
- package/dist/workers/server.d.ts +21 -0
- package/dist/workers/server.js +205 -0
- package/dist/workers/worker.d.ts +19 -0
- package/dist/workers/worker.js +147 -0
- package/package.json +102 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ProtocolHandler, ProtocolResult } from '../base';
|
|
2
|
+
import { VUContext, SOAPStep } from '../../config/types';
|
|
3
|
+
export declare class SOAPHandler implements ProtocolHandler {
|
|
4
|
+
private client;
|
|
5
|
+
private wsdlUrl;
|
|
6
|
+
constructor(wsdlUrl: string);
|
|
7
|
+
initialize(): Promise<void>;
|
|
8
|
+
execute(operation: SOAPStep, context: VUContext): Promise<ProtocolResult>;
|
|
9
|
+
private executeRawSOAP;
|
|
10
|
+
private extractSOAPActionFromXML;
|
|
11
|
+
private parseSOAPResponse;
|
|
12
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.SOAPHandler = void 0;
|
|
37
|
+
const soap = __importStar(require("soap"));
|
|
38
|
+
const logger_1 = require("../../utils/logger");
|
|
39
|
+
class SOAPHandler {
|
|
40
|
+
constructor(wsdlUrl) {
|
|
41
|
+
this.wsdlUrl = wsdlUrl;
|
|
42
|
+
}
|
|
43
|
+
async initialize() {
|
|
44
|
+
try {
|
|
45
|
+
this.client = await soap.createClientAsync(this.wsdlUrl);
|
|
46
|
+
logger_1.logger.debug(`🧼 SOAP client initialized for ${this.wsdlUrl}`);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
logger_1.logger.error(`❌ Failed to initialize SOAP client:`, error);
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async execute(operation, context) {
|
|
54
|
+
const startTime = performance.now();
|
|
55
|
+
try {
|
|
56
|
+
if (!this.client) {
|
|
57
|
+
await this.initialize();
|
|
58
|
+
}
|
|
59
|
+
// If body is provided, use raw XML SOAP request
|
|
60
|
+
if (operation.body) {
|
|
61
|
+
const result = await this.executeRawSOAP(operation.body);
|
|
62
|
+
const duration = performance.now() - startTime;
|
|
63
|
+
return {
|
|
64
|
+
success: true,
|
|
65
|
+
data: result,
|
|
66
|
+
response_size: JSON.stringify(result).length,
|
|
67
|
+
duration,
|
|
68
|
+
request_url: this.wsdlUrl,
|
|
69
|
+
request_method: 'SOAP',
|
|
70
|
+
response_body: JSON.stringify(result),
|
|
71
|
+
custom_metrics: {
|
|
72
|
+
operation: operation.operation,
|
|
73
|
+
wsdl_url: this.wsdlUrl,
|
|
74
|
+
used_raw_xml: true
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// Use traditional SOAP client with args
|
|
79
|
+
const result = await new Promise((resolve, reject) => {
|
|
80
|
+
this.client[operation.operation](operation.args, (err, result) => {
|
|
81
|
+
if (err)
|
|
82
|
+
reject(err);
|
|
83
|
+
else
|
|
84
|
+
resolve(result);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
const duration = performance.now() - startTime;
|
|
88
|
+
const responseData = JSON.stringify(result);
|
|
89
|
+
return {
|
|
90
|
+
success: true,
|
|
91
|
+
data: result,
|
|
92
|
+
response_size: responseData.length,
|
|
93
|
+
duration,
|
|
94
|
+
request_url: this.wsdlUrl,
|
|
95
|
+
request_method: 'SOAP',
|
|
96
|
+
response_body: responseData,
|
|
97
|
+
custom_metrics: {
|
|
98
|
+
operation: operation.operation,
|
|
99
|
+
wsdl_url: this.wsdlUrl
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
const duration = performance.now() - startTime;
|
|
105
|
+
return {
|
|
106
|
+
success: false,
|
|
107
|
+
error: error.message,
|
|
108
|
+
error_code: 'SOAP_ERROR',
|
|
109
|
+
response_size: 0,
|
|
110
|
+
duration,
|
|
111
|
+
request_url: this.wsdlUrl,
|
|
112
|
+
request_method: 'SOAP',
|
|
113
|
+
custom_metrics: {
|
|
114
|
+
operation: operation.operation,
|
|
115
|
+
wsdl_url: this.wsdlUrl
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async executeRawSOAP(xmlBody) {
|
|
121
|
+
try {
|
|
122
|
+
// Extract endpoint URL from WSDL URL
|
|
123
|
+
const endpointUrl = this.wsdlUrl.replace('?wsdl', '');
|
|
124
|
+
// Make direct HTTP request with the XML body
|
|
125
|
+
const response = await fetch(endpointUrl, {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
headers: {
|
|
128
|
+
'Content-Type': 'text/xml; charset=utf-8',
|
|
129
|
+
'SOAPAction': this.extractSOAPActionFromXML(xmlBody)
|
|
130
|
+
},
|
|
131
|
+
body: xmlBody
|
|
132
|
+
});
|
|
133
|
+
const responseText = await response.text();
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
throw new Error(`HTTP ${response.status}: ${responseText}`);
|
|
136
|
+
}
|
|
137
|
+
return this.parseSOAPResponse(responseText);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
logger_1.logger.error('Raw SOAP execution failed:', error);
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
extractSOAPActionFromXML(xmlBody) {
|
|
145
|
+
const operationMatch = xmlBody.match(/<(\w+)\s+xmlns=/);
|
|
146
|
+
const operationName = operationMatch ? operationMatch[1] : 'UnknownOperation';
|
|
147
|
+
return `"http://tempuri.org/${operationName}"`;
|
|
148
|
+
}
|
|
149
|
+
parseSOAPResponse(responseXml) {
|
|
150
|
+
try {
|
|
151
|
+
const resultMatch = responseXml.match(/<(\w+Result)[^>]*>([^<]*)<\/\1>/);
|
|
152
|
+
if (resultMatch) {
|
|
153
|
+
return {
|
|
154
|
+
[resultMatch[1]]: resultMatch[2]
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return { rawResponse: responseXml };
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
logger_1.logger.error('Failed to parse SOAP response:', error);
|
|
161
|
+
return { rawResponse: responseXml };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
exports.SOAPHandler = SOAPHandler;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Page } from 'playwright';
|
|
2
|
+
export interface CoreWebVitals {
|
|
3
|
+
lcp?: number;
|
|
4
|
+
cls?: number;
|
|
5
|
+
inp?: number;
|
|
6
|
+
ttfb?: number;
|
|
7
|
+
fcp?: number;
|
|
8
|
+
fid?: number;
|
|
9
|
+
tti?: number;
|
|
10
|
+
tbt?: number;
|
|
11
|
+
speedIndex?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface WebVitalsThresholds {
|
|
14
|
+
lcp: {
|
|
15
|
+
good: number;
|
|
16
|
+
poor: number;
|
|
17
|
+
};
|
|
18
|
+
cls: {
|
|
19
|
+
good: number;
|
|
20
|
+
poor: number;
|
|
21
|
+
};
|
|
22
|
+
inp: {
|
|
23
|
+
good: number;
|
|
24
|
+
poor: number;
|
|
25
|
+
};
|
|
26
|
+
ttfb: {
|
|
27
|
+
good: number;
|
|
28
|
+
poor: number;
|
|
29
|
+
};
|
|
30
|
+
fcp: {
|
|
31
|
+
good: number;
|
|
32
|
+
poor: number;
|
|
33
|
+
};
|
|
34
|
+
fid?: {
|
|
35
|
+
good: number;
|
|
36
|
+
poor: number;
|
|
37
|
+
};
|
|
38
|
+
tti?: {
|
|
39
|
+
good: number;
|
|
40
|
+
poor: number;
|
|
41
|
+
};
|
|
42
|
+
tbt?: {
|
|
43
|
+
good: number;
|
|
44
|
+
poor: number;
|
|
45
|
+
};
|
|
46
|
+
speedIndex?: {
|
|
47
|
+
good: number;
|
|
48
|
+
poor: number;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export declare const DEFAULT_WEB_VITALS_THRESHOLDS: WebVitalsThresholds;
|
|
52
|
+
export declare class CoreWebVitalsCollector {
|
|
53
|
+
private static webVitalsScript;
|
|
54
|
+
static injectVitalsCollector(page: Page): Promise<void>;
|
|
55
|
+
static collectVitals(page: Page, waitTime?: number): Promise<CoreWebVitals>;
|
|
56
|
+
static evaluateVitals(vitals: CoreWebVitals, thresholds?: WebVitalsThresholds): {
|
|
57
|
+
score: 'good' | 'needs-improvement' | 'poor';
|
|
58
|
+
details: {
|
|
59
|
+
[metric: string]: {
|
|
60
|
+
value: number;
|
|
61
|
+
score: 'good' | 'needs-improvement' | 'poor';
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
static generateVitalsThresholds(vitals: CoreWebVitals): Array<{
|
|
66
|
+
metric: string;
|
|
67
|
+
value: number;
|
|
68
|
+
operator: string;
|
|
69
|
+
severity: string;
|
|
70
|
+
description: string;
|
|
71
|
+
}>;
|
|
72
|
+
}
|
|
73
|
+
export interface VerificationStepMetrics {
|
|
74
|
+
step_name: string;
|
|
75
|
+
step_type: string;
|
|
76
|
+
selector?: string;
|
|
77
|
+
duration: number;
|
|
78
|
+
success: boolean;
|
|
79
|
+
error_message?: string;
|
|
80
|
+
element_count?: number;
|
|
81
|
+
expected_text?: string;
|
|
82
|
+
actual_text?: string;
|
|
83
|
+
screenshot_size?: number;
|
|
84
|
+
dom_ready_time?: number;
|
|
85
|
+
network_idle_time?: number;
|
|
86
|
+
}
|
|
87
|
+
export declare class VerificationMetricsCollector {
|
|
88
|
+
static measureVerificationStep<T>(stepName: string, stepType: string, operation: () => Promise<T>, additionalMetrics?: Partial<VerificationStepMetrics>): Promise<{
|
|
89
|
+
result: T;
|
|
90
|
+
metrics: VerificationStepMetrics;
|
|
91
|
+
}>;
|
|
92
|
+
static generateVerificationThresholds(metrics: VerificationStepMetrics[]): Array<{
|
|
93
|
+
metric: string;
|
|
94
|
+
value: number;
|
|
95
|
+
operator: string;
|
|
96
|
+
severity: string;
|
|
97
|
+
description: string;
|
|
98
|
+
}>;
|
|
99
|
+
}
|
|
100
|
+
export interface WebPerformanceMetrics {
|
|
101
|
+
core_web_vitals: CoreWebVitals;
|
|
102
|
+
vitals_score: 'good' | 'needs-improvement' | 'poor';
|
|
103
|
+
vitals_details: {
|
|
104
|
+
[metric: string]: {
|
|
105
|
+
value: number;
|
|
106
|
+
score: 'good' | 'needs-improvement' | 'poor';
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
verification_metrics?: VerificationStepMetrics;
|
|
110
|
+
page_load_time: number;
|
|
111
|
+
dom_content_loaded: number;
|
|
112
|
+
network_requests: number;
|
|
113
|
+
failed_requests: number;
|
|
114
|
+
total_transfer_size: number;
|
|
115
|
+
page_size: number;
|
|
116
|
+
javascript_execution_time: number;
|
|
117
|
+
style_recalculation_time: number;
|
|
118
|
+
}
|
|
119
|
+
export declare class WebPerformanceCollector {
|
|
120
|
+
static collectAllMetrics(page: Page, verificationMetrics?: VerificationStepMetrics): Promise<WebPerformanceMetrics>;
|
|
121
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebPerformanceCollector = exports.VerificationMetricsCollector = exports.CoreWebVitalsCollector = exports.DEFAULT_WEB_VITALS_THRESHOLDS = void 0;
|
|
4
|
+
const logger_1 = require("../../utils/logger");
|
|
5
|
+
exports.DEFAULT_WEB_VITALS_THRESHOLDS = {
|
|
6
|
+
lcp: { good: 2500, poor: 4000 },
|
|
7
|
+
cls: { good: 0.1, poor: 0.25 },
|
|
8
|
+
inp: { good: 200, poor: 500 },
|
|
9
|
+
ttfb: { good: 800, poor: 1800 },
|
|
10
|
+
fcp: { good: 1800, poor: 3000 },
|
|
11
|
+
fid: { good: 100, poor: 300 },
|
|
12
|
+
tti: { good: 3800, poor: 7300 },
|
|
13
|
+
tbt: { good: 200, poor: 600 },
|
|
14
|
+
speedIndex: { good: 3400, poor: 5800 }
|
|
15
|
+
};
|
|
16
|
+
class CoreWebVitalsCollector {
|
|
17
|
+
static async injectVitalsCollector(page) {
|
|
18
|
+
try {
|
|
19
|
+
await page.addInitScript(this.webVitalsScript);
|
|
20
|
+
logger_1.logger.debug('Core Web Vitals collector injected');
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
logger_1.logger.warn('Failed to inject Web Vitals collector:', error);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
static async collectVitals(page, waitTime = 3000) {
|
|
27
|
+
try {
|
|
28
|
+
// First ensure the script is injected on the current page
|
|
29
|
+
await page.evaluate(this.webVitalsScript);
|
|
30
|
+
// Wait for vitals to be collected
|
|
31
|
+
await page.waitForTimeout(waitTime);
|
|
32
|
+
// Get the collected vitals
|
|
33
|
+
const vitals = await page.evaluate(() => {
|
|
34
|
+
const collected = window.__webVitals || {};
|
|
35
|
+
const interactions = window.__interactions || [];
|
|
36
|
+
// Ensure INP is calculated if we have interactions
|
|
37
|
+
if (!collected.inp && interactions.length > 0) {
|
|
38
|
+
if (interactions.length >= 10) {
|
|
39
|
+
const sorted = [...interactions].sort((a, b) => b.latency - a.latency);
|
|
40
|
+
const index = Math.min(Math.floor(interactions.length * 0.98), sorted.length - 1);
|
|
41
|
+
collected.inp = sorted[index].latency;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
collected.inp = Math.max(...interactions.map((i) => i.latency));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Calculate additional metrics
|
|
48
|
+
const navigation = performance.getEntriesByType('navigation')[0];
|
|
49
|
+
if (navigation) {
|
|
50
|
+
// Time to Interactive approximation
|
|
51
|
+
collected.tti = navigation.domInteractive - navigation.fetchStart;
|
|
52
|
+
// Total Blocking Time approximation (simplified)
|
|
53
|
+
const longTasks = performance.getEntriesByType('longtask');
|
|
54
|
+
collected.tbt = longTasks.reduce((total, task) => {
|
|
55
|
+
const blockingTime = Math.max(0, task.duration - 50);
|
|
56
|
+
return total + blockingTime;
|
|
57
|
+
}, 0);
|
|
58
|
+
// Speed Index approximation (simplified)
|
|
59
|
+
collected.speedIndex = navigation.domContentLoadedEventStart - navigation.fetchStart;
|
|
60
|
+
// Ensure TTFB is captured
|
|
61
|
+
if (!collected.ttfb) {
|
|
62
|
+
collected.ttfb = navigation.responseStart - navigation.fetchStart;
|
|
63
|
+
}
|
|
64
|
+
// Ensure FCP is captured from paint timing if not already
|
|
65
|
+
if (!collected.fcp) {
|
|
66
|
+
const paintEntries = performance.getEntriesByType('paint');
|
|
67
|
+
const fcpEntry = paintEntries.find((entry) => entry.name === 'first-contentful-paint');
|
|
68
|
+
if (fcpEntry) {
|
|
69
|
+
collected.fcp = fcpEntry.startTime;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Get LCP from performance entries if not already captured
|
|
74
|
+
if (!collected.lcp) {
|
|
75
|
+
const lcpEntries = performance.getEntriesByType('largest-contentful-paint');
|
|
76
|
+
console.log('LCP fallback - checking entries. Found:', lcpEntries.length);
|
|
77
|
+
if (lcpEntries.length > 0) {
|
|
78
|
+
// Use the latest LCP entry (most recent candidate)
|
|
79
|
+
const latestEntry = lcpEntries[lcpEntries.length - 1];
|
|
80
|
+
collected.lcp = latestEntry.startTime;
|
|
81
|
+
console.log('LCP fallback collected:', collected.lcp, 'from entry:', latestEntry);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// Try alternative approach - estimate LCP from paint timing
|
|
85
|
+
const paintEntries = performance.getEntriesByType('paint');
|
|
86
|
+
console.log('No LCP entries found. Paint entries:', paintEntries.length);
|
|
87
|
+
if (paintEntries.length > 0) {
|
|
88
|
+
const fcpEntry = paintEntries.find(entry => entry.name === 'first-contentful-paint');
|
|
89
|
+
if (fcpEntry) {
|
|
90
|
+
// Use FCP as a rough approximation for LCP if no LCP is available
|
|
91
|
+
collected.lcp = fcpEntry.startTime;
|
|
92
|
+
console.log('LCP approximated from FCP:', collected.lcp);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Get CLS from layout-shift entries if not captured
|
|
98
|
+
if (collected.cls === undefined) {
|
|
99
|
+
const layoutShiftEntries = performance.getEntriesByType('layout-shift');
|
|
100
|
+
collected.cls = layoutShiftEntries.reduce((total, entry) => {
|
|
101
|
+
if (!entry.hadRecentInput) {
|
|
102
|
+
return total + entry.value;
|
|
103
|
+
}
|
|
104
|
+
return total;
|
|
105
|
+
}, 0);
|
|
106
|
+
console.log('CLS fallback collected:', collected.cls, 'from', layoutShiftEntries.length, 'shifts');
|
|
107
|
+
}
|
|
108
|
+
console.log('Final collected vitals before return:', collected);
|
|
109
|
+
return collected;
|
|
110
|
+
});
|
|
111
|
+
logger_1.logger.debug('Core Web Vitals collection complete:', vitals);
|
|
112
|
+
return vitals;
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
logger_1.logger.warn('Failed to collect Core Web Vitals:', error);
|
|
116
|
+
return {};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
static evaluateVitals(vitals, thresholds = exports.DEFAULT_WEB_VITALS_THRESHOLDS) {
|
|
120
|
+
const details = {};
|
|
121
|
+
let goodCount = 0;
|
|
122
|
+
let poorCount = 0;
|
|
123
|
+
let totalCount = 0;
|
|
124
|
+
const metrics = ['lcp', 'cls', 'inp', 'ttfb', 'fcp', 'fid', 'tti', 'tbt', 'speedIndex'];
|
|
125
|
+
for (const metric of metrics) {
|
|
126
|
+
const value = vitals[metric];
|
|
127
|
+
if (value !== undefined && value !== null) {
|
|
128
|
+
totalCount++;
|
|
129
|
+
const threshold = thresholds[metric];
|
|
130
|
+
let score;
|
|
131
|
+
if (value <= threshold.good) {
|
|
132
|
+
score = 'good';
|
|
133
|
+
goodCount++;
|
|
134
|
+
}
|
|
135
|
+
else if (value <= threshold.poor) {
|
|
136
|
+
score = 'needs-improvement';
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
score = 'poor';
|
|
140
|
+
poorCount++;
|
|
141
|
+
}
|
|
142
|
+
details[metric] = { value, score };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Overall score calculation
|
|
146
|
+
let overallScore;
|
|
147
|
+
if (totalCount === 0) {
|
|
148
|
+
overallScore = 'needs-improvement';
|
|
149
|
+
}
|
|
150
|
+
else if (goodCount >= totalCount * 0.75) {
|
|
151
|
+
overallScore = 'good';
|
|
152
|
+
}
|
|
153
|
+
else if (poorCount > totalCount * 0.25) {
|
|
154
|
+
overallScore = 'poor';
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
overallScore = 'needs-improvement';
|
|
158
|
+
}
|
|
159
|
+
return { score: overallScore, details };
|
|
160
|
+
}
|
|
161
|
+
static generateVitalsThresholds(vitals) {
|
|
162
|
+
const thresholds = [];
|
|
163
|
+
const baseThresholds = exports.DEFAULT_WEB_VITALS_THRESHOLDS;
|
|
164
|
+
for (const [metric, value] of Object.entries(vitals)) {
|
|
165
|
+
if (value !== undefined && value !== null && metric in baseThresholds) {
|
|
166
|
+
const threshold = baseThresholds[metric];
|
|
167
|
+
thresholds.push({
|
|
168
|
+
metric: `web_vitals_${metric}`,
|
|
169
|
+
value: threshold.good,
|
|
170
|
+
operator: 'lte',
|
|
171
|
+
severity: 'warning',
|
|
172
|
+
description: `${metric.toUpperCase()} should be good (≤${threshold.good}${metric === 'cls' ? '' : 'ms'})`
|
|
173
|
+
});
|
|
174
|
+
thresholds.push({
|
|
175
|
+
metric: `web_vitals_${metric}`,
|
|
176
|
+
value: threshold.poor,
|
|
177
|
+
operator: 'lte',
|
|
178
|
+
severity: 'error',
|
|
179
|
+
description: `${metric.toUpperCase()} should not be poor (≤${threshold.poor}${metric === 'cls' ? '' : 'ms'})`
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return thresholds;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
exports.CoreWebVitalsCollector = CoreWebVitalsCollector;
|
|
187
|
+
CoreWebVitalsCollector.webVitalsScript = `
|
|
188
|
+
// Improved Web Vitals measurement script based on best practices
|
|
189
|
+
(() => {
|
|
190
|
+
const vitals = {};
|
|
191
|
+
const interactions = [];
|
|
192
|
+
|
|
193
|
+
// LCP Observer with buffered entries
|
|
194
|
+
const lcpObserver = new PerformanceObserver((list) => {
|
|
195
|
+
const entries = list.getEntries();
|
|
196
|
+
const lcp = entries.at(-1);
|
|
197
|
+
if (lcp) {
|
|
198
|
+
vitals.lcp = Number(lcp.startTime);
|
|
199
|
+
console.log('LCP collected:', vitals.lcp);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// TTFB Observer
|
|
204
|
+
const ttfbObserver = new PerformanceObserver((list) => {
|
|
205
|
+
const entries = list.getEntries();
|
|
206
|
+
if (entries.length > 0) {
|
|
207
|
+
const navigationEntry = entries[0];
|
|
208
|
+
vitals.ttfb = navigationEntry.responseStart - navigationEntry.fetchStart;
|
|
209
|
+
console.log('TTFB collected:', vitals.ttfb);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// CLS Observer
|
|
214
|
+
const clsObserver = new PerformanceObserver((list) => {
|
|
215
|
+
let total = vitals.cls || 0;
|
|
216
|
+
for (const entry of list.getEntries()) {
|
|
217
|
+
if (!entry.hadRecentInput) {
|
|
218
|
+
total += entry.value;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
vitals.cls = Number(total.toPrecision(4));
|
|
222
|
+
console.log('CLS updated:', vitals.cls);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// FID Observer
|
|
226
|
+
const fidObserver = new PerformanceObserver((list) => {
|
|
227
|
+
for (const entry of list.getEntries()) {
|
|
228
|
+
vitals.fid = entry.processingStart - entry.startTime;
|
|
229
|
+
console.log('FID collected:', vitals.fid);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Start observing with error handling
|
|
234
|
+
try {
|
|
235
|
+
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
|
|
236
|
+
console.log('LCP observer started');
|
|
237
|
+
} catch (e) { console.warn('LCP observer failed:', e); }
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
ttfbObserver.observe({ type: 'navigation', buffered: true });
|
|
241
|
+
console.log('TTFB observer started');
|
|
242
|
+
} catch (e) { console.warn('TTFB observer failed:', e); }
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
clsObserver.observe({ type: 'layout-shift', buffered: true });
|
|
246
|
+
console.log('CLS observer started');
|
|
247
|
+
} catch (e) { console.warn('CLS observer failed:', e); }
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
fidObserver.observe({ type: 'first-input', buffered: true });
|
|
251
|
+
console.log('FID observer started');
|
|
252
|
+
} catch (e) { console.warn('FID observer failed:', e); }
|
|
253
|
+
|
|
254
|
+
// Get FCP from paint timings
|
|
255
|
+
const paintTimings = performance.getEntriesByType('paint');
|
|
256
|
+
for (const entry of paintTimings) {
|
|
257
|
+
if (entry.name === 'first-contentful-paint') {
|
|
258
|
+
vitals.fcp = Number(entry.startTime);
|
|
259
|
+
console.log('FCP collected:', vitals.fcp);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Initialize CLS to 0 if not set
|
|
264
|
+
if (vitals.cls === undefined) {
|
|
265
|
+
vitals.cls = 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Store vitals on window for retrieval
|
|
269
|
+
window.__webVitals = vitals;
|
|
270
|
+
window.__interactions = interactions;
|
|
271
|
+
|
|
272
|
+
console.log('Improved Web Vitals collector initialized');
|
|
273
|
+
|
|
274
|
+
return vitals;
|
|
275
|
+
})();
|
|
276
|
+
`;
|
|
277
|
+
class VerificationMetricsCollector {
|
|
278
|
+
static async measureVerificationStep(stepName, stepType, operation, additionalMetrics) {
|
|
279
|
+
const startTime = performance.now();
|
|
280
|
+
let success = false;
|
|
281
|
+
let error_message;
|
|
282
|
+
let result;
|
|
283
|
+
try {
|
|
284
|
+
result = await operation();
|
|
285
|
+
success = true;
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
error_message = error.message;
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
// Round to 1 decimal place for cleaner output
|
|
293
|
+
const duration = Math.round((performance.now() - startTime) * 10) / 10;
|
|
294
|
+
const metrics = {
|
|
295
|
+
step_name: stepName,
|
|
296
|
+
step_type: stepType,
|
|
297
|
+
duration,
|
|
298
|
+
success,
|
|
299
|
+
error_message,
|
|
300
|
+
...additionalMetrics
|
|
301
|
+
};
|
|
302
|
+
return { result: result, metrics };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
static generateVerificationThresholds(metrics) {
|
|
306
|
+
const thresholds = [];
|
|
307
|
+
// Analyze verification step performance
|
|
308
|
+
const durations = metrics.map(m => m.duration);
|
|
309
|
+
const avgDuration = durations.reduce((a, b) => a + b, 0) / durations.length;
|
|
310
|
+
const maxDuration = Math.max(...durations);
|
|
311
|
+
const p95Duration = durations.sort((a, b) => a - b)[Math.floor(durations.length * 0.95)];
|
|
312
|
+
// Generate thresholds based on observed performance
|
|
313
|
+
thresholds.push({
|
|
314
|
+
metric: 'verification_step_duration',
|
|
315
|
+
value: Math.max(avgDuration * 2, 1000), // 2x average or 1 second minimum
|
|
316
|
+
operator: 'lte',
|
|
317
|
+
severity: 'warning',
|
|
318
|
+
description: 'Verification steps should complete within reasonable time'
|
|
319
|
+
});
|
|
320
|
+
thresholds.push({
|
|
321
|
+
metric: 'verification_step_duration',
|
|
322
|
+
value: Math.max(p95Duration * 1.5, 3000), // 1.5x p95 or 3 seconds minimum
|
|
323
|
+
operator: 'lte',
|
|
324
|
+
severity: 'error',
|
|
325
|
+
description: 'Verification steps should not take too long'
|
|
326
|
+
});
|
|
327
|
+
// Success rate threshold
|
|
328
|
+
const successRate = metrics.filter(m => m.success).length / metrics.length;
|
|
329
|
+
thresholds.push({
|
|
330
|
+
metric: 'verification_success_rate',
|
|
331
|
+
value: Math.max(successRate * 0.9, 0.95), // 90% of observed or 95% minimum
|
|
332
|
+
operator: 'gte',
|
|
333
|
+
severity: 'critical',
|
|
334
|
+
description: 'Verification steps should have high success rate'
|
|
335
|
+
});
|
|
336
|
+
return thresholds;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
exports.VerificationMetricsCollector = VerificationMetricsCollector;
|
|
340
|
+
class WebPerformanceCollector {
|
|
341
|
+
static async collectAllMetrics(page, verificationMetrics) {
|
|
342
|
+
// Collect Core Web Vitals
|
|
343
|
+
const coreWebVitals = await CoreWebVitalsCollector.collectVitals(page);
|
|
344
|
+
const vitalsEvaluation = CoreWebVitalsCollector.evaluateVitals(coreWebVitals);
|
|
345
|
+
// Collect performance metrics
|
|
346
|
+
const performanceMetrics = await page.evaluate(() => {
|
|
347
|
+
const navigation = performance.getEntriesByType('navigation')[0];
|
|
348
|
+
const resources = performance.getEntriesByType('resource');
|
|
349
|
+
return {
|
|
350
|
+
page_load_time: navigation ? navigation.loadEventEnd - navigation.fetchStart : 0,
|
|
351
|
+
dom_content_loaded: navigation ? navigation.domContentLoadedEventEnd - navigation.fetchStart : 0,
|
|
352
|
+
network_requests: resources.length,
|
|
353
|
+
failed_requests: resources.filter(r => 'responseStatus' in r && r.responseStatus >= 400).length,
|
|
354
|
+
total_transfer_size: resources.reduce((total, r) => total + (r.transferSize || 0), 0),
|
|
355
|
+
page_size: resources.reduce((total, r) => total + (r.decodedBodySize || 0), 0),
|
|
356
|
+
javascript_execution_time: performance.getEntriesByType('measure')
|
|
357
|
+
.filter(m => m.name.includes('js'))
|
|
358
|
+
.reduce((total, m) => total + m.duration, 0),
|
|
359
|
+
style_recalculation_time: performance.getEntriesByType('measure')
|
|
360
|
+
.filter(m => m.name.includes('style'))
|
|
361
|
+
.reduce((total, m) => total + m.duration, 0)
|
|
362
|
+
};
|
|
363
|
+
});
|
|
364
|
+
return {
|
|
365
|
+
core_web_vitals: coreWebVitals,
|
|
366
|
+
vitals_score: vitalsEvaluation.score,
|
|
367
|
+
vitals_details: vitalsEvaluation.details,
|
|
368
|
+
verification_metrics: verificationMetrics,
|
|
369
|
+
...performanceMetrics
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
exports.WebPerformanceCollector = WebPerformanceCollector;
|