PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.12.7
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.12.7
2.12.7 2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 0.0.2 All 97 releases
← All changes | admin/analytics.php +222 -8 2.7.12.12.7 View file →
@@ -6,10 +6,13 @@
6 6 */
7 7
8 8 namespace SRFM\Admin;
9 9
10 +use SRFM\Inc\Database\Register;
10 11 use SRFM\Inc\Database\Tables\Entries;
12 +use SRFM\Inc\Form_Views;
11 13 use SRFM\Inc\Helper;
14 +use SRFM\Inc\Learn;
12 15 use SRFM\Inc\Traits\Get_Instance;
13 16
14 17 if ( ! defined( 'ABSPATH' ) ) {
15 18 exit; // Exit if accessed directly.
@@ -42,10 +45,10 @@
42 45 if ( ! class_exists( 'BSF_Analytics_Loader' ) ) {
43 46 require_once SRFM_DIR . 'inc/lib/bsf-analytics/class-bsf-analytics-loader.php';
44 47 }
45 48
46 - if ( ! class_exists( 'Astra_Notices' ) ) {
47 - require_once SRFM_DIR . 'inc/lib/astra-notices/class-astra-notices.php';
49 + if ( ! class_exists( 'BSF_Admin_Notices' ) ) {
50 + require_once SRFM_DIR . 'inc/lib/astra-notices/class-bsf-admin-notices.php';
48 51 }
49 52
50 53 add_filter(
51 54 'uds_survey_allowed_screens',
@@ -127,9 +130,9 @@
127 130 * @since 1.4.0
128 131 * @return array
129 132 */
130 133 public function add_srfm_analytics_data( $stats_data ) {
131 - $stats_data['plugin_data']['sureforms'] = [
134 + $stats_data['plugin_data']['sureforms'] = [
132 135 'free_version' => SRFM_VER,
133 136 'site_language' => get_locale(),
134 137 'most_used_anti_spam' => $this->most_used_anti_spam(),
135 138 'user_status' => $this->user_status(),
@@ -134,8 +137,12 @@
134 137 'most_used_anti_spam' => $this->most_used_anti_spam(),
135 138 'user_status' => $this->user_status(),
136 139 'pointer_popup_clicked' => $this->pointer_popup_clicked(),
137 140 ];
141 + // Every query against the entries table errors when the table is missing, so
142 + // resolve that once here rather than letting each caller below trip over it.
143 + $entries_table_missing = Register::is_entries_table_missing();
144 +
138 145 $stats_data['plugin_data']['sureforms']['numeric_values'] = [
139 146 'total_forms' => wp_count_posts( SRFM_FORMS_POST_TYPE )->publish ?? 0,
140 147 'instant_forms_enabled' => $this->instant_forms_enabled(),
141 148 'forms_using_custom_css' => $this->forms_using_custom_css(),
@@ -141,15 +148,22 @@
141 148 'forms_using_custom_css' => $this->forms_using_custom_css(),
142 149 'ai_generated_forms' => $this->ai_generated_forms(),
143 150 'ai_generated_payment_forms' => $this->ai_generated_forms( 'payments' ),
144 151 'payment_forms' => $this->get_payment_forms_count(),
145 - 'total_entries' => Entries::get_total_entries_by_status(),
152 + 'total_entries' => $entries_table_missing ? 0 : Entries::get_total_entries_by_status(),
146 153 'restricted_forms' => $this->get_restricted_forms(),
147 154 'embed_styling_gb_default' => self::embed_styling_gutenberg_count( 'default' ),
148 155 'embed_styling_el_default' => self::embed_styling_elementor_count( 'default' ),
149 156 'embed_styling_br_default' => self::embed_styling_bricks_count( 'default' ),
157 + 'total_form_views' => $this->total_form_views(),
158 + 'forms_with_views' => $this->forms_with_views(),
150 159 ];
151 160
161 + // Whether the entries table is currently missing. An event fires once when a
162 + // site first sees the notice; this is what shows the state persisting, and
163 + // catches a recurrence that the event's one-time dedup would swallow.
164 + $stats_data['plugin_data']['sureforms']['boolean_values']['db_entries_table_missing'] = $entries_table_missing;
165 +
152 166 $stats_data['plugin_data']['sureforms'] = array_merge_recursive( $stats_data['plugin_data']['sureforms'], $this->global_settings_data() );
153 167 // Add KPI tracking data.
154 168 $kpi_data = $this->get_kpi_tracking_data();
155 169 if ( ! empty( $kpi_data ) ) {
@@ -155,8 +169,11 @@
155 169 if ( ! empty( $kpi_data ) ) {
156 170 $stats_data['plugin_data']['sureforms']['kpi_records'] = $kpi_data;
157 171 }
158 172
173 + // Build learn progress snapshot.
174 + $this->get_learn_tracking_data();
175 +
159 176 // Flush pending events into payload (only if any exist).
160 177 $pending_events = self::events()->flush_pending();
161 178 if ( ! empty( $pending_events ) ) {
162 179 $stats_data['plugin_data']['sureforms']['events_record'] = $pending_events;
@@ -483,12 +500,88 @@
483 500
484 501 // Payment analytics - check if any payment method is enabled.
485 502 $global_data['boolean_values']['stripe_enabled'] = $this->is_stripe_enabled();
486 503
504 + // Delegated rather than read from $general_settings directly, so the telemetry
505 + // can never disagree with what the Forms list actually shows. That matters
506 + // because the default has already changed once: reimplementing the check here
507 + // means a future change has two places to be made and one to be forgotten.
508 + $global_data['boolean_values']['form_views_columns_enabled'] = Form_Views::get_instance()->is_tracking_enabled();
509 +
487 510 return $global_data;
488 511 }
489 512
490 513 /**
514 + * Total page views counted across all published forms.
515 + *
516 + * Read alongside `forms_with_views`, which is what makes this figure legible: a
517 + * site with views but no forms recording any reads very differently from one
518 + * with neither.
519 + *
520 + * Not a denominator for `total_entries`, and the two must not be divided. This
521 + * total starts at the moment tracking opened and leaves out anyone who can edit
522 + * the site, while `total_entries` is an unfiltered all-time count of every
523 + * entry ever received. A rate built from the pair answers neither question and
524 + * will not match the Conversion Rate column, which measures both halves over
525 + * the same window with the same exclusion.
526 + *
527 + * Counts only published forms, so views left behind by a trashed or draft form
528 + * do not inflate the total against a denominator that no longer includes them.
529 + *
530 + * @since 2.12.6
531 + * @return int
532 + */
533 + public function total_form_views() {
534 + global $wpdb;
535 +
536 + // Deliberately uncached: the only caller is the analytics payload builder,
537 + // which runs once per cron pass. A cache would never see a second read inside
538 + // a request, and any TTL would always have expired between passes — while
539 + // still going stale if a second caller ever appeared.
540 + // PHPCS: Ignore direct database query warning, as there is no built-in alternative.
541 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
542 + $total = $wpdb->get_var(
543 + $wpdb->prepare(
544 + "
545 + SELECT SUM( pm.meta_value )
546 + FROM {$wpdb->postmeta} pm
547 + INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
548 + WHERE pm.meta_key = %s
549 + AND p.post_type = %s
550 + AND p.post_status = 'publish'
551 + ",
552 + Form_Views::META_KEY,
553 + SRFM_FORMS_POST_TYPE
554 + )
555 + );
556 +
557 + return Helper::get_integer_value( $total );
558 + }
559 +
560 + /**
561 + * Number of published forms that have been viewed at least once.
562 + *
563 + * Distinguishes "the feature is on but nothing is embedded anywhere" from "the
564 + * beacon is firing" — a total alone cannot, because one busy form looks the
565 + * same as many quiet ones.
566 + *
567 + * @since 2.12.6
568 + * @return int
569 + */
570 + public function forms_with_views() {
571 + return $this->custom_wp_query_total_posts(
572 + [
573 + [
574 + 'key' => Form_Views::META_KEY,
575 + 'value' => 0,
576 + 'compare' => '>',
577 + 'type' => 'NUMERIC',
578 + ],
579 + ]
580 + );
581 + }
582 +
583 + /**
491 584 * Returns user status.
492 585 *
493 586 * @since 1.8.0
494 587 * @return string
@@ -715,8 +808,24 @@
715 808 );
716 809 }
717 810
718 811 /**
812 + * Track the plugin_activated event with the correct install referer.
813 + *
814 + * Dedup in self::events()->track() ensures this fires only once. Runs on
815 + * 'shutdown' (see detect_state_events()) so it reads bsf_product_referers
816 + * after any late-writing referer call has had a chance to run.
817 + *
818 + * @since 2.12.6
819 + * @return void
820 + */
821 + public function track_plugin_activated_event() {
822 + $bsf_referrers = get_option( 'bsf_product_referers', [] );
823 + $source = ! empty( $bsf_referrers['sureforms'] ) ? $bsf_referrers['sureforms'] : 'self';
824 + self::events()->track( 'plugin_activated', SRFM_VER, [ 'source' => $source ] );
825 + }
826 +
827 + /**
719 828 * Extract non-inherit formTheme values from Bricks element data.
720 829 *
721 830 * Recursively walks the unserialized Bricks elements array looking for
722 831 * sureforms elements with custom formTheme settings.
@@ -819,8 +928,16 @@
819 928 * @since 2.4.0
820 929 * @return int Daily submissions count.
821 930 */
822 931 private function get_daily_submissions_count( $date ) {
932 + // Guarded here rather than at the call site: this is the function that runs
933 + // the query, so a future caller is covered too. Every read against a missing
934 + // entries table raises a DB error, and the daily send would raise one per
935 + // day counted on exactly the sites whose breakage we most need reported.
936 + if ( Register::is_entries_table_missing() ) {
937 + return 0;
938 + }
939 +
823 940 $start_date = $date . ' 00:00:00';
824 941 $end_date = $date . ' 23:59:59';
825 942
826 943 $where_conditions = [
@@ -848,12 +965,19 @@
848 965 * @since 2.5.1
849 966 * @return void
850 967 */
851 968 private function detect_state_events() {
852 - // plugin_activated: dedup in self::events()->track() ensures this fires only once.
853 - $bsf_referrers = get_option( 'bsf_product_referers', [] );
854 - $source = ! empty( $bsf_referrers['sureforms'] ) ? $bsf_referrers['sureforms'] : 'self';
855 - self::events()->track( 'plugin_activated', SRFM_VER, [ 'source' => $source ] );
969 + // plugin_activated: deferred to 'shutdown' so that a referring plugin/theme's
970 + // own BSF_UTM_Analytics::update_referer() call — which some products (incorrectly)
971 + // make only after their activate_plugin() call returns, in the same request — has
972 + // already run by the time we read bsf_product_referers. Reading this synchronously
973 + // here would race that write, since this constructor can execute mid-request while
974 + // SureForms itself is being activated by that other plugin.
975 + if ( did_action( 'shutdown' ) ) {
976 + $this->track_plugin_activated_event();
977 + } else {
978 + add_action( 'shutdown', [ $this, 'track_plugin_activated_event' ], PHP_INT_MAX );
979 + }
856 980
857 981 // One-time: re-send onboarding_completed with full properties (v2).
858 982 if ( ! Helper::get_srfm_option( 'onboarding_event_v2_flushed', false )
859 983 && \SRFM\Inc\Onboarding::get_instance()->get_onboarding_status() ) {
@@ -941,7 +1065,97 @@
941 1065
942 1066 if ( ! empty( $mcp_settings['srfm_mcp_server'] ) ) {
943 1067 self::events()->track( 'mcp_server_enabled' );
944 1068 }
1069 + }
1070 +
1071 + /**
1072 + * Get learn section progress aggregated across all users.
1073 + *
1074 + * Queries all users with srfm_learn_progress meta, counts completions
1075 + * per step. Called at analytics send time (bsf_core_stats filter).
1076 + *
1077 + * @since 2.8.0
1078 + * @return void
1079 + */
1080 + private function get_learn_tracking_data() {
1081 + // Only send when progress changed or event has never been tracked.
1082 + $has_changed = get_transient( 'srfm_learn_progress_changed' );
1083 + if ( ! $has_changed && self::events()->is_tracked( 'learn' ) ) {
1084 + return;
1085 + }
1086 +
1087 + $chapters = Learn::get_chapters_structure();
1088 + $step_counts = [];
1089 + $total_steps = 0;
1090 +
1091 + // Initialize step counters from canonical structure.
1092 + foreach ( $chapters as $chapter ) {
1093 + if ( ! isset( $chapter['id'], $chapter['steps'] ) || ! is_array( $chapter['steps'] ) ) {
1094 + continue;
1095 + }
1096 + foreach ( $chapter['steps'] as $step ) {
1097 + if ( ! isset( $step['id'] ) ) {
1098 + continue;
1099 + }
1100 + $step_counts[ $chapter['id'] . '/' . $step['id'] ] = 0;
1101 + ++$total_steps;
1102 + }
1103 + }
1104 +
1105 + // Query all users who have learn progress.
1106 + $users = get_users(
1107 + [
1108 + 'meta_key' => 'srfm_learn_progress', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Runs once per analytics send cycle; bounded by admin user count.
1109 + 'fields' => 'ID',
1110 + ]
1111 + );
1112 +
1113 + $users_with_progress = 0;
1114 + $users_fully_completed = 0;
1115 + $max_completed = 0;
1116 +
1117 + foreach ( $users as $user_id ) {
1118 + $progress = get_user_meta( $user_id, 'srfm_learn_progress', true );
1119 + if ( ! is_array( $progress ) || empty( $progress ) ) {
1120 + continue;
1121 + }
1122 +
1123 + $user_completed = 0;
1124 +
1125 + foreach ( $step_counts as $key => $count ) {
1126 + [ $chapter_id, $step_id ] = explode( '/', $key );
1127 + if ( ! empty( $progress[ $chapter_id ][ $step_id ] ) ) {
1128 + $step_counts[ $key ] = $count + 1;
1129 + ++$user_completed;
1130 + }
1131 + }
1132 +
1133 + if ( $user_completed > 0 ) {
1134 + ++$users_with_progress;
1135 + }
1136 + if ( $user_completed === $total_steps ) {
1137 + ++$users_fully_completed;
1138 + }
1139 + $max_completed = max( $max_completed, $user_completed );
1140 + }
1141 +
1142 + // Build flat properties — step keys use snake_case (hyphens → underscores).
1143 + $properties = [];
1144 + foreach ( $step_counts as $key => $count ) {
1145 + $step_id = explode( '/', $key )[1];
1146 + $properties[ str_replace( '-', '_', $step_id ) ] = (string) $count;
1147 + }
1148 +
1149 + $properties['users_with_progress'] = (string) $users_with_progress;
1150 + $properties['users_fully_completed'] = (string) $users_fully_completed;
1151 + $properties['total_steps'] = (string) $total_steps;
1152 +
1153 + // Flush dedup so event re-tracks with latest snapshot.
1154 + self::events()->flush_pushed( [ 'learn' ] );
1155 + self::events()->track( 'learn', (string) $max_completed, $properties );
1156 +
1157 + // Clear the change flag after tracking.
1158 + delete_transient( 'srfm_learn_progress_changed' );
945 1159 }
946 1160
947 1161 }