*/ private $ranges = array(); /** * Page hook suffixes for asset scoping. * * @var string[] */ private $hooks = array(); public function __construct() { $this->ranges = array( '24h' => __( 'Last 24h', 'aiso-ai-crawler-analytics' ), '7d' => __( 'Last 7 days', 'aiso-ai-crawler-analytics' ), '30d' => __( 'Last 30 days', 'aiso-ai-crawler-analytics' ), '90d' => __( 'Last 90 days', 'aiso-ai-crawler-analytics' ), 'all' => __( 'All time', 'aiso-ai-crawler-analytics' ), ); add_action( 'admin_menu', array( $this, 'menu' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'assets' ) ); add_filter( 'plugin_action_links_' . plugin_basename( AISO_FILE ), array( $this, 'action_links' ) ); } /** * Register the top-level menu + sub-pages. */ public function menu() { $this->hooks['dashboard'] = add_menu_page( __( 'AI Crawler Analytics', 'aiso-ai-crawler-analytics' ), __( 'AI Crawlers', 'aiso-ai-crawler-analytics' ), 'manage_options', 'aiso-dashboard', array( $this, 'render_dashboard' ), 'dashicons-chart-area', 58 ); add_submenu_page( 'aiso-dashboard', __( 'AI Crawler Analytics', 'aiso-ai-crawler-analytics' ), __( 'Dashboard', 'aiso-ai-crawler-analytics' ), 'manage_options', 'aiso-dashboard', array( $this, 'render_dashboard' ) ); $this->hooks['settings'] = add_submenu_page( 'aiso-dashboard', __( 'Aiso Settings', 'aiso-ai-crawler-analytics' ), __( 'Settings', 'aiso-ai-crawler-analytics' ), 'manage_options', 'aiso-settings', array( $this, 'render_settings' ) ); } /** * "Settings" shortcut on the Plugins screen. * * @param string[] $links Existing links. * @return string[] */ public function action_links( $links ) { $url = admin_url( 'admin.php?page=aiso-settings' ); array_unshift( $links, '' . esc_html__( 'Settings', 'aiso-ai-crawler-analytics' ) . '' ); return $links; } /** * Enqueue our scoped styles on plugin screens only. * * @param string $hook Current admin page hook. */ public function assets( $hook ) { if ( ! in_array( $hook, $this->hooks, true ) ) { return; } wp_enqueue_style( 'aiso-admin', AISO_URL . 'admin/css/aiso-admin.css', array(), AISO_VERSION ); } /** * Current range from the query string, validated against the whitelist. * * @return string */ private function current_range() { $range = isset( $_GET['range'] ) ? sanitize_key( wp_unslash( $_GET['range'] ) ) : '7d'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only navigation. return isset( $this->ranges[ $range ] ) ? $range : '7d'; } // --------------------------------------------------------------------- // Dashboard // --------------------------------------------------------------------- /** * Render the analytics dashboard. */ public function render_dashboard() { if ( ! current_user_can( 'manage_options' ) ) { return; } $range = $this->current_range(); $totals = Aiso_Store::totals( $range ); $connected = Aiso_Analytics::instance()->sync->is_connected(); echo '
'; $this->header( $range, $connected ); if ( 0 === $totals['total'] ) { $this->empty_state( $connected ); echo '
'; return; } $this->kpi_cards( $totals, $range ); echo '
'; $this->section_platforms( $range ); $this->section_purpose( $range ); echo '
'; $this->section_timeline( $range ); echo '
'; $this->section_crawlers( $range ); $this->section_pages( $range ); echo '
'; $this->section_recent( $range ); echo ''; // .wrap } /** * Page header: title, range tabs, status pill, export. * * @param string $range Current range. * @param bool $connected Whether syncing is active. */ private function header( $range, $connected ) { echo '
'; echo '
'; echo '

' . esc_html__( 'AI Crawler Analytics', 'aiso-ai-crawler-analytics' ) . '

'; echo '

' . esc_html__( 'Impressions for the AI era — see exactly which AI assistants are reading your site, what they read, and when.', 'aiso-ai-crawler-analytics' ) . '

