From 3a2eefe6643798860a1f614bb32719d1fa87d397 Mon Sep 17 00:00:00 2001 From: Aashish Sharma Date: Wed, 11 Feb 2026 19:10:30 +0530 Subject: [PATCH 1/6] Add optional description support for Server-Timing metrics --- .../class-perflab-server-timing-metric.php | 50 +++++++++++++ .../class-perflab-server-timing.php | 28 ++++--- .../test-perflab-server-timing-metric.php | 16 ++++ .../test-perflab-server-timing.php | 75 +++++++++++++++++++ 4 files changed, 160 insertions(+), 9 deletions(-) diff --git a/plugins/performance-lab/includes/server-timing/class-perflab-server-timing-metric.php b/plugins/performance-lab/includes/server-timing/class-perflab-server-timing-metric.php index fc01e682c3..ef5c7e0970 100644 --- a/plugins/performance-lab/includes/server-timing/class-perflab-server-timing-metric.php +++ b/plugins/performance-lab/includes/server-timing/class-perflab-server-timing-metric.php @@ -37,6 +37,14 @@ class Perflab_Server_Timing_Metric { */ private $before_value; + /** + * The metric description. + * + * @since n.e.x.t + * @var string|null + */ + private $description; + /** * Constructor. * @@ -142,4 +150,46 @@ public function measure_after(): void { $this->set_value( ( microtime( true ) - $this->before_value ) * 1000.0 ); } + + /** + * Sets the metric description. + * + * @since n.e.x.t + * + * @param string|mixed $description The metric description. + */ + public function set_description( $description ): void { + if ( ! is_string( $description ) ) { + _doing_it_wrong( + __METHOD__, + /* translators: %s: PHP parameter name */ + sprintf( esc_html__( 'The %s parameter must be a string.', 'performance-lab' ), '$description' ), + '' + ); + return; + } + + if ( 0 !== did_action( 'perflab_server_timing_send_header' ) && ! doing_action( 'perflab_server_timing_send_header' ) ) { + _doing_it_wrong( + __METHOD__, + /* translators: %s: WordPress action name */ + sprintf( esc_html__( 'The method must be called before or during the %s action.', 'performance-lab' ), 'perflab_server_timing_send_header' ), + '' + ); + return; + } + + $this->description = $description; + } + + /** + * Gets the metric description. + * + * @since n.e.x.t + * + * @return string|null The metric description, or null if none set. + */ + public function get_description(): ?string { + return $this->description; + } } diff --git a/plugins/performance-lab/includes/server-timing/class-perflab-server-timing.php b/plugins/performance-lab/includes/server-timing/class-perflab-server-timing.php index ba16cf6218..52e5a8aaa6 100644 --- a/plugins/performance-lab/includes/server-timing/class-perflab-server-timing.php +++ b/plugins/performance-lab/includes/server-timing/class-perflab-server-timing.php @@ -290,23 +290,33 @@ function ( string $output, ?int $phase ): string { * @since 1.8.0 * * @param Perflab_Server_Timing_Metric $metric The metric to format. - * @return string|null Segment for the Server-Timing header, or null if no value set. + * @return string|null Segment for the Server-Timing header, or null if neither value nor description is set. */ private function format_metric_header_value( Perflab_Server_Timing_Metric $metric ): ?string { - $value = $metric->get_value(); + $value = $metric->get_value(); + $description = $metric->get_description(); - // If no value is set, make sure it's just passed through. - if ( null === $value ) { + // If neither value nor description is set, skip this metric. + if ( null === $value && null === $description ) { return null; } - if ( is_float( $value ) ) { - $value = round( $value, 2 ); - } - // See https://github.com/WordPress/performance/issues/955. $name = preg_replace( '/[^!#$%&\'*+\-.^_`|~0-9a-zA-Z]/', '-', $metric->get_slug() ); - return sprintf( 'wp-%1$s;dur=%2$s', $name, $value ); + $parts = array( sprintf( 'wp-%s', $name ) ); + + if ( null !== $value ) { + if ( is_float( $value ) ) { + $value = round( $value, 2 ); + } + $parts[] = sprintf( 'dur=%s', $value ); + } + + if ( null !== $description ) { + $parts[] = sprintf( 'desc="%s"', $description ); + } + + return implode( ';', $parts ); } } diff --git a/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing-metric.php b/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing-metric.php index 842272f1c5..c0468b6de6 100644 --- a/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing-metric.php +++ b/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing-metric.php @@ -75,4 +75,20 @@ public function test_measure_after_without_before(): void { $this->assertNull( $this->metric->get_value() ); } + + public function test_set_description_with_string(): void { + $this->metric->set_description( 'Database queries' ); + $this->assertSame( 'Database queries', $this->metric->get_description() ); + } + + public function test_set_description_requires_string(): void { + $this->setExpectedIncorrectUsage( Perflab_Server_Timing_Metric::class . '::set_description' ); + + $this->metric->set_description( 123 ); + $this->assertNull( $this->metric->get_description() ); + } + + public function test_get_description_returns_null_by_default(): void { + $this->assertNull( $this->metric->get_description() ); + } } diff --git a/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing.php b/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing.php index 561c3bb481..414838ec9e 100644 --- a/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing.php +++ b/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing.php @@ -281,4 +281,79 @@ public function test_use_output_buffer( callable $set_up, bool $expected ): void $set_up(); $this->assertSame( $expected, $this->server_timing->use_output_buffer() ); } + + /** + * @dataProvider data_get_header_with_description + * + * @phpstan-param array $metrics + */ + public function test_get_header_with_description( string $expected, array $metrics ): void { + foreach ( $metrics as $metric_slug => $args ) { + $this->server_timing->register_metric( $metric_slug, $args ); + } + $this->assertSame( $expected, $this->server_timing->get_header() ); + } + + /** + * @return array + */ + public function data_get_header_with_description(): array { + $measure_with_description = static function ( Perflab_Server_Timing_Metric $metric ): void { + $metric->set_value( 100 ); + $metric->set_description( 'Database queries' ); + }; + $measure_description_only = static function ( Perflab_Server_Timing_Metric $metric ): void { + $metric->set_description( 'Cache operations' ); + }; + $measure_duration_only = static function ( Perflab_Server_Timing_Metric $metric ): void { + $metric->set_value( 50 ); + }; + + return array( + 'metric with duration and description' => array( + 'wp-db-query;dur=100;desc="Database queries"', + array( + 'db-query' => array( + 'measure_callback' => $measure_with_description, + 'access_cap' => 'exist', + ), + ), + ), + 'metric with description only' => array( + 'wp-cache-ops;desc="Cache operations"', + array( + 'cache-ops' => array( + 'measure_callback' => $measure_description_only, + 'access_cap' => 'exist', + ), + ), + ), + 'metric with duration only' => array( + 'wp-duration-only;dur=50', + array( + 'duration-only' => array( + 'measure_callback' => $measure_duration_only, + 'access_cap' => 'exist', + ), + ), + ), + 'mixed metrics' => array( + 'wp-with-both;dur=100;desc="Database queries", wp-desc-only;desc="Cache operations", wp-dur-only;dur=50', + array( + 'with-both' => array( + 'measure_callback' => $measure_with_description, + 'access_cap' => 'exist', + ), + 'desc-only' => array( + 'measure_callback' => $measure_description_only, + 'access_cap' => 'exist', + ), + 'dur-only' => array( + 'measure_callback' => $measure_duration_only, + 'access_cap' => 'exist', + ), + ), + ), + ); + } } From c91e96c97cd50f88562b5d148afae35780bdc206 Mon Sep 17 00:00:00 2001 From: Aashish Sharma Date: Wed, 11 Feb 2026 20:56:51 +0530 Subject: [PATCH 2/6] Sanitize description for HTTP header in Server-Timing metrics and add tests for edge cases --- .../class-perflab-server-timing.php | 10 ++- .../test-perflab-server-timing.php | 64 +++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/plugins/performance-lab/includes/server-timing/class-perflab-server-timing.php b/plugins/performance-lab/includes/server-timing/class-perflab-server-timing.php index 52e5a8aaa6..154985ed63 100644 --- a/plugins/performance-lab/includes/server-timing/class-perflab-server-timing.php +++ b/plugins/performance-lab/includes/server-timing/class-perflab-server-timing.php @@ -314,7 +314,15 @@ private function format_metric_header_value( Perflab_Server_Timing_Metric $metri } if ( null !== $description ) { - $parts[] = sprintf( 'desc="%s"', $description ); + // Sanitize description for HTTP header quoted-string format. + // Remove control characters (CR/LF) and escape backslashes and quotes. + $sanitized_description = preg_replace( '/[\r\n]/', '', $description ); + if ( null === $sanitized_description ) { + $sanitized_description = ''; + } + $sanitized_description = addcslashes( $sanitized_description, '\\' ); + $sanitized_description = addcslashes( $sanitized_description, '"' ); + $parts[] = sprintf( 'desc="%s"', $sanitized_description ); } return implode( ';', $parts ); diff --git a/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing.php b/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing.php index 414838ec9e..2fefb56532 100644 --- a/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing.php +++ b/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing.php @@ -356,4 +356,68 @@ public function data_get_header_with_description(): array { ), ); } + + /** + * @dataProvider data_get_header_with_description_edge_cases + * + * @phpstan-param array $metrics + */ + public function test_get_header_with_description_edge_cases( string $expected, array $metrics ): void { + foreach ( $metrics as $metric_slug => $args ) { + $this->server_timing->register_metric( $metric_slug, $args ); + } + $this->assertSame( $expected, $this->server_timing->get_header() ); + } + + /** + * @return array + */ + public function data_get_header_with_description_edge_cases(): array { + return array( + 'description with double quote' => array( + 'wp-quoted;desc="Say \\"hello\\""', + array( + 'quoted' => array( + 'measure_callback' => static function ( Perflab_Server_Timing_Metric $metric ): void { + $metric->set_description( 'Say "hello"' ); + }, + 'access_cap' => 'exist', + ), + ), + ), + 'description with backslash' => array( + 'wp-backslash;desc="path\\\\to\\\\file"', + array( + 'backslash' => array( + 'measure_callback' => static function ( Perflab_Server_Timing_Metric $metric ): void { + $metric->set_description( 'path\\to\\file' ); + }, + 'access_cap' => 'exist', + ), + ), + ), + 'description with newline' => array( + 'wp-newline;desc="Line 1Line 2"', + array( + 'newline' => array( + 'measure_callback' => static function ( Perflab_Server_Timing_Metric $metric ): void { + $metric->set_description( "Line 1\nLine 2" ); + }, + 'access_cap' => 'exist', + ), + ), + ), + 'description with carriage return' => array( + 'wp-cr;desc="BeforeAfter"', + array( + 'cr' => array( + 'measure_callback' => static function ( Perflab_Server_Timing_Metric $metric ): void { + $metric->set_description( "Before\rAfter" ); + }, + 'access_cap' => 'exist', + ), + ), + ), + ); + } } From 41b677b3290d1c33ce7eaea2bb430fbb473af652 Mon Sep 17 00:00:00 2001 From: Aashish Sharma Date: Mon, 23 Feb 2026 17:02:01 +0530 Subject: [PATCH 3/6] Use native type hint for set_description(), add @covers annotations, and improve type definitions - Replace string|mixed param with native string type hint in set_description(), removing manual type check - Explicitly initialize $description = null - Combine addcslashes() calls into single expression - Add @covers annotations to new description test methods - Import MetricArguments phpstan type and replace mixed with array in test param annotations --- .../class-perflab-server-timing-metric.php | 15 +++------------ .../server-timing/class-perflab-server-timing.php | 3 +-- .../test-perflab-server-timing-metric.php | 10 ++++++++++ .../server-timing/test-perflab-server-timing.php | 10 +++++++--- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/plugins/performance-lab/includes/server-timing/class-perflab-server-timing-metric.php b/plugins/performance-lab/includes/server-timing/class-perflab-server-timing-metric.php index ef5c7e0970..69117c4531 100644 --- a/plugins/performance-lab/includes/server-timing/class-perflab-server-timing-metric.php +++ b/plugins/performance-lab/includes/server-timing/class-perflab-server-timing-metric.php @@ -43,7 +43,7 @@ class Perflab_Server_Timing_Metric { * @since n.e.x.t * @var string|null */ - private $description; + private $description = null; /** * Constructor. @@ -156,18 +156,9 @@ public function measure_after(): void { * * @since n.e.x.t * - * @param string|mixed $description The metric description. + * @param non-empty-string $description The metric description. */ - public function set_description( $description ): void { - if ( ! is_string( $description ) ) { - _doing_it_wrong( - __METHOD__, - /* translators: %s: PHP parameter name */ - sprintf( esc_html__( 'The %s parameter must be a string.', 'performance-lab' ), '$description' ), - '' - ); - return; - } + public function set_description( string $description ): void { if ( 0 !== did_action( 'perflab_server_timing_send_header' ) && ! doing_action( 'perflab_server_timing_send_header' ) ) { _doing_it_wrong( diff --git a/plugins/performance-lab/includes/server-timing/class-perflab-server-timing.php b/plugins/performance-lab/includes/server-timing/class-perflab-server-timing.php index 154985ed63..e1a168d758 100644 --- a/plugins/performance-lab/includes/server-timing/class-perflab-server-timing.php +++ b/plugins/performance-lab/includes/server-timing/class-perflab-server-timing.php @@ -320,8 +320,7 @@ private function format_metric_header_value( Perflab_Server_Timing_Metric $metri if ( null === $sanitized_description ) { $sanitized_description = ''; } - $sanitized_description = addcslashes( $sanitized_description, '\\' ); - $sanitized_description = addcslashes( $sanitized_description, '"' ); + $sanitized_description = addcslashes( $sanitized_description, '\\"' ); $parts[] = sprintf( 'desc="%s"', $sanitized_description ); } diff --git a/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing-metric.php b/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing-metric.php index c0468b6de6..cbc16383f1 100644 --- a/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing-metric.php +++ b/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing-metric.php @@ -76,11 +76,18 @@ public function test_measure_after_without_before(): void { $this->assertNull( $this->metric->get_value() ); } + /** + * @covers Perflab_Server_Timing_Metric::set_description + * @covers Perflab_Server_Timing_Metric::get_description + */ public function test_set_description_with_string(): void { $this->metric->set_description( 'Database queries' ); $this->assertSame( 'Database queries', $this->metric->get_description() ); } + /** + * @covers Perflab_Server_Timing_Metric::set_description + */ public function test_set_description_requires_string(): void { $this->setExpectedIncorrectUsage( Perflab_Server_Timing_Metric::class . '::set_description' ); @@ -88,6 +95,9 @@ public function test_set_description_requires_string(): void { $this->assertNull( $this->metric->get_description() ); } + /** + * @covers Perflab_Server_Timing_Metric::get_description + */ public function test_get_description_returns_null_by_default(): void { $this->assertNull( $this->metric->get_description() ); } diff --git a/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing.php b/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing.php index 2fefb56532..f592f14090 100644 --- a/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing.php +++ b/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing.php @@ -7,6 +7,7 @@ /** * @group server-timing + * @phpstan-import-type MetricArguments from Perflab_Server_Timing */ class Test_Perflab_Server_Timing extends WP_UnitTestCase { @@ -154,7 +155,8 @@ public function test_register_metric_replaces_slashes(): void { /** * @dataProvider data_get_header * - * @phpstan-param array $metrics + * @param string $expected The expected header value. + * @param array $metrics The metric configurations. */ public function test_get_header( string $expected, array $metrics ): void { foreach ( $metrics as $metric_slug => $args ) { @@ -285,7 +287,8 @@ public function test_use_output_buffer( callable $set_up, bool $expected ): void /** * @dataProvider data_get_header_with_description * - * @phpstan-param array $metrics + * @param string $expected The expected header value. + * @param array $metrics The metric configurations. */ public function test_get_header_with_description( string $expected, array $metrics ): void { foreach ( $metrics as $metric_slug => $args ) { @@ -360,7 +363,8 @@ public function data_get_header_with_description(): array { /** * @dataProvider data_get_header_with_description_edge_cases * - * @phpstan-param array $metrics + * @param string $expected The expected header value. + * @param array $metrics The metric configurations. */ public function test_get_header_with_description_edge_cases( string $expected, array $metrics ): void { foreach ( $metrics as $metric_slug => $args ) { From ca2985e1af7e7fe4ba073b202ae965ff8f11e695 Mon Sep 17 00:00:00 2001 From: Aashish Sharma Date: Wed, 25 Feb 2026 17:24:46 +0530 Subject: [PATCH 4/6] chore: Remove redundant test for set_description() requiring a string. --- .../class-perflab-server-timing-metric.php | 1 - .../test-perflab-server-timing-metric.php | 10 ---------- 2 files changed, 11 deletions(-) diff --git a/plugins/performance-lab/includes/server-timing/class-perflab-server-timing-metric.php b/plugins/performance-lab/includes/server-timing/class-perflab-server-timing-metric.php index 69117c4531..4d557a4eae 100644 --- a/plugins/performance-lab/includes/server-timing/class-perflab-server-timing-metric.php +++ b/plugins/performance-lab/includes/server-timing/class-perflab-server-timing-metric.php @@ -159,7 +159,6 @@ public function measure_after(): void { * @param non-empty-string $description The metric description. */ public function set_description( string $description ): void { - if ( 0 !== did_action( 'perflab_server_timing_send_header' ) && ! doing_action( 'perflab_server_timing_send_header' ) ) { _doing_it_wrong( __METHOD__, diff --git a/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing-metric.php b/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing-metric.php index cbc16383f1..db35bad213 100644 --- a/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing-metric.php +++ b/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing-metric.php @@ -85,16 +85,6 @@ public function test_set_description_with_string(): void { $this->assertSame( 'Database queries', $this->metric->get_description() ); } - /** - * @covers Perflab_Server_Timing_Metric::set_description - */ - public function test_set_description_requires_string(): void { - $this->setExpectedIncorrectUsage( Perflab_Server_Timing_Metric::class . '::set_description' ); - - $this->metric->set_description( 123 ); - $this->assertNull( $this->metric->get_description() ); - } - /** * @covers Perflab_Server_Timing_Metric::get_description */ From d5b4eecd49af30cf4ca1eefdef4ca1a5f869f780 Mon Sep 17 00:00:00 2001 From: Aashish Sharma Date: Thu, 26 Feb 2026 18:45:36 +0530 Subject: [PATCH 5/6] chore: use str_replace instead of preg_replace for simple sanitazation and improved performance. --- .../includes/server-timing/class-perflab-server-timing.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/plugins/performance-lab/includes/server-timing/class-perflab-server-timing.php b/plugins/performance-lab/includes/server-timing/class-perflab-server-timing.php index e1a168d758..c52a750df3 100644 --- a/plugins/performance-lab/includes/server-timing/class-perflab-server-timing.php +++ b/plugins/performance-lab/includes/server-timing/class-perflab-server-timing.php @@ -316,10 +316,7 @@ private function format_metric_header_value( Perflab_Server_Timing_Metric $metri if ( null !== $description ) { // Sanitize description for HTTP header quoted-string format. // Remove control characters (CR/LF) and escape backslashes and quotes. - $sanitized_description = preg_replace( '/[\r\n]/', '', $description ); - if ( null === $sanitized_description ) { - $sanitized_description = ''; - } + $sanitized_description = str_replace( array( "\r", "\n" ), '', $description ); $sanitized_description = addcslashes( $sanitized_description, '\\"' ); $parts[] = sprintf( 'desc="%s"', $sanitized_description ); } From 83dd4e266e418548d066302070bd684bce0bfa72 Mon Sep 17 00:00:00 2001 From: Aashish Sharma Date: Fri, 27 Feb 2026 16:26:48 +0530 Subject: [PATCH 6/6] fix: update for name-only metrics support --- .../class-perflab-server-timing.php | 9 ++------- .../test-perflab-server-timing.php | 18 +++++++++++++++++- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/plugins/performance-lab/includes/server-timing/class-perflab-server-timing.php b/plugins/performance-lab/includes/server-timing/class-perflab-server-timing.php index c52a750df3..6e69553a92 100644 --- a/plugins/performance-lab/includes/server-timing/class-perflab-server-timing.php +++ b/plugins/performance-lab/includes/server-timing/class-perflab-server-timing.php @@ -290,17 +290,12 @@ function ( string $output, ?int $phase ): string { * @since 1.8.0 * * @param Perflab_Server_Timing_Metric $metric The metric to format. - * @return string|null Segment for the Server-Timing header, or null if neither value nor description is set. + * @return string Segment for the Server-Timing header. */ - private function format_metric_header_value( Perflab_Server_Timing_Metric $metric ): ?string { + private function format_metric_header_value( Perflab_Server_Timing_Metric $metric ): string { $value = $metric->get_value(); $description = $metric->get_description(); - // If neither value nor description is set, skip this metric. - if ( null === $value && null === $description ) { - return null; - } - // See https://github.com/WordPress/performance/issues/955. $name = preg_replace( '/[^!#$%&\'*+\-.^_`|~0-9a-zA-Z]/', '-', $metric->get_slug() ); diff --git a/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing.php b/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing.php index f592f14090..799d2c9250 100644 --- a/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing.php +++ b/plugins/performance-lab/tests/includes/server-timing/test-perflab-server-timing.php @@ -311,6 +311,9 @@ public function data_get_header_with_description(): array { $measure_duration_only = static function ( Perflab_Server_Timing_Metric $metric ): void { $metric->set_value( 50 ); }; + $measure_name_only = static function ( Perflab_Server_Timing_Metric $metric ): void { + unset( $metric ); + }; return array( 'metric with duration and description' => array( @@ -340,8 +343,17 @@ public function data_get_header_with_description(): array { ), ), ), + 'metric with name only' => array( + 'wp-missed-cache', + array( + 'missed-cache' => array( + 'measure_callback' => $measure_name_only, + 'access_cap' => 'exist', + ), + ), + ), 'mixed metrics' => array( - 'wp-with-both;dur=100;desc="Database queries", wp-desc-only;desc="Cache operations", wp-dur-only;dur=50', + 'wp-with-both;dur=100;desc="Database queries", wp-desc-only;desc="Cache operations", wp-dur-only;dur=50, wp-name-only', array( 'with-both' => array( 'measure_callback' => $measure_with_description, @@ -355,6 +367,10 @@ public function data_get_header_with_description(): array { 'measure_callback' => $measure_duration_only, 'access_cap' => 'exist', ), + 'name-only' => array( + 'measure_callback' => $measure_name_only, + 'access_cap' => 'exist', + ), ), ), );