← 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.")