# 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
