PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.5.2
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.5.2
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 +120 -478 2.9.12.5.2 View file →
@@ -6,11 +6,11 @@
6 6 */
7 7
8 8 namespace SRFM\Admin;
9 9
10 +use SRFM\Inc\Analytics_Events;
10 11 use SRFM\Inc\Database\Tables\Entries;
11 12 use SRFM\Inc\Helper;
12 -use SRFM\Inc\Learn;
13 13 use SRFM\Inc\Traits\Get_Instance;
14 14
15 15 if ( ! defined( 'ABSPATH' ) ) {
16 16 exit; // Exit if accessed directly.
@@ -23,15 +23,8 @@
23 23 class Analytics {
24 24 use Get_Instance;
25 25
26 26 /**
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 27 * Class constructor.
35 28 *
36 29 * @return void
37 30 * @since 1.4.0
@@ -43,10 +36,10 @@
43 36 if ( ! class_exists( 'BSF_Analytics_Loader' ) ) {
44 37 require_once SRFM_DIR . 'inc/lib/bsf-analytics/class-bsf-analytics-loader.php';
45 38 }
46 39
47 - if ( ! class_exists( 'BSF_Admin_Notices' ) ) {
48 - require_once SRFM_DIR . 'inc/lib/astra-notices/class-bsf-admin-notices.php';
40 + if ( ! class_exists( 'Astra_Notices' ) ) {
41 + require_once SRFM_DIR . 'inc/lib/astra-notices/class-astra-notices.php';
49 42 }
50 43
51 44 add_filter(
52 45 'uds_survey_allowed_screens',
@@ -88,9 +81,8 @@
88 81
89 82 // Event tracking hooks.
90 83 add_action( 'current_screen', [ $this, 'track_first_editor_open' ] );
91 84 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 85
94 86 // Detect state-based events on admin load (dedup prevents repeat tracking).
95 87 $this->detect_state_events();
96 88 }
@@ -95,34 +87,8 @@
95 87 $this->detect_state_events();
96 88 }
97 89
98 90 /**
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 91 * Callback function to add SureForms specific analytics data.
126 92 *
127 93 * @param array $stats_data existing stats_data.
128 94 * @since 1.4.0
@@ -144,14 +110,14 @@
144 110 'ai_generated_payment_forms' => $this->ai_generated_forms( 'payments' ),
145 111 'payment_forms' => $this->get_payment_forms_count(),
146 112 'total_entries' => Entries::get_total_entries_by_status(),
147 113 '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' ),
151 114 ];
152 115
153 116 $stats_data['plugin_data']['sureforms'] = array_merge_recursive( $stats_data['plugin_data']['sureforms'], $this->global_settings_data() );
117 + // Add onboarding analytics data.
118 + $stats_data['plugin_data']['sureforms'] = array_merge_recursive( $stats_data['plugin_data']['sureforms'], $this->onboarding_analytics_data() );
119 +
154 120 // Add KPI tracking data.
155 121 $kpi_data = $this->get_kpi_tracking_data();
156 122 if ( ! empty( $kpi_data ) ) {
157 123 $stats_data['plugin_data']['sureforms']['kpi_records'] = $kpi_data;
@@ -156,13 +122,10 @@
156 122 if ( ! empty( $kpi_data ) ) {
157 123 $stats_data['plugin_data']['sureforms']['kpi_records'] = $kpi_data;
158 124 }
159 125
160 - // Build learn progress snapshot.
161 - $this->get_learn_tracking_data();
162 -
163 126 // Flush pending events into payload (only if any exist).
164 - $pending_events = self::events()->flush_pending();
127 + $pending_events = Analytics_Events::flush_pending();
165 128 if ( ! empty( $pending_events ) ) {
166 129 $stats_data['plugin_data']['sureforms']['events_record'] = $pending_events;
167 130 }
168 131
@@ -296,149 +259,8 @@
296 259 return $this->custom_wp_query_total_posts( $meta_query );
297 260 }
298 261
299 262 /**
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 263 * Return total number of restricted forms.
442 264 *
443 265 * @since 1.10.1
444 266 * @return int
@@ -491,8 +313,110 @@
491 313 return $global_data;
492 314 }
493 315
494 316 /**
317 + * Generates onboarding analytics data
318 + *
319 + * @since 1.9.1
320 + * @return array
321 + */
322 + public function onboarding_analytics_data() {
323 + $onboarding_data = [];
324 + $analytics_option = Helper::get_srfm_option( 'onboarding_analytics', [] );
325 +
326 + if ( empty( $analytics_option ) ) {
327 + return $onboarding_data;
328 + }
329 +
330 + // Process skipped steps - store as an array.
331 + if ( ! empty( $analytics_option['skippedSteps'] ) && is_array( $analytics_option['skippedSteps'] ) ) {
332 + // Map step keys to more descriptive names.
333 + $step_mapping = [
334 + 'welcome' => 'Welcome',
335 + 'connect' => 'Connect',
336 + 'emailDelivery' => 'SureMail',
337 + 'premiumFeatures' => 'Features',
338 + 'done' => 'Done',
339 + ];
340 +
341 + // Transform the step keys to their descriptive names.
342 + $mapped_steps = array_map(
343 + static function( $step ) use ( $step_mapping ) {
344 + return $step_mapping[ $step ] ?? $step;
345 + },
346 + $analytics_option['skippedSteps']
347 + );
348 +
349 + // Store as an array.
350 + $onboarding_data['onboarding_skipped_steps'] = $mapped_steps;
351 + }
352 +
353 + // SureMail Installation Status.
354 + if ( isset( $analytics_option['suremailInstalled'] ) ) {
355 + $onboarding_data['boolean_values']['onboarding_suremail_installed'] = (bool) $analytics_option['suremailInstalled'];
356 + }
357 +
358 + // Account Connection Status.
359 + if ( isset( $analytics_option['accountConnected'] ) ) {
360 + $onboarding_data['boolean_values']['onboarding_account_connected'] = (bool) $analytics_option['accountConnected'];
361 + }
362 +
363 + // Onboarding Completion Status.
364 + if ( isset( $analytics_option['completed'] ) ) {
365 + $onboarding_data['boolean_values']['onboarding_completed'] = (bool) $analytics_option['completed'];
366 + }
367 +
368 + // Onboarding Early Exit Status.
369 + if ( isset( $analytics_option['exitedEarly'] ) ) {
370 + $onboarding_data['boolean_values']['onboarding_exited_early'] = (bool) $analytics_option['exitedEarly'];
371 + }
372 +
373 + // Onboarding Selected Premium Features.
374 + if ( ! empty( $analytics_option['premiumFeatures'] ) && ! empty( $analytics_option['premiumFeatures']['selectedFeatures'] ) ) {
375 + // Map feature IDs to more descriptive names - exclude free features.
376 + $feature_mapping = [
377 + // Starter features.
378 + 'multi_step_form' => 'Multi-step Forms',
379 + 'conditional_logic' => 'Conditional Fields',
380 + 'webhooks' => 'Webhooks',
381 + 'advanced_fields' => 'Advanced Fields',
382 +
383 + // Pro features.
384 + 'conversational_forms' => 'Conversational Forms',
385 + 'digital_signatures' => 'Digital Signatures',
386 +
387 + // Business features.
388 + 'calculations' => 'Calculators',
389 + 'user_registration' => 'User Registration and Login',
390 + 'custom_app' => 'Custom App',
391 + 'pdf_generation' => 'PDF Generation',
392 + ];
393 +
394 + // Filter out any free features that might have been included.
395 + $premium_features = array_filter(
396 + $analytics_option['premiumFeatures']['selectedFeatures'],
397 + static function( $feature ) {
398 + // Exclude free features (ai-form-generation and entries).
399 + return 'ai-form-generation' !== $feature && 'entries' !== $feature;
400 + }
401 + );
402 +
403 + // Transform the feature IDs to their descriptive names.
404 + $mapped_features = array_map(
405 + static function( $feature ) use ( $feature_mapping ) {
406 + return $feature_mapping[ $feature ] ?? $feature;
407 + },
408 + $premium_features
409 + );
410 +
411 + // Store as an array.
412 + $onboarding_data['onboarding_selected_premium_features'] = $mapped_features;
413 + }
414 +
415 + return $onboarding_data;
416 + }
417 +
418 + /**
495 419 * Returns user status.
496 420 *
497 421 * @since 1.8.0
498 422 * @return string
@@ -607,14 +531,14 @@
607 531 */
608 532 public function track_first_editor_open() {
609 533 $screen = get_current_screen();
610 534 if ( $screen && 'sureforms_form' === $screen->id ) {
611 - self::events()->track( 'first_form_editor_opened' );
535 + Analytics_Events::track( 'first_form_editor_opened' );
612 536 }
613 537 }
614 538
615 539 /**
616 - * Track first time a form is published (activation event).
540 + * Track first time a form is published.
617 541 *
618 542 * @param string $new_status New post status.
619 543 * @param string $old_status Old post status.
620 544 * @param \WP_Post $post Post object.
@@ -628,134 +552,19 @@
628 552
629 553 $is_ai = ! empty( get_post_meta( $post->ID, '_srfm_is_ai_generated', true ) );
630 554 $block_count = substr_count( $post->post_content, '<!-- wp:srfm/' );
631 555
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(
556 + Analytics_Events::track(
640 557 'first_form_published',
641 558 (string) $post->ID,
642 559 [
643 - 'is_ai_generated' => (string) (int) $is_ai,
644 - 'block_count' => (string) $block_count,
645 - 'days_since_install' => (string) $days_since_install,
560 + 'is_ai_generated' => $is_ai,
561 + 'block_count' => $block_count,
646 562 ]
647 563 );
648 564 }
649 565
650 566 /**
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 567 * Check if any payment method is enabled.
759 568 *
760 569 * This function checks if any payment gateway is connected and enabled.
761 570 * Currently supports Stripe, but can be extended for other payment methods in the future.
@@ -846,65 +655,22 @@
846 655 }
847 656
848 657 /**
849 658 * Detect state-based events that can't use direct hooks.
850 - * Uses dedup in self::events()->track() — safe to call repeatedly.
659 + * Uses dedup in Analytics_Events::track() — safe to call repeatedly.
851 660 *
852 661 * @since 2.5.1
853 662 * @return void
854 663 */
855 664 private function detect_state_events() {
856 - // plugin_activated: dedup in self::events()->track() ensures this fires only once.
665 + // plugin_activated: dedup in Analytics_Events::track() ensures this fires only once.
857 666 $bsf_referrers = get_option( 'bsf_product_referers', [] );
858 667 $source = ! empty( $bsf_referrers['sureforms'] ) ? $bsf_referrers['sureforms'] : 'self';
859 - self::events()->track( 'plugin_activated', SRFM_VER, [ 'source' => $source ] );
668 + Analytics_Events::track( 'plugin_activated', SRFM_VER, [ 'source' => $source ] );
860 669
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.
670 + // onboarding_completed: detect completed state.
869 671 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 );
672 + Analytics_Events::track( 'onboarding_completed' );
907 673 }
908 674
909 675 // stripe_connected: detect connection state.
910 676 if ( class_exists( '\SRFM\Inc\Payments\Stripe\Stripe_Helper' )
@@ -909,133 +675,9 @@
909 675 // stripe_connected: detect connection state.
910 676 if ( class_exists( '\SRFM\Inc\Payments\Stripe\Stripe_Helper' )
911 677 && \SRFM\Inc\Payments\Stripe\Stripe_Helper::is_stripe_connected() ) {
912 678 $mode = \SRFM\Inc\Payments\Stripe\Stripe_Helper::get_stripe_mode();
913 - self::events()->track( 'stripe_connected', ! empty( $mode ) ? $mode : 'live' );
679 + Analytics_Events::track( 'stripe_connected', ! empty( $mode ) ? $mode : 'live' );
914 680 }
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 681 }
1040 682
1041 683 }