Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -192,4 +192,18 @@ public function unblur_rows() {

return $db->query( "UPDATE `$prefixed_table_name` SET is_blurred = '0' WHERE status = 'completed' AND is_blurred = '1'" );
}

/**
* Get metrics_data for completed tests.
*
* @return array
*/
public function get_completed_metrics(): array {
return $this->query(
[
'fields' => 'metric_data',
'status' => 'completed',
]
);
}
}
131 changes: 131 additions & 0 deletions inc/Engine/Admin/RocketInsights/GlobalMetrics/Calculator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);

namespace WP_Rocket\Engine\Admin\RocketInsights\GlobalMetrics;

use WP_Rocket\Engine\Admin\RocketInsights\Database\Queries\RocketInsights as Query;

/**
* Calculates average performance metrics across all completed tests.
*/
class Calculator {
/**
* Metric keys to calculate averages for.
*
* @var string[]
*/
private const METRIC_KEYS = [
'largest_contentful_paint',
'total_blocking_time',
'cumulative_layout_shift',
'time_to_first_byte',
];

/**
* Rocket Insights Query instance.
*
* @var Query
*/
private $query;

/**
* Constructor.
*
* @param Query $query Rocket Insights Query instance.
*/
public function __construct( Query $query ) {
$this->query = $query;
}

/**
* Calculate average metrics from all completed tests.
*
* Metrics are returned in the format expected by the Recommendations API:
* - LCP, TTFB: seconds (converted from milliseconds)
* - CLS: unitless decimal
* - TBT: milliseconds (kept as-is)
*
* @return array {
* Average performance metrics.
*
* @type float|null $lcp Largest Contentful Paint in seconds.
* @type float|null $ttfb Time to First Byte in seconds.
* @type float|null $cls Cumulative Layout Shift.
* @type int|null $tbt Total Blocking Time in milliseconds.
* }
*/
public function calculate_average_metrics(): array {
// Get all completed tests with metric_data.
$tests = $this->query->get_completed_metrics();

// No completed tests - return null for all metrics.
if ( empty( $tests ) ) {
return [
'lcp' => null,
'ttfb' => null,
'cls' => null,
'tbt' => null,
];
}

// Initialize accumulators.
$totals = [
'largest_contentful_paint' => 0,
'total_blocking_time' => 0,
'cumulative_layout_shift' => 0,
'time_to_first_byte' => 0,
];

$test_count = 0;

// Sum up all metrics.
foreach ( $tests as $test ) {
$metric_data = json_decode( $test, true );

// Skip if metric_data is empty or not an array.
if ( empty( $metric_data ) || ! is_array( $metric_data ) ) {
continue;
}

// Sum all metric values.
foreach ( self::METRIC_KEYS as $key ) {
if ( isset( $metric_data[ $key ] ) ) {
$totals[ $key ] += (float) $metric_data[ $key ];
}
}

++$test_count;
}

// No valid tests found.
if ( 0 === $test_count ) {
return [
'lcp' => null,
'ttfb' => null,
'cls' => null,
'tbt' => null,
];
}

// Calculate averages and format for Recommendations API.
return [
// LCP: milliseconds → seconds.
'lcp' => round( ( $totals['largest_contentful_paint'] / $test_count ) / 1000, 3 ),
// TTFB: milliseconds → seconds.
'ttfb' => round( ( $totals['time_to_first_byte'] / $test_count ) / 1000, 3 ),
// CLS: unitless decimal.
'cls' => round( $totals['cumulative_layout_shift'] / $test_count, 3 ),
// TBT: keep in milliseconds (integer).
'tbt' => (int) round( $totals['total_blocking_time'] / $test_count ),
];
}

/**
* Check if there are any completed tests available.
*
* @return bool True if at least one completed test exists.
*/
public function has_completed_tests(): bool {
return 0 < (int) $this->query->get_completed_count();
}
}
54 changes: 54 additions & 0 deletions inc/Engine/Admin/RocketInsights/GlobalMetrics/Subscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);

namespace WP_Rocket\Engine\Admin\RocketInsights\GlobalMetrics;

use WP_Rocket\Event_Management\Subscriber_Interface;

/**
* Subscriber that injects average metrics into global score data.
*/
class Subscriber implements Subscriber_Interface {
/**
* GlobalMetricsCalculator instance.
*
* @var Calculator
*/
private $calculator;

/**
* Constructor.
*
* @param Calculator $calculator Metrics calculator instance.
*/
public function __construct( Calculator $calculator ) {
$this->calculator = $calculator;
}

/**
* {@inheritdoc}
*/
public static function get_subscribed_events(): array {
return [
'rocket_insights_global_score_data' => 'add_average_metrics',
];
}

/**
* Add average metrics to global score data.
*
* @param array $data Existing global score data.
* @return array Modified data with average_metrics added.
*/
public function add_average_metrics( array $data ): array {
// Only add metrics if there are completed tests.
if ( 'in-progress' === $data['status'] ) {
$data['average_metrics'] = null;
return $data;
}

$data['average_metrics'] = $this->calculator->calculate_average_metrics();

return $data;
}
}
55 changes: 46 additions & 9 deletions inc/Engine/Admin/RocketInsights/GlobalScore.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,52 @@ private function get_cached_data() {
* @return array Calculated data containing score, pages_num, and status.
*/
private function calculate_and_cache_data(): array {
$status = $this->calculate_current_status();

$data = [
'score' => $this->calculate_global_score(),
'pages_num' => $this->calculate_pages_number(),
'status' => $this->calculate_current_status(),
'is_running' => $this->calculate_current_status() === 'in-progress',
'status' => $status,
'is_running' => 'in-progress' === $status,
];

/**
* Filters the global score data before caching.
*
* Allows other features (e.g., recommendations) to inject additional data
* into the global score transient without tight coupling.
*
* @param array $data {
* Global score data.
*
* @type int $score Global score (0-100).
* @type int $pages_num Number of monitored pages.
* @type string $status Current system status.
* @type bool $is_running Whether tests are in progress.
* }
* @return array Filtered global score data.
*/
$data = wpm_apply_filters_typed( 'array', 'rocket_insights_global_score_data', $data );

// Get previous status before overwriting the transient.
$previous_data = get_transient( self::TRANSIENT_NAME );
$previous_status = is_array( $previous_data ) && isset( $previous_data['status'] )
? $previous_data['status']
: null;

set_transient( self::TRANSIENT_NAME, $data, self::CACHE_EXPIRATION );

// Fire event if status changed.
if ( $previous_status !== $status ) {
/**
* Fires when global score status changes.
*
* @param string $new_status New status.
* @param string $previous_status Previous status.
*/
do_action( 'rocket_insights_global_score_status_changed', $status, $previous_status );
}

return $data;
}

Expand All @@ -103,7 +140,7 @@ private function calculate_global_score(): int {
'status' => 'completed',
'score__not_in' => [ 0 ],
]
);
);

if ( empty( $scores ) ) {
return 0;
Expand Down Expand Up @@ -143,19 +180,19 @@ private function calculate_current_status(): string {
'count' => true,
'status__in' => [ 'to-submit', 'pending', 'in-progress' ],
]
);
);

if ( (int) $in_progress_count > 0 ) {
return 'in-progress';
}

// Check if any URLs are blurred.
$blurred_count = $this->query->query(
[
'count' => true,
'status__in' => [ 'completed' ],
'is_blurred' => 1,
]
[
'count' => true,
'status__in' => [ 'completed' ],
'is_blurred' => 1,
]
);

if ( (int) $blurred_count > 0 ) {
Expand Down
Loading
Loading