PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 1.13.0
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v1.13.0
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 +106 -795 trunk1.13.0 View file →
@@ -6,13 +6,10 @@
6 6 */
7 7
8 8 namespace SRFM\Admin;
9 9
10 -use SRFM\Inc\Database\Register;
11 10 use SRFM\Inc\Database\Tables\Entries;
12 -use SRFM\Inc\Form_Views;
13 11 use SRFM\Inc\Helper;
14 -use SRFM\Inc\Learn;
15 12 use SRFM\Inc\Traits\Get_Instance;
16 13
17 14 if ( ! defined( 'ABSPATH' ) ) {
18 15 exit; // Exit if accessed directly.
@@ -25,15 +22,8 @@
25 22 class Analytics {
26 23 use Get_Instance;
27 24
28 25 /**
29 - * BSF_Analytics_Events instance for one-time event tracking.
30 - *
31 - * @var \BSF_Analytics_Events|null
32 - */
33 - private static $events = null;
34 -
35 - /**
36 26 * Class constructor.
37 27 *
38 28 * @return void
39 29 * @since 1.4.0
@@ -45,10 +35,10 @@
45 35 if ( ! class_exists( 'BSF_Analytics_Loader' ) ) {
46 36 require_once SRFM_DIR . 'inc/lib/bsf-analytics/class-bsf-analytics-loader.php';
47 37 }
48 38
49 - if ( ! class_exists( 'BSF_Admin_Notices' ) ) {
50 - require_once SRFM_DIR . 'inc/lib/astra-notices/class-bsf-admin-notices.php';
39 + if ( ! class_exists( 'Astra_Notices' ) ) {
40 + require_once SRFM_DIR . 'inc/lib/astra-notices/class-astra-notices.php';
51 41 }
52 42
53 43 add_filter(
54 44 'uds_survey_allowed_screens',
@@ -86,45 +76,11 @@
86 76 ]
87 77 );
88 78
89 79 add_filter( 'bsf_core_stats', [ $this, 'add_srfm_analytics_data' ] );
90 -
91 - // Event tracking hooks.
92 - add_action( 'current_screen', [ $this, 'track_first_editor_open' ] );
93 - add_action( 'transition_post_status', [ $this, 'track_first_form_published' ], 10, 3 );
94 - add_action( 'save_post', [ $this, 'track_embed_styling_configured' ], 10, 2 );
95 -
96 - // Detect state-based events on admin load (dedup prevents repeat tracking).
97 - $this->detect_state_events();
98 80 }
99 81
100 82 /**
101 - * Get the shared BSF_Analytics_Events instance.
102 - *
103 - * Uses SureForms' Helper option methods so data stays in the
104 - * existing srfm_options row — zero migration required.
105 - *
106 - * @since 2.7.0
107 - * @return \BSF_Analytics_Events
108 - */
109 - public static function events() {
110 - if ( null === self::$events ) {
111 - if ( ! class_exists( 'BSF_Analytics_Events' ) ) {
112 - require_once SRFM_DIR . 'inc/lib/bsf-analytics/class-bsf-analytics-events.php';
113 - }
114 -
115 - self::$events = new \BSF_Analytics_Events(
116 - 'sureforms',
117 - [
118 - 'get' => [ Helper::class, 'get_srfm_option' ],
119 - 'update' => [ Helper::class, 'update_srfm_option' ],
120 - ]
121 - );
122 - }
123 - return self::$events;
124 - }
125 -
126 - /**
127 83 * Callback function to add SureForms specific analytics data.
128 84 *
129 85 * @param array $stats_data existing stats_data.
130 86 * @since 1.4.0
@@ -130,9 +86,9 @@
130 86 * @since 1.4.0
131 87 * @return array
132 88 */
133 89 public function add_srfm_analytics_data( $stats_data ) {
134 - $stats_data['plugin_data']['sureforms'] = [
90 + $stats_data['plugin_data']['sureforms'] = [
135 91 'free_version' => SRFM_VER,
136 92 'site_language' => get_locale(),
137 93 'most_used_anti_spam' => $this->most_used_anti_spam(),
138 94 'user_status' => $this->user_status(),
@@ -137,49 +93,22 @@
137 93 'most_used_anti_spam' => $this->most_used_anti_spam(),
138 94 'user_status' => $this->user_status(),
139 95 'pointer_popup_clicked' => $this->pointer_popup_clicked(),
140 96 ];
141 - // Every query against the entries table errors when the table is missing, so
142 - // resolve that once here rather than letting each caller below trip over it.
143 - $entries_table_missing = Register::is_entries_table_missing();
144 -
145 97 $stats_data['plugin_data']['sureforms']['numeric_values'] = [
146 - 'total_forms' => wp_count_posts( SRFM_FORMS_POST_TYPE )->publish ?? 0,
147 - 'instant_forms_enabled' => $this->instant_forms_enabled(),
148 - 'forms_using_custom_css' => $this->forms_using_custom_css(),
149 - 'ai_generated_forms' => $this->ai_generated_forms(),
150 - 'ai_generated_payment_forms' => $this->ai_generated_forms( 'payments' ),
151 - 'payment_forms' => $this->get_payment_forms_count(),
152 - 'total_entries' => $entries_table_missing ? 0 : Entries::get_total_entries_by_status(),
153 - 'restricted_forms' => $this->get_restricted_forms(),
154 - 'embed_styling_gb_default' => self::embed_styling_gutenberg_count( 'default' ),
155 - 'embed_styling_el_default' => self::embed_styling_elementor_count( 'default' ),
156 - 'embed_styling_br_default' => self::embed_styling_bricks_count( 'default' ),
157 - 'total_form_views' => $this->total_form_views(),
158 - 'forms_with_views' => $this->forms_with_views(),
98 + 'total_forms' => wp_count_posts( SRFM_FORMS_POST_TYPE )->publish ?? 0,
99 + 'instant_forms_enabled' => $this->instant_forms_enabled(),
100 + 'forms_using_custom_css' => $this->forms_using_custom_css(),
101 + 'ai_generated_forms' => $this->ai_generated_forms(),
102 + 'total_entries' => Entries::get_total_entries_by_status(),
103 + 'restricted_forms' => $this->get_restricted_forms(),
159 104 ];
160 105
161 - // Whether the entries table is currently missing. An event fires once when a
162 - // site first sees the notice; this is what shows the state persisting, and
163 - // catches a recurrence that the event's one-time dedup would swallow.
164 - $stats_data['plugin_data']['sureforms']['boolean_values']['db_entries_table_missing'] = $entries_table_missing;
165 -
166 106 $stats_data['plugin_data']['sureforms'] = array_merge_recursive( $stats_data['plugin_data']['sureforms'], $this->global_settings_data() );
167 - // Add KPI tracking data.
168 - $kpi_data = $this->get_kpi_tracking_data();
169 - if ( ! empty( $kpi_data ) ) {
170 - $stats_data['plugin_data']['sureforms']['kpi_records'] = $kpi_data;
171 - }
172 107
173 - // Build learn progress snapshot.
174 - $this->get_learn_tracking_data();
108 + // Add onboarding analytics data.
109 + $stats_data['plugin_data']['sureforms'] = array_merge_recursive( $stats_data['plugin_data']['sureforms'], $this->onboarding_analytics_data() );
175 110
176 - // Flush pending events into payload (only if any exist).
177 - $pending_events = self::events()->flush_pending();
178 - if ( ! empty( $pending_events ) ) {
179 - $stats_data['plugin_data']['sureforms']['events_record'] = $pending_events;
180 - }
181 -
182 111 return $stats_data;
183 112 }
184 113
185 114 /**
@@ -202,15 +131,12 @@
202 131
203 132 /**
204 133 * Return total number of ai generated forms.
205 134 *
206 - * @param string $form_type Form type to check.
207 - *
208 135 * @since 1.4.0
209 136 * @return int
210 137 */
211 - public function ai_generated_forms( $form_type = '' ) {
212 - $form_type = empty( $form_type ) || ! is_string( $form_type ) ? '' : $form_type;
138 + public function ai_generated_forms() {
213 139 $meta_query = [
214 140 [
215 141 'key' => '_srfm_is_ai_generated',
216 142 'value' => '',
@@ -217,13 +143,8 @@
217 143 'compare' => '!=', // Checks if the value is NOT empty.
218 144 ],
219 145 ];
220 146
221 - if ( 'payments' === $form_type ) {
222 - $search = 'wp:srfm/payment';
223 - return $this->custom_wp_query_total_posts_with_search( $meta_query, $search );
224 - }
225 -
226 147 return $this->custom_wp_query_total_posts( $meta_query );
227 148 }
228 149
229 150 /**
@@ -309,149 +230,8 @@
309 230 return $this->custom_wp_query_total_posts( $meta_query );
310 231 }
311 232
312 233 /**
313 - * Count Gutenberg srfm/form embed blocks using a specific formTheme.
314 - *
315 - * When formTheme is 'inherit' (the block.json default), WordPress does not
316 - * serialize it in the block comment. So only 'default' and 'custom' appear.
317 - *
318 - * Counts individual embed blocks (not pages), since a single page can
319 - * contain multiple form embeds each with different styling.
320 - *
321 - * @param string $theme Theme slug to count ('default' or 'custom').
322 - * @since 2.7.0
323 - * @return int
324 - */
325 - public static function embed_styling_gutenberg_count( $theme ) {
326 - global $wpdb;
327 -
328 - $cache_key = 'embed_styling_gb_' . $theme;
329 - $cached_result = wp_cache_get( $cache_key, 'sureforms' );
330 -
331 - if ( false !== $cached_result ) {
332 - return (int) $cached_result;
333 - }
334 -
335 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- No WP_Query alternative for cross-post-type content search with multiple LIKE conditions.
336 - $posts = $wpdb->get_col(
337 - $wpdb->prepare(
338 - "SELECT post_content FROM {$wpdb->posts}
339 - WHERE post_status = 'publish'
340 - AND post_content LIKE %s
341 - AND post_content LIKE %s",
342 - '%' . $wpdb->esc_like( 'wp:srfm/form' ) . '%',
343 - '%' . $wpdb->esc_like( '"formTheme":"' . $theme . '"' ) . '%'
344 - )
345 - );
346 -
347 - $count = 0;
348 - $escaped_theme = preg_quote( $theme, '/' );
349 - foreach ( $posts as $content ) {
350 - $count += preg_match_all( '/<!--\s*wp:srfm\/form\s+\{[^}]*"formTheme"\s*:\s*"' . $escaped_theme . '"/', $content );
351 - }
352 -
353 - wp_cache_set( $cache_key, $count, 'sureforms', HOUR_IN_SECONDS );
354 -
355 - return $count;
356 - }
357 -
358 - /**
359 - * Count Elementor sureforms_form widgets using a specific formTheme.
360 - *
361 - * Searches _elementor_data post meta for sureforms_form widgets with
362 - * a non-inherit formTheme. Counts individual widgets (not pages).
363 - *
364 - * @param string $theme Theme slug to count ('default' or 'custom').
365 - * @since 2.7.0
366 - * @return int
367 - */
368 - public static function embed_styling_elementor_count( $theme ) {
369 - global $wpdb;
370 -
371 - $cache_key = 'embed_styling_el_' . $theme;
372 - $cached_result = wp_cache_get( $cache_key, 'sureforms' );
373 -
374 - if ( false !== $cached_result ) {
375 - return (int) $cached_result;
376 - }
377 -
378 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- No WP_Query alternative for post meta content search with multiple LIKE conditions.
379 - $meta_values = $wpdb->get_col(
380 - $wpdb->prepare(
381 - "SELECT pm.meta_value FROM {$wpdb->postmeta} pm
382 - INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
383 - WHERE p.post_status = 'publish'
384 - AND pm.meta_key = '_elementor_data'
385 - AND pm.meta_value LIKE %s
386 - AND pm.meta_value LIKE %s",
387 - '%' . $wpdb->esc_like( 'sureforms_form' ) . '%',
388 - '%' . $wpdb->esc_like( '"formTheme":"' . $theme . '"' ) . '%'
389 - )
390 - );
391 -
392 - $count = 0;
393 - $escaped_theme = preg_quote( $theme, '/' );
394 - foreach ( $meta_values as $json ) {
395 - // Count widget instances with the specific formTheme in the JSON.
396 - $count += preg_match_all( '/"widgetType"\s*:\s*"sureforms_form"[^}]*"formTheme"\s*:\s*"' . $escaped_theme . '"/', $json );
397 - }
398 -
399 - wp_cache_set( $cache_key, $count, 'sureforms', HOUR_IN_SECONDS );
400 -
401 - return $count;
402 - }
403 -
404 - /**
405 - * Count Bricks sureforms elements using a specific formTheme.
406 - *
407 - * Searches _bricks_page_content_2 post meta (serialized PHP) for
408 - * sureforms elements with a non-inherit formTheme. Counts individual elements.
409 - *
410 - * @param string $theme Theme slug to count ('default' or 'custom').
411 - * @since 2.7.0
412 - * @return int
413 - */
414 - public static function embed_styling_bricks_count( $theme ) {
415 - global $wpdb;
416 -
417 - $cache_key = 'embed_styling_br_' . $theme;
418 - $cached_result = wp_cache_get( $cache_key, 'sureforms' );
419 -
420 - if ( false !== $cached_result ) {
421 - return (int) $cached_result;
422 - }
423 -
424 - // Bricks stores element data as serialized PHP in _bricks_page_content_2.
425 - // In serialized format: s:9:"formTheme";s:7:"default" (for 'default' theme).
426 - $serialized_theme = sprintf( '"formTheme";s:%d:"%s"', strlen( $theme ), $theme );
427 -
428 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- No WP_Query alternative for post meta content search with multiple LIKE conditions.
429 - $meta_values = $wpdb->get_col(
430 - $wpdb->prepare(
431 - "SELECT pm.meta_value FROM {$wpdb->postmeta} pm
432 - INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
433 - WHERE p.post_status = 'publish'
434 - AND pm.meta_key = '_bricks_page_content_2'
435 - AND pm.meta_value LIKE %s
436 - AND pm.meta_value LIKE %s",
437 - '%' . $wpdb->esc_like( '"sureforms"' ) . '%',
438 - '%' . $wpdb->esc_like( $serialized_theme ) . '%'
439 - )
440 - );
441 -
442 - $count = 0;
443 - $escaped_serial_theme = preg_quote( $serialized_theme, '/' );
444 - foreach ( $meta_values as $data ) {
445 - $count += preg_match_all( '/' . $escaped_serial_theme . '/', $data );
446 - }
447 -
448 - wp_cache_set( $cache_key, $count, 'sureforms', HOUR_IN_SECONDS );
449 -
450 - return $count;
451 - }
452 -
453 - /**
454 234 * Return total number of restricted forms.
455 235 *
456 236 * @since 1.10.1
457 237 * @return int
@@ -497,81 +277,111 @@
497 277
498 278 $validation_messages = get_option( 'srfm_default_dynamic_block_option', [] );
499 279 $global_data['boolean_values']['custom_validation_message'] = ! empty( $validation_messages ) && is_array( $validation_messages );
500 280
501 - // Payment analytics - check if any payment method is enabled.
502 - $global_data['boolean_values']['stripe_enabled'] = $this->is_stripe_enabled();
503 -
504 - // Delegated rather than read from $general_settings directly, so the telemetry
505 - // can never disagree with what the Forms list actually shows. That matters
506 - // because the default has already changed once: reimplementing the check here
507 - // means a future change has two places to be made and one to be forgotten.
508 - $global_data['boolean_values']['form_views_columns_enabled'] = Form_Views::get_instance()->is_tracking_enabled();
509 -
510 281 return $global_data;
511 282 }
512 283
513 284 /**
514 - * Total page views counted across all published forms.
285 + * Generates onboarding analytics data
515 286 *
516 - * Paired with `forms_with_views` and the existing `total_entries`, this is what
517 - * makes the conversion figure interpretable in the warehouse: a site with views
518 - * but no entries reads very differently from one with neither.
519 - *
520 - * Counts only published forms, so views left behind by a trashed or draft form
521 - * do not inflate the total against a denominator that no longer includes them.
522 - *
523 - * @since 2.12.6
524 - * @return int
287 + * @since 1.9.1
288 + * @return array
525 289 */
526 - public function total_form_views() {
527 - global $wpdb;
290 + public function onboarding_analytics_data() {
291 + $onboarding_data = [];
292 + $analytics_option = Helper::get_srfm_option( 'onboarding_analytics', [] );
528 293
529 - // Deliberately uncached: the only caller is the analytics payload builder,
530 - // which runs once per cron pass. A cache would never see a second read inside
531 - // a request, and any TTL would always have expired between passes — while
532 - // still going stale if a second caller ever appeared.
533 - // PHPCS: Ignore direct database query warning, as there is no built-in alternative.
534 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
535 - $total = $wpdb->get_var(
536 - $wpdb->prepare(
537 - "
538 - SELECT SUM( pm.meta_value )
539 - FROM {$wpdb->postmeta} pm
540 - INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
541 - WHERE pm.meta_key = %s
542 - AND p.post_type = %s
543 - AND p.post_status = 'publish'
544 - ",
545 - Form_Views::META_KEY,
546 - SRFM_FORMS_POST_TYPE
547 - )
548 - );
294 + if ( empty( $analytics_option ) ) {
295 + return $onboarding_data;
296 + }
549 297
550 - return Helper::get_integer_value( $total );
551 - }
298 + // Process skipped steps - store as an array.
299 + if ( ! empty( $analytics_option['skippedSteps'] ) && is_array( $analytics_option['skippedSteps'] ) ) {
300 + // Map step keys to more descriptive names.
301 + $step_mapping = [
302 + 'welcome' => 'Welcome',
303 + 'connect' => 'Connect',
304 + 'emailDelivery' => 'SureMail',
305 + 'premiumFeatures' => 'Features',
306 + 'done' => 'Done',
307 + ];
552 308
553 - /**
554 - * Number of published forms that have been viewed at least once.
555 - *
556 - * Distinguishes "the feature is on but nothing is embedded anywhere" from "the
557 - * beacon is firing" — a total alone cannot, because one busy form looks the
558 - * same as many quiet ones.
559 - *
560 - * @since 2.12.6
561 - * @return int
562 - */
563 - public function forms_with_views() {
564 - return $this->custom_wp_query_total_posts(
565 - [
566 - [
567 - 'key' => Form_Views::META_KEY,
568 - 'value' => 0,
569 - 'compare' => '>',
570 - 'type' => 'NUMERIC',
571 - ],
572 - ]
573 - );
309 + // Transform the step keys to their descriptive names.
310 + $mapped_steps = array_map(
311 + static function( $step ) use ( $step_mapping ) {
312 + return $step_mapping[ $step ] ?? $step;
313 + },
314 + $analytics_option['skippedSteps']
315 + );
316 +
317 + // Store as an array.
318 + $onboarding_data['onboarding_skipped_steps'] = $mapped_steps;
319 + }
320 +
321 + // SureMail Installation Status.
322 + if ( isset( $analytics_option['suremailInstalled'] ) ) {
323 + $onboarding_data['boolean_values']['onboarding_suremail_installed'] = (bool) $analytics_option['suremailInstalled'];
324 + }
325 +
326 + // Account Connection Status.
327 + if ( isset( $analytics_option['accountConnected'] ) ) {
328 + $onboarding_data['boolean_values']['onboarding_account_connected'] = (bool) $analytics_option['accountConnected'];
329 + }
330 +
331 + // Onboarding Completion Status.
332 + if ( isset( $analytics_option['completed'] ) ) {
333 + $onboarding_data['boolean_values']['onboarding_completed'] = (bool) $analytics_option['completed'];
334 + }
335 +
336 + // Onboarding Early Exit Status.
337 + if ( isset( $analytics_option['exitedEarly'] ) ) {
338 + $onboarding_data['boolean_values']['onboarding_exited_early'] = (bool) $analytics_option['exitedEarly'];
339 + }
340 +
341 + // Onboarding Selected Premium Features.
342 + if ( ! empty( $analytics_option['premiumFeatures'] ) && ! empty( $analytics_option['premiumFeatures']['selectedFeatures'] ) ) {
343 + // Map feature IDs to more descriptive names - exclude free features.
344 + $feature_mapping = [
345 + // Starter features.
346 + 'multi_step_form' => 'Multi-step Forms',
347 + 'conditional_logic' => 'Conditional Fields',
348 + 'webhooks' => 'Webhooks',
349 + 'advanced_fields' => 'Advanced Fields',
350 +
351 + // Pro features.
352 + 'conversational_forms' => 'Conversational Forms',
353 + 'digital_signatures' => 'Digital Signatures',
354 +
355 + // Business features.
356 + 'calculations' => 'Calculators',
357 + 'user_registration' => 'User Registration and Login',
358 + 'custom_app' => 'Custom App',
359 + 'pdf_generation' => 'PDF Generation',
360 + ];
361 +
362 + // Filter out any free features that might have been included.
363 + $premium_features = array_filter(
364 + $analytics_option['premiumFeatures']['selectedFeatures'],
365 + static function( $feature ) {
366 + // Exclude free features (ai-form-generation and entries).
367 + return 'ai-form-generation' !== $feature && 'entries' !== $feature;
368 + }
369 + );
370 +
371 + // Transform the feature IDs to their descriptive names.
372 + $mapped_features = array_map(
373 + static function( $feature ) use ( $feature_mapping ) {
374 + return $feature_mapping[ $feature ] ?? $feature;
375 + },
376 + $premium_features
377 + );
378 +
379 + // Store as an array.
380 + $onboarding_data['onboarding_selected_premium_features'] = $mapped_features;
381 + }
382 +
383 + return $onboarding_data;
574 384 }
575 385
576 386 /**
577 387 * Returns user status.
@@ -629,245 +439,8 @@
629 439 return $accepted ? 'accepted' : 'dismissed';
630 440 }
631 441
632 442 /**
633 - * Runs a custom WP_Query to fetch the total number of posts matching the given meta query and optional search string.
634 - *
635 - * This function is used to count SureForms posts based on specific meta query conditions.
636 - * Optionally, a search string can be included to further filter results by keyword match.
637 - *
638 - * @since 2.0.0
639 - *
640 - * @param array $meta_query Meta query array for WP_Query.
641 - * @param string $search Optional. Search string for WP_Query. Default empty.
642 - * @return int The number of matching posts.
643 - */
644 - public function custom_wp_query_total_posts_with_search( $meta_query = [], $search = '' ) {
645 - $args = [
646 - 'post_type' => SRFM_FORMS_POST_TYPE,
647 - 'post_status' => 'publish',
648 - 'posts_per_page' => -1,
649 - ];
650 -
651 - if ( ! empty( $meta_query ) && is_array( $meta_query ) ) {
652 - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query required as we need to fetch count of nested data.
653 - $args['meta_query'] = $meta_query;
654 - }
655 -
656 - // If search string is provided, add it to the query.
657 - if ( ! empty( $search ) ) {
658 - $args['s'] = sanitize_text_field( $search );
659 - }
660 -
661 - $query = new \WP_Query( $args );
662 - $posts_count = $query->found_posts;
663 -
664 - wp_reset_postdata();
665 -
666 - return $posts_count;
667 - }
668 -
669 - /**
670 - * Get the total number of forms that utilize payment blocks.
671 - *
672 - * This function searches for forms containing the payment block identifier
673 - * ('wp:srfm/payment') to determine how many forms include payment capabilities.
674 - *
675 - * @since 2.0.0
676 - * @return int The number of forms that contain payment blocks.
677 - */
678 - public function get_payment_forms_count() {
679 - $search = 'wp:srfm/payment';
680 - // Runs a custom WP_Query to find the count of forms with payment block.
681 - return $this->custom_wp_query_total_posts_with_search( [], $search );
682 - }
683 -
684 - /**
685 - * Track first time a user opens the form editor.
686 - *
687 - * @since 2.5.1
688 - * @return void
689 - */
690 - public function track_first_editor_open() {
691 - $screen = get_current_screen();
692 - if ( $screen && 'sureforms_form' === $screen->id ) {
693 - self::events()->track( 'first_form_editor_opened' );
694 - }
695 - }
696 -
697 - /**
698 - * Track first time a form is published (activation event).
699 - *
700 - * @param string $new_status New post status.
701 - * @param string $old_status Old post status.
702 - * @param \WP_Post $post Post object.
703 - * @since 2.5.1
704 - * @return void
705 - */
706 - public function track_first_form_published( $new_status, $old_status, $post ) {
707 - if ( 'publish' !== $new_status || 'publish' === $old_status || SRFM_FORMS_POST_TYPE !== $post->post_type ) {
708 - return;
709 - }
710 -
711 - $is_ai = ! empty( get_post_meta( $post->ID, '_srfm_is_ai_generated', true ) );
712 - $block_count = substr_count( $post->post_content, '<!-- wp:srfm/' );
713 -
714 - // Time-to-value: days between install and first form published.
715 - $install_time = get_site_option( 'sureforms_usage_installed_time', 0 );
716 - $days_since_install = 0;
717 - if ( $install_time > 0 ) {
718 - $days_since_install = (int) floor( ( time() - $install_time ) / DAY_IN_SECONDS );
719 - }
720 -
721 - self::events()->track(
722 - 'first_form_published',
723 - (string) $post->ID,
724 - [
725 - 'is_ai_generated' => (string) (int) $is_ai,
726 - 'block_count' => (string) $block_count,
727 - 'days_since_install' => (string) $days_since_install,
728 - ]
729 - );
730 - }
731 -
732 - /**
733 - * Track embed styling configuration when a post/page is saved.
734 - *
735 - * Detects custom formTheme in Gutenberg blocks (post_content),
736 - * Elementor widgets (_elementor_data meta), and Bricks elements
737 - * (_bricks_page_content_2 meta). Re-tracks on each save by flushing
738 - * the dedup flag.
739 - *
740 - * @param int $post_id Post ID.
741 - * @param \WP_Post $post Post object.
742 - * @since 2.7.0
743 - * @return void
744 - */
745 - public function track_embed_styling_configured( $post_id, $post ) {
746 - if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
747 - return;
748 - }
749 -
750 - if ( 'publish' !== $post->post_status ) {
751 - return;
752 - }
753 -
754 - $themes = [];
755 - $source = '';
756 -
757 - // Check Gutenberg blocks in post_content.
758 - if ( preg_match_all( '/<!--\s*wp:srfm\/form\s+\{[^}]*"formTheme"\s*:\s*"(?!inherit")([^"]*)"/', $post->post_content, $matches ) ) {
759 - $themes = array_count_values( $matches[1] );
760 - $source = 'gutenberg';
761 - }
762 -
763 - // Check Elementor widgets in _elementor_data meta.
764 - if ( empty( $themes ) ) {
765 - $elementor_data = get_post_meta( $post_id, '_elementor_data', true );
766 - if ( is_string( $elementor_data )
767 - && preg_match_all( '/"widgetType"\s*:\s*"sureforms_form"[^}]*"formTheme"\s*:\s*"(?!inherit")([^"]*)"/', $elementor_data, $el_matches )
768 - ) {
769 - $themes = array_count_values( $el_matches[1] );
770 - $source = 'elementor';
771 - }
772 - }
773 -
774 - // Check Bricks elements in _bricks_page_content_2 meta (serialized PHP).
775 - if ( empty( $themes ) ) {
776 - $bricks_data = get_post_meta( $post_id, '_bricks_page_content_2', true );
777 - if ( is_array( $bricks_data ) ) {
778 - $bricks_themes = self::extract_bricks_form_themes( $bricks_data );
779 - if ( ! empty( $bricks_themes ) ) {
780 - $themes = array_count_values( $bricks_themes );
781 - $source = 'bricks';
782 - }
783 - }
784 - }
785 -
786 - if ( empty( $themes ) ) {
787 - return;
788 - }
789 -
790 - // Flush dedup so event is re-tracked on each meaningful save.
791 - self::events()->flush_pushed( [ 'embed_styling_configured' ] );
792 -
793 - self::events()->track(
794 - 'embed_styling_configured',
795 - (string) $post_id,
796 - [
797 - 'themes' => $themes,
798 - 'block_count' => array_sum( $themes ),
799 - 'source' => $source,
800 - ]
801 - );
802 - }
803 -
804 - /**
805 - * Track the plugin_activated event with the correct install referer.
806 - *
807 - * Dedup in self::events()->track() ensures this fires only once. Runs on
808 - * 'shutdown' (see detect_state_events()) so it reads bsf_product_referers
809 - * after any late-writing referer call has had a chance to run.
810 - *
811 - * @since 2.12.6
812 - * @return void
813 - */
814 - public function track_plugin_activated_event() {
815 - $bsf_referrers = get_option( 'bsf_product_referers', [] );
816 - $source = ! empty( $bsf_referrers['sureforms'] ) ? $bsf_referrers['sureforms'] : 'self';
817 - self::events()->track( 'plugin_activated', SRFM_VER, [ 'source' => $source ] );
818 - }
819 -
820 - /**
821 - * Extract non-inherit formTheme values from Bricks element data.
822 - *
823 - * Recursively walks the unserialized Bricks elements array looking for
824 - * sureforms elements with custom formTheme settings.
825 - *
826 - * @param array<mixed> $elements Bricks elements array.
827 - * @return array<string> List of formTheme values (non-inherit).
828 - * @since 2.7.0
829 - */
830 - private static function extract_bricks_form_themes( $elements ) {
831 - $themes = [];
832 -
833 - foreach ( $elements as $element ) {
834 - if ( ! is_array( $element ) ) {
835 - continue;
836 - }
837 -
838 - $name = $element['name'] ?? '';
839 - $settings = $element['settings'] ?? [];
840 - $form_theme = $settings['formTheme'] ?? '';
841 -
842 - if ( 'sureforms' === $name && ! empty( $form_theme ) && 'inherit' !== $form_theme ) {
843 - $themes[] = sanitize_text_field( $form_theme );
844 - }
845 -
846 - // Recurse into nested children.
847 - if ( ! empty( $element['children'] ) && is_array( $element['children'] ) ) {
848 - $themes = array_merge( $themes, self::extract_bricks_form_themes( $element['children'] ) );
849 - }
850 - }
851 -
852 - return $themes;
853 - }
854 -
855 - /**
856 - * Check if any payment method is enabled.
857 - *
858 - * This function checks if any payment gateway is connected and enabled.
859 - * Currently supports Stripe, but can be extended for other payment methods in the future.
860 - *
861 - * @since 2.0.0
862 - * @return bool True if any payment method is enabled, false otherwise.
863 - */
864 - private function is_stripe_enabled() {
865 - // Check if Stripe is connected.
866 - return class_exists( 'SRFM\Inc\Payments\Stripe\Stripe_Helper' ) && \SRFM\Inc\Payments\Stripe\Stripe_Helper::is_stripe_connected();
867 - }
868 -
869 - /**
870 443 * Runs custom WP_Query to fetch data as per requirement
871 444 *
872 445 * @param array $meta_query meta query array for WP_Query.
873 446 * @since 1.4.0
@@ -888,267 +461,5 @@
888 461 wp_reset_postdata();
889 462
890 463 return $posts_count;
891 464 }
892 -
893 - /**
894 - * Get KPI tracking data for the last 2 days (excluding today).
895 - *
896 - * @since 2.4.0
897 - * @return array KPI data organized by date.
898 - */
899 - private function get_kpi_tracking_data() {
900 - $kpi_data = [];
901 - $today = current_time( 'Y-m-d' );
902 -
903 - // Get data for yesterday and day before yesterday.
904 - for ( $i = 1; $i <= 2; $i++ ) {
905 - $date = gmdate( 'Y-m-d', strtotime( $today . ' -' . $i . ' days' ) );
906 -
907 - $kpi_data[ $date ] = [
908 - 'numeric_values' => [
909 - 'submissions' => $this->get_daily_submissions_count( $date ),
910 - ],
911 - ];
912 - }
913 -
914 - return $kpi_data;
915 - }
916 -
917 - /**
918 - * Get daily submissions count for a specific date.
919 - *
920 - * @param string $date Date in Y-m-d format.
921 - * @since 2.4.0
922 - * @return int Daily submissions count.
923 - */
924 - private function get_daily_submissions_count( $date ) {
925 - // Guarded here rather than at the call site: this is the function that runs
926 - // the query, so a future caller is covered too. Every read against a missing
927 - // entries table raises a DB error, and the daily send would raise one per
928 - // day counted on exactly the sites whose breakage we most need reported.
929 - if ( Register::is_entries_table_missing() ) {
930 - return 0;
931 - }
932 -
933 - $start_date = $date . ' 00:00:00';
934 - $end_date = $date . ' 23:59:59';
935 -
936 - $where_conditions = [
937 - [
938 - [
939 - 'key' => 'created_at',
940 - 'compare' => '>=',
941 - 'value' => $start_date,
942 - ],
943 - [
944 - 'key' => 'created_at',
945 - 'compare' => '<=',
946 - 'value' => $end_date,
947 - ],
948 - ],
949 - ];
950 -
951 - return Entries::get_instance()->get_total_count( $where_conditions );
952 - }
953 -
954 - /**
955 - * Detect state-based events that can't use direct hooks.
956 - * Uses dedup in self::events()->track() — safe to call repeatedly.
957 - *
958 - * @since 2.5.1
959 - * @return void
960 - */
961 - private function detect_state_events() {
962 - // plugin_activated: deferred to 'shutdown' so that a referring plugin/theme's
963 - // own BSF_UTM_Analytics::update_referer() call — which some products (incorrectly)
964 - // make only after their activate_plugin() call returns, in the same request — has
965 - // already run by the time we read bsf_product_referers. Reading this synchronously
966 - // here would race that write, since this constructor can execute mid-request while
967 - // SureForms itself is being activated by that other plugin.
968 - if ( did_action( 'shutdown' ) ) {
969 - $this->track_plugin_activated_event();
970 - } else {
971 - add_action( 'shutdown', [ $this, 'track_plugin_activated_event' ], PHP_INT_MAX );
972 - }
973 -
974 - // One-time: re-send onboarding_completed with full properties (v2).
975 - if ( ! Helper::get_srfm_option( 'onboarding_event_v2_flushed', false )
976 - && \SRFM\Inc\Onboarding::get_instance()->get_onboarding_status() ) {
977 - self::events()->flush_pushed( [ 'onboarding_completed' ] );
978 - Helper::update_srfm_option( 'onboarding_event_v2_flushed', true );
979 - }
980 -
981 - // onboarding_completed: detect completed state with full onboarding details.
982 - if ( \SRFM\Inc\Onboarding::get_instance()->get_onboarding_status() ) {
983 - $onboarding_props = [];
984 - $onboarding_analytics = Helper::get_srfm_option( 'onboarding_analytics', [] );
985 -
986 - if ( ! empty( $onboarding_analytics ) && is_array( $onboarding_analytics ) ) {
987 - if ( ! empty( $onboarding_analytics['skippedSteps'] ) && is_array( $onboarding_analytics['skippedSteps'] ) ) {
988 - $onboarding_props['skipped_steps'] = implode( ',', $onboarding_analytics['skippedSteps'] );
989 - }
990 -
991 - if ( isset( $onboarding_analytics['suremailInstalled'] ) ) {
992 - $onboarding_props['suremail_installed'] = (bool) $onboarding_analytics['suremailInstalled'] ? 'yes' : 'no';
993 - }
994 -
995 - if ( isset( $onboarding_analytics['accountConnected'] ) ) {
996 - $onboarding_props['account_connected'] = (bool) $onboarding_analytics['accountConnected'] ? 'yes' : 'no';
997 - }
998 -
999 - if ( isset( $onboarding_analytics['completed'] ) ) {
1000 - $onboarding_props['completed'] = (bool) $onboarding_analytics['completed'] ? 'yes' : 'no';
1001 - }
1002 -
1003 - if ( isset( $onboarding_analytics['exitedEarly'] ) ) {
1004 - $onboarding_props['exited_early'] = (bool) $onboarding_analytics['exitedEarly'] ? 'yes' : 'no';
1005 - }
1006 -
1007 - if ( ! empty( $onboarding_analytics['premiumFeatures']['selectedFeatures'] ) && is_array( $onboarding_analytics['premiumFeatures']['selectedFeatures'] ) ) {
1008 - $premium = array_filter(
1009 - $onboarding_analytics['premiumFeatures']['selectedFeatures'],
1010 - static function( $f ) {
1011 - return 'ai-form-generation' !== $f && 'entries' !== $f;
1012 - }
1013 - );
1014 - $onboarding_props['selected_premium_features'] = implode( ',', $premium );
1015 - $onboarding_props['premium_features_count'] = (string) count( $premium );
1016 - }
1017 - }
1018 -
1019 - self::events()->track( 'onboarding_completed', '', $onboarding_props );
1020 - }
1021 -
1022 - // stripe_connected: detect connection state.
1023 - if ( class_exists( '\SRFM\Inc\Payments\Stripe\Stripe_Helper' )
1024 - && \SRFM\Inc\Payments\Stripe\Stripe_Helper::is_stripe_connected() ) {
1025 - $mode = \SRFM\Inc\Payments\Stripe\Stripe_Helper::get_stripe_mode();
1026 - self::events()->track( 'stripe_connected', ! empty( $mode ) ? $mode : 'live' );
1027 - }
1028 -
1029 - // first_ai_form_generated: detect if any AI-generated form exists.
1030 - // Guard with is_tracked() to skip the meta_query after the event is already tracked.
1031 - if ( ! self::events()->is_tracked( 'first_ai_form_generated' ) ) {
1032 - $ai_forms = get_posts(
1033 - [
1034 - 'post_type' => SRFM_FORMS_POST_TYPE,
1035 - 'post_status' => 'any',
1036 - 'posts_per_page' => 1,
1037 - 'fields' => 'ids',
1038 - 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Runs once per lifecycle via is_tracked guard.
1039 - [
1040 - 'key' => '_srfm_is_ai_generated',
1041 - 'value' => '',
1042 - 'compare' => '!=',
1043 - ],
1044 - ],
1045 - ]
1046 - );
1047 - if ( ! empty( $ai_forms ) ) {
1048 - self::events()->track( 'first_ai_form_generated' );
1049 - }
1050 - }
1051 -
1052 - // MCP / Abilities API first-enable events.
1053 - $mcp_settings = get_option( 'srfm_mcp_settings_options', [] );
1054 -
1055 - if ( ! empty( $mcp_settings['srfm_abilities_api'] ) ) {
1056 - self::events()->track( 'abilities_api_enabled' );
1057 - }
1058 -
1059 - if ( ! empty( $mcp_settings['srfm_mcp_server'] ) ) {
1060 - self::events()->track( 'mcp_server_enabled' );
1061 - }
1062 - }
1063 -
1064 - /**
1065 - * Get learn section progress aggregated across all users.
1066 - *
1067 - * Queries all users with srfm_learn_progress meta, counts completions
1068 - * per step. Called at analytics send time (bsf_core_stats filter).
1069 - *
1070 - * @since 2.8.0
1071 - * @return void
1072 - */
1073 - private function get_learn_tracking_data() {
1074 - // Only send when progress changed or event has never been tracked.
1075 - $has_changed = get_transient( 'srfm_learn_progress_changed' );
1076 - if ( ! $has_changed && self::events()->is_tracked( 'learn' ) ) {
1077 - return;
1078 - }
1079 -
1080 - $chapters = Learn::get_chapters_structure();
1081 - $step_counts = [];
1082 - $total_steps = 0;
1083 -
1084 - // Initialize step counters from canonical structure.
1085 - foreach ( $chapters as $chapter ) {
1086 - if ( ! isset( $chapter['id'], $chapter['steps'] ) || ! is_array( $chapter['steps'] ) ) {
1087 - continue;
1088 - }
1089 - foreach ( $chapter['steps'] as $step ) {
1090 - if ( ! isset( $step['id'] ) ) {
1091 - continue;
1092 - }
1093 - $step_counts[ $chapter['id'] . '/' . $step['id'] ] = 0;
1094 - ++$total_steps;
1095 - }
1096 - }
1097 -
1098 - // Query all users who have learn progress.
1099 - $users = get_users(
1100 - [
1101 - '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.
1102 - 'fields' => 'ID',
1103 - ]
1104 - );
1105 -
1106 - $users_with_progress = 0;
1107 - $users_fully_completed = 0;
1108 - $max_completed = 0;
1109 -
1110 - foreach ( $users as $user_id ) {
1111 - $progress = get_user_meta( $user_id, 'srfm_learn_progress', true );
1112 - if ( ! is_array( $progress ) || empty( $progress ) ) {
1113 - continue;
1114 - }
1115 -
1116 - $user_completed = 0;
1117 -
1118 - foreach ( $step_counts as $key => $count ) {
1119 - [ $chapter_id, $step_id ] = explode( '/', $key );
1120 - if ( ! empty( $progress[ $chapter_id ][ $step_id ] ) ) {
1121 - $step_counts[ $key ] = $count + 1;
1122 - ++$user_completed;
1123 - }
1124 - }
1125 -
1126 - if ( $user_completed > 0 ) {
1127 - ++$users_with_progress;
1128 - }
1129 - if ( $user_completed === $total_steps ) {
1130 - ++$users_fully_completed;
1131 - }
1132 - $max_completed = max( $max_completed, $user_completed );
1133 - }
1134 -
1135 - // Build flat properties — step keys use snake_case (hyphens → underscores).
1136 - $properties = [];
1137 - foreach ( $step_counts as $key => $count ) {
1138 - $step_id = explode( '/', $key )[1];
1139 - $properties[ str_replace( '-', '_', $step_id ) ] = (string) $count;
1140 - }
1141 -
1142 - $properties['users_with_progress'] = (string) $users_with_progress;
1143 - $properties['users_fully_completed'] = (string) $users_fully_completed;
1144 - $properties['total_steps'] = (string) $total_steps;
1145 -
1146 - // Flush dedup so event re-tracks with latest snapshot.
1147 - self::events()->flush_pushed( [ 'learn' ] );
1148 - self::events()->track( 'learn', (string) $max_completed, $properties );
1149 -
1150 - // Clear the change flag after tracking.
1151 - delete_transient( 'srfm_learn_progress_changed' );
1152 - }
1153 -
1154 465 }