From ab0be78656867b42d6f4c4fb7855071ea474e76c Mon Sep 17 00:00:00 2001 From: Ajay-Singh-Adhikari Date: Tue, 5 May 2026 16:22:16 +0530 Subject: [PATCH 1/4] feat(collector): add plugin, hook, and DB table metrics Extends ProPerf_Data_Collector to collect server-side metrics that GTmetrix cannot see, closing audit checklist gaps for sections 2, 3, and 4: - Plugin layer: active/inactive/total plugin count via get_plugins() - Hook count: registered hooks via wp_filter count - Database: total DB size + top 10 tables by size via information_schema Updates format_for_bigquery() with 5 new fields sent to BigQuery. Updates BigQuery schema and admin dashboard with targets alongside values. --- includes/class-bigquery-client.php | 15 +++- includes/class-data-collector.php | 111 +++++++++++++++++++++++++++-- properf-wordpress-adapter.php | 86 ++++++++++++++++++---- 3 files changed, 190 insertions(+), 22 deletions(-) diff --git a/includes/class-bigquery-client.php b/includes/class-bigquery-client.php index 756b600..3cf32ae 100644 --- a/includes/class-bigquery-client.php +++ b/includes/class-bigquery-client.php @@ -131,10 +131,19 @@ public function push_metrics( $metrics ) { rewind( $stream ); $schema = array( 'fields' => array( - array( 'name' => 'timestamp_utc', 'type' => 'TIMESTAMP' ), + array( 'name' => 'timestamp_utc', 'type' => 'TIMESTAMP' ), + array( 'name' => 'site_url', 'type' => 'STRING' ), + // Autoload. array( 'name' => 'autoloaded_option_count', 'type' => 'INTEGER' ), - array( 'name' => 'autoloaded_option_size', 'type' => 'INTEGER' ), - array( 'name' => 'site_url', 'type' => 'STRING' ), + array( 'name' => 'autoloaded_option_size', 'type' => 'INTEGER' ), + // Plugins. + array( 'name' => 'active_plugin_count', 'type' => 'INTEGER' ), + array( 'name' => 'inactive_plugin_count', 'type' => 'INTEGER' ), + array( 'name' => 'total_plugin_count', 'type' => 'INTEGER' ), + // Hooks. + array( 'name' => 'hook_count', 'type' => 'INTEGER' ), + // Database. + array( 'name' => 'db_total_size_bytes', 'type' => 'INTEGER' ), ), ); diff --git a/includes/class-data-collector.php b/includes/class-data-collector.php index 0bc2b99..49b0839 100644 --- a/includes/class-data-collector.php +++ b/includes/class-data-collector.php @@ -20,7 +20,12 @@ class ProPerf_Data_Collector { * @return array Collected metrics. */ public function get_data() { - return $this->collect_autoloaded_options(); + return array_merge( + $this->collect_autoloaded_options(), + $this->collect_plugin_metrics(), + $this->collect_hook_metrics(), + $this->collect_db_table_metrics() + ); } /** @@ -67,6 +72,95 @@ public function collect_autoloaded_options() { ); } + /** + * Collect plugin metrics. + * + * Counts active and inactive plugins. Business value: every inactive plugin + * is dead code loading on every request — costing page load time and conversions. + * + * @return array Plugin metrics. + */ + public function collect_plugin_metrics() { + if ( ! function_exists( 'get_plugins' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + $all_plugins = get_plugins(); + $active_plugins = get_option( 'active_plugins', array() ); + $active_count = count( $active_plugins ); + $total_count = count( $all_plugins ); + $inactive_count = $total_count - $active_count; + + return array( + 'plugins' => array( + 'active_count' => $active_count, + 'inactive_count' => $inactive_count, + 'total_count' => $total_count, + ), + ); + } + + /** + * Collect hook metrics. + * + * Counts registered hooks via $wp_filter. Business value: hook count is a + * direct indicator of plugin sprawl and backend execution complexity — MFM + * had 344,000 hooks/page, translating directly to slow checkout. + * + * @return array Hook metrics. + */ + public function collect_hook_metrics() { + global $wp_filter; + $hook_count = is_array( $wp_filter ) ? count( $wp_filter ) : 0; + + return array( + 'hooks' => array( + 'registered_count' => $hook_count, + ), + ); + } + + /** + * Collect database table size metrics. + * + * Reports total DB size and top 10 tables by size. Business value: a bloated + * database means slower queries, slower order processing, and higher hosting + * costs — surfacing which tables are growing enables targeted cleanup before + * the client notices slowness. + * + * @return array Database size metrics. + */ + public function collect_db_table_metrics() { + global $wpdb; + + $total_size = $wpdb->get_var( + 'SELECT SUM(data_length + index_length) + FROM information_schema.tables + WHERE table_schema = DATABASE()' + ); + + $top_tables = $wpdb->get_results( + 'SELECT table_name, ROUND(data_length + index_length) AS size_bytes + FROM information_schema.tables + WHERE table_schema = DATABASE() + ORDER BY size_bytes DESC LIMIT 10' + ); + + $top_tables_map = array(); + if ( $top_tables ) { + foreach ( $top_tables as $table ) { + $top_tables_map[ $table->table_name ] = intval( $table->size_bytes ); + } + } + + return array( + 'database' => array( + 'total_size_bytes' => $total_size ? intval( $total_size ) : 0, + 'top_tables' => $top_tables_map, + ), + ); + } + /** * Format metrics for BigQuery. * @@ -74,14 +168,23 @@ public function collect_autoloaded_options() { * @return array Formatted data for BigQuery. */ public function format_for_bigquery( $metrics ) { - $site_url = get_site_url(); + $site_url = get_site_url(); $timestamp = gmdate( 'Y-m-d H:i:s' ); return array( - 'timestamp_utc' => $timestamp, + 'timestamp_utc' => $timestamp, + 'site_url' => $site_url, + // Autoload. 'autoloaded_option_count' => $metrics['autoloaded_option']['count'], 'autoloaded_option_size' => $metrics['autoloaded_option']['size_bytes'], - 'site_url' => $site_url, + // Plugins. + 'active_plugin_count' => $metrics['plugins']['active_count'], + 'inactive_plugin_count' => $metrics['plugins']['inactive_count'], + 'total_plugin_count' => $metrics['plugins']['total_count'], + // Hooks. + 'hook_count' => $metrics['hooks']['registered_count'], + // Database. + 'db_total_size_bytes' => $metrics['database']['total_size_bytes'], ); } } diff --git a/properf-wordpress-adapter.php b/properf-wordpress-adapter.php index da9bf60..36376ec 100644 --- a/properf-wordpress-adapter.php +++ b/properf-wordpress-adapter.php @@ -215,8 +215,11 @@ function properf_render_dashboard() { wp_die( 'Unauthorized' ); } - $metrics = properf_get_live_data(); + $metrics = properf_get_live_data(); $autoloaded_data_metrics = $metrics['autoloaded_option']; + $plugin_metrics = $metrics['plugins']; + $hook_metrics = $metrics['hooks']; + $db_metrics = $metrics['database']; $count = $autoloaded_data_metrics['count']; $size_bytes = $autoloaded_data_metrics['size_bytes']; @@ -267,16 +270,39 @@ function properf_render_dashboard() { + + — + < 500 KB + + + + + < 15 + + + + + 0 + + + + + < 50,000 + + + + + < 10 GB @@ -302,6 +328,28 @@ function properf_render_dashboard() {

+ +

+ + + + + + + + + + $table_size ) : ?> + + + + + + +
+ +

+ array( + 'count' => 0, + 'size_bytes' => 0, + 'top_size_keys' => array(), + ), + 'plugins' => array( + 'active_count' => 0, + 'inactive_count' => 0, + 'total_count' => 0, + ), + 'hooks' => array( + 'registered_count' => 0, + ), + 'database' => array( + 'total_size_bytes' => 0, + 'top_tables' => array(), + ), + ); + if ( ! class_exists( 'ProPerf_Data_Collector' ) ) { - return array( - 'autoloaded_option' => array( - 'count' => 'Error: Collector Class Missing', - 'size_bytes' => 0, - 'top_size_keys' => array(), - ), - ); + return $empty_response; } try { @@ -590,12 +652,6 @@ function properf_get_live_data() { return $collector->get_data(); } catch ( Exception $e ) { error_log( 'ProPerf Error: ' . $e->getMessage() ); - return array( - 'autoloaded_option' => array( - 'count' => 'Error: ' . $e->getMessage(), - 'size_bytes' => 0, - 'top_size_keys' => array(), - ), - ); + return $empty_response; } } From d13498494f4446f5f12f72a68c960f7967dff4cf Mon Sep 17 00:00:00 2001 From: Ajay-Singh-Adhikari Date: Tue, 5 May 2026 16:28:23 +0530 Subject: [PATCH 2/4] refactor(collector): clean up docblock comments --- includes/class-data-collector.php | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/includes/class-data-collector.php b/includes/class-data-collector.php index 49b0839..97bfea2 100644 --- a/includes/class-data-collector.php +++ b/includes/class-data-collector.php @@ -75,8 +75,7 @@ public function collect_autoloaded_options() { /** * Collect plugin metrics. * - * Counts active and inactive plugins. Business value: every inactive plugin - * is dead code loading on every request — costing page load time and conversions. + * Counts active and inactive plugins installed on the site. * * @return array Plugin metrics. */ @@ -103,9 +102,7 @@ public function collect_plugin_metrics() { /** * Collect hook metrics. * - * Counts registered hooks via $wp_filter. Business value: hook count is a - * direct indicator of plugin sprawl and backend execution complexity — MFM - * had 344,000 hooks/page, translating directly to slow checkout. + * Counts registered hooks via $wp_filter. * * @return array Hook metrics. */ @@ -123,10 +120,7 @@ public function collect_hook_metrics() { /** * Collect database table size metrics. * - * Reports total DB size and top 10 tables by size. Business value: a bloated - * database means slower queries, slower order processing, and higher hosting - * costs — surfacing which tables are growing enables targeted cleanup before - * the client notices slowness. + * Reports total DB size and top 10 tables by size. * * @return array Database size metrics. */ From a18a5e966b57673b2f8551b903caf6d504d19399 Mon Sep 17 00:00:00 2001 From: Ajay-Singh-Adhikari Date: Wed, 6 May 2026 16:25:02 +0530 Subject: [PATCH 3/4] fix(collector): use ARRAY_A to handle information_schema column casing MySQL returns TABLE_NAME (uppercase) from information_schema on some platforms (macOS). Switching to ARRAY_A mode with case fallback ensures top tables map is populated correctly on all environments. --- includes/class-data-collector.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/includes/class-data-collector.php b/includes/class-data-collector.php index 97bfea2..16a73d9 100644 --- a/includes/class-data-collector.php +++ b/includes/class-data-collector.php @@ -137,13 +137,15 @@ public function collect_db_table_metrics() { 'SELECT table_name, ROUND(data_length + index_length) AS size_bytes FROM information_schema.tables WHERE table_schema = DATABASE() - ORDER BY size_bytes DESC LIMIT 10' + ORDER BY size_bytes DESC LIMIT 10', + ARRAY_A ); $top_tables_map = array(); if ( $top_tables ) { foreach ( $top_tables as $table ) { - $top_tables_map[ $table->table_name ] = intval( $table->size_bytes ); + $name = $table['table_name'] ?? $table['TABLE_NAME'] ?? ''; + $top_tables_map[ $name ] = intval( $table['size_bytes'] ?? $table['SIZE_BYTES'] ?? 0 ); } } From 5cfc3ef1538608c6506c3231ab2201740392a56c Mon Sep 17 00:00:00 2001 From: Ajay-Singh-Adhikari Date: Wed, 6 May 2026 17:09:48 +0530 Subject: [PATCH 4/4] feat(g5): add WooCommerce order table metrics Extends ProPerf_Data_Collector with collect_woo_order_metrics(): - order_items table size (bytes) - order_itemmeta table size (bytes) - Oldest order age in days - Archival trigger flag (true when oldest order > 365 days) Gracefully returns zeros + woo_active=false when WooCommerce is not installed. Updates BigQuery schema with 4 new INTEGER fields and adds a WooCommerce Order Metrics section to the admin dashboard with red/green archival trigger status. --- includes/class-bigquery-client.php | 7 ++- includes/class-data-collector.php | 74 +++++++++++++++++++++++++++++- properf-wordpress-adapter.php | 44 ++++++++++++++++++ 3 files changed, 122 insertions(+), 3 deletions(-) diff --git a/includes/class-bigquery-client.php b/includes/class-bigquery-client.php index 3cf32ae..18ef034 100644 --- a/includes/class-bigquery-client.php +++ b/includes/class-bigquery-client.php @@ -143,7 +143,12 @@ public function push_metrics( $metrics ) { // Hooks. array( 'name' => 'hook_count', 'type' => 'INTEGER' ), // Database. - array( 'name' => 'db_total_size_bytes', 'type' => 'INTEGER' ), + array( 'name' => 'db_total_size_bytes', 'type' => 'INTEGER' ), + // WooCommerce orders. + array( 'name' => 'woo_order_items_size_bytes', 'type' => 'INTEGER' ), + array( 'name' => 'woo_order_itemmeta_size_bytes', 'type' => 'INTEGER' ), + array( 'name' => 'woo_oldest_order_age_days', 'type' => 'INTEGER' ), + array( 'name' => 'woo_archival_trigger', 'type' => 'INTEGER' ), ), ); diff --git a/includes/class-data-collector.php b/includes/class-data-collector.php index 16a73d9..325ea61 100644 --- a/includes/class-data-collector.php +++ b/includes/class-data-collector.php @@ -24,7 +24,8 @@ public function get_data() { $this->collect_autoloaded_options(), $this->collect_plugin_metrics(), $this->collect_hook_metrics(), - $this->collect_db_table_metrics() + $this->collect_db_table_metrics(), + $this->collect_woo_order_metrics() ); } @@ -157,6 +158,70 @@ public function collect_db_table_metrics() { ); } + /** + * Collect WooCommerce order table metrics. + * + * Measures order_items and order_itemmeta table sizes, oldest order age, + * and sets an archival trigger flag when orders older than 365 days exist. + * Returns zeros with woo_active=false when WooCommerce is not installed. + * + * @return array WooCommerce order metrics. + */ + public function collect_woo_order_metrics() { + if ( ! class_exists( 'WooCommerce' ) ) { + return array( + 'woo_orders' => array( + 'woo_active' => false, + 'order_items_size_bytes' => 0, + 'order_itemmeta_size_bytes' => 0, + 'oldest_order_age_days' => 0, + 'archival_trigger' => false, + ), + ); + } + + global $wpdb; + + $order_items_table = $wpdb->prefix . 'woocommerce_order_items'; + $order_itemmeta_table = $wpdb->prefix . 'woocommerce_order_itemmeta'; + + $order_items_size = $wpdb->get_var( + $wpdb->prepare( + 'SELECT ROUND(data_length + index_length) + FROM information_schema.tables + WHERE table_schema = DATABASE() AND table_name = %s', + $order_items_table + ) + ); + + $order_itemmeta_size = $wpdb->get_var( + $wpdb->prepare( + 'SELECT ROUND(data_length + index_length) + FROM information_schema.tables + WHERE table_schema = DATABASE() AND table_name = %s', + $order_itemmeta_table + ) + ); + + $oldest_order_age_days = $wpdb->get_var( + "SELECT DATEDIFF(NOW(), MIN(post_date)) + FROM {$wpdb->posts} + WHERE post_type IN ('shop_order', 'wc_order') + AND post_status != 'trash'" + ); + $oldest_order_age_days = $oldest_order_age_days ? intval( $oldest_order_age_days ) : 0; + + return array( + 'woo_orders' => array( + 'woo_active' => true, + 'order_items_size_bytes' => $order_items_size ? intval( $order_items_size ) : 0, + 'order_itemmeta_size_bytes' => $order_itemmeta_size ? intval( $order_itemmeta_size ) : 0, + 'oldest_order_age_days' => $oldest_order_age_days, + 'archival_trigger' => $oldest_order_age_days > 365, + ), + ); + } + /** * Format metrics for BigQuery. * @@ -180,7 +245,12 @@ public function format_for_bigquery( $metrics ) { // Hooks. 'hook_count' => $metrics['hooks']['registered_count'], // Database. - 'db_total_size_bytes' => $metrics['database']['total_size_bytes'], + 'db_total_size_bytes' => $metrics['database']['total_size_bytes'], + // WooCommerce orders. + 'woo_order_items_size_bytes' => $metrics['woo_orders']['order_items_size_bytes'], + 'woo_order_itemmeta_size_bytes' => $metrics['woo_orders']['order_itemmeta_size_bytes'], + 'woo_oldest_order_age_days' => $metrics['woo_orders']['oldest_order_age_days'], + 'woo_archival_trigger' => $metrics['woo_orders']['archival_trigger'] ? 1 : 0, ); } } diff --git a/properf-wordpress-adapter.php b/properf-wordpress-adapter.php index 36376ec..94ade3c 100644 --- a/properf-wordpress-adapter.php +++ b/properf-wordpress-adapter.php @@ -220,6 +220,7 @@ function properf_render_dashboard() { $plugin_metrics = $metrics['plugins']; $hook_metrics = $metrics['hooks']; $db_metrics = $metrics['database']; + $woo_metrics = $metrics['woo_orders']; $count = $autoloaded_data_metrics['count']; $size_bytes = $autoloaded_data_metrics['size_bytes']; @@ -350,6 +351,49 @@ function properf_render_dashboard() {

+ +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
< 365 days
+ + + + + +
+