← back to Domain Landings

ops/tk10631/create_all_ga4.py

103 lines

#!/usr/bin/env python3
"""TK-10631 (expanded scope, 2026-09-03): create GA4 properties for ALL
domain-landings domains lacking GA4 — not just the original 12, now all
150 remaining of the 162 total (Steve: "any website we have live even
domain for sale sites"). GATED: Google-account write — run only with
approval (Steve approved this run verbatim 2026-09-03).

Idempotent by DOMAIN (not display_name — two titles collide: "Cypres
Awards", "Petition Your"), matched against every existing web data
stream's default_uri host in the APPS account, so a re-run never
double-creates. Read-back verifies each domain->measurement_id mapping.

This script ONLY creates GA4 properties. It does NOT touch
data/domains.json and does NOT deploy anything live — that remains a
separate gated customer-facing-deploy step per standing rule.
"""
import json, time, sys
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"
APPS_ACCOUNT = "accounts/15714274"  # DesignerWallcoverings.com — CORRECTED 2026-09-04:
# every already-live sibling property (commercialwallcovering.com, hollywoodwallcovering.com,
# designerwallcoverings.ai) actually lives HERE, not under accounts/371997142 "Steve Abrams
# APPS" which every prior script/attempt in this ticket had wrongly hardcoded. gate1_result.json
# in ~/Projects/ga-allsites (2026-08-21) already recorded that even a real browser login had no
# access to 371997142, and had to fall back to this account — the SA's repeated 403s on
# create_property were almost certainly this same wrong-account issue, not a bad Editor grant.
SCOPES = ["https://www.googleapis.com/auth/analytics.edit",
          "https://www.googleapis.com/auth/analytics.readonly"]
DOMAINS_JSON = "data/domains.json"
OUT = "/tmp/tk10631_results_full.json"

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

cfg = json.load(open(DOMAINS_JSON))
targets = {d: v for d, v in cfg.items() if not v.get("ga4")}
print(f"{len(targets)} domains lack ga4 out of {len(cfg)} total")

# Build existing domain -> (property, measurement_id) map from EVERY
# property the SA can see in ANY account (not just APPS_ACCOUNT) — corrected
# 2026-09-04 after finding the reference properties live under a DIFFERENT
# account than the one this script creates into. Keyed by data-stream
# default_uri host (NOT display_name — display names are not guaranteed unique).
print("Indexing existing properties/streams across ALL visible accounts...")
existing_by_domain = {}
n_props = 0
for s in client.list_account_summaries():
    for p in s.property_summaries:
        n_props += 1
        try:
            for st in client.list_data_streams(parent=p.property):
                if st.web_stream_data and st.web_stream_data.default_uri:
                    host = urlparse(st.web_stream_data.default_uri).netloc.lower()
                    host = host[4:] if host.startswith("www.") else host
                    mid = st.web_stream_data.measurement_id
                    if host and mid:
                        existing_by_domain[host] = {"property": p.property, "measurement_id": mid}
        except Exception as e:
            print(f"  WARN: couldn't list streams for {p.property}: {e}", file=sys.stderr)
print(f"Indexed {n_props} properties, {len(existing_by_domain)} domain->stream mappings")

results = {}
created_count = 0
for i, (domain, v) in enumerate(sorted(targets.items()), 1):
    name = v.get("title") or domain
    if domain in existing_by_domain:
        r = existing_by_domain[domain]
        print(f"[{i}/{len(targets)}] EXISTS  {domain:32} {r['property']} mid={r['measurement_id']}")
        results[domain] = {**r, "created": False}
        continue
    try:
        display_name = f"{name} ({domain})"[:100]
        prop = Property(parent=APPS_ACCOUNT, display_name=display_name,
                         time_zone="America/Los_Angeles", currency_code="USD",
                         industry_category="SHOPPING")
        created = client.create_property(property=prop)
        prop_name = created.name
        stream = DataStream(type_=DataStream.DataStreamType.WEB_DATA_STREAM,
                             display_name=f"{name} Web"[:100],
                             web_stream_data=DataStream.WebStreamData(default_uri=f"https://{domain}"))
        st = client.create_data_stream(parent=prop_name, data_stream=stream)
        mid = st.web_stream_data.measurement_id
        ok = False
        for s2 in client.list_data_streams(parent=prop_name):
            if s2.web_stream_data and s2.web_stream_data.measurement_id == mid:
                ok = (domain in s2.web_stream_data.default_uri) and mid.startswith("G-")
        print(f"[{i}/{len(targets)}] CREATED {domain:32} {prop_name} mid={mid} readback_ok={ok}")
        results[domain] = {"property": prop_name, "measurement_id": mid, "created": True, "readback_ok": ok}
        created_count += 1
        time.sleep(0.6)
    except Exception as e:
        print(f"[{i}/{len(targets)}] FAILED  {domain:32} {e}", file=sys.stderr)
        results[domain] = {"error": str(e), "created": False}
        time.sleep(0.6)

json.dump(results, open(OUT, "w"), indent=2)
ok_count = sum(1 for r in results.values() if r.get("measurement_id"))
print(f"\nSUMMARY: {ok_count} of {len(targets)} have a measurement_id ({created_count} newly created). Written to {OUT}")