Skip to content

Commit 223e862

Browse files
feat(django): Instrument database rollbacks
1 parent 943e3bb commit 223e862

File tree

5 files changed

+264
-0
lines changed

5 files changed

+264
-0
lines changed

sentry_sdk/consts.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ class INSTRUMENTER:
116116

117117
class DBOPERATION:
118118
COMMIT = "COMMIT"
119+
ROLLBACK = "ROLLBACK"
119120

120121

121122
class SPANDATA:

sentry_sdk/integrations/django/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,7 @@ def install_sql_hook():
634634
real_executemany = CursorWrapper.executemany
635635
real_connect = BaseDatabaseWrapper.connect
636636
real_commit = BaseDatabaseWrapper.commit
637+
real_rollback = BaseDatabaseWrapper.rollback
637638
except AttributeError:
638639
# This won't work on Django versions < 1.6
639640
return
@@ -702,11 +703,23 @@ def commit(self):
702703
_set_db_data(span, self, DBOPERATION.COMMIT)
703704
return real_commit(self)
704705

706+
@ensure_integration_enabled(DjangoIntegration, real_rollback)
707+
def rollback(self):
708+
# type: (BaseDatabaseWrapper) -> None
709+
with sentry_sdk.start_span(
710+
op=OP.DB,
711+
name=DBOPERATION.ROLLBACK,
712+
origin=DjangoIntegration.origin_db,
713+
) as span:
714+
_set_db_data(span, self, DBOPERATION.ROLLBACK)
715+
return real_rollback(self)
716+
705717
CursorWrapper.execute = execute
706718
CursorWrapper.executemany = executemany
707719
BaseDatabaseWrapper.connect = connect
708720
BaseDatabaseWrapper.commit = commit
709721
ignore_logger("django.db.backends")
722+
BaseDatabaseWrapper.rollback = rollback
710723

711724

712725
def _set_db_data(span, cursor_or_db, db_operation=None):

