Changed source root directory

This commit is contained in:
2026-03-05 16:30:11 +01:00
parent dc85447ee1
commit 538f85d7a2
5868 changed files with 749734 additions and 99 deletions

View File

@@ -0,0 +1,146 @@
<?php
/**
* Metrics bootstrap class.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics;
use Cmatic\Metrics\Core\Storage;
use Cmatic\Metrics\Core\Tracker;
use Cmatic\Metrics\Core\Scheduler;
use Cmatic\Metrics\Core\Sync;
use Cmatic\Metrics\Core\Collector;
use Cmatic_Options_Repository;
defined( 'ABSPATH' ) || exit;
class Bootstrap {
private static $instance = null;
private $config = array();
public static function init( $config = array() ) {
if ( null === self::$instance ) {
self::$instance = new self( $config );
}
return self::$instance;
}
private function __construct( $config ) {
try {
$this->config = wp_parse_args(
$config,
array(
'plugin_basename' => '',
'endpoint_url' => '',
)
);
Sync::set_endpoint( $this->config['endpoint_url'] );
$this->init_components();
} catch ( \Exception $e ) {
return;
}
}
private function init_components() {
try {
Storage::init();
Tracker::init();
Scheduler::init();
add_action( 'admin_init', array( $this, 'admin_init_failsafe' ), 999 );
add_action( 'cmatic_weekly_telemetry', array( $this, 'execute_weekly_telemetry' ) );
$this->ensure_weekly_schedule();
} catch ( \Exception $e ) {
return;
}
}
public function admin_init_failsafe() {
try {
$transient_key = 'cmatic_admin_checked';
if ( get_transient( $transient_key ) ) {
return;
}
set_transient( $transient_key, 1, HOUR_IN_SECONDS );
if ( ! class_exists( 'Cmatic_Options_Repository' ) ) {
return;
}
Storage::init();
if ( ! Storage::is_enabled() ) {
return;
}
global $pagenow;
if ( isset( $pagenow ) && in_array( $pagenow, array( 'plugins.php', 'plugin-install.php', 'plugin-editor.php' ), true ) ) {
return;
}
$last_heartbeat = Storage::get_last_heartbeat();
$two_weeks = 2 * WEEK_IN_SECONDS;
if ( 0 === $last_heartbeat || ( time() - $last_heartbeat ) > $two_weeks ) {
$payload = Collector::collect( 'heartbeat' );
Sync::send_async( $payload );
}
} catch ( \Exception $e ) {
return;
}
}
private function ensure_weekly_schedule() {
try {
add_filter( 'cron_schedules', array( $this, 'add_weekly_schedule' ) );
if ( ! wp_next_scheduled( 'cmatic_weekly_telemetry' ) ) {
wp_schedule_event( time() + WEEK_IN_SECONDS, 'cmatic_weekly', 'cmatic_weekly_telemetry' );
}
} catch ( \Exception $e ) {
return;
}
}
public function add_weekly_schedule( $schedules ) {
if ( ! isset( $schedules['cmatic_weekly'] ) ) {
$schedules['cmatic_weekly'] = array(
'interval' => WEEK_IN_SECONDS,
'display' => 'Once Weekly',
);
}
return $schedules;
}
public function execute_weekly_telemetry() {
try {
if ( ! Storage::is_enabled() ) {
return;
}
$last_run = Cmatic_Options_Repository::get_option( 'telemetry.last_run' ) ?: 0;
$current_time = time();
if ( $current_time - $last_run < ( 6 * DAY_IN_SECONDS ) ) {
return;
}
Cmatic_Options_Repository::set_option( 'telemetry.last_run', $current_time );
$payload = Collector::collect( 'heartbeat' );
Sync::send( $payload );
} catch ( \Exception $e ) {
return;
}
}
public static function get_instance() {
return self::$instance;
}
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* Metrics data collector orchestrator.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core;
use Cmatic\Metrics\Core\Collectors\Install_Collector;
use Cmatic\Metrics\Core\Collectors\Metadata_Collector;
use Cmatic\Metrics\Core\Collectors\Lifecycle_Collector;
use Cmatic\Metrics\Core\Collectors\Environment_Collector;
use Cmatic\Metrics\Core\Collectors\Api_Collector;
use Cmatic\Metrics\Core\Collectors\Submissions_Collector;
use Cmatic\Metrics\Core\Collectors\Features_Collector;
use Cmatic\Metrics\Core\Collectors\Forms_Collector;
use Cmatic\Metrics\Core\Collectors\Performance_Collector;
use Cmatic\Metrics\Core\Collectors\Plugins_Collector;
use Cmatic\Metrics\Core\Collectors\Competitors_Collector;
use Cmatic\Metrics\Core\Collectors\Server_Collector;
use Cmatic\Metrics\Core\Collectors\WordPress_Collector;
defined( 'ABSPATH' ) || exit;
class Collector {
public static function collect( $event = 'heartbeat' ): array {
return array(
'install_id' => Storage::get_install_id(),
'timestamp' => time(),
'event' => $event,
'install' => Install_Collector::collect(),
'metadata' => Metadata_Collector::collect(),
'lifecycle' => Lifecycle_Collector::collect(),
'environment' => Environment_Collector::collect(),
'api' => Api_Collector::collect(),
'submissions' => Submissions_Collector::collect(),
'features' => Features_Collector::collect(),
'forms' => Forms_Collector::collect(),
'performance' => Performance_Collector::collect(),
'plugins' => Plugins_Collector::collect(),
'competitors' => Competitors_Collector::collect(),
'server' => Server_Collector::collect(),
'wordpress' => WordPress_Collector::collect(),
);
}
}

View File

@@ -0,0 +1,104 @@
<?php
/**
* API data collector.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core\Collectors;
use Cmatic_Options_Repository;
defined( 'ABSPATH' ) || exit;
class Api_Collector {
public static function collect(): array {
$has_api_key = false;
$api_data_center = '';
$api_key_length = 0;
$forms_with_api = 0;
$cf7_forms = Forms_Collector::get_cf7_forms();
$form_ids = array_map( fn( $form ) => $form->id(), $cf7_forms );
$form_options = Forms_Collector::batch_load_form_options( $form_ids );
foreach ( $cf7_forms as $form ) {
$form_id = $form->id();
$cf7_mch = $form_options[ $form_id ]['settings'] ?? array();
if ( ! empty( $cf7_mch['api'] ) ) {
$has_api_key = true;
++$forms_with_api;
if ( empty( $api_data_center ) && preg_match( '/-([a-z0-9]+)$/i', $cf7_mch['api'], $matches ) ) {
$api_data_center = $matches[1];
}
if ( 0 === $api_key_length ) {
$api_key_length = strlen( $cf7_mch['api'] );
}
}
}
$total_sent = (int) Cmatic_Options_Repository::get_option( 'stats.sent', 0 );
$total_attempts = (int) Cmatic_Options_Repository::get_option( 'api.total_attempts', $total_sent );
$total_successes = (int) Cmatic_Options_Repository::get_option( 'api.total_successes', $total_sent );
$total_failures = (int) Cmatic_Options_Repository::get_option( 'api.total_failures', 0 );
$success_rate = $total_attempts > 0 ? round( ( $total_successes / $total_attempts ) * 100, 2 ) : 0;
$first_connected = (int) Cmatic_Options_Repository::get_option( 'api.first_connected', 0 );
$last_success = (int) Cmatic_Options_Repository::get_option( 'api.last_success', 0 );
$last_failure = (int) Cmatic_Options_Repository::get_option( 'api.last_failure', 0 );
$avg_response_time = (int) Cmatic_Options_Repository::get_option( 'api.avg_response_time', 0 );
$error_summary = self::get_error_summary();
$consecutive_failures = (int) Cmatic_Options_Repository::get_option( 'api.consecutive_failures', 0 );
$uptime_percentage = $total_attempts > 0 ? round( ( $total_successes / $total_attempts ) * 100, 2 ) : 100;
$days_since_last_success = $last_success > 0 ? floor( ( time() - $last_success ) / DAY_IN_SECONDS ) : 0;
$days_since_last_failure = $last_failure > 0 ? floor( ( time() - $last_failure ) / DAY_IN_SECONDS ) : 0;
$data = array(
'is_connected' => $has_api_key,
'forms_with_api' => $forms_with_api,
'api_data_center' => $api_data_center,
'api_key_length' => $api_key_length,
'first_connected' => $first_connected,
'total_attempts' => $total_attempts,
'total_successes' => $total_successes,
'total_failures' => $total_failures,
'success_rate' => $success_rate,
'uptime_percentage' => $uptime_percentage,
'last_success' => $last_success,
'last_failure' => $last_failure,
'days_since_last_success' => $days_since_last_success,
'days_since_last_failure' => $days_since_last_failure,
'avg_response_time_ms' => $avg_response_time,
'error_codes' => $error_summary,
'api_health_score' => min( 100, max( 0, $uptime_percentage - ( $consecutive_failures * 5 ) ) ),
'setup_sync_attempted' => Cmatic_Options_Repository::get_option( 'api.sync_attempted', false ),
'setup_sync_attempts_count' => (int) Cmatic_Options_Repository::get_option( 'api.sync_attempts_count', 0 ),
'setup_first_success' => Cmatic_Options_Repository::get_option( 'api.setup_first_success', false ),
'setup_first_failure' => Cmatic_Options_Repository::get_option( 'api.setup_first_failure', false ),
'setup_failure_count' => (int) Cmatic_Options_Repository::get_option( 'api.setup_failure_count', 0 ),
'setup_audience_selected' => Cmatic_Options_Repository::get_option( 'api.audience_selected', false ),
);
return array_filter( $data, fn( $v ) => $v !== 0 && $v !== false && $v !== '' && $v !== array() );
}
private static function get_error_summary(): array {
$error_codes = Cmatic_Options_Repository::get_option( 'api.error_codes', array() );
$error_summary = array();
foreach ( $error_codes as $code => $count ) {
if ( $count > 0 ) {
$error_summary[ $code ] = (int) $count;
}
}
return $error_summary;
}
}

View File

@@ -0,0 +1,159 @@
<?php
/**
* Competitors data collector.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core\Collectors;
defined( 'ABSPATH' ) || exit;
class Competitors_Collector {
private const COMPETITORS = array(
'mc4wp' => array(
'slug' => 'mailchimp-for-wp/mailchimp-for-wp.php',
'name' => 'MC4WP: Mailchimp for WordPress',
),
'mc4wp_premium' => array(
'slug' => 'mc4wp-premium/mc4wp-premium.php',
'name' => 'MC4WP Premium',
),
'mailchimp_woo' => array(
'slug' => 'mailchimp-for-woocommerce/mailchimp-woocommerce.php',
'name' => 'Mailchimp for WooCommerce',
),
'crm_perks' => array(
'slug' => 'cf7-mailchimp/cf7-mailchimp.php',
'name' => 'CRM Perks CF7 Mailchimp',
),
'easy_forms' => array(
'slug' => 'jetwp-easy-mailchimp/jetwp-easy-mailchimp.php',
'name' => 'Easy Forms for Mailchimp',
),
'jetrail' => array(
'slug' => 'jetrail-cf7-mailchimp/jetrail-cf7-mailchimp.php',
'name' => 'Jetrail CF7 Mailchimp',
),
'cf7_mailchimp_ext' => array(
'slug' => 'contact-form-7-mailchimp-extension-jetrail/cf7-mailchimp-ext.php',
'name' => 'CF7 Mailchimp Extension Jetrail',
),
'newsletter' => array(
'slug' => 'newsletter/plugin.php',
'name' => 'Newsletter',
),
'mailpoet' => array(
'slug' => 'mailpoet/mailpoet.php',
'name' => 'MailPoet',
),
'fluent_forms' => array(
'slug' => 'fluentform/fluentform.php',
'name' => 'Fluent Forms',
),
'wpforms' => array(
'slug' => 'wpforms-lite/wpforms.php',
'name' => 'WPForms',
),
'gravity_forms' => array(
'slug' => 'gravityforms/gravityforms.php',
'name' => 'Gravity Forms',
),
'ninja_forms' => array(
'slug' => 'ninja-forms/ninja-forms.php',
'name' => 'Ninja Forms',
),
'formidable' => array(
'slug' => 'formidable/formidable.php',
'name' => 'Formidable Forms',
),
'hubspot' => array(
'slug' => 'leadin/leadin.php',
'name' => 'HubSpot',
),
'elementor_pro' => array(
'slug' => 'elementor-pro/elementor-pro.php',
'name' => 'Elementor Pro',
),
);
public static function collect(): array {
if ( ! function_exists( 'is_plugin_active' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$all_plugins = get_plugins();
$competitors = self::check_competitors( $all_plugins );
$summary = self::build_summary( $competitors );
$individual = self::build_individual_status( $competitors );
return array_merge( $summary, $individual );
}
private static function check_competitors( array $all_plugins ): array {
$competitors = array();
foreach ( self::COMPETITORS as $key => $competitor ) {
$competitors[ $key ] = array(
'slug' => $competitor['slug'],
'name' => $competitor['name'],
'installed' => isset( $all_plugins[ $competitor['slug'] ] ),
'active' => is_plugin_active( $competitor['slug'] ),
);
}
return $competitors;
}
private static function build_summary( array $competitors ): array {
$installed_count = 0;
$active_count = 0;
$installed_list = array();
$active_list = array();
foreach ( $competitors as $key => $competitor ) {
if ( $competitor['installed'] ) {
++$installed_count;
$installed_list[] = $key;
}
if ( $competitor['active'] ) {
++$active_count;
$active_list[] = $key;
}
}
$risk_level = 'none';
if ( $active_count > 0 ) {
$risk_level = 'high';
} elseif ( $installed_count > 0 ) {
$risk_level = 'medium';
}
return array(
'has_competitors' => $installed_count > 0,
'competitors_installed' => $installed_count,
'competitors_active' => $active_count,
'churn_risk' => $risk_level,
'installed_list' => $installed_list,
'active_list' => $active_list,
);
}
private static function build_individual_status( array $competitors ): array {
$status = array();
foreach ( $competitors as $key => $competitor ) {
$status[ $key . '_installed' ] = $competitor['installed'];
$status[ $key . '_active' ] = $competitor['active'];
}
return $status;
}
}

View File

@@ -0,0 +1,94 @@
<?php
/**
* Environment data collector.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core\Collectors;
defined( 'ABSPATH' ) || exit;
class Environment_Collector {
public static function collect(): array {
global $wp_version, $wpdb;
$php_extensions = get_loaded_extensions();
$critical_extensions = array( 'curl', 'json', 'mbstring', 'openssl', 'zip', 'gd', 'xml', 'dom', 'SimpleXML' );
$loaded_critical = array_intersect( $critical_extensions, $php_extensions );
$theme = wp_get_theme();
$parent_theme = $theme->parent() ? $theme->parent()->get( 'Name' ) : '';
$data = array(
'php_version' => phpversion(),
'php_sapi' => php_sapi_name(),
'php_os' => PHP_OS,
'php_architecture' => PHP_INT_SIZE === 8 ? '64-bit' : '32-bit',
'php_memory_limit' => ini_get( 'memory_limit' ),
'php_max_execution_time' => (int) ini_get( 'max_execution_time' ),
'php_max_input_time' => (int) ini_get( 'max_input_time' ),
'php_max_input_vars' => (int) ini_get( 'max_input_vars' ),
'php_post_max_size' => ini_get( 'post_max_size' ),
'php_upload_max_filesize' => ini_get( 'upload_max_filesize' ),
'php_default_timezone' => ini_get( 'date.timezone' ),
'php_log_errors' => ini_get( 'log_errors' ),
'php_extensions_count' => count( $php_extensions ),
'php_critical_extensions' => implode( ',', $loaded_critical ),
'php_curl_version' => function_exists( 'curl_version' ) ? curl_version()['version'] : '',
'php_openssl_version' => OPENSSL_VERSION_TEXT,
'wp_version' => $wp_version,
'wp_db_version' => get_option( 'db_version' ),
'wp_memory_limit' => WP_MEMORY_LIMIT,
'wp_max_memory_limit' => WP_MAX_MEMORY_LIMIT,
'wp_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
'wp_debug_log' => defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG,
'wp_debug_display' => defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY,
'script_debug' => defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG,
'wp_cache' => defined( 'WP_CACHE' ) && WP_CACHE,
'wp_cron_disabled' => defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON,
'wp_auto_update_core' => get_option( 'auto_update_core', 'enabled' ),
'mysql_version' => $wpdb->db_version(),
'mysql_client_version' => $wpdb->get_var( 'SELECT VERSION()' ),
'db_charset' => $wpdb->charset,
'db_collate' => $wpdb->collate,
'db_prefix' => strlen( $wpdb->prefix ),
'server_software' => isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : '',
'server_protocol' => isset( $_SERVER['SERVER_PROTOCOL'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_PROTOCOL'] ) ) : '',
'server_port' => isset( $_SERVER['SERVER_PORT'] ) ? (int) $_SERVER['SERVER_PORT'] : 0,
'https' => is_ssl(),
'http_host' => hash( 'sha256', isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '' ),
'locale' => get_locale(),
'timezone' => wp_timezone_string(),
'site_language' => get_bloginfo( 'language' ),
'site_charset' => get_bloginfo( 'charset' ),
'permalink_structure' => get_option( 'permalink_structure' ),
'home_url' => hash( 'sha256', home_url() ),
'site_url' => hash( 'sha256', site_url() ),
'admin_email' => hash( 'sha256', get_option( 'admin_email' ) ),
'theme' => $theme->get( 'Name' ),
'theme_version' => $theme->get( 'Version' ),
'theme_author' => $theme->get( 'Author' ),
'parent_theme' => $parent_theme,
'is_child_theme' => ! empty( $parent_theme ),
'theme_supports_html5' => current_theme_supports( 'html5' ),
'theme_supports_post_thumbnails' => current_theme_supports( 'post-thumbnails' ),
'active_plugins_count' => count( get_option( 'active_plugins', array() ) ),
'total_plugins_count' => count( get_plugins() ),
'must_use_plugins_count' => count( wp_get_mu_plugins() ),
'is_multisite' => is_multisite(),
'is_subdomain_install' => is_multisite() ? ( defined( 'SUBDOMAIN_INSTALL' ) && SUBDOMAIN_INSTALL ) : false,
'network_count' => is_multisite() ? get_blog_count() : 1,
'is_main_site' => is_multisite() ? is_main_site() : true,
'cf7_version' => defined( 'WPCF7_VERSION' ) ? WPCF7_VERSION : '',
'cf7_installed' => class_exists( 'WPCF7_ContactForm' ),
'plugin_version' => defined( 'SPARTAN_MCE_VERSION' ) ? SPARTAN_MCE_VERSION : '',
'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '',
);
return array_filter( $data, fn( $v ) => $v !== 0 && $v !== false && $v !== '' && $v !== 'none' );
}
}

View File

@@ -0,0 +1,172 @@
<?php
/**
* Features data collector.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core\Collectors;
use Cmatic_Options_Repository;
defined( 'ABSPATH' ) || exit;
class Features_Collector {
public static function collect(): array {
$features = array(
'double_optin' => 0,
'required_consent' => 0,
'debug_logger' => 0,
'custom_merge_fields' => 0,
'interest_groups' => 0,
'groups_total_mapped' => 0,
'tags_enabled' => 0,
'tags_total_selected' => 0,
'arbitrary_tags' => 0,
'conditional_logic' => 0,
'auto_update' => (bool) Cmatic_Options_Repository::get_option( 'auto_update', true ),
'telemetry_enabled' => true,
'debug' => (bool) Cmatic_Options_Repository::get_option( 'debug', false ),
'backlink' => (bool) Cmatic_Options_Repository::get_option( 'backlink', false ),
);
$cf7_forms = Forms_Collector::get_cf7_forms();
$form_ids = array_map( fn( $form ) => $form->id(), $cf7_forms );
$form_options = Forms_Collector::batch_load_form_options( $form_ids );
foreach ( $cf7_forms as $form ) {
$form_id = $form->id();
$cf7_mch = $form_options[ $form_id ]['settings'] ?? array();
$features = self::check_double_optin( $cf7_mch, $features );
$features = self::check_required_consent( $cf7_mch, $features );
$features = self::check_debug_logger( $cf7_mch, $features );
$features = self::check_custom_merge_fields( $cf7_mch, $features );
$features = self::check_interest_groups( $cf7_mch, $features );
$features = self::check_tags( $cf7_mch, $features );
$features = self::check_conditional_logic( $cf7_mch, $features );
}
return self::build_data( $features );
}
private static function check_double_optin( array $cf7_mch, array $features ): array {
if ( isset( $cf7_mch['confsubs'] ) && '1' === $cf7_mch['confsubs'] ) {
++$features['double_optin'];
}
return $features;
}
private static function check_required_consent( array $cf7_mch, array $features ): array {
if ( ! empty( $cf7_mch['consent_required'] ) && ' ' !== $cf7_mch['consent_required'] ) {
++$features['required_consent'];
}
return $features;
}
private static function check_debug_logger( array $cf7_mch, array $features ): array {
if ( isset( $cf7_mch['logfileEnabled'] ) && '1' === $cf7_mch['logfileEnabled'] ) {
++$features['debug_logger'];
}
return $features;
}
private static function check_custom_merge_fields( array $cf7_mch, array $features ): array {
$merge_fields_raw = array();
if ( ! empty( $cf7_mch['merge_fields'] ) && is_array( $cf7_mch['merge_fields'] ) ) {
$merge_fields_raw = $cf7_mch['merge_fields'];
} elseif ( ! empty( $cf7_mch['merge-vars'] ) && is_array( $cf7_mch['merge-vars'] ) ) {
$merge_fields_raw = $cf7_mch['merge-vars'];
}
if ( ! empty( $merge_fields_raw ) ) {
$default_tags = array( 'EMAIL', 'FNAME', 'LNAME', 'ADDRESS', 'PHONE' );
foreach ( $merge_fields_raw as $field ) {
if ( isset( $field['tag'] ) && ! in_array( $field['tag'], $default_tags, true ) ) {
++$features['custom_merge_fields'];
}
}
}
return $features;
}
private static function check_interest_groups( array $cf7_mch, array $features ): array {
$group_count = 0;
for ( $i = 1; $i <= 20; $i++ ) {
$key = $cf7_mch[ "ggCustomKey{$i}" ] ?? '';
$value = $cf7_mch[ "ggCustomValue{$i}" ] ?? '';
if ( ! empty( $key ) && ! empty( trim( $value ) ) && ' ' !== $value ) {
++$group_count;
}
}
if ( $group_count > 0 ) {
++$features['interest_groups'];
$features['groups_total_mapped'] += $group_count;
}
return $features;
}
private static function check_tags( array $cf7_mch, array $features ): array {
if ( ! empty( $cf7_mch['labeltags'] ) && is_array( $cf7_mch['labeltags'] ) ) {
$enabled_tags = array_filter( $cf7_mch['labeltags'], fn( $v ) => '1' === $v );
if ( count( $enabled_tags ) > 0 ) {
++$features['tags_enabled'];
$features['tags_total_selected'] += count( $enabled_tags );
}
}
if ( ! empty( $cf7_mch['labeltags_cm-tag'] ) && trim( $cf7_mch['labeltags_cm-tag'] ) !== '' ) {
++$features['arbitrary_tags'];
}
return $features;
}
private static function check_conditional_logic( array $cf7_mch, array $features ): array {
if ( ! empty( $cf7_mch['conditional_logic'] ) ) {
++$features['conditional_logic'];
}
return $features;
}
private static function build_data( array $features ): array {
$data = array(
'double_optin_count' => $features['double_optin'],
'required_consent_count' => $features['required_consent'],
'debug_logger_count' => $features['debug_logger'],
'custom_merge_fields_count' => $features['custom_merge_fields'],
'interest_groups_count' => $features['interest_groups'],
'groups_total_mapped' => $features['groups_total_mapped'],
'tags_enabled_count' => $features['tags_enabled'],
'tags_total_selected' => $features['tags_total_selected'],
'arbitrary_tags_count' => $features['arbitrary_tags'],
'conditional_logic_count' => $features['conditional_logic'],
'double_optin' => $features['double_optin'] > 0,
'required_consent' => $features['required_consent'] > 0,
'debug_logger' => $features['debug_logger'] > 0,
'custom_merge_fields' => $features['custom_merge_fields'] > 0,
'interest_groups' => $features['interest_groups'] > 0,
'tags_enabled' => $features['tags_enabled'] > 0,
'arbitrary_tags' => $features['arbitrary_tags'] > 0,
'conditional_logic' => $features['conditional_logic'] > 0,
'auto_update' => $features['auto_update'],
'telemetry_enabled' => $features['telemetry_enabled'],
'debug' => $features['debug'],
'backlink' => $features['backlink'],
'total_features_enabled' => count( array_filter( $features ) ),
'features_usage_percentage' => round( ( count( array_filter( $features ) ) / count( $features ) ) * 100, 2 ),
'webhook_enabled' => (bool) Cmatic_Options_Repository::get_option( 'features.webhook_enabled', false ),
'custom_api_endpoint' => (bool) Cmatic_Options_Repository::get_option( 'features.custom_api_endpoint', false ),
'email_notifications' => (bool) Cmatic_Options_Repository::get_option( 'features.email_notifications', false ),
'test_modal_used' => (bool) Cmatic_Options_Repository::get_option( 'features.test_modal_used', false ),
'contact_lookup_used' => (bool) Cmatic_Options_Repository::get_option( 'features.contact_lookup_used', false ),
);
return array_filter( $data, fn( $v ) => $v !== 0 && $v !== false && $v !== '' );
}
}

View File

@@ -0,0 +1,426 @@
<?php
/**
* Forms data collector.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core\Collectors;
use Cmatic_Options_Repository;
defined( 'ABSPATH' ) || exit;
class Forms_Collector {
private const MAX_FORMS = 100;
public static function collect(): array {
$cf7_forms = self::get_cf7_forms();
$processed_forms = count( $cf7_forms );
$total_forms = self::get_total_form_count();
$forms_truncated = $total_forms > self::MAX_FORMS;
$active_forms = 0;
$forms_with_api = 0;
$forms_with_lists = 0;
$total_lists = 0;
$total_fields = 0;
$unique_audiences = array();
$forms_data = array();
$paired_lists = array();
$forms_detail = array();
$field_type_counts = array();
$total_mappings = 0;
$total_mc_fields = 0;
$unique_audiences = self::build_audiences_from_global();
$form_ids = array_map( fn( $form ) => $form->id(), $cf7_forms );
$form_options = self::batch_load_form_options( $form_ids );
foreach ( $cf7_forms as $form ) {
$form_id = $form->id();
$cf7_mch = $form_options[ $form_id ]['settings'] ?? array();
if ( ! empty( $cf7_mch['api'] ) ) {
++$forms_with_api;
++$active_forms;
}
$selected_list = self::get_selected_list( $cf7_mch );
if ( ! empty( $selected_list ) && isset( $unique_audiences[ $selected_list ] ) ) {
$unique_audiences[ $selected_list ]['is_paired'] = true;
$paired_lists[ $selected_list ] = true;
}
$audience_count = 0;
$has_list = false;
if ( isset( $cf7_mch['lisdata']['lists'] ) && is_array( $cf7_mch['lisdata']['lists'] ) ) {
$audience_count = count( $cf7_mch['lisdata']['lists'] );
$has_list = $audience_count > 0;
$total_lists += $audience_count;
if ( $has_list ) {
++$forms_with_lists;
}
}
$form_fields = $form->scan_form_tags();
$total_fields += count( $form_fields );
$form_field_details = self::extract_field_details( $form_fields, $field_type_counts );
list( $form_mappings, $unmapped_cf7, $unmapped_mc, $mapped_cf7_fields, $form_mc_fields, $form_total_mappings ) =
self::extract_mappings( $cf7_mch, $form_field_details );
$total_mc_fields += $form_mc_fields;
$total_mappings += $form_total_mappings;
$form_features = self::extract_form_features( $cf7_mch );
if ( count( $forms_detail ) < 50 ) {
$forms_detail[] = array(
'form_id' => hash( 'sha256', (string) $form_id ),
'field_count' => count( $form_field_details ),
'fields' => $form_field_details,
'paired_audience_id' => ! empty( $selected_list ) ? hash( 'sha256', $selected_list ) : null,
'mappings' => $form_mappings,
'unmapped_cf7_fields' => $unmapped_cf7,
'unmapped_mc_fields' => $unmapped_mc,
'features' => $form_features,
);
}
$forms_data[] = array(
'form_id' => hash( 'sha256', (string) $form_id ),
'has_api' => ! empty( $cf7_mch['api'] ),
'has_list' => $has_list,
'audience_count' => $audience_count,
'field_count' => count( $form_fields ),
'has_double_opt' => isset( $cf7_mch['confsubs'] ) && '1' === $cf7_mch['confsubs'],
'has_consent' => ! empty( $cf7_mch['accept'] ) && ' ' !== $cf7_mch['accept'],
'submissions' => (int) ( $form_options[ $form_id ]['submissions'] ?? 0 ),
'last_submission' => (int) ( $form_options[ $form_id ]['last_submission'] ?? 0 ),
);
}
$avg_fields_per_form = $processed_forms > 0 ? round( $total_fields / $processed_forms, 2 ) : 0;
$avg_lists_per_form = $forms_with_lists > 0 ? round( $total_lists / $forms_with_lists, 2 ) : 0;
list( $oldest_form, $newest_form ) = self::get_form_age_range( $cf7_forms );
$audience_data = array_values( $unique_audiences );
$total_unique_audiences = count( $audience_data );
$total_contacts = array_sum( array_column( $audience_data, 'member_count' ) );
return array(
'total_forms' => $total_forms,
'processed_forms' => $processed_forms,
'active_forms' => $active_forms,
'forms_with_api' => $forms_with_api,
'forms_with_lists' => $forms_with_lists,
'inactive_forms' => $processed_forms - $active_forms,
'total_audiences' => $total_unique_audiences,
'audiences' => $audience_data,
'total_contacts' => $total_contacts,
'avg_lists_per_form' => $avg_lists_per_form,
'max_lists_per_form' => $total_lists > 0 ? max( array_column( $forms_data, 'audience_count' ) ) : 0,
'total_fields_all_forms' => $total_fields,
'avg_fields_per_form' => $avg_fields_per_form,
'min_fields_per_form' => $processed_forms > 0 ? min( array_column( $forms_data, 'field_count' ) ) : 0,
'max_fields_per_form' => $processed_forms > 0 ? max( array_column( $forms_data, 'field_count' ) ) : 0,
'oldest_form_created' => $oldest_form,
'newest_form_created' => $newest_form,
'days_since_oldest_form' => $oldest_form > 0 ? floor( ( time() - $oldest_form ) / DAY_IN_SECONDS ) : 0,
'days_since_newest_form' => $newest_form > 0 ? floor( ( time() - $newest_form ) / DAY_IN_SECONDS ) : 0,
'forms_with_submissions' => count( array_filter( $forms_data, fn( $f ) => $f['submissions'] > 0 ) ),
'forms_never_submitted' => count( array_filter( $forms_data, fn( $f ) => 0 === $f['submissions'] ) ),
'forms_with_double_opt' => count( array_filter( $forms_data, fn( $f ) => $f['has_double_opt'] ) ),
'forms_with_consent' => count( array_filter( $forms_data, fn( $f ) => $f['has_consent'] ) ),
'total_submissions_all_forms' => array_sum( array_column( $forms_data, 'submissions' ) ),
'form_utilization_rate' => $processed_forms > 0 ? round( ( $active_forms / $processed_forms ) * 100, 2 ) : 0,
'forms_detail' => $forms_detail,
'forms_truncated' => $forms_truncated,
'forms_detail_truncated' => $processed_forms > 50,
'field_types_aggregate' => $field_type_counts,
'mapping_stats' => array(
'total_cf7_fields' => $total_fields,
'total_mc_fields' => $total_mc_fields,
'mapped_fields' => $total_mappings,
'mapping_rate' => $total_fields > 0 ? round( ( $total_mappings / $total_fields ) * 100, 2 ) : 0,
),
);
}
public static function get_cf7_forms(): array {
if ( ! class_exists( 'WPCF7_ContactForm' ) ) {
return array();
}
$post_ids = get_posts(
array(
'post_type' => 'wpcf7_contact_form',
'posts_per_page' => self::MAX_FORMS,
'post_status' => 'publish',
'fields' => 'ids',
'orderby' => 'modified',
'order' => 'DESC',
)
);
$forms = array();
foreach ( $post_ids as $post_id ) {
$form = \WPCF7_ContactForm::get_instance( $post_id );
if ( $form ) {
$forms[] = $form;
}
}
return $forms;
}
public static function get_total_form_count(): int {
if ( ! class_exists( 'WPCF7_ContactForm' ) ) {
return 0;
}
global $wpdb;
return (int) $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'wpcf7_contact_form' AND post_status = 'publish'"
);
}
public static function batch_load_form_options( array $form_ids ): array {
global $wpdb;
if ( empty( $form_ids ) ) {
return array();
}
$option_names = array();
foreach ( $form_ids as $form_id ) {
$option_names[] = 'cf7_mch_' . $form_id;
$option_names[] = 'cf7_mch_submissions_' . $form_id;
$option_names[] = 'cf7_mch_last_submission_' . $form_id;
}
$placeholders = implode( ', ', array_fill( 0, count( $option_names ), '%s' ) );
$query = $wpdb->prepare(
"SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name IN ({$placeholders})",
...$option_names
);
$results = $wpdb->get_results( $query, ARRAY_A );
$options_map = array();
foreach ( $results as $row ) {
$options_map[ $row['option_name'] ] = maybe_unserialize( $row['option_value'] );
}
$form_options = array();
foreach ( $form_ids as $form_id ) {
$form_options[ $form_id ] = array(
'settings' => $options_map[ 'cf7_mch_' . $form_id ] ?? array(),
'submissions' => $options_map[ 'cf7_mch_submissions_' . $form_id ] ?? 0,
'last_submission' => $options_map[ 'cf7_mch_last_submission_' . $form_id ] ?? 0,
);
}
return $form_options;
}
private static function build_audiences_from_global(): array {
$unique_audiences = array();
$global_lisdata = Cmatic_Options_Repository::get_option( 'lisdata', array() );
if ( ! empty( $global_lisdata['lists'] ) && is_array( $global_lisdata['lists'] ) ) {
foreach ( $global_lisdata['lists'] as $list ) {
$list_id = $list['id'] ?? '';
if ( empty( $list_id ) ) {
continue;
}
$unique_audiences[ $list_id ] = array(
'audience_id' => hash( 'sha256', $list_id ),
'member_count' => (int) ( $list['stats']['member_count'] ?? 0 ),
'merge_field_count' => (int) ( $list['stats']['merge_field_count'] ?? 0 ),
'double_optin' => ! empty( $list['double_optin'] ),
'marketing_permissions' => ! empty( $list['marketing_permissions'] ),
'campaign_count' => (int) ( $list['stats']['campaign_count'] ?? 0 ),
'is_paired' => false,
);
}
}
return $unique_audiences;
}
private static function get_selected_list( array $cf7_mch ): string {
$selected_list = $cf7_mch['list'] ?? '';
if ( is_array( $selected_list ) ) {
$selected_list = $selected_list[0] ?? '';
}
return $selected_list;
}
private static function extract_field_details( array $form_fields, array &$field_type_counts ): array {
$form_field_details = array();
$form_field_limit = 30;
foreach ( $form_fields as $ff_index => $tag ) {
if ( $ff_index >= $form_field_limit ) {
break;
}
$basetype = '';
if ( is_object( $tag ) && isset( $tag->basetype ) ) {
$basetype = $tag->basetype;
} elseif ( is_array( $tag ) && isset( $tag['basetype'] ) ) {
$basetype = $tag['basetype'];
}
$field_name = '';
if ( is_object( $tag ) && isset( $tag->name ) ) {
$field_name = $tag->name;
} elseif ( is_array( $tag ) && isset( $tag['name'] ) ) {
$field_name = $tag['name'];
}
if ( ! empty( $field_name ) && ! empty( $basetype ) ) {
$form_field_details[] = array(
'name' => $field_name,
'type' => $basetype,
);
if ( ! isset( $field_type_counts[ $basetype ] ) ) {
$field_type_counts[ $basetype ] = 0;
}
++$field_type_counts[ $basetype ];
}
}
return $form_field_details;
}
private static function extract_mappings( array $cf7_mch, array $form_field_details ): array {
$form_mappings = array();
$unmapped_cf7 = 0;
$unmapped_mc = 0;
$mapped_cf7_fields = array();
$total_mc_fields = 0;
$total_mappings = 0;
for ( $i = 1; $i <= 20; $i++ ) {
$mc_tag = $cf7_mch[ 'CustomKey' . $i ] ?? '';
$mc_type = $cf7_mch[ 'CustomKeyType' . $i ] ?? '';
$cf7_field = trim( $cf7_mch[ 'CustomValue' . $i ] ?? '' );
if ( ! empty( $mc_tag ) ) {
++$total_mc_fields;
if ( '' !== $cf7_field ) {
$form_mappings[] = array(
'cf7_field' => $cf7_field,
'mc_tag' => $mc_tag,
'mc_type' => $mc_type,
);
$mapped_cf7_fields[] = $cf7_field;
++$total_mappings;
} else {
++$unmapped_mc;
}
}
}
foreach ( $form_field_details as $field ) {
if ( ! in_array( $field['name'], $mapped_cf7_fields, true ) ) {
++$unmapped_cf7;
}
}
return array( $form_mappings, $unmapped_cf7, $unmapped_mc, $mapped_cf7_fields, $total_mc_fields, $total_mappings );
}
private static function extract_form_features( array $cf7_mch ): array {
$form_features = array();
if ( isset( $cf7_mch['confsubs'] ) && '1' === $cf7_mch['confsubs'] ) {
$form_features['double_optin'] = true;
}
if ( ! empty( $cf7_mch['consent_required'] ) && ' ' !== $cf7_mch['consent_required'] ) {
$form_features['required_consent'] = true;
}
if ( isset( $cf7_mch['logfileEnabled'] ) && '1' === $cf7_mch['logfileEnabled'] ) {
$form_features['debug_logger'] = true;
}
if ( ! empty( $cf7_mch['labeltags'] ) && is_array( $cf7_mch['labeltags'] ) ) {
$enabled_tags = array_filter( $cf7_mch['labeltags'], fn( $v ) => '1' === $v );
if ( count( $enabled_tags ) > 0 ) {
$form_features['tags_enabled'] = true;
}
}
$form_group_count = 0;
for ( $gi = 1; $gi <= 20; $gi++ ) {
$gkey = $cf7_mch[ "ggCustomKey{$gi}" ] ?? '';
$gvalue = $cf7_mch[ "ggCustomValue{$gi}" ] ?? '';
if ( ! empty( $gkey ) && ! empty( trim( $gvalue ) ) && ' ' !== $gvalue ) {
++$form_group_count;
}
}
if ( $form_group_count > 0 ) {
$form_features['interest_groups'] = true;
}
$form_merge_fields_raw = array();
if ( ! empty( $cf7_mch['merge_fields'] ) && is_array( $cf7_mch['merge_fields'] ) ) {
$form_merge_fields_raw = $cf7_mch['merge_fields'];
} elseif ( ! empty( $cf7_mch['merge-vars'] ) && is_array( $cf7_mch['merge-vars'] ) ) {
$form_merge_fields_raw = $cf7_mch['merge-vars'];
}
if ( ! empty( $form_merge_fields_raw ) ) {
$default_tags = array( 'EMAIL', 'FNAME', 'LNAME', 'ADDRESS', 'PHONE' );
$custom_field_count = 0;
foreach ( $form_merge_fields_raw as $mfield ) {
if ( isset( $mfield['tag'] ) && ! in_array( $mfield['tag'], $default_tags, true ) ) {
++$custom_field_count;
}
}
if ( $custom_field_count > 0 ) {
$form_features['custom_merge_fields'] = true;
}
}
if ( ! empty( $cf7_mch['conditional_logic'] ) ) {
$form_features['conditional_logic'] = true;
}
return $form_features;
}
private static function get_form_age_range( array $cf7_forms ): array {
$oldest_form = 0;
$newest_form = 0;
foreach ( $cf7_forms as $form ) {
$created = get_post_field( 'post_date', $form->id(), 'raw' );
$timestamp = strtotime( $created );
if ( 0 === $oldest_form || $timestamp < $oldest_form ) {
$oldest_form = $timestamp;
}
if ( 0 === $newest_form || $timestamp > $newest_form ) {
$newest_form = $timestamp;
}
}
return array( $oldest_form, $newest_form );
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* Install data collector.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core\Collectors;
use Cmatic\Metrics\Core\Storage;
use Cmatic_Options_Repository;
defined( 'ABSPATH' ) || exit;
class Install_Collector {
public static function collect(): array {
return array(
'plugin_slug' => Cmatic_Options_Repository::get_option( 'install.plugin_slug', 'contact-form-7-mailchimp-extension' ),
'quest' => Storage::get_quest(),
'pro' => array(
'installed' => (bool) Cmatic_Options_Repository::get_option( 'install.pro.installed', false ),
'activated' => (bool) Cmatic_Options_Repository::get_option( 'install.pro.activated', false ),
'version' => Cmatic_Options_Repository::get_option( 'install.pro.version', null ),
'licensed' => (bool) Cmatic_Options_Repository::get_option( 'install.pro.licensed', false ),
'license_expires' => Cmatic_Options_Repository::get_option( 'install.pro.license_expires', null ),
),
);
}
}

View File

@@ -0,0 +1,90 @@
<?php
/**
* Lifecycle data collector.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core\Collectors;
use Cmatic\Metrics\Core\Storage;
use Cmatic_Options_Repository;
defined( 'ABSPATH' ) || exit;
class Lifecycle_Collector {
public static function collect(): array {
$activations = Cmatic_Options_Repository::get_option( 'lifecycle.activations', array() );
$deactivations = Cmatic_Options_Repository::get_option( 'lifecycle.deactivations', array() );
$upgrades = Cmatic_Options_Repository::get_option( 'lifecycle.upgrades', array() );
$first_activated = Storage::get_quest();
$last_activated = ! empty( $activations ) ? max( $activations ) : 0;
$last_deactivated = ! empty( $deactivations ) ? max( $deactivations ) : 0;
$last_upgrade = ! empty( $upgrades ) ? max( $upgrades ) : 0;
$days_since_first = 0;
if ( $first_activated > 0 ) {
$days_since_first = floor( ( time() - $first_activated ) / DAY_IN_SECONDS );
}
$avg_session_length = self::calculate_avg_session_length( $activations, $deactivations );
$version_history = Cmatic_Options_Repository::get_option( 'lifecycle.version_history', array() );
$previous_version = Cmatic_Options_Repository::get_option( 'lifecycle.previous_version', '' );
$days_since_last_upgrade = $last_upgrade > 0 ? floor( ( time() - $last_upgrade ) / DAY_IN_SECONDS ) : 0;
$active_session = empty( $deactivations ) || $last_activated > $last_deactivated;
$data = array(
'activation_count' => count( $activations ),
'deactivation_count' => count( $deactivations ),
'upgrade_count' => count( $upgrades ),
'first_activated' => $first_activated,
'last_activated' => $last_activated,
'last_deactivated' => $last_deactivated,
'last_upgrade' => $last_upgrade,
'days_since_first_activation' => (int) $days_since_first,
'days_since_last_upgrade' => (int) $days_since_last_upgrade,
'avg_session_length_seconds' => $avg_session_length,
'total_sessions' => count( $activations ),
'previous_version' => $previous_version,
'version_history_count' => count( $version_history ),
'install_method' => Cmatic_Options_Repository::get_option( 'lifecycle.install_method', 'unknown' ),
'days_on_current_version' => $last_upgrade > 0 ? floor( ( time() - $last_upgrade ) / DAY_IN_SECONDS ) : $days_since_first,
'activation_timestamps' => $activations,
'deactivation_timestamps' => $deactivations,
'upgrade_timestamps' => $upgrades,
);
$data = array_filter( $data, fn( $v ) => $v !== 0 && $v !== '' && $v !== 'unknown' );
$data['active_session'] = $active_session;
return $data;
}
private static function calculate_avg_session_length( array $activations, array $deactivations ): int {
if ( count( $activations ) === 0 || count( $deactivations ) === 0 ) {
return 0;
}
$total_session_time = 0;
$session_count = 0;
foreach ( $activations as $index => $activation_time ) {
if ( isset( $deactivations[ $index ] ) ) {
$total_session_time += $deactivations[ $index ] - $activation_time;
++$session_count;
}
}
if ( $session_count > 0 ) {
return (int) floor( $total_session_time / $session_count );
}
return 0;
}
}

View File

@@ -0,0 +1,36 @@
<?php
/**
* Metadata collector.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core\Collectors;
use Cmatic\Metrics\Core\Storage;
use Cmatic_Options_Repository;
defined( 'ABSPATH' ) || exit;
class Metadata_Collector {
public static function collect(): array {
$first_installed = Storage::get_quest();
$total_uptime = $first_installed > 0 ? time() - $first_installed : 0;
$failed_count = (int) Cmatic_Options_Repository::get_option( 'telemetry.failed_count', 0 );
return array(
'schedule' => Cmatic_Options_Repository::get_option( 'telemetry.schedule', 'frequent' ),
'frequent_started_at' => (int) Cmatic_Options_Repository::get_option( 'telemetry.frequent_started_at', 0 ),
'is_reactivation' => Storage::is_reactivation(),
'disabled_count' => (int) Cmatic_Options_Repository::get_option( 'telemetry.disabled_count', 0 ),
'opt_in_date' => (int) Cmatic_Options_Repository::get_option( 'telemetry.opt_in_date', 0 ),
'last_heartbeat' => (int) Cmatic_Options_Repository::get_option( 'telemetry.last_heartbeat', 0 ),
'failed_heartbeats' => $failed_count,
'total_uptime_seconds' => $total_uptime,
'telemetry_version' => SPARTAN_MCE_VERSION,
);
}
}

View File

@@ -0,0 +1,140 @@
<?php
/**
* Performance data collector.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core\Collectors;
use Cmatic_Options_Repository;
defined( 'ABSPATH' ) || exit;
class Performance_Collector {
public static function collect(): array {
$memory_current = memory_get_usage( true );
$memory_peak = memory_get_peak_usage( true );
$memory_limit = ini_get( 'memory_limit' );
$memory_limit_bytes = self::convert_to_bytes( $memory_limit );
$memory_usage_percent = $memory_limit_bytes > 0 ? round( ( $memory_peak / $memory_limit_bytes ) * 100, 2 ) : 0;
$db_queries = get_num_queries();
$db_time = timer_stop( 0, 3 );
$page_load_time = isset( $_SERVER['REQUEST_TIME_FLOAT'] ) ? ( microtime( true ) - floatval( $_SERVER['REQUEST_TIME_FLOAT'] ) ) * 1000 : 0;
list( $object_cache_hits, $object_cache_misses ) = self::get_object_cache_stats();
$plugin_load_time = (float) Cmatic_Options_Repository::get_option( 'performance.plugin_load_time', 0 );
$api_avg_response = (float) Cmatic_Options_Repository::get_option( 'performance.api_avg_response', 0 );
$data = array(
'memory_current' => $memory_current,
'memory_peak' => $memory_peak,
'memory_limit' => $memory_limit,
'memory_limit_bytes' => $memory_limit_bytes,
'memory_usage_percent' => $memory_usage_percent,
'memory_available' => max( 0, $memory_limit_bytes - $memory_peak ),
'php_max_execution_time' => (int) ini_get( 'max_execution_time' ),
'page_load_time_ms' => round( $page_load_time, 2 ),
'plugin_load_time_ms' => round( $plugin_load_time, 2 ),
'db_queries_count' => $db_queries,
'db_query_time_seconds' => (float) $db_time,
'db_size_mb' => self::get_database_size(),
'api_avg_response_ms' => round( $api_avg_response, 2 ),
'api_slowest_response_ms' => (int) Cmatic_Options_Repository::get_option( 'performance.api_slowest', 0 ),
'api_fastest_response_ms' => (int) Cmatic_Options_Repository::get_option( 'performance.api_fastest', 0 ),
'object_cache_enabled' => wp_using_ext_object_cache(),
'object_cache_hits' => $object_cache_hits,
'object_cache_misses' => $object_cache_misses,
'object_cache_hit_rate' => ( $object_cache_hits + $object_cache_misses ) > 0 ? round( ( $object_cache_hits / ( $object_cache_hits + $object_cache_misses ) ) * 100, 2 ) : 0,
'opcache_enabled' => self::is_opcache_enabled(),
'opcache_hit_rate' => self::get_opcache_hit_rate(),
);
return array_filter( $data, fn( $v ) => $v !== 0 && $v !== 0.0 && $v !== false && $v !== null && $v !== '' );
}
private static function is_opcache_enabled(): bool {
if ( ! function_exists( 'opcache_get_status' ) ) {
return false;
}
$status = @opcache_get_status();
return false !== $status && is_array( $status );
}
public static function convert_to_bytes( $value ): int {
$value = trim( $value );
if ( empty( $value ) ) {
return 0;
}
$last = strtolower( $value[ strlen( $value ) - 1 ] );
$value = (int) $value;
switch ( $last ) {
case 'g':
$value *= 1024;
// Fall through.
case 'm':
$value *= 1024;
// Fall through.
case 'k':
$value *= 1024;
}
return $value;
}
private static function get_database_size(): float {
global $wpdb;
$size = $wpdb->get_var(
$wpdb->prepare(
'SELECT SUM(data_length + index_length) / 1024 / 1024
FROM information_schema.TABLES
WHERE table_schema = %s',
DB_NAME
)
);
return round( (float) $size, 2 );
}
private static function get_opcache_hit_rate(): float {
if ( ! function_exists( 'opcache_get_status' ) ) {
return 0;
}
$status = @opcache_get_status();
if ( false === $status || ! is_array( $status ) || ! isset( $status['opcache_statistics'] ) ) {
return 0;
}
$stats = $status['opcache_statistics'];
$hits = isset( $stats['hits'] ) ? (int) $stats['hits'] : 0;
$misses = isset( $stats['misses'] ) ? (int) $stats['misses'] : 0;
if ( ( $hits + $misses ) === 0 ) {
return 0;
}
return round( ( $hits / ( $hits + $misses ) ) * 100, 2 );
}
private static function get_object_cache_stats(): array {
$object_cache_hits = 0;
$object_cache_misses = 0;
if ( function_exists( 'wp_cache_get_stats' ) ) {
$cache_stats = wp_cache_get_stats();
$object_cache_hits = isset( $cache_stats['hits'] ) ? $cache_stats['hits'] : 0;
$object_cache_misses = isset( $cache_stats['misses'] ) ? $cache_stats['misses'] : 0;
}
return array( $object_cache_hits, $object_cache_misses );
}
}

View File

@@ -0,0 +1,135 @@
<?php
/**
* Plugins data collector.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core\Collectors;
defined( 'ABSPATH' ) || exit;
class Plugins_Collector {
public static function collect(): array {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$all_plugins = get_plugins();
$active_plugins = get_option( 'active_plugins', array() );
$mu_plugins = get_mu_plugins();
$plugin_list = self::build_plugin_list( $all_plugins, $active_plugins, $mu_plugins );
$plugin_stats = self::get_plugin_stats( $all_plugins );
$known_plugins = self::get_known_plugins();
$data = array(
'total_plugins' => count( $all_plugins ),
'active_plugins' => count( $active_plugins ),
'inactive_plugins' => count( $all_plugins ) - count( $active_plugins ),
'mu_plugins' => count( $mu_plugins ),
'premium_plugins' => $plugin_stats['premium'],
'cf7_addons' => $plugin_stats['cf7'],
'mailchimp_plugins' => $plugin_stats['mailchimp'],
'security_plugins' => $plugin_stats['security'],
'cache_plugins' => $plugin_stats['cache'],
'seo_plugins' => $plugin_stats['seo'],
'has_woocommerce' => $known_plugins['woocommerce'],
'has_elementor' => $known_plugins['elementor'],
'has_jetpack' => $known_plugins['jetpack'],
'has_wordfence' => $known_plugins['wordfence'],
'has_yoast_seo' => $known_plugins['yoast_seo'],
'plugin_list' => $plugin_list,
);
return array_filter( $data, fn( $v ) => $v !== 0 && $v !== false && $v !== '' && $v !== array() );
}
private static function build_plugin_list( array $all_plugins, array $active_plugins, array $mu_plugins ): array {
$plugin_list = array();
$network_active = is_multisite() ? get_site_option( 'active_sitewide_plugins', array() ) : array();
foreach ( $all_plugins as $plugin_path => $plugin_data ) {
$is_active = in_array( $plugin_path, $active_plugins, true );
$is_network = isset( $network_active[ $plugin_path ] );
$status = 'inactive';
if ( $is_network ) {
$status = 'network-active';
} elseif ( $is_active ) {
$status = 'active';
}
$dir = dirname( $plugin_path );
$plugin_list[] = array(
'slug' => '.' !== $dir ? $dir : basename( $plugin_path, '.php' ),
'name' => $plugin_data['Name'],
'version' => $plugin_data['Version'],
'author' => wp_strip_all_tags( $plugin_data['Author'] ),
'status' => $status,
);
}
foreach ( $mu_plugins as $mu_plugin_path => $mu_plugin_data ) {
$plugin_list[] = array(
'slug' => basename( $mu_plugin_path, '.php' ),
'name' => $mu_plugin_data['Name'],
'version' => $mu_plugin_data['Version'],
'author' => wp_strip_all_tags( $mu_plugin_data['Author'] ),
'status' => 'mu-plugin',
);
}
return $plugin_list;
}
private static function get_plugin_stats( array $all_plugins ): array {
$stats = array(
'premium' => 0,
'cf7' => 0,
'mailchimp' => 0,
'security' => 0,
'cache' => 0,
'seo' => 0,
);
foreach ( $all_plugins as $plugin_path => $plugin_data ) {
$name = strtolower( $plugin_data['Name'] );
if ( strpos( $name, 'pro' ) !== false || strpos( $name, 'premium' ) !== false ) {
++$stats['premium'];
}
if ( strpos( $name, 'contact form 7' ) !== false ) {
++$stats['cf7'];
}
if ( strpos( $name, 'mailchimp' ) !== false ) {
++$stats['mailchimp'];
}
if ( strpos( $name, 'security' ) !== false || strpos( $name, 'wordfence' ) !== false || strpos( $name, 'sucuri' ) !== false ) {
++$stats['security'];
}
if ( strpos( $name, 'cache' ) !== false || strpos( $name, 'wp rocket' ) !== false || strpos( $name, 'w3 total cache' ) !== false ) {
++$stats['cache'];
}
if ( strpos( $name, 'seo' ) !== false || strpos( $name, 'yoast' ) !== false ) {
++$stats['seo'];
}
}
return $stats;
}
private static function get_known_plugins(): array {
return array(
'woocommerce' => is_plugin_active( 'woocommerce/woocommerce.php' ),
'elementor' => is_plugin_active( 'elementor/elementor.php' ),
'jetpack' => is_plugin_active( 'jetpack/jetpack.php' ),
'wordfence' => is_plugin_active( 'wordfence/wordfence.php' ),
'yoast_seo' => is_plugin_active( 'wordpress-seo/wp-seo.php' ),
);
}
}

View File

@@ -0,0 +1,90 @@
<?php
/**
* Server data collector.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core\Collectors;
defined( 'ABSPATH' ) || exit;
class Server_Collector {
public static function collect(): array {
$server_load = self::get_load_average();
list( $disk_free, $disk_total ) = self::get_disk_space();
$disk_used = $disk_total - $disk_free;
$disk_usage_percent = $disk_total > 0 ? round( ( $disk_used / $disk_total ) * 100, 2 ) : 0;
$hostname = self::get_hostname();
$architecture = self::get_architecture();
return array(
'load_average_1min' => isset( $server_load[0] ) ? round( (float) $server_load[0], 2 ) : 0,
'load_average_5min' => isset( $server_load[1] ) ? round( (float) $server_load[1], 2 ) : 0,
'load_average_15min' => isset( $server_load[2] ) ? round( (float) $server_load[2], 2 ) : 0,
'disk_usage_percent' => $disk_usage_percent,
'disk_total_gb' => $disk_total ? round( $disk_total / 1024 / 1024 / 1024, 2 ) : 0,
'server_ip' => hash( 'sha256', isset( $_SERVER['SERVER_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_ADDR'] ) ) : '' ),
'server_hostname' => hash( 'sha256', $hostname ),
'server_os' => PHP_OS,
'server_architecture' => $architecture,
);
}
private static function get_load_average(): array {
if ( ! function_exists( 'sys_getloadavg' ) ) {
return array( 0, 0, 0 );
}
$load = @sys_getloadavg();
if ( false !== $load && is_array( $load ) ) {
return $load;
}
return array( 0, 0, 0 );
}
private static function get_disk_space(): array {
$disk_free = 0;
$disk_total = 0;
if ( function_exists( 'disk_free_space' ) ) {
$free = @disk_free_space( ABSPATH );
if ( false !== $free ) {
$disk_free = $free;
}
}
if ( function_exists( 'disk_total_space' ) ) {
$total = @disk_total_space( ABSPATH );
if ( false !== $total ) {
$disk_total = $total;
}
}
return array( $disk_free, $disk_total );
}
private static function get_hostname(): string {
if ( ! function_exists( 'gethostname' ) ) {
return '';
}
$name = @gethostname();
return false !== $name ? $name : '';
}
private static function get_architecture(): string {
if ( ! function_exists( 'php_uname' ) ) {
return '';
}
$arch = @php_uname( 'm' );
return false !== $arch ? $arch : '';
}
}

View File

@@ -0,0 +1,116 @@
<?php
/**
* Submissions data collector.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core\Collectors;
use Cmatic\Metrics\Core\Storage;
use Cmatic_Options_Repository;
defined( 'ABSPATH' ) || exit;
class Submissions_Collector {
public static function collect(): array {
$total_sent = (int) Cmatic_Options_Repository::get_option( 'stats.sent', 0 );
$total_failed = (int) Cmatic_Options_Repository::get_option( 'submissions.failed', 0 );
$first_submission = (int) Cmatic_Options_Repository::get_option( 'submissions.first', 0 );
$last_submission = (int) Cmatic_Options_Repository::get_option( 'submissions.last', 0 );
$last_success = (int) Cmatic_Options_Repository::get_option( 'submissions.last_success', 0 );
$last_failure = (int) Cmatic_Options_Repository::get_option( 'submissions.last_failure', 0 );
$first_activated = Storage::get_quest();
$days_active = 1;
if ( $first_activated > 0 ) {
$days_active = max( 1, floor( ( time() - $first_activated ) / DAY_IN_SECONDS ) );
}
$total_submissions = $total_sent + $total_failed;
$avg_per_day = $days_active > 0 ? round( $total_submissions / $days_active, 2 ) : 0;
$success_rate = $total_submissions > 0 ? round( ( $total_sent / $total_submissions ) * 100, 2 ) : 100;
$days_since_first = $first_submission > 0 ? floor( ( time() - $first_submission ) / DAY_IN_SECONDS ) : 0;
$days_since_last = $last_submission > 0 ? floor( ( time() - $last_submission ) / DAY_IN_SECONDS ) : 0;
$hours_since_last = $last_submission > 0 ? floor( ( time() - $last_submission ) / HOUR_IN_SECONDS ) : 0;
list( $busiest_hour, $max_submissions ) = self::get_busiest_hour();
list( $busiest_day, $max_day_submissions ) = self::get_busiest_day();
$this_month = (int) Cmatic_Options_Repository::get_option( 'submissions.this_month', 0 );
$last_month = (int) Cmatic_Options_Repository::get_option( 'submissions.last_month', 0 );
$peak_month = (int) Cmatic_Options_Repository::get_option( 'submissions.peak_month', 0 );
$consecutive_successes = (int) Cmatic_Options_Repository::get_option( 'submissions.consecutive_successes', 0 );
$consecutive_failures = (int) Cmatic_Options_Repository::get_option( 'submissions.consecutive_failures', 0 );
$data = array(
'total_sent' => $total_sent,
'total_failed' => $total_failed,
'total_submissions' => $total_submissions,
'successful_submissions_count' => $total_sent,
'failed_count' => $total_failed,
'success_rate' => $success_rate,
'first_submission' => $first_submission,
'last_submission' => $last_submission,
'last_success' => $last_success,
'last_failure' => $last_failure,
'days_since_first' => $days_since_first,
'days_since_last' => $days_since_last,
'hours_since_last' => $hours_since_last,
'avg_per_day' => $avg_per_day,
'avg_per_week' => round( $avg_per_day * 7, 2 ),
'avg_per_month' => round( $avg_per_day * 30, 2 ),
'busiest_hour' => $busiest_hour,
'busiest_day' => $busiest_day,
'submissions_busiest_hour' => $max_submissions,
'submissions_busiest_day' => $max_day_submissions,
'this_month' => $this_month,
'last_month' => $last_month,
'peak_month' => $peak_month,
'month_over_month_change' => $last_month > 0 ? round( ( ( $this_month - $last_month ) / $last_month ) * 100, 2 ) : 0,
'consecutive_successes' => $consecutive_successes,
'consecutive_failures' => $consecutive_failures,
'longest_success_streak' => (int) Cmatic_Options_Repository::get_option( 'submissions.longest_success_streak', 0 ),
'active_forms_count' => (int) Cmatic_Options_Repository::get_option( 'submissions.active_forms', 0 ),
'forms_with_submissions' => count( Cmatic_Options_Repository::get_option( 'submissions.forms_used', array() ) ),
);
return array_filter( $data, fn( $v ) => $v !== 0 && $v !== 0.0 );
}
private static function get_busiest_hour(): array {
$hourly_distribution = Cmatic_Options_Repository::get_option( 'submissions.hourly', array() );
$busiest_hour = 0;
$max_submissions = 0;
foreach ( $hourly_distribution as $hour => $count ) {
if ( $count > $max_submissions ) {
$max_submissions = $count;
$busiest_hour = (int) $hour;
}
}
return array( $busiest_hour, $max_submissions );
}
private static function get_busiest_day(): array {
$daily_distribution = Cmatic_Options_Repository::get_option( 'submissions.daily', array() );
$busiest_day = 0;
$max_day_submissions = 0;
foreach ( $daily_distribution as $day => $count ) {
if ( $count > $max_day_submissions ) {
$max_day_submissions = $count;
$busiest_day = (int) $day;
}
}
return array( $busiest_day, $max_day_submissions );
}
}

View File

@@ -0,0 +1,50 @@
<?php
/**
* WordPress data collector.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core\Collectors;
defined( 'ABSPATH' ) || exit;
class WordPress_Collector {
public static function collect(): array {
global $wpdb;
$post_counts = wp_count_posts( 'post' );
$page_counts = wp_count_posts( 'page' );
$comment_counts = wp_count_comments();
$user_count = count_users();
$media_counts = wp_count_posts( 'attachment' );
$category_count = wp_count_terms( 'category' );
$tag_count = wp_count_terms( 'post_tag' );
$revision_count = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'revision'" );
$data = array(
'posts_published' => isset( $post_counts->publish ) ? (int) $post_counts->publish : 0,
'posts_draft' => isset( $post_counts->draft ) ? (int) $post_counts->draft : 0,
'pages_published' => isset( $page_counts->publish ) ? (int) $page_counts->publish : 0,
'media_items' => isset( $media_counts->inherit ) ? (int) $media_counts->inherit : 0,
'comments_pending' => isset( $comment_counts->moderated ) ? (int) $comment_counts->moderated : 0,
'comments_spam' => isset( $comment_counts->spam ) ? (int) $comment_counts->spam : 0,
'users_total' => isset( $user_count['total_users'] ) ? (int) $user_count['total_users'] : 0,
'users_administrators' => isset( $user_count['avail_roles']['administrator'] ) ? (int) $user_count['avail_roles']['administrator'] : 0,
'users_editors' => isset( $user_count['avail_roles']['editor'] ) ? (int) $user_count['avail_roles']['editor'] : 0,
'users_authors' => isset( $user_count['avail_roles']['author'] ) ? (int) $user_count['avail_roles']['author'] : 0,
'users_subscribers' => isset( $user_count['avail_roles']['subscriber'] ) ? (int) $user_count['avail_roles']['subscriber'] : 0,
'categories_count' => is_wp_error( $category_count ) ? 0 : (int) $category_count,
'tags_count' => is_wp_error( $tag_count ) ? 0 : (int) $tag_count,
'revisions_count' => (int) $revision_count,
);
$data = array_filter( $data, fn( $v ) => $v !== 0 );
$data['auto_updates_enabled'] = (bool) get_option( 'auto_update_plugins', false );
return $data;
}
}

View File

@@ -0,0 +1,184 @@
<?php
/**
* Metrics scheduler.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core;
use Cmatic_Options_Repository;
defined( 'ABSPATH' ) || exit;
class Scheduler {
public static function init() {
add_filter( 'cron_schedules', array( __CLASS__, 'add_cron_intervals' ) );
add_action( 'cmatic_metrics_heartbeat', array( __CLASS__, 'execute_heartbeat' ) );
add_action( 'admin_init', array( __CLASS__, 'ensure_schedule' ) );
add_action( 'cmatic_subscription_success', array( __CLASS__, 'execute_heartbeat' ) );
}
public static function add_cron_intervals( $schedules ) {
$schedules['cmatic_2min'] = array(
'interval' => 2 * MINUTE_IN_SECONDS,
'display' => 'Every 2 Minutes',
);
$schedules['cmatic_10min'] = array(
'interval' => 10 * MINUTE_IN_SECONDS,
'display' => 'Every 10 Minutes',
);
$interval_hours = Storage::get_heartbeat_interval();
$schedules['cmatic_sparse'] = array(
'interval' => $interval_hours * HOUR_IN_SECONDS,
'display' => 'Every ' . $interval_hours . ' Hours',
);
return $schedules;
}
public static function execute_heartbeat() {
if ( ! Storage::is_enabled() ) {
return;
}
$schedule = Storage::get_schedule();
$started_at = Storage::get_frequent_started_at();
$elapsed = $started_at > 0 ? time() - $started_at : 0;
if ( 'super_frequent' === $schedule && $elapsed >= ( 15 * MINUTE_IN_SECONDS ) ) {
self::transition_to_frequent();
} elseif ( 'frequent' === $schedule && $elapsed >= ( 1 * HOUR_IN_SECONDS ) ) {
self::transition_to_sparse();
}
self::sync_global_lisdata();
$payload = Collector::collect( 'heartbeat' );
Sync::send( $payload );
}
private static function transition_to_frequent() {
Storage::set_schedule( 'frequent' );
$timestamp = wp_next_scheduled( 'cmatic_metrics_heartbeat' );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, 'cmatic_metrics_heartbeat' );
}
wp_schedule_event( time(), 'cmatic_10min', 'cmatic_metrics_heartbeat' );
}
private static function transition_to_sparse() {
Storage::set_schedule( 'sparse' );
$timestamp = wp_next_scheduled( 'cmatic_metrics_heartbeat' );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, 'cmatic_metrics_heartbeat' );
}
$interval_hours = Storage::get_heartbeat_interval();
wp_schedule_single_event(
time() + ( $interval_hours * HOUR_IN_SECONDS ),
'cmatic_metrics_heartbeat'
);
}
public static function ensure_schedule() {
if ( ! Storage::is_enabled() ) {
return;
}
if ( wp_next_scheduled( 'cmatic_metrics_heartbeat' ) ) {
return;
}
$schedule = Storage::get_schedule();
$started_at = Storage::get_frequent_started_at();
$elapsed = $started_at > 0 ? time() - $started_at : 0;
if ( 'super_frequent' === $schedule ) {
if ( $elapsed >= ( 15 * MINUTE_IN_SECONDS ) ) {
self::transition_to_frequent();
} else {
wp_schedule_event( time(), 'cmatic_2min', 'cmatic_metrics_heartbeat' );
}
} elseif ( 'frequent' === $schedule ) {
if ( Storage::is_frequent_elapsed() ) {
self::transition_to_sparse();
} else {
wp_schedule_event( time(), 'cmatic_10min', 'cmatic_metrics_heartbeat' );
}
} else {
$interval_hours = Storage::get_heartbeat_interval();
$last_heartbeat = Storage::get_last_heartbeat();
$next_heartbeat = $last_heartbeat + ( $interval_hours * HOUR_IN_SECONDS );
if ( $next_heartbeat < time() ) {
$next_heartbeat = time();
}
wp_schedule_single_event( $next_heartbeat, 'cmatic_metrics_heartbeat' );
}
}
public static function schedule_next_sparse() {
if ( 'sparse' !== Storage::get_schedule() ) {
return;
}
$timestamp = wp_next_scheduled( 'cmatic_metrics_heartbeat' );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, 'cmatic_metrics_heartbeat' );
}
$interval_hours = Storage::get_heartbeat_interval();
wp_schedule_single_event(
time() + ( $interval_hours * HOUR_IN_SECONDS ),
'cmatic_metrics_heartbeat'
);
}
public static function clear_schedule() {
$timestamp = wp_next_scheduled( 'cmatic_metrics_heartbeat' );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, 'cmatic_metrics_heartbeat' );
}
}
private static function sync_global_lisdata() {
global $wpdb;
$form_options = $wpdb->get_results(
"SELECT option_value FROM {$wpdb->options}
WHERE option_name LIKE 'cf7_mch_%'
AND option_value LIKE '%lisdata%'
LIMIT 50"
);
$best_lisdata = null;
$best_list_count = 0;
if ( ! empty( $form_options ) ) {
foreach ( $form_options as $row ) {
$cf7_mch = maybe_unserialize( $row->option_value );
if ( ! is_array( $cf7_mch ) || empty( $cf7_mch['lisdata']['lists'] ) ) {
continue;
}
$list_count = count( $cf7_mch['lisdata']['lists'] );
if ( $list_count > $best_list_count ) {
$best_list_count = $list_count;
$best_lisdata = $cf7_mch['lisdata'];
}
}
}
// Always update - either with found data or empty array to clear stale data.
Cmatic_Options_Repository::set_option( 'lisdata', $best_lisdata ?? array() );
Cmatic_Options_Repository::set_option( 'lisdata_updated', time() );
}
}

View File

@@ -0,0 +1,151 @@
<?php
/**
* Metrics storage handler.
*
* Delegates install_id, quest, and lifecycle tracking to Cmatic_Install_Data
* and Cmatic_Activator/Cmatic_Deactivator classes (single source of truth).
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core;
use Cmatic_Options_Repository;
defined( 'ABSPATH' ) || exit;
class Storage {
public static function init() {
if ( null !== Cmatic_Options_Repository::get_option( 'telemetry.enabled' ) ) {
return;
}
$defaults = array(
'enabled' => true,
'opt_in_date' => time(),
'disabled_count' => 0,
'heartbeat_interval' => 48,
'schedule' => 'frequent',
'frequent_started_at' => time(),
'last_heartbeat' => 0,
'heartbeat_count' => 0,
'failed_count' => 0,
'last_payload_hash' => '',
'last_error' => '',
);
foreach ( $defaults as $key => $value ) {
Cmatic_Options_Repository::set_option( "telemetry.{$key}", $value );
}
}
public static function is_enabled() {
return (bool) Cmatic_Options_Repository::get_option( 'telemetry.enabled', true );
}
public static function get_schedule() {
return Cmatic_Options_Repository::get_option( 'telemetry.schedule', 'frequent' );
}
public static function set_schedule( $schedule ) {
Cmatic_Options_Repository::set_option( 'telemetry.schedule', $schedule );
if ( 'frequent' === $schedule ) {
Cmatic_Options_Repository::set_option( 'telemetry.frequent_started_at', time() );
}
}
public static function get_heartbeat_interval() {
return (int) Cmatic_Options_Repository::get_option( 'telemetry.heartbeat_interval', 48 );
}
public static function get_last_heartbeat() {
return (int) Cmatic_Options_Repository::get_option( 'telemetry.last_heartbeat', 0 );
}
public static function update_last_heartbeat( $timestamp = null ) {
if ( null === $timestamp ) {
$timestamp = time();
}
Cmatic_Options_Repository::set_option( 'telemetry.last_heartbeat', $timestamp );
}
public static function increment_heartbeat_count() {
$count = (int) Cmatic_Options_Repository::get_option( 'telemetry.heartbeat_count', 0 );
Cmatic_Options_Repository::set_option( 'telemetry.heartbeat_count', $count + 1 );
}
public static function increment_failed_count() {
$count = (int) Cmatic_Options_Repository::get_option( 'telemetry.failed_count', 0 );
Cmatic_Options_Repository::set_option( 'telemetry.failed_count', $count + 1 );
}
public static function increment_disabled_count() {
$count = (int) Cmatic_Options_Repository::get_option( 'telemetry.disabled_count', 0 );
Cmatic_Options_Repository::set_option( 'telemetry.disabled_count', $count + 1 );
}
public static function get_frequent_started_at() {
return (int) Cmatic_Options_Repository::get_option( 'telemetry.frequent_started_at', 0 );
}
public static function is_frequent_elapsed() {
$started_at = self::get_frequent_started_at();
if ( 0 === $started_at ) {
return false;
}
$elapsed = time() - $started_at;
return $elapsed >= ( 1 * HOUR_IN_SECONDS );
}
public static function record_activation() {
}
public static function record_deactivation() {
}
public static function is_reactivation() {
return (bool) Cmatic_Options_Repository::get_option( 'lifecycle.is_reactivation', false );
}
public static function get_activation_count() {
$activations = Cmatic_Options_Repository::get_option( 'lifecycle.activations', array() );
return is_array( $activations ) ? count( $activations ) : 0;
}
public static function get_deactivation_count() {
$deactivations = Cmatic_Options_Repository::get_option( 'lifecycle.deactivations', array() );
return is_array( $deactivations ) ? count( $deactivations ) : 0;
}
public static function get_install_id(): string {
$install_id = \Cmatic_Options_Repository::get_option( 'install.id', '' );
if ( ! empty( $install_id ) ) {
return $install_id;
}
$install_data = new \Cmatic_Install_Data( \Cmatic_Options_Repository::instance() );
return $install_data->get_install_id();
}
public static function get_quest(): int {
$quest = (int) \Cmatic_Options_Repository::get_option( 'install.quest', 0 );
if ( $quest >= \Cmatic_Install_Data::MIN_VALID_TIMESTAMP ) {
return $quest;
}
$install_data = new \Cmatic_Install_Data( \Cmatic_Options_Repository::instance() );
return $install_data->get_quest();
}
public static function save_error( $error ): void {
Cmatic_Options_Repository::set_option( 'telemetry.last_error', $error );
}
public static function save_payload_hash( $hash ) {
Cmatic_Options_Repository::set_option( 'telemetry.last_payload_hash', $hash );
}
}

View File

@@ -0,0 +1,182 @@
<?php
/**
* Metrics sync handler.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core;
use Cmatic\Metrics\Security\Signature;
defined( 'ABSPATH' ) || exit;
class Sync {
private static $endpoint_url = '';
public static function set_endpoint( $url ) {
self::$endpoint_url = $url;
}
public static function send( $payload ) {
try {
if ( empty( self::$endpoint_url ) ) {
return false;
}
$request = self::prepare_request( $payload );
$response = wp_remote_post(
self::$endpoint_url,
array(
'body' => wp_json_encode( $request ),
'headers' => array( 'Content-Type' => 'application/json' ),
'timeout' => 5,
)
);
return self::handle_response( $response, $payload );
} catch ( \Exception $e ) {
return false;
}
}
public static function send_async( $payload ) {
try {
if ( empty( self::$endpoint_url ) ) {
return;
}
$request = self::prepare_request( $payload );
wp_remote_post(
self::$endpoint_url,
array(
'body' => wp_json_encode( $request ),
'headers' => array( 'Content-Type' => 'application/json' ),
'timeout' => 5,
'blocking' => false,
)
);
Storage::update_last_heartbeat();
} catch ( \Exception $e ) {
return;
}
}
private static function prepare_request( $payload ) {
$install_id = Storage::get_install_id();
$timestamp = time();
$payload_json = wp_json_encode( $payload );
$signature = Signature::generate( $install_id, $timestamp, $payload_json );
return array(
'install_id' => $install_id,
'timestamp' => $timestamp,
'signature' => $signature,
'public_key' => Signature::get_public_key(),
'payload_json' => $payload_json,
);
}
private static function handle_response( $response, $payload ) {
if ( is_wp_error( $response ) ) {
self::handle_failure( $response->get_error_message(), $payload );
return false;
}
$code = wp_remote_retrieve_response_code( $response );
if ( 200 !== $code && 201 !== $code ) {
$body = wp_remote_retrieve_body( $response );
self::handle_failure( "HTTP {$code}: {$body}", $payload );
return false;
}
self::handle_success( $payload );
return true;
}
private static function handle_success( $payload ) {
Storage::update_last_heartbeat();
$payload_hash = md5( wp_json_encode( $payload ) );
Storage::save_payload_hash( $payload_hash );
Storage::save_error( '' );
if ( 'sparse' === Storage::get_schedule() ) {
Scheduler::schedule_next_sparse();
}
}
private static function handle_failure( $error, $payload ) {
Storage::update_last_heartbeat();
Storage::increment_failed_count();
Storage::save_error( $error );
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( "[ChimpMatic Metrics] Heartbeat failed: {$error}" ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
}
if ( 'sparse' === Storage::get_schedule() ) {
Scheduler::schedule_next_sparse();
}
}
public static function send_lifecycle_signal( $event ) {
$options = get_option( 'cmatic', array() );
$default_enabled = 'activation' === $event;
$telemetry_enabled = $options['telemetry']['enabled'] ?? $default_enabled;
if ( ! $telemetry_enabled ) {
return;
}
$install_id = $options['install']['id'] ?? '';
if ( empty( $install_id ) ) {
return;
}
global $wpdb;
$payload = array(
'event' => $event,
'install_id' => $install_id,
'version' => defined( 'SPARTAN_MCE_VERSION' ) ? SPARTAN_MCE_VERSION : '1.0.0',
'site_url' => home_url(),
'timestamp' => time(),
'wp_version' => get_bloginfo( 'version' ),
'php' => PHP_VERSION,
'mysql_version' => $wpdb->db_version(),
'software' => array(
'server' => isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : 'unknown', // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
),
);
$timestamp = time();
$payload_json = wp_json_encode( $payload );
$signature = Signature::generate( $install_id, $timestamp, $payload_json );
$request = array(
'install_id' => $install_id,
'timestamp' => $timestamp,
'signature' => $signature,
'public_key' => Signature::get_public_key(),
'payload_json' => $payload_json,
);
wp_remote_post(
'https://signls.dev/wp-json/chimpmatic/v1/telemetry',
array(
'body' => wp_json_encode( $request ),
'headers' => array( 'Content-Type' => 'application/json' ),
'timeout' => 5,
'blocking' => true,
)
);
}
}

View File

@@ -0,0 +1,82 @@
<?php
/**
* Metrics event tracker.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core;
use Cmatic_Options_Repository;
defined( 'ABSPATH' ) || exit;
class Tracker {
public static function init() {
add_action( 'cmatic_metrics_on_activation', array( __CLASS__, 'on_activation' ) );
add_action( 'cmatic_metrics_on_deactivation', array( __CLASS__, 'on_deactivation' ) );
}
public static function on_activation() {
Storage::init();
Storage::record_activation();
Storage::set_schedule( 'super_frequent' );
if ( ! wp_next_scheduled( 'cmatic_metrics_heartbeat' ) ) {
wp_schedule_event( time() + ( 2 * MINUTE_IN_SECONDS ), 'cmatic_2min', 'cmatic_metrics_heartbeat' );
}
self::send_event_heartbeat( 'activation' );
}
public static function on_deactivation() {
Storage::record_deactivation();
$timestamp = wp_next_scheduled( 'cmatic_metrics_heartbeat' );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, 'cmatic_metrics_heartbeat' );
}
self::send_event_heartbeat( 'deactivation', true );
}
private static function send_event_heartbeat( $event, $force = false ) {
if ( ! $force && ! Storage::is_enabled() ) {
return;
}
$payload = Collector::collect( $event );
Sync::send_async( $payload );
}
public static function on_opt_out() {
Storage::increment_disabled_count();
$payload = Collector::collect( 'opt_out' );
Sync::send_async( $payload );
$timestamp = wp_next_scheduled( 'cmatic_metrics_heartbeat' );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, 'cmatic_metrics_heartbeat' );
}
}
public static function on_re_enable() {
if ( ! Cmatic_Options_Repository::get_option( 'telemetry.opt_in_date' ) ) {
Cmatic_Options_Repository::set_option( 'telemetry.opt_in_date', time() );
}
Storage::set_schedule( 'super_frequent' );
if ( ! wp_next_scheduled( 'cmatic_metrics_heartbeat' ) ) {
wp_schedule_event( time() + ( 2 * MINUTE_IN_SECONDS ), 'cmatic_2min', 'cmatic_metrics_heartbeat' );
}
$payload = Collector::collect( 'reactivation' );
Sync::send_async( $payload );
}
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* Cryptographic signature handler.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Security;
defined( 'ABSPATH' ) || exit;
class Signature {
const PUBLIC_KEY = 'chimpmatic_lite_v1';
public static function generate( $install_id, $timestamp, $payload_json ) {
$derived_secret = self::derive_secret( $install_id );
$string_to_sign = $install_id . $timestamp . $payload_json;
return hash_hmac( 'sha256', $string_to_sign, $derived_secret );
}
public static function derive_secret( $install_id ) {
return hash( 'sha256', $install_id . self::PUBLIC_KEY );
}
public static function validate( $signature, $install_id, $timestamp, $payload_json ) {
$expected = self::generate( $install_id, $timestamp, $payload_json );
return hash_equals( $expected, $signature );
}
public static function get_public_key() {
return self::PUBLIC_KEY;
}
}

View File

@@ -0,0 +1,13 @@
<?php
defined( 'ABSPATH' ) || exit;
spl_autoload_register( function ( $class ) {
$prefix = 'Cmatic\\Metrics\\';
if ( strncmp( $prefix, $class, strlen( $prefix ) ) !== 0 ) {
return;
}
$file = __DIR__ . '/' . str_replace( '\\', '/', substr( $class, strlen( $prefix ) ) ) . '.php';
if ( file_exists( $file ) ) {
require $file;
}
} );