PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 1.7.4
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v1.7.4
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 +11 -770 2.8.21.7.4 View file →
@@ -7,10 +7,8 @@
7 7
8 8 namespace SRFM\Admin;
9 9
10 10 use SRFM\Inc\Database\Tables\Entries;
11 -use SRFM\Inc\Helper;
12 -use SRFM\Inc\Learn;
13 11 use SRFM\Inc\Traits\Get_Instance;
14 12
15 13 if ( ! defined( 'ABSPATH' ) ) {
16 14 exit; // Exit if accessed directly.
@@ -23,15 +21,8 @@
23 21 class Analytics {
24 22 use Get_Instance;
25 23
26 24 /**
27 - * BSF_Analytics_Events instance for one-time event tracking.
28 - *
29 - * @var \BSF_Analytics_Events|null
30 - */
31 - private static $events = null;
32 -
33 - /**
34 25 * Class constructor.
35 26 *
36 27 * @return void
37 28 * @since 1.4.0
@@ -43,10 +34,10 @@
43 34 if ( ! class_exists( 'BSF_Analytics_Loader' ) ) {
44 35 require_once SRFM_DIR . 'inc/lib/bsf-analytics/class-bsf-analytics-loader.php';
45 36 }
46 37
47 - if ( ! class_exists( 'BSF_Admin_Notices' ) ) {
48 - require_once SRFM_DIR . 'inc/lib/astra-notices/class-bsf-admin-notices.php';
38 + if ( ! class_exists( 'Astra_Notices' ) ) {
39 + require_once SRFM_DIR . 'inc/lib/astra-notices/class-astra-notices.php';
49 40 }
50 41
51 42 add_filter(
52 43 'uds_survey_allowed_screens',
@@ -84,45 +75,11 @@
84 75 ]
85 76 );
86 77
87 78 add_filter( 'bsf_core_stats', [ $this, 'add_srfm_analytics_data' ] );
88 -
89 - // Event tracking hooks.
90 - add_action( 'current_screen', [ $this, 'track_first_editor_open' ] );
91 - add_action( 'transition_post_status', [ $this, 'track_first_form_published' ], 10, 3 );
92 - add_action( 'save_post', [ $this, 'track_embed_styling_configured' ], 10, 2 );
93 -
94 - // Detect state-based events on admin load (dedup prevents repeat tracking).
95 - $this->detect_state_events();
96 79 }
97 80
98 81 /**
99 - * Get the shared BSF_Analytics_Events instance.
100 - *
101 - * Uses SureForms' Helper option methods so data stays in the
102 - * existing srfm_options row — zero migration required.
103 - *
104 - * @since 2.7.0
105 - * @return \BSF_Analytics_Events
106 - */
107 - public static function events() {
108 - if ( null === self::$events ) {
109 - if ( ! class_exists( 'BSF_Analytics_Events' ) ) {
110 - require_once SRFM_DIR . 'inc/lib/bsf-analytics/class-bsf-analytics-events.php';
111 - }
112 -
113 - self::$events = new \BSF_Analytics_Events(
114 - 'sureforms',
115 - [
116 - 'get' => [ Helper::class, 'get_srfm_option' ],
117 - 'update' => [ Helper::class, 'update_srfm_option' ],
118 - ]
119 - );
120 - }
121 - return self::$events;
122 - }
123 -
124 - /**
125 82 * Callback function to add SureForms specific analytics data.
126 83 *
127 84 * @param array $stats_data existing stats_data.
128 85 * @since 1.4.0
@@ -129,44 +86,22 @@
129 86 * @return array
130 87 */
131 88 public function add_srfm_analytics_data( $stats_data ) {
132 89 $stats_data['plugin_data']['sureforms'] = [
133 - 'free_version' => SRFM_VER,
134 - 'site_language' => get_locale(),
135 - 'most_used_anti_spam' => $this->most_used_anti_spam(),
136 - 'user_status' => $this->user_status(),
137 - 'pointer_popup_clicked' => $this->pointer_popup_clicked(),
90 + 'free_version' => SRFM_VER,
91 + 'site_language' => get_locale(),
92 + 'most_used_anti_spam' => $this->most_used_anti_spam(),
138 93 ];
139 94 $stats_data['plugin_data']['sureforms']['numeric_values'] = [
140 - 'total_forms' => wp_count_posts( SRFM_FORMS_POST_TYPE )->publish ?? 0,
141 - 'instant_forms_enabled' => $this->instant_forms_enabled(),
142 - 'forms_using_custom_css' => $this->forms_using_custom_css(),
143 - 'ai_generated_forms' => $this->ai_generated_forms(),
144 - 'ai_generated_payment_forms' => $this->ai_generated_forms( 'payments' ),
145 - 'payment_forms' => $this->get_payment_forms_count(),
146 - 'total_entries' => Entries::get_total_entries_by_status(),
147 - 'restricted_forms' => $this->get_restricted_forms(),
148 - 'embed_styling_gb_default' => self::embed_styling_gutenberg_count( 'default' ),
149 - 'embed_styling_el_default' => self::embed_styling_elementor_count( 'default' ),
150 - 'embed_styling_br_default' => self::embed_styling_bricks_count( 'default' ),
95 + 'total_forms' => wp_count_posts( SRFM_FORMS_POST_TYPE )->publish ?? 0,
96 + 'instant_forms_enabled' => $this->instant_forms_enabled(),
97 + 'forms_using_custom_css' => $this->forms_using_custom_css(),
98 + 'ai_generated_forms' => $this->ai_generated_forms(),
99 + 'total_entries' => Entries::get_total_entries_by_status(),
151 100 ];
152 101
153 102 $stats_data['plugin_data']['sureforms'] = array_merge_recursive( $stats_data['plugin_data']['sureforms'], $this->global_settings_data() );
154 - // Add KPI tracking data.
155 - $kpi_data = $this->get_kpi_tracking_data();
156 - if ( ! empty( $kpi_data ) ) {
157 - $stats_data['plugin_data']['sureforms']['kpi_records'] = $kpi_data;
158 - }
159 103
160 - // Build learn progress snapshot.
161 - $this->get_learn_tracking_data();
162 -
163 - // Flush pending events into payload (only if any exist).
164 - $pending_events = self::events()->flush_pending();
165 - if ( ! empty( $pending_events ) ) {
166 - $stats_data['plugin_data']['sureforms']['events_record'] = $pending_events;
167 - }
168 -
169 104 return $stats_data;
170 105 }
171 106
172 107 /**
@@ -189,15 +124,12 @@
189 124
190 125 /**
191 126 * Return total number of ai generated forms.
192 127 *
193 - * @param string $form_type Form type to check.
194 - *
195 128 * @since 1.4.0
196 129 * @return int
197 130 */
198 - public function ai_generated_forms( $form_type = '' ) {
199 - $form_type = empty( $form_type ) || ! is_string( $form_type ) ? '' : $form_type;
131 + public function ai_generated_forms() {
200 132 $meta_query = [
201 133 [
202 134 'key' => '_srfm_is_ai_generated',
203 135 'value' => '',
@@ -204,13 +136,8 @@
204 136 'compare' => '!=', // Checks if the value is NOT empty.
205 137 ],
206 138 ];
207 139
208 - if ( 'payments' === $form_type ) {
209 - $search = 'wp:srfm/payment';
210 - return $this->custom_wp_query_total_posts_with_search( $meta_query, $search );
211 - }
212 -
213 140 return $this->custom_wp_query_total_posts( $meta_query );
214 141 }
215 142
216 143 /**
@@ -296,167 +223,8 @@
296 223 return $this->custom_wp_query_total_posts( $meta_query );
297 224 }
298 225
299 226 /**
300 - * Count Gutenberg srfm/form embed blocks using a specific formTheme.
301 - *
302 - * When formTheme is 'inherit' (the block.json default), WordPress does not
303 - * serialize it in the block comment. So only 'default' and 'custom' appear.
304 - *
305 - * Counts individual embed blocks (not pages), since a single page can
306 - * contain multiple form embeds each with different styling.
307 - *
308 - * @param string $theme Theme slug to count ('default' or 'custom').
309 - * @since 2.7.0
310 - * @return int
311 - */
312 - public static function embed_styling_gutenberg_count( $theme ) {
313 - global $wpdb;
314 -
315 - $cache_key = 'embed_styling_gb_' . $theme;
316 - $cached_result = wp_cache_get( $cache_key, 'sureforms' );
317 -
318 - if ( false !== $cached_result ) {
319 - return (int) $cached_result;
320 - }
321 -
322 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- No WP_Query alternative for cross-post-type content search with multiple LIKE conditions.
323 - $posts = $wpdb->get_col(
324 - $wpdb->prepare(
325 - "SELECT post_content FROM {$wpdb->posts}
326 - WHERE post_status = 'publish'
327 - AND post_content LIKE %s
328 - AND post_content LIKE %s",
329 - '%' . $wpdb->esc_like( 'wp:srfm/form' ) . '%',
330 - '%' . $wpdb->esc_like( '"formTheme":"' . $theme . '"' ) . '%'
331 - )
332 - );
333 -
334 - $count = 0;
335 - $escaped_theme = preg_quote( $theme, '/' );
336 - foreach ( $posts as $content ) {
337 - $count += preg_match_all( '/<!--\s*wp:srfm\/form\s+\{[^}]*"formTheme"\s*:\s*"' . $escaped_theme . '"/', $content );
338 - }
339 -
340 - wp_cache_set( $cache_key, $count, 'sureforms', HOUR_IN_SECONDS );
341 -
342 - return $count;
343 - }
344 -
345 - /**
346 - * Count Elementor sureforms_form widgets using a specific formTheme.
347 - *
348 - * Searches _elementor_data post meta for sureforms_form widgets with
349 - * a non-inherit formTheme. Counts individual widgets (not pages).
350 - *
351 - * @param string $theme Theme slug to count ('default' or 'custom').
352 - * @since 2.7.0
353 - * @return int
354 - */
355 - public static function embed_styling_elementor_count( $theme ) {
356 - global $wpdb;
357 -
358 - $cache_key = 'embed_styling_el_' . $theme;
359 - $cached_result = wp_cache_get( $cache_key, 'sureforms' );
360 -
361 - if ( false !== $cached_result ) {
362 - return (int) $cached_result;
363 - }
364 -
365 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- No WP_Query alternative for post meta content search with multiple LIKE conditions.
366 - $meta_values = $wpdb->get_col(
367 - $wpdb->prepare(
368 - "SELECT pm.meta_value FROM {$wpdb->postmeta} pm
369 - INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
370 - WHERE p.post_status = 'publish'
371 - AND pm.meta_key = '_elementor_data'
372 - AND pm.meta_value LIKE %s
373 - AND pm.meta_value LIKE %s",
374 - '%' . $wpdb->esc_like( 'sureforms_form' ) . '%',
375 - '%' . $wpdb->esc_like( '"formTheme":"' . $theme . '"' ) . '%'
376 - )
377 - );
378 -
379 - $count = 0;
380 - $escaped_theme = preg_quote( $theme, '/' );
381 - foreach ( $meta_values as $json ) {
382 - // Count widget instances with the specific formTheme in the JSON.
383 - $count += preg_match_all( '/"widgetType"\s*:\s*"sureforms_form"[^}]*"formTheme"\s*:\s*"' . $escaped_theme . '"/', $json );
384 - }
385 -
386 - wp_cache_set( $cache_key, $count, 'sureforms', HOUR_IN_SECONDS );
387 -
388 - return $count;
389 - }
390 -
391 - /**
392 - * Count Bricks sureforms elements using a specific formTheme.
393 - *
394 - * Searches _bricks_page_content_2 post meta (serialized PHP) for
395 - * sureforms elements with a non-inherit formTheme. Counts individual elements.
396 - *
397 - * @param string $theme Theme slug to count ('default' or 'custom').
398 - * @since 2.7.0
399 - * @return int
400 - */
401 - public static function embed_styling_bricks_count( $theme ) {
402 - global $wpdb;
403 -
404 - $cache_key = 'embed_styling_br_' . $theme;
405 - $cached_result = wp_cache_get( $cache_key, 'sureforms' );
406 -
407 - if ( false !== $cached_result ) {
408 - return (int) $cached_result;
409 - }
410 -
411 - // Bricks stores element data as serialized PHP in _bricks_page_content_2.
412 - // In serialized format: s:9:"formTheme";s:7:"default" (for 'default' theme).
413 - $serialized_theme = sprintf( '"formTheme";s:%d:"%s"', strlen( $theme ), $theme );
414 -
415 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- No WP_Query alternative for post meta content search with multiple LIKE conditions.
416 - $meta_values = $wpdb->get_col(
417 - $wpdb->prepare(
418 - "SELECT pm.meta_value FROM {$wpdb->postmeta} pm
419 - INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
420 - WHERE p.post_status = 'publish'
421 - AND pm.meta_key = '_bricks_page_content_2'
422 - AND pm.meta_value LIKE %s
423 - AND pm.meta_value LIKE %s",
424 - '%' . $wpdb->esc_like( '"sureforms"' ) . '%',
425 - '%' . $wpdb->esc_like( $serialized_theme ) . '%'
426 - )
427 - );
428 -
429 - $count = 0;
430 - $escaped_serial_theme = preg_quote( $serialized_theme, '/' );
431 - foreach ( $meta_values as $data ) {
432 - $count += preg_match_all( '/' . $escaped_serial_theme . '/', $data );
433 - }
434 -
435 - wp_cache_set( $cache_key, $count, 'sureforms', HOUR_IN_SECONDS );
436 -
437 - return $count;
438 - }
439 -
440 - /**
441 - * Return total number of restricted forms.
442 - *
443 - * @since 1.10.1
444 - * @return int
445 - */
446 - public function get_restricted_forms() {
447 - $meta_query = [
448 - [
449 - 'key' => '_srfm_form_restriction',
450 - 'value' => '"status":true',
451 - 'compare' => 'LIKE',
452 - ],
453 - ];
454 -
455 - return $this->custom_wp_query_total_posts( $meta_query );
456 - }
457 -
458 - /**
459 227 * Generates global setting data for analytics
460 228 *
461 229 * @since 1.4.0
462 230 * @return array
@@ -484,292 +252,12 @@
484 252
485 253 $validation_messages = get_option( 'srfm_default_dynamic_block_option', [] );
486 254 $global_data['boolean_values']['custom_validation_message'] = ! empty( $validation_messages ) && is_array( $validation_messages );
487 255
488 - // Payment analytics - check if any payment method is enabled.
489 - $global_data['boolean_values']['stripe_enabled'] = $this->is_stripe_enabled();
490 -
491 256 return $global_data;
492 257 }
493 258
494 259 /**
495 - * Returns user status.
496 - *
497 - * @since 1.8.0
498 - * @return string
499 - */
500 - public function user_status() {
501 - // First, check if user_active is already set in srfm_options.
502 - if ( Helper::get_srfm_option( 'user_active', false ) ) {
503 - return 'active';
504 - }
505 - // Get up to 10 published SureForms.
506 - $forms = get_posts(
507 - [
508 - 'post_type' => SRFM_FORMS_POST_TYPE,
509 - 'posts_per_page' => 10,
510 - 'post_status' => 'publish',
511 - ]
512 - );
513 - if ( empty( $forms ) ) {
514 - return 'inactive';
515 - }
516 - foreach ( $forms as $form ) {
517 - if ( ! get_post_meta( $form->ID, '_astra_sites_imported_post', true ) ) {
518 - // Mark user as active in srfm_options.
519 - Helper::update_srfm_option( 'user_active', true );
520 - return 'active';
521 - }
522 - }
523 - return 'inactive';
524 - }
525 -
526 - /**
527 - * Return pointer popup clicked status.
528 - *
529 - * @return string pointer click status.
530 - * @since 1.8.0
531 - */
532 - public function pointer_popup_clicked() {
533 - // Get both values from srfm_options.
534 - $accepted = Helper::get_srfm_option( 'pointer_popup_accepted', false );
535 - $dismissed = Helper::get_srfm_option( 'pointer_popup_dismissed', false );
536 - // If neither action has occurred.
537 - if ( ! $accepted && ! $dismissed ) {
538 - return '';
539 - }
540 -
541 - // If both are set, return the most recent one.
542 - if ( $accepted && $dismissed ) {
543 - return $accepted > $dismissed ? 'accepted' : 'dismissed';
544 - }
545 -
546 - // If only one is set, return it.
547 - return $accepted ? 'accepted' : 'dismissed';
548 - }
549 -
550 - /**
551 - * Runs a custom WP_Query to fetch the total number of posts matching the given meta query and optional search string.
552 - *
553 - * This function is used to count SureForms posts based on specific meta query conditions.
554 - * Optionally, a search string can be included to further filter results by keyword match.
555 - *
556 - * @since 2.0.0
557 - *
558 - * @param array $meta_query Meta query array for WP_Query.
559 - * @param string $search Optional. Search string for WP_Query. Default empty.
560 - * @return int The number of matching posts.
561 - */
562 - public function custom_wp_query_total_posts_with_search( $meta_query = [], $search = '' ) {
563 - $args = [
564 - 'post_type' => SRFM_FORMS_POST_TYPE,
565 - 'post_status' => 'publish',
566 - 'posts_per_page' => -1,
567 - ];
568 -
569 - if ( ! empty( $meta_query ) && is_array( $meta_query ) ) {
570 - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query required as we need to fetch count of nested data.
571 - $args['meta_query'] = $meta_query;
572 - }
573 -
574 - // If search string is provided, add it to the query.
575 - if ( ! empty( $search ) ) {
576 - $args['s'] = sanitize_text_field( $search );
577 - }
578 -
579 - $query = new \WP_Query( $args );
580 - $posts_count = $query->found_posts;
581 -
582 - wp_reset_postdata();
583 -
584 - return $posts_count;
585 - }
586 -
587 - /**
588 - * Get the total number of forms that utilize payment blocks.
589 - *
590 - * This function searches for forms containing the payment block identifier
591 - * ('wp:srfm/payment') to determine how many forms include payment capabilities.
592 - *
593 - * @since 2.0.0
594 - * @return int The number of forms that contain payment blocks.
595 - */
596 - public function get_payment_forms_count() {
597 - $search = 'wp:srfm/payment';
598 - // Runs a custom WP_Query to find the count of forms with payment block.
599 - return $this->custom_wp_query_total_posts_with_search( [], $search );
600 - }
601 -
602 - /**
603 - * Track first time a user opens the form editor.
604 - *
605 - * @since 2.5.1
606 - * @return void
607 - */
608 - public function track_first_editor_open() {
609 - $screen = get_current_screen();
610 - if ( $screen && 'sureforms_form' === $screen->id ) {
611 - self::events()->track( 'first_form_editor_opened' );
612 - }
613 - }
614 -
615 - /**
616 - * Track first time a form is published (activation event).
617 - *
618 - * @param string $new_status New post status.
619 - * @param string $old_status Old post status.
620 - * @param \WP_Post $post Post object.
621 - * @since 2.5.1
622 - * @return void
623 - */
624 - public function track_first_form_published( $new_status, $old_status, $post ) {
625 - if ( 'publish' !== $new_status || 'publish' === $old_status || SRFM_FORMS_POST_TYPE !== $post->post_type ) {
626 - return;
627 - }
628 -
629 - $is_ai = ! empty( get_post_meta( $post->ID, '_srfm_is_ai_generated', true ) );
630 - $block_count = substr_count( $post->post_content, '<!-- wp:srfm/' );
631 -
632 - // Time-to-value: days between install and first form published.
633 - $install_time = get_site_option( 'sureforms_usage_installed_time', 0 );
634 - $days_since_install = 0;
635 - if ( $install_time > 0 ) {
636 - $days_since_install = (int) floor( ( time() - $install_time ) / DAY_IN_SECONDS );
637 - }
638 -
639 - self::events()->track(
640 - 'first_form_published',
641 - (string) $post->ID,
642 - [
643 - 'is_ai_generated' => (string) (int) $is_ai,
644 - 'block_count' => (string) $block_count,
645 - 'days_since_install' => (string) $days_since_install,
646 - ]
647 - );
648 - }
649 -
650 - /**
651 - * Track embed styling configuration when a post/page is saved.
652 - *
653 - * Detects custom formTheme in Gutenberg blocks (post_content),
654 - * Elementor widgets (_elementor_data meta), and Bricks elements
655 - * (_bricks_page_content_2 meta). Re-tracks on each save by flushing
656 - * the dedup flag.
657 - *
658 - * @param int $post_id Post ID.
659 - * @param \WP_Post $post Post object.
660 - * @since 2.7.0
661 - * @return void
662 - */
663 - public function track_embed_styling_configured( $post_id, $post ) {
664 - if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
665 - return;
666 - }
667 -
668 - if ( 'publish' !== $post->post_status ) {
669 - return;
670 - }
671 -
672 - $themes = [];
673 - $source = '';
674 -
675 - // Check Gutenberg blocks in post_content.
676 - if ( preg_match_all( '/<!--\s*wp:srfm\/form\s+\{[^}]*"formTheme"\s*:\s*"(?!inherit")([^"]*)"/', $post->post_content, $matches ) ) {
677 - $themes = array_count_values( $matches[1] );
678 - $source = 'gutenberg';
679 - }
680 -
681 - // Check Elementor widgets in _elementor_data meta.
682 - if ( empty( $themes ) ) {
683 - $elementor_data = get_post_meta( $post_id, '_elementor_data', true );
684 - if ( is_string( $elementor_data )
685 - && preg_match_all( '/"widgetType"\s*:\s*"sureforms_form"[^}]*"formTheme"\s*:\s*"(?!inherit")([^"]*)"/', $elementor_data, $el_matches )
686 - ) {
687 - $themes = array_count_values( $el_matches[1] );
688 - $source = 'elementor';
689 - }
690 - }
691 -
692 - // Check Bricks elements in _bricks_page_content_2 meta (serialized PHP).
693 - if ( empty( $themes ) ) {
694 - $bricks_data = get_post_meta( $post_id, '_bricks_page_content_2', true );
695 - if ( is_array( $bricks_data ) ) {
696 - $bricks_themes = self::extract_bricks_form_themes( $bricks_data );
697 - if ( ! empty( $bricks_themes ) ) {
698 - $themes = array_count_values( $bricks_themes );
699 - $source = 'bricks';
700 - }
701 - }
702 - }
703 -
704 - if ( empty( $themes ) ) {
705 - return;
706 - }
707 -
708 - // Flush dedup so event is re-tracked on each meaningful save.
709 - self::events()->flush_pushed( [ 'embed_styling_configured' ] );
710 -
711 - self::events()->track(
712 - 'embed_styling_configured',
713 - (string) $post_id,
714 - [
715 - 'themes' => $themes,
716 - 'block_count' => array_sum( $themes ),
717 - 'source' => $source,
718 - ]
719 - );
720 - }
721 -
722 - /**
723 - * Extract non-inherit formTheme values from Bricks element data.
724 - *
725 - * Recursively walks the unserialized Bricks elements array looking for
726 - * sureforms elements with custom formTheme settings.
727 - *
728 - * @param array<mixed> $elements Bricks elements array.
729 - * @return array<string> List of formTheme values (non-inherit).
730 - * @since 2.7.0
731 - */
732 - private static function extract_bricks_form_themes( $elements ) {
733 - $themes = [];
734 -
735 - foreach ( $elements as $element ) {
736 - if ( ! is_array( $element ) ) {
737 - continue;
738 - }
739 -
740 - $name = $element['name'] ?? '';
741 - $settings = $element['settings'] ?? [];
742 - $form_theme = $settings['formTheme'] ?? '';
743 -
744 - if ( 'sureforms' === $name && ! empty( $form_theme ) && 'inherit' !== $form_theme ) {
745 - $themes[] = sanitize_text_field( $form_theme );
746 - }
747 -
748 - // Recurse into nested children.
749 - if ( ! empty( $element['children'] ) && is_array( $element['children'] ) ) {
750 - $themes = array_merge( $themes, self::extract_bricks_form_themes( $element['children'] ) );
751 - }
752 - }
753 -
754 - return $themes;
755 - }
756 -
757 - /**
758 - * Check if any payment method is enabled.
759 - *
760 - * This function checks if any payment gateway is connected and enabled.
761 - * Currently supports Stripe, but can be extended for other payment methods in the future.
762 - *
763 - * @since 2.0.0
764 - * @return bool True if any payment method is enabled, false otherwise.
765 - */
766 - private function is_stripe_enabled() {
767 - // Check if Stripe is connected.
768 - return class_exists( 'SRFM\Inc\Payments\Stripe\Stripe_Helper' ) && \SRFM\Inc\Payments\Stripe\Stripe_Helper::is_stripe_connected();
769 - }
770 -
771 - /**
772 260 * Runs custom WP_Query to fetch data as per requirement
773 261 *
774 262 * @param array $meta_query meta query array for WP_Query.
775 263 * @since 1.4.0
@@ -790,252 +278,5 @@
790 278 wp_reset_postdata();
791 279
792 280 return $posts_count;
793 281 }
794 -
795 - /**
796 - * Get KPI tracking data for the last 2 days (excluding today).
797 - *
798 - * @since 2.4.0
799 - * @return array KPI data organized by date.
800 - */
801 - private function get_kpi_tracking_data() {
802 - $kpi_data = [];
803 - $today = current_time( 'Y-m-d' );
804 -
805 - // Get data for yesterday and day before yesterday.
806 - for ( $i = 1; $i <= 2; $i++ ) {
807 - $date = gmdate( 'Y-m-d', strtotime( $today . ' -' . $i . ' days' ) );
808 -
809 - $kpi_data[ $date ] = [
810 - 'numeric_values' => [
811 - 'submissions' => $this->get_daily_submissions_count( $date ),
812 - ],
813 - ];
814 - }
815 -
816 - return $kpi_data;
817 - }
818 -
819 - /**
820 - * Get daily submissions count for a specific date.
821 - *
822 - * @param string $date Date in Y-m-d format.
823 - * @since 2.4.0
824 - * @return int Daily submissions count.
825 - */
826 - private function get_daily_submissions_count( $date ) {
827 - $start_date = $date . ' 00:00:00';
828 - $end_date = $date . ' 23:59:59';
829 -
830 - $where_conditions = [
831 - [
832 - [
833 - 'key' => 'created_at',
834 - 'compare' => '>=',
835 - 'value' => $start_date,
836 - ],
837 - [
838 - 'key' => 'created_at',
839 - 'compare' => '<=',
840 - 'value' => $end_date,
841 - ],
842 - ],
843 - ];
844 -
845 - return Entries::get_instance()->get_total_count( $where_conditions );
846 - }
847 -
848 - /**
849 - * Detect state-based events that can't use direct hooks.
850 - * Uses dedup in self::events()->track() — safe to call repeatedly.
851 - *
852 - * @since 2.5.1
853 - * @return void
854 - */
855 - 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 ] );
860 -
861 - // One-time: re-send onboarding_completed with full properties (v2).
862 - if ( ! Helper::get_srfm_option( 'onboarding_event_v2_flushed', false )
863 - && \SRFM\Inc\Onboarding::get_instance()->get_onboarding_status() ) {
864 - self::events()->flush_pushed( [ 'onboarding_completed' ] );
865 - Helper::update_srfm_option( 'onboarding_event_v2_flushed', true );
866 - }
867 -
868 - // onboarding_completed: detect completed state with full onboarding details.
869 - if ( \SRFM\Inc\Onboarding::get_instance()->get_onboarding_status() ) {
870 - $onboarding_props = [];
871 - $onboarding_analytics = Helper::get_srfm_option( 'onboarding_analytics', [] );
872 -
873 - if ( ! empty( $onboarding_analytics ) && is_array( $onboarding_analytics ) ) {
874 - if ( ! empty( $onboarding_analytics['skippedSteps'] ) && is_array( $onboarding_analytics['skippedSteps'] ) ) {
875 - $onboarding_props['skipped_steps'] = implode( ',', $onboarding_analytics['skippedSteps'] );
876 - }
877 -
878 - if ( isset( $onboarding_analytics['suremailInstalled'] ) ) {
879 - $onboarding_props['suremail_installed'] = (bool) $onboarding_analytics['suremailInstalled'] ? 'yes' : 'no';
880 - }
881 -
882 - if ( isset( $onboarding_analytics['accountConnected'] ) ) {
883 - $onboarding_props['account_connected'] = (bool) $onboarding_analytics['accountConnected'] ? 'yes' : 'no';
884 - }
885 -
886 - if ( isset( $onboarding_analytics['completed'] ) ) {
887 - $onboarding_props['completed'] = (bool) $onboarding_analytics['completed'] ? 'yes' : 'no';
888 - }
889 -
890 - if ( isset( $onboarding_analytics['exitedEarly'] ) ) {
891 - $onboarding_props['exited_early'] = (bool) $onboarding_analytics['exitedEarly'] ? 'yes' : 'no';
892 - }
893 -
894 - if ( ! empty( $onboarding_analytics['premiumFeatures']['selectedFeatures'] ) && is_array( $onboarding_analytics['premiumFeatures']['selectedFeatures'] ) ) {
895 - $premium = array_filter(
896 - $onboarding_analytics['premiumFeatures']['selectedFeatures'],
897 - static function( $f ) {
898 - return 'ai-form-generation' !== $f && 'entries' !== $f;
899 - }
900 - );
901 - $onboarding_props['selected_premium_features'] = implode( ',', $premium );
902 - $onboarding_props['premium_features_count'] = (string) count( $premium );
903 - }
904 - }
905 -
906 - self::events()->track( 'onboarding_completed', '', $onboarding_props );
907 - }
908 -
909 - // stripe_connected: detect connection state.
910 - if ( class_exists( '\SRFM\Inc\Payments\Stripe\Stripe_Helper' )
911 - && \SRFM\Inc\Payments\Stripe\Stripe_Helper::is_stripe_connected() ) {
912 - $mode = \SRFM\Inc\Payments\Stripe\Stripe_Helper::get_stripe_mode();
913 - self::events()->track( 'stripe_connected', ! empty( $mode ) ? $mode : 'live' );
914 - }
915 -
916 - // first_ai_form_generated: detect if any AI-generated form exists.
917 - // Guard with is_tracked() to skip the meta_query after the event is already tracked.
918 - if ( ! self::events()->is_tracked( 'first_ai_form_generated' ) ) {
919 - $ai_forms = get_posts(
920 - [
921 - 'post_type' => SRFM_FORMS_POST_TYPE,
922 - 'post_status' => 'any',
923 - 'posts_per_page' => 1,
924 - 'fields' => 'ids',
925 - 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Runs once per lifecycle via is_tracked guard.
926 - [
927 - 'key' => '_srfm_is_ai_generated',
928 - 'value' => '',
929 - 'compare' => '!=',
930 - ],
931 - ],
932 - ]
933 - );
934 - if ( ! empty( $ai_forms ) ) {
935 - self::events()->track( 'first_ai_form_generated' );
936 - }
937 - }
938 -
939 - // MCP / Abilities API first-enable events.
940 - $mcp_settings = get_option( 'srfm_mcp_settings_options', [] );
941 -
942 - if ( ! empty( $mcp_settings['srfm_abilities_api'] ) ) {
943 - self::events()->track( 'abilities_api_enabled' );
944 - }
945 -
946 - if ( ! empty( $mcp_settings['srfm_mcp_server'] ) ) {
947 - self::events()->track( 'mcp_server_enabled' );
948 - }
949 - }
950 -
951 - /**
952 - * Get learn section progress aggregated across all users.
953 - *
954 - * Queries all users with srfm_learn_progress meta, counts completions
955 - * per step. Called at analytics send time (bsf_core_stats filter).
956 - *
957 - * @since 2.8.0
958 - * @return void
959 - */
960 - private function get_learn_tracking_data() {
961 - // Only send when progress changed or event has never been tracked.
962 - $has_changed = get_transient( 'srfm_learn_progress_changed' );
963 - if ( ! $has_changed && self::events()->is_tracked( 'learn' ) ) {
964 - return;
965 - }
966 -
967 - $chapters = Learn::get_chapters_structure();
968 - $step_counts = [];
969 - $total_steps = 0;
970 -
971 - // Initialize step counters from canonical structure.
972 - foreach ( $chapters as $chapter ) {
973 - if ( ! isset( $chapter['id'], $chapter['steps'] ) || ! is_array( $chapter['steps'] ) ) {
974 - continue;
975 - }
976 - foreach ( $chapter['steps'] as $step ) {
977 - if ( ! isset( $step['id'] ) ) {
978 - continue;
979 - }
980 - $step_counts[ $chapter['id'] . '/' . $step['id'] ] = 0;
981 - ++$total_steps;
982 - }
983 - }
984 -
985 - // Query all users who have learn progress.
986 - $users = get_users(
987 - [
988 - '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.
989 - 'fields' => 'ID',
990 - ]
991 - );
992 -
993 - $users_with_progress = 0;
994 - $users_fully_completed = 0;
995 - $max_completed = 0;
996 -
997 - foreach ( $users as $user_id ) {
998 - $progress = get_user_meta( $user_id, 'srfm_learn_progress', true );
999 - if ( ! is_array( $progress ) || empty( $progress ) ) {
1000 - continue;
1001 - }
1002 -
1003 - $user_completed = 0;
1004 -
1005 - foreach ( $step_counts as $key => $count ) {
1006 - [ $chapter_id, $step_id ] = explode( '/', $key );
1007 - if ( ! empty( $progress[ $chapter_id ][ $step_id ] ) ) {
1008 - $step_counts[ $key ] = $count + 1;
1009 - ++$user_completed;
1010 - }
1011 - }
1012 -
1013 - if ( $user_completed > 0 ) {
1014 - ++$users_with_progress;
1015 - }
1016 - if ( $user_completed === $total_steps ) {
1017 - ++$users_fully_completed;
1018 - }
1019 - $max_completed = max( $max_completed, $user_completed );
1020 - }
1021 -
1022 - // Build flat properties — step keys use snake_case (hyphens → underscores).
1023 - $properties = [];
1024 - foreach ( $step_counts as $key => $count ) {
1025 - $step_id = explode( '/', $key )[1];
1026 - $properties[ str_replace( '-', '_', $step_id ) ] = (string) $count;
1027 - }
1028 -
1029 - $properties['users_with_progress'] = (string) $users_with_progress;
1030 - $properties['users_fully_completed'] = (string) $users_fully_completed;
1031 - $properties['total_steps'] = (string) $total_steps;
1032 -
1033 - // Flush dedup so event re-tracks with latest snapshot.
1034 - self::events()->flush_pushed( [ 'learn' ] );
1035 - self::events()->track( 'learn', (string) $max_completed, $properties );
1036 -
1037 - // Clear the change flag after tracking.
1038 - delete_transient( 'srfm_learn_progress_changed' );
1039 - }
1040 -
1041 282 }