* The WooCommerce tracker class adds functionality to track WooCommerce usage based on if the customer opted in.
* No personal information is tracked, only general WooCommerce settings, general product, order and user counts and admin email for discount code.
* @package WooCommerce\Classes
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Internal\Admin\EmailImprovements\EmailImprovements;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use Automattic\WooCommerce\Utilities\{ FeaturesUtil, OrderUtil, PluginUtil };
use Automattic\WooCommerce\Internal\Utilities\BlocksUtil;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
defined( 'ABSPATH' ) || exit;
// phpcs:disable Squiz.Classes.ClassFileName.NoMatch, Squiz.Classes.ValidClassName.NotCamelCaps -- Backwards compatibility.
* WooCommerce Tracker Class
* URL to the WooThemes Tracker API endpoint.
private static $api_url = 'https://tracking.woocommerce.com/v1/';
public static function init() { // phpcs:ignore WooCommerce.Functions.InternalInjectionMethod.MissingFinal, WooCommerce.Functions.InternalInjectionMethod.MissingInternalTag -- Not an injection.
add_action( 'woocommerce_tracker_send_event', array( __CLASS__, 'send_tracking_data' ) );
* Decide whether to send tracking data or not.
* @param boolean $override Should override?.
public static function send_tracking_data( $override = false ) {
// Don't trigger this on AJAX Requests.
if ( Constants::is_true( 'DOING_AJAX' ) ) {
* Filter whether to send tracking data or not.
if ( ! apply_filters( 'woocommerce_tracker_send_override', $override ) ) {
// Send a maximum of once per week by default.
$last_send = self::get_last_send_time();
if ( $last_send && $last_send > apply_filters( 'woocommerce_tracker_last_send_interval', strtotime( '-1 week' ) ) ) { // phpcs:ignore
// Make sure there is at least a 1 hour delay between override sends, we don't want duplicate calls due to double clicking links.
$last_send = self::get_last_send_time();
if ( $last_send && $last_send > strtotime( '-1 hours' ) ) {
// Update time first before sending to ensure it is set.
update_option( 'woocommerce_tracker_last_send', time() );
$params = self::get_tracking_data();
'headers' => array( 'user-agent' => 'WooCommerceTracker/' . md5( esc_url_raw( home_url( '/' ) ) ) . ';' ),
'body' => wp_json_encode( $params ),
* Get the last time tracking data was sent.
private static function get_last_send_time() {
* Filter the last time tracking data was sent.
return apply_filters( 'woocommerce_tracker_last_send_time', get_option( 'woocommerce_tracker_last_send', false ) );
* Test whether this site is a staging site according to the Jetpack criteria.
* With Jetpack 8.1+, Jetpack::is_staging_site has been deprecated.
* \Automattic\Jetpack\Status::is_staging_site is the replacement.
* However, there are version of JP where \Automattic\Jetpack\Status exists, but does *not* contain is_staging_site method,
* so with those, code still needs to use the previous check as a fallback.
* After upgrading Jetpack Status to v3.3.2 is_staging_site is also deprecated and in_safe_mode is the new replacement.
* So we check this first of all.
private static function is_jetpack_staging_site() {
if ( class_exists( '\Automattic\Jetpack\Status' ) ) {
$jp_status = new \Automattic\Jetpack\Status();
if ( is_callable( array( $jp_status, 'in_safe_mode' ) ) ) {
return $jp_status->in_safe_mode();
} elseif ( is_callable( array( $jp_status, 'is_staging_site' ) ) ) {
// Preferred way of checking with Jetpack 8.1+.
return $jp_status->is_staging_site();
return ( class_exists( 'Jetpack' ) && is_callable( 'Jetpack::is_staging_site' ) && Jetpack::is_staging_site() );
* Get all the tracking data.
public static function get_tracking_data() {
$start_time = microtime( true );
$data['url'] = home_url();
$data['store_id'] = get_option( \WC_Install::STORE_ID_OPTION, null );
$data['blog_id'] = class_exists( 'Jetpack_Options' ) ? Jetpack_Options::get_option( 'id' ) : null;
* Filter the admin email that's sent with data.
$data['email'] = apply_filters( 'woocommerce_tracker_admin_email', get_option( 'admin_email' ) );
$data['theme'] = self::get_theme_info();
$data['wp'] = self::get_wordpress_info();
$data['server'] = self::get_server_info();
$all_plugins = self::get_all_plugins();
$data['active_plugins'] = $all_plugins['active_plugins'];
$data['inactive_plugins'] = $all_plugins['inactive_plugins'];
// Jetpack & WooCommerce Connect.
$data['jetpack_version'] = Constants::is_defined( 'JETPACK__VERSION' ) ? Constants::get_constant( 'JETPACK__VERSION' ) : 'none';
$data['jetpack_connected'] = ( class_exists( 'Jetpack' ) && is_callable( 'Jetpack::is_active' ) && Jetpack::is_active() ) ? 'yes' : 'no';
$data['jetpack_is_staging'] = self::is_jetpack_staging_site() ? 'yes' : 'no';
$data['connect_installed'] = class_exists( 'WC_Connect_Loader' ) ? 'yes' : 'no';
$data['connect_active'] = ( class_exists( 'WC_Connect_Loader' ) && wp_next_scheduled( 'wc_connect_fetch_service_schemas' ) ) ? 'yes' : 'no';
$data['helper_connected'] = self::get_helper_connected();
$data['users'] = self::get_user_counts();
$data['products'] = self::get_product_counts();
$data['orders'] = self::get_orders();
$data['reviews'] = self::get_review_counts();
$data['categories'] = self::get_category_counts();
$data['brands'] = self::get_brands_counts();
$data['order_snapshot'] = self::get_order_snapshot();
$data['gateways'] = self::get_active_payment_gateways();
$data['wcpay_settings'] = self::get_wcpay_settings();
$data['shipping_methods'] = self::get_active_shipping_methods();
$data['enabled_features'] = self::get_enabled_features();
// Get all WooCommerce options info.
$data['settings'] = self::get_all_woocommerce_options_values();
$template_overrides = self::get_all_template_overrides();
$data['template_overrides'] = $template_overrides;
// Cart & checkout tech (blocks or shortcodes).
$data['cart_checkout'] = self::get_cart_checkout_info();
// Mini Cart block, which only exists since wp 5.9.
if ( version_compare( get_bloginfo( 'version' ), '5.9', '>=' ) ) {
$data['mini_cart_block'] = self::get_mini_cart_info();
* Filter whether to disable admin tracking.
$data['wc_admin_disabled'] = apply_filters( 'woocommerce_admin_disabled', false ) ? 'yes' : 'no';
$data['wc_mobile_usage'] = self::get_woocommerce_mobile_usage();
$data['woocommerce_allow_tracking'] = get_option( 'woocommerce_allow_tracking', 'no' );
$data['woocommerce_allow_tracking_last_modified'] = get_option( 'woocommerce_allow_tracking_last_modified', 'unknown' );
$data['woocommerce_allow_tracking_first_optin'] = get_option( 'woocommerce_allow_tracking_first_optin', 'unknown' );
// Email improvements tracking data.
$data['email_improvements'] = self::get_email_improvements_info( $template_overrides );
$data['store_emails'] = self::get_store_emails();
* Filter the data that's sent with the tracker.
$data = apply_filters( 'woocommerce_tracker_data', $data );
// Total seconds taken to generate snapshot (including filtered data).
$data['snapshot_generation_time'] = microtime( true ) - $start_time;
* Get the current theme info, theme name and version.
public static function get_theme_info() {
$theme_data = wp_get_theme();
$theme_child_theme = wc_bool_to_string( is_child_theme() );
$theme_wc_support = wc_bool_to_string( current_theme_supports( 'woocommerce' ) );
$theme_is_block_theme = wc_bool_to_string( wp_is_block_theme() );
'name' => $theme_data->Name, // @phpcs:ignore
'version' => $theme_data->Version, // @phpcs:ignore
'child_theme' => $theme_child_theme,
'wc_support' => $theme_wc_support,
'block_theme' => $theme_is_block_theme,
* Get WordPress related data.
private static function get_wordpress_info() {
$memory = wc_let_to_num( WP_MEMORY_LIMIT );
if ( function_exists( 'memory_get_usage' ) ) {
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- False positive.
$system_memory = wc_let_to_num( @ini_get( 'memory_limit' ) );
$memory = max( $memory, $system_memory );
// WordPress 5.5+ environment type specification.
// 'production' is the default in WP, thus using it as a default here, too.
$environment_type = 'production';
if ( function_exists( 'wp_get_environment_type' ) ) {
$environment_type = wp_get_environment_type();
$wp_data['memory_limit'] = size_format( $memory );
$wp_data['debug_mode'] = ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ? 'Yes' : 'No';
$wp_data['locale'] = get_locale();
$wp_data['version'] = get_bloginfo( 'version' );
$wp_data['multisite'] = is_multisite() ? 'Yes' : 'No';
$wp_data['env_type'] = $environment_type;
$wp_data['dropins'] = array_keys( get_dropins() );
* Get server related info.
private static function get_server_info() {
if ( ! empty( $_SERVER['SERVER_SOFTWARE'] ) ) {
$server_data['software'] = $_SERVER['SERVER_SOFTWARE']; // @phpcs:ignore
if ( function_exists( 'phpversion' ) ) {
$server_data['php_version'] = phpversion();
if ( function_exists( 'ini_get' ) ) {
$server_data['php_post_max_size'] = size_format( wc_let_to_num( ini_get( 'post_max_size' ) ) );
$server_data['php_time_limt'] = ini_get( 'max_execution_time' );
$server_data['php_max_input_vars'] = ini_get( 'max_input_vars' );
$server_data['php_suhosin'] = extension_loaded( 'suhosin' ) ? 'Yes' : 'No';
$database_version = wc_get_server_database_version();
$server_data['mysql_version'] = $database_version['number'];
$server_data['php_max_upload_size'] = size_format( wp_max_upload_size() );
$server_data['php_default_timezone'] = date_default_timezone_get();
$server_data['php_soap'] = class_exists( 'SoapClient' ) ? 'Yes' : 'No';
$server_data['php_fsockopen'] = function_exists( 'fsockopen' ) ? 'Yes' : 'No';
$server_data['php_curl'] = function_exists( 'curl_init' ) ? 'Yes' : 'No';
* Get all plugins grouped into activated or not.
public static function get_all_plugins() {
// Ensure get_plugins function is loaded.
if ( ! function_exists( 'get_plugins' ) ) {
include ABSPATH . '/wp-admin/includes/plugin.php';
$plugins = wc_get_container()->get( LegacyProxy::class )->call_function( 'get_plugins' );
$active_plugins_keys = get_option( 'active_plugins', array() );
$active_plugins = array();
foreach ( $plugins as $k => $v ) {
// Take care of formatting the data how we want it.
$formatted['name'] = wp_strip_all_tags( $v['Name'] );
if ( isset( $v['Version'] ) ) {
$formatted['version'] = wp_strip_all_tags( $v['Version'] );
if ( isset( $v['Author'] ) ) {
$formatted['author'] = wp_strip_all_tags( $v['Author'] );
if ( isset( $v['Network'] ) ) {
$formatted['network'] = wp_strip_all_tags( $v['Network'] );
if ( isset( $v['PluginURI'] ) ) {
$formatted['plugin_uri'] = wp_strip_all_tags( $v['PluginURI'] );
$formatted['feature_compatibility'] = array();
if ( wc_get_container()->get( PluginUtil::class )->is_woocommerce_aware_plugin( $k ) ) {
$formatted['feature_compatibility'] = array_filter( FeaturesUtil::get_compatible_features_for_plugin( $k ) );
if ( in_array( $k, $active_plugins_keys, true ) ) {
// Remove active plugins from list so we can show active and inactive separately.
$active_plugins[ $k ] = $formatted;
$plugins[ $k ] = $formatted;
'active_plugins' => $active_plugins,
'inactive_plugins' => $plugins,
* Get the settings of WooCommerce Payments plugin
private static function get_wcpay_settings() {
return get_option( 'woocommerce_woocommerce_payments_settings' );
* Check to see if the helper is connected to WooCommerce.com
private static function get_helper_connected() {
if ( class_exists( 'WC_Helper_Options' ) && is_callable( 'WC_Helper_Options::get' ) ) {
$authenticated = WC_Helper_Options::get( 'auth' );
return ( ! empty( $authenticated ) ) ? 'yes' : 'no';
* Get user totals based on user role.
private static function get_user_counts() {
$user_count_data = count_users();
$user_count['total'] = $user_count_data['total_users'];
// Get user count based on user role.
foreach ( $user_count_data['avail_roles'] as $role => $count ) {
$user_count[ $role ] = $count;
* Get product totals based on product type.
public static function get_product_counts() {
$product_count = array();
$product_count_data = wp_count_posts( 'product' );
$product_count['total'] = $product_count_data->publish;
$product_statuses = get_terms( 'product_type', array( 'hide_empty' => 0 ) );
foreach ( $product_statuses as $product_status ) {
$product_count[ $product_status->name ] = $product_status->count;
private static function get_order_counts() {
foreach ( wc_get_order_statuses() as $status_slug => $status_name ) {
$order_count[ $status_slug ] = wc_orders_count( $status_slug );
* Combine all order data.
private static function get_orders() {
$order_dates = self::get_order_dates();
$order_counts = self::get_order_counts();
$order_totals = self::get_order_totals();
$order_gateways = self::get_orders_by_gateway();
$order_origin = self::get_orders_origins();
return array_merge( $order_dates, $order_counts, $order_totals, $order_gateways, $order_origin );
* Keeping the internal statuses names as strings to avoid regression issues (not referencing Automattic\WooCommerce\Enums\OrderInternalStatus class).
private static function get_order_totals() {
$orders_table = OrdersTableDataStore::get_orders_table_name();
if ( OrderUtil::custom_orders_table_usage_is_enabled() ) {
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$gross_total = $wpdb->get_var(
SELECT SUM(total_amount) AS 'gross_total'
WHERE status in ('wc-completed', 'wc-refunded');