← back to Domain Landings

ops/tk10631/create_verified_ga4.py

137 lines

#!/usr/bin/env python3
"""TK-10631 (corrected scope, 2026-09-03): create GA4 properties ONLY for the
domains that live verification proved actually need one.

Supersedes create_all_ga4.py, which had TWO defects found by live re-verification:

  1. SCOPE  — it targeted all 150 domains in data/domains.json lacking a `ga4`
     field. Live sweep showed only 94 of those are untagged live landers.
     20 already serve a gtag, 29 are not landers at all (28 serve the shared
     "Trade Lines" DW page), 7 are 401/403/TLS-broken.

  2. DEDUPE — it indexed existing data streams in the APPS account ONLY
     (`if s.account != TARGET_ACCOUNT: continue`). 19 of the 20 already-tagged
     domains have their property in accounts/15714274 (DesignerWallcoverings.com)
     or accounts/136095445, so an APPS-only index would NOT have deduped them
     and the run would have minted ~19 duplicate properties — including
     duplicates for domains that 301 to designerwallcoverings.com and inherit
     the do-not-touch Shopify tag G-53F1QBZSG0.

This version indexes EVERY account the service account can see, and drives from
a verified target file rather than from the absence of a `ga4` key.

GATED: create_property is an external Google-account write. Run only on
explicit approval. Creates properties ONLY; does NOT touch data/domains.json
and does NOT deploy anything (both remain separately gated).

Usage:
  python3 ops/tk10631/create_verified_ga4.py --targets ops/tk10631/verified_targets.txt --dry-run
  python3 ops/tk10631/create_verified_ga4.py --targets ops/tk10631/verified_targets.txt --apply
"""
import argparse, json, sys, time
from urllib.parse import urlparse

from google.oauth2 import service_account
from google.analytics.admin import AnalyticsAdminServiceClient
from google.analytics.admin_v1alpha.types import Property, DataStream

KEY = "/Users/macstudio3/.config/ga-analytics-agent/service-account.json"
# DTD panel (8/8, 2026-09-03) ruled the create target is accounts/15714274, NOT APPS:
# all 12 originally-wired landers AND all 84 backfilled ones live in 15714274, zero in
# APPS (96-0). The APPS targeting was the anomaly and the source of every 403.
TARGET_ACCOUNT = "accounts/15714274"  # DesignerWallcoverings.com — home of this fleet
SCOPES = ["https://www.googleapis.com/auth/analytics.edit",
          "https://www.googleapis.com/auth/analytics.readonly"]
DOMAINS_JSON = "data/domains.json"

ap = argparse.ArgumentParser()
ap.add_argument("--targets", required=True, help="newline-delimited verified target domains")
ap.add_argument("--out", default="/tmp/tk10631_verified_results.json")
ap.add_argument("--account", default="accounts/15714274", help="GA account to create in")
g = ap.add_mutually_exclusive_group(required=True)
g.add_argument("--dry-run", action="store_true", help="index + plan only, no writes")
g.add_argument("--apply", action="store_true", help="perform the gated create")
args = ap.parse_args()
TARGET_ACCOUNT = args.account

cfg = json.load(open(DOMAINS_JSON))
targets = [d.strip() for d in open(args.targets) if d.strip() and not d.startswith("#")]
unknown = [d for d in targets if d not in cfg]
if unknown:
    sys.exit(f"ABORT: {len(unknown)} target(s) not in {DOMAINS_JSON}: {unknown[:5]}")
print(f"{len(targets)} verified targets (of {len(cfg)} domains in {DOMAINS_JSON})")

creds = service_account.Credentials.from_service_account_file(KEY, scopes=SCOPES)
client = AnalyticsAdminServiceClient(credentials=creds)

# --- dedupe index across EVERY visible account (fix #2) ---
print("Indexing existing web streams across ALL visible accounts...")
existing_by_domain, n_props, n_accts = {}, 0, set()
for s in client.list_account_summaries():
    n_accts.add(s.account)
    for p in s.property_summaries:
        n_props += 1
        try:
            for st in client.list_data_streams(parent=p.property):
                w = st.web_stream_data
                if w and w.default_uri and w.measurement_id:
                    host = urlparse(w.default_uri).netloc.lower()
                    host = host[4:] if host.startswith("www.") else host
                    if host:
                        existing_by_domain.setdefault(host, {
                            "account": s.account, "property": p.property,
                            "measurement_id": w.measurement_id})
        except Exception as e:
            print(f"  WARN: streams unreadable for {p.property}: {e}", file=sys.stderr)
print(f"Indexed {n_props} properties across {len(n_accts)} accounts "
      f"-> {len(existing_by_domain)} domain->stream mappings")

todo = [d for d in targets if d not in existing_by_domain]
skip = [d for d in targets if d in existing_by_domain]
if skip:
    print(f"\nSKIP (already have a stream somewhere) — {len(skip)}:")
    for d in skip:
        e = existing_by_domain[d]
        print(f"  {d:34s} {e['measurement_id']:15s} {e['account']}")
print(f"\nWOULD CREATE {len(todo)} propert{'y' if len(todo)==1 else 'ies'} in {TARGET_ACCOUNT}")

if args.dry_run:
    json.dump({"targets": targets, "would_create": todo,
               "skip_existing": {d: existing_by_domain[d] for d in skip}},
              open(args.out, "w"), indent=2)
    print(f"\nDRY RUN — no writes performed. Plan written to {args.out}")
    sys.exit(0)

results = {d: {**existing_by_domain[d], "created": False} for d in skip}
created = 0
for i, domain in enumerate(todo, 1):
    name = cfg[domain].get("title") or domain
    try:
        prop = Property(parent=TARGET_ACCOUNT,
                        display_name=f"{name} ({domain})"[:100],
                        time_zone="America/Los_Angeles", currency_code="USD",
                        industry_category="SHOPPING")
        pn = client.create_property(property=prop).name
        st = client.create_data_stream(parent=pn, data_stream=DataStream(
            type_=DataStream.DataStreamType.WEB_DATA_STREAM,
            display_name=f"{name} Web"[:100],
            web_stream_data=DataStream.WebStreamData(default_uri=f"https://{domain}")))
        mid = st.web_stream_data.measurement_id
        ok = any(s2.web_stream_data and s2.web_stream_data.measurement_id == mid
                 and domain in s2.web_stream_data.default_uri and mid.startswith("G-")
                 for s2 in client.list_data_streams(parent=pn))
        print(f"[{i}/{len(todo)}] CREATED {domain:34s} {pn} mid={mid} readback_ok={ok}")
        results[domain] = {"account": TARGET_ACCOUNT, "property": pn,
                           "measurement_id": mid, "created": True, "readback_ok": ok}
        created += 1
    except Exception as e:
        print(f"[{i}/{len(todo)}] FAILED  {domain:34s} {e}", file=sys.stderr)
        results[domain] = {"error": str(e), "created": False}
    time.sleep(0.6)

json.dump(results, open(args.out, "w"), indent=2)
have = sum(1 for r in results.values() if r.get("measurement_id"))
print(f"\nSUMMARY: {have}/{len(targets)} have a measurement_id ({created} newly created).")
print(f"Results -> {args.out}")
print("NOTE: data/domains.json NOT modified and nothing deployed — both remain gated.")