Add monitor mode: report violations without rejecting requests - #5
Open
VSN2015 wants to merge 1 commit into
Open
Add monitor mode: report violations without rejecting requests#5VSN2015 wants to merge 1 commit into
VSN2015 wants to merge 1 commit into
Conversation
Adopting contracts on a live API used to mean flipping unknown clients from "accepted" to "422" in a single deploy. mode: :monitor runs the full pipeline but reports violations instead of rejecting: the same invalid_parameters.permittable event fires (payload mode: :monitor), the logger warns, and permitted_params returns the raw params passed through untouched. Monitor rules validate eagerly in the before_action regardless of enforce:, so telemetry never depends on the action calling permitted_params. Permittable.mode sets the app-wide default; a rule's own mode: wins in both directions. permittable_violations reads the recorded details, and exported OpenAPI operations carry x-permittable-mode: "monitor" so the docs don't promise a 422 the server doesn't yet send. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Almost no Rails API is greenfield. The apps that need typed params contracts most are years-old monoliths with unknown clients in the wild — old mobile app versions, third-party integrations, forgotten cron jobs. Adopting Permittable (or even tightening one field on an existing contract) meant flipping live traffic from "accepted" to "422" in a single deploy, with no way to know what breaks until it breaks. So the safe choice was to never adopt.
This PR adds the same escape hatch browsers invented for CSP (
Content-Security-Policy-Report-Only): monitor mode. The full pipeline runs — unwrap, cast, validate, defaults — but a violation is reported instead of rejected, and the request proceeds exactly as it did before the contract existed. You can't enforce what you haven't measured; now you can measure first.How to use it
Per contract
App-wide, from an initializer
A rule's own
mode:always beats the global, in both directions — so you can monitor app-wide and pin finished controllers tomode: :enforceone at a time, or enforce app-wide and monitor just the contract you're tightening.Dashboard the would-be rejections
The existing
invalid_parameters.permittableevent now carriesmode:in its payload (:monitor/:enforce), so one subscriber covers both:The logger also warns on each monitored violation with the offending paths:
Read the verdict in the action or in tests
Under enforce mode it swallows its own trigger's raise, which makes "would this request fail?" a one-liner in request specs.
The rollout recipe
PERMITTABLE_MODE=monitor. Behaviour is unchanged; telemetry starts.Behaviour on a violating request in monitor mode
permitted_paramsreturns the raw pass-through: exactly what the client sent — no casts, no defaults, no transforms — so behaviour is byte-for-byte the pre-contract app. A missingroot:passes an empty hash (the envelope you asked for isn't there); a rootless contract drops only Rails' routing keys, mirroring their exemption from the unknown-keys check.before_actionregardless ofenforce:— telemetry must not depend on the action callingpermitted_params, since legacy actions still readingparamsdirectly are exactly the ones worth monitoring. Results stay memoized per action, so nothing validates or instruments twice.x-permittable-mode: "monitor"— the docs must not promise a 422 the server doesn't yet send. Only the per-rule declaration is exported; the globalPermittable.modeis runtime configuration, not contract data.Design notes
modeis just a fourth reader decision at the raise-vs-report branch point; the precedents (unknown: :ignore/:log/:error,enforce:, the notification event) were already in place.mode:raises at class load;Permittable.mode =rejects invalid values at assignment.:enforce, and the enforce path behaves exactly as before. The only observable change for existing apps is the newmode: :enforcekey on the notification payload.before_action, monitor validation stays lazy — documented).Testing
Stacked on #3
Based on
feat/openapi-exportbecause thex-permittable-modeexport marker touchesPermittable::OpenAPI. Merge #3 first, then re-target this PR tomaster(GitHub does it automatically on merge).🤖 Generated with Claude Code