Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 30 additions & 21 deletions sentry_sdk/integrations/_wsgi_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,38 +89,47 @@ def extract_into_event(self, event: "Event") -> None:
content_length = self.content_length()
request_info = event.get("request", {})

# Prior to data collection being implemented we unconditionally attached
# the request body, which is why we default to True here.
attach_request_body = True

if has_data_collection_enabled(client.options):
cookies = _apply_key_value_collection_filtering(
items=dict(self.cookies()),
behaviour=client.options["data_collection"]["cookies"],
)
if cookies:
request_info["cookies"] = cookies

attach_request_body = (
"incoming_request" in client.options["data_collection"]["http_bodies"]
)
elif should_send_default_pii():
request_info["cookies"] = dict(self.cookies())

if not request_body_within_bounds(client, content_length):
data = AnnotatedValue.removed_because_over_size_limit()
else:
# First read the raw body data
# It is important to read this first because if it is Django
# it will cache the body and then we can read the cached version
# again in parsed_body() (or json() or wherever).
raw_data = None
try:
raw_data = self.raw_data()
except _RAW_DATA_EXCEPTIONS:
# If DjangoRestFramework is used it already read the body for us
# so reading it here will fail. We can ignore this.
pass

parsed_body = self.parsed_body()
if parsed_body is not None:
data = parsed_body
elif raw_data:
data = AnnotatedValue.removed_because_raw_data()
if attach_request_body:
if not request_body_within_bounds(client, content_length):
data = AnnotatedValue.removed_because_over_size_limit()
else:
data = None
# First read the raw body data
# It is important to read this first because if it is Django
# it will cache the body and then we can read the cached version
# again in parsed_body() (or json() or wherever).
raw_data = None
try:
raw_data = self.raw_data()
except _RAW_DATA_EXCEPTIONS:
# If DjangoRestFramework is used it already read the body for us
# so reading it here will fail. We can ignore this.
pass

parsed_body = self.parsed_body()
if parsed_body is not None:
data = parsed_body
elif raw_data:
data = AnnotatedValue.removed_because_raw_data()
else:
data = None

if data is not None:
request_info["data"] = data
Expand Down
142 changes: 142 additions & 0 deletions tests/integrations/flask/test_flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -1594,3 +1594,145 @@ def login():
assert "user.id" not in segment.get("attributes", {})
assert "user.email" not in segment.get("attributes", {})
assert "user.name" not in segment.get("attributes", {})


@pytest.mark.parametrize(
"data_collection, expect_body",
[
pytest.param({}, True, id="data_collection_http_bodies_default"),
pytest.param(
{"http_bodies": ["incoming_request"]},
True,
id="data_collection_http_bodies_incoming_request",
),
pytest.param(
{"http_bodies": ["outgoing_request"]},
False,
id="data_collection_http_bodies_outgoing_request_only",
),
pytest.param(
{"http_bodies": []}, False, id="data_collection_http_bodies_empty"
),
],
)
def test_flask_request_body_data_collection(
sentry_init, capture_events, app, monkeypatch, data_collection, expect_body
):
sentry_init(
integrations=[flask_sentry.FlaskIntegration()],
_experiments={"data_collection": data_collection},
)
# This test is about request body gating, not user data.
monkeypatch.setattr(flask_sentry, "flask_login", None)

data = {"foo": "bar"}

@app.route("/", methods=["POST"])
def index():
capture_message("hi")
return "ok"

events = capture_events()

client = app.test_client()
response = client.post("/", content_type="application/json", data=json.dumps(data))
assert response.status_code == 200

(event,) = events
if expect_body:
assert event["request"]["data"] == data
else:
assert "data" not in event["request"]


def test_flask_request_body_dropped_with_form_and_files_data_collection(
sentry_init, capture_events, app, monkeypatch
):
sentry_init(
integrations=[flask_sentry.FlaskIntegration()],
max_request_body_size="always",
_experiments={"data_collection": {"http_bodies": []}},
)
monkeypatch.setattr(flask_sentry, "flask_login", None)

data = {
"foo": "bar",
"file": (BytesIO(b"hello"), "hello.txt"),
}

@app.route("/", methods=["POST"])
def index():
assert list(request.form) == ["foo"]
assert list(request.files) == ["file"]
capture_message("hi")
return "ok"

events = capture_events()

client = app.test_client()
response = client.post("/", data=data)
assert response.status_code == 200

(event,) = events
assert "data" not in event["request"]
assert "data" not in event.get("_meta", {}).get("request", {})


def test_flask_transaction_request_body_data_collection(
sentry_init, capture_events, app, monkeypatch
):
sentry_init(
integrations=[flask_sentry.FlaskIntegration()],
traces_sample_rate=1.0,
_experiments={"data_collection": {"http_bodies": []}},
)
monkeypatch.setattr(flask_sentry, "flask_login", None)

data = {"username": "sentry-user", "age": "26"}

@app.route("/", methods=["POST"])
def index():
capture_message("hi")
return "ok"

events = capture_events()

client = app.test_client()
response = client.post("/", content_type="application/json", data=data)
assert response.status_code == 200

event, transaction_event = events
assert "data" not in event["request"]
assert "data" not in transaction_event["request"]


def test_flask_oversized_request_body_not_annotated_data_collection(
sentry_init, capture_events, app, monkeypatch
):
"""
The gating happens before the size check, so an oversized body is dropped
outright instead of being reported as removed because of the size limit.
"""
sentry_init(
integrations=[flask_sentry.FlaskIntegration()],
max_request_body_size="small",
_experiments={"data_collection": {"http_bodies": []}},
)
monkeypatch.setattr(flask_sentry, "flask_login", None)

data = "a" * 2000

@app.route("/", methods=["POST"])
def index():
capture_message("hi")
return "ok"

events = capture_events()

client = app.test_client()
response = client.post("/", data=data)
assert response.status_code == 200

(event,) = events
assert "data" not in event["request"]
assert "data" not in event.get("_meta", {}).get("request", {})
Loading