PluginProbe
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce / 3.1.1
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce v3.1.1
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.1.1, at classes/class-cartflows-analytics.php

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