/home/egir5919/public_html/wp-content/plugins/surerank/inc/analytics/analytics.php
<?php
/**
* Analytics class helps to connect BSFAnalytics.
*
* @package surerank.
*/
namespace SureRank\Inc\Analytics;
use SureRank\Inc\Functions\Defaults;
use SureRank\Inc\Functions\Settings;
use SureRank\Inc\GoogleSearchConsole\Controller;
use SureRank\Inc\Traits\Get_Instance;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Analytics class.
*
* @since 1.4.0
*/
class Analytics {
use Get_Instance;
/**
* Events tracker instance.
*
* @var \BSF_Analytics_Events|null
*/
private static $events = null;
/**
* Class constructor.
*
* @return void
* @since 1.4.0
*/
public function __construct() {
// Stats payload filter.
add_filter( 'bsf_core_stats', [ $this, 'add_surerank_analytics_data' ] );
// Only run analytics in admin context.
if ( ! is_admin() ) {
return;
}
if ( ! class_exists( 'BSF_Admin_Notices' ) ) {
require_once SURERANK_DIR . 'inc/lib/astra-notices/class-bsf-admin-notices.php';
}
add_filter(
'uds_survey_allowed_screens',
static function () {
return [ 'plugins' ];
}
);
/*
* BSF Analytics.
*/
if ( ! class_exists( 'BSF_Analytics_Loader' ) ) {
require_once SURERANK_DIR . 'inc/lib/bsf-analytics/class-bsf-analytics-loader.php';
}
if ( ! class_exists( 'BSF_Analytics_Loader' ) ) {
return;
}
$surerank_bsf_analytics = \BSF_Analytics_Loader::get_instance();
$surerank_bsf_analytics->set_entity(
[
'surerank' => [
'product_name' => 'SureRank',
'path' => SURERANK_DIR . 'inc/lib/bsf-analytics',
'author' => 'SureRank',
'time_to_display' => '+24 hours',
'deactivation_survey' => apply_filters(
'surerank_deactivation_survey_data',
[
[
'id' => 'deactivation-survey-surerank',
'popup_logo' => SURERANK_URL . 'inc/admin/assets/images/surerank.png',
'plugin_slug' => 'surerank',
'popup_title' => 'Quick Feedback',
'support_url' => 'https://surerank.com/contact/',
'popup_description' => 'If you have a moment, please share why you are deactivating SureRank:',
'show_on_screens' => [ 'plugins' ],
'plugin_version' => SURERANK_VERSION,
],
]
),
'hide_optin_checkbox' => true,
],
]
);
// Plugin version change detection — must run before throttle gate so updates are never missed between daily checks.
$stored_version = get_option( 'surerank_tracked_version', '' );
if ( ! empty( $stored_version ) && SURERANK_VERSION !== $stored_version ) {
delete_transient( 'surerank_state_events_checked' );
}
// State-based events — throttled to once per day.
// Transient is set inside detect_state_events() only after confirming BSF_Analytics_Events class is loaded, so it retries on next load if not ready.
if ( false === get_transient( 'surerank_state_events_checked' ) ) {
$this->detect_state_events();
}
}
/**
* Get shared event tracker instance.
*
* @return \BSF_Analytics_Events|null
* @since 1.7.0
*/
public static function events() {
if ( null === self::$events ) {
if ( ! class_exists( 'BSF_Analytics_Events' ) ) {
return null;
}
self::$events = new \BSF_Analytics_Events( 'surerank' );
}
return self::$events;
}
/**
* Callback function to add SureRank specific analytics data.
*
* @param array<string, mixed> $stats_data existing stats_data.
* @since 1.4.0
* @return array<string, mixed>
*/
public function add_surerank_analytics_data( $stats_data ) {
$events = self::events();
$stats_data['plugin_data']['surerank'] = [
'plugin_version' => SURERANK_VERSION,
'site_language' => get_locale(),
// One-time events (flushed from pending queue).
'events_record' => $events ? $events->flush_pending() : [],
// Daily KPIs (last 2 days).
'kpi_records' => $this->get_kpi_tracking_data(),
];
return $stats_data;
}
/**
* Compare top-level and one-level nested settings with defaults.
*
* @param array<string, mixed> $settings Current settings.
* @param array<string, mixed> $defaults Default settings.
* @return array<string, mixed> Changed settings (top-level + one-level deep).
*/
public static function shallow_two_level_diff( array $settings, array $defaults ) {
$difference = [];
if ( isset( $defaults['surerank_usage_optin'] ) ) {
unset( $defaults['surerank_usage_optin'] );
}
foreach ( $settings as $key => $value ) {
// Key missing in defaults = changed.
if ( ! array_key_exists( $key, $defaults ) ) {
$difference[ $key ] = $value;
continue;
}
// If value is an array, only check one level deep.
if ( is_array( $value ) && is_array( $defaults[ $key ] ) ) {
$nested_diff = [];
foreach ( $value as $sub_key => $sub_value ) {
if ( ! array_key_exists( $sub_key, $defaults[ $key ] ) || $sub_value !== $defaults[ $key ][ $sub_key ] ) {
$nested_diff[ $sub_key ] = $sub_value;
}
}
if ( ! empty( $nested_diff ) ) {
$difference[ $key ] = $nested_diff;
}
} elseif ( $value !== $defaults[ $key ] ) {
// Compare scalar values directly.
$difference[ $key ] = $value;
}
}
return $difference;
}
/**
* Detect state-based events.
*
* Checks conditions on admin load. BSF_Analytics_Events dedup prevents duplicates.
* Throttled via transient so this only runs once per day.
*
* @return void
* @since 1.7.0
*/
private function detect_state_events() {
$events = self::events();
if ( null === $events ) {
// BSF_Analytics_Events class not loaded yet — do NOT set transient, so this retries on the next admin page load.
return;
}
// Class is available — set throttle transient so we don't re-run for 24h.
set_transient( 'surerank_state_events_checked', 1, DAY_IN_SECONDS );
// One-time dedup flush so corrected events re-fire with proper values.
$fix_key = 'surerank_events_value_fix_v1';
if ( ! get_option( $fix_key, false ) ) {
$events->flush_pushed(
[
'onboarding_completed',
'onboarding_skipped',
'pro_license_activated',
'gsc_connected',
'migration_completed',
'first_ai_content_generated',
'first_schema_added',
'first_redirect_created',
'first_bulk_action_used',
'first_link_scan_completed',
]
);
update_option( $fix_key, true );
}
// Plugin activated.
$bsf_referrers = get_option( 'bsf_product_referers', [] );
$source = ! empty( $bsf_referrers['surerank'] )
? sanitize_text_field( $bsf_referrers['surerank'] )
: 'self';
$events->track( 'plugin_activated', SURERANK_VERSION, [ 'source' => $source ] );
// Plugin updated (version change detection).
$stored_version = get_option( 'surerank_tracked_version', '' );
if ( SURERANK_VERSION !== $stored_version ) {
if ( ! empty( $stored_version ) ) {
$events->flush_pushed( [ 'plugin_updated' ] );
$events->track(
'plugin_updated',
SURERANK_VERSION,
[
'from_version' => $stored_version,
]
);
}
update_option( 'surerank_tracked_version', SURERANK_VERSION );
}
// Onboarding: track skip and completion as separate events.
$settings = Settings::get();
$website_type = $settings['website_type'] ?? [];
$onboarding_done = ! empty( $website_type );
$onboarding_skipped = (bool) get_option( 'surerank_onboarding_skipped', false );
if ( $onboarding_skipped ) {
$events->track( 'onboarding_skipped', 'yes' );
}
if ( $onboarding_done ) {
$events->flush_pushed( [ 'onboarding_completed' ] );
$events->track(
'onboarding_completed',
'completed',
[
'previously_skipped' => (string) (int) $onboarding_skipped,
]
);
}
// First post optimized (activation event).
if ( $this->is_active() ) {
$install_time = get_option( 'surerank_usage_installed_time', 0 );
$days = 0;
if ( $install_time > 0 ) {
$days = (int) floor( ( time() - $install_time ) / DAY_IN_SECONDS );
}
$events->track(
'first_post_optimized',
'',
[
'days_since_install' => (string) $days,
]
);
}
// Google Search Console connected.
if ( $this->get_gsc_connected() ) {
$events->track( 'gsc_connected', 'yes' );
}
// Pro license activated.
if ( defined( 'SURERANK_PRO_VERSION' ) && 'licensed' === get_option( 'surerank_pro_license_status', 'unlicensed' ) ) {
$events->track( 'pro_license_activated', 'licensed' );
}
// Migration completed (check for migration option).
$migration_done = get_option( 'surerank_migration_completed', '' );
if ( ! empty( $migration_done ) ) {
$events->track(
'migration_completed',
sanitize_text_field( $migration_done ),
[
'source' => sanitize_text_field( $migration_done ),
]
);
}
// First AI content generated (Pro feature).
if ( defined( 'SURERANK_PRO_VERSION' ) ) {
$ai_used = get_option( 'surerank_ai_content_used', false );
if ( $ai_used ) {
$events->track( 'first_ai_content_generated', 'yes' );
}
}
// First schema added (site-wide or page-specific).
if ( $this->has_schema_usage() ) {
$events->track( 'first_schema_added', 'yes' );
}
// First redirect created (Pro feature).
if ( defined( 'SURERANK_PRO_VERSION' ) && 'licensed' === get_option( 'surerank_pro_license_status', 'unlicensed' ) ) {
$redirect_count = $this->get_redirect_count();
if ( $redirect_count > 0 ) {
$events->track( 'first_redirect_created', 'yes' );
}
}
// First bulk action used.
$bulk_used = get_option( 'surerank_bulk_action_used', false );
if ( $bulk_used ) {
$events->track( 'first_bulk_action_used', 'yes' );
}
}
/**
* Check if schemas are actually in use (site-wide or page-specific).
*
* Checks for explicitly saved global schema settings or any
* page-specific schemas added via Dashboard > Advanced > Schema.
*
* @return bool
* @since 1.7.1
*/
private function has_schema_usage() {
// Check if global schemas have been explicitly saved in settings.
$raw_settings = get_option( SURERANK_SETTINGS, [] );
if ( is_array( $raw_settings ) && ! empty( $raw_settings['schemas'] ) ) {
return true;
}
// Check if any post has page-specific schemas.
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$has_post_schemas = $wpdb->get_var(
$wpdb->prepare(
"SELECT 1 FROM {$wpdb->postmeta} WHERE meta_key = %s LIMIT 1",
'surerank_settings_schemas'
)
);
return ! empty( $has_post_schemas );
}
/**
* Get redirect count (Pro feature).
*
* Redirects are stored in the 'surerank_redirections' option as an array.
*
* @return int
* @since 1.7.0
*/
private function get_redirect_count() {
$redirects = get_option( 'surerank_redirections', [] );
return is_array( $redirects ) ? count( $redirects ) : 0;
}
/**
* Get Google Search Console connected status.
*
* @return bool
*/
private function get_gsc_connected() {
return Controller::get_instance()->get_auth_status();
}
/**
* Check if SureRank is active (has settings different from defaults).
*
* @return bool
* @since 1.5.0
*/
private function is_active() {
$cached = get_transient( 'surerank_analytics_is_active' );
if ( false !== $cached ) {
return 'yes' === $cached;
}
$surerank_defaults = Defaults::get_instance()->get_global_defaults();
$surerank_settings = get_option( SURERANK_SETTINGS, [] );
if ( is_array( $surerank_settings ) && is_array( $surerank_defaults ) ) {
$changed_settings = self::shallow_two_level_diff( $surerank_settings, $surerank_defaults );
if ( count( $changed_settings ) >= 1 ) {
set_transient( 'surerank_analytics_is_active', 'yes', DAY_IN_SECONDS );
return true;
}
}
global $wpdb;
$posts_like = $wpdb->esc_like( 'surerank_settings_' ) . '%';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$posts = $wpdb->get_col(
$wpdb->prepare(
"
SELECT DISTINCT pm.post_id
FROM {$wpdb->postmeta} pm
INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID
WHERE pm.meta_key LIKE %s
AND p.post_status = 'publish'
LIMIT 1
",
$posts_like
)
);
// Check if any terms have been optimized.
$terms_like = $wpdb->esc_like( 'surerank_seo_checks' ) . '%';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$terms = $wpdb->get_col(
$wpdb->prepare(
"
SELECT DISTINCT tm.term_id
FROM {$wpdb->termmeta} tm
INNER JOIN {$wpdb->terms} t ON tm.term_id = t.term_id
INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
WHERE tm.meta_key LIKE %s
LIMIT 1
",
$terms_like
)
);
$is_active = ( ! empty( $posts ) && is_array( $posts ) ) || ( ! empty( $terms ) && is_array( $terms ) );
set_transient( 'surerank_analytics_is_active', $is_active ? 'yes' : 'no', DAY_IN_SECONDS );
return $is_active;
}
/**
* Get public post types for database queries.
*
* @return array<string>
* @since 1.6.3
*/
private function get_public_post_types_for_query() {
$post_types = get_post_types( [ 'public' => true ], 'names' );
$excluded = [ 'attachment', 'revision' ];
return array_values( array_diff( $post_types, $excluded ) );
}
/**
* Get optimized posts count for a specific date.
*
* @param string $date Date in Y-m-d format.
* @since 1.6.3
* @return int
*/
private function get_optimized_posts_count_within_date( $date ) {
global $wpdb;
$start_timestamp = strtotime( $date . ' 00:00:00' );
$end_timestamp = strtotime( $date . ' 23:59:59' );
$public_post_types = $this->get_public_post_types_for_query();
if ( empty( $public_post_types ) ) {
$post_count = 0;
} else {
$placeholders = implode( ', ', array_fill( 0, count( $public_post_types ), '%s' ) );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$post_count = $wpdb->get_var(
// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
$wpdb->prepare(
"SELECT COUNT(DISTINCT pm.post_id)
FROM {$wpdb->postmeta} pm
INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID
WHERE pm.meta_key = 'surerank_post_optimized_at'
AND CAST(pm.meta_value AS UNSIGNED) >= %d
AND CAST(pm.meta_value AS UNSIGNED) <= %d
AND p.post_status = 'publish'
AND p.post_type IN ({$placeholders})", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
array_merge( [ $start_timestamp, $end_timestamp ], $public_post_types )
)
);
}
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$term_count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(DISTINCT tm.term_id)
FROM {$wpdb->termmeta} tm
INNER JOIN {$wpdb->terms} t ON tm.term_id = t.term_id
INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
WHERE tm.meta_key = 'surerank_term_optimized_at'
AND CAST(tm.meta_value AS UNSIGNED) >= %d
AND CAST(tm.meta_value AS UNSIGNED) <= %d",
$start_timestamp,
$end_timestamp
)
);
return absint( $post_count ) + absint( $term_count );
}
/**
* Get KPI tracking data for the last 2 days.
*
* @since 1.6.3
* @return array<string, array<string, array<string, int>>>
*/
private function get_kpi_tracking_data() {
$kpi_data = [];
$today = current_time( 'Y-m-d' );
for ( $i = 1; $i <= 2; $i++ ) {
$timestamp = strtotime( $today . ' -' . $i . ' days' );
if ( false === $timestamp ) {
continue;
}
$date = (string) wp_date( 'Y-m-d', $timestamp );
$optimized_count = $this->get_optimized_posts_count_within_date( $date );
$kpi_data[ $date ] = [
'numeric_values' => [
'optimized_posts' => $optimized_count,
],
];
}
return $kpi_data;
}
}