<?php /** * Astra Sites Analytics * * @since 4.4.27 * @package Astra Sites */ if ( ! defined( 'ABSPATH' ) ) { exit; } if ( ! class_exists( 'Astra_Sites_Analytics' ) ) { /** * Astra_Sites_Analytics */ class Astra_Sites_Analytics { /** * Instance of Astra_Sites_Analytics * * @since 4.4.27 * @var self Astra_Sites_Analytics */ private static $instance = null; /** * Instance of Astra_Sites_Analytics. * * @since 4.4.27 * * @return self Class object. */ public static function get_instance() { if ( ! isset( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } /** * Constructor. * * @since 4.4.27 */ private function __construct() { add_action( 'astra_sites_after_plugin_activation', array( $this, 'update_settings_after_plugin_activation' ), 10, 2 ); add_action( 'admin_init', array( $this, 'maybe_update_finish_setup_banner_clicked' ) ); add_action( 'wp_ajax_astra_sites_set_woopayments_analytics', array( $this, 'set_woopayments_analytics' ) ); add_filter( 'bsf_core_stats', array( $this, 'add_astra_sites_analytics_data' ), 10, 1 ); } /** * Update settings after plugin activation. * * @param string $plugin_init The plugin initialization path. * @param array $data Additional data (optional). * * @since 4.4.27 * @return void */ public function update_settings_after_plugin_activation( $plugin_init, $data = array() ) { // Bail if the plugin slug is not set or empty. if ( ! isset( $data['plugin_slug'] ) || '' === $data['plugin_slug'] ) { return; } // Set WooPayments related settings. $this->maybe_woopayments_included( $plugin_init, $data ); $plugin_slug = $data['plugin_slug']; $required_plugins = Astra_Sites_Page::get_instance()->get_setting( 'required_plugins', array() ); // If the plugin is already activated by starter templates, return early. if ( ( isset( $required_plugins[ $plugin_slug ] ) && 'activated' === $required_plugins[ $plugin_slug ] ) ) { return; } // If required plugins is not an array, initialize it. if ( ! is_array( $required_plugins ) ) { $required_plugins = array(); } // Set the plugin activation status and update in settings. $required_plugins[ $plugin_slug ] = isset( $data['was_plugin_active'] ) && $data['was_plugin_active'] ? 'was_active' : 'activated'; Astra_Sites_Page::get_instance()->update_settings( array( 'required_plugins' => $required_plugins, ) ); } /** * Maybe update finish setup banner clicked. * * @since 4.4.37 * @return void */ public function maybe_update_finish_setup_banner_clicked() { // Bail early if the source is not dashboard-banner. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification not needed here. if ( ! isset( $_GET['source'] ) || 'dashboard-banner' !== sanitize_text_field( $_GET['source'] ) ) { return; } // Update the finish setup banner clicked setting. Astra_Sites_Page::get_instance()->update_settings( array( 'fs_banner_clicked' => 'yes', ) ); } /** * Check if WooCommerce Payments plugin is included and update settings accordingly. * * @param string $plugin_init The plugin initialization path. * @param array $data Additional data (optional). * * @since 4.4.23 * @return void */ public function maybe_woopayments_included( $plugin_init, $data = array() ) { if ( 'woocommerce-payments/woocommerce-payments.php' === $plugin_init ) { // Prevent showing the banner if plugin was already active. if ( ! isset( $data['was_plugin_active'] ) || ! $data['was_plugin_active'] ) { Astra_Sites_Page::get_instance()->update_settings( array( 'woopayments_ref' => true, 'wcpay_referred_time' => time(), ) ); } Astra_Sites_Page::get_instance()->update_settings( array( 'woopayments_included' => true, ) ); } } /** * Set WooPayments analytics. * * @since 4.4.23 * @return void */ public function set_woopayments_analytics() { // Verify nonce. if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'woopayments_nonce' ) ) { wp_send_json_error( array( 'message' => __( 'Invalid nonce', 'astra-sites' ) ) ); exit; } $source = isset( $_POST['source'] ) ? sanitize_text_field( wp_unslash( $_POST['source'] ) ) : ''; if ( ! in_array( $source, array( 'banner', 'onboarding' ), true ) ) { wp_send_json_error( array( 'message' => __( 'Invalid source', 'astra-sites' ) ) ); exit; } $key = "woopayments_{$source}_clicked"; Astra_Sites_Page::get_instance()->update_settings( array( $key => true ) ); wp_send_json_success( array( 'message' => 'WooPayments analytics updated!' ) ); exit; } /** * Check if WooPayments is configured and connected to Stripe. * * @since 4.4.24 * @return bool True if WooPayments is active and connected to Stripe, false otherwise. */ public static function is_woo_payments_configured() { // Check if WCPay account is connected to Stripe. if ( class_exists( 'WC_Payments' ) && method_exists( 'WC_Payments', 'get_account_service' ) ) { $account_service = WC_Payments::get_account_service(); if ( method_exists( $account_service, 'is_stripe_connected' ) ) { return $account_service->is_stripe_connected(); } } return false; } /** * Checks if WooPayments has processed at least one transaction. * * @since 4.4.34 * @return bool True if WooPayments has transactions, false otherwise. */ private static function has_woopayments_successful_transaction() { // Check if WooPayments plugin is active. if ( ! is_plugin_active( 'woocommerce-payments/woocommerce-payments.php' ) ) { return false; } // Check if WooCommerce is active (required for WooPayments). if ( ! is_plugin_active( 'woocommerce/woocommerce.php' ) || ! function_exists( 'wc_get_orders' ) ) { return false; } // Get the timestamp after which to check for transactions. $after_timestamp = Astra_Sites_Page::get_instance()->get_setting( 'wcpay_referred_time', 0 ); // Use WooCommerce's built-in order query system to support both legacy and HPOS storage. $orders = wc_get_orders( array( 'payment_method' => 'woocommerce_payments', 'status' => array( 'completed', 'processing', 'on-hold' ), 'limit' => 1, 'date_after' => intval( $after_timestamp ), 'meta_query' => array( //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Required for analytics and run in background. array( 'key' => '_wcpay_payment_transaction_id', 'value' => '', 'compare' => '!=', ), ), ) ); return ! empty( $orders ); } /** * Get all posts that have been imported and not modified since import. * * These are posts that were imported via Starter Templates and haven't been * edited by the user since import (within a 3-minute buffer for background processing). * * @param string $post_type The post type to check. Default is 'any'. * * @since 4.4.33 * @return array Post IDs that have not been modified since their import. */ private static function get_unmodified_imported_post_ids( $post_type = 'any' ) { // Get all posts that have been imported. $args = array( 'post_type' => $post_type, 'post_status' => 'publish', 'posts_per_page' => -1, 'meta_query' => array( //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Required for analytics and run in background. array( 'key' => '_astra_sites_imported_post', 'value' => '1', 'compare' => '=', ), ), ); $posts = get_posts( $args ); // Get all post IDs that have not been modified since their import. return array_reduce( $posts, function ( $carry, $post ) { $post_date = strtotime( $post->post_date ); $modified_date = strtotime( $post->post_modified ); // Check if modified time is within 3 minutes (180 seconds) of post date. 3 mins buffer for background post processing. if ( abs( $modified_date - $post_date ) <= 180 ) { $carry[] = $post->ID; } return $carry; }, array() ); } /** * Check if any user-engaged posts exist for a given post type. * * "User-engaged" means either: * - Posts created by the user (not imported). * - Imported posts that have been modified by the user since import. * * @param string $post_type The post type to check. * @param array $args Optional arguments for WP_Query. * * @since 4.4.33 * @return bool True if any qualifying posts exist, false otherwise. */ private static function has_user_engaged_posts_of_type( $post_type = 'any', $args = array() ) { // If post type is not 'any', ensure it exists. if ( 'any' !== $post_type && ! post_type_exists( $post_type ) ) { return false; } // Merge default arguments with provided ones. $args = array_merge( array( 'post_type' => $post_type, 'post_status' => 'publish', 'fields' => 'ids', 'numberposts' => 1, ), $args ); // Exclude post__not_in if already set. if ( ! isset( $args['post__not_in'] ) ) { // Exclude posts that have been imported and not modified since import. $excluded_post_ids = self::get_unmodified_imported_post_ids( $post_type ); if ( ! empty( $excluded_post_ids ) ) { $args['post__not_in'] = $excluded_post_ids; } } $query = new \WP_Query( $args ); $found = $query->have_posts(); wp_reset_postdata(); return $found; } /** * Checks if any Spectra block is used on the site. * * ACTIVE CONDITION: * - Imported post/page is updated, published and has at least one Spectra block. * - New post/page is published and has at least one Spectra block. * * @since 4.4.27 * * @return bool */ public static function is_spectra_blocks_used() { if ( ! is_plugin_active( 'ultimate-addons-for-gutenberg/ultimate-addons-for-gutenberg.php' ) ) { return false; } // Check for Spectra v2 blocks (wp:uagb/). $has_spectra_v2_blocks = self::has_user_engaged_posts_of_type( 'any', array( 's' => '<!-- wp:uagb/' ) ); if ( $has_spectra_v2_blocks ) { return true; } // Check for Spectra v3 blocks (wp:spectra/). return self::has_user_engaged_posts_of_type( 'any', array( 's' => '<!-- wp:spectra/' ) ); } /** * Checks if any UAE Header Footer Layout is published or any UAE widget is used on the site. * * ACTIVE CONDITION: * - Imported header footer post is updated, published. * - New header footer post is published. * * @since 4.4.27 * * @return bool */ public static function is_uae_widgets_used() { if ( ! is_plugin_active( 'header-footer-elementor/header-footer-elementor.php' ) ) { return false; } // Check for user-engaged elementor-hf posts. if ( self::has_user_engaged_posts_of_type( 'elementor-hf' ) ) { return true; } // Check UAE widget usage data (fallback for older detection method). $uae_used_widgets = get_option( 'uae_widgets_usage_data_option', array() ); if ( is_array( $uae_used_widgets ) && ! empty( $uae_used_widgets ) ) { return true; } return false; } /** * Checks if any SureForms form is published. * * ACTIVE CONDITION: * - Imported form is updated and published. * - New form is published. * * @since 4.4.27 * * @return bool */ public static function is_sureforms_form_published() { return self::has_user_engaged_posts_of_type( 'sureforms_form' ); } /** * Checks if SureMail has at least one connection configured. * * ACTIVE CONDITION: SureMails is connected. * * @since 4.4.27 * * @return bool */ public static function is_suremails_connected() { if ( ! is_plugin_active( 'suremails/suremails.php' ) ) { return false; } // Get SureMails connections from options. $suremails_connections_option = defined( 'SUREMAILS_CONNECTIONS' ) ? SUREMAILS_CONNECTIONS : 'suremails_connections'; $suremails_connections = get_option( $suremails_connections_option, array() ); if ( is_array( $suremails_connections ) && isset( $suremails_connections['connections'] ) && ! empty( $suremails_connections['connections'] ) ) { return true; } return false; } /** * Checks if SureCart has any published product. * * ACTIVE CONDITION: * - Imported SureCart product is updated and published. * - New SureCart product is published. * * @since 4.4.27 * * @return bool */ public static function is_surecart_product_published() { return self::has_user_engaged_posts_of_type( 'sc_product' ); } /** * Checks if CartFlows has any published funnel. * * ACTIVE CONDITION: * - Imported funnel is updated and published. * - New funnel is published. * * @since 4.4.27 * * @return bool */ public static function is_cartflows_funnel_published() { return self::has_user_engaged_posts_of_type( 'cartflows_flow' ); } /** * Checks if LatePoint activities are made by admin or appointment created by users. * * ACTIVE CONDITION: Any activity is made by admin or appointment created by users. * * @since 4.4.27 * * @return bool */ public static function is_latepoint_booking_managed() { if ( ! is_plugin_active( 'latepoint/latepoint.php' ) ) { return false; } global $wpdb; $latepoint_activities_table = defined( 'LATEPOINT_TABLE_ACTIVITIES' ) ? LATEPOINT_TABLE_ACTIVITIES : $wpdb->prefix . 'latepoint_activities'; // Table names can't be parameterized in wpdb::prepare(), safe to ignore PHPCS here. // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $count = $wpdb->get_var( "SELECT COUNT(*) FROM {$latepoint_activities_table}" ); return $count > 0; } /** * Checks if a Presto Player video is embedded on the site. * * ACTIVE CONDITION: Presto Player block/shortcode used on any post/page * * @since 4.4.27 * * @return bool */ public static function is_presto_player_used() { if ( ! is_plugin_active( 'presto-player/presto-player.php' ) ) { return false; } // Exclude 'pp_video_block' post type to avoid false positives. $args = array( 'post_type' => array_diff( get_post_types( array( 'public' => true ), 'names' ), array( 'pp_video_block' ) ), 'posts__not_in' => array(), // To search in all posts including imported ones. 's' => '<!-- /wp:presto-player', ); // Check for Presto Player block in posts. $is_presto_player_block_used = self::has_user_engaged_posts_of_type( 'any', $args ); if ( $is_presto_player_block_used ) { return true; } // Check for Presto Player shortcode in posts. $args['s'] = '[presto_player id='; return self::has_user_engaged_posts_of_type( 'any', $args ); } /** * Add required plugins analytics data. * * @param array $stats Stats array. * * @since 4.4.27 * @return void */ private static function add_required_plugins_analytics( &$stats ) { $required_plugins = Astra_Sites_Page::get_instance()->get_setting( 'required_plugins', array() ); if ( ! is_array( $required_plugins ) ) { return; } $stats['plugins_data'] = ! empty( $required_plugins ) ? wp_json_encode( $required_plugins ) : ''; } /** * Add plugin active analytics data. * * @param array $stats Stats array. * * @since 4.4.27 * @return void */ private static function add_plugin_active_analytics( &$stats ) { $stats = array_merge( $stats, array( 'spectra_blocks_used' => self::is_spectra_blocks_used(), 'uae_widgets_used' => self::is_uae_widgets_used(), 'sureforms_form_published' => self::is_sureforms_form_published(), 'suremails_connected' => self::is_suremails_connected(), 'surecart_product_published' => self::is_surecart_product_published(), 'cartflows_funnel_published' => self::is_cartflows_funnel_published(), 'latepoint_booking_managed' => self::is_latepoint_booking_managed(), 'presto_player_used' => self::is_presto_player_used(), ) ); } /** * Add finish setup analytics data. * * @param array $stats Stats array. * * @since 4.4.28 * @return void */ private static function add_finish_setup_analytics( &$stats ) { // Get setup wizard showing option name. $option_name = class_exists( '\GS\Classes\GS_Helper' ) ? \GS\Classes\GS_Helper::get_setup_wizard_showing_option_name() : 'getting_started_is_setup_wizard_showing'; $is_setup_wizard_showing = get_option( $option_name, false ); $action_items_status = get_option( 'getting_started_action_items', array() ); $menu_priority_val = (string) Astra_Sites_Page::get_instance()->get_setting( 'fs_menu_position', '' ); $fs_banner_clicked = (string) Astra_Sites_Page::get_instance()->get_setting( 'fs_banner_clicked', '' ); // Determine menu position. if ( '1' === $menu_priority_val ) { $menu_priority = 'before-dashboard'; } elseif ( '2.00001' === $menu_priority_val ) { $menu_priority = 'after-dashboard'; } $courses_status = array(); $no_of_completed_courses = 0; // Get the courses status from action items. if ( is_array( $action_items_status ) ) { foreach ( $action_items_status as $key => $action_item ) { $status = isset( $action_item['status'] ) ? $action_item['status'] : false; $courses_status[ $key ] = $status ? 'done' : 'not_done'; if ( $status ) { $no_of_completed_courses++; } } } // Total number of courses. $total_courses = count( $courses_status ); // Boolean and numeric values. $stats['boolean_values']['is_finish_setup_showing'] = $is_setup_wizard_showing; $stats['numeric_values']['total_courses'] = $total_courses; $stats['numeric_values']['no_of_completed_courses'] = $no_of_completed_courses; $stats['boolean_values']['course_completed'] = 0 !== $total_courses && $no_of_completed_courses >= $total_courses; // Dashboard banner clicked. if ( $fs_banner_clicked ) { $stats['boolean_values']['finish_setup_banner_clicked'] = 'yes' === $fs_banner_clicked; } $stats['finish_setup_menu_position'] = $menu_priority; // Plain Json data. $stats['courses_status'] = ! empty( $courses_status ) ? wp_json_encode( $courses_status ) : ''; } /** * Add astra sites analytics data. * * @param array $stats stats array. * * @since 4.4.27 * @return array */ public function add_astra_sites_analytics_data( $stats ) { // Load the plugin.php file to use is_plugin_active function. if ( ! function_exists( 'is_plugin_active' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $import_complete = get_option( 'astra_sites_import_complete', 'no' ) === 'yes'; $stats['plugin_data']['astra_sites'] = array( 'version' => defined( 'ASTRA_PRO_SITES_NAME' ) ? 'premium' : 'free', 'site_language' => get_locale(), 'plugin_version' => defined( 'ASTRA_SITES_VER' ) ? ASTRA_SITES_VER : 'unknown', 'page_builder' => Astra_Sites_Page::get_instance()->get_setting( 'page_builder' ), 'boolean_values' => array( 'import_complete' => $import_complete, 'woopayments_included' => Astra_Sites_Page::get_instance()->get_setting( 'woopayments_included' ), 'was_woopayments_referred' => Astra_Sites_Page::get_instance()->get_setting( 'woopayments_ref' ), 'woopayments_banner_clicked' => Astra_Sites_Page::get_instance()->get_setting( 'woopayments_banner_clicked' ), 'woopayments_onboarding_clicked' => Astra_Sites_Page::get_instance()->get_setting( 'woopayments_onboarding_clicked' ), 'woopayments_configured' => self::is_woo_payments_configured(), 'has_woopayments_transaction' => self::has_woopayments_successful_transaction(), ), 'numeric_values' => array( 'woopayments_banner_dismissed_count' => Astra_Sites_Page::get_instance()->get_setting( 'woopayments_banner_dismissed_count' ), ), ); if ( $import_complete ) { self::add_required_plugins_analytics( $stats['plugin_data']['astra_sites'] ); self::add_plugin_active_analytics( $stats['plugin_data']['astra_sites']['boolean_values'] ); self::add_finish_setup_analytics( $stats['plugin_data']['astra_sites'] ); } return $stats; } } /** * Kicking this off by calling 'get_instance()' method */ Astra_Sites_Analytics::get_instance(); }