From 223e86271243b13112d60e1902b86bc87cafffae Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 18 Nov 2025 16:35:50 +0100 Subject: [PATCH 01/14] feat(django): Instrument database rollbacks --- sentry_sdk/consts.py | 1 + sentry_sdk/integrations/django/__init__.py | 13 ++ tests/integrations/django/myapp/urls.py | 10 + tests/integrations/django/myapp/views.py | 22 ++ .../integrations/django/test_db_query_data.py | 218 ++++++++++++++++++ 5 files changed, 264 insertions(+) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 44715be525..3166d3802a 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -116,6 +116,7 @@ class INSTRUMENTER: class DBOPERATION: COMMIT = "COMMIT" + ROLLBACK = "ROLLBACK" class SPANDATA: diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 69bab50719..527ce67caa 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -634,6 +634,7 @@ def install_sql_hook(): real_executemany = CursorWrapper.executemany real_connect = BaseDatabaseWrapper.connect real_commit = BaseDatabaseWrapper.commit + real_rollback = BaseDatabaseWrapper.rollback except AttributeError: # This won't work on Django versions < 1.6 return @@ -702,11 +703,23 @@ def commit(self): _set_db_data(span, self, DBOPERATION.COMMIT) return real_commit(self) + @ensure_integration_enabled(DjangoIntegration, real_rollback) + def rollback(self): + # type: (BaseDatabaseWrapper) -> None + with sentry_sdk.start_span( + op=OP.DB, + name=DBOPERATION.ROLLBACK, + origin=DjangoIntegration.origin_db, + ) as span: + _set_db_data(span, self, DBOPERATION.ROLLBACK) + return real_rollback(self) + CursorWrapper.execute = execute CursorWrapper.executemany = executemany BaseDatabaseWrapper.connect = connect BaseDatabaseWrapper.commit = commit ignore_logger("django.db.backends") + BaseDatabaseWrapper.rollback = rollback def _set_db_data(span, cursor_or_db, db_operation=None): diff --git a/tests/integrations/django/myapp/urls.py b/tests/integrations/django/myapp/urls.py index 2d39228524..39523b2d9e 100644 --- a/tests/integrations/django/myapp/urls.py +++ b/tests/integrations/django/myapp/urls.py @@ -66,11 +66,21 @@ def path(path, *args, **kwargs): views.postgres_insert_orm_no_autocommit, name="postgres_insert_orm_no_autocommit", ), + path( + "postgres-insert-no-autocommit-rollback", + views.postgres_insert_orm_no_autocommit_rollback, + name="postgres_insert_orm_no_autocommit_rollback", + ), path( "postgres-insert-atomic", views.postgres_insert_orm_atomic, name="postgres_insert_orm_atomic", ), + path( + "postgres-insert-atomic-rollback", + views.postgres_insert_orm_atomic_rollback, + name="postgres_insert_orm_atomic_rollback", + ), path( "postgres-select-slow-from-supplement", helper_views.postgres_select_orm, diff --git a/tests/integrations/django/myapp/views.py b/tests/integrations/django/myapp/views.py index 9b6e6f2253..51da3583d2 100644 --- a/tests/integrations/django/myapp/views.py +++ b/tests/integrations/django/myapp/views.py @@ -259,6 +259,18 @@ def postgres_insert_orm_no_autocommit(request, *args, **kwargs): return HttpResponse("ok {}".format(user)) +@csrf_exempt +def postgres_insert_orm_no_autocommit_rollback(request, *args, **kwargs): + transaction.set_autocommit(False, using="postgres") + user = User.objects.db_manager("postgres").create_user( + username="user1", + ) + transaction.rollback(using="postgres") + transaction.set_autocommit(True, using="postgres") + + return HttpResponse("ok {}".format(user)) + + @csrf_exempt def postgres_insert_orm_atomic(request, *args, **kwargs): with transaction.atomic(using="postgres"): @@ -268,6 +280,16 @@ def postgres_insert_orm_atomic(request, *args, **kwargs): return HttpResponse("ok {}".format(user)) +@csrf_exempt +def postgres_insert_orm_atomic_rollback(request, *args, **kwargs): + with transaction.atomic(using="postgres"): + user = User.objects.db_manager("postgres").create_user( + username="user1", + ) + transaction.set_rollback(True, using="postgres") + return HttpResponse("ok {}".format(user)) + + @csrf_exempt def permission_denied_exc(*args, **kwargs): raise PermissionDenied("bye") diff --git a/tests/integrations/django/test_db_query_data.py b/tests/integrations/django/test_db_query_data.py index 6c7b2de196..5af74961bd 100644 --- a/tests/integrations/django/test_db_query_data.py +++ b/tests/integrations/django/test_db_query_data.py @@ -640,6 +640,118 @@ def test_db_no_autocommit_executemany(sentry_init, client, capture_events): assert commit_span["origin"] == "auto.db.django" +@pytest.mark.forked +@pytest_mark_django_db_decorator(transaction=True, databases=["postgres"]) +def test_db_no_autocommit_rollback_execute(sentry_init, client, capture_events): + sentry_init( + integrations=[DjangoIntegration()], + traces_sample_rate=1.0, + ) + + if "postgres" not in connections: + pytest.skip("postgres tests disabled") + + # trigger Django to open a new connection by marking the existing one as None. + connections["postgres"].connection = None + + events = capture_events() + + client.get(reverse("postgres_insert_orm_no_autocommit_rollback")) + + (event,) = events + + # Ensure operation is rolled back + assert not User.objects.using("postgres").exists() + + assert event["contexts"]["trace"]["origin"] == "auto.http.django" + + for span in event["spans"]: + if span["op"] == "db": + assert span["origin"] == "auto.db.django" + else: + assert span["origin"] == "auto.http.django" + + commit_spans = [ + span + for span in event["spans"] + if span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.ROLLBACK + ] + assert len(commit_spans) == 1 + commit_span = commit_spans[0] + assert commit_span["origin"] == "auto.db.django" + + +@pytest.mark.forked +@pytest_mark_django_db_decorator(transaction=True) +def test_db_no_autocommit_rollback_executemany(sentry_init, client, capture_events): + sentry_init( + integrations=[DjangoIntegration()], + traces_sample_rate=1.0, + ) + + events = capture_events() + + if "postgres" not in connections: + pytest.skip("postgres tests disabled") + + with start_transaction(name="test_transaction"): + from django.db import connection, transaction + + cursor = connection.cursor() + + query = """INSERT INTO auth_user ( + password, + is_superuser, + username, + first_name, + last_name, + email, + is_staff, + is_active, + date_joined +) +VALUES ('password', false, %s, %s, %s, %s, false, true, %s);""" + + query_list = ( + ( + "user1", + "John", + "Doe", + "user1@example.com", + datetime(1970, 1, 1), + ), + ( + "user2", + "Max", + "Mustermann", + "user2@example.com", + datetime(1970, 1, 1), + ), + ) + + transaction.set_autocommit(False) + cursor.executemany(query, query_list) + transaction.rollback() + transaction.set_autocommit(True) + + (event,) = events + + # Ensure operation is rolled back + assert not User.objects.exists() + + assert event["contexts"]["trace"]["origin"] == "manual" + assert event["spans"][0]["origin"] == "auto.db.django" + + commit_spans = [ + span + for span in event["spans"] + if span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.ROLLBACK + ] + assert len(commit_spans) == 1 + commit_span = commit_spans[0] + assert commit_span["origin"] == "auto.db.django" + + @pytest.mark.forked @pytest_mark_django_db_decorator(transaction=True, databases=["postgres"]) def test_db_atomic_execute(sentry_init, client, capture_events): @@ -743,3 +855,109 @@ def test_db_atomic_executemany(sentry_init, client, capture_events): assert len(commit_spans) == 1 commit_span = commit_spans[0] assert commit_span["origin"] == "auto.db.django" + + +@pytest.mark.forked +@pytest_mark_django_db_decorator(transaction=True, databases=["postgres"]) +def test_db_atomic_rollback_execute(sentry_init, client, capture_events): + sentry_init( + integrations=[DjangoIntegration()], + send_default_pii=True, + traces_sample_rate=1.0, + ) + + if "postgres" not in connections: + pytest.skip("postgres tests disabled") + + # trigger Django to open a new connection by marking the existing one as None. + connections["postgres"].connection = None + + events = capture_events() + + client.get(reverse("postgres_insert_orm_atomic_rollback")) + + (event,) = events + + # Ensure operation is rolled back + assert not User.objects.using("postgres").exists() + + assert event["contexts"]["trace"]["origin"] == "auto.http.django" + + commit_spans = [ + span + for span in event["spans"] + if span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.ROLLBACK + ] + assert len(commit_spans) == 1 + commit_span = commit_spans[0] + assert commit_span["origin"] == "auto.db.django" + + +@pytest.mark.forked +@pytest_mark_django_db_decorator(transaction=True) +def test_db_atomic_rollback_executemany(sentry_init, client, capture_events): + sentry_init( + integrations=[DjangoIntegration()], + send_default_pii=True, + traces_sample_rate=1.0, + ) + + if "postgres" not in connections: + pytest.skip("postgres tests disabled") + + # trigger Django to open a new connection by marking the existing one as None. + connections["postgres"].connection = None + + events = capture_events() + + with start_transaction(name="test_transaction"): + with transaction.atomic(): + cursor = connection.cursor() + + query = """INSERT INTO auth_user ( + password, + is_superuser, + username, + first_name, + last_name, + email, + is_staff, + is_active, + date_joined +) +VALUES ('password', false, %s, %s, %s, %s, false, true, %s);""" + + query_list = ( + ( + "user1", + "John", + "Doe", + "user1@example.com", + datetime(1970, 1, 1), + ), + ( + "user2", + "Max", + "Mustermann", + "user2@example.com", + datetime(1970, 1, 1), + ), + ) + cursor.executemany(query, query_list) + transaction.set_rollback(True) + + (event,) = events + + # Ensure operation is rolled back + assert not User.objects.exists() + + assert event["contexts"]["trace"]["origin"] == "manual" + + commit_spans = [ + span + for span in event["spans"] + if span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.ROLLBACK + ] + assert len(commit_spans) == 1 + commit_span = commit_spans[0] + assert commit_span["origin"] == "auto.db.django" From 63be88b39225a67b88028fa689774f411ba46590 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 18 Nov 2025 16:45:16 +0100 Subject: [PATCH 02/14] . --- sentry_sdk/integrations/django/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 527ce67caa..06e853f2c9 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -718,8 +718,8 @@ def rollback(self): CursorWrapper.executemany = executemany BaseDatabaseWrapper.connect = connect BaseDatabaseWrapper.commit = commit - ignore_logger("django.db.backends") BaseDatabaseWrapper.rollback = rollback + ignore_logger("django.db.backends") def _set_db_data(span, cursor_or_db, db_operation=None): From f0dfc063f3feab062b50e167f63a08d9e3ae691d Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 19 Nov 2025 13:55:17 +0100 Subject: [PATCH 03/14] . --- .../django/test_db_transactions.py | 249 ++++++++++++++++++ 1 file changed, 249 insertions(+) diff --git a/tests/integrations/django/test_db_transactions.py b/tests/integrations/django/test_db_transactions.py index b69a756dde..edb88bb744 100644 --- a/tests/integrations/django/test_db_transactions.py +++ b/tests/integrations/django/test_db_transactions.py @@ -150,6 +150,131 @@ def test_db_no_autocommit_executemany(sentry_init, client, capture_events): ) or conn_params.get("dbname") +@pytest.mark.forked +@pytest_mark_django_db_decorator(transaction=True) +def test_db_no_autocommit_rollback_execute(sentry_init, client, capture_events): + sentry_init( + integrations=[DjangoIntegration()], + traces_sample_rate=1.0, + ) + + if "postgres" not in connections: + pytest.skip("postgres tests disabled") + + # trigger Django to open a new connection by marking the existing one as None. + connections["postgres"].connection = None + + events = capture_events() + + client.get(reverse("postgres_insert_orm_no_autocommit_rollback")) + + (event,) = events + + # Ensure operation is rolled back + assert not User.objects.using("postgres").exists() + + assert event["contexts"]["trace"]["origin"] == "auto.http.django" + + commit_spans = [ + span + for span in event["spans"] + if span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.ROLLBACK + ] + assert len(commit_spans) == 1 + commit_span = commit_spans[0] + assert commit_span["origin"] == "auto.db.django" + + # Verify other database attributes + assert commit_span["data"].get(SPANDATA.DB_SYSTEM) == "postgresql" + conn_params = connections["postgres"].get_connection_params() + assert commit_span["data"].get(SPANDATA.DB_NAME) is not None + assert commit_span["data"].get(SPANDATA.DB_NAME) == conn_params.get( + "database" + ) or conn_params.get("dbname") + assert commit_span["data"].get(SPANDATA.SERVER_ADDRESS) == os.environ.get( + "SENTRY_PYTHON_TEST_POSTGRES_HOST", "localhost" + ) + assert commit_span["data"].get(SPANDATA.SERVER_PORT) == os.environ.get( + "SENTRY_PYTHON_TEST_POSTGRES_PORT", "5432" + ) + + +@pytest.mark.forked +@pytest_mark_django_db_decorator(transaction=True) +def test_db_no_autocommit_rollback_executemany(sentry_init, client, capture_events): + sentry_init( + integrations=[DjangoIntegration()], + traces_sample_rate=1.0, + ) + + events = capture_events() + + with start_transaction(name="test_transaction"): + from django.db import connection, transaction + + cursor = connection.cursor() + + query = """INSERT INTO auth_user ( + password, + is_superuser, + username, + first_name, + last_name, + email, + is_staff, + is_active, + date_joined +) +VALUES ('password', false, %s, %s, %s, %s, false, true, %s);""" + + query_list = ( + ( + "user1", + "John", + "Doe", + "user1@example.com", + datetime(1970, 1, 1), + ), + ( + "user2", + "Max", + "Mustermann", + "user2@example.com", + datetime(1970, 1, 1), + ), + ) + + transaction.set_autocommit(False) + cursor.executemany(query, query_list) + transaction.rollback() + transaction.set_autocommit(True) + + (event,) = events + + # Ensure operation is rolled back + assert not User.objects.exists() + + assert event["contexts"]["trace"]["origin"] == "manual" + assert event["spans"][0]["origin"] == "auto.db.django" + + commit_spans = [ + span + for span in event["spans"] + if span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.ROLLBACK + ] + assert len(commit_spans) == 1 + commit_span = commit_spans[0] + assert commit_span["origin"] == "auto.db.django" + + # Verify other database attributes + assert commit_span["data"].get(SPANDATA.DB_SYSTEM) == "sqlite" + conn_params = connection.get_connection_params() + assert commit_span["data"].get(SPANDATA.DB_NAME) is not None + assert commit_span["data"].get(SPANDATA.DB_NAME) == conn_params.get( + "database" + ) or conn_params.get("dbname") + + @pytest.mark.forked @pytest_mark_django_db_decorator(transaction=True) def test_db_atomic_execute(sentry_init, client, capture_events): @@ -270,3 +395,127 @@ def test_db_atomic_executemany(sentry_init, client, capture_events): assert commit_span["data"].get(SPANDATA.DB_NAME) == conn_params.get( "database" ) or conn_params.get("dbname") + + +@pytest.mark.forked +@pytest_mark_django_db_decorator(transaction=True) +def test_db_atomic_rollback_execute(sentry_init, client, capture_events): + sentry_init( + integrations=[DjangoIntegration()], + send_default_pii=True, + traces_sample_rate=1.0, + ) + + if "postgres" not in connections: + pytest.skip("postgres tests disabled") + + # trigger Django to open a new connection by marking the existing one as None. + connections["postgres"].connection = None + + events = capture_events() + + client.get(reverse("postgres_insert_orm_atomic_rollback")) + + (event,) = events + + # Ensure operation is rolled back + assert not User.objects.using("postgres").exists() + + assert event["contexts"]["trace"]["origin"] == "auto.http.django" + + commit_spans = [ + span + for span in event["spans"] + if span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.ROLLBACK + ] + assert len(commit_spans) == 1 + commit_span = commit_spans[0] + assert commit_span["origin"] == "auto.db.django" + + # Verify other database attributes + assert commit_span["data"].get(SPANDATA.DB_SYSTEM) == "postgresql" + conn_params = connections["postgres"].get_connection_params() + assert commit_span["data"].get(SPANDATA.DB_NAME) is not None + assert commit_span["data"].get(SPANDATA.DB_NAME) == conn_params.get( + "database" + ) or conn_params.get("dbname") + assert commit_span["data"].get(SPANDATA.SERVER_ADDRESS) == os.environ.get( + "SENTRY_PYTHON_TEST_POSTGRES_HOST", "localhost" + ) + assert commit_span["data"].get(SPANDATA.SERVER_PORT) == os.environ.get( + "SENTRY_PYTHON_TEST_POSTGRES_PORT", "5432" + ) + + +@pytest.mark.forked +@pytest_mark_django_db_decorator(transaction=True) +def test_db_atomic_rollback_executemany(sentry_init, client, capture_events): + sentry_init( + integrations=[DjangoIntegration()], + send_default_pii=True, + traces_sample_rate=1.0, + ) + + events = capture_events() + + with start_transaction(name="test_transaction"): + from django.db import connection, transaction + + with transaction.atomic(): + cursor = connection.cursor() + + query = """INSERT INTO auth_user ( + password, + is_superuser, + username, + first_name, + last_name, + email, + is_staff, + is_active, + date_joined +) +VALUES ('password', false, %s, %s, %s, %s, false, true, %s);""" + + query_list = ( + ( + "user1", + "John", + "Doe", + "user1@example.com", + datetime(1970, 1, 1), + ), + ( + "user2", + "Max", + "Mustermann", + "user2@example.com", + datetime(1970, 1, 1), + ), + ) + cursor.executemany(query, query_list) + transaction.set_rollback(True) + + (event,) = events + + # Ensure operation is rolled back + assert not User.objects.exists() + + assert event["contexts"]["trace"]["origin"] == "manual" + + commit_spans = [ + span + for span in event["spans"] + if span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.ROLLBACK + ] + assert len(commit_spans) == 1 + commit_span = commit_spans[0] + assert commit_span["origin"] == "auto.db.django" + + # Verify other database attributes + assert commit_span["data"].get(SPANDATA.DB_SYSTEM) == "sqlite" + conn_params = connection.get_connection_params() + assert commit_span["data"].get(SPANDATA.DB_NAME) is not None + assert commit_span["data"].get(SPANDATA.DB_NAME) == conn_params.get( + "database" + ) or conn_params.get("dbname") From 1fd06a9ac045908cf8c168f0e8aeea6c6bfede59 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 21 Nov 2025 10:06:48 +0100 Subject: [PATCH 04/14] verify query and rollback are siblings --- .../django/test_db_transactions.py | 98 +++++++++++++------ 1 file changed, 66 insertions(+), 32 deletions(-) diff --git a/tests/integrations/django/test_db_transactions.py b/tests/integrations/django/test_db_transactions.py index a0d833f902..4ad80b97c8 100644 --- a/tests/integrations/django/test_db_transactions.py +++ b/tests/integrations/django/test_db_transactions.py @@ -192,29 +192,38 @@ def test_db_no_autocommit_rollback_execute(sentry_init, client, capture_events): assert event["contexts"]["trace"]["origin"] == "auto.http.django" - commit_spans = [ + rollback_spans = [ span for span in event["spans"] if span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.ROLLBACK ] - assert len(commit_spans) == 1 - commit_span = commit_spans[0] - assert commit_span["origin"] == "auto.db.django" + assert len(rollback_spans) == 1 + rollback_span = rollback_spans[0] + assert rollback_span["origin"] == "auto.db.django" # Verify other database attributes - assert commit_span["data"].get(SPANDATA.DB_SYSTEM) == "postgresql" + assert rollback_span["data"].get(SPANDATA.DB_SYSTEM) == "postgresql" conn_params = connections["postgres"].get_connection_params() - assert commit_span["data"].get(SPANDATA.DB_NAME) is not None - assert commit_span["data"].get(SPANDATA.DB_NAME) == conn_params.get( + assert rollback_span["data"].get(SPANDATA.DB_NAME) is not None + assert rollback_span["data"].get(SPANDATA.DB_NAME) == conn_params.get( "database" ) or conn_params.get("dbname") - assert commit_span["data"].get(SPANDATA.SERVER_ADDRESS) == os.environ.get( + assert rollback_span["data"].get(SPANDATA.SERVER_ADDRESS) == os.environ.get( "SENTRY_PYTHON_TEST_POSTGRES_HOST", "localhost" ) - assert commit_span["data"].get(SPANDATA.SERVER_PORT) == os.environ.get( + assert rollback_span["data"].get(SPANDATA.SERVER_PORT) == os.environ.get( "SENTRY_PYTHON_TEST_POSTGRES_PORT", "5432" ) + insert_spans = [ + span for span in event["spans"] if span["description"].startswith("INSERT INTO") + ] + assert len(insert_spans) == 1 + insert_span = insert_spans[0] + + # Verify query and commit statements are siblings + assert rollback_span["parent_span_id"] == insert_span["parent_span_id"] + @pytest.mark.forked @pytest_mark_django_db_decorator(transaction=True) @@ -274,23 +283,31 @@ def test_db_no_autocommit_rollback_executemany(sentry_init, client, capture_even assert event["contexts"]["trace"]["origin"] == "manual" assert event["spans"][0]["origin"] == "auto.db.django" - commit_spans = [ + rollback_spans = [ span for span in event["spans"] if span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.ROLLBACK ] - assert len(commit_spans) == 1 - commit_span = commit_spans[0] - assert commit_span["origin"] == "auto.db.django" + assert len(rollback_spans) == 1 + rollback_span = rollback_spans[0] + assert rollback_span["origin"] == "auto.db.django" # Verify other database attributes - assert commit_span["data"].get(SPANDATA.DB_SYSTEM) == "sqlite" + assert rollback_span["data"].get(SPANDATA.DB_SYSTEM) == "sqlite" conn_params = connection.get_connection_params() - assert commit_span["data"].get(SPANDATA.DB_NAME) is not None - assert commit_span["data"].get(SPANDATA.DB_NAME) == conn_params.get( + assert rollback_span["data"].get(SPANDATA.DB_NAME) is not None + assert rollback_span["data"].get(SPANDATA.DB_NAME) == conn_params.get( "database" ) or conn_params.get("dbname") + insert_spans = [ + span for span in event["spans"] if span["description"].startswith("INSERT INTO") + ] + + # Verify queries and commit statements are siblings + for insert_span in insert_spans: + assert rollback_span["parent_span_id"] == insert_span["parent_span_id"] + @pytest.mark.forked @pytest_mark_django_db_decorator(transaction=True) @@ -457,29 +474,38 @@ def test_db_atomic_rollback_execute(sentry_init, client, capture_events): assert event["contexts"]["trace"]["origin"] == "auto.http.django" - commit_spans = [ + rollback_spans = [ span for span in event["spans"] if span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.ROLLBACK ] - assert len(commit_spans) == 1 - commit_span = commit_spans[0] - assert commit_span["origin"] == "auto.db.django" + assert len(rollback_spans) == 1 + rollback_span = rollback_spans[0] + assert rollback_span["origin"] == "auto.db.django" # Verify other database attributes - assert commit_span["data"].get(SPANDATA.DB_SYSTEM) == "postgresql" + assert rollback_span["data"].get(SPANDATA.DB_SYSTEM) == "postgresql" conn_params = connections["postgres"].get_connection_params() - assert commit_span["data"].get(SPANDATA.DB_NAME) is not None - assert commit_span["data"].get(SPANDATA.DB_NAME) == conn_params.get( + assert rollback_span["data"].get(SPANDATA.DB_NAME) is not None + assert rollback_span["data"].get(SPANDATA.DB_NAME) == conn_params.get( "database" ) or conn_params.get("dbname") - assert commit_span["data"].get(SPANDATA.SERVER_ADDRESS) == os.environ.get( + assert rollback_span["data"].get(SPANDATA.SERVER_ADDRESS) == os.environ.get( "SENTRY_PYTHON_TEST_POSTGRES_HOST", "localhost" ) - assert commit_span["data"].get(SPANDATA.SERVER_PORT) == os.environ.get( + assert rollback_span["data"].get(SPANDATA.SERVER_PORT) == os.environ.get( "SENTRY_PYTHON_TEST_POSTGRES_PORT", "5432" ) + insert_spans = [ + span for span in event["spans"] if span["description"].startswith("INSERT INTO") + ] + assert len(insert_spans) == 1 + insert_span = insert_spans[0] + + # Verify query and commit statements are siblings + assert rollback_span["parent_span_id"] == insert_span["parent_span_id"] + @pytest.mark.forked @pytest_mark_django_db_decorator(transaction=True) @@ -537,19 +563,27 @@ def test_db_atomic_rollback_executemany(sentry_init, client, capture_events): assert event["contexts"]["trace"]["origin"] == "manual" - commit_spans = [ + rollback_spans = [ span for span in event["spans"] if span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.ROLLBACK ] - assert len(commit_spans) == 1 - commit_span = commit_spans[0] - assert commit_span["origin"] == "auto.db.django" + assert len(rollback_spans) == 1 + rollback_span = rollback_spans[0] + assert rollback_span["origin"] == "auto.db.django" # Verify other database attributes - assert commit_span["data"].get(SPANDATA.DB_SYSTEM) == "sqlite" + assert rollback_span["data"].get(SPANDATA.DB_SYSTEM) == "sqlite" conn_params = connection.get_connection_params() - assert commit_span["data"].get(SPANDATA.DB_NAME) is not None - assert commit_span["data"].get(SPANDATA.DB_NAME) == conn_params.get( + assert rollback_span["data"].get(SPANDATA.DB_NAME) is not None + assert rollback_span["data"].get(SPANDATA.DB_NAME) == conn_params.get( "database" ) or conn_params.get("dbname") + + insert_spans = [ + span for span in event["spans"] if span["description"].startswith("INSERT INTO") + ] + + # Verify queries and commit statements are siblings + for insert_span in insert_spans: + assert rollback_span["parent_span_id"] == insert_span["parent_span_id"] From 95df407f907b3e6768454a37b3144b7ad9e3a157 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 21 Nov 2025 10:08:58 +0100 Subject: [PATCH 05/14] comment --- tests/integrations/django/test_db_transactions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integrations/django/test_db_transactions.py b/tests/integrations/django/test_db_transactions.py index 4ad80b97c8..c690bd470b 100644 --- a/tests/integrations/django/test_db_transactions.py +++ b/tests/integrations/django/test_db_transactions.py @@ -221,7 +221,7 @@ def test_db_no_autocommit_rollback_execute(sentry_init, client, capture_events): assert len(insert_spans) == 1 insert_span = insert_spans[0] - # Verify query and commit statements are siblings + # Verify query and rollback statements are siblings assert rollback_span["parent_span_id"] == insert_span["parent_span_id"] @@ -304,7 +304,7 @@ def test_db_no_autocommit_rollback_executemany(sentry_init, client, capture_even span for span in event["spans"] if span["description"].startswith("INSERT INTO") ] - # Verify queries and commit statements are siblings + # Verify queries and rollback statements are siblings for insert_span in insert_spans: assert rollback_span["parent_span_id"] == insert_span["parent_span_id"] @@ -503,7 +503,7 @@ def test_db_atomic_rollback_execute(sentry_init, client, capture_events): assert len(insert_spans) == 1 insert_span = insert_spans[0] - # Verify query and commit statements are siblings + # Verify query and rollback statements are siblings assert rollback_span["parent_span_id"] == insert_span["parent_span_id"] @@ -584,6 +584,6 @@ def test_db_atomic_rollback_executemany(sentry_init, client, capture_events): span for span in event["spans"] if span["description"].startswith("INSERT INTO") ] - # Verify queries and commit statements are siblings + # Verify queries and rollback statements are siblings for insert_span in insert_spans: assert rollback_span["parent_span_id"] == insert_span["parent_span_id"] From db28135a8d92e280ef24e651acddaa40a702e734 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 21 Nov 2025 11:25:21 +0100 Subject: [PATCH 06/14] add tests cases for exception in atomic block --- tests/integrations/django/myapp/urls.py | 5 + tests/integrations/django/myapp/views.py | 14 ++ .../django/test_db_transactions.py | 144 ++++++++++++++++++ 3 files changed, 163 insertions(+) diff --git a/tests/integrations/django/myapp/urls.py b/tests/integrations/django/myapp/urls.py index 39523b2d9e..26d5a1bf2c 100644 --- a/tests/integrations/django/myapp/urls.py +++ b/tests/integrations/django/myapp/urls.py @@ -81,6 +81,11 @@ def path(path, *args, **kwargs): views.postgres_insert_orm_atomic_rollback, name="postgres_insert_orm_atomic_rollback", ), + path( + "postgres-insert-atomic-exception", + views.postgres_insert_orm_atomic_exception, + name="postgres_insert_orm_atomic_exception", + ), path( "postgres-select-slow-from-supplement", helper_views.postgres_select_orm, diff --git a/tests/integrations/django/myapp/views.py b/tests/integrations/django/myapp/views.py index 51da3583d2..5155365848 100644 --- a/tests/integrations/django/myapp/views.py +++ b/tests/integrations/django/myapp/views.py @@ -290,6 +290,20 @@ def postgres_insert_orm_atomic_rollback(request, *args, **kwargs): return HttpResponse("ok {}".format(user)) +@csrf_exempt +def postgres_insert_orm_atomic_exception(request, *args, **kwargs): + try: + with transaction.atomic(using="postgres"): + user = User.objects.db_manager("postgres").create_user( + username="user1", + ) + transaction.set_rollback(True, using="postgres") + 1 / 0 + except ZeroDivisionError: + pass + return HttpResponse("ok {}".format(user)) + + @csrf_exempt def permission_denied_exc(*args, **kwargs): raise PermissionDenied("bye") diff --git a/tests/integrations/django/test_db_transactions.py b/tests/integrations/django/test_db_transactions.py index c690bd470b..0417b796e2 100644 --- a/tests/integrations/django/test_db_transactions.py +++ b/tests/integrations/django/test_db_transactions.py @@ -587,3 +587,147 @@ def test_db_atomic_rollback_executemany(sentry_init, client, capture_events): # Verify queries and rollback statements are siblings for insert_span in insert_spans: assert rollback_span["parent_span_id"] == insert_span["parent_span_id"] + + +@pytest.mark.forked +@pytest_mark_django_db_decorator(transaction=True) +def test_db_atomic_execute_exception(sentry_init, client, capture_events): + sentry_init( + integrations=[DjangoIntegration()], + send_default_pii=True, + traces_sample_rate=1.0, + ) + + if "postgres" not in connections: + pytest.skip("postgres tests disabled") + + # trigger Django to open a new connection by marking the existing one as None. + connections["postgres"].connection = None + + events = capture_events() + + client.get(reverse("postgres_insert_orm_atomic_exception")) + + (event,) = events + + # Ensure operation is rolled back + assert not User.objects.using("postgres").exists() + + assert event["contexts"]["trace"]["origin"] == "auto.http.django" + + rollback_spans = [ + span + for span in event["spans"] + if span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.ROLLBACK + ] + assert len(rollback_spans) == 1 + rollback_span = rollback_spans[0] + assert rollback_span["origin"] == "auto.db.django" + + # Verify other database attributes + assert rollback_span["data"].get(SPANDATA.DB_SYSTEM) == "postgresql" + conn_params = connections["postgres"].get_connection_params() + assert rollback_span["data"].get(SPANDATA.DB_NAME) is not None + assert rollback_span["data"].get(SPANDATA.DB_NAME) == conn_params.get( + "database" + ) or conn_params.get("dbname") + assert rollback_span["data"].get(SPANDATA.SERVER_ADDRESS) == os.environ.get( + "SENTRY_PYTHON_TEST_POSTGRES_HOST", "localhost" + ) + assert rollback_span["data"].get(SPANDATA.SERVER_PORT) == os.environ.get( + "SENTRY_PYTHON_TEST_POSTGRES_PORT", "5432" + ) + + insert_spans = [ + span for span in event["spans"] if span["description"].startswith("INSERT INTO") + ] + assert len(insert_spans) == 1 + insert_span = insert_spans[0] + + # Verify query and rollback statements are siblings + assert rollback_span["parent_span_id"] == insert_span["parent_span_id"] + + +@pytest.mark.forked +@pytest_mark_django_db_decorator(transaction=True) +def test_db_atomic_executemany_exception(sentry_init, client, capture_events): + sentry_init( + integrations=[DjangoIntegration()], + send_default_pii=True, + traces_sample_rate=1.0, + ) + + events = capture_events() + + with start_transaction(name="test_transaction"): + from django.db import connection, transaction + + try: + with transaction.atomic(): + cursor = connection.cursor() + + query = """INSERT INTO auth_user ( + password, + is_superuser, + username, + first_name, + last_name, + email, + is_staff, + is_active, + date_joined +) +VALUES ('password', false, %s, %s, %s, %s, false, true, %s);""" + + query_list = ( + ( + "user1", + "John", + "Doe", + "user1@example.com", + datetime(1970, 1, 1), + ), + ( + "user2", + "Max", + "Mustermann", + "user2@example.com", + datetime(1970, 1, 1), + ), + ) + cursor.executemany(query, query_list) + 1 / 0 + except ZeroDivisionError: + pass + + (event,) = events + + # Ensure operation is rolled back + assert not User.objects.exists() + + assert event["contexts"]["trace"]["origin"] == "manual" + + rollback_spans = [ + span + for span in event["spans"] + if span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.ROLLBACK + ] + assert len(rollback_spans) == 1 + rollback_span = rollback_spans[0] + assert rollback_span["origin"] == "auto.db.django" + + # Verify other database attributes + assert rollback_span["data"].get(SPANDATA.DB_SYSTEM) == "sqlite" + conn_params = connection.get_connection_params() + assert rollback_span["data"].get(SPANDATA.DB_NAME) is not None + assert rollback_span["data"].get(SPANDATA.DB_NAME) == conn_params.get( + "database" + ) or conn_params.get("dbname") + + insert_spans = [ + span for span in event["spans"] if span["description"].startswith("INSERT INTO") + ] + + # Verify queries and rollback statements are siblings + for insert_span in insert_spans: + assert rollback_span["parent_span_id"] == insert_span["parent_span_id"] From 3ec92c53dc4410d26d0fd23d349d4d872887248a Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 21 Nov 2025 12:42:46 +0100 Subject: [PATCH 07/14] try-finally for resetting autocommit --- tests/integrations/django/myapp/views.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/integrations/django/myapp/views.py b/tests/integrations/django/myapp/views.py index 5feab4eaa5..b9cb6076bb 100644 --- a/tests/integrations/django/myapp/views.py +++ b/tests/integrations/django/myapp/views.py @@ -264,11 +264,13 @@ def postgres_insert_orm_no_autocommit(request, *args, **kwargs): @csrf_exempt def postgres_insert_orm_no_autocommit_rollback(request, *args, **kwargs): transaction.set_autocommit(False, using="postgres") - user = User.objects.db_manager("postgres").create_user( - username="user1", - ) - transaction.rollback(using="postgres") - transaction.set_autocommit(True, using="postgres") + try: + user = User.objects.db_manager("postgres").create_user( + username="user1", + ) + transaction.rollback(using="postgres") + finally: + transaction.set_autocommit(True, using="postgres") return HttpResponse("ok {}".format(user)) From 88654e5bfdf158214e9fe0fb1b0b93676a5915ee Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 21 Nov 2025 12:45:59 +0100 Subject: [PATCH 08/14] . --- tests/integrations/django/myapp/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integrations/django/myapp/views.py b/tests/integrations/django/myapp/views.py index ffaa0e6df8..885f98f574 100644 --- a/tests/integrations/django/myapp/views.py +++ b/tests/integrations/django/myapp/views.py @@ -271,9 +271,11 @@ def postgres_insert_orm_no_autocommit_rollback(request, *args, **kwargs): username="user1", ) transaction.rollback(using="postgres") - finally: + except Exception: transaction.set_autocommit(True, using="postgres") + raise + transaction.set_autocommit(True, using="postgres") return HttpResponse("ok {}".format(user)) From 4c1dc4d871359e0bcb52bd712ed1444ee3256b84 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 24 Nov 2025 14:06:43 +0100 Subject: [PATCH 09/14] gate database transactions behind a flag --- sentry_sdk/integrations/django/__init__.py | 5 + .../django/test_db_transactions.py | 110 ++++++++++++++++-- 2 files changed, 107 insertions(+), 8 deletions(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index d983a28850..e165c71472 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -713,6 +713,11 @@ def _commit(self): @ensure_integration_enabled(DjangoIntegration, real_rollback) def _rollback(self): # type: (BaseDatabaseWrapper) -> None + integration = sentry_sdk.get_client().get_integration(DjangoIntegration) + + if integration is None or not integration.database_transaction_spans: + return real_rollback(self) + with sentry_sdk.start_span( op=OP.DB, name=DBOPERATION.ROLLBACK, diff --git a/tests/integrations/django/test_db_transactions.py b/tests/integrations/django/test_db_transactions.py index d84f34ad39..e5901c713c 100644 --- a/tests/integrations/django/test_db_transactions.py +++ b/tests/integrations/django/test_db_transactions.py @@ -44,6 +44,7 @@ def test_db_transaction_spans_disabled_no_autocommit( events = capture_events() + client.get(reverse("postgres_insert_orm_no_autocommit_rollback")) client.get(reverse("postgres_insert_orm_no_autocommit")) with start_transaction(name="test_transaction"): @@ -81,22 +82,69 @@ def test_db_transaction_spans_disabled_no_autocommit( ), ) + transaction.set_autocommit(False) + cursor.executemany(query, query_list) + transaction.rollback() + transaction.set_autocommit(True) + + with start_transaction(name="test_transaction"): + from django.db import connection, transaction + + cursor = connection.cursor() + + query = """INSERT INTO auth_user ( + password, + is_superuser, + username, + first_name, + last_name, + email, + is_staff, + is_active, + date_joined +) +VALUES ('password', false, %s, %s, %s, %s, false, true, %s);""" + + query_list = ( + ( + "user1", + "John", + "Doe", + "user1@example.com", + datetime(1970, 1, 1), + ), + ( + "user2", + "Max", + "Mustermann", + "user2@example.com", + datetime(1970, 1, 1), + ), + ) + transaction.set_autocommit(False) cursor.executemany(query, query_list) transaction.commit() transaction.set_autocommit(True) - (postgres_spans, sqlite_spans) = events + (postgres_rollback, postgres_commit, sqlite_rollback, sqlite_commit) = events # Ensure operation is persisted assert User.objects.using("postgres").exists() - assert postgres_spans["contexts"]["trace"]["origin"] == "auto.http.django" - assert sqlite_spans["contexts"]["trace"]["origin"] == "manual" + assert postgres_rollback["contexts"]["trace"]["origin"] == "auto.http.django" + assert postgres_commit["contexts"]["trace"]["origin"] == "auto.http.django" + assert sqlite_rollback["contexts"]["trace"]["origin"] == "manual" + assert sqlite_commit["contexts"]["trace"]["origin"] == "manual" commit_spans = [ span - for span in itertools.chain(postgres_spans["spans"], sqlite_spans["spans"]) + for span in itertools.chain( + postgres_rollback["spans"], + postgres_commit["spans"], + sqlite_rollback["spans"], + sqlite_commit["spans"], + ) if span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.COMMIT ] assert len(commit_spans) == 0 @@ -118,6 +166,7 @@ def test_db_transaction_spans_disabled_atomic(sentry_init, client, capture_event events = capture_events() + client.get(reverse("postgres_insert_orm_atomic_rollback")) client.get(reverse("postgres_insert_orm_atomic")) with start_transaction(name="test_transaction"): @@ -156,18 +205,63 @@ def test_db_transaction_spans_disabled_atomic(sentry_init, client, capture_event ), ) cursor.executemany(query, query_list) + transaction.set_rollback(True) + + with start_transaction(name="test_transaction"): + from django.db import connection, transaction - (postgres_spans, sqlite_spans) = events + with transaction.atomic(): + cursor = connection.cursor() + + query = """INSERT INTO auth_user ( + password, + is_superuser, + username, + first_name, + last_name, + email, + is_staff, + is_active, + date_joined +) +VALUES ('password', false, %s, %s, %s, %s, false, true, %s);""" + + query_list = ( + ( + "user1", + "John", + "Doe", + "user1@example.com", + datetime(1970, 1, 1), + ), + ( + "user2", + "Max", + "Mustermann", + "user2@example.com", + datetime(1970, 1, 1), + ), + ) + cursor.executemany(query, query_list) + + (postgres_rollback, postgres_commit, sqlite_rollback, sqlite_commit) = events # Ensure operation is persisted assert User.objects.using("postgres").exists() - assert postgres_spans["contexts"]["trace"]["origin"] == "auto.http.django" - assert sqlite_spans["contexts"]["trace"]["origin"] == "manual" + assert postgres_rollback["contexts"]["trace"]["origin"] == "auto.http.django" + assert postgres_commit["contexts"]["trace"]["origin"] == "auto.http.django" + assert sqlite_rollback["contexts"]["trace"]["origin"] == "manual" + assert sqlite_commit["contexts"]["trace"]["origin"] == "manual" commit_spans = [ span - for span in itertools.chain(postgres_spans["spans"], postgres_spans["spans"]) + for span in itertools.chain( + postgres_rollback["spans"], + postgres_commit["spans"], + sqlite_rollback["spans"], + sqlite_commit["spans"], + ) if span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.COMMIT ] assert len(commit_spans) == 0 From f3fffa1ce36390bee2355e061733e1088903e47e Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 24 Nov 2025 14:25:29 +0100 Subject: [PATCH 10/14] . --- tests/integrations/django/test_db_transactions.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/integrations/django/test_db_transactions.py b/tests/integrations/django/test_db_transactions.py index e5901c713c..40f6e0f2fc 100644 --- a/tests/integrations/django/test_db_transactions.py +++ b/tests/integrations/django/test_db_transactions.py @@ -413,7 +413,7 @@ def test_db_no_autocommit_executemany(sentry_init, client, capture_events): @pytest_mark_django_db_decorator(transaction=True) def test_db_no_autocommit_rollback_execute(sentry_init, client, capture_events): sentry_init( - integrations=[DjangoIntegration()], + integrations=[DjangoIntegration(database_transaction_spans=True)], traces_sample_rate=1.0, ) @@ -471,7 +471,7 @@ def test_db_no_autocommit_rollback_execute(sentry_init, client, capture_events): @pytest_mark_django_db_decorator(transaction=True) def test_db_no_autocommit_rollback_executemany(sentry_init, client, capture_events): sentry_init( - integrations=[DjangoIntegration()], + integrations=[DjangoIntegration(database_transaction_spans=True)], traces_sample_rate=1.0, ) @@ -694,7 +694,7 @@ def test_db_atomic_executemany(sentry_init, client, capture_events): @pytest_mark_django_db_decorator(transaction=True) def test_db_atomic_rollback_execute(sentry_init, client, capture_events): sentry_init( - integrations=[DjangoIntegration()], + integrations=[DjangoIntegration(database_transaction_spans=True)], send_default_pii=True, traces_sample_rate=1.0, ) @@ -753,7 +753,7 @@ def test_db_atomic_rollback_execute(sentry_init, client, capture_events): @pytest_mark_django_db_decorator(transaction=True) def test_db_atomic_rollback_executemany(sentry_init, client, capture_events): sentry_init( - integrations=[DjangoIntegration()], + integrations=[DjangoIntegration(database_transaction_spans=True)], send_default_pii=True, traces_sample_rate=1.0, ) @@ -835,7 +835,7 @@ def test_db_atomic_rollback_executemany(sentry_init, client, capture_events): @pytest_mark_django_db_decorator(transaction=True) def test_db_atomic_execute_exception(sentry_init, client, capture_events): sentry_init( - integrations=[DjangoIntegration()], + integrations=[DjangoIntegration(database_transaction_spans=True)], send_default_pii=True, traces_sample_rate=1.0, ) @@ -894,7 +894,7 @@ def test_db_atomic_execute_exception(sentry_init, client, capture_events): @pytest_mark_django_db_decorator(transaction=True) def test_db_atomic_executemany_exception(sentry_init, client, capture_events): sentry_init( - integrations=[DjangoIntegration()], + integrations=[DjangoIntegration(database_transaction_spans=True)], send_default_pii=True, traces_sample_rate=1.0, ) From 9bec32aad7afbdd1d9cce983bf3d96f3e9ca0dc1 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 24 Nov 2025 15:42:00 +0100 Subject: [PATCH 11/14] rollback on exception in sample app --- tests/integrations/django/myapp/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integrations/django/myapp/views.py b/tests/integrations/django/myapp/views.py index d185050374..2d35962eab 100644 --- a/tests/integrations/django/myapp/views.py +++ b/tests/integrations/django/myapp/views.py @@ -274,6 +274,7 @@ def postgres_insert_orm_no_autocommit_rollback(request, *args, **kwargs): transaction.rollback(using="postgres") except Exception: transaction.set_autocommit(True, using="postgres") + transaction.rollback() raise transaction.set_autocommit(True, using="postgres") From a0579ad8472ab7fc02f5987bb17f9e06422717ce Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 24 Nov 2025 15:47:02 +0100 Subject: [PATCH 12/14] use postgres in rollback on exception --- tests/integrations/django/myapp/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/django/myapp/views.py b/tests/integrations/django/myapp/views.py index 88603b6c5e..6d199a3740 100644 --- a/tests/integrations/django/myapp/views.py +++ b/tests/integrations/django/myapp/views.py @@ -273,8 +273,8 @@ def postgres_insert_orm_no_autocommit_rollback(request, *args, **kwargs): ) transaction.rollback(using="postgres") except Exception: + transaction.rollback(using="postgres") transaction.set_autocommit(True, using="postgres") - transaction.rollback() raise transaction.set_autocommit(True, using="postgres") From 34078678a7a3966a73d0bf9a2e90d095b21ee36c Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 24 Nov 2025 15:49:50 +0100 Subject: [PATCH 13/14] check no rollback spans when option disabled --- tests/integrations/django/test_db_transactions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/integrations/django/test_db_transactions.py b/tests/integrations/django/test_db_transactions.py index 40f6e0f2fc..a7f8e11e58 100644 --- a/tests/integrations/django/test_db_transactions.py +++ b/tests/integrations/django/test_db_transactions.py @@ -146,6 +146,7 @@ def test_db_transaction_spans_disabled_no_autocommit( sqlite_commit["spans"], ) if span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.COMMIT + or span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.ROLLBACK ] assert len(commit_spans) == 0 @@ -263,6 +264,7 @@ def test_db_transaction_spans_disabled_atomic(sentry_init, client, capture_event sqlite_commit["spans"], ) if span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.COMMIT + or span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.ROLLBACK ] assert len(commit_spans) == 0 From a53b5ee395595d8275f533193a6f92254ba6268d Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 24 Nov 2025 16:57:14 +0100 Subject: [PATCH 14/14] remove redundant annotation --- sentry_sdk/integrations/django/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 4bccff6ea9..c18a03a38c 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -709,7 +709,6 @@ def _commit(self): _set_db_data(span, self, SPANNAME.DB_COMMIT) return real_commit(self) - @ensure_integration_enabled(DjangoIntegration, real_rollback) def _rollback(self): # type: (BaseDatabaseWrapper) -> None integration = sentry_sdk.get_client().get_integration(DjangoIntegration)