Plugin: add Google Analytics + Search Console healthcheck steps (hc-u5c)
Slot two new steps between performance (Step 7) and security (Step 8): - 72-analytics.php: sniffs the homepage for GA4 (G-), GTM, and legacy UA measurement IDs plus known loader URLs (gtag.js, gtm.js, analytics.js, ga.js) and detects common analytics/tag plugins. Warns if only UA is still in use. - 74-search-console.php: looks for google-site-verification meta tags on the homepage, probes for a reachable sitemap (wp-sitemap.xml, then sitemap_index.xml, then sitemap.xml), parses robots.txt for a Googlebot/* Disallow: /, flags the WP "Discourage search engines" setting when on, and notes whether Site Kit is active. Titles use the "Step —" (unnumbered) convention already used by the email and handover steps so the existing numbered steps don't shift. steps.md updated to match. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
108
includes/steps/72-analytics.php
Normal file
108
includes/steps/72-analytics.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
return new class extends ATT_HC_Step {
|
||||
public function id(): string { return 'analytics'; }
|
||||
public function title(): string { return 'Step — Google Analytics Check'; }
|
||||
public function blurb(): string {
|
||||
return 'Confirm the site is still reporting into Google Analytics. Autocheck sniffs the homepage HTML for a tracking snippet and known GA plugins — the human check is that events are actually landing in the GA property.';
|
||||
}
|
||||
public function sub_items(): array {
|
||||
return [
|
||||
'Confirm a tracking snippet is present on the homepage (view source, look for gtag/GTM/analytics.js)',
|
||||
'Log into Google Analytics and confirm real-time users are being recorded when you load the site',
|
||||
'Confirm the measurement ID on the site matches the client record (G-XXXX for GA4, GTM-XXXX for Tag Manager)',
|
||||
'Check that today has page-view events in the property — flag if traffic has dropped to zero',
|
||||
'Flag if only Universal Analytics (UA-XXXX) is still in use — GA4 has been required since July 2023',
|
||||
];
|
||||
}
|
||||
public function watch_outs(): array {
|
||||
return [
|
||||
'A snippet on the page does not prove data is arriving — cookie banners that block until consent will suppress hits until accepted',
|
||||
'Some caching/optimisation plugins defer or strip inline scripts — the snippet may only appear once cache is warm',
|
||||
];
|
||||
}
|
||||
|
||||
public function autocheck(array $session_state): array {
|
||||
$f = [];
|
||||
$home = home_url('/');
|
||||
|
||||
$resp = wp_remote_get($home, ['timeout' => 8, 'redirection' => 3]);
|
||||
if (is_wp_error($resp)) {
|
||||
$f[] = $this->finding('fetch', 'warn', 'Homepage fetch', 'failed', $resp->get_error_message());
|
||||
return $f;
|
||||
}
|
||||
$body = (string) wp_remote_retrieve_body($resp);
|
||||
|
||||
// Measurement / container IDs anywhere in the HTML.
|
||||
$found_ids = [];
|
||||
if (preg_match_all('/\bG-[A-Z0-9]{6,}\b/', $body, $m)) $found_ids = array_merge($found_ids, $m[0]);
|
||||
if (preg_match_all('/\bGTM-[A-Z0-9]{4,}\b/', $body, $m)) $found_ids = array_merge($found_ids, $m[0]);
|
||||
if (preg_match_all('/\bUA-\d{4,}-\d+\b/', $body, $m)) $found_ids = array_merge($found_ids, $m[0]);
|
||||
$found_ids = array_values(array_unique($found_ids));
|
||||
|
||||
// Snippet vendors (any script/URL referencing a known loader).
|
||||
$signals = [
|
||||
'gtag.js' => 'googletagmanager.com/gtag/js',
|
||||
'gtm.js' => 'googletagmanager.com/gtm.js',
|
||||
'analytics.js (UA)' => 'google-analytics.com/analytics.js',
|
||||
'ga.js (legacy UA)' => 'google-analytics.com/ga.js',
|
||||
];
|
||||
$vendors = [];
|
||||
foreach ($signals as $label => $needle) {
|
||||
if (stripos($body, $needle) !== false) $vendors[] = $label;
|
||||
}
|
||||
|
||||
if ($found_ids || $vendors) {
|
||||
$has_ga4 = (bool) preg_grep('/^G-/', $found_ids);
|
||||
$has_ua = (bool) preg_grep('/^UA-/', $found_ids);
|
||||
$level = ($has_ua && !$has_ga4) ? 'warn' : 'ok';
|
||||
$detail = $vendors ? 'loader: ' . implode(', ', $vendors) : '';
|
||||
if ($has_ua && !$has_ga4) {
|
||||
$detail = trim($detail . ' — Universal Analytics only; GA4 required since July 2023.');
|
||||
}
|
||||
$f[] = $this->finding(
|
||||
'snippet',
|
||||
$level,
|
||||
'Tracking snippet',
|
||||
$found_ids ? implode(', ', array_slice($found_ids, 0, 4)) : 'present',
|
||||
$detail
|
||||
);
|
||||
} else {
|
||||
$f[] = $this->finding(
|
||||
'snippet',
|
||||
'warn',
|
||||
'Tracking snippet',
|
||||
'not found',
|
||||
'No GA/GTM/UA measurement ID or loader was seen in the homepage HTML. A consent banner may be blocking the script, or tracking may have been removed.'
|
||||
);
|
||||
}
|
||||
|
||||
// GA/GTM plugin detection — informational only.
|
||||
if (!function_exists('is_plugin_active')) require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||
$plugins = [
|
||||
'google-site-kit/google-site-kit.php' => 'Site Kit by Google',
|
||||
'google-analytics-for-wordpress/googleanalytics.php' => 'MonsterInsights',
|
||||
'google-analytics-premium/googleanalytics-premium.php' => 'MonsterInsights Pro',
|
||||
'google-analytics-dashboard-for-wp/gadwp.php' => 'ExactMetrics',
|
||||
'ga-google-analytics/ga-google-analytics.php' => 'GA Google Analytics',
|
||||
'analytify/wp-analytify.php' => 'Analytify',
|
||||
'duracelltomi-google-tag-manager/duracelltomi-google-tag-manager.php' => 'GTM4WP',
|
||||
'header-footer-code-manager/header-footer-code-manager.php' => 'Header Footer Code Manager',
|
||||
'insert-headers-and-footers/ihaf.php' => 'WPCode / Insert Headers and Footers',
|
||||
];
|
||||
$active = [];
|
||||
foreach ($plugins as $file => $label) {
|
||||
if (is_plugin_active($file)) $active[] = $label;
|
||||
}
|
||||
$f[] = $this->finding(
|
||||
'plugin',
|
||||
$active ? 'ok' : 'info',
|
||||
'Analytics/tag plugin',
|
||||
$active ? implode(', ', $active) : 'none detected',
|
||||
$active ? '' : 'Tracking may be hard-coded in the theme or injected by a page builder — check view-source if the snippet check passed.'
|
||||
);
|
||||
|
||||
return $f;
|
||||
}
|
||||
};
|
||||
166
includes/steps/74-search-console.php
Normal file
166
includes/steps/74-search-console.php
Normal file
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
return new class extends ATT_HC_Step {
|
||||
public function id(): string { return 'search_console'; }
|
||||
public function title(): string { return 'Step — Google Search Console Check'; }
|
||||
public function blurb(): string {
|
||||
return 'Confirm the site is still verified in Google Search Console and that Google is able to crawl and index it. Autocheck looks for verification meta tags, a reachable sitemap, and a robots.txt that isn\'t blocking Googlebot — the human check is in the GSC UI itself.';
|
||||
}
|
||||
public function sub_items(): array {
|
||||
return [
|
||||
'Log into Google Search Console and confirm the property is still verified',
|
||||
'Check the Pages / Coverage report for new indexing errors since the last healthcheck',
|
||||
'Confirm the sitemap is submitted and its "Last read" date is recent',
|
||||
'Skim the Performance report — flag significant drops in impressions or clicks (>25% vs. previous period)',
|
||||
'Check Manual Actions and Security Issues — flag anything that is not "No issues detected"',
|
||||
'Confirm the verified property matches the canonical URL (http vs https, www vs non-www) actually serving',
|
||||
];
|
||||
}
|
||||
public function watch_outs(): array {
|
||||
return [
|
||||
'Verification meta tags can be added by SEO plugins (Yoast, Rank Math, AIOSEO) or by Site Kit — the presence of a tag does not tell you which Google account owns the property',
|
||||
'A missing tag does not mean the site is unverified — DNS TXT and file-based verification are equally valid and not visible from the front end',
|
||||
];
|
||||
}
|
||||
|
||||
public function autocheck(array $session_state): array {
|
||||
$f = [];
|
||||
$home = home_url('/');
|
||||
|
||||
// 1. Homepage HTML — look for verification meta tag(s).
|
||||
$resp = wp_remote_get($home, ['timeout' => 8, 'redirection' => 3]);
|
||||
if (is_wp_error($resp)) {
|
||||
$f[] = $this->finding('fetch', 'warn', 'Homepage fetch', 'failed', $resp->get_error_message());
|
||||
} else {
|
||||
$body = (string) wp_remote_retrieve_body($resp);
|
||||
$tokens = [];
|
||||
if (preg_match_all('/<meta[^>]+name=["\']google-site-verification["\'][^>]*content=["\']([^"\']+)["\']/i', $body, $m)) {
|
||||
$tokens = $m[1];
|
||||
}
|
||||
$f[] = $this->finding(
|
||||
'verification_meta',
|
||||
$tokens ? 'ok' : 'info',
|
||||
'GSC verification meta tag',
|
||||
$tokens ? count($tokens) . ' present' : 'not found on homepage',
|
||||
$tokens
|
||||
? 'Token(s): ' . implode(', ', array_map(fn($t) => substr($t, 0, 12) . '…', $tokens))
|
||||
: 'Absent tag is not a problem if verification is via DNS TXT or an uploaded HTML file — confirm in Search Console.'
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Sitemap reachability. Try common locations, take the first that responds 200.
|
||||
$candidates = [
|
||||
'wp-sitemap.xml', // WP core (5.5+)
|
||||
'sitemap_index.xml', // Yoast/Rank Math default
|
||||
'sitemap.xml',
|
||||
];
|
||||
$found_sitemap = null;
|
||||
foreach ($candidates as $rel) {
|
||||
$url = trailingslashit($home) . $rel;
|
||||
$head = wp_remote_head($url, ['timeout' => 5, 'redirection' => 2]);
|
||||
if (is_wp_error($head)) continue;
|
||||
$code = (int) wp_remote_retrieve_response_code($head);
|
||||
if ($code === 200) {
|
||||
$found_sitemap = $url;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($found_sitemap) {
|
||||
$f[] = $this->finding(
|
||||
'sitemap',
|
||||
'ok',
|
||||
'Sitemap',
|
||||
$found_sitemap,
|
||||
'Reachable — confirm this URL is the one submitted in Search Console.'
|
||||
);
|
||||
} else {
|
||||
$f[] = $this->finding(
|
||||
'sitemap',
|
||||
'warn',
|
||||
'Sitemap',
|
||||
'none of the common URLs responded',
|
||||
'Tried: ' . implode(', ', $candidates) . '. A missing sitemap doesn\'t prevent indexing but Search Console will show a fetch error.'
|
||||
);
|
||||
}
|
||||
|
||||
// 3. robots.txt — surface if it blocks Googlebot from the site root.
|
||||
$robots_url = trailingslashit($home) . 'robots.txt';
|
||||
$rob = wp_remote_get($robots_url, ['timeout' => 5, 'redirection' => 2]);
|
||||
if (is_wp_error($rob)) {
|
||||
$f[] = $this->finding('robots', 'info', 'robots.txt', 'unreachable', $rob->get_error_message());
|
||||
} else {
|
||||
$code = (int) wp_remote_retrieve_response_code($rob);
|
||||
if ($code !== 200) {
|
||||
$f[] = $this->finding('robots', 'info', 'robots.txt', 'HTTP ' . $code, 'WordPress serves a virtual robots.txt by default; a non-200 response may indicate a redirect or a plugin intercepting it.');
|
||||
} else {
|
||||
$rb = (string) wp_remote_retrieve_body($rob);
|
||||
$blocks_root = $this->robots_blocks_root($rb);
|
||||
$f[] = $this->finding(
|
||||
'robots',
|
||||
$blocks_root ? 'bad' : 'ok',
|
||||
'robots.txt',
|
||||
$blocks_root ? 'blocks Googlebot from /' : 'does not block /',
|
||||
$blocks_root
|
||||
? 'A "Disallow: /" applying to * or Googlebot will prevent indexing. Check Settings → Reading for "Discourage search engines".'
|
||||
: ''
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. "Discourage search engines" WP setting — hard block on indexing.
|
||||
if ((int) get_option('blog_public') === 0) {
|
||||
$f[] = $this->finding(
|
||||
'blog_public',
|
||||
'bad',
|
||||
'Search engine indexing',
|
||||
'discouraged (Settings → Reading)',
|
||||
'The "Discourage search engines from indexing this site" checkbox is on. This should be off on production.'
|
||||
);
|
||||
} else {
|
||||
$f[] = $this->finding('blog_public', 'ok', 'Search engine indexing', 'allowed', '');
|
||||
}
|
||||
|
||||
// 5. Site Kit — the WP-side surface for Search Console data. Informational.
|
||||
if (!function_exists('is_plugin_active')) require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||
$sitekit = is_plugin_active('google-site-kit/google-site-kit.php');
|
||||
$f[] = $this->finding(
|
||||
'site_kit',
|
||||
$sitekit ? 'ok' : 'info',
|
||||
'Site Kit by Google',
|
||||
$sitekit ? 'active' : 'not active',
|
||||
$sitekit ? 'GSC data may be visible on the WP dashboard.' : ''
|
||||
);
|
||||
|
||||
return $f;
|
||||
}
|
||||
|
||||
/** Parse robots.txt and decide whether Googlebot (or *) is disallowed from /. */
|
||||
private function robots_blocks_root(string $robots): bool {
|
||||
$lines = preg_split('/\r?\n/', $robots) ?: [];
|
||||
$current_agents = [];
|
||||
$groups = []; // agent => [disallow rules]
|
||||
foreach ($lines as $line) {
|
||||
$line = trim(preg_replace('/#.*$/', '', $line));
|
||||
if ($line === '') { $current_agents = []; continue; }
|
||||
if (preg_match('/^user-agent:\s*(.+)$/i', $line, $m)) {
|
||||
$current_agents[] = strtolower(trim($m[1]));
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/^disallow:\s*(.*)$/i', $line, $m)) {
|
||||
$rule = trim($m[1]);
|
||||
foreach ($current_agents as $ua) {
|
||||
$groups[$ua][] = $rule;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (['googlebot', '*'] as $ua) {
|
||||
if (!empty($groups[$ua])) {
|
||||
foreach ($groups[$ua] as $rule) {
|
||||
if ($rule === '/') return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user