'; echo '
'; echo '
'; if ( $connected ) { echo '● ' . esc_html__( 'Syncing to Aiso', 'aiso-ai-crawler-analytics' ) . ''; } else { echo '○ ' . esc_html__( 'Local only — connect Aiso', 'aiso-ai-crawler-analytics' ) . ''; } echo '
'; echo '
'; // Range tabs. echo '
'; echo '
'; foreach ( $this->ranges as $key => $label ) { $url = add_query_arg( array( 'page' => 'aiso-dashboard', 'range' => $key, ), admin_url( 'admin.php' ) ); $cls = ( $key === $range ) ? 'aiso-tab aiso-tab-active' : 'aiso-tab'; echo '' . esc_html( $label ) . ''; } echo '
'; // Export. $export_url = wp_nonce_url( add_query_arg( array( 'action' => 'aiso_export', 'range' => $range, ), admin_url( 'admin-post.php' ) ), 'aiso_export' ); echo '' . esc_html__( 'Export CSV', 'aiso-ai-crawler-analytics' ) . ''; echo '
'; } /** * Empty-state guidance when no hits have been recorded yet. * * @param bool $connected Whether syncing is active. */ private function empty_state( $connected ) { echo '
'; echo '

' . esc_html__( 'No AI crawler visits recorded yet', 'aiso-ai-crawler-analytics' ) . '

'; echo '

' . esc_html__( 'As soon as ChatGPT, Claude, Perplexity, Google or another AI crawler requests a page on this site, it will appear here. AI traffic is usually a small slice of total traffic, so it can take a little while.', 'aiso-ai-crawler-analytics' ) . '

'; echo '

' . esc_html__( 'Want to test it now? From a terminal, run:', 'aiso-ai-crawler-analytics' ) . '

'; echo '
curl -A "Mozilla/5.0 (compatible; GPTBot/1.1; +https://openai.com/gptbot)" ' . esc_html( home_url( '/' ) ) . "
"; echo '

' . esc_html__( 'Reload this page after a few seconds and the hit will show up.', 'aiso-ai-crawler-analytics' ) . '

'; if ( ! $connected ) { echo '

' . esc_html__( 'Connect your Aiso account', 'aiso-ai-crawler-analytics' ) . '

'; } echo '
'; } /** * Headline KPI cards. * * @param array $t Totals. * @param string $range Range key. */ private function kpi_cards( $t, $range ) { $cards = array( array( 'label' => __( 'AI crawler hits', 'aiso-ai-crawler-analytics' ), 'value' => $t['total'], 'hint' => $this->ranges[ $range ], ), array( 'label' => __( 'Chat impressions', 'aiso-ai-crawler-analytics' ), 'value' => $t['impressions'], 'hint' => __( 'Live fetches during AI answers', 'aiso-ai-crawler-analytics' ), 'accent' => true, ), array( 'label' => __( 'Distinct crawlers', 'aiso-ai-crawler-analytics' ), 'value' => $t['bots'], 'hint' => __( 'Different AI user-agents', 'aiso-ai-crawler-analytics' ), ), array( 'label' => __( 'Pages crawled', 'aiso-ai-crawler-analytics' ), 'value' => $t['pages'], 'hint' => __( 'Unique URLs touched', 'aiso-ai-crawler-analytics' ), ), ); echo '
'; foreach ( $cards as $c ) { $cls = isset( $c['accent'] ) ? 'aiso-kpi aiso-kpi-accent' : 'aiso-kpi'; echo '
'; echo '
' . esc_html( $c['label'] ) . '
'; echo '
' . esc_html( number_format_i18n( $c['value'] ) ) . '
'; echo '
' . esc_html( $c['hint'] ) . '
'; echo '
'; } echo '
'; } /** * Render a single labeled horizontal bar. * * @param string $label Left label. * @param int $count Value. * @param int $max Max value for scaling. * @param int $total Grand total for percent. * @param string $color Bar color (hex). */ private function bar( $label, $count, $max, $total, $color ) { $pct_width = $max > 0 ? round( ( $count / $max ) * 100 ) : 0; $pct_share = $total > 0 ? round( ( $count / $total ) * 100 ) : 0; echo '
'; echo '
' . esc_html( $label ) . '
'; echo '
'; echo '
'; echo '
'; echo '
' . esc_html( number_format_i18n( $count ) ) . ' (' . esc_html( $pct_share ) . '%)
'; echo '
'; } /** * Breakdown by AI platform / provider. * * @param string $range Range key. */ private function section_platforms( $range ) { $rows = Aiso_Store::group_by( 'platform', $range, 12 ); $platforms = Aiso_Bots::platforms(); $total = array_sum( wp_list_pluck( $rows, 'count' ) ); $max = $rows ? max( wp_list_pluck( $rows, 'count' ) ) : 0; echo '
'; echo '

' . esc_html__( 'By AI platform', 'aiso-ai-crawler-analytics' ) . '

'; echo '

