PluginProbe
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce / 3.3.0
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce v3.3.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
← All changes | classes/class-cartflows-analytics.php +294 -27 3.0.1 → 3.3.0 View file →
@@ -23,9 +23,9 @@
23 23 /**
24 24 * Instance
25 25 *
26 26 * @access private
27 - * @var object Class object.
27 + * @var Cartflows_Analytics|null Class object.
28 28 * @since 2.2.4
29 29 */
30 30 private static $instance;
31 31
@@ -39,9 +39,9 @@
39 39 /**
40 40 * Initiator
41 41 *
42 42 * @since 2.2.4
43 - * @return object initialized object of class.
43 + * @return Cartflows_Analytics initialized object of class.
44 44 */
45 45 public static function get_instance() {
46 46 if ( ! isset( self::$instance ) ) {
47 47 self::$instance = new self();
@@ -64,8 +64,12 @@
64 64 add_filter( 'bsf_core_stats', array( $this, 'get_stats' ) );
65 65
66 66 // Hook-based event: first flow published.
67 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 );
68 72 }
69 73
70 74 /**
71 75 * Get shared BSF_Analytics_Events instance.
@@ -84,8 +88,47 @@
84 88 }
85 89 return self::$events;
86 90 }
87 91
92 + /**
93 + * Whether non-sensitive usage tracking is opted in.
94 + *
95 + * Reads the site option so multisite network opt-in resolves the same way BSF Analytics does.
96 + *
97 + * @since x.x.x
98 + * @return bool
99 + */
100 + public static function is_tracking_opted_in() {
101 + // Honour the BSF-wide kill switch and white-label first — BSF_Analytics::is_tracking_enabled() checks both.
102 + if ( ! apply_filters( 'bsf_usage_tracking_enabled', true ) ) {
103 + return false;
104 + }
105 +
106 + $white_label = apply_filters( 'cf_white_label_options', array() );
107 + if ( is_array( $white_label ) && in_array( true, $white_label, true ) ) {
108 + return false;
109 + }
110 +
111 + // BSF stores the literal string 'no' on opt-out, which a bare bool cast would treat as opted in.
112 + return (bool) apply_filters( 'cartflows_enable_non_sensitive_data_tracking', 'yes' === get_site_option( 'cf_usage_optin', false ) );
113 + }
114 +
115 + /**
116 + * Whole days elapsed since the plugin was installed, 0 when unknown.
117 + *
118 + * @since x.x.x
119 + * @return int
120 + */
121 + private function get_days_since_install() {
122 + $install_time = Cartflows_Helper::get_analytics_flag( 'usage_installed_time', 0 );
123 +
124 + if ( ! is_numeric( $install_time ) || (int) $install_time <= 0 ) {
125 + return 0;
126 + }
127 +
128 + return (int) floor( ( time() - (int) $install_time ) / DAY_IN_SECONDS );
129 + }
130 +
88 131 // -------------------------------------------------------------------------
89 132 // BSF Analytics stats payload
90 133 // -------------------------------------------------------------------------
91 134
@@ -99,11 +142,9 @@
99 142 * @param array $stats_data Existing analytics payload.
100 143 * @return array
101 144 */
102 145 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 ) ) ) {
146 + if ( ! self::is_tracking_opted_in() ) {
106 147 return $stats_data;
107 148 }
108 149
109 150 $theme_data = $this->get_active_theme();
@@ -142,12 +183,14 @@
142 183 private function get_default_stats( $theme_data ) {
143 184 $bsf_internal_referer = get_option( 'bsf_product_referers', array() );
144 185 $store_location = '';
145 186 $woo_version = '';
187 + $store_currency = '';
146 188
147 189 if ( wcf()->is_woo_active ) {
148 190 $store_location = wc_get_base_location();
149 191 $woo_version = WC()->version;
192 + $store_currency = get_woocommerce_currency();
150 193 }
151 194
152 195 // Sanitize knowledge base search terms before sending.
153 196 $raw_kb_searches = get_option( 'cartflows_kb_searches', array() );
@@ -163,15 +206,17 @@
163 206 'active-theme' => $theme_data['parent_theme'],
164 207 'active-gateway-count' => wcf()->is_woo_active ? count( (array) $this->get_active_gateways() ) : 0,
165 208 'social-tracking' => $this->get_social_tracking_flags(),
166 209 'store-country' => ! empty( $store_location['country'] ) ? $store_location['country'] : '',
210 + // Required to make the fbt_revenue KPI comparable across stores.
211 + 'store-currency' => $store_currency,
167 212 'documentation-search-terms' => $kb_searches,
168 213 'internal_referer' => ! empty( $bsf_internal_referer['cartflows'] ) ? $bsf_internal_referer['cartflows'] : '',
169 214 // Simplified: boolean flag only — no raw feedback object.
170 215 'nps_survey_submitted' => ! empty( get_option( 'nps-survey-cartflows', array() ) ),
171 216 '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() ) ),
217 + // Resolved count — includes auto-completed modules the option never stores.
218 + 'learn_modules_completed' => Cartflows_Learn_Progress::get_instance()->get_completed_count(),
174 219 );
175 220 }
176 221
177 222 /**
@@ -183,15 +228,25 @@
183 228 private function get_kpi_tracking_data() {
184 229 $kpi_data = array();
185 230
186 231 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 - ),
232 + $date = wp_date( 'Y-m-d', strtotime( "-{$i} days" ) );
233 + // Keys stay present when WooCommerce is inactive so the columns never disappear mid-series.
234 + $numeric = array(
235 + 'order_count' => $this->get_daily_orders_count( $date ),
236 + 'offer_orders' => $this->get_daily_offer_orders_count( $date ),
237 + 'fbt_orders' => 0,
238 + 'fbt_revenue' => '0.00',
193 239 );
240 +
241 + // FBT only loads when WooCommerce is active, so the class may be absent.
242 + if ( class_exists( 'Cartflows_Fbt_Analytics' ) ) {
243 + $fbt = Cartflows_Fbt_Analytics::get_daily_stats( (string) $date );
244 + $numeric['fbt_orders'] = $fbt['fbt_orders'];
245 + $numeric['fbt_revenue'] = $fbt['fbt_revenue'];
246 + }
247 +
248 + $kpi_data[ $date ] = array( 'numeric_values' => $numeric );
194 249 }
195 250
196 251 return $kpi_data;
197 252 }
@@ -320,22 +375,32 @@
320 375 'ready_made_template' => 0,
321 376 )
322 377 );
323 378
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'] ),
379 + $numeric_stats = array(
380 + 'total_flows' => wp_count_posts( CARTFLOWS_FLOW_POST_TYPE )->publish,
381 + 'total_ic_funnels' => strval( $flows_with_instant_checkout ),
382 + 'optin_step_count' => strval( $steps_counts['optin'] ),
383 + 'landing_step_count' => strval( $steps_counts['landing'] ),
384 + 'checkout_step_count' => strval( $steps_counts['checkout'] ),
385 + 'upsell_step_count' => strval( $steps_counts['upsell'] ),
386 + 'downsell_step_count' => strval( $steps_counts['downsell'] ),
387 + 'thankyou_step_count' => strval( $steps_counts['thankyou'] ),
388 + 'optin_custom_fields' => strval( $custom_fields_data['optin'] ),
389 + 'checkout_custom_fields' => strval( $custom_fields_data['checkout'] ),
390 + 'funnels_from_scratch' => strval( $funnel_building_behavior['scratch'] ),
391 + 'funnels_from_template' => strval( $funnel_building_behavior['ready_made_template'] ),
392 + // Keys stay present when WooCommerce is inactive so the columns never disappear mid-series.
393 + 'fbt_products_configured' => '0',
394 + 'fbt_ai_products' => '0',
337 395 );
396 +
397 + // FBT only loads when WooCommerce is active, so the class may be absent.
398 + if ( class_exists( 'Cartflows_Fbt_Analytics' ) ) {
399 + $numeric_stats = array_merge( $numeric_stats, Cartflows_Fbt_Analytics::get_numeric_stats() );
400 + }
401 +
402 + return $numeric_stats;
338 403 }
339 404
340 405 /**
341 406 * Retrieve boolean data statistics for CartFlows.
@@ -352,12 +417,14 @@
352 417 'disallow-indexing' => ! empty( $common_settings['disallow_indexing'] ) && 'enable' === $common_settings['disallow_indexing'] ? true : false,
353 418 'paypal-reference-transactions' => ! empty( $common_settings['paypal_reference_transactions'] ) && 'enable' === $common_settings['paypal_reference_transactions'],
354 419 'cartflows-stats-report-emails' => 'enable' === get_option( 'cartflows_stats_report_emails', 'enable' ),
355 420 'cartflows-delete-plugin-data' => 'enable' === get_option( 'cartflows_delete_plugin_data' ),
356 - 'pre-checkout-offer' => 'enable' === $common_settings['pre_checkout_offer'],
421 + 'pre-checkout-offer' => isset( $common_settings['pre_checkout_offer'] ) && 'enable' === $common_settings['pre_checkout_offer'],
357 422 'store-checkout-set' => ! empty( intval( Cartflows_Helper::get_global_setting( '_cartflows_store_checkout' ) ) ),
358 423 'is-child-theme' => $theme_data['child_theme'],
359 424 'suretriggers_active' => is_plugin_active( 'suretriggers/suretriggers.php' ),
425 + // Account-level AI connection — powers FBT Auto Suggest and content generation.
426 + 'ai_auth_connected' => class_exists( 'Cartflows_Ai_Auth' ) && (bool) Cartflows_Ai_Auth::get_instance()->get_auth_status(),
360 427 );
361 428 }
362 429
363 430 // -------------------------------------------------------------------------
@@ -753,8 +820,31 @@
753 820 if ( Cartflows_Helper::get_analytics_flag( 'first_webhook_configured' ) ) {
754 821 $events->track( 'first_webhook_configured' );
755 822 }
756 823
824 + // first_fbt_configured (flag set in modules/frequently-bought-together/classes/class-cartflows-fbt-product-meta.php).
825 + $fbt_configured = Cartflows_Helper::get_analytics_flag( 'first_fbt_configured' );
826 + if ( is_array( $fbt_configured ) ) {
827 + $events->track(
828 + 'first_fbt_configured',
829 + '',
830 + array(
831 + 'source' => sanitize_text_field( (string) ( $fbt_configured['source'] ?? 'manual' ) ),
832 + 'days_since_install' => (string) absint( $fbt_configured['days_since_install'] ?? 0 ),
833 + )
834 + );
835 + }
836 +
837 + // first_fbt_widget_added_to_cart (flag set in modules/frequently-bought-together/classes/class-cartflows-fbt-frontend.php).
838 + $fbt_atc = Cartflows_Helper::get_analytics_flag( 'first_fbt_widget_added_to_cart' );
839 + if ( is_array( $fbt_atc ) ) {
840 + $events->track(
841 + 'first_fbt_widget_added_to_cart',
842 + '',
843 + array( 'item_count' => (string) absint( $fbt_atc['item_count'] ?? 0 ) )
844 + );
845 + }
846 +
757 847 // ---- Engagement events ----
758 848
759 849 $pointer_data = get_option( 'cartflows_pointer_data', array() );
760 850
@@ -827,8 +917,185 @@
827 917 'source' => $source,
828 918 'step_count' => (string) $step_count,
829 919 )
830 920 );
921 + }
922 +
923 + /**
924 + * Track first checkout configured.
925 + *
926 + * Fires whenever wcf-checkout-products is written on a checkout step, so
927 + * template imports and Store Checkout are captured alongside the admin save.
928 + *
929 + * @since 2.2.4
930 + * @param int $meta_id Meta row ID.
931 + * @param int $post_id Post ID the meta belongs to.
932 + * @param string $meta_key Meta key being written.
933 + * @param mixed $meta_value Meta value being written.
934 + * @return void
935 + */
936 + public function track_first_checkout_configured( $meta_id, $post_id, $meta_key, $meta_value ) {
937 + if ( 'wcf-checkout-products' !== $meta_key ) {
938 + return;
939 + }
940 +
941 + if ( empty( $meta_value ) ) {
942 + return;
943 + }
944 +
945 + if ( CARTFLOWS_STEP_POST_TYPE !== get_post_type( $post_id ) ) {
946 + return;
947 + }
948 +
949 + // Value is the step ID, not a boolean — get_stats reads it back to resolve the layout.
950 + Cartflows_Helper::set_analytics_flag( 'first_checkout_configured', $post_id );
951 + }
952 +
953 + // -------------------------------------------------------------------------
954 + // Learn section events
955 + // -------------------------------------------------------------------------
956 +
957 + /**
958 + * Learn event names the browser is allowed to record.
959 + *
960 + * @since x.x.x
961 + * @return array<int, string>
962 + */
963 + public static function get_trackable_learn_events() {
964 + return array( 'learn_page_viewed', 'learn_module_action', 'learn_action_failed', 'learn_video_opened' );
965 + }
966 +
967 + /**
968 + * Learn CTA action types accepted from the browser.
969 + *
970 + * @since x.x.x
971 + * @return array<int, string>
972 + */
973 + public static function get_learn_action_types() {
974 + return array( 'install_plugin', 'redirect', 'learn_how', 'upgrade', 'activate_pro' );
975 + }
976 +
977 + /**
978 + * Learn failure reasons accepted from the browser.
979 + *
980 + * @since x.x.x
981 + * @return array<int, string>
982 + */
983 + public static function get_learn_failure_reasons() {
984 + return array( 'install_failed', 'activate_failed', 'progress_save_failed' );
985 + }
986 +
987 + /**
988 + * Plugin slugs the Learn checklist can install.
989 + *
990 + * @since x.x.x
991 + * @return array<int, string>
992 + */
993 + public static function get_learn_plugin_slugs() {
994 + return array( 'woocommerce', 'woo-cart-abandonment-recovery', 'modern-cart' );
995 + }
996 +
997 + /**
998 + * Track the first Learn page view as a milestone and refresh the per-module progress snapshot.
999 + *
1000 + * Fired from the AJAX recorder, not the REST route, so a GET never writes options.
1001 + *
1002 + * @since x.x.x
1003 + * @return void
1004 + */
1005 + public function track_learn_page_view() {
1006 +
1007 + $events = self::events();
1008 + if ( null === $events || ! self::is_tracking_opted_in() ) {
1009 + return;
1010 + }
1011 +
1012 + $progress = Cartflows_Learn_Progress::get_instance();
1013 + $states = $progress->get_module_states();
1014 +
1015 + // Unforced on purpose: a one-time milestone keyed on time-to-value, like first_flow_published.
1016 + $events->track(
1017 + 'learn_page_viewed',
1018 + (string) $this->get_days_since_install(),
1019 + array(
1020 + 'total' => (string) $progress->get_total_count(),
1021 + 'completed' => (string) $progress->get_completed_count(),
1022 + )
1023 + );
1024 +
1025 + // Forced so the server always holds the latest snapshot — one row per site.
1026 + $events->track( 'learn_progress', $progress->get_progress_value(), $states, true );
1027 + }
1028 +
1029 + /**
1030 + * Track a Learn module CTA click.
1031 + *
1032 + * @since x.x.x
1033 + * @param string $module_id Whitelisted module identifier.
1034 + * @param string $action_type Whitelisted action type.
1035 + * @param bool $is_pro Whether the module is a Pro feature.
1036 + * @return void
1037 + */
1038 + public function track_learn_module_action( $module_id, $action_type, $is_pro ) {
1039 +
1040 + $events = self::events();
1041 + if ( null === $events || ! self::is_tracking_opted_in() ) {
1042 + return;
1043 + }
1044 +
1045 + // Forced events overwrite by name, so the module is folded in or clicks on two modules collapse to one.
1046 + $events->track(
1047 + 'learn_module_action_' . $module_id,
1048 + $module_id,
1049 + array(
1050 + 'action_type' => $action_type,
1051 + 'is_pro' => $is_pro ? 'yes' : 'no',
1052 + ),
1053 + true
1054 + );
1055 + }
1056 +
1057 + /**
1058 + * Track a failed Learn module action.
1059 + *
1060 + * @since x.x.x
1061 + * @param string $module_id Whitelisted module identifier.
1062 + * @param string $reason Whitelisted failure reason.
1063 + * @param string $plugin_slug Whitelisted plugin slug, empty when not plugin related.
1064 + * @return void
1065 + */
1066 + public function track_learn_action_failed( $module_id, $reason, $plugin_slug = '' ) {
1067 +
1068 + $events = self::events();
1069 + if ( null === $events || ! self::is_tracking_opted_in() ) {
1070 + return;
1071 + }
1072 +
1073 + $events->track(
1074 + 'learn_action_failed_' . $module_id,
1075 + $module_id,
1076 + array(
1077 + 'reason' => $reason,
1078 + 'plugin_slug' => $plugin_slug,
1079 + ),
1080 + true
1081 + );
1082 + }
1083 +
1084 + /**
1085 + * Track the Learn getting-started video being opened.
1086 + *
1087 + * @since x.x.x
1088 + * @return void
1089 + */
1090 + public function track_learn_video_opened() {
1091 +
1092 + $events = self::events();
1093 + if ( null === $events || ! self::is_tracking_opted_in() ) {
1094 + return;
1095 + }
1096 +
1097 + $events->track( 'learn_video_opened', CARTFLOWS_VER );
831 1098 }
832 1099 }
833 1100
834 1101 Cartflows_Analytics::get_instance();