- name
- analytics-cloudflare
- description
- Cloudflare GraphQL Analytics for zone traffic, firewall events, Workers metrics, and schema exploration. Use when querying Cloudflare analytics data or exploring the GraphQL API.
- connection_type
- cloudflare
- preload
- false
# Analytics Cloudflare
## Discovery
<critical>
**If no `[cached_from_skill:analytics-cloudflare:discover]` context exists, run discovery first:**
```bash
bun run ./_skills/connections/cloudflare/analytics-cloudflare/scripts/discover.ts
bun run ./_skills/connections/cloudflare/analytics-cloudflare/scripts/discover.ts --max-zones 5
bun run ./_skills/connections/cloudflare/analytics-cloudflare/scripts/discover.ts --zone example.com
```
Output is auto-cached.
</critical>
**What discovery provides:**
- `accounts`: List of Cloudflare accounts with `id`, `name`
- `zones`: Zone details with `id`, `name`, `status`, `accountId`, `accountName`, `plan`
- `dataRetention`: Retention windows by data type (DNS, HTTP, firewall, workers) based on plan tier
- `availableDatasets`: GraphQL datasets with `name`, `scope` (account/zone), `variableType` (Date!/Time!), `retention`
- `queryHints`:
- `criticalRequirement`: Must call `graphql_zones_list()` before DNS queries
- `correctWorkflow`: Step-by-step workflow for queries
- `variableTypes`: Which datasets use `Date!` vs `Time!`
- `scopeRequirements`: Which datasets are account-level vs zone-level
- `safeDateRanges`: Pre-calculated safe date ranges based on retention
**Why run discovery:**
- Get `accountId` required for DNS/Workers queries
- Get `zoneId` required 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):
```typescript
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:
1. JSON response data
2. 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:**
```typescript
// ✅ 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:**
```typescript
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
```typescript
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)
```typescript
// 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)
```typescript
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)
```typescript
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)
```typescript
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
1. Filter zone: `graphql_zones_list({ name: 'example.com' })`
2. Parse result: `JSON.parse(zonesResult)`
3. Extract account ID: `zones.zones[0].account.id`
4. Query DNS with Date! format
5. **Parse response**: `JSON.parse(result.split('\n\n')[0])`
6. Check errors, then access data
### Check zone traffic
1. Get zone ID: `graphql_zones_list({})`
2. Parse result: `JSON.parse(zonesResult)`
3. Query HTTP with Time! format
4. **Parse response**: `JSON.parse(result.split('\n\n')[0])`
5. Access: `parsed.data.viewer.zones[0].httpRequests1hGroups`
### Find blocked requests
1. Get zone ID: `graphql_zones_list({})`
2. Parse result: `JSON.parse(zonesResult)`
3. Query firewall events
4. **Parse response**: `JSON.parse(result.split('\n\n')[0])`
5. 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 |
View on GitHub