fix(sentry): cap request bodies at 1KB and scrub Postgres DETAIL rows - #54
Open
blarghmatey wants to merge 3 commits into
Open
fix(sentry): cap request bodies at 1KB and scrub Postgres DETAIL rows#54blarghmatey wants to merge 3 commits into
blarghmatey wants to merge 3 commits into
Conversation
Two ways learner data reaches Sentry, neither gated by send_default_pii. Request bodies. The SDK sets request.data unconditionally at sentry_sdk/integrations/_wsgi_common.py:123; max_request_body_size, checked at :61, is the only control, and left unset it defaults to "medium" -- 10,000-byte bodies. Nobody chose that. Measured over the last 30 days, the write endpoints that actually raise are the sensitive ones: SCIM user PATCH, /api/v1/enrollments/, /api/checkout/result/, /api/checkout/redeem_discount/, /api/profile/details/, and CMS page edits. Set to "small" explicitly, so the choice is findable at the call site instead of in a dependency's defaults. Postgres DETAIL lines. A constraint violation carries a DETAIL line that echoes the whole offending row, and psycopg puts it in str(exc) -- so it ships inside the exception value, which no SDK privacy option covers. Measured on mitxonline MITXONLINE-6PK: a SCIM PATCH IntegrityError reproducing a learner email address three times per event, 46,764 occurrences since 2026-05-27. before_send now truncates at the DETAIL marker across exception values, logentry, and the legacy top-level message, keeping the primary error that names the failure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RVJKTGk9KHUTN2xfU59ujX
This was referenced Sep 8, 2026
fix(sentry): set max_request_body_size to small and scrub Postgres DETAIL rows
mitodl/mit-learn#3915
Merged
Open
Open
Merged
…fields
Copilot review, verified against sentry-sdk 2.55.0 source. The first pass
enumerated three paths -- exception values, logentry.message/.formatted,
and the legacy top-level message -- and missed every other field that can
carry the same string:
breadcrumbs[].message LoggingIntegration records each log record as a
breadcrumb (integrations/logging.py:311). This is
exactly MITXONLINE-6PK's shape: mechanism=logging,
logger=django_scim.views.
logentry.params record.args verbatim (:274), so
logger.error("...: %s", exc) carries it.
frames[].vars include_local_variables defaults to True
(consts.py:1028, utils.py:616), so a catch block
holding the exception in a local carries it.
Confirmed the old implementation leaked on all three shapes before
changing it; the new tests fail against it and pass against the walk.
Replaced with a recursive walk of the event instead of a longer path
list -- it covers these without enumerating them and does not go stale
when the SDK grows another such field. The walk only rewrites str leaves
and preserves everything else, with a test pinning that.
Copilot also suggested normalizing exception-valued params. Not needed:
client._prepare_event serializes the event before calling before_send
(client.py:650 vs :658), so every leaf is already a JSON primitive by
then and there are no live exception objects left to coerce.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RVJKTGk9KHUTN2xfU59ujX
The SDK repr()s frame locals and logging params before before_send, so the DETAIL newline arrives there as a literal backslash-n and the old find() missed it. Tests now use that shape, plus one that goes through the real SDK. The body-size comment cited the WSGI extractor; this app uses the Starlette one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MYc2F3cFvSCfVzqeRMshGy
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.
This repo
This app never passed
max_request_body_size, so it has been on the SDK default of"medium"— 10KB bodies — by omission rather than by decision. Sets it to"small"explicitly at thesentry_sdk.initcall site, so the choice is findable there rather than in a dependency's defaults, and adds the PostgresDETAIL:scrub to the existingbefore_send.This is a FastAPI app. The Starlette integration's
extract_request_infodoes the capture and attaches no body when thecontent-lengthheader is absent. Every route it registers is a GET.What
Two independent ways learner data reaches Sentry, neither of them governed by
send_default_pii. Part of a ten-repo change on branchtmacey/sentry-scrub-request-bodies.1. HTTP request bodies are captured with no PII gate
request_info["data"]is set unconditionally inRequestExtractor.extract_into_event. The only control ismax_request_body_size, checked byrequest_body_within_bounds:neversmallmediumalwaysVerified against sentry-sdk source:
request_body_within_boundsdoes the bounds check,RequestExtractor.extract_into_eventsets the body unconditionally, and"medium"is themax_request_body_sizedefault. The repos in this change pin 1.9.0 to 2.68.0, and the behaviour is the same across them.The bound is checked against the declared
Content-Length, and a missing or malformed header counts as 0. Under WSGI, Django then reads 0 bytes, so nothing gets past it. Under ASGI, Django takesCONTENT_LENGTHfrom the header and reads the whole body, so on learn-ai and mit-learn a chunked request with noContent-Lengthis captured in full. The Starlette integration (ol-analytics-api) attaches no body when the header is absent.Which endpoints that actually means, measured in Sentry over the 30 days to 2026-09-08 (
dataset=errors,http.method:[POST,PUT,PATCH], grouped by transaction):PATCH /scim/v2/Users/{uuid}POST /api/v1/webhooks/content_files/POST /api/v1/enrollments/POST .../xblock/{usage_id}/handler/{handler}/POST /cms/pages/{page_id}/edit/POST /api/profile/details/POST /api/checkout/result/POST /api/checkout/redeem_discount/Enrollment, checkout, profile edits, SCIM user attributes, and xblock handler submissions — governed by a knob nobody set.
2. Postgres
DETAIL:lines leak rows through the exception textA constraint violation carries a
DETAIL: Failing row contains (...)line reproducing the whole offending row, and psycopg puts it instr(exc). It therefore ships inside the exception value, where no SDK privacy option reaches it —send_default_piigoverns user/cookie/header capture andmax_request_body_sizegoverns bodies; neither touches exception text.Measured on MITXONLINE-6PK: a SCIM
PATCHIntegrityErroronusers_userwhoseDETAILline reproduces a learner email address three times per event. First seen 2026-05-27, 46,764 occurrences, last seen 2026-09-08 — still firing.before_sendnow walks the whole serialized event and truncates any string at itsDETAIL:line, whether the newline arrives raw or as a literal\n(the SDKrepr()s frame locals and non-string log params beforebefore_sendruns). It keeps the primary error that names the failure and drops the row echo plus anyHINT/CONTEXTafter it.Testing
Unit tests next to the module use a real MITXONLINE-6PK exception value with the learner identifiers replaced. They assert the primary message survives, the email and external UUID do not, and
HINT/CONTEXTare dropped, for both the rawDETAIL:line and the repr'd form the SDK produces for frame locals and log params. One test goes through a real SDK client, with this module's_before_sendand a fake transport, and checks that no outgoing event carries the email. Against the pre-review module, 4 of the tests fail, that one included.Not in scope
send_default_piiand the salted-user-hash work are deliberately untouched here — different knob, different failure mode, tracked separately.🤖 Generated with Claude Code
https://claude.ai/code/session_01RVJKTGk9KHUTN2xfU59ujX
The other nine PRs
Same branch name (
tmacey/sentry-scrub-request-bodies) in each repo. Independent — no merge order required.neversmallsmallsmallsmallsmallsmall(asrequest_bodies, SDK 1.9.0)smallsmallsmall