Skip to content

fix(datadog): withhold failed secrets-operation text, which quotes secret names - #1806

Merged
dawsontoth merged 3 commits into
claude/1665-redact-error-causesfrom
claude/1664-secrets-relayed-error
Oct 12, 2026
Merged

dawsontoth merged 3 commits into
claude/1665-redact-error-causesfrom
claude/1664-secrets-relayed-error

Conversation

@dawsontoth

@dawsontoth dawsontoth commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

A squirrel archivist covering secret names before sending scrolls

Stacked on #1799 (studio#1665). The base is claude/1665-redact-error-causes, so this diff shows only the #1664 change. Merge #1799 first, then retarget this PR to stage.

⊙ Problem

A failed secrets operation (set, delete, grant or revoke) rejected with a plain Error carrying Harper's response text. React Query's global handler passes that to console.error, and the RUM SDK reports it, so Harper's text reached Error Tracking, where it is retained and searchable. Harper builds those messages by interpolating the secret's and component's names: No secret found with name '<name>', and Secret '<name>' cannot be both processEnv and grant-scoped (harper components/secretOperations.ts). When #1664 was filed, there were no such events in the previous 7 days of production. The exposure is latent, the same class #1768 fixed for import jobs.

Closes #1664.

❓ Your call: Are secret and component names sensitive enough to withhold the whole message? I think so. They are customer-chosen identifiers (ACME_STRIPE_KEY), and #1768 already set the precedent of withholding all of a relayed message rather than guessing where the quoted part ends. The cost is that Error Tracking can no longer tell "not found" from "processEnv conflict" for secrets. That is easy to reverse.

💡 Solution

This follows #1768's ImportJobError pattern:

Also changed: the secrets dialogs and the grants editor toasted String(error), which would now read SecretsOperationError: …. All five toasts now show describeError(error).message, the same text the global handler shows (SecretModals, SecretGrantsEditor). SecretModals is shared with the env-var editor, whose failures are raw AxiosErrors. Its toasts change too: they now show Harper's response text (or axios's own message when there is none) instead of AxiosError: Request failed with status code ….

❓ Your call: Is that env-var toast change acceptable in this PR? It is a side effect of fixing the shared dialog, and I think it is an improvement. The alternative is to keep String(error) for env vars and special-case the secrets class.

🔧 Changes

  • src/integrations/api/instance/secrets/secrets.ts: adds SecretsOperationError and operationError, which decides between Harper's text and the original failure, and calls them at the three throw sites (set, delete, grant/revoke).
  • src/integrations/datadog/redactRelayedMessage.ts: adds the type to RELAYED_ERROR_TYPES and documents why it belongs there.
  • src/features/instance/secrets/SecretModals.tsx (add-secret toast; the edit and delete toasts are the same one-line change) and src/features/instance/config/secrets/SecretGrantsEditor.tsx (grant toast, and revoke): the five toasts now show the message without the class name.

✅ Verification

  • End-to-end route: a SecretsOperationError goes through the installed SDK's real collection and assembly with Studio's beforeSend (beforeSend.sdk.test.ts). The event is sent with the message withheld, which proves the type beforeSend keys on is the one the SDK reads off the thrown class.
  • Unit tests:
  • Mutation checks: each of these turned at least one test red when reverted on its own: the set entry, each of the three throw sites, the name assignment, the pass-through, and the toast extractor. The grants-editor toasts use the same one-line extractor but have no test of their own.
  • Gates: tsc -b, oxlint and dprint check all exited 0. The pre-commit hook's full suite passed at the head commit (396 files, 3,694 tests). One intermediate commit (af418d6) was made with --no-verify. On that run the hook's full suite failed twice on two unrelated timeouts, editorApi.test.ts and NewApplication.test.tsx, while the machine's load average was about 86. Both files pass in isolation. I ran that commit's other hook steps (oxlint, staged dprint, commitlint) by hand, and they passed.

