From c27d5335dc7009b7d1e88bf2d9c8661a89a6ab4b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 12:15:07 +0000 Subject: [PATCH 01/11] Initial plan From a7f209a6f101534a2ac137ff479dfd1a0e5a2e31 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 12:23:20 +0000 Subject: [PATCH 02/11] Add wp profile requests command Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- composer.json | 3 +- features/profile-requests.feature | 37 ++++++++++++++++ src/Command.php | 72 +++++++++++++++++++++++++++++++ src/Profiler.php | 50 +++++++++++++++++++-- 4 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 features/profile-requests.feature diff --git a/composer.json b/composer.json index 7c2eee3a..03dc8d7e 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,8 @@ "profile stage", "profile hook", "profile eval", - "profile eval-file" + "profile eval-file", + "profile requests" ], "readme": { "sections": [ diff --git a/features/profile-requests.feature b/features/profile-requests.feature new file mode 100644 index 00000000..3f85d0f3 --- /dev/null +++ b/features/profile-requests.feature @@ -0,0 +1,37 @@ +Feature: Profile HTTP requests + + Scenario: Profile HTTP requests during WordPress load + Given a WP install + + When I run `wp profile requests --fields=method,url,status,time` + Then STDOUT should contain: + """ + method + """ + And STDOUT should contain: + """ + url + """ + And STDOUT should contain: + """ + status + """ + And STDOUT should contain: + """ + time + """ + + Scenario: Profile shows no requests when none are made + Given a WP install + And a wp-content/mu-plugins/no-requests.php file: + """ + display_items( $loggers, true, $order, $orderby ); } + /** + * Profile HTTP requests made during the WordPress load process. + * + * Monitors all HTTP requests made during the WordPress load process, + * displaying information about each request including URL, method, + * execution time, and response code. + * + * ## OPTIONS + * + * [--url=] + * : Execute a request against a specified URL. Defaults to the home URL. + * + * [--fields=] + * : Limit the output to specific fields. Default is all fields. + * + * [--format=] + * : Render output in a particular format. + * --- + * default: table + * options: + * - table + * - json + * - yaml + * - csv + * --- + * + * [--order=] + * : Ascending or Descending order. + * --- + * default: ASC + * options: + * - ASC + * - DESC + * --- + * + * [--orderby=] + * : Set orderby which field. + * + * ## EXAMPLES + * + * # List all HTTP requests during page load + * $ wp profile requests + * +-----------+----------------------------+----------+---------+ + * | method | url | status | time | + * +-----------+----------------------------+----------+---------+ + * | GET | https://api.example.com | 200 | 0.2341s | + * | POST | https://api.example.com | 201 | 0.1653s | + * +-----------+----------------------------+----------+---------+ + * | total (2) | | | 0.3994s | + * +-----------+----------------------------+----------+---------+ + * + * @when before_wp_load + */ + public function requests( $args, $assoc_args ) { + $order = Utils\get_flag_value( $assoc_args, 'order', 'ASC' ); + $orderby = Utils\get_flag_value( $assoc_args, 'orderby', null ); + + $profiler = new Profiler( 'request', false ); + $profiler->run(); + + $fields = array( + 'method', + 'url', + 'status', + 'time', + ); + $formatter = new Formatter( $assoc_args, $fields ); + $loggers = $profiler->get_loggers(); + + $formatter->display_items( $loggers, true, $order, $orderby ); + } + /** * Profile arbitrary code execution. * diff --git a/src/Profiler.php b/src/Profiler.php index 7d376575..013722df 100644 --- a/src/Profiler.php +++ b/src/Profiler.php @@ -48,6 +48,9 @@ class Profiler { private $tick_cache_hit_offset = null; private $tick_cache_miss_offset = null; + private $request_start_time = null; + private $request_args = null; + public function __construct( $type, $focus ) { $this->type = $type; $this->focus = $focus; @@ -127,8 +130,8 @@ function () { } else { WP_CLI::add_wp_hook( 'all', array( $this, 'wp_hook_begin' ) ); } - WP_CLI::add_wp_hook( 'pre_http_request', array( $this, 'wp_request_begin' ) ); - WP_CLI::add_wp_hook( 'http_api_debug', array( $this, 'wp_request_end' ) ); + WP_CLI::add_wp_hook( 'pre_http_request', array( $this, 'wp_request_begin' ), 10, 3 ); + WP_CLI::add_wp_hook( 'http_api_debug', array( $this, 'wp_request_end' ), 10, 5 ); $this->load_wordpress_with_template(); } @@ -381,20 +384,59 @@ public function handle_function_tick() { /** * Profiling request time for any active Loggers */ - public function wp_request_begin( $filter_value = null ) { + public function wp_request_begin( $filter_value = null, $parsed_args = null, $url = null ) { foreach ( Logger::$active_loggers as $logger ) { $logger->start_request_timer(); } + + // For request profiling, capture details of each HTTP request + if ( 'request' === $this->type ) { + $this->request_start_time = microtime( true ); + $this->request_args = array( + 'url' => $url, + 'method' => isset( $parsed_args['method'] ) ? $parsed_args['method'] : 'GET', + ); + } + return $filter_value; } /** * Profiling request time for any active Loggers */ - public function wp_request_end( $filter_value = null ) { + public function wp_request_end( $filter_value = null, $response = null, $context = null, $class = null, $parsed_args = null, $url = null ) { foreach ( Logger::$active_loggers as $logger ) { $logger->stop_request_timer(); } + + // For request profiling, log individual request + if ( 'request' === $this->type && ! is_null( $this->request_start_time ) ) { + $request_time = microtime( true ) - $this->request_start_time; + $status = ''; + + // Extract status code from response + if ( is_wp_error( $response ) ) { + $status = 'Error'; + } elseif ( is_array( $response ) && isset( $response['response']['code'] ) ) { + $status = $response['response']['code']; + } + + $logger = new Logger( + array( + 'method' => $this->request_args['method'], + 'url' => $this->request_args['url'], + 'status' => $status, + ) + ); + $logger->time = $request_time; + + $this->loggers[] = $logger; + + // Reset for next request + $this->request_start_time = null; + $this->request_args = null; + } + return $filter_value; } From 9c3cf1da9d42563c10075de8b0ef5e2c8506375b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 12:25:39 +0000 Subject: [PATCH 03/11] Update test to include requests command in usage output Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/profile.feature | 1 + 1 file changed, 1 insertion(+) diff --git a/features/profile.feature b/features/profile.feature index 459d7d3b..225b3816 100644 --- a/features/profile.feature +++ b/features/profile.feature @@ -9,6 +9,7 @@ Feature: Basic profile usage usage: wp profile eval [--hook[=]] [--fields=] [--format=] [--order=] [--orderby=] or: wp profile eval-file [--hook[=]] [--fields=] [--format=] [--order=] [--orderby=] or: wp profile hook [] [--all] [--spotlight] [--url=] [--fields=] [--format=] [--order=] [--orderby=] + or: wp profile requests [--url=] [--fields=] [--format=] [--order=] [--orderby=] or: wp profile stage [] [--all] [--spotlight] [--url=] [--fields=] [--format=] [--order=] [--orderby=] See 'wp help profile ' for more information on a specific command. From 69f6d5636e3440ecc6bbf62587501f4d345eea74 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 12:26:31 +0000 Subject: [PATCH 04/11] Avoid adding 'all' hook when profiling requests Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Profiler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Profiler.php b/src/Profiler.php index 013722df..326cf257 100644 --- a/src/Profiler.php +++ b/src/Profiler.php @@ -127,7 +127,7 @@ function () { ) { $start_hook = substr( $this->focus, 0, -6 ); WP_CLI::add_wp_hook( $start_hook, array( $this, 'wp_tick_profile_begin' ), 9999 ); - } else { + } elseif ( 'request' !== $this->type ) { WP_CLI::add_wp_hook( 'all', array( $this, 'wp_hook_begin' ) ); } WP_CLI::add_wp_hook( 'pre_http_request', array( $this, 'wp_request_begin' ), 10, 3 ); From 8418ad2d970dc069d5cd57f54762db6989669c4c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 12:27:53 +0000 Subject: [PATCH 05/11] Fix hook parameter signatures to match WordPress Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Profiler.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Profiler.php b/src/Profiler.php index 326cf257..c572bdb3 100644 --- a/src/Profiler.php +++ b/src/Profiler.php @@ -384,7 +384,7 @@ public function handle_function_tick() { /** * Profiling request time for any active Loggers */ - public function wp_request_begin( $filter_value = null, $parsed_args = null, $url = null ) { + public function wp_request_begin( $preempt = null, $parsed_args = null, $url = null ) { foreach ( Logger::$active_loggers as $logger ) { $logger->start_request_timer(); } @@ -398,13 +398,13 @@ public function wp_request_begin( $filter_value = null, $parsed_args = null, $ur ); } - return $filter_value; + return $preempt; } /** * Profiling request time for any active Loggers */ - public function wp_request_end( $filter_value = null, $response = null, $context = null, $class = null, $parsed_args = null, $url = null ) { + public function wp_request_end( $response = null, $context = null, $class = null, $parsed_args = null, $url = null ) { foreach ( Logger::$active_loggers as $logger ) { $logger->stop_request_timer(); } @@ -437,7 +437,7 @@ public function wp_request_end( $filter_value = null, $response = null, $context $this->request_args = null; } - return $filter_value; + return $response; } /** From 49258a1e0944cad6c61bc0b9a9e9bcb22d7561c6 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 7 Nov 2025 15:32:15 +0100 Subject: [PATCH 06/11] Lint fixes --- src/Profiler.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Profiler.php b/src/Profiler.php index c572bdb3..07f4da2e 100644 --- a/src/Profiler.php +++ b/src/Profiler.php @@ -404,7 +404,7 @@ public function wp_request_begin( $preempt = null, $parsed_args = null, $url = n /** * Profiling request time for any active Loggers */ - public function wp_request_end( $response = null, $context = null, $class = null, $parsed_args = null, $url = null ) { + public function wp_request_end( $response = null ) { foreach ( Logger::$active_loggers as $logger ) { $logger->stop_request_timer(); } @@ -421,7 +421,7 @@ public function wp_request_end( $response = null, $context = null, $class = null $status = $response['response']['code']; } - $logger = new Logger( + $logger = new Logger( array( 'method' => $this->request_args['method'], 'url' => $this->request_args['url'], From 9ae69612e9e7e9a7683735e5523dc7d5335d53a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 20:58:50 +0000 Subject: [PATCH 07/11] Improve test with mu-plugin making HTTP requests Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/profile-requests.feature | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/features/profile-requests.feature b/features/profile-requests.feature index 3f85d0f3..de897b15 100644 --- a/features/profile-requests.feature +++ b/features/profile-requests.feature @@ -2,23 +2,23 @@ Feature: Profile HTTP requests Scenario: Profile HTTP requests during WordPress load Given a WP install - - When I run `wp profile requests --fields=method,url,status,time` - Then STDOUT should contain: - """ - method - """ - And STDOUT should contain: - """ - url + And a wp-content/mu-plugins/http-requests.php file: """ - And STDOUT should contain: - """ - status + 'test' ) ); + }); """ + + When I run `wp profile requests --fields=method,url` + Then STDOUT should be a table containing rows: + | method | url | + | GET | https://www.apple.com/ | + | POST | https://www.example.com/ | And STDOUT should contain: """ - time + total (2) """ Scenario: Profile shows no requests when none are made From 721c27a7471af9f7fba6794de1b93bb2213932a0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 21:04:30 +0000 Subject: [PATCH 08/11] Add HTTP mocking to test to avoid real requests Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/profile-requests.feature | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/features/profile-requests.feature b/features/profile-requests.feature index de897b15..50ddd1f7 100644 --- a/features/profile-requests.feature +++ b/features/profile-requests.feature @@ -2,6 +2,20 @@ Feature: Profile HTTP requests Scenario: Profile HTTP requests during WordPress load Given a WP install + And that HTTP requests to https://www.apple.com/ will respond with: + """ + HTTP/1.1 200 + Content-Type: text/plain + + Hello world + """ + And that HTTP requests to https://www.example.com/ will respond with: + """ + HTTP/1.1 201 + Content-Type: application/json + + {"status":"created"} + """ And a wp-content/mu-plugins/http-requests.php file: """ Date: Mon, 10 Nov 2025 22:16:26 +0100 Subject: [PATCH 09/11] Update src/Profiler.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Profiler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Profiler.php b/src/Profiler.php index 07f4da2e..fec11a3f 100644 --- a/src/Profiler.php +++ b/src/Profiler.php @@ -404,7 +404,7 @@ public function wp_request_begin( $preempt = null, $parsed_args = null, $url = n /** * Profiling request time for any active Loggers */ - public function wp_request_end( $response = null ) { + public function wp_request_end( $response = null, $context = null, $class = null, $parsed_args = null, $url = null ) { foreach ( Logger::$active_loggers as $logger ) { $logger->stop_request_timer(); } From dbbb4f138775025c25844ae9229bc829cb85c103 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 10 Nov 2025 22:16:33 +0100 Subject: [PATCH 10/11] Update src/Profiler.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Profiler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Profiler.php b/src/Profiler.php index fec11a3f..3db7b89e 100644 --- a/src/Profiler.php +++ b/src/Profiler.php @@ -394,7 +394,7 @@ public function wp_request_begin( $preempt = null, $parsed_args = null, $url = n $this->request_start_time = microtime( true ); $this->request_args = array( 'url' => $url, - 'method' => isset( $parsed_args['method'] ) ? $parsed_args['method'] : 'GET', + 'method' => ( is_array( $parsed_args ) && isset( $parsed_args['method'] ) ) ? $parsed_args['method'] : 'GET', ); } From 64c64266804489f8b31229a9d7fb57fefc8c6ba8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 21:18:43 +0000 Subject: [PATCH 11/11] Handle preempted HTTP requests by resetting properties Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Profiler.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Profiler.php b/src/Profiler.php index 3db7b89e..228d4799 100644 --- a/src/Profiler.php +++ b/src/Profiler.php @@ -391,6 +391,11 @@ public function wp_request_begin( $preempt = null, $parsed_args = null, $url = n // For request profiling, capture details of each HTTP request if ( 'request' === $this->type ) { + // Reset properties first to handle cases where previous request was preempted + $this->request_start_time = null; + $this->request_args = null; + + // Now capture the new request details $this->request_start_time = microtime( true ); $this->request_args = array( 'url' => $url,