Nothing
#' Why a domain produced no category
#'
#' Ported from the sibling Python package piedomains (`src/piedomains/outcomes.py`), and
#' the strings are **copied verbatim** so results from either package aggregate together.
#' They are part of the public output contract: safe to group on, and they will not change
#' meaning between releases.
#'
#' The problem this solves in rdomains: every failure used to collapse to `NA` plus a
#' warning, so "the host does not resolve", "we were rate-limited" and "this domain has no
#' category" were indistinguishable. Only the second is worth retrying, and a caller had no
#' way to tell which rows those were.
#'
#' @keywords internal
#' @noRd
NULL
#' Pipeline stage a domain reached
#' @keywords internal
#' @noRd
RD_STAGES <- c("validate", "fetch", "process", "infer")
#' The closed set of failure reasons
#'
#' `retryable` marks transient conditions. Two deliberate divergences from piedomains,
#' both because rdomains has no archive.org fallback to retry *into*:
#' * `bot_blocked` is not retryable here -- retrying from the same address is rudeness
#' with a guaranteed identical outcome.
#' * The archive-specific codes are absent.
#'
#' @keywords internal
#' @noRd
RD_ERROR_CODES <- tribble(
~code, ~stage, ~retryable, ~description,
"invalid_domain", "validate", FALSE, "Not a fetchable http(s) host.",
"robots_blocked", "validate", FALSE, "Disallowed by the host's robots.txt.",
"private_address", "validate", FALSE, "Host resolves to a non-global address.",
"dns_error", "fetch", TRUE, "Host did not resolve.",
"connection_error", "fetch", TRUE, "Connection refused or reset.",
"timeout", "fetch", TRUE, "Request exceeded the timeout.",
"http_error", "fetch", TRUE, "Non-2xx status.",
"content_type_rejected", "fetch", FALSE, "Response was not HTML.",
"content_too_large", "fetch", FALSE, "Body exceeded the byte cap.",
"bot_blocked", "fetch", FALSE, "An anti-bot interstitial was served.",
"too_many_redirects", "fetch", FALSE, "Redirect chain exceeded max_redirects.",
"empty_text", "process", FALSE, "No text could be extracted.",
"thin_content", "process", FALSE, "Below the token floor for an honest label.",
"service_unavailable", "infer", TRUE, "Classification service asleep or down.",
"rate_limited", "infer", TRUE, "Classification service rate-limited us.",
"service_error", "infer", TRUE, "Classification service returned an error.",
"unknown", NA, FALSE, "Unclassified failure."
)
#' Reasons a domain can fail to get a category
#'
#' The closed set of `error_code` values that [collect_content()] and [pie_cat()] can
#' return, with the pipeline stage each belongs to and whether it is worth retrying.
#'
#' These strings are shared verbatim with the Python package `piedomains`, so results from
#' the two can be aggregated together.
#'
#' @return A tibble with columns `code`, `stage`, `retryable` and `description`.
#'
#' @export
#' @seealso [fetch_report()] to summarise an actual run, [source_vintage()] for the
#' provenance of the static label sources.
#' @examples
#' fetch_error_codes()
#'
#' # which failures are worth trying again?
#' subset(fetch_error_codes(), retryable)
fetch_error_codes <- function() {
RD_ERROR_CODES
}
#' Summarise the outcome of a fetch or classification run
#'
#' A pure function over the returned tibble, deliberately **not** a stored summary. A
#' snapshot goes stale the moment you filter the rows; recomputing means
#' `fetch_report(subset(x, retryable))` is always correct.
#'
#' @param x A tibble from [collect_content()] or [pie_cat()].
#'
#' @return A one-row tibble: `n`, `ok`, `failed`, `retryable`, and list-columns
#' `by_stage`, `by_reason` and `missing` (the domains that produced nothing).
#'
#' @export
#' @examples
#' rows <- data.frame(
#' domain_name = c("a.com", "b.com", "c.com"),
#' status = c("ok", "failed", "failed"),
#' stage = c("infer", "fetch", "process"),
#' error_code = c(NA, "timeout", "thin_content"),
#' retryable = c(FALSE, TRUE, FALSE)
#' )
#' fetch_report(rows)
fetch_report <- function(x) {
if (!is.data.frame(x)) {
cli_abort("{.arg x} must be a data frame returned by {.fn collect_content}")
}
needed <- c("status", "stage", "error_code")
missing_cols <- setdiff(needed, names(x))
if (length(missing_cols)) {
cli_abort("{.arg x} is missing column{?s}: {.field {missing_cols}}")
}
# Named `is_failed`, not `failed`: tibble() evaluates its arguments sequentially and
# later ones see earlier ones, so a `failed = sum(failed)` column would shadow the mask
# and every subsequent subset would index with the count instead.
is_failed <- x$status %in% "failed"
tibble(
n = nrow(x),
ok = sum(x$status %in% "ok"),
failed = sum(is_failed),
retryable = if ("retryable" %in% names(x)) sum(x$retryable %in% TRUE) else NA_integer_,
by_stage = list(count_values(x$stage[is_failed])),
by_reason = list(count_values(x$error_code[is_failed])),
missing = list(
if ("domain_name" %in% names(x)) x$domain_name[is_failed] else character()
)
)
}
#' Tally a character vector, commonest first
#'
#' @param values character vector, `NA`s dropped
#' @return tibble of value and n
#' @keywords internal
#' @noRd
count_values <- function(values) {
values <- values[!is.na(values)]
if (!length(values)) {
return(tibble(value = character(), n = integer()))
}
counted <- sort(table(values), decreasing = TRUE)
tibble(value = names(counted), n = as.integer(counted))
}
#' Look up whether a code is retryable
#'
#' @param code error code, or NA
#' @return logical
#' @keywords internal
#' @noRd
is_retryable <- function(code) {
if (is.na(code)) {
return(FALSE)
}
hit <- RD_ERROR_CODES$retryable[match(code, RD_ERROR_CODES$code)]
isTRUE(hit)
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.