Back to skills
SKILL.md
Analytics Cloudflare
ASecurityCloudflare GraphQL Analytics for zone traffic, firewall events, Workers metrics, and schema exploration. Use when querying Cloudflare analytics data or exploring the GraphQL API.
- 6 stars
- 0 votes
- 0 copies
- 3 views
- Added September 8, 2026
Works with
Security analysis
100/100Pro scans all 2 files and shows the line behind each finding
npx -y skills add cloudthinker-ai/CloudSkills --skill analytics-cloudflare --agent claude-codeAre you the author of Analytics Cloudflare?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/cloudthinker-ai-analytics-cloudflare)---
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 |
| 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?**
1. π¨ 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
2. Using `Date!` format (not `Time!`)?
3. Dates within 7-day retention window?
4. Using `viewer.accounts` (not `viewer.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:
```typescript
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
1. **String Response**: Tool returns string, not object
2. **Markdown Appended**: `\n\n**[Open in GraphQL Explorer](...)**` breaks JSON.parse
3. **GraphQL Errors**: Can have `errors` array even with 200 status
4. **Empty Results**: Valid response but no data (common with wrong context/dates)
5. **Token Efficiency**: Use `format()` instead of `JSON.stringify()`
### Quick Reference
```typescript
// β
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
1. **NEVER assume resource names** β always discover via CLI/API in Phase 1 before referencing in Phase 2.
2. **NEVER fabricate metric names or dimensions** β verify against the service documentation or `--help` output.
3. **NEVER mix CLI commands between service versions** β confirm which version/API you are targeting.
4. **ALWAYS use the discovery β verify β analyze chain** β every resource referenced must have been discovered first.
5. **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 |
Files in this skill
- SKILL.md
- scripts/discover.ts
Attribution
Comments
Loading commentsβ¦