fix(sentry): cap request bodies at 1KB and scrub Postgres DETAIL rows - #5561
Open
blarghmatey wants to merge 3 commits into
Open
fix(sentry): cap request bodies at 1KB and scrub Postgres DETAIL rows#5561blarghmatey wants to merge 3 commits into
blarghmatey wants to merge 3 commits into
Conversation
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
Two ways learner data reaches Sentry, neither gated by send_default_pii.
Request bodies. The SDK sets request.data unconditionally and the body
size option is the only control; left unset it defaults to "medium",
i.e. 10,000-byte bodies. Nobody chose that. This app raises on POSTs to
/cms/pages/{page_id}/edit/ (273 events in the last 30 days), so the
capture is not theoretical. Set to "small" explicitly.
Spelled `request_bodies` rather than `max_request_body_size` because
this app is pinned to sentry-sdk==1.9.0, where that is the option name
(consts.py:70, _wsgi_common.py:39); it was renamed in 2.x. init
validates kwargs strictly and raises TypeError on an unknown one
(client.py:61), so the 2.x spelling would take Sentry down on this app
rather than being ignored. Every other application in the estate uses
the 2.x name. Rename when the pin moves.
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.
The scrub helpers are module-level, unlike before_send which is nested
inside init_sentry here, so they can be tested without Django settings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RVJKTGk9KHUTN2xfU59ujX
blarghmatey
force-pushed
the
tmacey/sentry-scrub-request-bodies
branch
from
September 8, 2026 15:41
f8997cd to
c773bc4
Compare
…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 non-string logging params before
before_send, so there the DETAIL line arrives with a literal backslash-n
and the old find("\nDETAIL:") missed it. Match both forms, and test
through a real sentry-sdk 1.9.0 client instead of hand-built events.
Cite SDK function names instead of line numbers, which drift, and drop
the tuple branch: the serializer turns tuples into lists.
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 the body-size option, so it has been on the SDK default of
"medium"— 10KB bodies. Sets it to"small"explicitly, and adds the PostgresDETAIL:scrub tobefore_send.request_bodies, notmax_request_body_size. This app is pinned tosentry-sdk==1.9.0(pyproject.toml:38, anduv.lockagrees), where that is the option name — the default is inClientConstructorand the bounds check isrequest_body_within_bounds. It was renamed in 2.x.initvalidates its kwargs strictly and raisesTypeError: Unknown option 'max_request_body_size'on an unknown one (_get_options), so the 2.x spelling would take Sentry down entirely on this app rather than being silently ignored — hit while writing this change, so it is observed, not predicted. Every other repo in this ten-repo change uses the 2.x name. Rename when the pin moves; tracked at the micromasters SDK-bump task.The estate spans
sentry-sdk1.9.0 (here) to 2.68.0, so this is the one repo where the uniform edit does not apply verbatim.The scrub helpers are module-level here, unlike
before_sendwhich is nested insideinit_sentry, so they are testable without Django settings. For the same reason, the SDK-driven test can't go through this app'sbefore_send; see Testing.Production runs Django under WSGI (granian
--interface wsgi), so the Content-Length caveat below doesn't apply.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 sentry-sdk 1.9.0 client with a fake transport and checks that no outgoing event carries the email.before_sendis nested insideinit_sentryhere, so that test passes the scrub to the client directly. 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