mirror of
https://github.com/prdlk/dotfiles.git
synced 2026-11-05 04:08:03 +00:00
config(git,claude,ssh): templated gitconfig; claude settings/commands/hooks/skills; ssh config
gitconfig: name/email from chezmoi data, gh credential helper by PATH lookup (portable to darwin). claude: no credentials, history, projects, sessions, or plugin caches. ssh config as private_ (0600). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3c691e8dc5
commit
20891f3910
476 files changed
+59086
-7
No files matched your search
@@ -0,0 +1,147 @@
|
||||
# Cloudflare GraphQL Analytics API
|
||||
|
||||
Query analytics data across all Cloudflare products via a single GraphQL endpoint. Covers HTTP requests, Workers metrics, DNS, Firewall events, Network Analytics, and 70+ other datasets.
|
||||
|
||||
## Overview
|
||||
|
||||
- **Single endpoint** for all analytics: `https://api.cloudflare.com/client/v4/graphql`
|
||||
- **1,400+ schema types** spanning every Cloudflare product
|
||||
- **Two scopes**: zone-level (per-domain) and account-level (cross-domain)
|
||||
- **Adaptive sampling** on high-traffic datasets with confidence intervals
|
||||
- **No mutations** - read-only analytics (the Mutation type is a stub)
|
||||
- **Cost-based rate limiting** - default 300 queries per 5 minutes per user (max 320, varies by query cost)
|
||||
|
||||
## Quick Decision Tree
|
||||
|
||||
```
|
||||
Need analytics data from Cloudflare?
|
||||
├─ HTTP traffic (requests, bandwidth, cache) → httpRequestsAdaptiveGroups (zone or account)
|
||||
├─ Workers performance (CPU, wall time, errors) → workersInvocationsAdaptive (account)
|
||||
├─ Firewall/WAF events → firewallEventsAdaptive / firewallEventsAdaptiveGroups (zone or account)
|
||||
├─ DNS query analytics → dnsAnalyticsAdaptive / dnsAnalyticsAdaptiveGroups (zone or account)
|
||||
├─ Network layer (DDoS, Magic Transit) → *NetworkAnalyticsAdaptiveGroups (account)
|
||||
├─ Storage (R2, KV, D1, DO) → r2OperationsAdaptiveGroups / kvOperationsAdaptiveGroups / etc. (account)
|
||||
├─ AI (Workers AI, AI Gateway) → aiInferenceAdaptive / aiGatewayRequestsAdaptiveGroups (account)
|
||||
├─ Load Balancing → loadBalancingRequestsAdaptiveGroups (zone)
|
||||
├─ Custom high-cardinality metrics → Workers Analytics Engine (see ../analytics-engine/)
|
||||
└─ Need raw logs, not aggregates → Logpush (see Cloudflare docs)
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
| Concept | Description |
|
||||
|---------|-------------|
|
||||
| **Endpoint** | `POST https://api.cloudflare.com/client/v4/graphql` |
|
||||
| **Explorer** | [graphql.cloudflare.com](https://graphql.cloudflare.com/) - interactive query builder |
|
||||
| **Viewer** | Root query object: `viewer { zones(...) { ... } }` or `viewer { accounts(...) { ... } }` |
|
||||
| **Dataset (Node)** | A queryable table under a zone or account (e.g., `httpRequestsAdaptiveGroups`) |
|
||||
| **Dimensions** | Fields to group by (time buckets, country, status code, script name, etc.) |
|
||||
| **Metrics** | Aggregation fields: `count`, `sum { ... }`, `avg { ... }`, `quantiles { ... }`, `ratio { ... }` |
|
||||
| **Filter** | Input object constraining results by time range, dimensions, etc. |
|
||||
| **Limit** | Maximum rows returned per dataset node (required, max varies by dataset) |
|
||||
| **OrderBy** | Enum-based sorting: `[field_ASC]` or `[field_DESC]` |
|
||||
| **Adaptive Sampling** | Nodes with `Adaptive` in the name use ABR sampling; results are statistically representative |
|
||||
|
||||
## Query Structure
|
||||
|
||||
Every query follows this pattern:
|
||||
|
||||
```graphql
|
||||
{
|
||||
viewer {
|
||||
# Zone-scoped
|
||||
zones(filter: { zoneTag: "ZONE_ID" }) {
|
||||
datasetName(
|
||||
filter: { datetime_gt: "...", datetime_lt: "..." }
|
||||
limit: 1000
|
||||
orderBy: [datetimeFiveMinutes_DESC]
|
||||
) {
|
||||
count
|
||||
dimensions { ... }
|
||||
sum { ... }
|
||||
}
|
||||
}
|
||||
# Account-scoped
|
||||
accounts(filter: { accountTag: "ACCOUNT_ID" }) {
|
||||
datasetName(filter: { ... }, limit: 100) {
|
||||
count
|
||||
dimensions { ... }
|
||||
sum { ... }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dataset Naming Convention
|
||||
|
||||
Dataset names follow a consistent pattern visible in the schema:
|
||||
|
||||
| Pattern | Meaning | Example |
|
||||
|---------|---------|---------|
|
||||
| `*Adaptive` | Raw rows with adaptive sampling; some (e.g., `workersInvocationsAdaptive`) also support aggregation fields (`sum`, `quantiles`, `avg`) | `httpRequestsAdaptive`, `workersInvocationsAdaptive` |
|
||||
| `*AdaptiveGroups` | Aggregated data with adaptive sampling | `httpRequestsAdaptiveGroups` |
|
||||
| `*1hGroups` | Hourly rollups (pre-aggregated) | `httpRequests1hGroups` |
|
||||
| `*1dGroups` | Daily rollups (pre-aggregated) | `httpRequests1dGroups` |
|
||||
| `*1mGroups` | Minutely rollups | `httpRequests1mGroups` |
|
||||
| `Zone*` prefix | Zone-scoped dataset | `ZoneHttpRequestsAdaptiveGroups` |
|
||||
| `Account*` prefix | Account-scoped dataset | `AccountWorkersInvocationsAdaptive` |
|
||||
|
||||
**Prefer `*AdaptiveGroups` nodes** for most use cases - they support flexible time grouping via dimension fields (`datetimeFiveMinutes`, `datetimeHour`, etc.) and are the most commonly used.
|
||||
|
||||
## Key Datasets by Product
|
||||
|
||||
### Zone-Scoped (per-domain)
|
||||
|
||||
| Dataset | Description |
|
||||
|---------|-------------|
|
||||
| `httpRequestsAdaptiveGroups` | HTTP traffic: requests, bytes, cache status, bot scores, WAF scores |
|
||||
| `httpRequests1hGroups` / `1dGroups` / `1mGroups` | Pre-aggregated HTTP rollups (hourly/daily/minutely) |
|
||||
| `firewallEventsAdaptiveGroups` | WAF, rate limiting, bot management, firewall rule events |
|
||||
| `dnsAnalyticsAdaptiveGroups` | DNS query volumes, response codes, query types |
|
||||
| `loadBalancingRequestsAdaptiveGroups` | Load Balancer origin request metrics |
|
||||
| `pageShieldReportsAdaptiveGroups` | Page Shield CSP reports |
|
||||
|
||||
### Account-Scoped (cross-domain)
|
||||
|
||||
| Dataset | Description |
|
||||
|---------|-------------|
|
||||
| `workersInvocationsAdaptive` | Workers: requests, errors, CPU time, wall time, subrequests |
|
||||
| `durableObjectsInvocationsAdaptiveGroups` | DO invocations |
|
||||
| `durableObjectsStorageGroups` / `durableObjectsPeriodicGroups` | DO storage and periodic metrics |
|
||||
| `d1AnalyticsAdaptiveGroups` / `d1QueriesAdaptiveGroups` | D1 database analytics |
|
||||
| `r2OperationsAdaptiveGroups` / `r2StorageAdaptiveGroups` | R2 operations and storage |
|
||||
| `kvOperationsAdaptiveGroups` / `kvStorageAdaptiveGroups` | KV operations and storage |
|
||||
| `aiInferenceAdaptiveGroups` | Workers AI inference metrics |
|
||||
| `aiGatewayRequestsAdaptiveGroups` | AI Gateway request analytics |
|
||||
| `pagesFunctionsInvocationsAdaptiveGroups` | Pages Functions metrics |
|
||||
| `magicTransitNetworkAnalyticsAdaptiveGroups` | Magic Transit packet/byte analytics |
|
||||
| `spectrumNetworkAnalyticsAdaptiveGroups` | Spectrum TCP/UDP analytics |
|
||||
| `gatewayL7RequestsAdaptiveGroups` | Zero Trust Gateway HTTP metrics |
|
||||
| `gatewayResolverQueriesAdaptiveGroups` | Zero Trust Gateway DNS metrics |
|
||||
|
||||
## Reading Order
|
||||
|
||||
| Task | Start Here | Then Read |
|
||||
|------|------------|-----------|
|
||||
| **First query** | [configuration.md](configuration.md) (auth) -> this README (structure) | [api.md](api.md) |
|
||||
| **Build a dashboard** | [patterns.md](patterns.md) (time-series, top-N) | [api.md](api.md) (aggregation fields) |
|
||||
| **Debug query issues** | [gotchas.md](gotchas.md) | [api.md](api.md) (filtering) |
|
||||
| **Understand sampling** | [gotchas.md](gotchas.md) (sampling section) | [api.md](api.md) (confidence intervals) |
|
||||
| **Product-specific metrics** | [patterns.md](patterns.md) (per-product examples) | [api.md](api.md) (dataset reference) |
|
||||
|
||||
## In This Reference
|
||||
|
||||
- **[api.md](api.md)** - Query structure, aggregation fields (sum/avg/quantiles/count), filtering operators, dimensions, dataset details
|
||||
- **[configuration.md](configuration.md)** - Authentication, API tokens, client setup (curl, JS, Python), introspection
|
||||
- **[patterns.md](patterns.md)** - Common queries: time-series, top-N, Workers metrics, HTTP analytics, firewall events, multi-zone
|
||||
- **[gotchas.md](gotchas.md)** - Rate limits, sampling caveats, query cost, common errors, plan-based limits
|
||||
|
||||
## See Also
|
||||
|
||||
- [GraphQL Analytics API Docs](https://developers.cloudflare.com/analytics/graphql-api/)
|
||||
- [GraphQL API Explorer](https://graphql.cloudflare.com/)
|
||||
- [Observability Reference](../observability/) - Workers Logs, Tail Workers, console logging
|
||||
- [Analytics Engine Reference](../analytics-engine/) - Custom high-cardinality analytics via Workers
|
||||
- [Web Analytics Reference](../web-analytics/) - Client-side (RUM) analytics
|
||||
- [API Reference](../api/) - REST API, SDKs, authentication basics
|
||||
@@ -0,0 +1,175 @@
|
||||
# GraphQL Analytics API Reference
|
||||
|
||||
## Query Root
|
||||
|
||||
The schema has a single entry point: `Query.viewer`. Mutations are not supported.
|
||||
|
||||
```graphql
|
||||
{
|
||||
cost # uint64 -- query cost (returned in response)
|
||||
viewer {
|
||||
budget # uint64 -- remaining budget
|
||||
zones(filter: { zoneTag: "..." }) { ... }
|
||||
accounts(filter: { accountTag: "..." }) { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Aggregation Fields
|
||||
|
||||
Aggregated dataset nodes (`*Groups`) return these field categories. Not every node has all — use introspection to check.
|
||||
|
||||
### count
|
||||
|
||||
Total events in the group. Available on `*Groups` nodes but **not** on raw `*Adaptive` nodes (e.g., `workersInvocationsAdaptive` — use `sum { requests }` instead).
|
||||
|
||||
### sum
|
||||
|
||||
Cumulative metrics. Fields vary by dataset:
|
||||
|
||||
```graphql
|
||||
# HTTP requests
|
||||
sum { edgeResponseBytes edgeRequestBytes visits edgeTimeToFirstByteMs originResponseDurationMs }
|
||||
|
||||
# Workers invocations
|
||||
sum { requests errors subrequests cpuTimeUs wallTime duration responseBodySize clientDisconnects requestDuration }
|
||||
```
|
||||
|
||||
### quantiles
|
||||
|
||||
Percentile distributions (on datasets like `workersInvocationsAdaptive`). Available percentiles: P25, P50, P75, P90, P95, P99, P999 for `cpuTime`, `wallTime`, `requestDuration`, `duration`, `responseBodySize`.
|
||||
|
||||
```graphql
|
||||
quantiles { cpuTimeP50 cpuTimeP99 wallTimeP50 wallTimeP99 }
|
||||
```
|
||||
|
||||
### ratio, avg, uniq, confidence
|
||||
|
||||
```graphql
|
||||
ratio { status4xx status5xx } # float64 (0 to 1) -- HTTP datasets only
|
||||
avg { sampleInterval } # useful for understanding sampling resolution
|
||||
uniq { uniques } # unique IP count -- rollup datasets (*1hGroups, *1dGroups) only
|
||||
confidence(level: 0.95) { # Adaptive datasets only; works on count and sum fields
|
||||
count { estimate lower upper sampleSize }
|
||||
}
|
||||
```
|
||||
|
||||
## Dimensions
|
||||
|
||||
Dimensions are fields you can group by via the `dimensions` sub-selection.
|
||||
|
||||
### Time Dimensions
|
||||
|
||||
| Dimension | Granularity |
|
||||
|-----------|------------|
|
||||
| `date` | Day |
|
||||
| `datetime` | Exact timestamp |
|
||||
| `datetimeMinute` | 1 minute |
|
||||
| `datetimeFiveMinutes` | 5 minutes |
|
||||
| `datetimeFifteenMinutes` | 15 minutes |
|
||||
| `datetimeHour` | 1 hour |
|
||||
|
||||
Workers datasets also support `datetimeSixHours`.
|
||||
|
||||
### HTTP Request Dimensions (httpRequestsAdaptiveGroups)
|
||||
|
||||
83 dimensions available. Key ones:
|
||||
|
||||
| Dimension | Description |
|
||||
|-----------|-------------|
|
||||
| `clientCountryName` | Country of origin |
|
||||
| `clientRequestHTTPHost` | Requested hostname |
|
||||
| `clientRequestHTTPMethodName` | HTTP method |
|
||||
| `clientRequestPath` | URI path |
|
||||
| `edgeResponseStatus` | Edge HTTP status code |
|
||||
| `cacheStatus` | Cache status (hit, miss, dynamic, etc.) |
|
||||
| `coloCode` | Cloudflare datacenter IATA code |
|
||||
| `clientIP` / `clientAsn` | Client IP address / ASN |
|
||||
| `botScore` / `botManagementDecision` | Bot management score (0-99) / verdict |
|
||||
| `wafAttackScore` / `securityAction` | WAF score / firewall action taken |
|
||||
| `ja3Hash` / `ja4` | TLS fingerprints |
|
||||
| `sampleInterval` | ABR sample interval |
|
||||
|
||||
### Workers Dimensions (workersInvocationsAdaptive)
|
||||
|
||||
`scriptName`, `scriptTag`, `scriptVersion`, `environmentName`, `status`, `usageModel`, `coloCode`, `dispatchNamespaceName`, `isDispatcher`
|
||||
|
||||
### Firewall Dimensions (firewallEventsAdaptive)
|
||||
|
||||
`action`, `source`, `ruleId`, `clientCountryName`, `clientIP`, `clientAsn`, `userAgent`
|
||||
|
||||
## Filtering
|
||||
|
||||
### Scope Filters
|
||||
|
||||
```graphql
|
||||
zones(filter: { zoneTag: "ZONE_ID" }) # up to 10 zones
|
||||
zones(filter: { zoneTag_in: ["Z1", "Z2"] })
|
||||
accounts(filter: { accountTag: "ACCOUNT_ID" }) # exactly 1 account
|
||||
```
|
||||
|
||||
### Dataset Filters
|
||||
|
||||
**Always include a time range filter.** Multiple filters at the same level are implicitly AND-ed.
|
||||
|
||||
```graphql
|
||||
httpRequestsAdaptiveGroups(
|
||||
filter: { datetime_gt: "2025-01-01T00:00:00Z", datetime_lt: "2025-01-02T00:00:00Z", clientCountryName: "US" }
|
||||
limit: 1000
|
||||
)
|
||||
```
|
||||
|
||||
### Filter Operators
|
||||
|
||||
| Operator | Meaning | Example |
|
||||
|----------|---------|---------|
|
||||
| (none) | equals | `clientCountryName: "US"` |
|
||||
| `_gt` / `_lt` | greater / less than | `datetime_gt: "..."` |
|
||||
| `_geq` / `_leq` | greater/less or equal | `datetime_geq: "..."` |
|
||||
| `_neq` | not equal | `cacheStatus_neq: "hit"` |
|
||||
| `_in` / `_notin` | in / not in list | `clientCountryName_in: ["US", "GB"]` |
|
||||
| `_like` / `_notlike` | SQL LIKE with `%` | `clientRequestPath_like: "/api/%"` |
|
||||
| `_has` / `_hasall` / `_hasany` | array contains | `botDetectionIds_has: "abc"` |
|
||||
|
||||
> `_notin` and `_notlike` are in the schema but not in official docs. Confirmed via introspection.
|
||||
|
||||
### Boolean Operators (AND / OR)
|
||||
|
||||
```graphql
|
||||
# Explicit AND
|
||||
filter: { AND: [{ datetime_gt: "..." }, { datetime_lt: "..." }, { clientCountryName: "US" }] }
|
||||
|
||||
# Explicit OR
|
||||
filter: { datetime_gt: "...", OR: [{ edgeResponseStatus: 403 }, { edgeResponseStatus: 429 }] }
|
||||
```
|
||||
|
||||
## Pagination & Sorting
|
||||
|
||||
No cursor-based pagination. Use `limit`, `orderBy`, and filter-based offsets:
|
||||
|
||||
```graphql
|
||||
# First page
|
||||
httpRequestsAdaptiveGroups(filter: { datetime_gt: "..." }, limit: 100, orderBy: [datetime_ASC])
|
||||
|
||||
# Next page: filter by last seen value from previous page
|
||||
httpRequestsAdaptiveGroups(filter: { datetime_gt: "2025-01-01T01:35:00Z" }, limit: 100, orderBy: [datetime_ASC])
|
||||
```
|
||||
|
||||
Sort with `orderBy: [field_ASC]` or `[field_DESC]`. Multiple sort fields supported.
|
||||
|
||||
## Settings Node
|
||||
|
||||
Query per-node limits and availability:
|
||||
|
||||
```graphql
|
||||
viewer { zones(filter: { zoneTag: "..." }) { settings {
|
||||
httpRequestsAdaptiveGroups { enabled maxDuration maxNumberOfFields maxPageSize notOlderThan }
|
||||
} } }
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [README.md](README.md) - Overview, decision tree, dataset index
|
||||
- [configuration.md](configuration.md) - Authentication, client setup, introspection queries
|
||||
- [patterns.md](patterns.md) - Common query patterns (time-series, top-N, per-product)
|
||||
- [gotchas.md](gotchas.md) - Rate limits, sampling, troubleshooting
|
||||
@@ -0,0 +1,118 @@
|
||||
# GraphQL Analytics API Configuration
|
||||
|
||||
## Authentication
|
||||
|
||||
### API Token (Recommended)
|
||||
|
||||
| Permission | Scope | Use Case |
|
||||
|------------|-------|----------|
|
||||
| **Account Analytics: Read** | Account-wide | Workers, R2, KV, D1, DO, AI, Network Analytics |
|
||||
| **Zone Analytics: Read** | Per-zone | HTTP requests, Firewall, DNS, Load Balancing |
|
||||
| **All zones - Analytics: Read** | All zones | Multi-zone HTTP/Firewall/DNS queries |
|
||||
|
||||
Create tokens at: [dash.cloudflare.com > Account API Tokens](https://dash.cloudflare.com/?to=/:account/api-tokens)
|
||||
|
||||
```bash
|
||||
# Verify token
|
||||
curl -s https://api.cloudflare.com/client/v4/graphql \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data '{"query":"{ viewer { zones(filter: {zoneTag: \"ZONE_ID\"}) { httpRequestsAdaptiveGroups(limit: 1, filter: {datetime_gt: \"2025-01-01T00:00:00Z\"}) { count } } } }"}'
|
||||
```
|
||||
|
||||
### API Key + Email (Legacy)
|
||||
|
||||
Not recommended. Use `X-Auth-Email` + `X-Auth-Key` headers instead of `Authorization: Bearer`.
|
||||
|
||||
## Client Setup
|
||||
|
||||
### curl
|
||||
|
||||
```bash
|
||||
curl -s https://api.cloudflare.com/client/v4/graphql \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data '{
|
||||
"query": "query($zoneTag: string!, $start: Time!, $end: Time!) { viewer { zones(filter: {zoneTag: $zoneTag}) { httpRequestsAdaptiveGroups(filter: {datetime_gt: $start, datetime_lt: $end}, limit: 10, orderBy: [datetimeFiveMinutes_DESC]) { count dimensions { datetimeFiveMinutes } } } } }",
|
||||
"variables": { "zoneTag": "ZONE_ID", "start": "2025-01-01T00:00:00Z", "end": "2025-01-02T00:00:00Z" }
|
||||
}' | jq .
|
||||
```
|
||||
|
||||
### TypeScript / JavaScript
|
||||
|
||||
```typescript
|
||||
const GRAPHQL_ENDPOINT = "https://api.cloudflare.com/client/v4/graphql";
|
||||
|
||||
async function queryGraphQL<T>(query: string, variables: Record<string, unknown> = {}): Promise<T> {
|
||||
const response = await fetch(GRAPHQL_ENDPOINT, {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${process.env.CF_API_TOKEN}`, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ query, variables }),
|
||||
});
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
const json = await response.json() as { data: T | null; errors?: { message: string }[] };
|
||||
if (json.errors?.length) throw new Error(json.errors.map((e) => e.message).join("; "));
|
||||
return json.data!;
|
||||
}
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import requests, os
|
||||
|
||||
def query_graphql(query: str, variables: dict = None) -> dict:
|
||||
r = requests.post("https://api.cloudflare.com/client/v4/graphql",
|
||||
headers={"Authorization": f"Bearer {os.environ['CF_API_TOKEN']}", "Content-Type": "application/json"},
|
||||
json={"query": query, "variables": variables or {}})
|
||||
r.raise_for_status()
|
||||
result = r.json()
|
||||
if result.get("errors"):
|
||||
raise Exception("; ".join(e["message"] for e in result["errors"]))
|
||||
return result["data"]
|
||||
```
|
||||
|
||||
### From a Cloudflare Worker
|
||||
|
||||
Store the API token as a secret (`CF_API_TOKEN`). Use standard `fetch` to POST to `https://api.cloudflare.com/client/v4/graphql` with the same JSON body format as above. Always check `response.errors` — GraphQL returns 200 even on query failures.
|
||||
|
||||
## GraphQL API Explorer
|
||||
|
||||
Interactive explorer at [graphql.cloudflare.com](https://graphql.cloudflare.com/) — provides schema docs, autocomplete, variable panel, and shareable queries. Authenticates via your Cloudflare dashboard session.
|
||||
|
||||
## Schema Introspection
|
||||
|
||||
```graphql
|
||||
# List zone-scoped datasets
|
||||
{ __type(name: "zone") { fields { name description } } }
|
||||
|
||||
# List account-scoped datasets
|
||||
{ __type(name: "account") { fields { name description } } }
|
||||
|
||||
# Discover dimensions for a dataset
|
||||
{ __type(name: "ZoneHttpRequestsAdaptiveGroupsDimensions") {
|
||||
fields { name type { name kind } }
|
||||
} }
|
||||
|
||||
# Discover filter operators for a dataset
|
||||
{ __type(name: "ZoneHttpRequestsAdaptiveGroupsFilter_InputObject") {
|
||||
inputFields { name type { name kind } }
|
||||
} }
|
||||
```
|
||||
|
||||
## Finding Your Zone and Account IDs
|
||||
|
||||
- **Zone ID**: Dashboard > select zone > Overview (right sidebar), or via API
|
||||
- **Account ID**: Dashboard > Account Home URL, or via API
|
||||
|
||||
```bash
|
||||
curl -s https://api.cloudflare.com/client/v4/zones -H "Authorization: Bearer $CF_API_TOKEN" | jq '.result[] | {name, id}'
|
||||
curl -s https://api.cloudflare.com/client/v4/accounts -H "Authorization: Bearer $CF_API_TOKEN" | jq '.result[] | {name, id}'
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [README.md](README.md) - Overview, decision tree, dataset index
|
||||
- [api.md](api.md) - Query structure, aggregation fields, filtering operators
|
||||
- [patterns.md](patterns.md) - Common query patterns (time-series, top-N, per-product)
|
||||
- [gotchas.md](gotchas.md) - Rate limits, sampling, troubleshooting
|
||||
@@ -0,0 +1,110 @@
|
||||
# GraphQL Analytics API Gotchas & Troubleshooting
|
||||
|
||||
## Rate Limits
|
||||
|
||||
| Limit | Value |
|
||||
|-------|-------|
|
||||
| GraphQL queries per user | **Default 300 per 5 minutes** (max 320, at least 1/sec) |
|
||||
| General API rate limit | 1200 requests per 5 minutes (shared across all API calls) |
|
||||
| Zone scope per query | Up to **10 zones** |
|
||||
| Account scope per query | Exactly **1 account** |
|
||||
|
||||
The GraphQL rate limit is separate from the general API limit. Exceeding either results in `HTTP 429` and blocks all API calls for 5 minutes. Enterprise customers can contact support to raise limits.
|
||||
|
||||
### "429 Too Many Requests"
|
||||
|
||||
**Cause:** Exceeded rate limit.
|
||||
|
||||
**Solution:** Batch multiple datasets into single queries, cache results, increase intervals between queries. Use `{ viewer { budget } }` to monitor remaining budget.
|
||||
|
||||
## Sampling & Data Accuracy
|
||||
|
||||
### Adaptive Bit Rate (ABR) Sampling
|
||||
|
||||
Datasets with `Adaptive` in the name use adaptive sampling:
|
||||
- Results are **statistically representative**, not exact
|
||||
- Same query may return **slightly different numbers** each run
|
||||
- Higher traffic = higher sampling rate = more accurate
|
||||
- `sampleInterval` dimension shows the ratio (1 = no sampling, 10 = ~1-in-10 sampled)
|
||||
|
||||
For high-confidence numbers, use `confidence(level: 0.95)` to get estimate bounds. For exact counts, use rollup nodes (`httpRequests1hGroups`, `httpRequests1dGroups`) which are pre-aggregated without sampling.
|
||||
|
||||
### Rollup vs. Adaptive
|
||||
|
||||
| Feature | Rollup (`*1hGroups`, `*1dGroups`) | Adaptive (`*AdaptiveGroups`) |
|
||||
|---------|-----------------------------------|-----------------------------|
|
||||
| Sampling | No (pre-aggregated) | Yes (ABR) |
|
||||
| Flexibility | Fixed time buckets | Any granularity |
|
||||
| Dimensions | Fewer | Many more |
|
||||
| Accuracy | Exact | Statistical estimate |
|
||||
|
||||
## Common Errors
|
||||
|
||||
### "Access denied" / "authentication error"
|
||||
|
||||
**Cause:** Token lacks required permission or wrong scope.
|
||||
|
||||
**Solution:** Account-scoped queries need **Account Analytics: Read**. Zone-scoped queries need **Zone Analytics: Read**. Verify: `curl -s https://api.cloudflare.com/client/v4/user/tokens/verify -H "Authorization: Bearer $TOKEN"`
|
||||
|
||||
### "field not found" / "Cannot query field"
|
||||
|
||||
**Cause:** Wrong dataset name, nonexistent field, or wrong scope (zone vs. account).
|
||||
|
||||
**Solution:** Names are case-sensitive camelCase (`httpRequestsAdaptiveGroups`). Zone datasets go under `zones(...)`, account datasets under `accounts(...)`. Use introspection to verify.
|
||||
|
||||
### "filter is required" / empty results
|
||||
|
||||
**Cause:** Missing required time range filter or incorrect zone/account tag.
|
||||
|
||||
**Solution:** Always include `datetime_gt` / `datetime_lt` (or `_geq` / `_leq`).
|
||||
|
||||
### "limit is required" / "limit exceeds maximum"
|
||||
|
||||
**Cause:** Missing `limit` or exceeding node's max page size.
|
||||
|
||||
**Solution:** Always specify `limit`. Max varies by dataset (typically 10,000 for groups, 100 for raw events). Check via settings query.
|
||||
|
||||
### "query is too complex" / "query exceeds budget"
|
||||
|
||||
**Cause:** Too many fields, datasets, or too broad a time range.
|
||||
|
||||
**Solution:** Reduce time range, request fewer dimensions/metrics, break into smaller queries. Monitor `cost` and `budget` in responses.
|
||||
|
||||
### 200 Response with Errors
|
||||
|
||||
GraphQL returns HTTP 200 even on failures. **Always check `response.errors`:**
|
||||
|
||||
```json
|
||||
{ "data": null, "errors": [{ "message": "filter is required for httpRequestsAdaptiveGroups" }] }
|
||||
```
|
||||
|
||||
## Plan-Based Availability
|
||||
|
||||
Not all datasets are available on all plans. Higher plans get more datasets, longer retention (`notOlderThan`), wider time ranges (`maxDuration`), more fields, and larger page sizes.
|
||||
|
||||
### "node is not available" / "node is disabled"
|
||||
|
||||
**Cause:** Dataset not on your plan, or product not enabled.
|
||||
|
||||
**Solution:** Check `settings { <nodeName> { enabled } }`. Some datasets require specific subscriptions (e.g., Network Analytics requires Magic Transit/Spectrum).
|
||||
|
||||
## DateTime & Timezone Handling
|
||||
|
||||
- All times are **UTC only** (ISO 8601: `"2025-01-15T10:30:00Z"`)
|
||||
- `Date` type: `"2025-01-15"` (used in `date_geq`/`date_leq` for storage datasets)
|
||||
- `Time` type: `"2025-01-15T10:30:00Z"` (used in `datetime_gt`/`datetime_lt`)
|
||||
- Filters are start-inclusive: events that start within the window are included
|
||||
|
||||
## Performance Tips
|
||||
|
||||
- **Narrow time ranges** are faster and cheaper
|
||||
- **Select only needed dimensions** — each additional dimension increases cost
|
||||
- **Use rollup nodes** (`*1dGroups`) for simple daily totals without dimension breakdowns
|
||||
- **Batch datasets** into one query instead of separate HTTP requests
|
||||
|
||||
## See Also
|
||||
|
||||
- [README.md](README.md) - Overview, decision tree, dataset index
|
||||
- [api.md](api.md) - Query structure, aggregation fields, filtering operators
|
||||
- [configuration.md](configuration.md) - Authentication, client setup, introspection queries
|
||||
- [patterns.md](patterns.md) - Common query patterns (time-series, top-N, per-product)
|
||||
@@ -0,0 +1,225 @@
|
||||
# GraphQL Analytics API Patterns & Best Practices
|
||||
|
||||
## Time-Series Queries
|
||||
|
||||
Use time dimension granularity matching your range (see Best Practices below).
|
||||
|
||||
```graphql
|
||||
query TrafficTimeSeries($zoneTag: string!, $start: Time!, $end: Time!) {
|
||||
viewer {
|
||||
zones(filter: { zoneTag: $zoneTag }) {
|
||||
httpRequestsAdaptiveGroups(
|
||||
filter: { datetime_gt: $start, datetime_lt: $end }
|
||||
limit: 1000
|
||||
orderBy: [datetimeFiveMinutes_ASC] # or datetimeHour_ASC for longer ranges
|
||||
) {
|
||||
count
|
||||
dimensions { datetimeFiveMinutes }
|
||||
sum { edgeResponseBytes }
|
||||
ratio { status4xx status5xx }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Top-N Queries
|
||||
|
||||
### Top Countries by Request Count
|
||||
|
||||
```graphql
|
||||
query TopCountries($zoneTag: string!, $start: Time!, $end: Time!) {
|
||||
viewer {
|
||||
zones(filter: { zoneTag: $zoneTag }) {
|
||||
httpRequestsAdaptiveGroups(
|
||||
filter: { datetime_gt: $start, datetime_lt: $end }
|
||||
limit: 10
|
||||
orderBy: [count_DESC]
|
||||
) {
|
||||
count
|
||||
dimensions { clientCountryName }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use `orderBy: [sum_edgeResponseBytes_DESC]` for top paths by bandwidth. Add `edgeResponseStatus_geq: 400` to the filter for top error status codes.
|
||||
|
||||
## Workers Analytics
|
||||
|
||||
```graphql
|
||||
query WorkersOverview($accountTag: string!, $start: Time!, $end: Time!) {
|
||||
viewer {
|
||||
accounts(filter: { accountTag: $accountTag }) {
|
||||
workersInvocationsAdaptive(
|
||||
filter: { datetime_gt: $start, datetime_lt: $end }
|
||||
limit: 100
|
||||
orderBy: [sum_requests_DESC]
|
||||
) {
|
||||
sum { requests errors subrequests wallTime }
|
||||
quantiles { cpuTimeP50 cpuTimeP99 wallTimeP50 wallTimeP99 }
|
||||
dimensions { scriptName }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Filter by `scriptName` for a specific Worker. Add `datetimeFiveMinutes` dimension + `orderBy: [datetimeFiveMinutes_ASC]` for error rate over time.
|
||||
|
||||
## Firewall / Security
|
||||
|
||||
```graphql
|
||||
query RecentFirewallEvents($zoneTag: string!, $start: Time!) {
|
||||
viewer {
|
||||
zones(filter: { zoneTag: $zoneTag }) {
|
||||
firewallEventsAdaptive(
|
||||
filter: { datetime_gt: $start }
|
||||
limit: 50
|
||||
orderBy: [datetime_DESC]
|
||||
) {
|
||||
action source clientIP clientCountryName userAgent
|
||||
clientRequestHTTPHost clientRequestPath ruleId datetime
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For aggregated firewall stats, use `firewallEventsAdaptiveGroups` with `action: "block"` filter and group by `ruleId`, `source`, `datetimeHour`.
|
||||
|
||||
## DNS Analytics
|
||||
|
||||
```graphql
|
||||
query DNSQueryVolume($zoneTag: string!, $start: Time!, $end: Time!) {
|
||||
viewer {
|
||||
zones(filter: { zoneTag: $zoneTag }) {
|
||||
dnsAnalyticsAdaptiveGroups(
|
||||
filter: { datetime_gt: $start, datetime_lt: $end }
|
||||
limit: 500
|
||||
orderBy: [datetimeFiveMinutes_ASC]
|
||||
) {
|
||||
count
|
||||
dimensions { datetimeFiveMinutes }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Storage Analytics (Account-Scoped)
|
||||
|
||||
R2, KV, and D1 use `date` (Date type) filters instead of `datetime` (Time type).
|
||||
|
||||
```graphql
|
||||
# R2 operations
|
||||
r2OperationsAdaptiveGroups(filter: { date_geq: $start, date_leq: $end }, limit: 100, orderBy: [date_DESC]) {
|
||||
dimensions { date bucketName actionType }
|
||||
sum { requests }
|
||||
}
|
||||
|
||||
# KV operations
|
||||
kvOperationsAdaptiveGroups(filter: { date_geq: $start, date_leq: $end }, limit: 100, orderBy: [date_DESC]) {
|
||||
dimensions { date actionType }
|
||||
sum { requests }
|
||||
}
|
||||
|
||||
# D1 analytics
|
||||
d1AnalyticsAdaptiveGroups(filter: { date_geq: $start, date_leq: $end }, limit: 100, orderBy: [date_DESC]) {
|
||||
dimensions { date databaseId }
|
||||
sum { readQueries writeQueries rowsRead rowsWritten }
|
||||
}
|
||||
```
|
||||
|
||||
## Cache Analytics
|
||||
|
||||
```graphql
|
||||
query CacheStatusBreakdown($zoneTag: string!, $start: Time!, $end: Time!) {
|
||||
viewer {
|
||||
zones(filter: { zoneTag: $zoneTag }) {
|
||||
httpRequestsAdaptiveGroups(
|
||||
filter: { datetime_gt: $start, datetime_lt: $end }
|
||||
limit: 20
|
||||
orderBy: [count_DESC]
|
||||
) {
|
||||
count
|
||||
dimensions { cacheStatus }
|
||||
sum { edgeResponseBytes }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For cache hit ratio over time, use aliases to query the same dataset twice — once with `cacheStatus: "hit"` filter and once without — then compute the ratio client-side.
|
||||
|
||||
## Multi-Dataset Queries
|
||||
|
||||
A single request can query multiple datasets, avoiding extra HTTP round-trips:
|
||||
|
||||
```graphql
|
||||
query DashboardOverview($zoneTag: string!, $start: Time!, $end: Time!) {
|
||||
viewer {
|
||||
zones(filter: { zoneTag: $zoneTag }) {
|
||||
httpTraffic: httpRequestsAdaptiveGroups(
|
||||
filter: { datetime_gt: $start, datetime_lt: $end }, limit: 1
|
||||
) { count sum { edgeResponseBytes } ratio { status4xx status5xx } }
|
||||
firewallEvents: firewallEventsAdaptiveGroups(
|
||||
filter: { datetime_gt: $start, datetime_lt: $end }, limit: 5, orderBy: [count_DESC]
|
||||
) { count dimensions { action source } }
|
||||
dnsQueries: dnsAnalyticsAdaptiveGroups(
|
||||
filter: { datetime_gt: $start, datetime_lt: $end }, limit: 1
|
||||
) { count }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## AI & Gateway Analytics
|
||||
|
||||
```graphql
|
||||
# Workers AI inference
|
||||
aiInferenceAdaptiveGroups(
|
||||
filter: { datetime_gt: $start, datetime_lt: $end }, limit: 100, orderBy: [datetimeHour_DESC]
|
||||
) {
|
||||
count
|
||||
sum { totalInputTokens totalOutputTokens totalRequestBytesIn }
|
||||
dimensions { modelId datetimeHour }
|
||||
}
|
||||
|
||||
# AI Gateway requests
|
||||
aiGatewayRequestsAdaptiveGroups(
|
||||
filter: { datetime_gt: $start, datetime_lt: $end }, limit: 100, orderBy: [datetimeHour_DESC]
|
||||
) {
|
||||
count
|
||||
dimensions { gateway provider model datetimeHour }
|
||||
sum { cachedTokensIn cachedTokensOut uncachedTokensIn uncachedTokensOut }
|
||||
}
|
||||
```
|
||||
|
||||
Both are account-scoped — nest under `accounts(filter: { accountTag: $accountTag })`.
|
||||
|
||||
## Best Practices
|
||||
|
||||
**Always include time filters.** Queries without time filters scan all data and are slow/expensive.
|
||||
|
||||
**Match time granularity to range:**
|
||||
|
||||
| Time Range | Recommended Dimension |
|
||||
|------------|----------------------|
|
||||
| < 6 hours | `datetimeMinute` or `datetimeFiveMinutes` |
|
||||
| 6-48 hours | `datetimeFiveMinutes` or `datetimeFifteenMinutes` |
|
||||
| 2-14 days | `datetimeHour` |
|
||||
| 14+ days | `date` |
|
||||
|
||||
**Use aliases** for querying the same dataset with different filters in one request.
|
||||
|
||||
**Request only needed fields.** Extra dimensions and metrics increase query cost.
|
||||
|
||||
## See Also
|
||||
|
||||
- [README.md](README.md) - Overview, decision tree, dataset index
|
||||
- [api.md](api.md) - Query structure, aggregation fields, filtering operators
|
||||
- [configuration.md](configuration.md) - Authentication, client setup, introspection queries
|
||||
- [gotchas.md](gotchas.md) - Rate limits, sampling, troubleshooting
|
||||
Reference in new issue
Block a user