PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.5.2
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.5.2
2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 0.0.2 0.0.3 All 96 releases
sureforms / admin / analytics.php

analytics.php in SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz 2.5.2, at admin/analytics.php

684 lines 19.8 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\Analytics_Events;
11 use SRFM\Inc\Database\Tables\Entries;
12 use SRFM\Inc\Helper;
13 use SRFM\Inc\Traits\Get_Instance;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit; // Exit if accessed directly.
17 }
18 /**
19 * Analytics class.
20 *
21 * @since 1.4.0
22 */
23 class Analytics {
24 use Get_Instance;
25
26 /**
27 * Class constructor.
28 *
29 * @return void
30 * @since 1.4.0
31 */
32 public function __construct() {
33 /*
34 * BSF Analytics.
35 */
36 if ( ! class_exists( 'BSF_Analytics_Loader' ) ) {
37 require_once SRFM_DIR . 'inc/lib/bsf-analytics/class-bsf-analytics-loader.php';
38 }
39
40 if ( ! class_exists( 'Astra_Notices' ) ) {
41 require_once SRFM_DIR . 'inc/lib/astra-notices/class-astra-notices.php';
42 }
43
44 add_filter(
45 'uds_survey_allowed_screens',
46 static function () {
47 return [ 'plugins' ];
48 }
49 );
50
51 $srfm_bsf_analytics = \BSF_Analytics_Loader::get_instance();
52
53 $srfm_bsf_analytics->set_entity(
54 [
55 'sureforms' => [
56 'product_name' => 'SureForms',
57 'path' => SRFM_DIR . 'inc/lib/bsf-analytics',
58 'author' => 'SureForms',
59 'time_to_display' => '+24 hours',
60 'deactivation_survey' => apply_filters(
61 'srfm_deactivation_survey_data',
62 [
63 [
64 'id' => 'deactivation-survey-sureforms',
65 'popup_logo' => SRFM_URL . 'admin/assets/sureforms-logo.png',
66 'plugin_slug' => 'sureforms',
67 'popup_title' => 'Quick Feedback',
68 'support_url' => 'https://sureforms.com/contact/',
69 'popup_description' => 'If you have a moment, please share why you are deactivating SureForms:',
70 'show_on_screens' => [ 'plugins' ],
71 'plugin_version' => SRFM_VER,
72 ],
73 ]
74 ),
75 'hide_optin_checkbox' => true,
76 ],
77 ]
78 );
79
80 add_filter( 'bsf_core_stats', [ $this, 'add_srfm_analytics_data' ] );
81
82 // Event tracking hooks.
83 add_action( 'current_screen', [ $this, 'track_first_editor_open' ] );
84 add_action( 'transition_post_status', [ $this, 'track_first_form_published' ], 10, 3 );
85
86 // Detect state-based events on admin load (dedup prevents repeat tracking).
87 $this->detect_state_events();
88 }
89
90 /**
91 * Callback function to add SureForms specific analytics data.
92 *
93 * @param array $stats_data existing stats_data.
94 * @since 1.4.0
95 * @return array
96 */
97 public function add_srfm_analytics_data( $stats_data ) {
98 $stats_data['plugin_data']['sureforms'] = [
99 'free_version' => SRFM_VER,
100 'site_language' => get_locale(),
101 'most_used_anti_spam' => $this->most_used_anti_spam(),
102 'user_status' => $this->user_status(),
103 'pointer_popup_clicked' => $this->pointer_popup_clicked(),
104 ];
105 $stats_data['plugin_data']['sureforms']['numeric_values'] = [
106 'total_forms' => wp_count_posts( SRFM_FORMS_POST_TYPE )->publish ?? 0,
107 'instant_forms_enabled' => $this->instant_forms_enabled(),
108 'forms_using_custom_css' => $this->forms_using_custom_css(),
109 'ai_generated_forms' => $this->ai_generated_forms(),
110 'ai_generated_payment_forms' => $this->ai_generated_forms( 'payments' ),
111 'payment_forms' => $this->get_payment_forms_count(),
112 'total_entries' => Entries::get_total_entries_by_status(),
113 'restricted_forms' => $this->get_restricted_forms(),
114 ];
115
116 $stats_data['plugin_data']['sureforms'] = array_merge_recursive( $stats_data['plugin_data']['sureforms'], $this->global_settings_data() );
117 // Add onboarding analytics data.
118 $stats_data['plugin_data']['sureforms'] = array_merge_recursive( $stats_data['plugin_data']['sureforms'], $this->onboarding_analytics_data() );
119
120 // Add KPI tracking data.
121 $kpi_data = $this->get_kpi_tracking_data();
122 if ( ! empty( $kpi_data ) ) {
123 $stats_data['plugin_data']['sureforms']['kpi_records'] = $kpi_data;
124 }
125
126 // Flush pending events into payload (only if any exist).
127 $pending_events = Analytics_Events::flush_pending();
128 if ( ! empty( $pending_events ) ) {
129 $stats_data['plugin_data']['sureforms']['events_record'] = $pending_events;
130 }
131
132 return $stats_data;
133 }
134
135 /**
136 * Return total number of forms using instant forms.
137 *
138 * @since 1.4.0
139 * @return int
140 */
141 public function instant_forms_enabled() {
142 $meta_query = [
143 [
144 'key' => '_srfm_instant_form_settings',
145 'value' => '"enable_instant_form";b:1;',
146 'compare' => 'LIKE',
147 ],
148 ];
149
150 return $this->custom_wp_query_total_posts( $meta_query );
151 }
152
153 /**
154 * Return total number of ai generated forms.
155 *
156 * @param string $form_type Form type to check.
157 *
158 * @since 1.4.0
159 * @return int
160 */
161 public function ai_generated_forms( $form_type = '' ) {
162 $form_type = empty( $form_type ) || ! is_string( $form_type ) ? '' : $form_type;
163 $meta_query = [
164 [
165 'key' => '_srfm_is_ai_generated',
166 'value' => '',
167 'compare' => '!=', // Checks if the value is NOT empty.
168 ],
169 ];
170
171 if ( 'payments' === $form_type ) {
172 $search = 'wp:srfm/payment';
173 return $this->custom_wp_query_total_posts_with_search( $meta_query, $search );
174 }
175
176 return $this->custom_wp_query_total_posts( $meta_query );
177 }
178
179 /**
180 * Return most used anti-spam type on this site.
181 *
182 * @since 1.4.4
183 * @return int
184 */
185 public function most_used_anti_spam() {
186 global $wpdb;
187
188 // Attempt to get from cache first.
189 $cache_key = 'most_used_anti_spam';
190 $cached_result = wp_cache_get( $cache_key, 'sureforms' );
191
192 if ( false !== $cached_result ) {
193 return $cached_result;
194 }
195
196 $meta_key = '_srfm_captcha_security_type';
197
198 // Query to get the most used captcha type.
199 // PHPCS: Ignore direct database query warning, as there is no built-in alternative.
200 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
201 $result = $wpdb->get_row(
202 $wpdb->prepare(
203 "
204 SELECT meta_value, COUNT(meta_value) as count
205 FROM {$wpdb->postmeta}
206 WHERE meta_key = %s
207 AND meta_value != ''
208 GROUP BY meta_value
209 ORDER BY count DESC
210 LIMIT 1
211 ",
212 $meta_key
213 ),
214 ARRAY_A
215 );
216
217 $output = '';
218 if ( $result && ! empty( $result['meta_value'] ) ) {
219 switch ( $result['meta_value'] ) {
220 case 'g-recaptcha':
221 $output = 'Google reCAPTCHA';
222 break;
223
224 case 'cf-turnstile':
225 $output = 'CloudFlare Turnstile';
226 break;
227
228 case 'hcaptcha':
229 $output = 'hCaptcha';
230 break;
231
232 default:
233 $output = '';
234 break;
235 }
236 }
237
238 // Store result in cache for 1 hour.
239 wp_cache_set( $cache_key, $output, 'sureforms', HOUR_IN_SECONDS );
240
241 return $output;
242 }
243
244 /**
245 * Returns total number of forms using custom css.
246 *
247 * @since 1.4.0
248 * @return int
249 */
250 public function forms_using_custom_css() {
251 $meta_query = [
252 [
253 'key' => '_srfm_form_custom_css',
254 'value' => '',
255 'compare' => '!=', // Checks if the value is NOT empty.
256 ],
257 ];
258
259 return $this->custom_wp_query_total_posts( $meta_query );
260 }
261
262 /**
263 * Return total number of restricted forms.
264 *
265 * @since 1.10.1
266 * @return int
267 */
268 public function get_restricted_forms() {
269 $meta_query = [
270 [
271 'key' => '_srfm_form_restriction',
272 'value' => '"status":true',
273 'compare' => 'LIKE',
274 ],
275 ];
276
277 return $this->custom_wp_query_total_posts( $meta_query );
278 }
279
280 /**
281 * Generates global setting data for analytics
282 *
283 * @since 1.4.0
284 * @return array
285 */
286 public function global_settings_data() {
287 $global_data = [];
288
289 $security_settings = get_option( 'srfm_security_settings_options', [] );
290 $global_data['boolean_values']['honeypot_enabled'] = isset( $security_settings['srfm_honeypot'] ) && true === $security_settings['srfm_honeypot'];
291
292 $email_summary_data = get_option( 'srfm_email_summary_settings_options', [] );
293 $global_data['boolean_values']['email_summary_enabled'] = isset( $email_summary_data['srfm_email_summary'] ) && true === $email_summary_data['srfm_email_summary'];
294
295 $global_data['boolean_values']['suretriggers_active'] = is_plugin_active( 'suretriggers/suretriggers.php' );
296
297 $bsf_internal_referrer = get_option( 'bsf_product_referers', [] );
298 if ( ! empty( $bsf_internal_referrer['sureforms'] ) ) {
299 $global_data['internal_referer'] = $bsf_internal_referrer['sureforms'];
300 } else {
301 $global_data['internal_referer'] = '';
302 }
303
304 $general_settings = get_option( 'srfm_general_settings_options', [] );
305 $global_data['boolean_values']['ip_logging_enabled'] = ! empty( $general_settings['srfm_ip_log'] );
306
307 $validation_messages = get_option( 'srfm_default_dynamic_block_option', [] );
308 $global_data['boolean_values']['custom_validation_message'] = ! empty( $validation_messages ) && is_array( $validation_messages );
309
310 // Payment analytics - check if any payment method is enabled.
311 $global_data['boolean_values']['stripe_enabled'] = $this->is_stripe_enabled();
312
313 return $global_data;
314 }
315
316 /**
317 * Generates onboarding analytics data
318 *
319 * @since 1.9.1
320 * @return array
321 */
322 public function onboarding_analytics_data() {
323 $onboarding_data = [];
324 $analytics_option = Helper::get_srfm_option( 'onboarding_analytics', [] );
325
326 if ( empty( $analytics_option ) ) {
327 return $onboarding_data;
328 }
329
330 // Process skipped steps - store as an array.
331 if ( ! empty( $analytics_option['skippedSteps'] ) && is_array( $analytics_option['skippedSteps'] ) ) {
332 // Map step keys to more descriptive names.
333 $step_mapping = [
334 'welcome' => 'Welcome',
335 'connect' => 'Connect',
336 'emailDelivery' => 'SureMail',
337 'premiumFeatures' => 'Features',
338 'done' => 'Done',
339 ];
340
341 // Transform the step keys to their descriptive names.
342 $mapped_steps = array_map(
343 static function( $step ) use ( $step_mapping ) {
344 return $step_mapping[ $step ] ?? $step;
345 },
346 $analytics_option['skippedSteps']
347 );
348
349 // Store as an array.
350 $onboarding_data['onboarding_skipped_steps'] = $mapped_steps;
351 }
352
353 // SureMail Installation Status.
354 if ( isset( $analytics_option['suremailInstalled'] ) ) {
355 $onboarding_data['boolean_values']['onboarding_suremail_installed'] = (bool) $analytics_option['suremailInstalled'];
356 }
357
358 // Account Connection Status.
359 if ( isset( $analytics_option['accountConnected'] ) ) {
360 $onboarding_data['boolean_values']['onboarding_account_connected'] = (bool) $analytics_option['accountConnected'];
361 }
362
363 // Onboarding Completion Status.
364 if ( isset( $analytics_option['completed'] ) ) {
365 $onboarding_data['boolean_values']['onboarding_completed'] = (bool) $analytics_option['completed'];
366 }
367
368 // Onboarding Early Exit Status.
369 if ( isset( $analytics_option['exitedEarly'] ) ) {
370 $onboarding_data['boolean_values']['onboarding_exited_early'] = (bool) $analytics_option['exitedEarly'];
371 }
372
373 // Onboarding Selected Premium Features.
374 if ( ! empty( $analytics_option['premiumFeatures'] ) && ! empty( $analytics_option['premiumFeatures']['selectedFeatures'] ) ) {
375 // Map feature IDs to more descriptive names - exclude free features.
376 $feature_mapping = [
377 // Starter features.
378 'multi_step_form' => 'Multi-step Forms',
379 'conditional_logic' => 'Conditional Fields',
380 'webhooks' => 'Webhooks',
381 'advanced_fields' => 'Advanced Fields',
382
383 // Pro features.
384 'conversational_forms' => 'Conversational Forms',
385 'digital_signatures' => 'Digital Signatures',
386
387 // Business features.
388 'calculations' => 'Calculators',
389 'user_registration' => 'User Registration and Login',
390 'custom_app' => 'Custom App',
391 'pdf_generation' => 'PDF Generation',
392 ];
393
394 // Filter out any free features that might have been included.
395 $premium_features = array_filter(
396 $analytics_option['premiumFeatures']['selectedFeatures'],
397 static function( $feature ) {
398 // Exclude free features (ai-form-generation and entries).
399 return 'ai-form-generation' !== $feature && 'entries' !== $feature;
400 }
401 );
402
403 // Transform the feature IDs to their descriptive names.
404 $mapped_features = array_map(
405 static function( $feature ) use ( $feature_mapping ) {
406 return $feature_mapping[ $feature ] ?? $feature;
407 },
408 $premium_features
409 );
410
411 // Store as an array.
412 $onboarding_data['onboarding_selected_premium_features'] = $mapped_features;
413 }
414
415 return $onboarding_data;
416 }
417
418 /**
419 * Returns user status.
420 *
421 * @since 1.8.0
422 * @return string
423 */
424 public function user_status() {
425 // First, check if user_active is already set in srfm_options.
426 if ( Helper::get_srfm_option( 'user_active', false ) ) {
427 return 'active';
428 }
429 // Get up to 10 published SureForms.
430 $forms = get_posts(
431 [
432 'post_type' => SRFM_FORMS_POST_TYPE,
433 'posts_per_page' => 10,
434 'post_status' => 'publish',
435 ]
436 );
437 if ( empty( $forms ) ) {
438 return 'inactive';
439 }
440 foreach ( $forms as $form ) {
441 if ( ! get_post_meta( $form->ID, '_astra_sites_imported_post', true ) ) {
442 // Mark user as active in srfm_options.
443 Helper::update_srfm_option( 'user_active', true );
444 return 'active';
445 }
446 }
447 return 'inactive';
448 }
449
450 /**
451 * Return pointer popup clicked status.
452 *
453 * @return string pointer click status.
454 * @since 1.8.0
455 */
456 public function pointer_popup_clicked() {
457 // Get both values from srfm_options.
458 $accepted = Helper::get_srfm_option( 'pointer_popup_accepted', false );
459 $dismissed = Helper::get_srfm_option( 'pointer_popup_dismissed', false );
460 // If neither action has occurred.
461 if ( ! $accepted && ! $dismissed ) {
462 return '';
463 }
464
465 // If both are set, return the most recent one.
466 if ( $accepted && $dismissed ) {
467 return $accepted > $dismissed ? 'accepted' : 'dismissed';
468 }
469
470 // If only one is set, return it.
471 return $accepted ? 'accepted' : 'dismissed';
472 }
473
474 /**
475 * Runs a custom WP_Query to fetch the total number of posts matching the given meta query and optional search string.
476 *
477 * This function is used to count SureForms posts based on specific meta query conditions.
478 * Optionally, a search string can be included to further filter results by keyword match.
479 *
480 * @since 2.0.0
481 *
482 * @param array $meta_query Meta query array for WP_Query.
483 * @param string $search Optional. Search string for WP_Query. Default empty.
484 * @return int The number of matching posts.
485 */
486 public function custom_wp_query_total_posts_with_search( $meta_query = [], $search = '' ) {
487 $args = [
488 'post_type' => SRFM_FORMS_POST_TYPE,
489 'post_status' => 'publish',
490 'posts_per_page' => -1,
491 ];
492
493 if ( ! empty( $meta_query ) && is_array( $meta_query ) ) {
494 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query required as we need to fetch count of nested data.
495 $args['meta_query'] = $meta_query;
496 }
497
498 // If search string is provided, add it to the query.
499 if ( ! empty( $search ) ) {
500 $args['s'] = sanitize_text_field( $search );
501 }
502
503 $query = new \WP_Query( $args );
504 $posts_count = $query->found_posts;
505
506 wp_reset_postdata();
507
508 return $posts_count;
509 }
510
511 /**
512 * Get the total number of forms that utilize payment blocks.
513 *
514 * This function searches for forms containing the payment block identifier
515 * ('wp:srfm/payment') to determine how many forms include payment capabilities.
516 *
517 * @since 2.0.0
518 * @return int The number of forms that contain payment blocks.
519 */
520 public function get_payment_forms_count() {
521 $search = 'wp:srfm/payment';
522 // Runs a custom WP_Query to find the count of forms with payment block.
523 return $this->custom_wp_query_total_posts_with_search( [], $search );
524 }
525
526 /**
527 * Track first time a user opens the form editor.
528 *
529 * @since 2.5.1
530 * @return void
531 */
532 public function track_first_editor_open() {
533 $screen = get_current_screen();
534 if ( $screen && 'sureforms_form' === $screen->id ) {
535 Analytics_Events::track( 'first_form_editor_opened' );
536 }
537 }
538
539 /**
540 * Track first time a form is published.
541 *
542 * @param string $new_status New post status.
543 * @param string $old_status Old post status.
544 * @param \WP_Post $post Post object.
545 * @since 2.5.1
546 * @return void
547 */
548 public function track_first_form_published( $new_status, $old_status, $post ) {
549 if ( 'publish' !== $new_status || 'publish' === $old_status || SRFM_FORMS_POST_TYPE !== $post->post_type ) {
550 return;
551 }
552
553 $is_ai = ! empty( get_post_meta( $post->ID, '_srfm_is_ai_generated', true ) );
554 $block_count = substr_count( $post->post_content, '<!-- wp:srfm/' );
555
556 Analytics_Events::track(
557 'first_form_published',
558 (string) $post->ID,
559 [
560 'is_ai_generated' => $is_ai,
561 'block_count' => $block_count,
562 ]
563 );
564 }
565
566 /**
567 * Check if any payment method is enabled.
568 *
569 * This function checks if any payment gateway is connected and enabled.
570 * Currently supports Stripe, but can be extended for other payment methods in the future.
571 *
572 * @since 2.0.0
573 * @return bool True if any payment method is enabled, false otherwise.
574 */
575 private function is_stripe_enabled() {
576 // Check if Stripe is connected.
577 return class_exists( 'SRFM\Inc\Payments\Stripe\Stripe_Helper' ) && \SRFM\Inc\Payments\Stripe\Stripe_Helper::is_stripe_connected();
578 }
579
580 /**
581 * Runs custom WP_Query to fetch data as per requirement
582 *
583 * @param array $meta_query meta query array for WP_Query.
584 * @since 1.4.0
585 * @return int
586 */
587 private function custom_wp_query_total_posts( $meta_query ) {
588
589 $args = [
590 'post_type' => SRFM_FORMS_POST_TYPE,
591 'post_status' => 'publish',
592 'posts_per_page' => -1,
593 '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.
594 ];
595
596 $query = new \WP_Query( $args );
597 $posts_count = $query->found_posts;
598
599 wp_reset_postdata();
600
601 return $posts_count;
602 }
603
604 /**
605 * Get KPI tracking data for the last 2 days (excluding today).
606 *
607 * @since 2.4.0
608 * @return array KPI data organized by date.
609 */
610 private function get_kpi_tracking_data() {
611 $kpi_data = [];
612 $today = current_time( 'Y-m-d' );
613
614 // Get data for yesterday and day before yesterday.
615 for ( $i = 1; $i <= 2; $i++ ) {
616 $date = gmdate( 'Y-m-d', strtotime( $today . ' -' . $i . ' days' ) );
617
618 $kpi_data[ $date ] = [
619 'numeric_values' => [
620 'submissions' => $this->get_daily_submissions_count( $date ),
621 ],
622 ];
623 }
624
625 return $kpi_data;
626 }
627
628 /**
629 * Get daily submissions count for a specific date.
630 *
631 * @param string $date Date in Y-m-d format.
632 * @since 2.4.0
633 * @return int Daily submissions count.
634 */
635 private function get_daily_submissions_count( $date ) {
636 $start_date = $date . ' 00:00:00';
637 $end_date = $date . ' 23:59:59';
638
639 $where_conditions = [
640 [
641 [
642 'key' => 'created_at',
643 'compare' => '>=',
644 'value' => $start_date,
645 ],
646 [
647 'key' => 'created_at',
648 'compare' => '<=',
649 'value' => $end_date,
650 ],
651 ],
652 ];
653
654 return Entries::get_instance()->get_total_count( $where_conditions );
655 }
656
657 /**
658 * Detect state-based events that can't use direct hooks.
659 * Uses dedup in Analytics_Events::track() — safe to call repeatedly.
660 *
661 * @since 2.5.1
662 * @return void
663 */
664 private function detect_state_events() {
665 // plugin_activated: dedup in Analytics_Events::track() ensures this fires only once.
666 $bsf_referrers = get_option( 'bsf_product_referers', [] );
667 $source = ! empty( $bsf_referrers['sureforms'] ) ? $bsf_referrers['sureforms'] : 'self';
668 Analytics_Events::track( 'plugin_activated', SRFM_VER, [ 'source' => $source ] );
669
670 // onboarding_completed: detect completed state.
671 if ( \SRFM\Inc\Onboarding::get_instance()->get_onboarding_status() ) {
672 Analytics_Events::track( 'onboarding_completed' );
673 }
674
675 // stripe_connected: detect connection state.
676 if ( class_exists( '\SRFM\Inc\Payments\Stripe\Stripe_Helper' )
677 && \SRFM\Inc\Payments\Stripe\Stripe_Helper::is_stripe_connected() ) {
678 $mode = \SRFM\Inc\Payments\Stripe\Stripe_Helper::get_stripe_mode();
679 Analytics_Events::track( 'stripe_connected', ! empty( $mode ) ? $mode : 'live' );
680 }
681 }
682
683 }
684