Cloudflare exposes two analytics surfaces that this package wraps:
| Surface | Best for | Wrapper |
|---|---|---|
| Web Analytics / RUM | Site catalogue (sites with the JavaScript beacon installed) | [cf_list_rum_sites()],
[cf_get_rum_site()] |
| GraphQL Analytics | Everything else – HTTP requests, bandwidth, page views, uniques, firewall events, Workers, R2, DNS | [cf_graphql()] |
The legacy REST Zone Analytics endpoints
(/zones/{id}/analytics/dashboard and
/analytics/colos) were retired by Cloudflare in favour of
the GraphQL API, so this package does not wrap them. Every example below
uses GraphQL where the legacy REST call used to live.
The GraphQL schema explorer is the fastest way to discover the right node, filter, and dimension for any analytics question.
Cloudflare Web Analytics is the free real-user-monitoring product that uses a small JavaScript beacon – it works on any site, not just proxied ones. List the sites configured in an account with:
accounts <- cf_list_accounts(max_pages = 1)
account_id <- accounts[[1]]$id
sites <- cf_list_rum_sites(account_id)
sites[[1]]$host
sites[[1]]$site_tag
cf_get_rum_site(account_id, sites[[1]]$site_tag)For per-site metrics (page views, visits, country breakdowns, …),
query the GraphQL API with the site’s site_tag. See the Top countries
example below.
[cf_graphql()] accepts the query as a string and
variables as named ... arguments. The full response body is
returned, so you access fields under $data$....
query <- "
query Viewer($accountTag: String!) {
viewer {
accounts(filter: { accountTag: $accountTag }) {
accountTag
}
}
}
"
res <- cf_graphql(query, accountTag = account_id)
res$data$viewer$accountsTwo things worth knowing about Cloudflare’s GraphQL
viewer:
viewer { accounts { ... } } query
enumerates every account the credential might touch and fails with
not authorized for that account if any one of them is out
of scope. Always supply a filter with the
accountTag you care about.Account type only exposes accountTag.
The human-readable name lives on the REST /accounts
endpoint, which you already have from
[cf_list_accounts()].zones <- cf_list_zones(max_pages = 1)
zone_id <- zones[[1]]$id
query <- "
query ZoneRequests($zoneTag: String!, $since: Date!, $until: Date!) {
viewer {
zones(filter: { zoneTag: $zoneTag }) {
httpRequests1dGroups(
limit: 100,
filter: { date_geq: $since, date_lt: $until },
orderBy: [date_ASC]
) {
dimensions { date }
sum {
requests
bytes
pageViews
threats
}
uniq { uniques }
}
}
}
}
"
res <- cf_graphql(
query,
zoneTag = zone_id,
since = format(Sys.Date() - 7, "%Y-%m-%d"),
until = format(Sys.Date(), "%Y-%m-%d")
)
days <- res$data$viewer$zones[[1]]$httpRequests1dGroups
data.frame(
date = vapply(days, function(d) d$dimensions$date, character(1)),
requests = vapply(days, function(d) d$sum$requests, integer(1)),
bytes = vapply(days, function(d) d$sum$bytes, numeric(1)),
pageviews = vapply(days, function(d) d$sum$pageViews, integer(1)),
uniques = vapply(days, function(d) d$uniq$uniques, integer(1))
)httpRequests1hGroups (hourly bins) and
httpRequestsAdaptiveGroups (sampled adaptive bins) follow
the same shape.
query <- "
query RumByCountry($accountTag: String!, $siteTag: String!,
$since: Time!, $until: Time!) {
viewer {
accounts(filter: { accountTag: $accountTag }) {
rumPageloadEventsAdaptiveGroups(
limit: 25,
filter: {
siteTag: $siteTag,
datetime_geq: $since,
datetime_lt: $until
},
orderBy: [count_DESC]
) {
count
dimensions { countryName }
}
}
}
}
"
res <- cf_graphql(
query,
accountTag = account_id,
siteTag = sites[[1]]$site_tag,
since = format(Sys.Date() - 7, "%Y-%m-%dT00:00:00Z"),
until = format(Sys.Date(), "%Y-%m-%dT00:00:00Z")
)Both REST and GraphQL paths raise a classed
cloudflarer_error on failure, so you can wrap calls in
tryCatch():
tryCatch(
cf_graphql("{ broken }"),
cloudflarer_error = function(err) {
message("Cloudflare said: ", conditionMessage(err))
NULL
}
)For GraphQL specifically, any non-empty errors[] array
in the response is treated as a failure, even when the HTTP status is
200 (which is the standard GraphQL convention).
For the most common analytics questions, cloudflarer ships ready-made
data.frame wrappers on top of cf_graphql(). Reach for these
first; drop down to cf_graphql() only when you need a
dimension or filter they do not expose.
Columns: date, requests,
cached_requests, bytes,
cached_bytes, request_hit_ratio,
bandwidth_hit_ratio.
Available on Pro, Business, and Enterprise plans
only – the underlying firewallEventsAdaptiveGroups GraphQL
node is not exposed on the Free plan and returns
zone ... does not have access to the path.
cf_firewall_events_by_day(zone_id, Sys.Date() - 7, Sys.Date())
cf_firewall_events_top(
zone_id,
since = Sys.Date() - 7,
until = Sys.Date(),
dimension = "action",
limit = 10
)Other useful cf_firewall_events_top() dimensions:
"source", "ruleId",
"clientCountryName", "clientRequestPath",
"clientRequestHTTPHost", "userAgent".
cf_zone_requests() (or
httpRequestsAdaptiveGroups directly via
cf_graphql() for sub-hour granularity).cf_cache_ratio().cf_dns_queries().cf_firewall_events_by_day() /
cf_firewall_events_top().cf_rum_page_views() / cf_rum_top().cf_graphql()
directly. The GraphQL
schema explorer is the fastest way to find the right node and
dimensions.