Skip to content

feat(sync_report): emit failure_class on ResourceOutcome for HTTP errors#587

Merged
nathantournant merged 1 commit into
mainfrom
nathan.tournant/failure-class-on-outcomes
Jun 3, 2026
Merged

feat(sync_report): emit failure_class on ResourceOutcome for HTTP errors#587
nathantournant merged 1 commit into
mainfrom
nathan.tournant/failure-class-on-outcomes

Conversation

@nathantournant

@nathantournant nathantournant commented Jun 3, 2026

Copy link
Copy Markdown
Member

Summary

Extends ResourceOutcome with an optional failure_class field that
carries a structured error taxonomy for HTTP failures. _sanitize_reason
is updated to return a (reason, failure_class) tuple, and every emit
call site forwards both values through. The field is omitempty
absent from the NDJSON record when empty — preserving backward
compatibility for all existing consumers.

Motivation

Today the only signal an NDJSON consumer has for why an outcome failed
is the freetext reason string ("HTTP 500", "HTTP 403",
"TimeoutError", etc.). Consumers that want to count, alert, or branch
on failure modes must pattern-match the string, which is fragile and
duplicates classification logic that sync-cli already has at exception
time.

This PR exposes that classification directly as a structured enum on
the outcome record, so consumers can route on failure_class without
parsing.

Schema change

New optional field on type:"outcome" NDJSON records:

{"type": "outcome", "resource_type": "...", "status": "failure",
 "reason": "HTTP 403", "failure_class": "http_4xx_403"}

The field is absent when empty (omitempty). Success / skip / filtered
outcomes never set failure_class.

Canonical values

Python exception reason failure_class
CustomClientHTTPError(403) HTTP 403 http_4xx_403
CustomClientHTTPError(404) HTTP 404 http_4xx_404
CustomClientHTTPError(429) HTTP 429 http_4xx_429
CustomClientHTTPError(4xx) HTTP <code> http_4xx_other
CustomClientHTTPError(5xx) HTTP <code> http_5xx
TimeoutError TimeoutError http_timeout
ResourceConnectionError connection_error http_connection

Generic Exception subclasses produce (type(err).__name__, "unknown"),
unchanged from current behavior on the reason side.

Backward compatibility

  • The reason string format is unchanged — every value sync-cli emits
    today is still emitted in the same shape.
  • The new failure_class field is omitempty, so consumers using
    json.loads and ignoring unknown keys see no schema change when the
    field is absent.
  • Consumers that want to adopt the new field can do so incrementally;
    no upgrade lockstep is required.

Test plan

  • tox -e py312 -- tests/unit/ — 675 passed (existing + 30 new).
  • tox -e ruff — clean.
  • New file tests/unit/test_failure_class.py covers:
    • Every branch of _sanitize_reason (HTTP 403/404/429, 4xx-other,
      5xx, TimeoutError, ResourceConnectionError, generic Exception).
    • omitempty round-trips: field absent when empty, present when set.
    • _emit wiring: both tuple values forwarded to NDJSON output.
    • The dedicated inline except TimeoutError: branch in
      _import_get_resources_cb (verified consistent with
      _sanitize_reason).

Out of scope

  • The freetext reason string format is preserved as-is for backward
    compatibility; classification is purely additive.
  • No version bump in this PR.

Extend ResourceOutcome with an optional failure_class field that carries
a structured error taxonomy. _sanitize_reason returns a (reason,
failure_class) tuple; every emit call site forwards both values through.
The field is omitempty in to_dict() so consumers that don't know it see
no schema change.

Today the only signal an NDJSON consumer has for why an outcome failed
is the freetext reason string ("HTTP 500", "HTTP 403", "TimeoutError",
...). Consumers that want to count, alert, or branch on failure modes
must pattern-match the string, which is fragile and duplicates
classification logic that sync-cli already has at exception time. This
PR exposes that classification directly as a structured enum on the
outcome record.

Schema change (additive, optional):

    {"type": "outcome", "resource_type": "...", "status": "failure",
     "reason": "HTTP 403", "failure_class": "http_4xx_403"}

Canonical failure_class values:
    http_4xx_403   CustomClientHTTPError(403)
    http_4xx_404   CustomClientHTTPError(404)
    http_4xx_429   CustomClientHTTPError(429)
    http_4xx_other CustomClientHTTPError(other 4xx)
    http_5xx       CustomClientHTTPError(5xx)
    http_timeout   TimeoutError
    http_connection ResourceConnectionError
    unknown        generic Exception fallback

The reason string format is unchanged — every value sync-cli emits today
is still emitted in the same shape, so consumers that don't adopt the
new field see no behavioral change.

Tests: tests/unit/test_failure_class.py covers every _sanitize_reason
branch, omitempty round-trips on to_dict, JSON serialisation, and the
_emit call-site wiring including the dedicated inline except TimeoutError
branch in _import_get_resources_cb.
@nathantournant
nathantournant force-pushed the nathan.tournant/failure-class-on-outcomes branch from b45d869 to 54dba13 Compare June 3, 2026 15:01
@nathantournant
nathantournant marked this pull request as ready for review June 3, 2026 15:09
@nathantournant
nathantournant requested a review from a team as a code owner June 3, 2026 15:09
@datadog-official

Copy link
Copy Markdown

Pipelines

Fix all issues with BitsAI

⚠️ Warnings

🚦 1 Pipeline job failed

Ensure labels | changelog   View in Datadog   GitHub Actions

See error Missing required label 'changelog/*' during changelog validation.

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 54dba13 | Docs | Datadog PR Page | Give us feedback!

@nathantournant
nathantournant merged commit 89b9898 into main Jun 3, 2026
21 of 22 checks passed
@nathantournant
nathantournant deleted the nathan.tournant/failure-class-on-outcomes branch June 3, 2026 15:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants