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 +126 -6 2.12.12.12.7 View file →
@@ -6,9 +6,11 @@
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;
12 14 use SRFM\Inc\Learn;
13 15 use SRFM\Inc\Traits\Get_Instance;
14 16
@@ -128,9 +130,9 @@
128 130 * @since 1.4.0
129 131 * @return array
130 132 */
131 133 public function add_srfm_analytics_data( $stats_data ) {
132 - $stats_data['plugin_data']['sureforms'] = [
134 + $stats_data['plugin_data']['sureforms'] = [
133 135 'free_version' => SRFM_VER,
134 136 'site_language' => get_locale(),
135 137 'most_used_anti_spam' => $this->most_used_anti_spam(),
136 138 'user_status' => $this->user_status(),
@@ -135,8 +137,12 @@
135 137 'most_used_anti_spam' => $this->most_used_anti_spam(),
136 138 'user_status' => $this->user_status(),
137 139 'pointer_popup_clicked' => $this->pointer_popup_clicked(),
138 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 +
139 145 $stats_data['plugin_data']['sureforms']['numeric_values'] = [
140 146 'total_forms' => wp_count_posts( SRFM_FORMS_POST_TYPE )->publish ?? 0,
141 147 'instant_forms_enabled' => $this->instant_forms_enabled(),
142 148 'forms_using_custom_css' => $this->forms_using_custom_css(),
@@ -142,15 +148,22 @@
142 148 'forms_using_custom_css' => $this->forms_using_custom_css(),
143 149 'ai_generated_forms' => $this->ai_generated_forms(),
144 150 'ai_generated_payment_forms' => $this->ai_generated_forms( 'payments' ),
145 151 'payment_forms' => $this->get_payment_forms_count(),
146 - 'total_entries' => Entries::get_total_entries_by_status(),
152 + 'total_entries' => $entries_table_missing ? 0 : Entries::get_total_entries_by_status(),
147 153 'restricted_forms' => $this->get_restricted_forms(),
148 154 'embed_styling_gb_default' => self::embed_styling_gutenberg_count( 'default' ),
149 155 'embed_styling_el_default' => self::embed_styling_elementor_count( 'default' ),
150 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(),
151 159 ];
152 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 +
153 166 $stats_data['plugin_data']['sureforms'] = array_merge_recursive( $stats_data['plugin_data']['sureforms'], $this->global_settings_data() );
154 167 // Add KPI tracking data.
155 168 $kpi_data = $this->get_kpi_tracking_data();
156 169 if ( ! empty( $kpi_data ) ) {
@@ -487,12 +500,88 @@
487 500
488 501 // Payment analytics - check if any payment method is enabled.
489 502 $global_data['boolean_values']['stripe_enabled'] = $this->is_stripe_enabled();
490 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 +
491 510 return $global_data;
492 511 }
493 512
494 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 + /**
495 584 * Returns user status.
496 585 *
497 586 * @since 1.8.0
498 587 * @return string
@@ -719,8 +808,24 @@
719 808 );
720 809 }
721 810
722 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 + /**
723 828 * Extract non-inherit formTheme values from Bricks element data.
724 829 *
725 830 * Recursively walks the unserialized Bricks elements array looking for
726 831 * sureforms elements with custom formTheme settings.
@@ -823,8 +928,16 @@
823 928 * @since 2.4.0
824 929 * @return int Daily submissions count.
825 930 */
826 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 +
827 940 $start_date = $date . ' 00:00:00';
828 941 $end_date = $date . ' 23:59:59';
829 942
830 943 $where_conditions = [
@@ -852,12 +965,19 @@
852 965 * @since 2.5.1
853 966 * @return void
854 967 */
855 968 private function detect_state_events() {
856 - // plugin_activated: dedup in self::events()->track() ensures this fires only once.
857 - $bsf_referrers = get_option( 'bsf_product_referers', [] );
858 - $source = ! empty( $bsf_referrers['sureforms'] ) ? $bsf_referrers['sureforms'] : 'self';
859 - 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 + }
860 980
861 981 // One-time: re-send onboarding_completed with full properties (v2).
862 982 if ( ! Helper::get_srfm_option( 'onboarding_event_v2_flushed', false )
863 983 && \SRFM\Inc\Onboarding::get_instance()->get_onboarding_status() ) {