Cross-model review. Three rounds ran against base claude/1665-redact-error-causes, with codex and gemini in each. Codex's verdict on the final round was LGTM, with no findings. The domain adjudicator failed on auth (Claude OAuth expired), and Cursor failed when the 1Password SSH agent refused to sign. I triaged the findings myself:

  • Taken:
    • Pass non-Harper failures through instead of withholding them (codex minor and gemini major, which agreed).
    • Assert instanceof as well as name.
    • Prefer whichever body field is a string (gemini minor). Its premise was half right: the RUM SDK never sends an AxiosError's response body, so the old path did not leak, but the classification was wrong.
    • Cite the OSS harper path instead of the private harper-pro one.
    • Remove a narrating fixture comment.
  • Rejected:
    • That describeError might return no .message. It always returns all three fields, falling back to "We had some trouble!".
    • The comment nits.

🤖 Generated by Claude Code (Anthropic Claude); posted via @dawsontoth.

🤖 Generated with Claude Code

Related PRs: #1799 overlaps (this PR is stacked on it; same SDK test and relayed-type module), #1768 overlaps (merged; the ImportJobError pattern this mirrors), #1666 overlaps (merged; introduced the relayed-type withhold)
Complexity: medium

Review-Coverage: authored=claude; ran=gemini,codex; blocked=cursor-grok(no-receipt),cursor-composer(no-receipt),domain(auth); declined=cursor-kimi,cursor-muse; rounds=3; full=1 @ 8884dee

Review-Attention: study ~15m (sensitive: SecretGrantsEditor.tsx, SecretModals.tsx +1; raised: degraded review) @ 8884dee

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new SecretsOperationError class and updates error handling for secrets operations to ensure that sensitive secret names are redacted in Datadog. It also replaces generic error stringifications with describeError(error).message for user-facing toast notifications and adds corresponding tests. Feedback was provided regarding a potential sensitive data leak in operationError; if body.error is a structured object, it may fail to be wrapped in SecretsOperationError, thereby bypassing Datadog redaction. A suggestion was made to use a helper like errorText to safely extract the message.