tests/integrations/django/myapp/urls.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,21 @@ def path(path, *args, **kwargs):
6666
views.postgres_insert_orm_no_autocommit,
6767
name="postgres_insert_orm_no_autocommit",
6868
),
69+
path(
70+
"postgres-insert-no-autocommit-rollback",
71+
views.postgres_insert_orm_no_autocommit_rollback,
72+
name="postgres_insert_orm_no_autocommit_rollback",
73+
),
6974
path(
7075
"postgres-insert-atomic",
7176
views.postgres_insert_orm_atomic,
7277
name="postgres_insert_orm_atomic",
7378
),
79+
path(
80+
"postgres-insert-atomic-rollback",
81+
views.postgres_insert_orm_atomic_rollback,
82+
name="postgres_insert_orm_atomic_rollback",
83+
),
7484
path(
7585
"postgres-select-slow-from-supplement",
7686
helper_views.postgres_select_orm,

tests/integrations/django/myapp/views.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,18 @@ def postgres_insert_orm_no_autocommit(request, *args, **kwargs):
259259
return HttpResponse("ok {}".format(user))
260260

261261

262+
@csrf_exempt
263+
def postgres_insert_orm_no_autocommit_rollback(request, *args, **kwargs):
264+
transaction.set_autocommit(False, using="postgres")
265+
user = User.objects.db_manager("postgres").create_user(
266+
username="user1",
267+
)
268+
transaction.rollback(using="postgres")
269+
transaction.set_autocommit(True, using="postgres")
270+
271+
return HttpResponse("ok {}".format(user))
272+
273+
262274
@csrf_exempt
263275
def postgres_insert_orm_atomic(request, *args, **kwargs):
264276
with transaction.atomic(using="postgres"):
@@ -268,6 +280,16 @@ def postgres_insert_orm_atomic(request, *args, **kwargs):
268280
return HttpResponse("ok {}".format(user))
269281

270282

283+
@csrf_exempt
284+
def postgres_insert_orm_atomic_rollback(request, *args, **kwargs):
285+
with transaction.atomic(using="postgres"):
286+
user = User.objects.db_manager("postgres").create_user(
287+
username="user1",
288+
)
289+
transaction.set_rollback(True, using="postgres")
290+
return HttpResponse("ok {}".format(user))
291+
292+
271293
@csrf_exempt
272294
def permission_denied_exc(*args, **kwargs):
273295
raise PermissionDenied("bye")

tests/integrations/django/test_db_query_data.py

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,118 @@ def test_db_no_autocommit_executemany(sentry_init, client, capture_events):
640640
assert commit_span["origin"] == "auto.db.django"
641641

642642

643+
@pytest.mark.forked
644+
@pytest_mark_django_db_decorator(transaction=True, databases=["postgres"])
645+
def test_db_no_autocommit_rollback_execute(sentry_init, client, capture_events):
646+
sentry_init(
647+
integrations=[DjangoIntegration()],
648+
traces_sample_rate=1.0,
649+
)
650+
651+
if "postgres" not in connections:
652+
pytest.skip("postgres tests disabled")
653+
654+
# trigger Django to open a new connection by marking the existing one as None.
655+
connections["postgres"].connection = None
656+
657+
events = capture_events()
658+
659+
client.get(reverse("postgres_insert_orm_no_autocommit_rollback"))
660+
661+
(event,) = events
662+
663+
# Ensure operation is rolled back
664+
assert not User.objects.using("postgres").exists()
665+
666+
assert event["contexts"]["trace"]["origin"] == "auto.http.django"
667+
668+
for span in event["spans"]:
669+
if span["op"] == "db":
670+
assert span["origin"] == "auto.db.django"
671+
else:
672+
assert span["origin"] == "auto.http.django"
673+
674+
commit_spans = [
675+
span
676+
for span in event["spans"]
677+
if span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.ROLLBACK
678+
]
679+
assert len(commit_spans) == 1
680+
commit_span = commit_spans[0]
681+
assert commit_span["origin"] == "auto.db.django"
682+
683+
684+
@pytest.mark.forked
685+
@pytest_mark_django_db_decorator(transaction=True)
686+
def test_db_no_autocommit_rollback_executemany(sentry_init, client, capture_events):
687+
sentry_init(
688+
integrations=[DjangoIntegration()],
689+
traces_sample_rate=1.0,
690+
)
691+
692+
events = capture_events()
693+
694+
if "postgres" not in connections:
695+
pytest.skip("postgres tests disabled")
696+
697+
with start_transaction(name="test_transaction"):
698+
from django.db import connection, transaction
699+
700+
cursor = connection.cursor()
701+
702+
query = """INSERT INTO auth_user (
703+
password,
704+
is_superuser,
705+
username,
706+
first_name,
707+
last_name,
708+
email,
709+
is_staff,
710+
is_active,
711+
date_joined
712+
)
713+
VALUES ('password', false, %s, %s, %s, %s, false, true, %s);"""
714+
715+
query_list = (
716+
(
717+
"user1",
718+
"John",
719+
"Doe",
720+
721+
datetime(1970, 1, 1),
722+
),
723+
(
724+
"user2",
725+
"Max",
726+
"Mustermann",
727+
728+
datetime(1970, 1, 1),
729+
),
730+
)
731+
732+
transaction.set_autocommit(False)
733+
cursor.executemany(query, query_list)
734+
transaction.rollback()
735+
transaction.set_autocommit(True)
736+
737+
(event,) = events
738+
739+
# Ensure operation is rolled back
740+
assert not User.objects.exists()
741+
742+
assert event["contexts"]["trace"]["origin"] == "manual"
743+
assert event["spans"][0]["origin"] == "auto.db.django"
744+
745+
commit_spans = [
746+
span
747+
for span in event["spans"]
748+
if span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.ROLLBACK
749+
]
750+
assert len(commit_spans) == 1
751+
commit_span = commit_spans[0]
752+
assert commit_span["origin"] == "auto.db.django"
753+
754+
643755
@pytest.mark.forked
644756
@pytest_mark_django_db_decorator(transaction=True, databases=["postgres"])
645757
def test_db_atomic_execute(sentry_init, client, capture_events):
@@ -743,3 +855,109 @@ def test_db_atomic_executemany(sentry_init, client, capture_events):
743855
assert len(commit_spans) == 1
744856
commit_span = commit_spans[0]
745857
assert commit_span["origin"] == "auto.db.django"
858+
859+
860+
@pytest.mark.forked
861+
@pytest_mark_django_db_decorator(transaction=True, databases=["postgres"])
862+
def test_db_atomic_rollback_execute(sentry_init, client, capture_events):
863+
sentry_init(
864+
integrations=[DjangoIntegration()],
865+
send_default_pii=True,
866+
traces_sample_rate=1.0,
867+
)
868+
869+
if "postgres" not in connections:
870+
pytest.skip("postgres tests disabled")
871+
872+
# trigger Django to open a new connection by marking the existing one as None.
873+
connections["postgres"].connection = None
874+
875+
events = capture_events()
876+
877+
client.get(reverse("postgres_insert_orm_atomic_rollback"))
878+
879+
(event,) = events
880+
881+
# Ensure operation is rolled back
882+
assert not User.objects.using("postgres").exists()
883+
884+
assert event["contexts"]["trace"]["origin"] == "auto.http.django"
885+
886+
commit_spans = [
887+
span
888+
for span in event["spans"]
889+
if span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.ROLLBACK
890+
]
891+
assert len(commit_spans) == 1
892+
commit_span = commit_spans[0]
893+
assert commit_span["origin"] == "auto.db.django"
894+
895+
896+
@pytest.mark.forked
897+
@pytest_mark_django_db_decorator(transaction=True)
898+
def test_db_atomic_rollback_executemany(sentry_init, client, capture_events):
899+
sentry_init(
900+
integrations=[DjangoIntegration()],
901+
send_default_pii=True,
902+
traces_sample_rate=1.0,
903+
)
904+
905+
if "postgres" not in connections:
906+
pytest.skip("postgres tests disabled")
907+
908+
# trigger Django to open a new connection by marking the existing one as None.
909+
connections["postgres"].connection = None
910+
911+
events = capture_events()
912+
913+
with start_transaction(name="test_transaction"):
914+
with transaction.atomic():
915+
cursor = connection.cursor()
916+
917+
query = """INSERT INTO auth_user (
918+
password,
919+
is_superuser,
920+
username,
921+
first_name,
922+
last_name,
923+
email,
924+
is_staff,
925+
is_active,
926+
date_joined
927+
)
928+
VALUES ('password', false, %s, %s, %s, %s, false, true, %s);"""
929+
930+
query_list = (
931+
(
932+
"user1",
933+
"John",
934+
"Doe",
935+
936+
datetime(1970, 1, 1),
937+
),
938+
(
939+
"user2",
940+
"Max",
941+
"Mustermann",
942+
943+
datetime(1970, 1, 1),
944+
),
945+
)
946+
cursor.executemany(query, query_list)
947+
transaction.set_rollback(True)
948+
949+
(event,) = events
950+
951+
# Ensure operation is rolled back
952+
assert not User.objects.exists()
953+
954+
assert event["contexts"]["trace"]["origin"] == "manual"
955+
956+
commit_spans = [
957+
span
958+
for span in event["spans"]
959+
if span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.ROLLBACK
960+
]
961+
assert len(commit_spans) == 1
962+
commit_span = commit_spans[0]
963+
assert commit_span["origin"] == "auto.db.django"

0 commit comments

Comments
 (0)