← back to Ga Allsites
tracking-coverage-matrix.py
112 lines
#!/usr/bin/env python3
"""Per-site tracking-coverage matrix — $0 local, no LLM.
Gathers owned domains (Cloudflare + GoDaddy + GA4 slugs), curls each in parallel,
detects GTM / GA4 / Meta Pixel / Pinterest in the HTML, cross-refs GSC grants from
the ga-allsites cache, and prints a matrix + punch-lists for the LIVE sites.
"""
import json
import os
import pathlib
import re
import ssl
import urllib.request
from concurrent.futures import ThreadPoolExecutor
HERE = pathlib.Path(__file__).parent
SEC = pathlib.Path.home() / "Projects" / "secrets-manager" / ".env"
# Probe tuning — tweak here if needed
PROBE_WORKERS = 24 # parallel HTTP workers
PROBE_TIMEOUT_S = 8 # per-site connect+read timeout
MAX_BODY_BYTES = 300_000 # cap HTML read to avoid huge pages
LIVE_BODY_MIN = 1_200 # minimum byte count to consider a page "real"
def env(*keys):
for k in keys:
if os.environ.get(k): return os.environ[k]
if SEC.exists():
for ln in SEC.read_text().splitlines():
ln = ln.strip()
for k in keys:
if ln.startswith(k + "="): return ln.split("=", 1)[1].strip().strip('"').strip("'")
return ""
def get_json(url, hdr):
return json.loads(urllib.request.urlopen(urllib.request.Request(url, headers=hdr), timeout=30).read())
# --- gather owned domains ---
domains = set()
# GA4 slugs
try:
for r in json.loads((HERE / "cache" / "data.json").read_text())["rows"]:
s = "".join(c for c in r["property"].lower().replace(" - ga4", "").replace("&", "and") if c.isalnum())
if s: domains.add(s + ".com")
except Exception as e: print("ga4 slugs err:", e)
# Cloudflare
cft = env("CLOUDFLARE_API_TOKEN", "CF_API_TOKEN", "CF_TOKEN"); p = 1
while cft:
r = get_json(f"https://api.cloudflare.com/client/v4/zones?per_page=50&page={p}", {"Authorization": "Bearer " + cft})
domains |= {z["name"] for z in r.get("result", [])}
if p >= r.get("result_info", {}).get("total_pages", 1): break
p += 1
# GoDaddy
gk, gs = env("GODADDY_API_KEY", "GODADDY_KEY"), env("GODADDY_API_SECRET", "GODADDY_SECRET")
if gk and gs:
try:
domains |= {d["domain"] for d in get_json("https://api.godaddy.com/v1/domains?limit=1000&statuses=ACTIVE", {"Authorization": f"sso-key {gk}:{gs}"})}
except Exception as e: print("godaddy err:", e)
# GSC granted sc-domains
gsc = set()
try:
kw = json.loads((HERE / "cache" / "keywords.json").read_text())
gsc = {s["url"].replace("sc-domain:", "") for s in kw.get("gsc_sites", []) if s["url"].startswith("sc-domain:")}
except Exception: pass
ctx = ssl.create_default_context(); ctx.check_hostname = False; ctx.verify_mode = ssl.CERT_NONE
def probe(d):
row = {"domain": d, "http": 0, "live": False, "gtm": False, "ga4": False, "meta": False, "pin": False,
"gsc": d in gsc}
try:
req = urllib.request.Request(f"https://{d}/", headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req, timeout=PROBE_TIMEOUT_S, context=ctx) as r:
row["http"] = r.status
final = r.geturl()
body = r.read(MAX_BODY_BYTES).decode("utf-8", "ignore")
# live = 200 + real body + not a cross-host redirect
host = re.sub(r"^https?://(www\.)?", "", final).split("/")[0]
row["live"] = row["http"] == 200 and len(body) > LIVE_BODY_MIN and d.replace("www.", "") in host
row["gtm"] = bool(re.search(r"GTM-[A-Z0-9]{6,8}", body))
row["ga4"] = bool(re.search(r"G-[A-Z0-9]{10}\b", body)) or "gtag(" in body
row["meta"] = "fbq(" in body or "connect.facebook.net" in body
row["pin"] = "pintrk(" in body or "s.pinimg.com/ct" in body
except Exception as e:
row["http"] = getattr(e, "code", 0)
return row
doms = sorted(domains)
print(f"Probing {len(doms)} owned domains (parallel, $0 local)...\n")
with ThreadPoolExecutor(max_workers=PROBE_WORKERS) as ex:
rows = list(ex.map(probe, doms))
live = [r for r in rows if r["live"]]
def yn(b): return "✅" if b else "—"
print(f"# Tracking coverage — {len(live)} LIVE of {len(rows)} owned domains\n")
print("| # | Domain | GSC | GA4 | GTM | Meta | Pinterest | missing |")
print("|---|--------|-----|-----|-----|------|-----------|---------|")
for i, r in enumerate(sorted(live, key=lambda x: (x["ga4"], x["meta"]), reverse=False), 1):
miss = [k.upper() for k in ("ga4", "gtm", "meta", "pin") if not r[k]]
print(f"| {i} | {r['domain']} | {yn(r['gsc'])} | {yn(r['ga4'])} | {yn(r['gtm'])} | {yn(r['meta'])} | {yn(r['pin'])} | {','.join(miss) or 'none'} |")
def pct(k): return f"{sum(1 for r in live if r[k])}/{len(live)}"
print(f"\n## Coverage (of {len(live)} live)")
print(f"- GSC: {pct('gsc')} · GA4: {pct('ga4')} · GTM: {pct('gtm')} · Meta: {pct('meta')} · Pinterest: {pct('pin')}")
zero = [r["domain"] for r in live if not (r["ga4"] or r["gtm"] or r["meta"] or r["pin"])]
print(f"\n## {len(zero)} LIVE sites with ZERO tracking (top priority):")
print(" " + ", ".join(zero))
# persist
out = HERE / "gtm" / "coverage-matrix.json"
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(rows, indent=2))
print(f"\n(raw → {out})")