PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.1.1
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.1.1
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 0.0.3 All 96 releases
← All changes | admin/analytics.php +0 -436 2.7.02.1.1 View file →
@@ -22,15 +22,8 @@
22 22 class Analytics {
23 23 use Get_Instance;
24 24
25 25 /**
26 - * BSF_Analytics_Events instance for one-time event tracking.
27 - *
28 - * @var \BSF_Analytics_Events|null
29 - */
30 - private static $events = null;
31 -
32 - /**
33 26 * Class constructor.
34 27 *
35 28 * @return void
36 29 * @since 1.4.0
@@ -83,45 +76,11 @@
83 76 ]
84 77 );
85 78
86 79 add_filter( 'bsf_core_stats', [ $this, 'add_srfm_analytics_data' ] );
87 -
88 - // Event tracking hooks.
89 - add_action( 'current_screen', [ $this, 'track_first_editor_open' ] );
90 - add_action( 'transition_post_status', [ $this, 'track_first_form_published' ], 10, 3 );
91 - add_action( 'save_post', [ $this, 'track_embed_styling_configured' ], 10, 2 );
92 -
93 - // Detect state-based events on admin load (dedup prevents repeat tracking).
94 - $this->detect_state_events();
95 80 }
96 81
97 82 /**
98 - * Get the shared BSF_Analytics_Events instance.
99 - *
100 - * Uses SureForms' Helper option methods so data stays in the
101 - * existing srfm_options row — zero migration required.
102 - *
103 - * @since 2.7.0
104 - * @return \BSF_Analytics_Events
105 - */
106 - public static function events() {
107 - if ( null === self::$events ) {
108 - if ( ! class_exists( 'BSF_Analytics_Events' ) ) {
109 - require_once SRFM_DIR . 'inc/lib/bsf-analytics/class-bsf-analytics-events.php';
110 - }
111 -
112 - self::$events = new \BSF_Analytics_Events(
113 - 'sureforms',
114 - [
115 - 'get' => [ Helper::class, 'get_srfm_option' ],
116 - 'update' => [ Helper::class, 'update_srfm_option' ],
117 - ]
118 - );
119 - }
120 - return self::$events;
121 - }
122 -
123 - /**
124 83 * Callback function to add SureForms specific analytics data.
125 84 *
126 85 * @param array $stats_data existing stats_data.
127 86 * @since 1.4.0
@@ -143,29 +102,13 @@
143 102 'ai_generated_payment_forms' => $this->ai_generated_forms( 'payments' ),
144 103 'payment_forms' => $this->get_payment_forms_count(),
145 104 'total_entries' => Entries::get_total_entries_by_status(),
146 105 'restricted_forms' => $this->get_restricted_forms(),
147 - 'embed_styling_gb_default' => self::embed_styling_gutenberg_count( 'default' ),
148 - 'embed_styling_el_default' => self::embed_styling_elementor_count( 'default' ),
149 - 'embed_styling_br_default' => self::embed_styling_bricks_count( 'default' ),
150 106 ];
151 107
152 108 $stats_data['plugin_data']['sureforms'] = array_merge_recursive( $stats_data['plugin_data']['sureforms'], $this->global_settings_data() );
153 109 // Add onboarding analytics data.
154 110 $stats_data['plugin_data']['sureforms'] = array_merge_recursive( $stats_data['plugin_data']['sureforms'], $this->onboarding_analytics_data() );
155 -
156 - // Add KPI tracking data.
157 - $kpi_data = $this->get_kpi_tracking_data();
158 - if ( ! empty( $kpi_data ) ) {
159 - $stats_data['plugin_data']['sureforms']['kpi_records'] = $kpi_data;
160 - }
161 -
162 - // Flush pending events into payload (only if any exist).
163 - $pending_events = self::events()->flush_pending();
164 - if ( ! empty( $pending_events ) ) {
165 - $stats_data['plugin_data']['sureforms']['events_record'] = $pending_events;
166 - }
167 -
168 111 return $stats_data;
169 112 }
170 113
171 114 /**
@@ -295,149 +238,8 @@
295 238 return $this->custom_wp_query_total_posts( $meta_query );
296 239 }
297 240
298 241 /**
299 - * Count Gutenberg srfm/form embed blocks using a specific formTheme.
300 - *
301 - * When formTheme is 'inherit' (the block.json default), WordPress does not
302 - * serialize it in the block comment. So only 'default' and 'custom' appear.
303 - *
304 - * Counts individual embed blocks (not pages), since a single page can
305 - * contain multiple form embeds each with different styling.
306 - *
307 - * @param string $theme Theme slug to count ('default' or 'custom').
308 - * @since 2.7.0
309 - * @return int
310 - */
311 - public static function embed_styling_gutenberg_count( $theme ) {
312 - global $wpdb;
313 -
314 - $cache_key = 'embed_styling_gb_' . $theme;
315 - $cached_result = wp_cache_get( $cache_key, 'sureforms' );
316 -
317 - if ( false !== $cached_result ) {
318 - return (int) $cached_result;
319 - }
320 -
321 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- No WP_Query alternative for cross-post-type content search with multiple LIKE conditions.
322 - $posts = $wpdb->get_col(
323 - $wpdb->prepare(
324 - "SELECT post_content FROM {$wpdb->posts}
325 - WHERE post_status = 'publish'
326 - AND post_content LIKE %s
327 - AND post_content LIKE %s",
328 - '%' . $wpdb->esc_like( 'wp:srfm/form' ) . '%',
329 - '%' . $wpdb->esc_like( '"formTheme":"' . $theme . '"' ) . '%'
330 - )
331 - );
332 -
333 - $count = 0;
334 - $escaped_theme = preg_quote( $theme, '/' );
335 - foreach ( $posts as $content ) {
336 - $count += preg_match_all( '/<!--\s*wp:srfm\/form\s+\{[^}]*"formTheme"\s*:\s*"' . $escaped_theme . '"/', $content );
337 - }
338 -
339 - wp_cache_set( $cache_key, $count, 'sureforms', HOUR_IN_SECONDS );
340 -
341 - return $count;
342 - }
343 -
344 - /**
345 - * Count Elementor sureforms_form widgets using a specific formTheme.
346 - *
347 - * Searches _elementor_data post meta for sureforms_form widgets with
348 - * a non-inherit formTheme. Counts individual widgets (not pages).
349 - *
350 - * @param string $theme Theme slug to count ('default' or 'custom').
351 - * @since 2.7.0
352 - * @return int
353 - */
354 - public static function embed_styling_elementor_count( $theme ) {
355 - global $wpdb;
356 -
357 - $cache_key = 'embed_styling_el_' . $theme;
358 - $cached_result = wp_cache_get( $cache_key, 'sureforms' );
359 -
360 - if ( false !== $cached_result ) {
361 - return (int) $cached_result;
362 - }
363 -
364 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- No WP_Query alternative for post meta content search with multiple LIKE conditions.
365 - $meta_values = $wpdb->get_col(
366 - $wpdb->prepare(
367 - "SELECT pm.meta_value FROM {$wpdb->postmeta} pm
368 - INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
369 - WHERE p.post_status = 'publish'
370 - AND pm.meta_key = '_elementor_data'
371 - AND pm.meta_value LIKE %s
372 - AND pm.meta_value LIKE %s",
373 - '%' . $wpdb->esc_like( 'sureforms_form' ) . '%',
374 - '%' . $wpdb->esc_like( '"formTheme":"' . $theme . '"' ) . '%'
375 - )
376 - );
377 -
378 - $count = 0;
379 - $escaped_theme = preg_quote( $theme, '/' );
380 - foreach ( $meta_values as $json ) {
381 - // Count widget instances with the specific formTheme in the JSON.
382 - $count += preg_match_all( '/"widgetType"\s*:\s*"sureforms_form"[^}]*"formTheme"\s*:\s*"' . $escaped_theme . '"/', $json );
383 - }
384 -
385 - wp_cache_set( $cache_key, $count, 'sureforms', HOUR_IN_SECONDS );
386 -
387 - return $count;
388 - }
389 -
390 - /**
391 - * Count Bricks sureforms elements using a specific formTheme.
392 - *
393 - * Searches _bricks_page_content_2 post meta (serialized PHP) for
394 - * sureforms elements with a non-inherit formTheme. Counts individual elements.
395 - *
396 - * @param string $theme Theme slug to count ('default' or 'custom').
397 - * @since 2.7.0
398 - * @return int
399 - */
400 - public static function embed_styling_bricks_count( $theme ) {
401 - global $wpdb;
402 -
403 - $cache_key = 'embed_styling_br_' . $theme;
404 - $cached_result = wp_cache_get( $cache_key, 'sureforms' );
405 -
406 - if ( false !== $cached_result ) {
407 - return (int) $cached_result;
408 - }
409 -
410 - // Bricks stores element data as serialized PHP in _bricks_page_content_2.
411 - // In serialized format: s:9:"formTheme";s:7:"default" (for 'default' theme).
412 - $serialized_theme = sprintf( '"formTheme";s:%d:"%s"', strlen( $theme ), $theme );
413 -
414 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- No WP_Query alternative for post meta content search with multiple LIKE conditions.
415 - $meta_values = $wpdb->get_col(
416 - $wpdb->prepare(
417 - "SELECT pm.meta_value FROM {$wpdb->postmeta} pm
418 - INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
419 - WHERE p.post_status = 'publish'
420 - AND pm.meta_key = '_bricks_page_content_2'
421 - AND pm.meta_value LIKE %s
422 - AND pm.meta_value LIKE %s",
423 - '%' . $wpdb->esc_like( '"sureforms"' ) . '%',
424 - '%' . $wpdb->esc_like( $serialized_theme ) . '%'
425 - )
426 - );
427 -
428 - $count = 0;
429 - $escaped_serial_theme = preg_quote( $serialized_theme, '/' );
430 - foreach ( $meta_values as $data ) {
431 - $count += preg_match_all( '/' . $escaped_serial_theme . '/', $data );
432 - }
433 -
434 - wp_cache_set( $cache_key, $count, 'sureforms', HOUR_IN_SECONDS );
435 -
436 - return $count;
437 - }
438 -
439 - /**
440 242 * Return total number of restricted forms.
441 243 *
442 244 * @since 1.10.1
443 245 * @return int
@@ -700,155 +502,8 @@
700 502 return $this->custom_wp_query_total_posts_with_search( [], $search );
701 503 }
702 504
703 505 /**
704 - * Track first time a user opens the form editor.
705 - *
706 - * @since 2.5.1
707 - * @return void
708 - */
709 - public function track_first_editor_open() {
710 - $screen = get_current_screen();
711 - if ( $screen && 'sureforms_form' === $screen->id ) {
712 - self::events()->track( 'first_form_editor_opened' );
713 - }
714 - }
715 -
716 - /**
717 - * Track first time a form is published.
718 - *
719 - * @param string $new_status New post status.
720 - * @param string $old_status Old post status.
721 - * @param \WP_Post $post Post object.
722 - * @since 2.5.1
723 - * @return void
724 - */
725 - public function track_first_form_published( $new_status, $old_status, $post ) {
726 - if ( 'publish' !== $new_status || 'publish' === $old_status || SRFM_FORMS_POST_TYPE !== $post->post_type ) {
727 - return;
728 - }
729 -
730 - $is_ai = ! empty( get_post_meta( $post->ID, '_srfm_is_ai_generated', true ) );
731 - $block_count = substr_count( $post->post_content, '<!-- wp:srfm/' );
732 -
733 - self::events()->track(
734 - 'first_form_published',
735 - (string) $post->ID,
736 - [
737 - 'is_ai_generated' => $is_ai,
738 - 'block_count' => $block_count,
739 - ]
740 - );
741 - }
742 -
743 - /**
744 - * Track embed styling configuration when a post/page is saved.
745 - *
746 - * Detects custom formTheme in Gutenberg blocks (post_content),
747 - * Elementor widgets (_elementor_data meta), and Bricks elements
748 - * (_bricks_page_content_2 meta). Re-tracks on each save by flushing
749 - * the dedup flag.
750 - *
751 - * @param int $post_id Post ID.
752 - * @param \WP_Post $post Post object.
753 - * @since 2.7.0
754 - * @return void
755 - */
756 - public function track_embed_styling_configured( $post_id, $post ) {
757 - if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
758 - return;
759 - }
760 -
761 - if ( 'publish' !== $post->post_status ) {
762 - return;
763 - }
764 -
765 - $themes = [];
766 - $source = '';
767 -
768 - // Check Gutenberg blocks in post_content.
769 - if ( preg_match_all( '/<!--\s*wp:srfm\/form\s+\{[^}]*"formTheme"\s*:\s*"(?!inherit")([^"]*)"/', $post->post_content, $matches ) ) {
770 - $themes = array_count_values( $matches[1] );
771 - $source = 'gutenberg';
772 - }
773 -
774 - // Check Elementor widgets in _elementor_data meta.
775 - if ( empty( $themes ) ) {
776 - $elementor_data = get_post_meta( $post_id, '_elementor_data', true );
777 - if ( is_string( $elementor_data )
778 - && preg_match_all( '/"widgetType"\s*:\s*"sureforms_form"[^}]*"formTheme"\s*:\s*"(?!inherit")([^"]*)"/', $elementor_data, $el_matches )
779 - ) {
780 - $themes = array_count_values( $el_matches[1] );
781 - $source = 'elementor';
782 - }
783 - }
784 -
785 - // Check Bricks elements in _bricks_page_content_2 meta (serialized PHP).
786 - if ( empty( $themes ) ) {
787 - $bricks_data = get_post_meta( $post_id, '_bricks_page_content_2', true );
788 - if ( is_array( $bricks_data ) ) {
789 - $bricks_themes = self::extract_bricks_form_themes( $bricks_data );
790 - if ( ! empty( $bricks_themes ) ) {
791 - $themes = array_count_values( $bricks_themes );
792 - $source = 'bricks';
793 - }
794 - }
795 - }
796 -
797 - if ( empty( $themes ) ) {
798 - return;
799 - }
800 -
801 - // Flush dedup so event is re-tracked on each meaningful save.
802 - self::events()->flush_pushed( [ 'embed_styling_configured' ] );
803 -
804 - self::events()->track(
805 - 'embed_styling_configured',
806 - (string) $post_id,
807 - [
808 - 'themes' => $themes,
809 - 'block_count' => array_sum( $themes ),
810 - 'source' => $source,
811 - ]
812 - );
813 - }
814 -
815 - /**
816 - * Extract non-inherit formTheme values from Bricks element data.
817 - *
818 - * Recursively walks the unserialized Bricks elements array looking for
819 - * sureforms elements with custom formTheme settings.
820 - *
821 - * @param array<mixed> $elements Bricks elements array.
822 - * @return array<string> List of formTheme values (non-inherit).
823 - * @since 2.7.0
824 - */
825 - private static function extract_bricks_form_themes( $elements ) {
826 - $themes = [];
827 -
828 - foreach ( $elements as $element ) {
829 - if ( ! is_array( $element ) ) {
830 - continue;
831 - }
832 -
833 - $name = $element['name'] ?? '';
834 - $settings = $element['settings'] ?? [];
835 - $form_theme = $settings['formTheme'] ?? '';
836 -
837 - if ( 'sureforms' === $name && ! empty( $form_theme ) && 'inherit' !== $form_theme ) {
838 - $themes[] = sanitize_text_field( $form_theme );
839 - }
840 -
841 - // Recurse into nested children.
842 - if ( ! empty( $element['children'] ) && is_array( $element['children'] ) ) {
843 - $themes = array_merge( $themes, self::extract_bricks_form_themes( $element['children'] ) );
844 - }
845 - }
846 -
847 - return $themes;
848 - }
849 -
850 - /**
851 506 * Check if any payment method is enabled.
852 507 *
853 508 * This function checks if any payment gateway is connected and enabled.
854 509 * Currently supports Stripe, but can be extended for other payment methods in the future.
@@ -883,96 +538,5 @@
883 538 wp_reset_postdata();
884 539
885 540 return $posts_count;
886 541 }
887 -
888 - /**
889 - * Get KPI tracking data for the last 2 days (excluding today).
890 - *
891 - * @since 2.4.0
892 - * @return array KPI data organized by date.
893 - */
894 - private function get_kpi_tracking_data() {
895 - $kpi_data = [];
896 - $today = current_time( 'Y-m-d' );
897 -
898 - // Get data for yesterday and day before yesterday.
899 - for ( $i = 1; $i <= 2; $i++ ) {
900 - $date = gmdate( 'Y-m-d', strtotime( $today . ' -' . $i . ' days' ) );
901 -
902 - $kpi_data[ $date ] = [
903 - 'numeric_values' => [
904 - 'submissions' => $this->get_daily_submissions_count( $date ),
905 - ],
906 - ];
907 - }
908 -
909 - return $kpi_data;
910 - }
911 -
912 - /**
913 - * Get daily submissions count for a specific date.
914 - *
915 - * @param string $date Date in Y-m-d format.
916 - * @since 2.4.0
917 - * @return int Daily submissions count.
918 - */
919 - private function get_daily_submissions_count( $date ) {
920 - $start_date = $date . ' 00:00:00';
921 - $end_date = $date . ' 23:59:59';
922 -
923 - $where_conditions = [
924 - [
925 - [
926 - 'key' => 'created_at',
927 - 'compare' => '>=',
928 - 'value' => $start_date,
929 - ],
930 - [
931 - 'key' => 'created_at',
932 - 'compare' => '<=',
933 - 'value' => $end_date,
934 - ],
935 - ],
936 - ];
937 -
938 - return Entries::get_instance()->get_total_count( $where_conditions );
939 - }
940 -
941 - /**
942 - * Detect state-based events that can't use direct hooks.
943 - * Uses dedup in self::events()->track() — safe to call repeatedly.
944 - *
945 - * @since 2.5.1
946 - * @return void
947 - */
948 - private function detect_state_events() {
949 - // plugin_activated: dedup in self::events()->track() ensures this fires only once.
950 - $bsf_referrers = get_option( 'bsf_product_referers', [] );
951 - $source = ! empty( $bsf_referrers['sureforms'] ) ? $bsf_referrers['sureforms'] : 'self';
952 - self::events()->track( 'plugin_activated', SRFM_VER, [ 'source' => $source ] );
953 -
954 - // onboarding_completed: detect completed state.
955 - if ( \SRFM\Inc\Onboarding::get_instance()->get_onboarding_status() ) {
956 - self::events()->track( 'onboarding_completed' );
957 - }
958 -
959 - // stripe_connected: detect connection state.
960 - if ( class_exists( '\SRFM\Inc\Payments\Stripe\Stripe_Helper' )
961 - && \SRFM\Inc\Payments\Stripe\Stripe_Helper::is_stripe_connected() ) {
962 - $mode = \SRFM\Inc\Payments\Stripe\Stripe_Helper::get_stripe_mode();
963 - self::events()->track( 'stripe_connected', ! empty( $mode ) ? $mode : 'live' );
964 - }
965 -
966 - // MCP / Abilities API first-enable events.
967 - $mcp_settings = get_option( 'srfm_mcp_settings_options', [] );
968 -
969 - if ( ! empty( $mcp_settings['srfm_abilities_api'] ) ) {
970 - self::events()->track( 'abilities_api_enabled' );
971 - }
972 -
973 - if ( ! empty( $mcp_settings['srfm_mcp_server'] ) ) {
974 - self::events()->track( 'mcp_server_enabled' );
975 - }
976 - }
977 -
978 542 }