first commit

This commit is contained in:
User A0264400
2026-04-01 23:20:16 +03:00
commit a766acdc90
23071 changed files with 4933189 additions and 0 deletions

View File

@@ -0,0 +1,423 @@
<?php
/**
* UAGB Block Analytics Manager.
*
* Class to manage block usage analytics collection and reporting.
*
* @since 2.19.13
* @package UAGB
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'UAGB_Block_Analytics' ) ) {
/**
* Class UAGB_Block_Analytics
*
* Manages block usage analytics collection and reporting.
*
* @since 2.19.13
* @package UAGB
*/
class UAGB_Block_Analytics {
/**
* Member Variable
*
* @var UAGB_Block_Analytics|null
* @since 2.19.13
*/
private static $instance;
/**
* Block stats processor instance.
*
* @var UAGB_Block_Stats_Processor
* @since 2.19.13
*/
private $stats_processor;
/**
* Incremental block tracker instance.
*
* @var UAGB_Incremental_Block_Tracker
* @since 2.19.13
*/
private $incremental_tracker;
/**
* Initiator
*
* @since 2.19.13
* @return UAGB_Block_Analytics
*/
public static function get_instance() {
if ( ! isset( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*
* @since 2.19.13
* @return void
*/
public function __construct() {
// Load the stats processor and incremental tracker.
require_once UAGB_DIR . 'classes/analytics/class-uagb-block-stats-processor.php';
require_once UAGB_DIR . 'classes/analytics/class-uagb-incremental-block-tracker.php';
$this->stats_processor = new UAGB_Block_Stats_Processor();
$this->incremental_tracker = UAGB_Incremental_Block_Tracker::get_instance();
// Hook into analytics option changes.
add_action( 'update_option_spectra_usage_optin', array( $this, 'handle_analytics_optin_change' ), 10, 3 );
add_action( 'add_option_spectra_usage_optin', array( $this, 'handle_analytics_optin_add' ), 10, 2 );
// Hook into plugin activation for first-run stats collection.
add_action( 'init', array( $this, 'maybe_start_first_run_collection' ) );
}
/**
* Handle analytics opt-in option update.
*
* @param string $old_value Old value.
* @param string $value New value.
* @param string $option Option name.
* @since 2.19.13
* @return void
*/
public function handle_analytics_optin_change( $old_value, $value, $option ) {
if ( 'yes' === $value && 'yes' !== $old_value ) {
// Analytics was just enabled, start collection.
$this->start_stats_collection();
}
}
/**
* Handle analytics opt-in option addition.
*
* @param string $option Option name.
* @param string $value Option value.
* @since 2.19.13
* @return void
*/
public function handle_analytics_optin_add( $option, $value ) {
if ( 'yes' === $value ) {
// Analytics was enabled, start collection.
$this->start_stats_collection();
}
}
/**
* Maybe start first-run stats collection.
*
* This is called during plugin initialization to check if this is a first-run
* installation and start stats collection.
*
* @since 2.19.13
* @return void
*/
public function maybe_start_first_run_collection() {
// Check if this is a first-run (plugin just installed).
$status = get_option( 'uagb_block_usage_status', array() );
if ( ! is_array( $status ) ) {
$status = array();
}
if ( empty( $status['first_run_check'] ) ) {
// Mark first run check as done.
$status['first_run_check'] = true;
update_option( 'uagb_block_usage_status', $status );
// Start initial stats collection and setup incremental tracking.
$this->start_initial_setup();
}
}
/**
* Start block usage stats collection (initial scan only).
*
* This method triggers the background process ONLY for initial setup.
* After initial setup, all tracking is done via real-time incremental updates.
*
* @since 2.19.13
* @return void
*/
public function start_stats_collection() {
// Only start if analytics is enabled or this is first run.
$analytics_enabled = get_option( 'spectra_usage_optin', 'no' ) === 'yes';
$status = get_option( 'uagb_block_usage_status', array() );
if ( ! is_array( $status ) ) {
$status = array();
}
$is_first_run = empty( $status['first_run_check'] );
if ( ! $analytics_enabled && ! $is_first_run ) {
return;
}
// Check if collection is already in progress.
if ( ! empty( $status['is_processing'] ) ) {
return;
}
// Only run background scan if we don't have existing stats or this is forced refresh.
$analytics_data = get_option( 'uagb_block_usage_data', array() );
if ( ! is_array( $analytics_data ) ) {
$analytics_data = array();
}
$has_existing_stats = ! empty( $analytics_data['block_usage_stats'] );
// Skip background scan if we already have stats and this isn't first run.
if ( $has_existing_stats && ! $is_first_run ) {
return;
}
// Start the background collection process.
$this->stats_processor->start_collection();
}
/**
* Get block usage statistics for analytics reporting.
*
* This method merges block usage statistics with existing spectra stats,
* ensuring numeric_values are added (not replaced) if they already exist.
*
* @since 2.19.13
* @param array $existing_stats Existing spectra stats to merge with.
* @return array Merged stats with block usage data.
*/
public function get_block_stats_for_analytics( $existing_stats = array() ) {
// Only return stats if analytics is enabled.
if ( get_option( 'spectra_usage_optin', 'no' ) !== 'yes' ) {
return $existing_stats;
}
$stats = UAGB_Block_Stats_Processor::get_block_stats();
$collection_complete = UAGB_Block_Stats_Processor::is_collection_complete();
$last_collection = UAGB_Block_Stats_Processor::get_last_collection_time();
// Format block usage stats to add 'block_usage_' prefix to the keys.
$formatted_block_usage_stats = array_combine(
array_map(
function ( $key ) {
return 'block_usage_' . $key;
},
array_keys( $stats )
),
array_values( $stats )
);
// Ensure array_combine succeeded, otherwise use empty array.
if ( ! is_array( $formatted_block_usage_stats ) ) {
$formatted_block_usage_stats = array();
}
// Get site activity level for Active Site / Super Site KPIs.
$site_activity = $this->get_site_activity_level();
// Prepare advanced stats structure.
$advanced_stats = array(
'numeric_values' => $formatted_block_usage_stats,
'block_usage_stats_metadata' => array(
'collection_complete' => $collection_complete,
'last_collected' => $last_collection ? gmdate( 'Y-m-d H:i:s', $last_collection ) : null,
'total_blocks_tracked' => count( array_filter( $stats ) ),
'most_used_blocks' => $this->get_most_used_blocks( $stats, 10 ),
),
'site_activity' => $site_activity,
);
// Merge numeric_values by adding numbers if they already exist.
// Check if numeric_values array exists in existing_stats and validate it's an array.
if ( isset( $existing_stats['numeric_values'] ) && is_array( $existing_stats['numeric_values'] ) ) {
// Loop through each block's usage count from advanced_stats.
foreach ( $advanced_stats['numeric_values'] as $key => $value ) {
// If the key exists in existing_stats and both values are numeric, add them together.
// Otherwise, use the new value from advanced_stats (either new key or non-numeric value).
$existing_stats['numeric_values'][ $key ] = ( isset( $existing_stats['numeric_values'][ $key ] )
&& is_numeric( $value )
&& is_numeric( $existing_stats['numeric_values'][ $key ] ) )
? $existing_stats['numeric_values'][ $key ] + $value
: $value;
}
// Remove numeric_values from advanced_stats to prevent duplication in array_merge_recursive below.
unset( $advanced_stats['numeric_values'] );
}
// Merge remaining advanced stats (metadata, etc.) with existing stats.
return array_merge_recursive( $existing_stats, $advanced_stats );
}
/**
* Get the most used blocks from stats.
*
* @param array $stats Block usage statistics.
* @param int $limit Number of top blocks to return.
* @since 2.19.13
* @return array Top used blocks.
*/
private function get_most_used_blocks( $stats, $limit = 10 ) {
// Filter out blocks with 0 usage and sort by usage count.
$filtered_stats = array_filter( $stats );
arsort( $filtered_stats );
// Return top blocks.
return array_slice( $filtered_stats, 0, $limit, true );
}
/**
* Force refresh block statistics (for data validation only).
*
* This method should only be used for manual data validation or troubleshooting.
* Normal operation relies on real-time incremental tracking.
*
* @since 2.19.13
* @return void
*/
public function force_refresh_stats() {
// Clear existing processing flag to allow new collection.
$status = get_option( 'uagb_block_usage_status', array() );
if ( ! is_array( $status ) ) {
$status = array();
}
$status['is_processing'] = false;
update_option( 'uagb_block_usage_status', $status );
// Reinitialize post tracking metadata.
$this->incremental_tracker->initialize_existing_posts();
// Start full collection for validation.
$this->start_stats_collection();
}
/**
* Start initial setup combining background scan and incremental tracking.
*
* This method is called on first-run to both scan existing content
* and setup incremental tracking for future changes.
*
* @since 2.19.13
* @return void
*/
public function start_initial_setup() {
// Only setup if analytics is enabled or this is first run.
$analytics_enabled = get_option( 'spectra_usage_optin', 'no' ) === 'yes';
$status = get_option( 'uagb_block_usage_status', array() );
if ( ! is_array( $status ) ) {
$status = array();
}
$is_first_run = empty( $status['first_run_check'] );
if ( ! $analytics_enabled && ! $is_first_run ) {
return;
}
// Initialize existing posts for incremental tracking.
$this->incremental_tracker->initialize_existing_posts();
// Start the background collection process to build initial stats.
$this->start_stats_collection();
}
/**
* Get stats collection status.
*
* @since 2.19.13
* @return array Status information about stats collection.
*/
public function get_collection_status() {
$status = get_option( 'uagb_block_usage_status', array() );
$analytics_data = get_option( 'uagb_block_usage_data', array() );
if ( ! is_array( $status ) ) {
$status = array();
}
if ( ! is_array( $analytics_data ) ) {
$analytics_data = array();
}
return array(
'is_processing' => ! empty( $status['is_processing'] ),
'is_complete' => ! empty( $status['collection_complete'] ),
'last_collected' => isset( $status['last_collected'] ) ? $status['last_collected'] : false,
'last_updated' => isset( $analytics_data['last_updated'] ) ? $analytics_data['last_updated'] : false,
'analytics_enabled' => get_option( 'spectra_usage_optin', 'no' ) === 'yes',
'first_run_done' => ! empty( $status['first_run_check'] ),
'has_stats' => ! empty( $analytics_data['block_usage_stats'] ),
'tracking_method' => 'incremental', // Now using incremental tracking instead of batch processing.
'total_tracked_blocks' => ! empty( $analytics_data['block_usage_stats'] ) && is_array( $analytics_data['block_usage_stats'] ) ? count( array_filter( $analytics_data['block_usage_stats'] ) ) : 0,
);
}
/**
* Get site activity level based on Spectra block edits in the last 180 days.
*
* Calculates KPIs for:
* - Active Site: Spectra blocks manually added/edited on at least 1 page in last 180 days
* - Super Site: Spectra blocks manually added/edited on at least 15 pages in last 180 days
*
* @since 2.19.19
* @return array Site activity data with classification.
*/
public function get_site_activity_level() {
$days_threshold = 180;
$cutoff_time = time() - ( $days_threshold * DAY_IN_SECONDS );
$post_types = get_post_types( array( 'public' => true ), 'names' );
// Query posts where Spectra blocks have been edited in the last 180 days.
$posts = get_posts(
array(
'post_type' => $post_types,
'post_status' => array( 'publish', 'private', 'draft' ),
'posts_per_page' => -1,
'fields' => 'ids',
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Required for site activity KPI calculation.
array(
'key' => '_uagb_last_spectra_edit',
'value' => $cutoff_time,
'compare' => '>=',
'type' => 'NUMERIC',
),
),
)
);
$active_pages_count = count( $posts );
// Determine site classification.
$site_type = 'inactive';
if ( $active_pages_count >= 15 ) {
$site_type = 'super_site';
} elseif ( $active_pages_count >= 1 ) {
$site_type = 'active_site';
}
return array(
'active_pages_180d' => $active_pages_count,
'site_type' => $site_type,
'is_active_site' => $active_pages_count >= 1,
'is_super_site' => $active_pages_count >= 15,
);
}
}
}

View File

@@ -0,0 +1,357 @@
<?php
/**
* UAGB Block Stats Background Processor.
*
* Class to execute background processing for block usage analytics.
*
* @since 2.19.13
* @package UAGB
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WP_Async_Request' ) ) {
require_once UAGB_DIR . 'lib/batch-processing/class-wp-async-request.php';
}
if ( ! class_exists( 'WP_Background_Process' ) ) {
require_once UAGB_DIR . 'lib/batch-processing/class-wp-background-process.php';
}
if ( ! class_exists( 'UAGB_Block_Stats_Processor' ) ) {
/**
* Class UAGB_Block_Stats_Processor
*
* Handles background processing for block usage statistics collection.
*
* @since 2.19.13
* @package UAGB
*/
class UAGB_Block_Stats_Processor extends WP_Background_Process {
/**
* Action name.
*
* @var string
* @since 2.19.13
*/
protected $action = 'uagb_block_stats_collection';
/**
* List of all Spectra blocks to track (Core + Pro).
*
* @var array
* @since 2.19.13
*/
private $spectra_blocks = array(
// Spectra Core Blocks.
'uagb/advanced-heading',
'uagb/blockquote',
'uagb/buttons',
'uagb/buttons-child',
'uagb/call-to-action',
'uagb/cf7-styler',
'uagb/column',
'uagb/columns',
'uagb/container',
'uagb/content-timeline',
'uagb/content-timeline-child',
'uagb/countdown',
'uagb/counter',
'uagb/faq',
'uagb/faq-child',
'uagb/forms',
'uagb/forms-accept',
'uagb/forms-checkbox',
'uagb/forms-date',
'uagb/forms-email',
'uagb/forms-hidden',
'uagb/forms-name',
'uagb/forms-phone',
'uagb/forms-radio',
'uagb/forms-select',
'uagb/forms-textarea',
'uagb/forms-toggle',
'uagb/forms-url',
'uagb/gf-styler',
'uagb/google-map',
'uagb/how-to',
'uagb/how-to-step',
'uagb/icon',
'uagb/icon-list',
'uagb/icon-list-child',
'uagb/image',
'uagb/image-gallery',
'uagb/info-box',
'uagb/inline-notice',
'uagb/lottie',
'uagb/marketing-button',
'uagb/modal',
'uagb/popup-builder',
'uagb/post-button',
'uagb/post-carousel',
'uagb/post-excerpt',
'uagb/post-grid',
'uagb/post-image',
'uagb/post-masonry',
'uagb/post-meta',
'uagb/post-taxonomy',
'uagb/post-timeline',
'uagb/post-title',
'uagb/restaurant-menu',
'uagb/restaurant-menu-child',
'uagb/review',
'uagb/section',
'uagb/separator',
'uagb/slider',
'uagb/slider-child',
'uagb/social-share',
'uagb/social-share-child',
'uagb/star-rating',
'uagb/sure-cart-checkout',
'uagb/sure-cart-product',
'uagb/sure-forms',
'uagb/table-of-contents',
'uagb/tabs',
'uagb/tabs-child',
'uagb/taxonomy-list',
'uagb/team',
'uagb/testimonial',
'uagb/wp-search',
// Spectra Pro Blocks.
'uagb/instagram-feed',
'uagb/login',
'uagb/loop-builder',
'uagb/loop-category',
'uagb/loop-pagination',
'uagb/loop-reset',
'uagb/loop-search',
'uagb/loop-sort',
'uagb/loop-wrapper',
'uagb/register',
'uagb/register-email',
'uagb/register-first-name',
'uagb/register-last-name',
'uagb/register-password',
'uagb/register-reenter-password',
'uagb/register-terms',
'uagb/register-username',
);
/**
* Task to be performed for each post.
*
* @param int $post_id Post ID to be processed.
* @since 2.19.13
* @return bool False when the task is complete.
*/
protected function task( $post_id ) {
$post = get_post( $post_id );
if ( ! is_object( $post ) || ! is_a( $post, 'WP_Post' ) ) {
return false;
}
// Check if post has Gutenberg blocks.
if ( ! has_blocks( $post->post_content ) ) {
return false;
}
// Count blocks in this post.
$block_counts = $this->count_blocks_in_post( $post->post_content );
// Get existing analytics data.
$analytics_data = get_option( 'uagb_block_usage_data', array() );
if ( ! is_array( $analytics_data ) ) {
$analytics_data = array();
}
if ( ! isset( $analytics_data['block_usage_stats'] ) ) {
$analytics_data['block_usage_stats'] = array();
}
// Merge with existing stats.
foreach ( $block_counts as $block_name => $count ) {
if ( ! isset( $analytics_data['block_usage_stats'][ $block_name ] ) ) {
$analytics_data['block_usage_stats'][ $block_name ] = 0;
}
$analytics_data['block_usage_stats'][ $block_name ] += $count;
}
// Update the consolidated analytics data.
update_option( 'uagb_block_usage_data', $analytics_data );
return false;
}
/**
* Count blocks recursively in post content.
*
* @param string $content Post content.
* @since 2.19.13
* @return array Array of block counts.
*/
private function count_blocks_in_post( $content ) {
$block_counts = array();
// Initialize all Spectra blocks with 0 count.
foreach ( $this->spectra_blocks as $block_name ) {
$block_counts[ $block_name ] = 0;
}
// Parse blocks.
$blocks = parse_blocks( $content );
// Count blocks recursively.
$this->count_blocks_recursive( $blocks, $block_counts );
return $block_counts;
}
/**
* Recursively count blocks including nested blocks.
*
* @param array $blocks Array of blocks.
* @param array $block_counts Reference to block counts array.
* @since 2.19.13
* @return void
*/
private function count_blocks_recursive( $blocks, &$block_counts ) {
foreach ( $blocks as $block ) {
$block_name = $block['blockName'];
// Count this block if it's a Spectra block.
if ( ! empty( $block_name ) && in_array( $block_name, $this->spectra_blocks, true ) ) {
$block_counts[ $block_name ]++;
}
// Recursively count inner blocks.
if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
$this->count_blocks_recursive( $block['innerBlocks'], $block_counts );
}
}
}
/**
* Complete the block stats collection process.
*
* @since 2.19.13
* @return void
*/
protected function complete() {
parent::complete();
// Update analytics status with completion data.
$status = get_option( 'uagb_block_usage_status', array() );
if ( ! is_array( $status ) ) {
$status = array();
}
$status['collection_complete'] = true;
$status['last_collected'] = time();
$status['is_processing'] = false;
update_option( 'uagb_block_usage_status', $status );
}
/**
* Start the block stats collection process.
*
* @since 2.19.13
* @return void
*/
public function start_collection() {
// Check if already processing.
$status = get_option( 'uagb_block_usage_status', array() );
if ( ! is_array( $status ) ) {
$status = array();
}
if ( ! empty( $status['is_processing'] ) ) {
return;
}
// Set processing flag and reset completion status.
$status['is_processing'] = true;
$status['collection_complete'] = false;
update_option( 'uagb_block_usage_status', $status );
// Reset analytics data.
update_option( 'uagb_block_usage_data', array() );
// Get all posts with blocks.
$post_types = get_post_types( array( 'public' => true ), 'names' );
$posts = get_posts(
array(
'post_type' => $post_types,
'post_status' => array( 'publish', 'private', 'draft' ),
'posts_per_page' => -1,
'fields' => 'ids',
)
);
// Add posts to queue.
foreach ( $posts as $post_id ) {
$this->push_to_queue( $post_id );
}
// Save queue and dispatch.
$this->save()->dispatch();
}
/**
* Get collected block usage statistics.
*
* @since 2.19.13
* @return array Block usage statistics.
*/
public static function get_block_stats() {
$analytics_data = get_option( 'uagb_block_usage_data', array() );
if ( ! is_array( $analytics_data ) ) {
$analytics_data = array();
}
return isset( $analytics_data['block_usage_stats'] ) ? $analytics_data['block_usage_stats'] : array();
}
/**
* Check if stats collection is complete.
*
* @since 2.19.13
* @return bool Whether stats collection is complete.
*/
public static function is_collection_complete() {
$status = get_option( 'uagb_block_usage_status', array() );
if ( ! is_array( $status ) ) {
$status = array();
}
return ! empty( $status['collection_complete'] );
}
/**
* Get the last collection timestamp.
*
* @since 2.19.13
* @return int|false Last collection timestamp or false if never collected.
*/
public static function get_last_collection_time() {
$status = get_option( 'uagb_block_usage_status', array() );
if ( ! is_array( $status ) ) {
$status = array();
}
return isset( $status['last_collected'] ) ? $status['last_collected'] : false;
}
}
}

