cli: reclassify parse/read failures, add source/http_status telemetry, reserve cli_ code prefix#208
Conversation
b48e59d to
c9034d5
Compare
26f1223 to
a010fc9
Compare
c375490 to
406a00e
Compare
c42c4de to
08254f1
Compare
406a00e to
61f30c8
Compare
08254f1 to
87b9f59
Compare
61f30c8 to
cc750c0
Compare
87b9f59 to
2291775
Compare
cc750c0 to
6d8bc96
Compare
2291775 to
5d905a5
Compare
Reclassify the remaining opaque `error` sites: a mid-response read failure -> network_error; JSON-parse and poll field-extraction failures -> response_parse_error. Capture the request id from x-amzn-request-id / x-amzn-trace-id (the app does not set X-Request-Id; the ALB does), falling back to X-Request-Id. Add telemetry-only origin signals to COMMAND_RUN_COMPLETE: source (api = from the API envelope, cli = CLI-generated; defaults to cli) and http_status (upstream status when known). Both are omitted on success and are NOT in the user-facing error envelope. Dashboards can then group by (source, error_code) collision-safely and separate an app 5xx from a no-envelope 5xx via source. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Namespace the CLI-originated response-parse code as cli_response_parse_error, add a central registry of all CLI-minted codes, and a test that scans source and fails on any unregistered or bare-but-not-allowlisted CLI code, forcing the cli_ prefix on future codes so they can never collide with an API code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6d8bc96 to
8f105ae
Compare
The x-amzn-request-id/trace-id fallback populated request_id, but api_service does not log those ids, so there is no working server-side correlation path for a caller to use. Revert to reading only X-Request-Id (which the app doesn't set today, so request_id stays omitted) rather than surfacing an id nobody can trace. The request_id envelope field is kept so it populates for free once the backend sets/logs a real request id. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-code note Codex nits on the cli_ namespacing: the source-scan regex missed the 'code :=' short-declaration form (a bare CLI code declared that way would evade the enforcement test), and the README said every bare code carries API semantics, omitting the grandfathered-legacy-CLI exception. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Reviewed at 6c4f744.
Solid finish to the error-handling stack. The reserved cli_ prefix + registry-with-source-scan is a clean way to prevent reverse collisions, the source/http_status telemetry split gives dashboards a clean origin axis, and the reclassifications (network_error for mid-body drops, cli_response_parse_error for parse/extract failures) fit the taxonomy well. Bare/grandfathered set spot-checks all clean, all six registered cli_* codes are used, and the added regex-form test locks the scan contract. LGTM from my side — leaving as a comment with a few observations inline.
Concerns
- 🟡
http_statusis populated for API-origin errors viaFromAPIErrorbut not on the CLI-transport paths that already know the status (mid-body read atinternal/client/executor.go:226-231, presigned-URL failures atcmd/heygen/video_download.go:220-231). Dashboards can't distinguish an expired-URL 403 from a 404, or an asset-host 500 from a 502. Trivially fixable — see inline. - 🟡 The enforcement-test regex in
internal/errors/codes_test.gocouples to the literal identifiercode; a future author writingerrCode := "cli_foo"would evade the scan. See inline for a suggestion.
Questions
- 🟡 Is the
cli_prefix reservation enforced on the API side? The design guarantee ("a new CLI code can never collide with an API code") is airtight only if the API also refuses to mintcli_*codes. See inline.
Nits
- Small hardcode in
cmd/heygen/main.go:50could move intoclassifyError. See inline.
What I did not verify
- API-side enforcement of
cli_*prefix reservation (noapi_servicerepo access from this workspace). - The PostHog dashboard
(source, error_code)grouping change referenced in the PR body — deferred by design, tracked separately. - Pre-existing
govulncheckfailure (golang.org/x/crypto/openpgpviaselfupdate+crypto/tls@go1.25.11) is onmainalready and unrelated to this diff.
| return nil, clierrors.New(fmt.Sprintf("failed to read response body: %v", err)) | ||
| // Mid-response read failure is a dropped connection, not a parse issue. | ||
| return nil, &clierrors.CLIError{ | ||
| Code: "network_error", |
There was a problem hiding this comment.
🟡 The mid-body-read failure now emits network_error (good), but resp.StatusCode is already known at this point (from lines 220-222) — yet HTTPStatus isn't populated on the CLIError. Dashboards using the (source, http_status) axis will see this as source=cli, http_status=0, indistinguishable from the pre-response transport failure four lines up (line 214-219) where the status genuinely isn't known.
Same shape in cmd/heygen/video_download.go:220-231: cli_download_url_expired (403/404) and cli_download_failed (other 4xx/5xx from the asset host) both have resp.StatusCode in scope but don't set it. The PR body says the design captures "the upstream status when known" — and it IS known at all three sites.
Suggestion: add HTTPStatus: resp.StatusCode on the three sites that have a resp in scope (this file 226-231, and video_download.go 220-231 x2). Zero-cost, completes the origin-axis telemetry design.
|
|
||
| // CLICodePrefix is reserved for CLI-originated error codes. The API must never | ||
| // emit a code beginning with this prefix. | ||
| const CLICodePrefix = "cli_" |
There was a problem hiding this comment.
🟡 Question: is the cli_ prefix reservation actually enforced on the API side? The design guarantee — "a new CLI code can never collide with an API code" — is airtight only if the API also refuses to mint cli_* codes. The PR body notes "provided the API never mints a cli_-prefixed code (enforced API-side)"; is there a symmetric check server-side (e.g. a test that scans the API's error registry for the reserved prefix), or is it convention-only right now?
Not blocking (this PR is the CLI half), but worth confirming the invariant is enforced end-to-end so the reservation is more than aspirational — otherwise an API engineer could accidentally ship cli_foo and produce exactly the reverse collision this PR is designed against.
| // synthesize a code). The `:?=` covers both `=` and `:=` without matching the | ||
| // `==` comparison in isNonSpecific. API codes relayed from a response come from | ||
| // a variable (apiErr.Code), not a literal, so they do not match. | ||
| var codeLiteral = regexp.MustCompile(`(?:\bCode:\s*|\bcode\s*:?=\s*)"([a-z][a-z0-9_]*)"`) |
There was a problem hiding this comment.
🟡 The regex catches Code: (struct field) and the local variable name code, which matches the current codebase convention. But if a future author writes errCode := "cli_foo" / myCode = "cli_bar" — or embeds a code literal in a slice/map/switch initializer — the enforcement test silently misses.
Two ways to harden:
-
Invert the invariant: scan any non-
_test.gofile for string literals matching^cli_[a-z0-9_]*$and fail if not incliPrefixedCodes. Catches everycli_*literal regardless of syntactic context. False-positive risk on comments/log strings mentioning a code by name — addressable via a small ignore-set or by requiring backtick-quoted code names in comments. -
Keep the current regex but widen the variable name to
[a-zA-Z_][a-zA-Z0-9_]*Code\s*:?=soerrCode/apiCodeetc. also match.
If the team is comfortable with the current "only Code: struct field or code local" convention, worth a docstring line near the regex spelling that out so future authors don't accidentally evade it. Not a blocker either way.
| formatter.Error(wrapped) | ||
| exitCode = wrapped.ExitCode | ||
| errorCode = wrapped.Code | ||
| source = "cli" |
There was a problem hiding this comment.
Nit: this hardcode duplicates the "empty-source-defaults-to-cli" logic four lines up. Could be moved into classifyError itself (set wrapped.Source = "cli" before returning) so the origin concern lives with the constructor, and the else-branch loses the special case entirely. Very minor.
…ert cli_ scan, tidy classifyError - Set HTTPStatus on the CLI-synthesized errors whose status is known: the mid-body-read network_error and the download url-expired/failed cases, so the telemetry (source, http_status) axis can tell a 403 from a 404 and a 500 from a 502 (previously http_status=0 there). Completes the origin-axis design. - Add an inverted registry scan: any cli_-prefixed string literal in production source must be a registered code (or an explicit non-code literal like the cli_version analytics key), closing the identifier-coupling gap where a code assigned via a non-'code' variable name would evade the Code:/code= scan. - Move the CLI-origin default into classifyError so main.go's wrapped-error branch no longer special-cases source. - Fix an em dash in the cli_download_failed hint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks — folded the three code items, filed the fourth: 🟡 🟡 enforcement-test identifier coupling — hardened. Added an inverted scan ( Nit: 🟡 Question: is Pushed as |
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Reviewed at 4a41a5b. R1 baseline was 6c4f744.
R2: all three code items landed cleanly. Enforcement-regex fix chose the stronger design — new TestAllCliLiteralsRegistered inverts the scan on the cli_ side, closing the identifier-coupling gap AND catching slice/map/switch initializer forms the old code-identifier scan couldn't reach. Verified all six registered cli_* codes still emit, and the sole non-code cli_ literal (cli_version at internal/analytics/analytics.go:126) is correctly allowlisted. http_status on the three transport sites populated with correct nil-safety (all inside resp != nil paths). source = "cli" folded into classifyError — sole prod caller is the Cobra-wrap path, which is always CLI-origin, so no misdirection risk. LGTM from my side — leaving as a comment with one small doc-tightening nit inline on codes.go.
Verified
- ✅
http_statuspopulated:internal/client/executor.go:233(mid-body readnetwork_error),cmd/heygen/video_download.go:224(cli_download_url_expired),cmd/heygen/video_download.go:232(cli_download_failed). All three sites are inside successfulresp != nilblocks, soresp.StatusCodeis always available; theresp == niltransport-fail case atexecutor.go:212correctly still reportshttp_status=0, and the new doc-comment on the mid-body read explains that distinction for future readers. - ✅
TestAllCliLiteralsRegistered(internal/errors/codes_test.go:186-217) — regex"(cli_[a-z0-9_]+)"requires whole-token match (opening + closing"), so mid-string substrings can't slip through. Skips_test.go. Walkscmd/+internal/. Confirmed the whitelist is minimal + correct: onlycli_version(PostHog property key). No fragile identifier coupling anywhere. - ✅
source = "cli"moved intoclassifyError(cmd/heygen/main.go:79). Only prod caller ofclassifyErrorismain.go:46, invoked in the Cobra-wrap branch (always CLI-origin) — no risk of a new caller inheriting a hardcoded default that shouldn't apply. CLIError branch retains theif source == "" { source = "cli" }fallback (main.go:39-41), so both paths agree on the default. - 🟡 API-side
cli_prefix enforcement — acknowledged as convention-only today; PRINFRA-251 tracks the EF-side symmetric check. See inline on the codes.go docstring — small doc-vs-reality nit.
CI
- All test jobs pass on ubuntu/macos/windows.
govulncheckremains the pre-existingx/crypto/openpgp(viaselfupdate) +crypto/tls@go1.25.11failure noted in R1 — unrelated to this diff. - Mergeable state is
behind(base has advanced); trivial rebase/merge.behind_by=0from R1 → HEAD means no rebase happened between rounds, so all prior verification still holds.
What I did not verify
- API-side enforcement of
cli_*prefix reservation still requires server-side work (PRINFRA-251). - The PostHog dashboard
(source, error_code)grouping change referenced in the PR body — deferred by design.
| // | ||
| // The cli_ prefix is reserved so a CLI code can never collide with a future API | ||
| // code, provided the API never mints a cli_-prefixed code (enforced API-side). | ||
| // codes_test.go scans the source and fails on any CLI-minted code that is either |
There was a problem hiding this comment.
🟡 Small doc-vs-reality tightening: the "(enforced API-side)" clause reads as if server-side enforcement is already in place, but per the PR comment it is convention-only today (PRINFRA-251 tracks the EF-side symmetric check). A future engineer reading this docstring alone might trust an end-to-end guarantee that doesn't yet exist. Consider:
// The cli_ prefix is reserved so a CLI code can never collide with a future API
// code, provided the API never mints a cli_-prefixed code — currently by
// convention; planned symmetric enforcement in EF tracked in PRINFRA-251.Small; not blocking. Feel free to defer to the follow-up PR that lands the EF check and doc together.
jrusso1020
left a comment
There was a problem hiding this comment.
Approving at head 4a41a5bf (matches RDJ's R2 head). Verified the R1 fixes at source:
- http_status gap —
HTTPStatus: resp.StatusCodenow populated on the transport sites (executor.go:224,video_download.go:224/232). - Enforcement-regex fragility — resolved with
TestAllCliLiteralsRegistered+ the whole-token"(cli_[a-z0-9_]+)"scan (codes_test.go:173/189), so any assignment shape is caught, not justcode := .... - source nit — folded into
classifyErrorper RDJ's R2.
Required checks green (test ×3 / lint / secrets / goreleaser);govulncheckred is non-required + pre-existing (x/crypto viaselfupdate+crypto/tls), same as #206/#207.mergeable_state=behindis a trivial rebase. Remaining item — thecodes.godocstring reading "enforced API-side" while it's convention-only (PRINFRA-251 pending EF-side enforcement) — is RDJ's non-blocking nit; fine to soften or defer. Clean finish to the stack. LGTM.
Scope
Surfaces: CLI (external v3 API) | Module: Error handling / telemetry
Final PR of the error-handling stack (#205, #206, #207 merged); now based on
main.Summary
Three things: (1) finishes reclassifying the opaque
errorbucket, the last CLI-side parse/read failures get specific codes; (2) introduces a reservedcli_prefix for CLI-originated error codes, with a registry and an enforcement test, so a CLI code can never collide with a current or future API code; (3) adds an internal origin signal (source+http_status) to telemetry so the error dashboards are collision-safe and can separate app failures from edge/CLI ones.How it works
Reclassification. The remaining generic
errorsites in the client get specific codes: a mid-response body-read failure becomesnetwork_error(a dropped connection, not a parse problem); JSON-parse and poll field-extraction failures becomecli_response_parse_error.Reserved
cli_prefix (the collision guarantee). Error codes share one flat namespace between the v3 API and the CLI, and origin is deliberately not in the user-facing envelope, so the API is authoritative. To prevent a reverse collision (the CLI ships a unique code, the API later mints the same name), CLI-originated codes now carry the reservedcli_prefix. A registry ininternal/errors/codes.gopartitions every CLI-minted code into three closed sets:cli_-prefixed (new), grandfathered-bare (codes already released bare, e.g.error/usage_error/network_error/timeout), and bare API-semantic (mirrors of API codes + theunclassified_*fallbacks). An enforcement test scans the source and fails on any CLI-minted code that is unregistered or bare-but-not-allowlisted, and pins the bare allowlists so they cannot silently grow. Because the API never emits acli_*code, every future CLI code is collision-proof by construction.Telemetry origin (
source+http_status).COMMAND_RUN_COMPLETEgains two properties on error:source(apiwhen the code came from an API response envelope, elsecli; defaults tocli) andhttp_status(the upstream status when known, omitted otherwise). Both are telemetry-only, not in the user-facing envelope. Dashboards group by(source, error_code), which is a second, independent guard against code-name collision in our metrics.request_id. Reads onlyX-Request-Id. Thex-amzn-*ALB fallback was intentionally dropped:api_servicedoes not log those ids, so there is no working server-side correlation path, and surfacing an untraceable id is worse than an empty one. Therequest_idenvelope field is kept (omit-when-empty), so it populates for free once the backend sets a real, traceable id.Design decisions
cli_for CLI-originated codes; released codes grandfathered bare. Prefixing is the only thing that prevents the reverse collision (a reserved namespace the API never uses) rather than merely detecting it. Released CLI codes stay bare (a frozen allowlist) to avoid a pure BC break, andunclassified_*stays bare because it labels an API/server fault, not a CLI condition, and the API will not mint an "unclassified" code. Zero BC: only previously-unreleased codes are prefixed.source/http_statuslive in telemetry, not the envelope. The agent acts oncode/message/hint/doc_url; whether an error originated in our CLI vs our API doesn't change remediation and would leak internal architecture. Dashboards are the consumer that needs origin, so it goes there.sourcedefaults tocli. OnlyFromAPIErrorsetsapi(and only for a preserved, specific API code); every other construction path is CLI-origin.Testing
Unit tests: the reclassified codes (
network_error,cli_response_parse_error); the registry partition, the source-scan that fails on any unregistered/wrongly-bare CLI code, and the frozen-allowlist check;FromAPIErrorsetssource/http_status(apifor a preserved code,clifor status-derived) and they are absent from the envelope; the analytics event carriessource/http_statusand omits them on success.make lintclean.Follow-up (not in this PR)
(source, error_code)and add the newcli_*codes to the server/client code-lists, a release-time PostHog change.cli_codes (and wiring theirdoc_url) is tracked in PRINFRA-247.