Add Cloudflare traffic helper scripts and commercialization notes.
Track reusable CF analytics utilities and the commercial prospects document while keeping local probe/sample artifacts untracked. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+217
@@ -0,0 +1,217 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user