Track reusable CF analytics utilities and the commercial prospects document while keeping local probe/sample artifacts untracked. Co-authored-by: Cursor <cursoragent@cursor.com>
218 lines
6.8 KiB
Python
218 lines
6.8 KiB
Python
"""Pull traffic for dota2.refining.dev via Cloudflare GraphQL Analytics API.
|
|
|
|
Credentials from env (keyzoo inject). Read-only; prints a short summary.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import urllib.error
|
|
import urllib.request
|
|
from datetime import date, timedelta
|
|
|
|
HOST = "dota2.refining.dev"
|
|
ZONE_NAME = "refining.dev"
|
|
DAYS = 14
|
|
|
|
|
|
def headers() -> dict[str, str]:
|
|
email = os.environ.get("CLOUDFLARE_EMAIL") or os.environ.get(
|
|
"KEYZOO_ASSET_META_USERNAME"
|
|
)
|
|
key = os.environ.get("CLOUDFLARE_API_KEY") or os.environ.get(
|
|
"KEYZOO_ASSET_SECRET_GLOBAL_API_KEY"
|
|
)
|
|
if not email or not key:
|
|
raise SystemExit("missing credentials in env")
|
|
return {
|
|
"X-Auth-Email": email,
|
|
"X-Auth-Key": key,
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
|
|
def api(path: str) -> dict:
|
|
req = urllib.request.Request(
|
|
"https://api.cloudflare.com/client/v4" + path,
|
|
method="GET",
|
|
headers=headers(),
|
|
)
|
|
with urllib.request.urlopen(req) as r:
|
|
return json.loads(r.read().decode())
|
|
|
|
|
|
def gql(query: str, variables: dict) -> dict:
|
|
req = urllib.request.Request(
|
|
"https://api.cloudflare.com/client/v4/graphql",
|
|
data=json.dumps({"query": query, "variables": variables}).encode(),
|
|
method="POST",
|
|
headers=headers(),
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(req) as r:
|
|
return json.loads(r.read().decode())
|
|
except urllib.error.HTTPError as e:
|
|
body = e.read().decode()
|
|
raise SystemExit(f"HTTP {e.code}: {body[:2000]}") from e
|
|
|
|
|
|
def fmt_bytes(n: int | float | None) -> str:
|
|
if n is None:
|
|
return "-"
|
|
n = float(n)
|
|
for unit in ("B", "KB", "MB", "GB", "TB"):
|
|
if abs(n) < 1024:
|
|
return f"{n:.1f} {unit}"
|
|
n /= 1024
|
|
return f"{n:.1f} PB"
|
|
|
|
|
|
def main() -> None:
|
|
zd = api(f"/zones?name={ZONE_NAME}")
|
|
zone = (zd.get("result") or [None])[0]
|
|
if not zone:
|
|
raise SystemExit(f"zone not found: {zd.get('errors')}")
|
|
zid = zone["id"]
|
|
|
|
end = date.today()
|
|
start = end - timedelta(days=DAYS)
|
|
|
|
# Free plan: httpRequests1dGroups works for multi-day zone totals.
|
|
# Adaptive (host/path filter) is limited to a 1-day window.
|
|
zone_q = """
|
|
query($zoneTag: string!, $start: Date!, $end: Date!) {
|
|
viewer {
|
|
zones(filter: {zoneTag: $zoneTag}) {
|
|
httpRequests1dGroups(
|
|
limit: 20
|
|
filter: {date_geq: $start, date_leq: $end}
|
|
orderBy: [date_ASC]
|
|
) {
|
|
dimensions { date }
|
|
sum { requests bytes cachedRequests threats }
|
|
uniq { uniques }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
data = gql(
|
|
zone_q,
|
|
{"zoneTag": zid, "start": start.isoformat(), "end": end.isoformat()},
|
|
)
|
|
if data.get("errors"):
|
|
raise SystemExit(json.dumps(data["errors"], indent=2)[:4000])
|
|
rows = data["data"]["viewer"]["zones"][0]["httpRequests1dGroups"]
|
|
print(f"\n{ZONE_NAME} zone (all hosts) {start} .. {end}")
|
|
print(f"{'date':<12} {'requests':>10} {'uniques':>10} {'bytes':>12} {'threats':>8}")
|
|
tot_r = tot_u = tot_b = tot_t = 0
|
|
for row in rows:
|
|
d = row["dimensions"]["date"]
|
|
s = row["sum"]
|
|
u = row["uniq"]["uniques"]
|
|
print(
|
|
f"{d:<12} {s['requests']:>10} {u:>10} "
|
|
f"{fmt_bytes(s['bytes']):>12} {s['threats']:>8}"
|
|
)
|
|
tot_r += s["requests"]
|
|
tot_u += u
|
|
tot_b += s["bytes"]
|
|
tot_t += s["threats"]
|
|
print(
|
|
f"{'TOTAL':<12} {tot_r:>10} {tot_u:>10} "
|
|
f"{fmt_bytes(tot_b):>12} {tot_t:>8}"
|
|
)
|
|
|
|
# Host-scoped detail: one day at a time (Free quota).
|
|
host_q = """
|
|
query($zoneTag: string!, $day: Date!, $host: string!) {
|
|
viewer {
|
|
zones(filter: {zoneTag: $zoneTag}) {
|
|
total: httpRequestsAdaptiveGroups(
|
|
limit: 1
|
|
filter: {date: $day, clientRequestHTTPHost: $host}
|
|
) {
|
|
count
|
|
sum { edgeResponseBytes visits }
|
|
}
|
|
byPath: httpRequestsAdaptiveGroups(
|
|
limit: 15
|
|
filter: {date: $day, clientRequestHTTPHost: $host}
|
|
orderBy: [count_DESC]
|
|
) {
|
|
dimensions { clientRequestPath }
|
|
count
|
|
sum { visits }
|
|
}
|
|
byCountry: httpRequestsAdaptiveGroups(
|
|
limit: 8
|
|
filter: {date: $day, clientRequestHTTPHost: $host}
|
|
orderBy: [count_DESC]
|
|
) {
|
|
dimensions { clientCountryName }
|
|
count
|
|
sum { visits }
|
|
}
|
|
byStatus: httpRequestsAdaptiveGroups(
|
|
limit: 8
|
|
filter: {date: $day, clientRequestHTTPHost: $host}
|
|
orderBy: [count_DESC]
|
|
) {
|
|
dimensions { edgeResponseStatus }
|
|
count
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
print(f"\n{HOST} host detail (last {min(DAYS, 7)} days, Free=1d/query)")
|
|
print(f"{'date':<12} {'requests':>10} {'visits':>10} {'bytes':>12}")
|
|
host_days = []
|
|
for i in range(min(DAYS, 7), -1, -1):
|
|
day = end - timedelta(days=i)
|
|
hd = gql(
|
|
host_q,
|
|
{"zoneTag": zid, "day": day.isoformat(), "host": HOST},
|
|
)
|
|
if hd.get("errors"):
|
|
print(f"{day.isoformat():<12} error: {hd['errors'][0].get('message')}")
|
|
continue
|
|
z = hd["data"]["viewer"]["zones"][0]
|
|
tot = (z["total"] or [{}])[0]
|
|
c = tot.get("count") or 0
|
|
v = (tot.get("sum") or {}).get("visits") or 0
|
|
b = (tot.get("sum") or {}).get("edgeResponseBytes") or 0
|
|
print(f"{day.isoformat():<12} {c:>10} {v:>10} {fmt_bytes(b):>12}")
|
|
host_days.append((day, z, c, v, b))
|
|
|
|
# Detail for the busiest recent day.
|
|
pick = max(host_days, key=lambda x: x[2], default=None)
|
|
if pick and pick[2] > 0:
|
|
day, z, _, _, _ = pick
|
|
print(f"\nDetail for busiest day {day.isoformat()} ({HOST})")
|
|
print("Top paths:")
|
|
for row in z["byPath"][:15]:
|
|
path = row["dimensions"].get("clientRequestPath") or "/"
|
|
v = (row.get("sum") or {}).get("visits") or 0
|
|
print(f" {row['count']:>8} req {v:>6} visits {path}")
|
|
print("Top countries:")
|
|
for row in z["byCountry"]:
|
|
name = row["dimensions"].get("clientCountryName") or "?"
|
|
v = (row.get("sum") or {}).get("visits") or 0
|
|
print(f" {row['count']:>8} req {v:>6} visits {name}")
|
|
print("Status codes:")
|
|
for row in z["byStatus"]:
|
|
st = row["dimensions"].get("edgeResponseStatus")
|
|
print(f" {st}: {row['count']}")
|
|
|
|
print(
|
|
"\nNote: edge requests/visits via Cloudflare proxy (not RUM pageviews). "
|
|
"Includes assets/bots. Zone totals mix all refining.dev hosts; "
|
|
"host table is dota2-only."
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|