Statsdot: * activating the plugin alone loads nothing from statsdot.com. * * @return array{enabled: bool, domain: string, site_id: int, exclude_admins: bool, auto: bool, woocommerce: bool, revenue: bool, order_id: bool, scroll: bool, bot_key: string} */ function statsdot_settings() { $saved = get_option( 'statsdot_settings', array() ); $saved = is_array( $saved ) ? $saved : array(); return array( 'enabled' => ! empty( $saved['enabled'] ), 'domain' => ! empty( $saved['domain'] ) ? (string) $saved['domain'] : statsdot_default_domain(), 'site_id' => isset( $saved['site_id'] ) ? absint( $saved['site_id'] ) : 0, 'exclude_admins' => isset( $saved['exclude_admins'] ) ? (bool) $saved['exclude_admins'] : true, 'auto' => isset( $saved['auto'] ) ? (bool) $saved['auto'] : true, 'woocommerce' => isset( $saved['woocommerce'] ) ? (bool) $saved['woocommerce'] : true, 'revenue' => isset( $saved['revenue'] ) ? (bool) $saved['revenue'] : true, 'order_id' => isset( $saved['order_id'] ) ? (bool) $saved['order_id'] : false, 'scroll' => isset( $saved['scroll'] ) ? (bool) $saved['scroll'] : false, 'bot_key' => isset( $saved['bot_key'] ) ? (string) $saved['bot_key'] : '', ); } /** * The site's host without "www.", which is how Statsdot stores domains. * * @return string */ function statsdot_default_domain() { $host = (string) wp_parse_url( home_url(), PHP_URL_HOST ); return (string) preg_replace( '/^www\./i', '', strtolower( $host ) ); } /** * Whether this page view is measured: only once tracking is turned on, never * in the admin, the customizer or previews, and not for administrators unless * the setting says so. * * @return bool */ function statsdot_should_track() { if ( is_admin() || is_customize_preview() || is_preview() ) { return false; } $settings = statsdot_settings(); if ( ! $settings['enabled'] ) { return false; } return ! ( $settings['exclude_admins'] && current_user_can( 'manage_options' ) ); } /** * Whether the WooCommerce events are on for this page view. * * @return bool */ function statsdot_woocommerce_on() { $settings = statsdot_settings(); return class_exists( 'WooCommerce' ) && $settings['woocommerce'] && statsdot_should_track(); } /** * Loads the tracker (under 2 KB, served by statsdot.com) in the head, deferred. */ function statsdot_enqueue() { if ( ! statsdot_should_track() ) { return; } // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- the file is versioned by statsdot.com. wp_enqueue_script( 'statsdot', STATSDOT_SCRIPT, array(), null, array( 'strategy' => 'defer' ) ); if ( statsdot_woocommerce_on() ) { // A handle of its own in the footer: an inline script attached to "statsdot" // would make WordPress drop the defer strategy and block the page. wp_register_script( 'statsdot-woocommerce', false, array(), STATSDOT_VERSION, array( 'in_footer' => true ) ); wp_enqueue_script( 'statsdot-woocommerce' ); // Product page forms submit normally; AJAX buttons on lists fire WooCommerce's "added_to_cart". wp_add_inline_script( 'statsdot-woocommerce', "document.addEventListener('DOMContentLoaded',function(){" . "document.querySelectorAll('form.cart').forEach(function(f){f.setAttribute('data-statsdot-event','Add to cart');});" . "if(window.jQuery){window.jQuery(document.body).on('added_to_cart',function(){window.statsdot&&window.statsdot.track('Add to cart');});}" . '});' ); } } add_action( 'wp_enqueue_scripts', 'statsdot_enqueue' ); /** * Adds the data attributes the tracker reads to its script tag. * * @param string $tag The script tag. * @param string $handle The script handle. * @return string */ function statsdot_script_attributes( $tag, $handle ) { if ( 'statsdot' !== $handle ) { return $tag; } $settings = statsdot_settings(); $attributes = ' data-domain="' . esc_attr( $settings['domain'] ) . '"'; if ( ! $settings['auto'] ) { $attributes .= ' data-auto="false"'; } // Scroll depth is read only with this attribute, and kept only if it is on in the Statsdot site settings. if ( $settings['scroll'] ) { $attributes .= ' data-scroll="true"'; } return str_replace( ' src=', $attributes . ' src=', $tag ); } add_filter( 'script_loader_tag', 'statsdot_script_attributes', 10, 2 ); /** * WooCommerce thank-you page: one "Purchase" event per order, with the order * total and currency unless turned off, and the order number only if turned * on. Nothing about the customer is sent. * * @param int $order_id The order ID. */ function statsdot_purchase( $order_id ) { if ( ! $order_id || ! statsdot_woocommerce_on() ) { return; } $order = wc_get_order( $order_id ); // Once per order, so reloading the thank-you page does not count twice. if ( ! $order || $order->get_meta( '_statsdot_tracked' ) ) { return; } $order->update_meta_data( '_statsdot_tracked', 1 ); $order->save(); $settings = statsdot_settings(); $data = array( 'revenue' => round( (float) $order->get_total(), 2 ), 'currency' => $order->get_currency(), ); // Optional: Statsdot keeps a keyed hash of it for 7 days, to delete a test or wrong order through its API. if ( $settings['order_id'] ) { $data['id'] = (string) $order->get_order_number(); } $revenue = $settings['revenue'] ? wp_json_encode( $data ) : 'null'; // Printed in the footer and run after the deferred tracker has loaded. add_action( 'wp_footer', function () use ( $revenue ) { wp_print_inline_script_tag( "document.addEventListener('DOMContentLoaded',function(){window.statsdot&&window.statsdot.track('Purchase',null," . $revenue . ');});' ); } ); } add_action( 'woocommerce_thankyou', 'statsdot_purchase' ); /** * Declares compatibility with WooCommerce's order storage (HPOS). */ function statsdot_declare_woocommerce_compatibility() { if ( class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' ) ) { \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true ); } } add_action( 'before_woocommerce_init', 'statsdot_declare_woocommerce_compatibility' ); /** * User agent tokens of AI crawlers, search bots and training agents, from the * Statsdot crawler directory (https://statsdot.com/crawlers). Lower case, * longest first. * * @return string[] */ function statsdot_crawler_tokens() { return array( 'cohere-training-data-crawler','sbintuitions-searchbot','google-gemininotebook','google-cloudvertexbot','meta-externalfetcher','meta-externalagent','mistralai-training','applebot-extended','claude-searchbot','yandexadditional','perplexity-user','meta-webindexer','mistralai-index','google-extended','sbintuitionsbot','webzio-extended','mistralai-user','amzn-searchbot','kimi-searchbot','firecrawlagent','duckassistbot','oai-searchbot','perplexitybot','google-agent','chatgpt-user','exasearchbot','imagesiftbot','claude-user','baiduspider','duckduckbot','icc-crawler','googleother','bytespider','terracotta','amzn-user','kimi-user','googlebot','linkupbot','yandexbot','amazonbot','claudebot','applebot','petalbot','pangubot','timpibot','cotoyogi','andibot','diffbot','bingbot','kimibot','youbot','ai2bot','gptbot','omgili','ccbot' ); } /** * AI crawler tracking: crawlers run no JavaScript, so the server reports their * requests to Statsdot with the site's bot key. Only requests whose user agent * is a known crawler are sent: the user agent, the address and the status code. * Nothing is sent for human visitors. */ function statsdot_report_crawler() { $settings = statsdot_settings(); if ( '' === $settings['bot_key'] || is_admin() || wp_doing_cron() || ( defined( 'WP_CLI' ) && WP_CLI ) ) { return; } $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; $haystack = strtolower( $user_agent ); $is_crawler = false; foreach ( statsdot_crawler_tokens() as $token ) { if ( false !== strpos( $haystack, $token ) ) { $is_crawler = true; break; } } if ( ! $is_crawler ) { return; } $path = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '/'; // Answer the crawler first, then report. if ( function_exists( 'fastcgi_finish_request' ) ) { fastcgi_finish_request(); } wp_remote_post( STATSDOT_URL . '/api/bots', array( 'blocking' => false, 'timeout' => 2, 'headers' => array( 'Content-Type' => 'application/json', 'X-Statsdot-Bot-Key' => $settings['bot_key'], ), 'body' => wp_json_encode( array( 'user_agent' => $user_agent, 'url' => home_url( $path ), 'status' => http_response_code(), ) ), ) ); } add_action( 'shutdown', 'statsdot_report_crawler' ); if ( is_admin() ) { require_once __DIR__ . '/admin.php'; }