' . esc_html__( 'Which assistants are reading your content.', 'aiso-ai-crawler-analytics' ) . '

'; foreach ( $rows as $r ) { $key = $r['label']; $label = isset( $platforms[ $key ]['label'] ) ? $platforms[ $key ]['label'] : ucfirst( $key ); $color = isset( $platforms[ $key ]['color'] ) ? $platforms[ $key ]['color'] : '#6d5ae8'; $this->bar( $label, (int) $r['count'], $max, $total, $color ); } echo '
'; } /** * Breakdown by purpose (function tag). * * @param string $range Range key. */ private function section_purpose( $range ) { $rows = Aiso_Store::group_by( 'function_tag', $range, 8 ); $fns = Aiso_Bots::functions(); $total = array_sum( wp_list_pluck( $rows, 'count' ) ); $max = $rows ? max( wp_list_pluck( $rows, 'count' ) ) : 0; echo '
'; echo '

' . esc_html__( 'By purpose', 'aiso-ai-crawler-analytics' ) . '

'; echo '

' . esc_html__( 'Training vs. citation vs. live chat impressions.', 'aiso-ai-crawler-analytics' ) . '

'; foreach ( $rows as $r ) { $key = $r['label']; $label = isset( $fns[ $key ]['label'] ) ? $fns[ $key ]['label'] : ucfirst( str_replace( '-', ' ', $key ) ); $color = isset( $fns[ $key ]['color'] ) ? $fns[ $key ]['color'] : '#6d5ae8'; $this->bar( $label, (int) $r['count'], $max, $total, $color ); } echo '
'; } /** * Breakdown by individual crawler ("user type" / user agent). * * @param string $range Range key. */ private function section_crawlers( $range ) { $rows = Aiso_Store::group_by( 'bot_slug', $range, 14 ); $platforms = Aiso_Bots::platforms(); // slug -> [name, platform] map from the catalog. $meta = array(); foreach ( Aiso_Bots::catalog() as $bot ) { $meta[ $bot['slug'] ] = array( 'name' => $bot['name'], 'platform' => $bot['platform'], ); } $total = array_sum( wp_list_pluck( $rows, 'count' ) ); $max = $rows ? max( wp_list_pluck( $rows, 'count' ) ) : 0; echo '
'; echo '

' . esc_html__( 'By crawler (user-agent)', 'aiso-ai-crawler-analytics' ) . '

'; echo '

' . esc_html__( 'The exact bots hitting your server.', 'aiso-ai-crawler-analytics' ) . '

'; foreach ( $rows as $r ) { $slug = $r['label']; $name = isset( $meta[ $slug ]['name'] ) ? $meta[ $slug ]['name'] : ( 'other' === $slug ? __( 'Other / Unknown', 'aiso-ai-crawler-analytics' ) : $slug ); $pkey = isset( $meta[ $slug ]['platform'] ) ? $meta[ $slug ]['platform'] : 'other'; $color = isset( $platforms[ $pkey ]['color'] ) ? $platforms[ $pkey ]['color'] : '#6d5ae8'; $this->bar( $name, (int) $r['count'], $max, $total, $color ); } echo '
'; } /** * Per-page breakdown. * * @param string $range Range key. */ private function section_pages( $range ) { $rows = Aiso_Store::top_pages( $range, 15 ); $max = $rows ? max( wp_list_pluck( $rows, 'hits' ) ) : 0; echo '
'; echo '

' . esc_html__( 'Top pages', 'aiso-ai-crawler-analytics' ) . '

'; echo '

' . esc_html__( 'The URLs AI crawlers request most.', 'aiso-ai-crawler-analytics' ) . '

'; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; foreach ( $rows as $r ) { $pct = $max > 0 ? round( ( (int) $r['hits'] / $max ) * 100 ) : 0; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } echo '
' . esc_html__( 'Page', 'aiso-ai-crawler-analytics' ) . '' . esc_html__( 'Hits', 'aiso-ai-crawler-analytics' ) . '' . esc_html__( 'Impr.', 'aiso-ai-crawler-analytics' ) . '' . esc_html__( 'Bots', 'aiso-ai-crawler-analytics' ) . '
' . esc_html( $r['path'] ) . '' . esc_html( number_format_i18n( (int) $r['hits'] ) ) . '' . esc_html( number_format_i18n( (int) $r['impressions'] ) ) . '' . esc_html( number_format_i18n( (int) $r['bots'] ) ) . '
'; echo '
'; } /** * Activity over time (daily bars; impressions highlighted). * * @param string $range Range key. */ private function section_timeline( $range ) { $rows = Aiso_Store::timeseries( $range ); if ( empty( $rows ) ) { return; } $max = max( 1, max( wp_list_pluck( $rows, 'total' ) ) ); echo '
'; echo '

