@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.
Files changed (164) hide show
  1. package/README.md +360 -0
  2. package/dist/cli/cli.d.ts +2 -0
  3. package/dist/cli/cli.js +192 -0
  4. package/dist/cli/commands/distributed.d.ts +11 -0
  5. package/dist/cli/commands/distributed.js +179 -0
  6. package/dist/cli/commands/import.d.ts +23 -0
  7. package/dist/cli/commands/import.js +461 -0
  8. package/dist/cli/commands/init.d.ts +7 -0
  9. package/dist/cli/commands/init.js +923 -0
  10. package/dist/cli/commands/mock.d.ts +7 -0
  11. package/dist/cli/commands/mock.js +281 -0
  12. package/dist/cli/commands/report.d.ts +5 -0
  13. package/dist/cli/commands/report.js +70 -0
  14. package/dist/cli/commands/run.d.ts +12 -0
  15. package/dist/cli/commands/run.js +260 -0
  16. package/dist/cli/commands/validate.d.ts +3 -0
  17. package/dist/cli/commands/validate.js +35 -0
  18. package/dist/cli/commands/worker.d.ts +27 -0
  19. package/dist/cli/commands/worker.js +320 -0
  20. package/dist/config/index.d.ts +2 -0
  21. package/dist/config/index.js +20 -0
  22. package/dist/config/parser.d.ts +19 -0
  23. package/dist/config/parser.js +330 -0
  24. package/dist/config/types/global-config.d.ts +74 -0
  25. package/dist/config/types/global-config.js +2 -0
  26. package/dist/config/types/hooks.d.ts +58 -0
  27. package/dist/config/types/hooks.js +3 -0
  28. package/dist/config/types/import-types.d.ts +33 -0
  29. package/dist/config/types/import-types.js +2 -0
  30. package/dist/config/types/index.d.ts +11 -0
  31. package/dist/config/types/index.js +27 -0
  32. package/dist/config/types/load-config.d.ts +32 -0
  33. package/dist/config/types/load-config.js +9 -0
  34. package/dist/config/types/output-config.d.ts +10 -0
  35. package/dist/config/types/output-config.js +2 -0
  36. package/dist/config/types/report-config.d.ts +10 -0
  37. package/dist/config/types/report-config.js +2 -0
  38. package/dist/config/types/runtime-types.d.ts +6 -0
  39. package/dist/config/types/runtime-types.js +2 -0
  40. package/dist/config/types/scenario-config.d.ts +30 -0
  41. package/dist/config/types/scenario-config.js +2 -0
  42. package/dist/config/types/step-types.d.ts +139 -0
  43. package/dist/config/types/step-types.js +2 -0
  44. package/dist/config/types/test-configuration.d.ts +18 -0
  45. package/dist/config/types/test-configuration.js +2 -0
  46. package/dist/config/types/worker-config.d.ts +12 -0
  47. package/dist/config/types/worker-config.js +2 -0
  48. package/dist/config/validator.d.ts +19 -0
  49. package/dist/config/validator.js +198 -0
  50. package/dist/core/csv-data-provider.d.ts +47 -0
  51. package/dist/core/csv-data-provider.js +265 -0
  52. package/dist/core/hooks-manager.d.ts +33 -0
  53. package/dist/core/hooks-manager.js +129 -0
  54. package/dist/core/index.d.ts +5 -0
  55. package/dist/core/index.js +11 -0
  56. package/dist/core/script-executor.d.ts +14 -0
  57. package/dist/core/script-executor.js +290 -0
  58. package/dist/core/step-executor.d.ts +41 -0
  59. package/dist/core/step-executor.js +680 -0
  60. package/dist/core/test-runner.d.ts +34 -0
  61. package/dist/core/test-runner.js +465 -0
  62. package/dist/core/threshold-evaluator.d.ts +43 -0
  63. package/dist/core/threshold-evaluator.js +170 -0
  64. package/dist/core/virtual-user-pool.d.ts +42 -0
  65. package/dist/core/virtual-user-pool.js +136 -0
  66. package/dist/core/virtual-user.d.ts +51 -0
  67. package/dist/core/virtual-user.js +488 -0
  68. package/dist/distributed/coordinator.d.ts +34 -0
  69. package/dist/distributed/coordinator.js +158 -0
  70. package/dist/distributed/health-monitor.d.ts +18 -0
  71. package/dist/distributed/health-monitor.js +72 -0
  72. package/dist/distributed/load-distributor.d.ts +17 -0
  73. package/dist/distributed/load-distributor.js +106 -0
  74. package/dist/distributed/remote-worker.d.ts +37 -0
  75. package/dist/distributed/remote-worker.js +241 -0
  76. package/dist/distributed/result-aggregator.d.ts +43 -0
  77. package/dist/distributed/result-aggregator.js +146 -0
  78. package/dist/dsl/index.d.ts +3 -0
  79. package/dist/dsl/index.js +11 -0
  80. package/dist/dsl/test-builder.d.ts +111 -0
  81. package/dist/dsl/test-builder.js +514 -0
  82. package/dist/importers/har-importer.d.ts +17 -0
  83. package/dist/importers/har-importer.js +172 -0
  84. package/dist/importers/open-api-importer.d.ts +23 -0
  85. package/dist/importers/open-api-importer.js +181 -0
  86. package/dist/importers/wsdl-importer.d.ts +42 -0
  87. package/dist/importers/wsdl-importer.js +440 -0
  88. package/dist/index.d.ts +5 -0
  89. package/dist/index.js +17 -0
  90. package/dist/load-patterns/arrivals.d.ts +7 -0
  91. package/dist/load-patterns/arrivals.js +118 -0
  92. package/dist/load-patterns/base.d.ts +9 -0
  93. package/dist/load-patterns/base.js +2 -0
  94. package/dist/load-patterns/basic.d.ts +7 -0
  95. package/dist/load-patterns/basic.js +117 -0
  96. package/dist/load-patterns/stepping.d.ts +6 -0
  97. package/dist/load-patterns/stepping.js +122 -0
  98. package/dist/metrics/collector.d.ts +72 -0
  99. package/dist/metrics/collector.js +662 -0
  100. package/dist/metrics/types.d.ts +135 -0
  101. package/dist/metrics/types.js +2 -0
  102. package/dist/outputs/base.d.ts +7 -0
  103. package/dist/outputs/base.js +2 -0
  104. package/dist/outputs/csv.d.ts +13 -0
  105. package/dist/outputs/csv.js +163 -0
  106. package/dist/outputs/graphite.d.ts +13 -0
  107. package/dist/outputs/graphite.js +126 -0
  108. package/dist/outputs/influxdb.d.ts +12 -0
  109. package/dist/outputs/influxdb.js +82 -0
  110. package/dist/outputs/json.d.ts +14 -0
  111. package/dist/outputs/json.js +107 -0
  112. package/dist/outputs/streaming-csv.d.ts +37 -0
  113. package/dist/outputs/streaming-csv.js +254 -0
  114. package/dist/outputs/streaming-json.d.ts +43 -0
  115. package/dist/outputs/streaming-json.js +353 -0
  116. package/dist/outputs/webhook.d.ts +16 -0
  117. package/dist/outputs/webhook.js +96 -0
  118. package/dist/protocols/base.d.ts +33 -0
  119. package/dist/protocols/base.js +2 -0
  120. package/dist/protocols/rest/handler.d.ts +67 -0
  121. package/dist/protocols/rest/handler.js +776 -0
  122. package/dist/protocols/soap/handler.d.ts +12 -0
  123. package/dist/protocols/soap/handler.js +165 -0
  124. package/dist/protocols/web/core-web-vitals.d.ts +121 -0
  125. package/dist/protocols/web/core-web-vitals.js +373 -0
  126. package/dist/protocols/web/handler.d.ts +50 -0
  127. package/dist/protocols/web/handler.js +706 -0
  128. package/dist/recorder/native-recorder.d.ts +14 -0
  129. package/dist/recorder/native-recorder.js +533 -0
  130. package/dist/recorder/scenario-recorder.d.ts +55 -0
  131. package/dist/recorder/scenario-recorder.js +296 -0
  132. package/dist/reporting/constants.d.ts +94 -0
  133. package/dist/reporting/constants.js +82 -0
  134. package/dist/reporting/enhanced-html-generator.d.ts +55 -0
  135. package/dist/reporting/enhanced-html-generator.js +965 -0
  136. package/dist/reporting/generator.d.ts +42 -0
  137. package/dist/reporting/generator.js +1217 -0
  138. package/dist/reporting/statistics.d.ts +144 -0
  139. package/dist/reporting/statistics.js +742 -0
  140. package/dist/reporting/templates/enhanced-report.hbs +2812 -0
  141. package/dist/reporting/templates/html.hbs +2453 -0
  142. package/dist/utils/faker-manager.d.ts +55 -0
  143. package/dist/utils/faker-manager.js +166 -0
  144. package/dist/utils/file-manager.d.ts +33 -0
  145. package/dist/utils/file-manager.js +154 -0
  146. package/dist/utils/handlebars-manager.d.ts +42 -0
  147. package/dist/utils/handlebars-manager.js +172 -0
  148. package/dist/utils/logger.d.ts +16 -0
  149. package/dist/utils/logger.js +46 -0
  150. package/dist/utils/template.d.ts +80 -0
  151. package/dist/utils/template.js +513 -0
  152. package/dist/utils/test-output-writer.d.ts +56 -0
  153. package/dist/utils/test-output-writer.js +643 -0
  154. package/dist/utils/time.d.ts +3 -0
  155. package/dist/utils/time.js +23 -0
  156. package/dist/utils/timestamp-helper.d.ts +17 -0
  157. package/dist/utils/timestamp-helper.js +53 -0
  158. package/dist/workers/manager.d.ts +18 -0
  159. package/dist/workers/manager.js +95 -0
  160. package/dist/workers/server.d.ts +21 -0
  161. package/dist/workers/server.js +205 -0
  162. package/dist/workers/worker.d.ts +19 -0
  163. package/dist/workers/worker.js +147 -0
  164. 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;