diff --git a/README.md b/README.md index 36b1980..0f39d68 100644 --- a/README.md +++ b/README.md @@ -305,6 +305,22 @@ curl -X DELETE https://your-worker.workers.dev/config | `cloudflare_zones_total` | gauge | - | | `cloudflare_zones_filtered` | gauge | - | | `cloudflare_zones_processed` | gauge | - | +| `cloudflare_zones_skipped_free_tier` | gauge | - | + +## Free Tier Zone Limitations + +Zones on Cloudflare's Free plan don't have access to the GraphQL Analytics API. The exporter automatically detects and skips free tier zones for metrics that require this API. + +**Free tier zones still export:** +- `cloudflare_zone_certificate_validation_status` (SSL certificates) +- `cloudflare_zone_lb_origin_weight` (Load balancer weights, if configured) + +**Monitor skipped zones:** +``` +cloudflare_zones_skipped_free_tier +``` + +For mixed accounts (enterprise + free zones), only free zones are skipped—paid zones continue to export all metrics. ## Architecture diff --git a/src/cloudflare/client.ts b/src/cloudflare/client.ts index 77234f9..f688336 100644 --- a/src/cloudflare/client.ts +++ b/src/cloudflare/client.ts @@ -840,8 +840,21 @@ export class CloudflareMetricsClient { result = await this.gql.query(HTTPMetricsQueryNoBots, queryVars); } + // Safety net: free tier zones should be filtered upstream, but handle gracefully + if (result.error?.message.includes("does not have access to the path")) { + this.logger.error( + "Zone(s) lack GraphQL analytics access - ensure free tier zones are filtered", + { error: result.error.message }, + ); + return []; + } + if (result.error) { - throw new Error(`GraphQL error: ${result.error.message}`); + throw new GraphQLError( + `GraphQL error: ${result.error.message}`, + result.error.graphQLErrors ?? [], + { context: { query: "http-metrics", zone_count: zoneIds.length } }, + ); } // Initialize all metric definitions diff --git a/src/cloudflare/queries.ts b/src/cloudflare/queries.ts index a8ad138..bb0e4a8 100644 --- a/src/cloudflare/queries.ts +++ b/src/cloudflare/queries.ts @@ -102,3 +102,38 @@ export const FREE_TIER_QUERIES = [ * Type for free tier query names. */ export type FreeTierQuery = (typeof FREE_TIER_QUERIES)[number]; + +/** + * Zone-level GraphQL queries that require paid tier. + * Free tier zones don't have access to adaptive analytics endpoints. + */ +export const PAID_TIER_GRAPHQL_QUERIES = [ + "http-metrics", + "adaptive-metrics", + "edge-country-metrics", + "colo-metrics", + "colo-error-metrics", + "request-method-metrics", + "health-check-metrics", + "load-balancer-metrics", + "logpush-zone", + "origin-status-metrics", + "cache-miss-metrics", +] as const; + +/** + * Type for paid tier GraphQL query names. + */ +export type PaidTierGraphQLQuery = (typeof PAID_TIER_GRAPHQL_QUERIES)[number]; + +/** + * Type guard for paid tier GraphQL queries. + * + * @param query Query name to check. + * @returns True if query requires paid tier. + */ +export function isPaidTierGraphQLQuery( + query: string, +): query is PaidTierGraphQLQuery { + return (PAID_TIER_GRAPHQL_QUERIES as readonly string[]).includes(query); +} diff --git a/src/components/LandingPage.tsx b/src/components/LandingPage.tsx index c5fbb79..64865f0 100644 --- a/src/components/LandingPage.tsx +++ b/src/components/LandingPage.tsx @@ -441,6 +441,7 @@ export const LandingPage: FC = ({ config }) => {

{/* Runtime Configuration Card */} + {!config.disableConfigApi && (
{/* Header with Save/Reset buttons */}
@@ -455,8 +456,9 @@ export const LandingPage: FC = ({ config }) => {
@@ -1121,6 +1123,7 @@ export const LandingPage: FC = ({ config }) => {
+ )} {/* Toast notification */}
{/* Live Metrics Section */}

Live Metrics

@@ -1184,7 +1187,7 @@ export const LandingPage: FC = ({ config }) => {
- + ); diff --git a/src/components/LandingPageScript.tsx b/src/components/LandingPageScript.tsx index c47c9cf..ca9b4c3 100644 --- a/src/components/LandingPageScript.tsx +++ b/src/components/LandingPageScript.tsx @@ -1,11 +1,14 @@ import { html } from "hono/html"; import type { FC } from "hono/jsx"; -type Props = { metricsPath: string }; +type Props = { metricsPath: string; disableConfigApi: boolean }; -export const LandingPageScript: FC = ({ metricsPath }) => { +export const LandingPageScript: FC = ({ metricsPath, disableConfigApi }) => { return html`