From 80ec79a0bc6d63c846422f8bf0cb74209871b117 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 14 Aug 2026 16:56:55 -0400 Subject: [PATCH] test(django): Add tests for data_collection gating of request body Refs PY-2419 Refs #6283 --- tests/integrations/django/asgi/test_asgi.py | 160 ++++++++++++++++++++ tests/integrations/django/test_basic.py | 123 +++++++++++++++ 2 files changed, 283 insertions(+) diff --git a/tests/integrations/django/asgi/test_asgi.py b/tests/integrations/django/asgi/test_asgi.py index 74b50d782c..c389b39889 100644 --- a/tests/integrations/django/asgi/test_asgi.py +++ b/tests/integrations/django/asgi/test_asgi.py @@ -819,6 +819,166 @@ async def test_asgi_request_body( assert "data" not in event["request"] +@pytest.mark.parametrize("application", APPS) +@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" + ), + ], +) +@pytest.mark.asyncio +@pytest.mark.skipif( + django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" +) +async def test_asgi_request_body_data_collection( + sentry_init, capture_events, application, data_collection, expect_body +): + sentry_init( + integrations=[DjangoIntegration()], + _experiments={"data_collection": data_collection}, + ) + events = capture_events() + + data = {"hey": 42} + comm = HttpCommunicator( + application, + method="POST", + headers=[(b"content-type", b"application/json")], + path=reverse("post_echo_async"), + body=json.dumps(data).encode("utf-8"), + ) + response = await comm.get_response() + await comm.wait() + + assert response["status"] == 200 + + (event,) = events + + if expect_body: + assert event["request"]["data"] == data + else: + assert "data" not in event["request"] + + +@pytest.mark.parametrize("application", APPS) +@pytest.mark.asyncio +@pytest.mark.skipif( + django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" +) +async def test_asgi_request_body_dropped_with_form_and_files_data_collection( + sentry_init, capture_events, application +): + sentry_init( + integrations=[DjangoIntegration()], + max_request_body_size="always", + _experiments={"data_collection": {"http_bodies": []}}, + ) + events = capture_events() + + comm = HttpCommunicator( + application, + method="POST", + headers=[ + (b"content-type", b"multipart/form-data; boundary=fd721ef49ea403a6"), + (b"content-length", BODY_FORM_CONTENT_LENGTH), + ], + path=reverse("post_echo_async"), + body=BODY_FORM, + ) + response = await comm.get_response() + await comm.wait() + + assert response["status"] == 200 + + (event,) = events + + assert "data" not in event["request"] + assert "data" not in event.get("_meta", {}).get("request", {}) + + +@pytest.mark.parametrize("application", APPS) +@pytest.mark.asyncio +@pytest.mark.skipif( + django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" +) +async def test_asgi_transaction_request_body_data_collection( + sentry_init, capture_events, application +): + sentry_init( + integrations=[DjangoIntegration()], + traces_sample_rate=1.0, + _experiments={"data_collection": {"http_bodies": []}}, + ) + events = capture_events() + + comm = HttpCommunicator( + application, + method="POST", + headers=[(b"content-type", b"application/json")], + path=reverse("post_echo_async"), + body=json.dumps({"hey": 42}).encode("utf-8"), + ) + response = await comm.get_response() + await comm.wait() + + assert response["status"] == 200 + + (event, transaction_event) = events + + assert "data" not in event["request"] + assert "data" not in transaction_event["request"] + + +@pytest.mark.parametrize("application", APPS) +@pytest.mark.asyncio +@pytest.mark.skipif( + django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" +) +async def test_asgi_oversized_request_body_not_annotated_data_collection( + sentry_init, capture_events, application +): + """ + 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=[DjangoIntegration()], + max_request_body_size="small", + _experiments={"data_collection": {"http_bodies": []}}, + ) + events = capture_events() + + comm = HttpCommunicator( + application, + method="POST", + headers=[(b"content-type", b"text/plain")], + path=reverse("post_echo_async"), + body=b"a" * 2000, + ) + response = await comm.get_response() + await comm.wait() + + assert response["status"] == 200 + + (event,) = events + + assert "data" not in event["request"] + assert "data" not in event.get("_meta", {}).get("request", {}) + + @pytest.mark.asyncio @pytest.mark.skipif( sys.version_info >= (3, 12), diff --git a/tests/integrations/django/test_basic.py b/tests/integrations/django/test_basic.py index d24b4b31cc..e94231beef 100644 --- a/tests/integrations/django/test_basic.py +++ b/tests/integrations/django/test_basic.py @@ -4,6 +4,7 @@ import re import sys from functools import partial +from io import BytesIO from unittest.mock import patch import pytest @@ -1191,6 +1192,128 @@ def test_request_body( assert "" not in event +@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_request_body_data_collection( + sentry_init, client, capture_items, data_collection, expect_body +): + sentry_init( + integrations=[DjangoIntegration()], + _experiments={"data_collection": data_collection}, + ) + items = capture_items("event") + + data = {"hey": 42} + content, status, headers = unpack_werkzeug_response( + client.post( + reverse("post_echo"), + data=json.dumps(data).encode("utf-8"), + content_type="application/json", + ) + ) + assert status.lower() == "200 ok" + + (event,) = (item.payload for item in items) + + if expect_body: + assert event["request"]["data"] == data + else: + assert "data" not in event["request"] + + +def test_request_body_dropped_with_form_and_files_data_collection( + sentry_init, client, capture_items +): + sentry_init( + integrations=[DjangoIntegration()], + max_request_body_size="always", + _experiments={"data_collection": {"http_bodies": []}}, + ) + items = capture_items("event") + + content, status, headers = unpack_werkzeug_response( + client.post( + reverse("post_echo"), + data={"foo": "bar", "file": (BytesIO(b"hello"), "hello.txt")}, + ) + ) + assert status.lower() == "200 ok" + + (event,) = (item.payload for item in items) + + assert "data" not in event["request"] + assert "data" not in event.get("_meta", {}).get("request", {}) + + +def test_transaction_request_body_data_collection(sentry_init, client, capture_events): + sentry_init( + integrations=[DjangoIntegration()], + traces_sample_rate=1.0, + _experiments={"data_collection": {"http_bodies": []}}, + ) + events = capture_events() + + content, status, headers = unpack_werkzeug_response( + client.post( + reverse("post_echo"), + data=json.dumps({"hey": 42}).encode("utf-8"), + content_type="application/json", + ) + ) + assert status.lower() == "200 ok" + + event, transaction_event = events + + assert "data" not in event["request"] + assert "data" not in transaction_event["request"] + + +def test_oversized_request_body_not_annotated_data_collection( + sentry_init, client, capture_items +): + """ + 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=[DjangoIntegration()], + max_request_body_size="small", + _experiments={"data_collection": {"http_bodies": []}}, + ) + items = capture_items("event") + + content, status, headers = unpack_werkzeug_response( + client.post( + reverse("post_echo"), + data=b"a" * 2000, + content_type="text/plain", + ) + ) + assert status.lower() == "200 ok" + + (event,) = (item.payload for item in items) + + assert "data" not in event["request"] + assert "data" not in event.get("_meta", {}).get("request", {}) + + @pytest.mark.parametrize("span_streaming", [True, False]) def test_read_request( sentry_init,