Analytics Cloudflare
Discovery
What discovery provides:
accounts: List of Cloudflare accounts withid,namezones: Zone details withid,name,status,accountId,accountName,plandataRetention: Retention windows by data type (DNS, HTTP, firewall, workers) based on plan tieravailableDatasets: GraphQL datasets withname,scope(account/zone),variableType(Date!/Time!),retentionqueryHints:criticalRequirement: Must callgraphql_zones_list()before DNS queriescorrectWorkflow: Step-by-step workflow for queriesvariableTypes: Which datasets useDate!vsTime!scopeRequirements: Which datasets are account-level vs zone-levelsafeDateRanges: Pre-calculated safe date ranges based on retention
Why run discovery:
- Get
accountIdrequired for DNS/Workers queries - Get
zoneIdrequired for HTTP/Firewall queries - Know data retention limits (7-62 days depending on plan)
- Understand correct variable types (Date! vs Time!)
- Avoid 0-record results from incorrect scope or context
Output Formatting:
Use format() for token-efficient output (40-60% token savings):
import { format } from "@connections/_utils/format";
console.log(format(result)); // CORRECT - Uses TOON encoding
// console.log(JSON.stringify(result, null, 2)); // WRONG - Wastes tokens
Critical Rules
🚨 MUST call graphql_zones_list({ name: 'domain.com' }) BEFORE querying DNS analytics
- Required to establish zone context
- Hardcoded account IDs → returns 0 records
- Without zone context → returns 0 records
🚨 CRITICAL: graphql_graphql_query Response Handling
The graphql_graphql_query tool returns a STRING containing:
- JSON response data
- A markdown link to GraphQL Explorer (appended after
\n\n)
Example response format:
{"data":{...},"errors":null}
**[Open in GraphQL Explorer](https://graphql.cloudflare.com/explorer?query=...)**
MANDATORY parsing pattern:
// ✅ CORRECT - Extract JSON before parsing
const resultStr = result as string;
const jsonPart = resultStr.split('\n\n')[0]; // Remove markdown link
const parsed = JSON.parse(jsonPart);
// ❌ WRONG - Parsing full string fails with "Unrecognized token '*'"
const parsed = JSON.parse(result as string); // Error!
Always check for GraphQL errors before accessing data:
if (parsed.errors && parsed.errors.length > 0) {
console.error('GraphQL Errors:', format(parsed.errors));
process.exit(1);
}
const data = parsed.data?.viewer?.accounts?.[0]?.dnsAnalyticsAdaptiveGroups || [];
Format requirements:
- DNS:
Date!format ("2026-01-27") - HTTP/Firewall/Workers:
Time!format ("2026-01-27T00:00:00Z")
Scope requirements:
- DNS & Workers: Account-level (
viewer.accounts) - HTTP & Firewall: Zone-level (
viewer.zones)
Retention (Free plan):
- DNS: 7 days only
- HTTP: 31 days
- Firewall/Workers: 30 days
Verified Working Example
import { graphql_zones_list, graphql_graphql_query } from '@connections/cloudflare';
import { format } from '@connections/_utils/format';
async function queryDNS() {
// Step 1: CRITICAL - Establish zone context
const zonesResult = await graphql_zones_list({ name: 'cloudthinker.io' });
const zones = typeof zonesResult === 'string' ? JSON.parse(zonesResult) : zonesResult;
const accountId = zones.zones[0].account.id;
// Step 2: Query with Date! format
const query = `query($accountTag: string!, $start: Date!, $end: Date!) {
viewer {
accounts(filter: { accountTag: $accountTag }) {
dnsAnalyticsAdaptiveGroups(
filter: { date_geq: $start, date_leq: $end }
limit: 1000
) {
count
dimensions { queryName queryType responseCode }
}
}
}
}`;
const result = await graphql_graphql_query({
query,
variables: { accountTag: accountId, start: '2026-01-27', end: '2026-01-28' }
});
// Step 3: CRITICAL - Extract JSON before parsing (remove markdown link)
const resultStr = result as string;
const jsonPart = resultStr.split('\n\n')[0];
const parsed = JSON.parse(jsonPart);
// Step 4: Check for GraphQL errors
if (parsed.errors && parsed.errors.length > 0) {
console.error('GraphQL Errors:', format(parsed.errors));
process.exit(1);
}
// Step 5: Access the data
const dnsData = parsed.data?.viewer?.accounts?.[0]?.dnsAnalyticsAdaptiveGroups || [];
if (dnsData.length === 0) {
console.log('No DNS data available for the specified period');
process.exit(0);
}
// Step 6: Display results with format() for token efficiency
console.log(format(dnsData));
}
queryDNS();
Query Templates
DNS Analytics (Account-Level)
// MUST establish zone context first
const zonesResult = await graphql_zones_list({ name: 'example.com' });
const zones = typeof zonesResult === 'string' ? JSON.parse(zonesResult) : zonesResult;
const accountTag = zones.zones[0].account.id;
const result = await graphql_graphql_query({
query: `query($accountTag: string!, $start: Date!, $end: Date!) {
viewer { accounts(filter: { accountTag: $accountTag }) {
dnsAnalyticsAdaptiveGroups(
filter: { date_geq: $start, date_leq: $end }
limit: 1000
) {
count
dimensions { queryName queryType responseCode zoneName }
}
}}
}`,
variables: {
accountTag,
start: '2026-01-27', // Date! format, within 7 days
end: '2026-01-28'
}
});
// CRITICAL: Extract JSON before parsing
const resultStr = result as string;
const jsonPart = resultStr.split('\n\n')[0];
const parsed = JSON.parse(jsonPart);
if (parsed.errors) {
console.error('Errors:', format(parsed.errors));
process.exit(1);
}
const dnsData = parsed.data?.viewer?.accounts?.[0]?.dnsAnalyticsAdaptiveGroups || [];
HTTP Traffic (Zone-Level)
const zonesResult = await graphql_zones_list({});
const zones = typeof zonesResult === 'string' ? JSON.parse(zonesResult) : zonesResult;
const zoneTag = zones.zones.find(z => z.name === 'example.com')?.id;
const result = await graphql_graphql_query({
query: `query($zoneTag: string!, $start: Time!, $end: Time!) {
viewer { zones(filter: { zoneTag: $zoneTag }) {
httpRequests1hGroups(
filter: { datetime_geq: $start, datetime_lt: $end }
limit: 24
) {
dimensions { datetime }
sum { requests bytes cachedRequests }
}
}}
}`,
variables: {
zoneTag,
start: '2026-01-10T00:00:00Z', // Time! format
end: '2026-01-11T00:00:00Z'
}
});
// CRITICAL: Extract JSON before parsing
const resultStr = result as string;
const jsonPart = resultStr.split('\n\n')[0];
const parsed = JSON.parse(jsonPart);
if (parsed.errors) {
console.error('Errors:', format(parsed.errors));
process.exit(1);
}
const httpData = parsed.data?.viewer?.zones?.[0]?.httpRequests1hGroups || [];
Firewall Events (Zone-Level)
const result = await graphql_graphql_query({
query: `query($zoneTag: string!, $start: Time!, $end: Time!) {
viewer { zones(filter: { zoneTag: $zoneTag }) {
firewallEventsAdaptive(
filter: { datetime_geq: $start, datetime_leq: $end }
limit: 100
) {
action clientCountryName clientIP clientRequestPath datetime
}
}}
}`,
variables: { zoneTag, start, end }
});
// CRITICAL: Extract JSON before parsing
const resultStr = result as string;
const jsonPart = resultStr.split('\n\n')[0];
const parsed = JSON.parse(jsonPart);
if (parsed.errors) {
console.error('Errors:', format(parsed.errors));
process.exit(1);
}
const firewallEvents = parsed.data?.viewer?.zones?.[0]?.firewallEventsAdaptive || [];
Workers Analytics (Account-Level)
const zonesResult = await graphql_zones_list({});
const zones = typeof zonesResult === 'string' ? JSON.parse(zonesResult) : zonesResult;
const accountTag = zones.zones[0].account.id;
const result = await graphql_graphql_query({
query: `query($accountTag: string!, $scriptName: string!, $start: Time!, $end: Time!) {
viewer { accounts(filter: { accountTag: $accountTag }) {
workersInvocationsAdaptive(
filter: { scriptName: $scriptName, datetime_geq: $start }
limit: 100
) {
dimensions { datetime scriptName status }
sum { requests errors }
}
}}
}`,
variables: { accountTag, scriptName, start, end }
});
// CRITICAL: Extract JSON before parsing
const resultStr = result as string;
const jsonPart = resultStr.split('\n\n')[0];
const parsed = JSON.parse(jsonPart);
if (parsed.errors) {
console.error('Errors:', format(parsed.errors));
process.exit(1);
}
const workersData = parsed.data?.viewer?.accounts?.[0]?.workersInvocationsAdaptive || [];
Common Workflows
Check DNS queries
- Filter zone:
graphql_zones_list({ name: 'example.com' }) - Parse result:
JSON.parse(zonesResult) - Extract account ID:
zones.zones[0].account.id - Query DNS with Date! format
- Parse response:
JSON.parse(result.split('\n\n')[0]) - Check errors, then access data
Check zone traffic
- Get zone ID:
graphql_zones_list({}) - Parse result:
JSON.parse(zonesResult) - Query HTTP with Time! format
- Parse response:
JSON.parse(result.split('\n\n')[0]) - Access:
parsed.data.viewer.zones[0].httpRequests1hGroups
Find blocked requests
- Get zone ID:
graphql_zones_list({}) - Parse result:
JSON.parse(zonesResult) - Query firewall events
- Parse response:
JSON.parse(result.split('\n\n')[0]) - Filter by
action: "block"
Troubleshooting
Common Errors
| Error | Cause | Fix |
|---|---|---|
JSON Parse error: Unrecognized token '*' |
Markdown link in response | Use result.split('\n\n')[0] before parsing |
| DNS returns 0 records | No zone context | MUST call graphql_zones_list({ name: 'domain.com' }) first |
| Type mismatch | Wrong variable type | DNS=Date!, others=Time! |
| HTTP returns 0 records | Wrong type or old dates | Use Time! + dates within 31 days |
Cannot read property 'data' of undefined |
Didn't parse response | Always JSON.parse(result.split('\n\n')[0]) |
Quick Diagnostic
DNS returns 0 records?
- 🚨 Did you call
graphql_zones_list({ name: 'domain.com' })FIRST?- ❌ Hardcoded account ID → 0 records
- ❌
graphql_zones_list({})without name → may return 0 records - ✅ MUST filter zone by name to establish context
- Using
Date!format (notTime!)? - Dates within 7-day retention window?
- Using
viewer.accounts(notviewer.zones)?
Still returns 0 records with correct workflow?
- Free/Pro plan API limitation (expected behavior)
- Dashboard shows data (uses internal APIs)
- Public GraphQL API has limited exposure on lower tiers
- Upgrade to Business/Enterprise for full API access
Reference
Available Datasets
| Dataset | Scope | Var Type | Retention (Free) |
|---|---|---|---|
dnsAnalyticsAdaptiveGroups |
Account | Date! |
7 days |
httpRequests1hGroups |
Zone | Time! |
31 days |
firewallEventsAdaptive |
Zone | Time! |
30 days |
workersInvocationsAdaptive |
Account | Time! |
30 days |
Filter Operators
| Operator | Example | Description |
|---|---|---|
_eq |
action_eq: "block" |
Equals |
_geq, _gte |
datetime_geq: $start |
Greater than or equal |
_leq, _lte |
datetime_leq: $end |
Less than or equal |
_in |
action_in: ["block", "challenge"] |
In list |
Response Handling Best Practices
The Complete Pattern
Every graphql_graphql_query call MUST follow this pattern:
import { graphql_graphql_query } from '@connections/cloudflare';
import { format } from '@connections/_utils/format';
try {
const result = await graphql_graphql_query({ query, variables });
// Step 1: Extract JSON portion (remove markdown link)
const resultStr = result as string;
const jsonPart = resultStr.split('\n\n')[0];
// Step 2: Parse JSON
const parsed = JSON.parse(jsonPart);
// Step 3: Check for GraphQL errors
if (parsed.errors && parsed.errors.length > 0) {
console.error('GraphQL Errors:');
console.error(format(parsed.errors));
process.exit(1);
}
// Step 4: Access data with fallback
const data = parsed.data?.viewer?.accounts?.[0]?.dnsAnalyticsAdaptiveGroups || [];
// Step 5: Handle empty results
if (data.length === 0) {
console.log('No data available for the specified period');
console.log('Note: Check date range, retention limits, and plan tier');
process.exit(0);
}
// Step 6: Display results with format() for token efficiency
console.log(format(data));
} catch (error) {
console.error('Query execution failed:', error);
process.exit(1);
}
Why This Pattern Is Mandatory
- String Response: Tool returns string, not object
- Markdown Appended:
\n\n**[Open in GraphQL Explorer](...)**breaks JSON.parse - GraphQL Errors: Can have
errorsarray even with 200 status - Empty Results: Valid response but no data (common with wrong context/dates)
- Token Efficiency: Use
format()instead ofJSON.stringify()
Quick Reference
// ✅ CORRECT
const resultStr = result as string;
const jsonPart = resultStr.split('\n\n')[0];
const parsed = JSON.parse(jsonPart);
// ❌ WRONG - Will fail with "Unrecognized token '*'"
const parsed = JSON.parse(result as string);
// ❌ WRONG - Assumes object response
const data = result.data?.viewer?.accounts;
// ✅ CORRECT - Check errors before accessing data
if (parsed.errors) {
console.error('Errors:', format(parsed.errors));
process.exit(1);
}
// ✅ CORRECT - Use format() for output
console.log(format(data));
// ❌ WRONG - Wastes tokens
console.log(JSON.stringify(data, null, 2));
Output Format
Present results as a structured report:
Analytics Cloudflare Report
═══════════════════════════
Resources discovered: [count]
Resource Status Key Metric Issues
──────────────────────────────────────────────
[name] [ok/warn] [value] [findings]
Summary: [total] resources | [ok] healthy | [warn] warnings | [crit] critical
Action Items: [list of prioritized findings]
Target ≤50 lines of output. Use tables for multi-resource comparisons.
Anti-Hallucination Rules
- NEVER assume resource names — always discover via CLI/API in Phase 1 before referencing in Phase 2.
- NEVER fabricate metric names or dimensions — verify against the service documentation or
--helpoutput. - NEVER mix CLI commands between service versions — confirm which version/API you are targeting.
- ALWAYS use the discovery → verify → analyze chain — every resource referenced must have been discovered first.
- ALWAYS handle empty results gracefully — an empty response is valid data, not an error to retry.
Counter-Rationalizations
| Shortcut | Counter | Why |
|---|---|---|
| "I'll skip discovery and check known resources" | Always run Phase 1 discovery first | Resource names change, new resources appear — assumed names cause errors |
| "The user only asked for a quick check" | Follow the full discovery → analysis flow | Quick checks miss critical issues; structured analysis catches silent failures |
| "Default configuration is probably fine" | Audit configuration explicitly | Defaults often leave logging, security, and optimization features disabled |
| "Metrics aren't needed for this" | Always check relevant metrics when available | API/CLI responses show current state; metrics reveal trends and intermittent issues |
| "I don't have access to that" | Try the command and report the actual error | Assumed permission failures prevent useful investigation; actual errors are informative |