PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.2.1
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.2.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 +104 -509 2.7.12.2.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,26 +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 - // Add KPI tracking data.
154 - $kpi_data = $this->get_kpi_tracking_data();
155 - if ( ! empty( $kpi_data ) ) {
156 - $stats_data['plugin_data']['sureforms']['kpi_records'] = $kpi_data;
157 - }
158 -
159 - // Flush pending events into payload (only if any exist).
160 - $pending_events = self::events()->flush_pending();
161 - if ( ! empty( $pending_events ) ) {
162 - $stats_data['plugin_data']['sureforms']['events_record'] = $pending_events;
163 - }
164 -
109 + // Add onboarding analytics data.
110 + $stats_data['plugin_data']['sureforms'] = array_merge_recursive( $stats_data['plugin_data']['sureforms'], $this->onboarding_analytics_data() );
165 111 return $stats_data;
166 112 }
167 113
168 114 /**
@@ -292,149 +238,8 @@
292 238 return $this->custom_wp_query_total_posts( $meta_query );
293 239 }
294 240
295 241 /**
296 - * Count Gutenberg srfm/form embed blocks using a specific formTheme.
297 - *
298 - * When formTheme is 'inherit' (the block.json default), WordPress does not
299 - * serialize it in the block comment. So only 'default' and 'custom' appear.
300 - *
301 - * Counts individual embed blocks (not pages), since a single page can
302 - * contain multiple form embeds each with different styling.
303 - *
304 - * @param string $theme Theme slug to count ('default' or 'custom').
305 - * @since 2.7.0
306 - * @return int
307 - */
308 - public static function embed_styling_gutenberg_count( $theme ) {
309 - global $wpdb;
310 -
311 - $cache_key = 'embed_styling_gb_' . $theme;
312 - $cached_result = wp_cache_get( $cache_key, 'sureforms' );
313 -
314 - if ( false !== $cached_result ) {
315 - return (int) $cached_result;
316 - }
317 -
318 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- No WP_Query alternative for cross-post-type content search with multiple LIKE conditions.
319 - $posts = $wpdb->get_col(
320 - $wpdb->prepare(
321 - "SELECT post_content FROM {$wpdb->posts}
322 - WHERE post_status = 'publish'
323 - AND post_content LIKE %s
324 - AND post_content LIKE %s",
325 - '%' . $wpdb->esc_like( 'wp:srfm/form' ) . '%',
326 - '%' . $wpdb->esc_like( '"formTheme":"' . $theme . '"' ) . '%'
327 - )
328 - );
329 -
330 - $count = 0;
331 - $escaped_theme = preg_quote( $theme, '/' );
332 - foreach ( $posts as $content ) {
333 - $count += preg_match_all( '/<!--\s*wp:srfm\/form\s+\{[^}]*"formTheme"\s*:\s*"' . $escaped_theme . '"/', $content );
334 - }
335 -
336 - wp_cache_set( $cache_key, $count, 'sureforms', HOUR_IN_SECONDS );
337 -
338 - return $count;
339 - }
340 -
341 - /**
342 - * Count Elementor sureforms_form widgets using a specific formTheme.
343 - *
344 - * Searches _elementor_data post meta for sureforms_form widgets with
345 - * a non-inherit formTheme. Counts individual widgets (not pages).
346 - *
347 - * @param string $theme Theme slug to count ('default' or 'custom').
348 - * @since 2.7.0
349 - * @return int
350 - */
351 - public static function embed_styling_elementor_count( $theme ) {
352 - global $wpdb;
353 -
354 - $cache_key = 'embed_styling_el_' . $theme;
355 - $cached_result = wp_cache_get( $cache_key, 'sureforms' );
356 -
357 - if ( false !== $cached_result ) {
358 - return (int) $cached_result;
359 - }
360 -
361 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- No WP_Query alternative for post meta content search with multiple LIKE conditions.
362 - $meta_values = $wpdb->get_col(
363 - $wpdb->prepare(
364 - "SELECT pm.meta_value FROM {$wpdb->postmeta} pm
365 - INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
366 - WHERE p.post_status = 'publish'
367 - AND pm.meta_key = '_elementor_data'
368 - AND pm.meta_value LIKE %s
369 - AND pm.meta_value LIKE %s",
370 - '%' . $wpdb->esc_like( 'sureforms_form' ) . '%',
371 - '%' . $wpdb->esc_like( '"formTheme":"' . $theme . '"' ) . '%'
372 - )
373 - );
374 -
375 - $count = 0;
376 - $escaped_theme = preg_quote( $theme, '/' );
377 - foreach ( $meta_values as $json ) {
378 - // Count widget instances with the specific formTheme in the JSON.
379 - $count += preg_match_all( '/"widgetType"\s*:\s*"sureforms_form"[^}]*"formTheme"\s*:\s*"' . $escaped_theme . '"/', $json );
380 - }
381 -
382 - wp_cache_set( $cache_key, $count, 'sureforms', HOUR_IN_SECONDS );
383 -
384 - return $count;
385 - }
386 -
387 - /**
388 - * Count Bricks sureforms elements using a specific formTheme.
389 - *
390 - * Searches _bricks_page_content_2 post meta (serialized PHP) for
391 - * sureforms elements with a non-inherit formTheme. Counts individual elements.
392 - *
393 - * @param string $theme Theme slug to count ('default' or 'custom').
394 - * @since 2.7.0
395 - * @return int
396 - */
397 - public static function embed_styling_bricks_count( $theme ) {
398 - global $wpdb;
399 -
400 - $cache_key = 'embed_styling_br_' . $theme;
401 - $cached_result = wp_cache_get( $cache_key, 'sureforms' );
402 -
403 - if ( false !== $cached_result ) {
404 - return (int) $cached_result;
405 - }
406 -
407 - // Bricks stores element data as serialized PHP in _bricks_page_content_2.
408 - // In serialized format: s:9:"formTheme";s:7:"default" (for 'default' theme).
409 - $serialized_theme = sprintf( '"formTheme";s:%d:"%s"', strlen( $theme ), $theme );
410 -
411 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- No WP_Query alternative for post meta content search with multiple LIKE conditions.
412 - $meta_values = $wpdb->get_col(
413 - $wpdb->prepare(
414 - "SELECT pm.meta_value FROM {$wpdb->postmeta} pm
415 - INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
416 - WHERE p.post_status = 'publish'
417 - AND pm.meta_key = '_bricks_page_content_2'
418 - AND pm.meta_value LIKE %s
419 - AND pm.meta_value LIKE %s",
420 - '%' . $wpdb->esc_like( '"sureforms"' ) . '%',
421 - '%' . $wpdb->esc_like( $serialized_theme ) . '%'
422 - )
423 - );
424 -
425 - $count = 0;
426 - $escaped_serial_theme = preg_quote( $serialized_theme, '/' );
427 - foreach ( $meta_values as $data ) {
428 - $count += preg_match_all( '/' . $escaped_serial_theme . '/', $data );
429 - }
430 -
431 - wp_cache_set( $cache_key, $count, 'sureforms', HOUR_IN_SECONDS );
432 -
433 - return $count;
434 - }
435 -
436 - /**
437 242 * Return total number of restricted forms.
438 243 *
439 244 * @since 1.10.1
440 245 * @return int
@@ -487,8 +292,110 @@
487 292 return $global_data;
488 293 }
489 294
490 295 /**
296 + * Generates onboarding analytics data
297 + *
298 + * @since 1.9.1
299 + * @return array
300 + */
301 + public function onboarding_analytics_data() {
302 + $onboarding_data = [];
303 + $analytics_option = Helper::get_srfm_option( 'onboarding_analytics', [] );
304 +
305 + if ( empty( $analytics_option ) ) {
306 + return $onboarding_data;
307 + }
308 +
309 + // Process skipped steps - store as an array.
310 + if ( ! empty( $analytics_option['skippedSteps'] ) && is_array( $analytics_option['skippedSteps'] ) ) {
311 + // Map step keys to more descriptive names.
312 + $step_mapping = [
313 + 'welcome' => 'Welcome',
314 + 'connect' => 'Connect',
315 + 'emailDelivery' => 'SureMail',
316 + 'premiumFeatures' => 'Features',
317 + 'done' => 'Done',
318 + ];
319 +
320 + // Transform the step keys to their descriptive names.
321 + $mapped_steps = array_map(
322 + static function( $step ) use ( $step_mapping ) {
323 + return $step_mapping[ $step ] ?? $step;
324 + },
325 + $analytics_option['skippedSteps']
326 + );
327 +
328 + // Store as an array.
329 + $onboarding_data['onboarding_skipped_steps'] = $mapped_steps;
330 + }
331 +
332 + // SureMail Installation Status.
333 + if ( isset( $analytics_option['suremailInstalled'] ) ) {
334 + $onboarding_data['boolean_values']['onboarding_suremail_installed'] = (bool) $analytics_option['suremailInstalled'];
335 + }
336 +
337 + // Account Connection Status.
338 + if ( isset( $analytics_option['accountConnected'] ) ) {
339 + $onboarding_data['boolean_values']['onboarding_account_connected'] = (bool) $analytics_option['accountConnected'];
340 + }
341 +
342 + // Onboarding Completion Status.
343 + if ( isset( $analytics_option['completed'] ) ) {
344 + $onboarding_data['boolean_values']['onboarding_completed'] = (bool) $analytics_option['completed'];
345 + }
346 +
347 + // Onboarding Early Exit Status.
348 + if ( isset( $analytics_option['exitedEarly'] ) ) {
349 + $onboarding_data['boolean_values']['onboarding_exited_early'] = (bool) $analytics_option['exitedEarly'];
350 + }
351 +
352 + // Onboarding Selected Premium Features.
353 + if ( ! empty( $analytics_option['premiumFeatures'] ) && ! empty( $analytics_option['premiumFeatures']['selectedFeatures'] ) ) {
354 + // Map feature IDs to more descriptive names - exclude free features.
355 + $feature_mapping = [
356 + // Starter features.
357 + 'multi_step_form' => 'Multi-step Forms',
358 + 'conditional_logic' => 'Conditional Fields',
359 + 'webhooks' => 'Webhooks',
360 + 'advanced_fields' => 'Advanced Fields',
361 +
362 + // Pro features.
363 + 'conversational_forms' => 'Conversational Forms',
364 + 'digital_signatures' => 'Digital Signatures',
365 +
366 + // Business features.
367 + 'calculations' => 'Calculators',
368 + 'user_registration' => 'User Registration and Login',
369 + 'custom_app' => 'Custom App',
370 + 'pdf_generation' => 'PDF Generation',
371 + ];
372 +
373 + // Filter out any free features that might have been included.
374 + $premium_features = array_filter(
375 + $analytics_option['premiumFeatures']['selectedFeatures'],
376 + static function( $feature ) {
377 + // Exclude free features (ai-form-generation and entries).
378 + return 'ai-form-generation' !== $feature && 'entries' !== $feature;
379 + }
380 + );
381 +
382 + // Transform the feature IDs to their descriptive names.
383 + $mapped_features = array_map(
384 + static function( $feature ) use ( $feature_mapping ) {
385 + return $feature_mapping[ $feature ] ?? $feature;
386 + },
387 + $premium_features
388 + );
389 +
390 + // Store as an array.
391 + $onboarding_data['onboarding_selected_premium_features'] = $mapped_features;
392 + }
393 +
394 + return $onboarding_data;
395 + }
396 +
397 + /**
491 398 * Returns user status.
492 399 *
493 400 * @since 1.8.0
494 401 * @return string
@@ -595,163 +502,8 @@
595 502 return $this->custom_wp_query_total_posts_with_search( [], $search );
596 503 }
597 504
598 505 /**
599 - * Track first time a user opens the form editor.
600 - *
601 - * @since 2.5.1
602 - * @return void
603 - */
604 - public function track_first_editor_open() {
605 - $screen = get_current_screen();
606 - if ( $screen && 'sureforms_form' === $screen->id ) {
607 - self::events()->track( 'first_form_editor_opened' );
608 - }
609 - }
610 -
611 - /**
612 - * Track first time a form is published (activation event).
613 - *
614 - * @param string $new_status New post status.
615 - * @param string $old_status Old post status.
616 - * @param \WP_Post $post Post object.
617 - * @since 2.5.1
618 - * @return void
619 - */
620 - public function track_first_form_published( $new_status, $old_status, $post ) {
621 - if ( 'publish' !== $new_status || 'publish' === $old_status || SRFM_FORMS_POST_TYPE !== $post->post_type ) {
622 - return;
623 - }
624 -
625 - $is_ai = ! empty( get_post_meta( $post->ID, '_srfm_is_ai_generated', true ) );
626 - $block_count = substr_count( $post->post_content, '<!-- wp:srfm/' );
627 -
628 - // Time-to-value: days between install and first form published.
629 - $install_time = get_site_option( 'sureforms_usage_installed_time', 0 );
630 - $days_since_install = 0;
631 - if ( $install_time > 0 ) {
632 - $days_since_install = (int) floor( ( time() - $install_time ) / DAY_IN_SECONDS );
633 - }
634 -
635 - self::events()->track(
636 - 'first_form_published',
637 - (string) $post->ID,
638 - [
639 - 'is_ai_generated' => (string) (int) $is_ai,
640 - 'block_count' => (string) $block_count,
641 - 'days_since_install' => (string) $days_since_install,
642 - ]
643 - );
644 - }
645 -
646 - /**
647 - * Track embed styling configuration when a post/page is saved.
648 - *
649 - * Detects custom formTheme in Gutenberg blocks (post_content),
650 - * Elementor widgets (_elementor_data meta), and Bricks elements
651 - * (_bricks_page_content_2 meta). Re-tracks on each save by flushing
652 - * the dedup flag.
653 - *
654 - * @param int $post_id Post ID.
655 - * @param \WP_Post $post Post object.
656 - * @since 2.7.0
657 - * @return void
658 - */
659 - public function track_embed_styling_configured( $post_id, $post ) {
660 - if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
661 - return;
662 - }
663 -
664 - if ( 'publish' !== $post->post_status ) {
665 - return;
666 - }
667 -
668 - $themes = [];
669 - $source = '';
670 -
671 - // Check Gutenberg blocks in post_content.
672 - if ( preg_match_all( '/<!--\s*wp:srfm\/form\s+\{[^}]*"formTheme"\s*:\s*"(?!inherit")([^"]*)"/', $post->post_content, $matches ) ) {
673 - $themes = array_count_values( $matches[1] );
674 - $source = 'gutenberg';
675 - }
676 -
677 - // Check Elementor widgets in _elementor_data meta.
678 - if ( empty( $themes ) ) {
679 - $elementor_data = get_post_meta( $post_id, '_elementor_data', true );
680 - if ( is_string( $elementor_data )
681 - && preg_match_all( '/"widgetType"\s*:\s*"sureforms_form"[^}]*"formTheme"\s*:\s*"(?!inherit")([^"]*)"/', $elementor_data, $el_matches )
682 - ) {
683 - $themes = array_count_values( $el_matches[1] );
684 - $source = 'elementor';
685 - }
686 - }
687 -
688 - // Check Bricks elements in _bricks_page_content_2 meta (serialized PHP).
689 - if ( empty( $themes ) ) {
690 - $bricks_data = get_post_meta( $post_id, '_bricks_page_content_2', true );
691 - if ( is_array( $bricks_data ) ) {
692 - $bricks_themes = self::extract_bricks_form_themes( $bricks_data );
693 - if ( ! empty( $bricks_themes ) ) {
694 - $themes = array_count_values( $bricks_themes );
695 - $source = 'bricks';
696 - }
697 - }
698 - }
699 -
700 - if ( empty( $themes ) ) {
701 - return;
702 - }
703 -
704 - // Flush dedup so event is re-tracked on each meaningful save.
705 - self::events()->flush_pushed( [ 'embed_styling_configured' ] );
706 -
707 - self::events()->track(
708 - 'embed_styling_configured',
709 - (string) $post_id,
710 - [
711 - 'themes' => $themes,
712 - 'block_count' => array_sum( $themes ),
713 - 'source' => $source,
714 - ]
715 - );
716 - }
717 -
718 - /**
719 - * Extract non-inherit formTheme values from Bricks element data.
720 - *
721 - * Recursively walks the unserialized Bricks elements array looking for
722 - * sureforms elements with custom formTheme settings.
723 - *
724 - * @param array<mixed> $elements Bricks elements array.
725 - * @return array<string> List of formTheme values (non-inherit).
726 - * @since 2.7.0
727 - */
728 - private static function extract_bricks_form_themes( $elements ) {
729 - $themes = [];
730 -
731 - foreach ( $elements as $element ) {
732 - if ( ! is_array( $element ) ) {
733 - continue;
734 - }
735 -
736 - $name = $element['name'] ?? '';
737 - $settings = $element['settings'] ?? [];
738 - $form_theme = $settings['formTheme'] ?? '';
739 -
740 - if ( 'sureforms' === $name && ! empty( $form_theme ) && 'inherit' !== $form_theme ) {
741 - $themes[] = sanitize_text_field( $form_theme );
742 - }
743 -
744 - // Recurse into nested children.
745 - if ( ! empty( $element['children'] ) && is_array( $element['children'] ) ) {
746 - $themes = array_merge( $themes, self::extract_bricks_form_themes( $element['children'] ) );
747 - }
748 - }
749 -
750 - return $themes;
751 - }
752 -
753 - /**
754 506 * Check if any payment method is enabled.
755 507 *
756 508 * This function checks if any payment gateway is connected and enabled.
757 509 * Currently supports Stripe, but can be extended for other payment methods in the future.
@@ -786,162 +538,5 @@
786 538 wp_reset_postdata();
787 539
788 540 return $posts_count;
789 541 }
790 -
791 - /**
792 - * Get KPI tracking data for the last 2 days (excluding today).
793 - *
794 - * @since 2.4.0
795 - * @return array KPI data organized by date.
796 - */
797 - private function get_kpi_tracking_data() {
798 - $kpi_data = [];
799 - $today = current_time( 'Y-m-d' );
800 -
801 - // Get data for yesterday and day before yesterday.
802 - for ( $i = 1; $i <= 2; $i++ ) {
803 - $date = gmdate( 'Y-m-d', strtotime( $today . ' -' . $i . ' days' ) );
804 -
805 - $kpi_data[ $date ] = [
806 - 'numeric_values' => [
807 - 'submissions' => $this->get_daily_submissions_count( $date ),
808 - ],
809 - ];
810 - }
811 -
812 - return $kpi_data;
813 - }
814 -
815 - /**
816 - * Get daily submissions count for a specific date.
817 - *
818 - * @param string $date Date in Y-m-d format.
819 - * @since 2.4.0
820 - * @return int Daily submissions count.
821 - */
822 - private function get_daily_submissions_count( $date ) {
823 - $start_date = $date . ' 00:00:00';
824 - $end_date = $date . ' 23:59:59';
825 -
826 - $where_conditions = [
827 - [
828 - [
829 - 'key' => 'created_at',
830 - 'compare' => '>=',
831 - 'value' => $start_date,
832 - ],
833 - [
834 - 'key' => 'created_at',
835 - 'compare' => '<=',
836 - 'value' => $end_date,
837 - ],
838 - ],
839 - ];
840 -
841 - return Entries::get_instance()->get_total_count( $where_conditions );
842 - }
843 -
844 - /**
845 - * Detect state-based events that can't use direct hooks.
846 - * Uses dedup in self::events()->track() — safe to call repeatedly.
847 - *
848 - * @since 2.5.1
849 - * @return void
850 - */
851 - 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 ] );
856 -
857 - // One-time: re-send onboarding_completed with full properties (v2).
858 - if ( ! Helper::get_srfm_option( 'onboarding_event_v2_flushed', false )
859 - && \SRFM\Inc\Onboarding::get_instance()->get_onboarding_status() ) {
860 - self::events()->flush_pushed( [ 'onboarding_completed' ] );
861 - Helper::update_srfm_option( 'onboarding_event_v2_flushed', true );
862 - }
863 -
864 - // onboarding_completed: detect completed state with full onboarding details.
865 - if ( \SRFM\Inc\Onboarding::get_instance()->get_onboarding_status() ) {
866 - $onboarding_props = [];
867 - $onboarding_analytics = Helper::get_srfm_option( 'onboarding_analytics', [] );
868 -
869 - if ( ! empty( $onboarding_analytics ) && is_array( $onboarding_analytics ) ) {
870 - if ( ! empty( $onboarding_analytics['skippedSteps'] ) && is_array( $onboarding_analytics['skippedSteps'] ) ) {
871 - $onboarding_props['skipped_steps'] = implode( ',', $onboarding_analytics['skippedSteps'] );
872 - }
873 -
874 - if ( isset( $onboarding_analytics['suremailInstalled'] ) ) {
875 - $onboarding_props['suremail_installed'] = (bool) $onboarding_analytics['suremailInstalled'] ? 'yes' : 'no';
876 - }
877 -
878 - if ( isset( $onboarding_analytics['accountConnected'] ) ) {
879 - $onboarding_props['account_connected'] = (bool) $onboarding_analytics['accountConnected'] ? 'yes' : 'no';
880 - }
881 -
882 - if ( isset( $onboarding_analytics['completed'] ) ) {
883 - $onboarding_props['completed'] = (bool) $onboarding_analytics['completed'] ? 'yes' : 'no';
884 - }
885 -
886 - if ( isset( $onboarding_analytics['exitedEarly'] ) ) {
887 - $onboarding_props['exited_early'] = (bool) $onboarding_analytics['exitedEarly'] ? 'yes' : 'no';
888 - }
889 -
890 - if ( ! empty( $onboarding_analytics['premiumFeatures']['selectedFeatures'] ) && is_array( $onboarding_analytics['premiumFeatures']['selectedFeatures'] ) ) {
891 - $premium = array_filter(
892 - $onboarding_analytics['premiumFeatures']['selectedFeatures'],
893 - static function( $f ) {
894 - return 'ai-form-generation' !== $f && 'entries' !== $f;
895 - }
896 - );
897 - $onboarding_props['selected_premium_features'] = implode( ',', $premium );
898 - $onboarding_props['premium_features_count'] = (string) count( $premium );
899 - }
900 - }
901 -
902 - self::events()->track( 'onboarding_completed', '', $onboarding_props );
903 - }
904 -
905 - // stripe_connected: detect connection state.
906 - if ( class_exists( '\SRFM\Inc\Payments\Stripe\Stripe_Helper' )
907 - && \SRFM\Inc\Payments\Stripe\Stripe_Helper::is_stripe_connected() ) {
908 - $mode = \SRFM\Inc\Payments\Stripe\Stripe_Helper::get_stripe_mode();
909 - self::events()->track( 'stripe_connected', ! empty( $mode ) ? $mode : 'live' );
910 - }
911 -
912 - // first_ai_form_generated: detect if any AI-generated form exists.
913 - // Guard with is_tracked() to skip the meta_query after the event is already tracked.
914 - if ( ! self::events()->is_tracked( 'first_ai_form_generated' ) ) {
915 - $ai_forms = get_posts(
916 - [
917 - 'post_type' => SRFM_FORMS_POST_TYPE,
918 - 'post_status' => 'any',
919 - 'posts_per_page' => 1,
920 - 'fields' => 'ids',
921 - 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Runs once per lifecycle via is_tracked guard.
922 - [
923 - 'key' => '_srfm_is_ai_generated',
924 - 'value' => '',
925 - 'compare' => '!=',
926 - ],
927 - ],
928 - ]
929 - );
930 - if ( ! empty( $ai_forms ) ) {
931 - self::events()->track( 'first_ai_form_generated' );
932 - }
933 - }
934 -
935 - // MCP / Abilities API first-enable events.
936 - $mcp_settings = get_option( 'srfm_mcp_settings_options', [] );
937 -
938 - if ( ! empty( $mcp_settings['srfm_abilities_api'] ) ) {
939 - self::events()->track( 'abilities_api_enabled' );
940 - }
941 -
942 - if ( ! empty( $mcp_settings['srfm_mcp_server'] ) ) {
943 - self::events()->track( 'mcp_server_enabled' );
944 - }
945 - }
946 -
947 542 }