← back to Ga Allsites

scripts/create_inject_properties.py

141 lines

#!/usr/bin/env python3
"""
Batch-create GA4 properties for the 21 inject targets.
Account: accounts/15714274 (SAND LLC / DesignerWallcoverings.com)
Output: cache/_ga4_inject_ids.json
Idempotent: skips domains already in the output file.
"""
from __future__ import annotations

import json
import os
import sys
import time
from pathlib import Path

SCRIPT_DIR = Path(__file__).parent
SKILLS_DIR = Path.home() / ".claude" / "skills" / "analytics" / "scripts"
sys.path.insert(0, str(SKILLS_DIR))
sys.path.insert(0, str(SCRIPT_DIR))   # so the fail-closed guard can import ga4_absence_detect

from _auth import ensure_credentials

ACCOUNT_ID = "accounts/15714274"   # SAND LLC — SA has Editor here
TARGETS_FILE = SCRIPT_DIR.parent / "cache" / "_ga4_inject_targets.json"
OUTPUT_FILE  = SCRIPT_DIR.parent / "cache" / "_ga4_inject_ids.json"


def load_existing() -> dict:
    if OUTPUT_FILE.exists():
        try:
            return json.loads(OUTPUT_FILE.read_text())
        except Exception:
            return {}
    return {}


def save(data: dict) -> None:
    OUTPUT_FILE.write_text(json.dumps(data, indent=2))


def create_property(client, domain: str) -> str:
    """Create a GA4 property + web data stream; return measurement ID."""
    from google.analytics.admin_v1alpha.types import DataStream, Property

    prop = Property(
        parent=ACCOUNT_ID,
        display_name=domain,
        time_zone="America/Los_Angeles",
        currency_code="USD",
        industry_category="ONLINE_COMMUNITIES",
    )
    created = client.create_property(property=prop)
    prop_name = created.name
    print(f"  property: {prop_name}", flush=True)

    stream = DataStream(
        type_=DataStream.DataStreamType.WEB_DATA_STREAM,
        display_name=domain,
        web_stream_data=DataStream.WebStreamData(
            default_uri=f"https://{domain}",
        ),
    )
    stream = client.create_data_stream(parent=prop_name, data_stream=stream)
    mid = stream.web_stream_data.measurement_id
    print(f"  stream:   {stream.name}  →  {mid}", flush=True)
    return mid


def main() -> None:
    ensure_credentials()

    # Force the correct account ID regardless of on-disk config
    os.environ["GA_ACCOUNT_ID"] = ACCOUNT_ID

    from google.analytics.admin import AnalyticsAdminServiceClient
    client = AnalyticsAdminServiceClient()

    targets: list[str] = json.loads(TARGETS_FILE.read_text())
    existing = load_existing()

    succeeded = 0
    failed = 0
    failed_domains: list[str] = []

    # Fail-closed dup/park guard: re-verify each domain is genuinely injectable at mint time,
    # even if the targets file is stale. Prevents the duplicate-property (already-wired) and
    # orphan-property (parked) mints that happened 2026-08-18. See ga4_absence_detect.py.
    try:
        from ga4_absence_detect import probe as _absence_probe
    except Exception:
        _absence_probe = None

    for domain in targets:
        if domain in existing:
            print(f"[SKIP] {domain} already has {existing[domain]}", flush=True)
            continue

        if _absence_probe is not None:
            _d, bucket, info = _absence_probe(domain)
            if bucket != "NEEDS_INJECT":
                print(f"[GUARD-SKIP] {domain} — {bucket} ({info}); not minting", flush=True)
                continue

        print(f"[CREATE] {domain}", flush=True)
        for attempt in range(3):
            try:
                mid = create_property(client, domain)
                existing[domain] = mid
                save(existing)
                print(f"[OK] {domain} -> {mid}", flush=True)
                succeeded += 1
                time.sleep(1.2)   # polite rate-limit spacing
                break
            except Exception as exc:
                msg = str(exc)
                if "429" in msg or "RESOURCE_EXHAUSTED" in msg:
                    wait = 30 * (attempt + 1)
                    print(f"  rate-limited, waiting {wait}s …", flush=True)
                    time.sleep(wait)
                else:
                    print(f"  [FAIL attempt {attempt+1}] {exc}", flush=True)
                    if attempt == 2:
                        print(f"[FAIL] {domain} — giving up", flush=True)
                        failed += 1
                        failed_domains.append(domain)
                    else:
                        time.sleep(3)

    print()
    print(f"Account used : {ACCOUNT_ID}")
    print(f"Succeeded    : {succeeded}")
    print(f"Skipped      : {len([d for d in targets if d in existing]) - succeeded}")
    print(f"Failed       : {failed}  {failed_domains if failed_domains else ''}")
    print(f"Output file  : {OUTPUT_FILE}")
    print()
    print(json.dumps(existing, indent=2))


if __name__ == "__main__":
    main()