PluginProbe
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce / 3.2.0
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce v3.2.0
3.3.0 3.2.1 3.2.0 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.1 trunk 1.0.4 1.1.0 1.1.0.1 1.1.1 1.1.10 1.1.11 1.1.12 1.1.13 1.1.14 1.1.15 1.1.16 1.1.17 1.1.18 1.1.19 1.1.2 All 162 releases
cartflows / classes / class-cartflows-analytics.php

class-cartflows-analytics.php in CartFlows – Funnel Builder & Checkout Plugin for WooCommerce 3.2.0, at classes/class-cartflows-analytics.php

918 lines 30.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * CartFlows Analytics — single entry point for all BSF Analytics tracking.
4 *
5 * Owns the complete CartFlows stats payload (default stats, numeric_values,
6 * boolean_values, kpi_records) and one-time milestone events via
7 * BSF_Analytics_Events. Registered on a single bsf_core_stats filter.
8 *
9 * @package CartFlows
10 * @since 2.2.4
11 */
12
13 // Exit if accessed directly.
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 /**
19 * Class Cartflows_Analytics.
20 */
21 class Cartflows_Analytics {
22
23 /**
24 * Instance
25 *
26 * @access private
27 * @var object Class object.
28 * @since 2.2.4
29 */
30 private static $instance;
31
32 /**
33 * Shared BSF_Analytics_Events instance.
34 *
35 * @var BSF_Analytics_Events|null
36 */
37 private static $events = null;
38
39 /**
40 * Initiator
41 *
42 * @since 2.2.4
43 * @return object initialized object of class.
44 */
45 public static function get_instance() {
46 if ( ! isset( self::$instance ) ) {
47 self::$instance = new self();
48 }
49 return self::$instance;
50 }
51
52 /**
53 * Constructor.
54 *
55 * Registers the bsf_core_stats filter on every request (frontend and admin)
56 * so events_record rides the BSF_Analytics::send() POST regardless of which
57 * request triggers transmission. State detection runs lazily inside the
58 * filter callback (see get_stats) — same pattern as Astra.
59 *
60 * @since 2.2.4
61 */
62 public function __construct() {
63 // Single bsf_core_stats filter — owns the complete CartFlows payload.
64 add_filter( 'bsf_core_stats', array( $this, 'get_stats' ) );
65
66 // Hook-based event: first flow published.
67 add_action( 'transition_post_status', array( $this, 'track_first_flow_published' ), 10, 3 );
68
69 // Meta-level so template imports and Store Checkout are caught, not just the admin save.
70 add_action( 'added_post_meta', array( $this, 'track_first_checkout_configured' ), 10, 4 );
71 add_action( 'updated_post_meta', array( $this, 'track_first_checkout_configured' ), 10, 4 );
72 }
73
74 /**
75 * Get shared BSF_Analytics_Events instance.
76 *
77 * Returns null if BSF_Analytics_Events is not yet loaded — callers must guard.
78 *
79 * @since 2.2.4
80 * @return BSF_Analytics_Events|null
81 */
82 public static function events() {
83 if ( null === self::$events ) {
84 if ( ! class_exists( 'BSF_Analytics_Events' ) ) {
85 return null;
86 }
87 self::$events = new BSF_Analytics_Events( 'cf' );
88 }
89 return self::$events;
90 }
91
92 // -------------------------------------------------------------------------
93 // BSF Analytics stats payload
94 // -------------------------------------------------------------------------
95
96 /**
97 * Build and append the complete CartFlows analytics payload.
98 *
99 * Single bsf_core_stats filter callback — replaces the former
100 * Cartflows_Admin::get_specific_stats() handler.
101 *
102 * @since 2.2.4
103 * @param array $stats_data Existing analytics payload.
104 * @return array
105 */
106 public function get_stats( $stats_data ) {
107 // Use get_site_option so multisite network opt-in flows correctly — matches
108 // BSF_Analytics::is_tracking_enabled() which also reads the site option.
109 if ( ! apply_filters( 'cartflows_enable_non_sensitive_data_tracking', get_site_option( 'cf_usage_optin', false ) ) ) {
110 return $stats_data;
111 }
112
113 $theme_data = $this->get_active_theme();
114
115 $stats_data['plugin_data']['cartflows'] = $this->get_default_stats( $theme_data );
116 $stats_data['plugin_data']['cartflows']['kpi_records'] = $this->get_kpi_tracking_data();
117 $stats_data['plugin_data']['cartflows']['numeric_values'] = $this->get_numeric_data_stats();
118 $stats_data['plugin_data']['cartflows']['boolean_values'] = $this->get_boolean_data_stats( $theme_data );
119
120 // Detect state-based events lazily inside the filter so detection runs
121 // in the same request as transmission (BSF_Analytics_Events dedup makes
122 // repeated calls safe). Throttled by a 24h transient.
123 if ( false === get_transient( 'cf_state_events_checked' ) ) {
124 $this->detect_state_events();
125 }
126
127 // Append pending milestone events.
128 $events = self::events();
129 if ( null !== $events ) {
130 $stats_data['plugin_data']['cartflows']['events_record'] = $events->flush_pending();
131 }
132
133 // Allow Pro or other extensions to append additional data.
134 $stats_data = apply_filters( 'cartflows_get_specific_stats', $stats_data );
135
136 return $stats_data;
137 }
138
139 /**
140 * Retrieve default statistics for CartFlows.
141 *
142 * @since 2.2.4
143 * @param array $theme_data Array containing active theme information.
144 * @return array
145 */
146 private function get_default_stats( $theme_data ) {
147 $bsf_internal_referer = get_option( 'bsf_product_referers', array() );
148 $store_location = '';
149 $woo_version = '';
150 $store_currency = '';
151
152 if ( wcf()->is_woo_active ) {
153 $store_location = wc_get_base_location();
154 $woo_version = WC()->version;
155 $store_currency = get_woocommerce_currency();
156 }
157
158 // Sanitize knowledge base search terms before sending.
159 $raw_kb_searches = get_option( 'cartflows_kb_searches', array() );
160 $kb_searches = array_map( 'sanitize_text_field', is_array( $raw_kb_searches ) ? $raw_kb_searches : array() );
161
162 return array(
163 'website-domain' => str_ireplace( array( 'http://', 'https://' ), '', home_url() ),
164 'site_language' => get_locale(),
165 'cartflows-lite-version' => CARTFLOWS_VER,
166 'cartflows-pro-version' => _is_cartflows_pro() ? CARTFLOWS_PRO_VER : '',
167 'woocommerce-version' => $woo_version,
168 'default-page-builder' => Cartflows_Helper::get_common_setting( 'default_page_builder' ),
169 'active-theme' => $theme_data['parent_theme'],
170 'active-gateway-count' => wcf()->is_woo_active ? count( (array) $this->get_active_gateways() ) : 0,
171 'social-tracking' => $this->get_social_tracking_flags(),
172 'store-country' => ! empty( $store_location['country'] ) ? $store_location['country'] : '',
173 // Required to make the fbt_revenue KPI comparable across stores.
174 'store-currency' => $store_currency,
175 'documentation-search-terms' => $kb_searches,
176 'internal_referer' => ! empty( $bsf_internal_referer['cartflows'] ) ? $bsf_internal_referer['cartflows'] : '',
177 // Simplified: boolean flag only — no raw feedback object.
178 'nps_survey_submitted' => ! empty( get_option( 'nps-survey-cartflows', array() ) ),
179 'pro_license_key_exists' => $this->check_pro_license_key_exists() ? true : false,
180 // Simplified: count only — no module ID list.
181 'learn_modules_completed' => count( (array) get_option( 'wcf_learn_data', array() ) ),
182 );
183 }
184
185 /**
186 * Get KPI tracking data for the last 2 days (excluding today).
187 *
188 * @since 2.2.4
189 * @return array KPI records keyed by date.
190 */
191 private function get_kpi_tracking_data() {
192 $kpi_data = array();
193
194 for ( $i = 1; $i <= 2; $i++ ) {
195 $date = wp_date( 'Y-m-d', strtotime( "-{$i} days" ) );
196 // Keys stay present when WooCommerce is inactive so the columns never disappear mid-series.
197 $numeric = array(
198 'order_count' => $this->get_daily_orders_count( $date ),
199 'offer_orders' => $this->get_daily_offer_orders_count( $date ),
200 'fbt_orders' => 0,
201 'fbt_revenue' => '0.00',
202 );
203
204 // FBT only loads when WooCommerce is active, so the class may be absent.
205 if ( class_exists( 'Cartflows_Fbt_Analytics' ) ) {
206 $fbt = Cartflows_Fbt_Analytics::get_daily_stats( (string) $date );
207 $numeric['fbt_orders'] = $fbt['fbt_orders'];
208 $numeric['fbt_revenue'] = $fbt['fbt_revenue'];
209 }
210
211 $kpi_data[ $date ] = array( 'numeric_values' => $numeric );
212 }
213
214 return $kpi_data;
215 }
216
217 /**
218 * Get daily CartFlows order count for a specific date.
219 *
220 * Counts completed/processing orders associated with a CartFlows flow.
221 * Supports both classic post-table and HPOS order storage.
222 *
223 * @since 2.2.4
224 * @param string $date Date in Y-m-d format.
225 * @return int
226 */
227 private function get_daily_orders_count( $date ) {
228 global $wpdb;
229
230 $start_date = $date . ' 00:00:00';
231 $end_date = $date . ' 23:59:59';
232
233 if ( wcf()->utils->is_hpos_enabled() ) {
234 $order_date_key = 'date_created_gmt';
235 $order_status_key = 'status';
236 $order_id_key = 'order_id';
237 $order_table = $wpdb->prefix . 'wc_orders';
238 $order_meta_table = $wpdb->prefix . 'wc_orders_meta';
239 $order_type_key = 'type';
240 $order_table_id = 'id';
241 } else {
242 $order_date_key = 'post_date';
243 $order_status_key = 'post_status';
244 $order_id_key = 'post_id';
245 $order_table = $wpdb->prefix . 'posts';
246 $order_meta_table = $wpdb->prefix . 'postmeta';
247 $order_type_key = 'post_type';
248 $order_table_id = 'ID';
249 }
250
251 //phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
252 $count = $wpdb->get_var(
253 $wpdb->prepare(
254 "SELECT COUNT(*)
255 FROM $order_table o
256 INNER JOIN $order_meta_table om ON o.$order_table_id = om.$order_id_key AND om.meta_key IN ('_wcf_flow_id', '_cartflows_parent_flow_id')
257 WHERE o.$order_type_key = 'shop_order'
258 AND o.$order_status_key IN ('wc-completed', 'wc-processing')
259 AND o.$order_date_key >= %s
260 AND o.$order_date_key <= %s",
261 $start_date,
262 $end_date
263 )
264 );
265 //phpcs:enable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
266
267 return absint( $count );
268 }
269
270 /**
271 * Get daily offer (upsell/downsell) order count for a specific date.
272 *
273 * Counts orders with _cartflows_offer meta set to 'yes'.
274 * Supports both classic post-table and HPOS order storage.
275 *
276 * @since 2.2.4
277 * @param string $date Date in Y-m-d format.
278 * @return int
279 */
280 private function get_daily_offer_orders_count( $date ) {
281 global $wpdb;
282
283 $start_date = $date . ' 00:00:00';
284 $end_date = $date . ' 23:59:59';
285
286 if ( wcf()->utils->is_hpos_enabled() ) {
287 $order_date_key = 'date_created_gmt';
288 $order_status_key = 'status';
289 $order_id_key = 'order_id';
290 $order_table = $wpdb->prefix . 'wc_orders';
291 $order_meta_table = $wpdb->prefix . 'wc_orders_meta';
292 $order_type_key = 'type';
293 $order_table_id = 'id';
294 } else {
295 $order_date_key = 'post_date';
296 $order_status_key = 'post_status';
297 $order_id_key = 'post_id';
298 $order_table = $wpdb->prefix . 'posts';
299 $order_meta_table = $wpdb->prefix . 'postmeta';
300 $order_type_key = 'post_type';
301 $order_table_id = 'ID';
302 }
303
304 //phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
305 $count = $wpdb->get_var(
306 $wpdb->prepare(
307 "SELECT COUNT(*)
308 FROM $order_table o
309 INNER JOIN $order_meta_table om ON o.$order_table_id = om.$order_id_key AND om.meta_key = '_cartflows_offer' AND om.meta_value = 'yes'
310 WHERE o.$order_type_key = 'shop_order'
311 AND o.$order_status_key IN ('wc-completed', 'wc-processing')
312 AND o.$order_date_key >= %s
313 AND o.$order_date_key <= %s",
314 $start_date,
315 $end_date
316 )
317 );
318 //phpcs:enable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
319
320 return absint( $count );
321 }
322
323 /**
324 * Retrieve numeric data statistics for CartFlows.
325 *
326 * @since 2.2.4
327 * @return array
328 */
329 private function get_numeric_data_stats() {
330 $steps_counts = $this->get_all_steps_count();
331 $flows_with_instant_checkout = $this->get_funnels_with_instant_layout();
332 $custom_fields_data = $this->get_custom_fields_enabled_data();
333
334 $funnel_building_behavior = get_option(
335 'cartflows_funnel_creation_method',
336 array(
337 'scratch' => 0,
338 'ready_made_template' => 0,
339 )
340 );
341
342 $numeric_stats = array(
343 'total_flows' => wp_count_posts( CARTFLOWS_FLOW_POST_TYPE )->publish,
344 'total_ic_funnels' => strval( $flows_with_instant_checkout ),
345 'optin_step_count' => strval( $steps_counts['optin'] ),
346 'landing_step_count' => strval( $steps_counts['landing'] ),
347 'checkout_step_count' => strval( $steps_counts['checkout'] ),
348 'upsell_step_count' => strval( $steps_counts['upsell'] ),
349 'downsell_step_count' => strval( $steps_counts['downsell'] ),
350 'thankyou_step_count' => strval( $steps_counts['thankyou'] ),
351 'optin_custom_fields' => strval( $custom_fields_data['optin'] ),
352 'checkout_custom_fields' => strval( $custom_fields_data['checkout'] ),
353 'funnels_from_scratch' => strval( $funnel_building_behavior['scratch'] ),
354 'funnels_from_template' => strval( $funnel_building_behavior['ready_made_template'] ),
355 // Keys stay present when WooCommerce is inactive so the columns never disappear mid-series.
356 'fbt_products_configured' => '0',
357 'fbt_ai_products' => '0',
358 );
359
360 // FBT only loads when WooCommerce is active, so the class may be absent.
361 if ( class_exists( 'Cartflows_Fbt_Analytics' ) ) {
362 $numeric_stats = array_merge( $numeric_stats, Cartflows_Fbt_Analytics::get_numeric_stats() );
363 }
364
365 return $numeric_stats;
366 }
367
368 /**
369 * Retrieve boolean data statistics for CartFlows.
370 *
371 * @since 2.2.4
372 * @param array $theme_data Array containing active theme information.
373 * @return array
374 */
375 private function get_boolean_data_stats( $theme_data ) {
376 $common_settings = Cartflows_Helper::get_common_settings();
377
378 return array(
379 'override-global-checkout' => ! empty( $common_settings['override_global_checkout'] ) && 'enable' === $common_settings['override_global_checkout'] ? true : false,
380 'disallow-indexing' => ! empty( $common_settings['disallow_indexing'] ) && 'enable' === $common_settings['disallow_indexing'] ? true : false,
381 'paypal-reference-transactions' => ! empty( $common_settings['paypal_reference_transactions'] ) && 'enable' === $common_settings['paypal_reference_transactions'],
382 'cartflows-stats-report-emails' => 'enable' === get_option( 'cartflows_stats_report_emails', 'enable' ),
383 'cartflows-delete-plugin-data' => 'enable' === get_option( 'cartflows_delete_plugin_data' ),
384 'pre-checkout-offer' => isset( $common_settings['pre_checkout_offer'] ) && 'enable' === $common_settings['pre_checkout_offer'],
385 'store-checkout-set' => ! empty( intval( Cartflows_Helper::get_global_setting( '_cartflows_store_checkout' ) ) ),
386 'is-child-theme' => $theme_data['child_theme'],
387 'suretriggers_active' => is_plugin_active( 'suretriggers/suretriggers.php' ),
388 // Account-level AI connection — powers FBT Auto Suggest and content generation.
389 'ai_auth_connected' => class_exists( 'Cartflows_Ai_Auth' ) && (bool) Cartflows_Ai_Auth::get_instance()->get_auth_status(),
390 );
391 }
392
393 // -------------------------------------------------------------------------
394 // Helper methods (stats)
395 // -------------------------------------------------------------------------
396
397 /**
398 * Retrieve the active theme name.
399 *
400 * @since 2.2.4
401 * @return array {parent_theme: string, child_theme: bool}
402 */
403 private function get_active_theme() {
404 $theme = wp_get_theme();
405 $parent_theme_name = '';
406 $child_theme_name = '';
407
408 if ( isset( $theme->parent_theme ) && ! empty( $theme->parent_theme ) ) {
409 $parent_theme_name = $theme->parent_theme;
410 $child_theme_name = $theme->name ? $theme->name : '';
411 } else {
412 $parent_theme_name = $theme->name;
413 }
414
415 return array(
416 'parent_theme' => ! empty( $child_theme_name ) ? $child_theme_name : $parent_theme_name,
417 'child_theme' => ! empty( $child_theme_name ) ? true : false,
418 );
419 }
420
421 /**
422 * Get the count of each step type.
423 *
424 * @since 2.2.4
425 * @return array Counts keyed by step type slug.
426 */
427 private function get_all_steps_count() {
428 $default_step_counts = array(
429 'optin' => 0,
430 'landing' => 0,
431 'checkout' => 0,
432 'upsell' => 0,
433 'downsell' => 0,
434 'thankyou' => 0,
435 );
436
437 global $wpdb;
438
439 //phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
440 $step_counts = $wpdb->get_results(
441 $wpdb->prepare(
442 "SELECT JSON_OBJECTAGG(meta_value, count) as counts
443 FROM (
444 SELECT pm.meta_value, COUNT(*) as count
445 FROM $wpdb->posts p
446 INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id
447 WHERE p.post_type = %s
448 AND p.post_status = %s
449 AND pm.meta_key = %s
450 AND pm.meta_value IN ('optin', 'landing', 'checkout', 'upsell', 'downsell', 'thankyou')
451 GROUP BY pm.meta_value
452 ) as subquery",
453 CARTFLOWS_STEP_POST_TYPE,
454 'publish',
455 'wcf-step-type'
456 ),
457 ARRAY_A
458 );
459 //phpcs:enable WordPress.DB.DirectDatabaseQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
460
461 if ( empty( $step_counts ) || empty( $step_counts[0]['counts'] ) ) {
462 return $default_step_counts;
463 }
464
465 return wp_parse_args( json_decode( $step_counts[0]['counts'], true ), $default_step_counts );
466 }
467
468 /**
469 * Retrieve the count of funnels with instant layout enabled.
470 *
471 * @since 2.2.4
472 * @return int
473 */
474 private function get_funnels_with_instant_layout() {
475 $args = array(
476 'post_type' => CARTFLOWS_FLOW_POST_TYPE,
477 'post_status' => 'publish',
478 'posts_per_page' => 1,
479 'no_found_rows' => false,
480 'fields' => 'ids',
481 'meta_query' => array( //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query required to count instant layout funnels.
482 array(
483 'key' => 'instant-layout-style',
484 'value' => 'yes',
485 'compare' => 'LIKE',
486 ),
487 ),
488 );
489
490 $query = new WP_Query( $args );
491 $posts_count = isset( $query->found_posts ) ? $query->found_posts : 0;
492
493 wp_reset_postdata();
494
495 return $posts_count;
496 }
497
498 /**
499 * Retrieve the count of steps with custom fields enabled, by type.
500 *
501 * @since 2.2.4
502 * @return array {optin: int, checkout: int}
503 */
504 private function get_custom_fields_enabled_data() {
505 $default_step_counts = array(
506 'optin' => 0,
507 'checkout' => 0,
508 );
509
510 global $wpdb;
511
512 //phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
513 $step_counts = $wpdb->get_results(
514 $wpdb->prepare(
515 "SELECT JSON_OBJECTAGG(subquery.step_type, subquery.count) AS counts
516 FROM ( SELECT step.meta_value AS step_type,
517 COUNT(DISTINCT p.ID) AS count
518 FROM $wpdb->posts p
519 INNER JOIN $wpdb->postmeta step
520 ON p.ID = step.post_id
521 AND step.meta_key = %s
522 AND step.meta_value IN ('optin', 'checkout')
523 INNER JOIN $wpdb->postmeta custom
524 ON p.ID = custom.post_id
525 AND (
526 (step.meta_value = 'optin' AND custom.meta_key = %s)
527 OR
528 (step.meta_value = 'checkout' AND custom.meta_key = %s)
529 )
530 AND custom.meta_value = %s
531 WHERE p.post_type = %s
532 AND p.post_status = %s
533 GROUP BY step.meta_value
534 ) AS subquery",
535 'wcf-step-type',
536 'wcf-optin-enable-custom-fields',
537 'wcf-custom-checkout-fields',
538 'yes',
539 CARTFLOWS_STEP_POST_TYPE,
540 'publish'
541 ),
542 ARRAY_A
543 );
544 //phpcs:enable WordPress.DB.DirectDatabaseQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
545
546 if ( empty( $step_counts ) || empty( $step_counts[0]['counts'] ) ) {
547 return $default_step_counts;
548 }
549
550 return wp_parse_args( json_decode( $step_counts[0]['counts'], true ), $default_step_counts );
551 }
552
553 /**
554 * Get the list of enabled payment gateway slugs.
555 *
556 * @since 2.2.4
557 * @return array
558 */
559 private function get_active_gateways() {
560 if ( ! wcf()->is_woo_active ) {
561 return array();
562 }
563
564 $gateways = WC()->payment_gateways->get_available_payment_gateways();
565 $enabled_gateways = array();
566
567 if ( is_array( $gateways ) ) {
568 foreach ( $gateways as $gateway ) {
569 if ( 'yes' === $gateway->enabled ) {
570 $gateway_key = strtolower( str_replace( array( ' ', '(', ')' ), '_', $gateway->get_title() ) );
571 $enabled_gateways[ $gateway_key ] = $gateway->get_title();
572 }
573 }
574 }
575
576 return $enabled_gateways;
577 }
578
579 /**
580 * Get social tracking enabled/disabled boolean flags for each platform.
581 *
582 * @since 2.2.4
583 * @return array
584 */
585 private function get_social_tracking_flags() {
586 $fb_setting = Cartflows_Helper::get_facebook_settings();
587 $ga_setting = Cartflows_Helper::get_google_analytics_settings();
588 $tik_pixel_settings = Cartflows_Helper::get_tiktok_settings();
589 $pinterest_settings = Cartflows_Helper::get_pinterest_settings();
590 $gads_settings = Cartflows_Helper::get_google_ads_settings();
591 $snapchat_settings = Cartflows_Helper::get_snapchat_settings();
592
593 return array(
594 'fb_pixel_enabled' => ! empty( $fb_setting['facebook_pixel_id'] ),
595 'ga_enabled' => ! empty( $ga_setting['google_analytics_id'] ),
596 'tiktok_enabled' => ! empty( $tik_pixel_settings['tiktok_pixel_id'] ),
597 'pinterest_enabled' => ! empty( $pinterest_settings['pinterest_tag_id'] ),
598 'gads_enabled' => ! empty( $gads_settings['google_ads_id'] ),
599 'snapchat_enabled' => ! empty( $snapchat_settings['snapchat_pixel_id'] ),
600 );
601 }
602
603 /**
604 * Check if CartFlows Pro license key exists.
605 *
606 * @since 2.2.4
607 * @return bool
608 */
609 private function check_pro_license_key_exists() {
610 if ( _is_cartflows_pro() && class_exists( 'CartFlows_Pro_Licence' ) ) {
611 $license_data = get_option( 'wc_am_client_' . \CartFlows_Pro_Licence::get_instance()->product_id . '_api_key', array() );
612 return ! empty( $license_data['api_key'] );
613 }
614 return false;
615 }
616
617 // -------------------------------------------------------------------------
618 // Milestone event tracking
619 // -------------------------------------------------------------------------
620
621 /**
622 * Detect and queue state-based one-time events.
623 *
624 * Called once per day (throttled by transient). BSF_Analytics_Events dedup
625 * prevents duplicate tracking across days.
626 *
627 * @since 2.2.4
628 * @return void
629 */
630 private function detect_state_events() {
631 $events = self::events();
632 if ( null === $events ) {
633 // BSF_Analytics_Events not yet loaded — do NOT set transient so we retry
634 // on the next admin page load.
635 return;
636 }
637
638 // Class is available — throttle for 24 hours.
639 set_transient( 'cf_state_events_checked', 1, DAY_IN_SECONDS );
640
641 // plugin_activated — fires once on first admin load after activation.
642 $bsf_referrers = get_option( 'bsf_product_referers', array() );
643 $source = ! empty( $bsf_referrers['cartflows'] ) ? sanitize_text_field( $bsf_referrers['cartflows'] ) : 'self';
644 $events->track( 'plugin_activated', CARTFLOWS_VER, array( 'source' => $source ) );
645
646 // plugin_updated — fires when version changes; re-tracks on each upgrade.
647 $stored_version = Cartflows_Helper::get_analytics_flag( 'tracked_version', '' );
648 if ( CARTFLOWS_VER !== $stored_version ) {
649 if ( ! empty( $stored_version ) ) {
650 $events->flush_pushed( array( 'plugin_updated' ) );
651 $events->track(
652 'plugin_updated',
653 CARTFLOWS_VER,
654 array( 'from_version' => $stored_version )
655 );
656 }
657 Cartflows_Helper::set_analytics_flag( 'tracked_version', CARTFLOWS_VER, true );
658 }
659
660 // onboarding_completed.
661 if ( get_option( 'wcf_setup_complete', false ) ) {
662 $events->track( 'onboarding_completed' );
663 }
664
665 // onboarding_skipped.
666 if ( get_option( 'wcf_setup_skipped', false ) ) {
667 $exit_step = get_option( 'wcf_exit_setup_step', '' );
668 $events->track(
669 'onboarding_skipped',
670 '',
671 array( 'exit_step' => sanitize_text_field( (string) $exit_step ) )
672 );
673 }
674
675 // pro_license_activated.
676 if ( _is_cartflows_pro() ) {
677 $events->track( 'pro_license_activated' );
678 }
679
680 // ---- Feature usage events ----
681
682 // first_checkout_configured (flag set in admin-core/inc/meta-ops.php).
683 $checkout_step_id = Cartflows_Helper::get_analytics_flag( 'first_checkout_configured' );
684 if ( $checkout_step_id ) {
685 $layout = get_post_meta( intval( $checkout_step_id ), 'wcf-checkout-layout', true );
686 $events->track(
687 'first_checkout_configured',
688 strval( $checkout_step_id ),
689 array( 'layout' => $layout ? sanitize_text_field( $layout ) : 'default' )
690 );
691 }
692
693 // first_template_imported (flag set in admin-core/ajax/importer.php).
694 if ( Cartflows_Helper::get_analytics_flag( 'first_template_imported' ) ) {
695 $page_builder = Cartflows_Helper::get_common_setting( 'default_page_builder' );
696 $events->track(
697 'first_template_imported',
698 '',
699 array( 'page_builder' => $page_builder ? sanitize_text_field( $page_builder ) : 'default' )
700 );
701 }
702
703 // ---- Pro feature events (flags set in cartflows-pro) ----
704
705 if ( Cartflows_Helper::get_analytics_flag( 'first_order_bump_created' ) ) {
706 $events->track( 'first_order_bump_created' );
707 }
708
709 if ( Cartflows_Helper::get_analytics_flag( 'first_upsell_accepted' ) ) {
710 $events->track( 'first_upsell_accepted' );
711 }
712
713 if ( Cartflows_Helper::get_analytics_flag( 'first_ab_test_started' ) ) {
714 $events->track( 'first_ab_test_started' );
715 }
716
717 if ( Cartflows_Helper::get_analytics_flag( 'first_ab_test_winner' ) ) {
718 $events->track( 'first_ab_test_winner_declared' );
719 }
720
721 // ---- Integration events (state-based, no flag-setters needed) ----
722
723 $tracking_flags = $this->get_social_tracking_flags();
724
725 if ( ! empty( $tracking_flags['fb_pixel_enabled'] ) ) {
726 $events->track( 'fb_pixel_connected' );
727 }
728
729 if ( ! empty( $tracking_flags['ga_enabled'] ) ) {
730 $events->track( 'ga_connected' );
731 }
732
733 if ( wcf()->is_woo_active ) {
734 $gateways = WC()->payment_gateways->get_available_payment_gateways();
735 foreach ( $gateways as $gateway_id => $gateway ) {
736 if ( 'yes' !== $gateway->enabled ) {
737 continue;
738 }
739 if ( false !== strpos( $gateway_id, 'stripe' ) ) {
740 $events->track( 'stripe_gateway_enabled' );
741 }
742 if ( false !== strpos( $gateway_id, 'paypal' ) ) {
743 $events->track( 'paypal_gateway_enabled' );
744 }
745 }
746 }
747
748 if ( ! empty( $tracking_flags['tiktok_enabled'] ) ) {
749 $events->track( 'tiktok_connected' );
750 }
751
752 if ( ! empty( $tracking_flags['pinterest_enabled'] ) ) {
753 $events->track( 'pinterest_connected' );
754 }
755
756 if ( ! empty( $tracking_flags['gads_enabled'] ) ) {
757 $events->track( 'gads_connected' );
758 }
759
760 if ( ! empty( $tracking_flags['snapchat_enabled'] ) ) {
761 $events->track( 'snapchat_connected' );
762 }
763
764 // ---- Pro feature state events ----
765
766 // first_downsell_accepted (flag set in cartflows-pro modules/downsell/classes/class-cartflows-downsell-markup.php).
767 if ( Cartflows_Helper::get_analytics_flag( 'first_downsell_accepted' ) ) {
768 $events->track( 'first_downsell_accepted' );
769 }
770
771 // first_instant_layout_enabled (flag set in admin-core/ajax/flows.php and admin-core/ajax/importer.php).
772 if ( Cartflows_Helper::get_analytics_flag( 'first_instant_layout_enabled' ) ) {
773 $events->track( 'first_instant_layout_enabled' );
774 }
775
776 // first_store_checkout_set — detected directly from the store checkout option.
777 $store_checkout_setting = Cartflows_Helper::get_global_setting( '_cartflows_store_checkout' );
778 if ( is_scalar( $store_checkout_setting ) && intval( $store_checkout_setting ) > 0 ) {
779 $events->track( 'first_store_checkout_set' );
780 }
781
782 // first_webhook_configured (flag set in admin-core/api/webhooks.php::create_webhook()).
783 if ( Cartflows_Helper::get_analytics_flag( 'first_webhook_configured' ) ) {
784 $events->track( 'first_webhook_configured' );
785 }
786
787 // first_fbt_configured (flag set in modules/frequently-bought-together/classes/class-cartflows-fbt-product-meta.php).
788 $fbt_configured = Cartflows_Helper::get_analytics_flag( 'first_fbt_configured' );
789 if ( is_array( $fbt_configured ) ) {
790 $events->track(
791 'first_fbt_configured',
792 '',
793 array(
794 'source' => sanitize_text_field( (string) ( $fbt_configured['source'] ?? 'manual' ) ),
795 'days_since_install' => (string) absint( $fbt_configured['days_since_install'] ?? 0 ),
796 )
797 );
798 }
799
800 // first_fbt_widget_added_to_cart (flag set in modules/frequently-bought-together/classes/class-cartflows-fbt-frontend.php).
801 $fbt_atc = Cartflows_Helper::get_analytics_flag( 'first_fbt_widget_added_to_cart' );
802 if ( is_array( $fbt_atc ) ) {
803 $events->track(
804 'first_fbt_widget_added_to_cart',
805 '',
806 array( 'item_count' => (string) absint( $fbt_atc['item_count'] ?? 0 ) )
807 );
808 }
809
810 // ---- Engagement events ----
811
812 $pointer_data = get_option( 'cartflows_pointer_data', array() );
813
814 if ( ! empty( $pointer_data['accepted'] ) ) {
815 $events->track( 'pointer_accepted' );
816 }
817
818 if ( ! empty( $pointer_data['dismissed'] ) ) {
819 $events->track( 'pointer_dismissed' );
820 }
821
822 // weekly_report_notice_dismissed (flag set in class-cartflows-admin-notices.php).
823 if ( Cartflows_Helper::get_analytics_flag( 'weekly_report_notice_dismissed' ) ) {
824 $events->track( 'weekly_report_notice_dismissed' );
825 }
826
827 // instant_checkout_notice_dismissed (option set in admin-core/ajax/flows.php).
828 if ( 'yes' === get_option( 'wcf-instant-checkout-notice-skipped' ) ) {
829 $events->track( 'instant_checkout_notice_dismissed' );
830 }
831 }
832
833 /**
834 * Track first flow published.
835 *
836 * Fires on transition_post_status when a cartflows_flow moves to 'publish'.
837 * BSF_Analytics_Events dedup ensures this fires only once per site lifecycle.
838 *
839 * @since 2.2.4
840 * @param string $new_status New post status.
841 * @param string $old_status Old post status.
842 * @param \WP_Post $post Post object.
843 * @return void
844 */
845 public function track_first_flow_published( $new_status, $old_status, $post ) {
846 if ( CARTFLOWS_FLOW_POST_TYPE !== $post->post_type ) {
847 return;
848 }
849
850 if ( 'publish' !== $new_status || 'publish' === $old_status ) {
851 return;
852 }
853
854 $events = self::events();
855 if ( null === $events ) {
856 return;
857 }
858
859 // days_since_install for time-to-value analysis.
860 $install_time = (int) Cartflows_Helper::get_analytics_flag( 'usage_installed_time', 0 );
861 $days = 0;
862 if ( $install_time > 0 ) {
863 $days = (int) floor( ( time() - $install_time ) / DAY_IN_SECONDS );
864 }
865
866 // Flow source: template or scratch.
867 // Read per-flow post meta written at creation time so each flow reports its own source.
868 $creation_method = get_post_meta( $post->ID, 'wcf-flow-creation-method', true );
869 $source = ( 'ready_made_template' === $creation_method ) ? 'template' : 'scratch';
870
871 // Step count for this flow.
872 $steps = get_post_meta( $post->ID, 'wcf-steps', true );
873 $step_count = is_array( $steps ) ? count( $steps ) : 0;
874
875 $events->track(
876 'first_flow_published',
877 strval( $post->ID ),
878 array(
879 'days_since_install' => (string) $days,
880 'source' => $source,
881 'step_count' => (string) $step_count,
882 )
883 );
884 }
885
886 /**
887 * Track first checkout configured.
888 *
889 * Fires whenever wcf-checkout-products is written on a checkout step, so
890 * template imports and Store Checkout are captured alongside the admin save.
891 *
892 * @since 2.2.4
893 * @param int $meta_id Meta row ID.
894 * @param int $post_id Post ID the meta belongs to.
895 * @param string $meta_key Meta key being written.
896 * @param mixed $meta_value Meta value being written.
897 * @return void
898 */
899 public function track_first_checkout_configured( $meta_id, $post_id, $meta_key, $meta_value ) {
900 if ( 'wcf-checkout-products' !== $meta_key ) {
901 return;
902 }
903
904 if ( empty( $meta_value ) ) {
905 return;
906 }
907
908 if ( CARTFLOWS_STEP_POST_TYPE !== get_post_type( $post_id ) ) {
909 return;
910 }
911
912 // Value is the step ID, not a boolean — get_stats reads it back to resolve the layout.
913 Cartflows_Helper::set_analytics_flag( 'first_checkout_configured', $post_id );
914 }
915 }
916
917 Cartflows_Analytics::get_instance();
918