' . esc_html__( 'Activity over time', 'aiso-ai-crawler-analytics' ) . '

'; echo '

' . esc_html__( 'Daily AI crawler hits. Green marks live chat impressions.', 'aiso-ai-crawler-analytics' ) . '

'; echo '
'; foreach ( $rows as $r ) { $h = round( ( (int) $r['total'] / $max ) * 100 ); $imp_h = (int) $r['total'] > 0 ? round( ( (int) $r['impressions'] / (int) $r['total'] ) * 100 ) : 0; $title = sprintf( /* translators: 1: date, 2: total hits, 3: impressions. */ __( '%1$s — %2$s hits, %3$s impressions', 'aiso-ai-crawler-analytics' ), $r['day'], number_format_i18n( (int) $r['total'] ), number_format_i18n( (int) $r['impressions'] ) ); echo '
'; echo '
'; echo '
'; echo '
'; echo '
' . esc_html( gmdate( 'j M', strtotime( $r['day'] . ' UTC' ) ) ) . '
'; echo '
'; } echo '
'; echo '
'; } /** * Recent crawler hits log. * * @param string $range Range key. */ private function section_recent( $range ) { $rows = Aiso_Store::recent( $range, 30 ); $platforms = Aiso_Bots::platforms(); echo '
'; echo '

' . esc_html__( 'Recent crawler hits', 'aiso-ai-crawler-analytics' ) . '

'; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; $fns = Aiso_Bots::functions(); foreach ( $rows as $r ) { $pkey = isset( $r['platform'] ) ? $r['platform'] : 'other'; $color = isset( $platforms[ $pkey ]['color'] ) ? $platforms[ $pkey ]['color'] : '#6d5ae8'; $fnkey = isset( $r['function_tag'] ) ? $r['function_tag'] : ''; $fnlbl = isset( $fns[ $fnkey ]['label'] ) ? $fns[ $fnkey ]['label'] : $fnkey; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; if ( null === $r['verified'] ) { echo ''; } elseif ( (int) $r['verified'] === 1 ) { echo ''; } else { echo ''; } echo ''; } echo '
' . esc_html__( 'Time (UTC)', 'aiso-ai-crawler-analytics' ) . '' . esc_html__( 'Crawler', 'aiso-ai-crawler-analytics' ) . '' . esc_html__( 'Purpose', 'aiso-ai-crawler-analytics' ) . '' . esc_html__( 'Page', 'aiso-ai-crawler-analytics' ) . '' . esc_html__( 'Country', 'aiso-ai-crawler-analytics' ) . '' . esc_html__( 'Verified', 'aiso-ai-crawler-analytics' ) . '
' . esc_html( $r['hit_time'] ) . '' . esc_html( $r['bot_name'] ) . '' . esc_html( $fnlbl ) . '' . esc_html( $r['path'] ) . '' . esc_html( $r['country'] ) . '' . esc_html__( '—', 'aiso-ai-crawler-analytics' ) . '' . esc_html__( 'verified', 'aiso-ai-crawler-analytics' ) . '' . esc_html__( 'spoofed?', 'aiso-ai-crawler-analytics' ) . '
'; echo '

' . esc_html__( 'Verification compares the source IP against each provider’s published ranges. “—” means the provider publishes no list (verify by reverse-DNS).', 'aiso-ai-crawler-analytics' ) . '

'; echo '
'; } // --------------------------------------------------------------------- // Settings // --------------------------------------------------------------------- /** * Render the settings screen. */ public function render_settings() { if ( ! current_user_can( 'manage_options' ) ) { return; } $o = Aiso_Analytics::options(); $connected = Aiso_Analytics::instance()->sync->is_connected(); $pending = Aiso_Store::unsynced_count(); $last_sync = (int) get_option( 'aiso_last_sync', 0 ); echo '
'; echo '

' . esc_html__( 'Aiso AI Crawler Analytics — Settings', 'aiso-ai-crawler-analytics' ) . '

'; // Connection card. echo '
'; echo '

' . esc_html__( 'Connect your Aiso account', 'aiso-ai-crawler-analytics' ) . '

'; echo '

' . esc_html__( 'Connecting sends AI-crawler hits to your Aiso dashboard so data is kept long-term and viewable across all your sites. Until you connect, the plugin works fully on its own and stores everything locally — nothing leaves your server.', 'aiso-ai-crawler-analytics' ) . '

