set_entity( [ 'sureforms' => [ 'product_name' => 'SureForms', 'path' => SRFM_DIR . 'inc/lib/bsf-analytics', 'author' => 'SureForms', 'time_to_display' => '+24 hours', 'deactivation_survey' => apply_filters( 'srfm_deactivation_survey_data', [ [ 'id' => 'deactivation-survey-sureforms', 'popup_logo' => SRFM_URL . 'admin/assets/sureforms-logo.png', 'plugin_slug' => 'sureforms', 'popup_title' => 'Quick Feedback', 'support_url' => 'https://sureforms.com/contact/', 'popup_description' => 'If you have a moment, please share why you are deactivating SureForms:', 'show_on_screens' => [ 'plugins' ], 'plugin_version' => SRFM_VER, ], ] ), 'hide_optin_checkbox' => true, ], ] ); add_filter( 'bsf_core_stats', [ $this, 'add_srfm_analytics_data' ] ); // Event tracking hooks. add_action( 'current_screen', [ $this, 'track_first_editor_open' ] ); add_action( 'transition_post_status', [ $this, 'track_first_form_published' ], 10, 3 ); add_action( 'save_post', [ $this, 'track_embed_styling_configured' ], 10, 2 ); // Detect state-based events on admin load (dedup prevents repeat tracking). $this->detect_state_events(); } /** * Get the shared BSF_Analytics_Events instance. * * Uses SureForms' Helper option methods so data stays in the * existing srfm_options row — zero migration required. * * @since 2.7.0 * @return \BSF_Analytics_Events */ public static function events() { if ( null === self::$events ) { if ( ! class_exists( 'BSF_Analytics_Events' ) ) { require_once SRFM_DIR . 'inc/lib/bsf-analytics/class-bsf-analytics-events.php'; } self::$events = new \BSF_Analytics_Events( 'sureforms', [ 'get' => [ Helper::class, 'get_srfm_option' ], 'update' => [ Helper::class, 'update_srfm_option' ], ] ); } return self::$events; } /** * Callback function to add SureForms specific analytics data. * * @param array $stats_data existing stats_data. * @since 1.4.0 * @return array */ public function add_srfm_analytics_data( $stats_data ) { $stats_data['plugin_data']['sureforms'] = [ 'free_version' => SRFM_VER, 'site_language' => get_locale(), 'most_used_anti_spam' => $this->most_used_anti_spam(), 'user_status' => $this->user_status(), 'pointer_popup_clicked' => $this->pointer_popup_clicked(), ]; $stats_data['plugin_data']['sureforms']['numeric_values'] = [ 'total_forms' => wp_count_posts( SRFM_FORMS_POST_TYPE )->publish ?? 0, 'instant_forms_enabled' => $this->instant_forms_enabled(), 'forms_using_custom_css' => $this->forms_using_custom_css(), 'ai_generated_forms' => $this->ai_generated_forms(), 'ai_generated_payment_forms' => $this->ai_generated_forms( 'payments' ), 'payment_forms' => $this->get_payment_forms_count(), 'total_entries' => Entries::get_total_entries_by_status(), 'restricted_forms' => $this->get_restricted_forms(), 'embed_styling_gb_default' => self::embed_styling_gutenberg_count( 'default' ), 'embed_styling_el_default' => self::embed_styling_elementor_count( 'default' ), 'embed_styling_br_default' => self::embed_styling_bricks_count( 'default' ), ]; $stats_data['plugin_data']['sureforms'] = array_merge_recursive( $stats_data['plugin_data']['sureforms'], $this->global_settings_data() ); // Add KPI tracking data. $kpi_data = $this->get_kpi_tracking_data(); if ( ! empty( $kpi_data ) ) { $stats_data['plugin_data']['sureforms']['kpi_records'] = $kpi_data; } // Flush pending events into payload (only if any exist). $pending_events = self::events()->flush_pending(); if ( ! empty( $pending_events ) ) { $stats_data['plugin_data']['sureforms']['events_record'] = $pending_events; } return $stats_data; } /** * Return total number of forms using instant forms. * * @since 1.4.0 * @return int */ public function instant_forms_enabled() { $meta_query = [ [ 'key' => '_srfm_instant_form_settings', 'value' => '"enable_instant_form";b:1;', 'compare' => 'LIKE', ], ]; return $this->custom_wp_query_total_posts( $meta_query ); } /** * Return total number of ai generated forms. * * @param string $form_type Form type to check. * * @since 1.4.0 * @return int */ public function ai_generated_forms( $form_type = '' ) { $form_type = empty( $form_type ) || ! is_string( $form_type ) ? '' : $form_type; $meta_query = [ [ 'key' => '_srfm_is_ai_generated', 'value' => '', 'compare' => '!=', // Checks if the value is NOT empty. ], ]; if ( 'payments' === $form_type ) { $search = 'wp:srfm/payment'; return $this->custom_wp_query_total_posts_with_search( $meta_query, $search ); } return $this->custom_wp_query_total_posts( $meta_query ); } /** * Return most used anti-spam type on this site. * * @since 1.4.4 * @return int */ public function most_used_anti_spam() { global $wpdb; // Attempt to get from cache first. $cache_key = 'most_used_anti_spam'; $cached_result = wp_cache_get( $cache_key, 'sureforms' ); if ( false !== $cached_result ) { return $cached_result; } $meta_key = '_srfm_captcha_security_type'; // Query to get the most used captcha type. // PHPCS: Ignore direct database query warning, as there is no built-in alternative. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $result = $wpdb->get_row( $wpdb->prepare( " SELECT meta_value, COUNT(meta_value) as count FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value != '' GROUP BY meta_value ORDER BY count DESC LIMIT 1 ", $meta_key ), ARRAY_A ); $output = ''; if ( $result && ! empty( $result['meta_value'] ) ) { switch ( $result['meta_value'] ) { case 'g-recaptcha': $output = 'Google reCAPTCHA'; break; case 'cf-turnstile': $output = 'CloudFlare Turnstile'; break; case 'hcaptcha': $output = 'hCaptcha'; break; default: $output = ''; break; } } // Store result in cache for 1 hour. wp_cache_set( $cache_key, $output, 'sureforms', HOUR_IN_SECONDS ); return $output; } /** * Returns total number of forms using custom css. * * @since 1.4.0 * @return int */ public function forms_using_custom_css() { $meta_query = [ [ 'key' => '_srfm_form_custom_css', 'value' => '', 'compare' => '!=', // Checks if the value is NOT empty. ], ]; return $this->custom_wp_query_total_posts( $meta_query ); } /** * Count Gutenberg srfm/form embed blocks using a specific formTheme. * * When formTheme is 'inherit' (the block.json default), WordPress does not * serialize it in the block comment. So only 'default' and 'custom' appear. * * Counts individual embed blocks (not pages), since a single page can * contain multiple form embeds each with different styling. * * @param string $theme Theme slug to count ('default' or 'custom'). * @since 2.7.0 * @return int */ public static function embed_styling_gutenberg_count( $theme ) { global $wpdb; $cache_key = 'embed_styling_gb_' . $theme; $cached_result = wp_cache_get( $cache_key, 'sureforms' ); if ( false !== $cached_result ) { return (int) $cached_result; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- No WP_Query alternative for cross-post-type content search with multiple LIKE conditions. $posts = $wpdb->get_col( $wpdb->prepare( "SELECT post_content FROM {$wpdb->posts} WHERE post_status = 'publish' AND post_content LIKE %s AND post_content LIKE %s", '%' . $wpdb->esc_like( 'wp:srfm/form' ) . '%', '%' . $wpdb->esc_like( '"formTheme":"' . $theme . '"' ) . '%' ) ); $count = 0; $escaped_theme = preg_quote( $theme, '/' ); foreach ( $posts as $content ) { $count += preg_match_all( '/