0.6 ? '#1f2430' : '#ffffff'; } /** Hue (degrees), saturation and lightness (0 to 1) of a colour */ function eacfa_to_hsl($hex) { list($r, $g, $b) = eacfa_hex_to_rgb($hex); $r /= 255; $g /= 255; $b /= 255; $max = max($r, $g, $b); $min = min($r, $g, $b); $l = ($max + $min) / 2; if ($max === $min) { return array(0.0, 0.0, $l); } $d = $max - $min; $s = $l > 0.5 ? $d / (2 - $max - $min) : $d / ($max + $min); if ($max === $r) { $h = ($g - $b) / $d + ($g < $b ? 6 : 0); } elseif ($max === $g) { $h = ($b - $r) / $d + 2; } else { $h = ($r - $g) / $d + 4; } return array($h * 60, $s, $l); } function eacfa_hue_to_channel($p, $q, $t) { if ($t < 0) { $t += 1; } if ($t > 1) { $t -= 1; } if ($t < 1 / 6) { return $p + ($q - $p) * 6 * $t; } if ($t < 1 / 2) { return $q; } if ($t < 2 / 3) { return $p + ($q - $p) * (2 / 3 - $t) * 6; } return $p; } /** Hex colour from hue (degrees), saturation and lightness */ function eacfa_from_hsl($hue, $sat, $light) { $h = fmod(fmod($hue, 360) + 360, 360) / 360; $s = max(0, min(1, $sat)); $l = max(0, min(1, $light)); if ($s == 0) { $r = $l; $g = $l; $b = $l; } else { $q = $l < 0.5 ? $l * (1 + $s) : $l + $s - $l * $s; $p = 2 * $l - $q; $r = eacfa_hue_to_channel($p, $q, $h + 1 / 3); $g = eacfa_hue_to_channel($p, $q, $h); $b = eacfa_hue_to_channel($p, $q, $h - 1 / 3); } return eacfa_rgb_to_hex(array($r * 255, $g * 255, $b * 255)); } /** * Every colour the plugin draws with, derived from the primary colour. Cached for the request. */ function eacfa_palette() { static $palette = null; if ($palette !== null) { return $palette; } $primary = eacfa_primary_color(); list($hue, $sat, $light) = eacfa_to_hsl($primary); $dark_accent = eacfa_mix($primary, '#ffffff', 0.25); // Lines and bars must stay visible on both light and dark backgrounds $line_light = min(max($light, 0.48), 0.62); $palette = array( 'light' => array( 'accent' => $primary, 'accent_hover' => eacfa_mix($primary, '#000000', 0.12), 'accent_soft' => eacfa_rgba($primary, 0.1), 'accent_ink' => eacfa_on_color($primary), ), 'dark' => array( 'accent' => $dark_accent, 'accent_hover' => eacfa_mix($dark_accent, '#ffffff', 0.12), 'accent_soft' => eacfa_rgba($primary, 0.24), 'accent_ink' => eacfa_on_color($dark_accent), ), 'series_visits' => eacfa_from_hsl($hue, $sat, $line_light), // Page views: a hue-shifted sibling. For a grey primary, lightness tells the two lines apart instead. 'series_views' => $sat < 0.12 ? eacfa_from_hsl($hue, 0, min($line_light + 0.25, 0.8)) : eacfa_from_hsl($hue + 60, $sat * 0.8, $line_light), 'subparts' => array( eacfa_from_hsl($hue, $sat, 0.5), eacfa_from_hsl($hue, $sat, 0.68), eacfa_from_hsl($hue, $sat, 0.34), eacfa_from_hsl($hue, $sat, 0.82), ), ); // Lines on the dark background: a lighter tint of the same colour. $palette['series_visits_dark'] = eacfa_mix($palette['series_visits'], '#ffffff', 0.25); $palette['series_views_dark'] = eacfa_mix($palette['series_views'], '#ffffff', 0.25); return $palette; } /** * CSS custom properties for the accent, added after admin.css so they take effect. * Only the accent group lives here; neutral and status tokens are in admin.css. */ function eacfa_palette_css() { $p = eacfa_palette(); $vars = function ($t, $series) { return '--cfa-accent:' . $t['accent'] . ';--cfa-accent-hover:' . $t['accent_hover'] . ';--cfa-accent-soft:' . $t['accent_soft'] . ';--cfa-accent-ink:' . $t['accent_ink'] . ';--cfa-chart-1:' . $series[0] . ';--cfa-chart-2:' . $series[1] . ';'; }; $light = array($p['series_visits'], $p['series_views']); $dark = array($p['series_visits_dark'], $p['series_views_dark']); return '.cfa-wrap,#cf_analytics_widget{' . $vars($p['light'], $light) . '}' . 'body.cfa-theme-dark .cfa-wrap,body.cfa-theme-dark #cf_analytics_widget{' . $vars($p['dark'], $dark) . '}' . '@media (prefers-color-scheme: dark){' . 'body.cfa-theme-auto .cfa-wrap,body.cfa-theme-auto #cf_analytics_widget{' . $vars($p['dark'], $dark) . '}}'; } add_filter('admin_body_class', function ($classes) { $screen = function_exists('get_current_screen') ? get_current_screen() : null; if ($screen && ($screen->id === 'dashboard' || strpos($screen->id, 'cf-analytics') !== false)) { $classes .= ' cfa-theme-' . eacfa_current_theme(); if ($screen->id !== 'dashboard') { $classes .= ' cfa-page'; } } return $classes; }); /* ---------------------------------------------------------------------- * Menu: one link under Dashboard, between Home and Updates * -------------------------------------------------------------------- */ add_action('admin_menu', function () { add_submenu_page( 'index.php', 'Web Analytics', 'Web Analytics', 'manage_options', 'cf-analytics', 'eacfa_analytics_page', 1 ); }); /* ---------------------------------------------------------------------- * Assets + request handlers * -------------------------------------------------------------------- */ add_action('admin_enqueue_scripts', function ($hook) { $is_plugin_page = strpos($hook, 'cf-analytics') !== false; if (!$is_plugin_page && $hook !== 'index.php') { return; } wp_enqueue_style('eacfa-admin', EACFA_PLUGIN_URL . 'assets/css/admin.css', array(), eacfa_asset_version('assets/css/admin.css')); wp_add_inline_style('eacfa-admin', eacfa_palette_css()); wp_enqueue_script('eacfa-chart', EACFA_PLUGIN_URL . 'assets/js/cfa-chart.js', array(), eacfa_asset_version('assets/js/cfa-chart.js'), true); wp_enqueue_script('eacfa-live', EACFA_PLUGIN_URL . 'assets/js/cfa-live.js', array(), eacfa_asset_version('assets/js/cfa-live.js'), true); wp_enqueue_script('eacfa-dashboard', EACFA_PLUGIN_URL . 'assets/js/cfa-dashboard.js', array('eacfa-chart', 'eacfa-live'), eacfa_asset_version('assets/js/cfa-dashboard.js'), true); if ($is_plugin_page && eacfa_param('section') === 'settings') { wp_enqueue_style('wp-components'); wp_enqueue_script('eacfa-color', EACFA_PLUGIN_URL . 'assets/js/cfa-color.js', array('wp-element', 'wp-components', 'wp-i18n'), eacfa_asset_version('assets/js/cfa-color.js'), true); wp_enqueue_script('eacfa-settings', EACFA_PLUGIN_URL . 'assets/js/cfa-settings.js', array('jquery'), eacfa_asset_version('assets/js/cfa-settings.js'), true); } }); add_action('admin_init', function () { if (strpos(eacfa_param('page'), 'cf-analytics') !== 0 || !current_user_can('manage_options')) { return; } // Refresh data if (eacfa_has_param('eacfa_refresh')) { check_admin_referer('eacfa_refresh'); EACFA_Live::queue_manual(); wp_safe_redirect(remove_query_arg(array('eacfa_refresh', '_wpnonce'))); exit; } // Quick theme switch if (eacfa_has_param('eacfa_theme')) { check_admin_referer('eacfa_theme'); $saved = eacfa_set_theme(eacfa_param('eacfa_theme')); $url = remove_query_arg(array('eacfa_theme', '_wpnonce')); wp_safe_redirect($saved ? $url : add_query_arg('eacfa_notice', 'save_failed', $url)); exit; } }); /* ---------------------------------------------------------------------- * Helpers * -------------------------------------------------------------------- */ function eacfa_sections() { return array( 'overview' => array('Overview', 'dashicons-chart-area'), 'pages' => array('Pages', 'dashicons-admin-page'), 'referrers' => array('Referrers', 'dashicons-share'), 'countries' => array('Countries', 'dashicons-admin-site-alt3'), 'technology' => array('Technology', 'dashicons-desktop'), 'vitals' => array('Core Web Vitals', 'dashicons-performance') ); } function eacfa_url($args = array()) { return EACFA_Live::dashboard_url($args); } function eacfa_num($n) { return number_format_i18n((float) $n); } function eacfa_total($rows, $key = 'views') { $sum = 0; foreach ($rows as $row) { $sum += $row[$key]; } return $sum; } function eacfa_share($value, $total) { return $total > 0 ? ($value / $total) * 100 : 0; } function eacfa_error($res, $inline = false) { // Nothing cached yet: not an error. The card shows a skeleton until the background refresh fills it. if (!empty($res['miss'])) { if ($inline) { // Inside a list card: a few shimmering lines fit the card shell. echo '
'; } else { // A whole section with nothing cached yet: show the card layout as skeletons. eacfa_skeleton(); } return; } $msg = isset($res['message']) ? $res['message'] : 'Something went wrong.'; $class = $inline ? 'cfa-empty is-error' : 'cfa-notice is-error'; echo '
Couldn’t load this data. ' . esc_html($msg) . ' Check settings
'; } function eacfa_country_label($code) { return ($code === '') ? 'Unknown' : EACFA_API::get_country_name($code); } /** Label callbacks shared by the list cards and tables */ function eacfa_plain_label($row) { return '' . esc_html($row['key'] === '' ? 'Unknown' : $row['key']) . ''; } function eacfa_page_label($row) { if (!empty($row['title'])) { return '' . esc_html($row['title']) . '' . esc_html($row['key']) . ''; } return '' . esc_html($row['key'] === '' ? '/' : $row['key']) . ''; } function eacfa_referrer_label($row) { return '' . esc_html($row['key'] === '' ? 'Direct / none' : $row['key']) . ''; } function eacfa_country_row_label($row) { return '' . esc_html(eacfa_country_label($row['key'])) . ''; } /** * Ranked list with share bars */ function eacfa_list_card($title, $res, $link, $label_cb, $limit = 6) { ?>

View all
No data for this period yet.
cell class. $rows: arrays of already-escaped cell HTML. * Used by Pages, Referrers, Countries and the Core Web Vitals "By page" table. */ function eacfa_table($cols, $rows, $title = '', $shares = array()) { $classes = array_values($cols); ?>

No data for this period yet.
$class): ?> $cells): ?> style="--w: %"> $html): ?>
$r) { $share = eacfa_share($r['views'], $total); $shares[] = round($share, 1); $rows[] = array( (int) ($i + 1), wp_kses_post(call_user_func($label_cb, $r)), esc_html(EACFA_Live::pct_text($share)), esc_html(eacfa_num($r['views'])), esc_html(eacfa_num($r['visits'])), ); } eacfa_table(array('#' => 'is-rank', $first_col => 'is-main', 'Share' => 'is-num is-pct', 'Page views' => 'is-num', 'Visits' => 'is-num'), $rows, '', $shares); } /** * Placeholder shown while a section loads in the background */ function eacfa_skeleton() { ?>
timestamp). */ function eacfa_render_section($section, $range) { switch ($section) { case 'pages': return eacfa_render_pages($range); case 'referrers': return eacfa_render_referrers($range); case 'countries': return eacfa_render_countries($range); case 'technology': return eacfa_render_technology($range); case 'vitals': return eacfa_render_vitals($range); default: return eacfa_render_overview($range); } } function eacfa_updated_text($meta) { return !empty($meta['fetched_at']) ? 'updated ' . human_time_diff($meta['fetched_at'], time()) . ' ago' : ''; } /** * AJAX: render a section with live fetching (used when the page had to defer it) */ add_action('wp_ajax_cf_analytics_section', function () { check_ajax_referer('cf_analytics_section', 'nonce'); if (!current_user_can('manage_options') || !EACFA_API::is_configured()) { wp_send_json_error('Unauthorized'); } $sections = eacfa_sections(); $section = isset($_POST['section']) ? sanitize_key(wp_unslash($_POST['section'])) : 'overview'; if (!isset($sections[$section])) { $section = 'overview'; } $range = EACFA_Live::sanitize_range(isset($_POST['range']) ? sanitize_key(wp_unslash($_POST['range'])) : '', '7days'); ignore_user_abort(true); ob_start(); $meta = eacfa_render_section($section, $range); $html = ob_get_clean(); // Anything that had to be served from stale cache is refreshed by the browser right after. $jobs = EACFA_Live::queued_jobs(); EACFA_Live::clear_queue(); wp_send_json_success(array( 'html' => $html, 'updated' => eacfa_updated_text($meta), 'jobs' => $jobs, 'refresh_nonce' => wp_create_nonce('cf_analytics_refresh') )); }); /* ---------------------------------------------------------------------- * Page * -------------------------------------------------------------------- */ function eacfa_analytics_page() { if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } $configured = EACFA_API::is_configured(); $settings = get_option('cf_analytics_settings', array()); $sections = eacfa_sections(); $section = eacfa_param('section', 'overview'); if ($section !== 'settings' && !isset($sections[$section])) { $section = 'overview'; } // Until Cloudflare is connected there is nothing to show but the setup screen. if (!$configured) { $section = 'settings'; } $is_settings = ($section === 'settings'); $range = EACFA_Live::sanitize_range(eacfa_param('range'), '7days'); $ranges = EACFA_Live::ranges(); $deferred = false; $updated = ''; $theme = eacfa_current_theme(); $args = array('section' => $section, 'range' => $range); $to_dark_url = wp_nonce_url(eacfa_url(array_merge($args, array('eacfa_theme' => 'dark'))), 'eacfa_theme'); $to_light_url = wp_nonce_url(eacfa_url(array_merge($args, array('eacfa_theme' => 'light'))), 'eacfa_theme'); $theme_url = ($theme === 'dark') ? $to_light_url : $to_dark_url; $theme_title = ($theme === 'dark') ? 'Switch to light mode' : (($theme === 'light') ? 'Switch to dark mode' : 'Switch between light and dark mode'); $refresh_url = wp_nonce_url(eacfa_url(array_merge($args, array('eacfa_refresh' => 1))), 'eacfa_refresh'); $settings_url = eacfa_url(array('section' => 'settings', 'range' => $range)); // Shared cache: apply a pending manual refresh, and queue anything stale for the page-load refresh. EACFA_Live::apply_pending(); if (!$is_settings) { EACFA_Live::queue_page_set($range); } $title = $is_settings ? 'Settings' : $sections[$section][0]; ?>

Cloudflare Web Analytics

Cloudflare Web Analytics · version ·

$label): ?>
Couldn’t save that setting. Please try again, or check your connection in Settings.

Cloudflare Web Analytics, read from Cloudflare and refreshed in the background . Cloudflare samples this data, so small differences from its dashboard are normal. Times use your WordPress timezone.

($section === 'overview'), 'colors' => array( 'visits' => eacfa_palette()['series_visits'], 'views' => eacfa_palette()['series_views'], ), 'updated' => ($updated !== '') ? '· ' . $updated : '', 'deferred' => (bool) $deferred, 'section' => $section, 'range' => $range, 'sectionNonce' => wp_create_nonce('cf_analytics_section'), 'ajaxUrl' => admin_url('admin-ajax.php'), )); } /* ---------------------------------------------------------------------- * Overview * -------------------------------------------------------------------- */ function eacfa_render_overview($range) { $series = EACFA_Live::series($range); $meta = array('fetched_at' => isset($series['fetched_at']) ? $series['fetched_at'] : 0); if (empty($series['success'])) { if (!empty($series['miss'])) { eacfa_skeleton(); // nothing cached yet: show the card layout as skeletons } else { eacfa_error($series); } return $meta; } $t = $series['totals']; $p = $series['prev']; $vitals = EACFA_Live::vitals($range); $load_change = ($p && $t['load_ms'] !== null && $p['load_ms'] !== null) ? EACFA_Live::change($t['load_ms'], $p['load_ms']) : null; $chart = array( 'labels' => array_values(wp_list_pluck($series['points'], 'label')), 'visits' => array_values(wp_list_pluck($series['points'], 'visits')), 'views' => array_values(wp_list_pluck($series['points'], 'views')) ); ?>
Visits
Page views
Page load time
Core Web Vitals (P75)
'LCP', 'inp' => 'INP', 'cls' => 'CLS') as $m => $label): $v = $vitals['metrics'][$m]['p75']; $rate = EACFA_Live::rating($m, $v); ?>
Not available yet

Traffic

'pages', 'range' => $range)), 'eacfa_page_label'); eacfa_list_card('Referrers', EACFA_Live::breakdown($range, 'refererHost', 20), eacfa_url(array('section' => 'referrers', 'range' => $range)), 'eacfa_referrer_label'); eacfa_list_card('Countries', EACFA_Live::breakdown($range, 'countryName', 30), eacfa_url(array('section' => 'countries', 'range' => $range)), 'eacfa_country_row_label'); eacfa_list_card('Browsers', EACFA_Live::breakdown($range, 'userAgentBrowser', 20), eacfa_url(array('section' => 'technology', 'range' => $range)), 'eacfa_plain_label'); ?>
isset($res['fetched_at']) ? $res['fetched_at'] : 0); } function eacfa_render_referrers($range) { $res = EACFA_Live::breakdown($range, 'refererHost', 50); eacfa_table_card($res, 'Referrer', 'eacfa_referrer_label'); return array('fetched_at' => isset($res['fetched_at']) ? $res['fetched_at'] : 0); } function eacfa_render_countries($range) { $res = EACFA_Live::breakdown($range, 'countryName', 100); eacfa_table_card($res, 'Country', function ($row) { return eacfa_country_row_label($row) . ($row['key'] !== '' ? '' . esc_html($row['key']) . '' : ''); }); return array('fetched_at' => isset($res['fetched_at']) ? $res['fetched_at'] : 0); } function eacfa_render_technology($range) { $browsers = EACFA_Live::breakdown($range, 'userAgentBrowser', 20); echo '
'; eacfa_list_card('Browsers', $browsers, '', 'eacfa_plain_label', 12); eacfa_list_card('Operating systems', EACFA_Live::breakdown($range, 'userAgentOS', 20), '', 'eacfa_plain_label', 12); echo '
'; eacfa_list_card('Devices', EACFA_Live::breakdown($range, 'deviceType', 10), '', function ($row) { return '' . esc_html($row['key'] === '' ? 'Unknown' : ucfirst($row['key'])) . ''; }, 10); return array('fetched_at' => isset($browsers['fetched_at']) ? $browsers['fetched_at'] : 0); } /* ---------------------------------------------------------------------- * Core Web Vitals * -------------------------------------------------------------------- */ function eacfa_render_vitals($range) { $v = EACFA_Live::vitals($range); $meta = array('fetched_at' => isset($v['fetched_at']) ? $v['fetched_at'] : 0); if (empty($v['success'])) { eacfa_error($v); return $meta; } $info = array( 'lcp' => array('Largest Contentful Paint', 'LCP', 'Render time of the largest image or text block visible in the viewport.'), 'inp' => array('Interaction to Next Paint', 'INP', 'How quickly the page responds to clicks, taps and key presses.'), 'cls' => array('Cumulative Layout Shift', 'CLS', 'The largest burst of unexpected layout shifts during the life of a page.') ); $percentiles = array('p50' => 'P50', 'p75' => 'P75', 'p90' => 'P90', 'p99' => 'P99'); $subpart_colors = eacfa_palette()['subparts']; foreach ($info as $metric => $text): $m = $v['metrics'][$metric]; $overall = EACFA_Live::rating($metric, $m['p75']); ?>

()

at P75

'Good', 'needs' => 'Needs improvement', 'poor' => 'Poor') as $k => $label): ?>
$label): $rate = EACFA_Live::rating($metric, $m[$key]); ?>
Sub-parts (P75)
$ms): ?> ms

By page (P75)

Per-page vitals aren’t available right now.
' . esc_html($r['key'] === '' ? '/' : $r['key']) . '', esc_html(eacfa_num($r['count']))); foreach (array('lcp', 'inp', 'cls') as $metric) { $rate = EACFA_Live::rating($metric, $r[$metric]); $cells[] = ' ' . esc_html(EACFA_Live::vital_text($metric, $r[$metric])); } $rows[] = $cells; } eacfa_table(array('Page' => 'is-main', 'Samples' => 'is-num', 'LCP' => 'is-num', 'INP' => 'is-num', 'CLS' => 'is-num'), $rows, 'By page (P75)'); } return $meta; }