PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.7.1
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.7.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
sureforms / admin / analytics.php
analytics.php
948 lines 28.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Analytics class helps to connect BSFAnalytics.
4 *
5 * @package sureforms.
6 */
7
8 namespace SRFM\Admin;
9
10 use SRFM\Inc\Database\Tables\Entries;
11 use SRFM\Inc\Helper;
12 use SRFM\Inc\Traits\Get_Instance;
13
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit; // Exit if accessed directly.
16 }
17 /**
18 * Analytics class.
19 *
20 * @since 1.4.0
21 */
22 class Analytics {
23 use Get_Instance;
24
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 * Class constructor.
34 *
35 * @return void
36 * @since 1.4.0
37 */
38 public function __construct() {
39 /*
40 * BSF Analytics.
41 */
42 if ( ! class_exists( 'BSF_Analytics_Loader' ) ) {
43 require_once SRFM_DIR . 'inc/lib/bsf-analytics/class-bsf-analytics-loader.php';
44 }
45
46 if ( ! class_exists( 'Astra_Notices' ) ) {
47 require_once SRFM_DIR . 'inc/lib/astra-notices/class-astra-notices.php';
48 }
49
50 add_filter(
51 'uds_survey_allowed_screens',
52 static function () {
53 return [ 'plugins' ];
54 }
55 );
56
57 $srfm_bsf_analytics = \BSF_Analytics_Loader::get_instance();
58
59 $srfm_bsf_analytics->set_entity(
60 [
61 'sureforms' => [
62 'product_name' => 'SureForms',
63 'path' => SRFM_DIR . 'inc/lib/bsf-analytics',
64 'author' => 'SureForms',
65 'time_to_display' => '+24 hours',
66 'deactivation_survey' => apply_filters(
67 'srfm_deactivation_survey_data',
68 [
69 [
70 'id' => 'deactivation-survey-sureforms',
71 'popup_logo' => SRFM_URL . 'admin/assets/sureforms-logo.png',
72 'plugin_slug' => 'sureforms',
73 'popup_title' => 'Quick Feedback',
74 'support_url' => 'https://sureforms.com/contact/',
75 'popup_description' => 'If you have a moment, please share why you are deactivating SureForms:',
76 'show_on_screens' => [ 'plugins' ],
77 'plugin_version' => SRFM_VER,
78 ],
79 ]
80 ),
81 'hide_optin_checkbox' => true,
82 ],
83 ]
84 );
85
86 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 }
96
97 /**
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 * Callback function to add SureForms specific analytics data.
125 *
126 * @param array $stats_data existing stats_data.
127 * @since 1.4.0
128 * @return array
129 */
130 public function add_srfm_analytics_data( $stats_data ) {
131 $stats_data['plugin_data']['sureforms'] = [
132 'free_version' => SRFM_VER,
133 'site_language' => get_locale(),
134 'most_used_anti_spam' => $this->most_used_anti_spam(),
135 'user_status' => $this->user_status(),
136 'pointer_popup_clicked' => $this->pointer_popup_clicked(),
137 ];
138 $stats_data['plugin_data']['sureforms']['numeric_values'] = [
139 'total_forms' => wp_count_posts( SRFM_FORMS_POST_TYPE )->publish ?? 0,
140 'instant_forms_enabled' => $this->instant_forms_enabled(),
141 'forms_using_custom_css' => $this->forms_using_custom_css(),
142 'ai_generated_forms' => $this->ai_generated_forms(),
143 'ai_generated_payment_forms' => $this->ai_generated_forms( 'payments' ),
144 'payment_forms' => $this->get_payment_forms_count(),
145 'total_entries' => Entries::get_total_entries_by_status(),
146 '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 ];
151
152 $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
165 return $stats_data;
166 }
167
168 /**
169 * Return total number of forms using instant forms.
170 *
171 * @since 1.4.0
172 * @return int
173 */
174 public function instant_forms_enabled() {
175 $meta_query = [
176 [
177 'key' => '_srfm_instant_form_settings',
178 'value' => '"enable_instant_form";b:1;',
179 'compare' => 'LIKE',
180 ],
181 ];
182
183 return $this->custom_wp_query_total_posts( $meta_query );
184 }
185
186 /**
187 * Return total number of ai generated forms.
188 *
189 * @param string $form_type Form type to check.
190 *
191 * @since 1.4.0
192 * @return int
193 */
194 public function ai_generated_forms( $form_type = '' ) {
195 $form_type = empty( $form_type ) || ! is_string( $form_type ) ? '' : $form_type;
196 $meta_query = [
197 [
198 'key' => '_srfm_is_ai_generated',
199 'value' => '',
200 'compare' => '!=', // Checks if the value is NOT empty.
201 ],
202 ];
203
204 if ( 'payments' === $form_type ) {
205 $search = 'wp:srfm/payment';
206 return $this->custom_wp_query_total_posts_with_search( $meta_query, $search );
207 }
208
209 return $this->custom_wp_query_total_posts( $meta_query );
210 }
211
212 /**
213 * Return most used anti-spam type on this site.
214 *
215 * @since 1.4.4
216 * @return int
217 */
218 public function most_used_anti_spam() {
219 global $wpdb;
220
221 // Attempt to get from cache first.
222 $cache_key = 'most_used_anti_spam';
223 $cached_result = wp_cache_get( $cache_key, 'sureforms' );
224
225 if ( false !== $cached_result ) {
226 return $cached_result;
227 }
228
229 $meta_key = '_srfm_captcha_security_type';
230
231 // Query to get the most used captcha type.
232 // PHPCS: Ignore direct database query warning, as there is no built-in alternative.
233 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
234 $result = $wpdb->get_row(
235 $wpdb->prepare(
236 "
237 SELECT meta_value, COUNT(meta_value) as count
238 FROM {$wpdb->postmeta}
239 WHERE meta_key = %s
240 AND meta_value != ''
241 GROUP BY meta_value
242 ORDER BY count DESC
243 LIMIT 1
244 ",
245 $meta_key
246 ),
247 ARRAY_A
248 );
249
250 $output = '';
251 if ( $result && ! empty( $result['meta_value'] ) ) {
252 switch ( $result['meta_value'] ) {
253 case 'g-recaptcha':
254 $output = 'Google reCAPTCHA';
255 break;
256
257 case 'cf-turnstile':
258 $output = 'CloudFlare Turnstile';
259 break;
260
261 case 'hcaptcha':
262 $output = 'hCaptcha';
263 break;
264
265 default:
266 $output = '';
267 break;
268 }
269 }
270
271 // Store result in cache for 1 hour.
272 wp_cache_set( $cache_key, $output, 'sureforms', HOUR_IN_SECONDS );
273
274 return $output;
275 }
276
277 /**
278 * Returns total number of forms using custom css.
279 *
280 * @since 1.4.0
281 * @return int
282 */
283 public function forms_using_custom_css() {
284 $meta_query = [
285 [
286 'key' => '_srfm_form_custom_css',
287 'value' => '',
288 'compare' => '!=', // Checks if the value is NOT empty.
289 ],
290 ];
291
292 return $this->custom_wp_query_total_posts( $meta_query );
293 }
294
295 /**
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 * Return total number of restricted forms.
438 *
439 * @since 1.10.1
440 * @return int
441 */
442 public function get_restricted_forms() {
443 $meta_query = [
444 [
445 'key' => '_srfm_form_restriction',
446 'value' => '"status":true',
447 'compare' => 'LIKE',
448 ],
449 ];
450
451 return $this->custom_wp_query_total_posts( $meta_query );
452 }
453
454 /**
455 * Generates global setting data for analytics
456 *
457 * @since 1.4.0
458 * @return array
459 */
460 public function global_settings_data() {
461 $global_data = [];
462
463 $security_settings = get_option( 'srfm_security_settings_options', [] );
464 $global_data['boolean_values']['honeypot_enabled'] = isset( $security_settings['srfm_honeypot'] ) && true === $security_settings['srfm_honeypot'];
465
466 $email_summary_data = get_option( 'srfm_email_summary_settings_options', [] );
467 $global_data['boolean_values']['email_summary_enabled'] = isset( $email_summary_data['srfm_email_summary'] ) && true === $email_summary_data['srfm_email_summary'];
468
469 $global_data['boolean_values']['suretriggers_active'] = is_plugin_active( 'suretriggers/suretriggers.php' );
470
471 $bsf_internal_referrer = get_option( 'bsf_product_referers', [] );
472 if ( ! empty( $bsf_internal_referrer['sureforms'] ) ) {
473 $global_data['internal_referer'] = $bsf_internal_referrer['sureforms'];
474 } else {
475 $global_data['internal_referer'] = '';
476 }
477
478 $general_settings = get_option( 'srfm_general_settings_options', [] );
479 $global_data['boolean_values']['ip_logging_enabled'] = ! empty( $general_settings['srfm_ip_log'] );
480
481 $validation_messages = get_option( 'srfm_default_dynamic_block_option', [] );
482 $global_data['boolean_values']['custom_validation_message'] = ! empty( $validation_messages ) && is_array( $validation_messages );
483
484 // Payment analytics - check if any payment method is enabled.
485 $global_data['boolean_values']['stripe_enabled'] = $this->is_stripe_enabled();
486
487 return $global_data;
488 }
489
490 /**
491 * Returns user status.
492 *
493 * @since 1.8.0
494 * @return string
495 */
496 public function user_status() {
497 // First, check if user_active is already set in srfm_options.
498 if ( Helper::get_srfm_option( 'user_active', false ) ) {
499 return 'active';
500 }
501 // Get up to 10 published SureForms.
502 $forms = get_posts(
503 [
504 'post_type' => SRFM_FORMS_POST_TYPE,
505 'posts_per_page' => 10,
506 'post_status' => 'publish',
507 ]
508 );
509 if ( empty( $forms ) ) {
510 return 'inactive';
511 }
512 foreach ( $forms as $form ) {
513 if ( ! get_post_meta( $form->ID, '_astra_sites_imported_post', true ) ) {
514 // Mark user as active in srfm_options.
515 Helper::update_srfm_option( 'user_active', true );
516 return 'active';
517 }
518 }
519 return 'inactive';
520 }
521
522 /**
523 * Return pointer popup clicked status.
524 *
525 * @return string pointer click status.
526 * @since 1.8.0
527 */
528 public function pointer_popup_clicked() {
529 // Get both values from srfm_options.
530 $accepted = Helper::get_srfm_option( 'pointer_popup_accepted', false );
531 $dismissed = Helper::get_srfm_option( 'pointer_popup_dismissed', false );
532 // If neither action has occurred.
533 if ( ! $accepted && ! $dismissed ) {
534 return '';
535 }
536
537 // If both are set, return the most recent one.
538 if ( $accepted && $dismissed ) {
539 return $accepted > $dismissed ? 'accepted' : 'dismissed';
540 }
541
542 // If only one is set, return it.
543 return $accepted ? 'accepted' : 'dismissed';
544 }
545
546 /**
547 * Runs a custom WP_Query to fetch the total number of posts matching the given meta query and optional search string.
548 *
549 * This function is used to count SureForms posts based on specific meta query conditions.
550 * Optionally, a search string can be included to further filter results by keyword match.
551 *
552 * @since 2.0.0
553 *
554 * @param array $meta_query Meta query array for WP_Query.
555 * @param string $search Optional. Search string for WP_Query. Default empty.
556 * @return int The number of matching posts.
557 */
558 public function custom_wp_query_total_posts_with_search( $meta_query = [], $search = '' ) {
559 $args = [
560 'post_type' => SRFM_FORMS_POST_TYPE,
561 'post_status' => 'publish',
562 'posts_per_page' => -1,
563 ];
564
565 if ( ! empty( $meta_query ) && is_array( $meta_query ) ) {
566 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query required as we need to fetch count of nested data.
567 $args['meta_query'] = $meta_query;
568 }
569
570 // If search string is provided, add it to the query.
571 if ( ! empty( $search ) ) {
572 $args['s'] = sanitize_text_field( $search );
573 }
574
575 $query = new \WP_Query( $args );
576 $posts_count = $query->found_posts;
577
578 wp_reset_postdata();
579
580 return $posts_count;
581 }
582
583 /**
584 * Get the total number of forms that utilize payment blocks.
585 *
586 * This function searches for forms containing the payment block identifier
587 * ('wp:srfm/payment') to determine how many forms include payment capabilities.
588 *
589 * @since 2.0.0
590 * @return int The number of forms that contain payment blocks.
591 */
592 public function get_payment_forms_count() {
593 $search = 'wp:srfm/payment';
594 // Runs a custom WP_Query to find the count of forms with payment block.
595 return $this->custom_wp_query_total_posts_with_search( [], $search );
596 }
597
598 /**
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 * Check if any payment method is enabled.
755 *
756 * This function checks if any payment gateway is connected and enabled.
757 * Currently supports Stripe, but can be extended for other payment methods in the future.
758 *
759 * @since 2.0.0
760 * @return bool True if any payment method is enabled, false otherwise.
761 */
762 private function is_stripe_enabled() {
763 // Check if Stripe is connected.
764 return class_exists( 'SRFM\Inc\Payments\Stripe\Stripe_Helper' ) && \SRFM\Inc\Payments\Stripe\Stripe_Helper::is_stripe_connected();
765 }
766
767 /**
768 * Runs custom WP_Query to fetch data as per requirement
769 *
770 * @param array $meta_query meta query array for WP_Query.
771 * @since 1.4.0
772 * @return int
773 */
774 private function custom_wp_query_total_posts( $meta_query ) {
775
776 $args = [
777 'post_type' => SRFM_FORMS_POST_TYPE,
778 'post_status' => 'publish',
779 'posts_per_page' => -1,
780 'meta_query' => $meta_query, //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query required as we need to fetch count of nested data.
781 ];
782
783 $query = new \WP_Query( $args );
784 $posts_count = $query->found_posts;
785
786 wp_reset_postdata();
787
788 return $posts_count;
789 }
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 }
948