'; if ( $connected ) { echo '

' . esc_html__( 'Status:', 'aiso-ai-crawler-analytics' ) . ' ' . esc_html__( 'Connected & syncing', 'aiso-ai-crawler-analytics' ) . '

'; } else { echo '

' . esc_html__( 'Status:', 'aiso-ai-crawler-analytics' ) . ' ' . esc_html__( 'Local only', 'aiso-ai-crawler-analytics' ) . '

'; // Generate-key form. echo '
'; echo ''; wp_nonce_field( 'aiso_connect' ); echo ''; echo '
'; echo ''; echo ''; echo '
'; echo '

' . esc_html__( 'We use this to create a key for', 'aiso-ai-crawler-analytics' ) . ' ' . esc_html( wp_parse_url( home_url(), PHP_URL_HOST ) ) . '. ' . esc_html__( 'Already have a key? Paste it below.', 'aiso-ai-crawler-analytics' ) . '

'; echo '
'; } echo '

'; printf( /* translators: %s: number of pending hits. */ esc_html( _n( '%s hit waiting to sync.', '%s hits waiting to sync.', $pending, 'aiso-ai-crawler-analytics' ) ), esc_html( number_format_i18n( $pending ) ) ); if ( $last_sync ) { echo ' ' . esc_html( sprintf( /* translators: %s: human-readable time difference. */ __( 'Last sync %s ago.', 'aiso-ai-crawler-analytics' ), human_time_diff( $last_sync, time() ) ) ); } echo '

'; // Test + Sync buttons. echo '
'; echo '
'; echo ''; wp_nonce_field( 'aiso_test' ); echo ''; echo '
'; echo '
'; echo ''; wp_nonce_field( 'aiso_sync_now' ); echo ''; echo '
'; echo '
'; echo '
'; // connection card // Options form. echo '
'; settings_fields( Aiso_Settings::GROUP ); echo '

' . esc_html__( 'Options', 'aiso-ai-crawler-analytics' ) . '

'; echo ''; // API key. $this->field_row( __( 'Aiso API key', 'aiso-ai-crawler-analytics' ), '' . '

' . esc_html__( 'Paste an existing key, or use “Connect” above to generate one.', 'aiso-ai-crawler-analytics' ) . '

' ); // Sync enabled. $this->field_row( __( 'Send data to Aiso', 'aiso-ai-crawler-analytics' ), '' ); // Track all. $this->field_row( __( 'Coverage', 'aiso-ai-crawler-analytics' ), '' ); // Anonymize IP. $this->field_row( __( 'Privacy', 'aiso-ai-crawler-analytics' ), '' ); // Retention. $this->field_row( __( 'Local retention (days)', 'aiso-ai-crawler-analytics' ), ' ' . '' . esc_html__( '0 = keep forever. Synced rows older than this are removed locally; the full history stays in your Aiso account.', 'aiso-ai-crawler-analytics' ) . '' ); // Advanced endpoint. $this->field_row( __( 'API base URL', 'aiso-ai-crawler-analytics' ), ' ' . '

' . esc_html__( 'Advanced — leave blank to use the default Aiso server.', 'aiso-ai-crawler-analytics' ) . '

' ); // Remove data on uninstall. $this->field_row( __( 'On uninstall', 'aiso-ai-crawler-analytics' ), '' ); echo ''; submit_button( __( 'Save settings', 'aiso-ai-crawler-analytics' ) ); echo '
'; // Privacy / data disclosure. echo '
'; echo '

' . esc_html__( 'What gets sent to Aiso', 'aiso-ai-crawler-analytics' ) . '

'; echo '

' . esc_html__( 'Only when “Send data to Aiso” is enabled, and only for AI-crawler hits, this plugin transmits: the requested path, the crawler’s user-agent, the detected bot/company, an (optionally hashed) IP, country code, HTTP status, timestamp, your site URL, and your API key. No human-visitor analytics or personal content is sent.', 'aiso-ai-crawler-analytics' ) . '

'; echo '

' . esc_html__( 'Aiso privacy policy', 'aiso-ai-crawler-analytics' ) . ' · ' . esc_html__( 'About Aiso', 'aiso-ai-crawler-analytics' ) . '

'; echo '
'; echo '
'; // .wrap } /** * One form-table row. * * @param string $label Field label. * @param string $html Pre-escaped field HTML. */ private function field_row( $label, $html ) { echo '' . esc_html( $label ) . ''; // $html is assembled from esc_* helpers above. echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- composed from escaped parts. echo ''; } }