View File

@@ -0,0 +1,558 @@
<?php
/**
* UAGB Incremental Block Tracker.
*
* Class to track block usage changes in real-time when posts are saved.
*
* @since 2.19.13
* @package UAGB
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'UAGB_Incremental_Block_Tracker' ) ) {
/**
* Class UAGB_Incremental_Block_Tracker
*
* Handles real-time block usage tracking when posts are saved.
*
* @since 2.19.13
* @package UAGB
*/
class UAGB_Incremental_Block_Tracker {
/**
* Member Variable
*
* @var UAGB_Incremental_Block_Tracker|null
* @since 2.19.13
*/
private static $instance;
/**
* List of all Spectra blocks to track (Core + Pro).
*
* @var array
* @since 2.19.13
*/
private $spectra_blocks = array(
// Spectra Core Blocks.
'uagb/advanced-heading',
'uagb/blockquote',
'uagb/buttons',
'uagb/buttons-child',
'uagb/call-to-action',
'uagb/cf7-styler',
'uagb/column',
'uagb/columns',
'uagb/container',
'uagb/content-timeline',
'uagb/content-timeline-child',
'uagb/countdown',
'uagb/counter',
'uagb/faq',
'uagb/faq-child',
'uagb/forms',
'uagb/forms-accept',
'uagb/forms-checkbox',
'uagb/forms-date',
'uagb/forms-email',
'uagb/forms-hidden',
'uagb/forms-name',
'uagb/forms-phone',
'uagb/forms-radio',
'uagb/forms-select',
'uagb/forms-textarea',
'uagb/forms-toggle',
'uagb/forms-url',
'uagb/gf-styler',
'uagb/google-map',
'uagb/how-to',
'uagb/how-to-step',
'uagb/icon',
'uagb/icon-list',
'uagb/icon-list-child',
'uagb/image',
'uagb/image-gallery',
'uagb/info-box',
'uagb/inline-notice',
'uagb/lottie',
'uagb/marketing-button',
'uagb/modal',
'uagb/popup-builder',
'uagb/post-button',
'uagb/post-carousel',
'uagb/post-excerpt',
'uagb/post-grid',
'uagb/post-image',
'uagb/post-masonry',
'uagb/post-meta',
'uagb/post-taxonomy',
'uagb/post-timeline',
'uagb/post-title',
'uagb/restaurant-menu',
'uagb/restaurant-menu-child',
'uagb/review',
'uagb/section',
'uagb/separator',
'uagb/slider',
'uagb/slider-child',
'uagb/social-share',
'uagb/social-share-child',
'uagb/star-rating',
'uagb/sure-cart-checkout',
'uagb/sure-cart-product',
'uagb/sure-forms',
'uagb/table-of-contents',
'uagb/tabs',
'uagb/tabs-child',
'uagb/taxonomy-list',
'uagb/team',
'uagb/testimonial',
'uagb/wp-search',
// Spectra Pro Blocks.
'uagb/instagram-feed',
'uagb/login',
'uagb/loop-builder',
'uagb/loop-category',
'uagb/loop-pagination',
'uagb/loop-reset',
'uagb/loop-search',
'uagb/loop-sort',
'uagb/loop-wrapper',
'uagb/register',
'uagb/register-email',
'uagb/register-first-name',
'uagb/register-last-name',
'uagb/register-password',
'uagb/register-reenter-password',
'uagb/register-terms',
'uagb/register-username',
);
/**
* Initiator
*
* @since 2.19.13
* @return UAGB_Incremental_Block_Tracker
*/
public static function get_instance() {
if ( ! isset( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*
* @since 2.19.13
* @return void
*/
public function __construct() {
// Hook into post save actions.
add_action( 'save_post', array( $this, 'track_block_changes_on_save' ), 10, 2 );
add_action( 'before_delete_post', array( $this, 'track_block_removal_on_delete' ) );
add_action( 'wp_trash_post', array( $this, 'track_block_removal_on_trash' ) );
add_action( 'untrash_post', array( $this, 'track_block_addition_on_untrash' ) );
}
/**
* Track block changes when a post is saved.
*
* @param int $post_id Post ID.
* @param WP_Post $post Post object.
* @since 2.19.13
* @return void
*/
public function track_block_changes_on_save( $post_id, $post ) {
// Skip if analytics is not enabled.
if ( get_option( 'spectra_usage_optin', 'no' ) !== 'yes' ) {
return;
}
// Skip autosaves and revisions.
if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
return;
}
// Only track public post types.
$public_post_types = get_post_types( array( 'public' => true ), 'names' );
if ( ! in_array( $post->post_type, $public_post_types, true ) ) {
return;
}
// Skip if content hasn't changed (performance optimization).
static $last_processed_content = array();
$content_hash = md5( $post->post_content );
if ( isset( $last_processed_content[ $post_id ] ) && $last_processed_content[ $post_id ] === $content_hash ) {
return;
}
$last_processed_content[ $post_id ] = $content_hash;
// Get the previous block counts for this post (what was in this post before saving).
$previous_blocks = get_post_meta( $post_id, '_uagb_previous_block_counts', true );
$previous_blocks = is_array( $previous_blocks ) ? $previous_blocks : array();
// Count current blocks in the post (what's in this post after saving).
$current_blocks = $this->count_blocks_in_post( $post->post_content );
// Check if Spectra blocks have changed (for site activity tracking).
$has_spectra_blocks_changed = $this->has_blocks_changed( $previous_blocks, $current_blocks );
// Update global stats with the correct logic:
// 1. Subtract the old blocks from global count (remove what this post had before)
// 2. Add the new blocks to global count (add what this post has now).
$this->update_global_stats_correctly( $previous_blocks, $current_blocks );
// Store current block counts for next comparison.
update_post_meta( $post_id, '_uagb_previous_block_counts', $current_blocks );
// Update the edit timestamp for Active Site / Super Site KPIs.
// Only set timestamp if the post currently has Spectra blocks.
// Delete the meta if all Spectra blocks have been removed.
if ( $has_spectra_blocks_changed ) {
if ( $this->has_spectra_blocks( $current_blocks ) ) {
// Post still has Spectra blocks, update the timestamp.
update_post_meta( $post_id, '_uagb_last_spectra_edit', time() );
} else {
// All Spectra blocks were removed, delete the timestamp.
delete_post_meta( $post_id, '_uagb_last_spectra_edit' );
}
}
}
/**
* Track block removal when a post is deleted.
*
* @param int $post_id Post ID being deleted.
* @since 2.19.13
* @return void
*/
public function track_block_removal_on_delete( $post_id ) {
// Skip if analytics is not enabled.
if ( get_option( 'spectra_usage_optin', 'no' ) !== 'yes' ) {
return;
}
$post = get_post( $post_id );
if ( ! $post ) {
return;
}
// Only track public post types.
$public_post_types = get_post_types( array( 'public' => true ), 'names' );
if ( ! is_object( $post ) || ! in_array( $post->post_type, $public_post_types, true ) ) {
return;
}
// Get the previous block counts for this post.
$previous_blocks = get_post_meta( $post_id, '_uagb_previous_block_counts', true );
if ( ! is_array( $previous_blocks ) || empty( $previous_blocks ) ) {
return;
}
// Create a negative diff to remove these blocks from stats.
$block_diff = array();
foreach ( $previous_blocks as $block_name => $count ) {
if ( $count > 0 ) {
$block_diff[ $block_name ] = -$count;
}
}
// Update global stats.
if ( ! empty( $block_diff ) ) {
$this->update_global_stats( $block_diff );
}
}
/**
* Track block removal when a post is trashed.
*
* @param int $post_id Post ID being trashed.
* @since 2.19.13
* @return void
*/
public function track_block_removal_on_trash( $post_id ) {
$this->track_block_removal_on_delete( $post_id );
}
/**
* Track block addition when a post is untrashed.
*
* @param int $post_id Post ID being untrashed.
* @since 2.19.13
* @return void
*/
public function track_block_addition_on_untrash( $post_id ) {
// Skip if analytics is not enabled.
if ( get_option( 'spectra_usage_optin', 'no' ) !== 'yes' ) {
return;
}
$post = get_post( $post_id );
if ( ! $post ) {
return;
}
// Only track public post types.
$public_post_types = get_post_types( array( 'public' => true ), 'names' );
if ( ! is_object( $post ) || ! in_array( $post->post_type, $public_post_types, true ) ) {
return;
}
// Count current blocks and add them back to stats.
$current_blocks = $this->count_blocks_in_post( $post->post_content );
if ( ! empty( $current_blocks ) ) {
$this->update_global_stats( $current_blocks );
}
// Store current block counts for future comparisons.
update_post_meta( $post_id, '_uagb_previous_block_counts', $current_blocks );
}
/**
* Count blocks recursively in post content.
*
* @param string $content Post content.
* @since 2.19.13
* @return array Array of block counts.
*/
private function count_blocks_in_post( $content ) {
$block_counts = array();
// Initialize all Spectra blocks with 0 count.
foreach ( $this->spectra_blocks as $block_name ) {
$block_counts[ $block_name ] = 0;
}
// Skip if content is empty or has no blocks.
if ( empty( $content ) || ! has_blocks( $content ) ) {
return $block_counts;
}
// Parse blocks.
$blocks = parse_blocks( $content );
// Count blocks recursively.
$this->count_blocks_recursive( $blocks, $block_counts );
return $block_counts;
}
/**
* Recursively count blocks including nested blocks.
*
* @param array $blocks Array of blocks.
* @param array $block_counts Reference to block counts array.
* @since 2.19.13
* @return void
*/
private function count_blocks_recursive( $blocks, &$block_counts ) {
foreach ( $blocks as $block ) {
$block_name = $block['blockName'];
// Count this block if it's a Spectra block.
if ( ! empty( $block_name ) && in_array( $block_name, $this->spectra_blocks, true ) ) {
$block_counts[ $block_name ]++;
}
// Recursively count inner blocks.
if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
$this->count_blocks_recursive( $block['innerBlocks'], $block_counts );
}
}
}
/**
* Check if Spectra blocks have changed between previous and current counts.
*
* @param array $previous_blocks Block counts before saving.
* @param array $current_blocks Block counts after saving.
* @since 2.19.19
* @return bool True if blocks have changed, false otherwise.
*/
private function has_blocks_changed( $previous_blocks, $current_blocks ) {
foreach ( $this->spectra_blocks as $block_name ) {
$previous_count = isset( $previous_blocks[ $block_name ] ) ? $previous_blocks[ $block_name ] : 0;
$current_count = isset( $current_blocks[ $block_name ] ) ? $current_blocks[ $block_name ] : 0;
if ( $previous_count !== $current_count ) {
return true;
}
}
return false;
}
/**
* Update global analytics stats with the correct incremental logic.
*
* @param array $previous_blocks Block counts that were in the post before saving.
* @param array $current_blocks Block counts that are in the post after saving.
* @since 2.19.13
* @return void
*/
private function update_global_stats_correctly( $previous_blocks, $current_blocks ) {
// Get existing analytics data.
$analytics_data = get_option( 'uagb_block_usage_data', array() );
if ( ! is_array( $analytics_data ) ) {
$analytics_data = array();
}
if ( ! isset( $analytics_data['block_usage_stats'] ) ) {
$analytics_data['block_usage_stats'] = array();
}
// Process each Spectra block type.
foreach ( $this->spectra_blocks as $block_name ) {
// Initialize if not set.
if ( ! isset( $analytics_data['block_usage_stats'][ $block_name ] ) ) {
$analytics_data['block_usage_stats'][ $block_name ] = 0;
}
$previous_count = isset( $previous_blocks[ $block_name ] ) ? $previous_blocks[ $block_name ] : 0;
$current_count = isset( $current_blocks[ $block_name ] ) ? $current_blocks[ $block_name ] : 0;
// Only update if there's a change.
if ( $previous_count !== $current_count ) {
// Step 1: Subtract what this post had before (remove old contribution).
$analytics_data['block_usage_stats'][ $block_name ] -= $previous_count;
// Step 2: Add what this post has now (add new contribution).
$analytics_data['block_usage_stats'][ $block_name ] += $current_count;
// Ensure we don't go below 0 (safety check).
if ( $analytics_data['block_usage_stats'][ $block_name ] < 0 ) {
$analytics_data['block_usage_stats'][ $block_name ] = 0;
}
}
}
// Update last modified timestamp.
$analytics_data['last_updated'] = time();
// Save the updated analytics data.
update_option( 'uagb_block_usage_data', $analytics_data );
}
/**
* Update global analytics stats with block count changes (legacy method for delete/trash operations).
*
* @param array $block_diff Array of block count changes.
* @since 2.19.13
* @return void
*/
private function update_global_stats( $block_diff ) {
// Get existing analytics data.
$analytics_data = get_option( 'uagb_block_usage_data', array() );
if ( ! is_array( $analytics_data ) ) {
$analytics_data = array();
}
if ( ! isset( $analytics_data['block_usage_stats'] ) ) {
$analytics_data['block_usage_stats'] = array();
}
// Apply the block count changes.
foreach ( $block_diff as $block_name => $diff ) {
if ( ! isset( $analytics_data['block_usage_stats'][ $block_name ] ) ) {
$analytics_data['block_usage_stats'][ $block_name ] = 0;
}
$analytics_data['block_usage_stats'][ $block_name ] += $diff;
// Ensure we don't go below 0.
$current_count = $analytics_data['block_usage_stats'][ $block_name ];
if ( is_numeric( $current_count ) && $current_count < 0 ) {
$analytics_data['block_usage_stats'][ $block_name ] = 0;
}
}
// Update last modified timestamp.
$analytics_data['last_updated'] = time();
// Save the updated analytics data.
update_option( 'uagb_block_usage_data', $analytics_data );
}
/**
* Initialize tracking for existing posts (one-time setup).
* This method populates the _uagb_previous_block_counts meta for existing posts.
* Also sets _uagb_last_spectra_edit timestamp for posts that have Spectra blocks.
*
* @since 2.19.13
* @return void
*/
public function initialize_existing_posts() {
// Get all posts that don't have block counts stored yet.
$post_types = get_post_types( array( 'public' => true ), 'names' );
$posts = get_posts(
array(
'post_type' => $post_types,
'post_status' => array( 'publish', 'private', 'draft' ),
'posts_per_page' => -1,
'fields' => 'ids',
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Intentional one-time setup query.
array(
'key' => '_uagb_previous_block_counts',
'compare' => 'NOT EXISTS',
),
),
)
);
$current_time = time();
foreach ( $posts as $post_id ) {
$post = get_post( $post_id );
if ( is_object( $post ) && has_blocks( $post->post_content ) ) {
$block_counts = $this->count_blocks_in_post( $post->post_content );
$actual_post_id = is_object( $post_id ) ? $post_id->ID : (int) $post_id;
update_post_meta( $actual_post_id, '_uagb_previous_block_counts', $block_counts );
// Set the edit timestamp if the post has any Spectra blocks.
// This ensures existing posts are counted in Active Site / Super Site KPIs.
if ( $this->has_spectra_blocks( $block_counts ) ) {
update_post_meta( $actual_post_id, '_uagb_last_spectra_edit', $current_time );
}
}
}
}
/**
* Check if block counts contain any Spectra blocks.
*
* @param array $block_counts Array of block counts.
* @since 2.19.19
* @return bool True if any Spectra blocks are present, false otherwise.
*/
private function has_spectra_blocks( $block_counts ) {
foreach ( $block_counts as $block_name => $count ) {
if ( $count > 0 && in_array( $block_name, $this->spectra_blocks, true ) ) {
return true;
}
}
return false;
}
/**
* Get block counts for a specific post.
*
* @param int $post_id Post ID.
* @since 2.19.13
* @return array Block counts for the post.
*/
public function get_post_block_counts( $post_id ) {
$block_counts = get_post_meta( $post_id, '_uagb_previous_block_counts', true );
return is_array( $block_counts ) ? $block_counts : array();
}
}
}