Comment on lines +70 to 74
function operationError(error: unknown): unknown {
const body = (error as { response?: { data?: { error?: unknown; message?: unknown } } })?.response?.data;
const message = typeof body?.error === 'string' ? body.error : body?.message;
return typeof message === 'string' ? new SecretsOperationError(message) : error;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The current implementation of operationError only extracts the error message if body.error is a string, or falls back to body.message. However, if body.error is a structured object, it can fail to extract the message and return the original error unwrapped. This prevents the error from being classified as a SecretsOperationError, causing Datadog to skip redaction and potentially leak sensitive secret names. To prevent displaying generic error messages or rendering '[object Object]', we should use a shared helper (like errorText) to extract the nested string message or fall back to JSON stringification.

Suggested change
function operationError(error: unknown): unknown {
const body = (error as { response?: { data?: { error?: unknown; message?: unknown } } })?.response?.data;
const message = typeof body?.error === 'string' ? body.error : body?.message;
return typeof message === 'string' ? new SecretsOperationError(message) : error;
}
function operationError(error: unknown): unknown {
const body = (error as { response?: { data?: { error?: unknown; message?: unknown } } })?.response?.data;
const message = errorText(body?.error) ?? errorText(body?.message);
return message ? new SecretsOperationError(message) : error;
}
References
  1. When handling error messages that can be either strings or structured/nested objects, use a shared helper (like errorText) to extract the nested string message or fall back to JSON stringification. This prevents displaying generic error messages or rendering '[object Object]' in the UI.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declining. I checked the leak path and the response shape, and neither supports the finding:

  • Nothing leaks from the unwrapped error. The SDK reports a console.error(axiosError) as `${name}: ${message}`, which here is axios's own AxiosError: Request failed with status code N (@datadog/browser-core 7.15.0, cjs/domain/console/consoleObservable.js:66). computeRawError copies only the stack, cause chain, dd_fingerprint and dd_context (cjs/domain/error/error.js:39-66). The response body never reaches the event, so a secret name inside it can't either.
  • These operations don't send a structured error. Every failure in harper components/secretOperations.ts is a ClientError with a string message, and the operations error handler sends that back as { error: error.message } (harper server/serverHelpers/serverHandlers.js:83).
  • The suggestion would also break a case the PR already handles. errorText({ code: 'NOT_FOUND' }) returns that object's JSON, not undefined. So errorText(body?.error) ?? … would show the JSON instead of a string message next to it, which is the case 8884dee fixed and secrets.test.tsx "takes the string message when the error field is structured" tests.

🤖 Generated by Claude Code (Claude Opus 5.5); posted via @dawsontoth.

dawsontoth and others added 3 commits October 12, 2026 16:39
…cret names

A failed set/delete/grant/revoke secret rejected with a plain Error
carrying Harper's response text, and React Query's global handler passes
that to console.error, which the RUM SDK reports. Harper composes those
messages by interpolating the secret and component names
(harper-pro core/components/secretOperations.ts), so they reached Error
Tracking as written (#1664).

All three throw sites now throw a named SecretsOperationError, and that
name joins RELAYED_ERROR_TYPES, so beforeSend withholds the message and
the stack header the same way it does for SSEOperationError and
ImportJobError. The class carries no cause: under #1665 a cause on a
relayed error would withhold the whole event, losing the failure rate.

The secrets dialogs and grants editor toasted String(error), which would
now read "SecretsOperationError: ..."; they toast describeError's message
instead, as the global handler does. The shared SecretModals also serve
the env-var editor, whose toasts now show Harper's response text (or
axios's own message when there is none) rather than
"AxiosError: Request failed with status code ...".

Closes #1664

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Wrapping every rejection in SecretsOperationError also withheld
failures that carry no Harper text at all: a network error or a bug
reached Error Tracking only as the withheld placeholder. Only a response
body with Harper's message now becomes a SecretsOperationError; anything
else is rethrown unchanged, keeping its own type and message. That also
stops a structured body from turning into "[object Object]".

Cite the OSS harper path for the interpolated secrets messages.

Refs #1664

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ructured

A body with a structured `error` and a string `message` short-circuited
on the object and fell through to the raw AxiosError, so the string
never became a SecretsOperationError. Prefer whichever field is a string.

Refs #1664

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dawsontoth
dawsontoth added this pull request to stack #1921 October 12, 2026 20:40
@dawsontoth
dawsontoth marked this pull request as ready for review October 12, 2026 20:40
@dawsontoth
dawsontoth requested a review from a team as a code owner October 12, 2026 20:40
@dawsontoth
dawsontoth force-pushed the claude/1664-secrets-relayed-error branch from 8884dee to 9d5fe20 Compare October 12, 2026 20:42
@dawsontoth
dawsontoth merged commit 9d5fe20 into stage Oct 12, 2026
@dawsontoth
dawsontoth deleted the claude/1664-secrets-relayed-error branch October 12, 2026 20:42
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 73.35% 12161 / 16578
🔵 Statements 73.44% 12953 / 17637
🔵 Functions 68.35% 3240 / 4740
🔵 Branches 69.07% 9576 / 13864
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/features/instance/config/secrets/SecretGrantsEditor.tsx 0% 0% 0% 0% 30-74
src/features/instance/secrets/SecretModals.tsx 91.66% 94.11% 92.3% 91.66% 128-130, 285, 304
src/integrations/api/instance/secrets/secrets.ts 90.9% 88.23% 85% 90.47% 133-140
src/integrations/datadog/redactRelayedMessage.ts 93.75% 92.85% 100% 93.75% 76
Generated in workflow #2282 for commit 9d5fe20 by the Vitest Coverage Report Action

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[RUM] Secrets operation failures publish Harper's composed error text to Error Tracking

1 participant