Skip to content
4 changes: 4 additions & 0 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ class INSTRUMENTER:
OTEL = "otel"


class DBOPERATION:
COMMIT = "COMMIT"


class SPANDATA:
"""
Additional information describing the type of the span.
Expand Down
22 changes: 19 additions & 3 deletions sentry_sdk/integrations/django/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from importlib import import_module

import sentry_sdk
from sentry_sdk.consts import OP, SPANDATA
from sentry_sdk.consts import OP, SPANDATA, DBOPERATION
from sentry_sdk.scope import add_global_event_processor, should_send_default_pii
from sentry_sdk.serializer import add_global_repr_processor, add_repr_sequence_type
from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource
Expand Down Expand Up @@ -633,6 +633,7 @@ def install_sql_hook():
real_execute = CursorWrapper.execute
real_executemany = CursorWrapper.executemany
real_connect = BaseDatabaseWrapper.connect
real_commit = BaseDatabaseWrapper._commit
except AttributeError:
# This won't work on Django versions < 1.6
return
Expand Down Expand Up @@ -690,18 +691,33 @@ def connect(self):
_set_db_data(span, self)
return real_connect(self)

@ensure_integration_enabled(DjangoIntegration, real_commit)
def _commit(self):
# type: (BaseDatabaseWrapper) -> None
with sentry_sdk.start_span(
op=OP.DB,
name=DBOPERATION.COMMIT,
origin=DjangoIntegration.origin_db,
) as span:
_set_db_data(span, self, DBOPERATION.COMMIT)
return real_commit(self)

CursorWrapper.execute = execute
CursorWrapper.executemany = executemany
BaseDatabaseWrapper.connect = connect
BaseDatabaseWrapper._commit = _commit
ignore_logger("django.db.backends")


def _set_db_data(span, cursor_or_db):
# type: (Span, Any) -> None
def _set_db_data(span, cursor_or_db, db_operation=None):
# type: (Span, Any, Optional[str]) -> None
db = cursor_or_db.db if hasattr(cursor_or_db, "db") else cursor_or_db
vendor = db.vendor
span.set_data(SPANDATA.DB_SYSTEM, vendor)

if db_operation is not None:
span.set_data(SPANDATA.DB_OPERATION, db_operation)

# Some custom backends override `__getattr__`, making it look like `cursor_or_db`
# actually has a `connection` and the `connection` has a `get_dsn_parameters`
# attribute, only to throw an error once you actually want to call it.
Expand Down
10 changes: 10 additions & 0 deletions tests/integrations/django/myapp/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ def path(path, *args, **kwargs):
path("template-test4", views.template_test4, name="template_test4"),
path("postgres-select", views.postgres_select, name="postgres_select"),
path("postgres-select-slow", views.postgres_select_orm, name="postgres_select_orm"),
path(
"postgres-insert-no-autocommit",
views.postgres_insert_orm_no_autocommit,
name="postgres_insert_orm_no_autocommit",
),
path(
"postgres-insert-atomic",
views.postgres_insert_orm_atomic,
name="postgres_insert_orm_atomic",
),
path(
"postgres-select-slow-from-supplement",
helper_views.postgres_select_orm,
Expand Down
22 changes: 22 additions & 0 deletions tests/integrations/django/myapp/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import json
import threading

from django.db import transaction
from django.contrib.auth import login
from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied
Expand Down Expand Up @@ -246,6 +247,27 @@ def postgres_select_orm(request, *args, **kwargs):
return HttpResponse("ok {}".format(user))


@csrf_exempt
def postgres_insert_orm_no_autocommit(request, *args, **kwargs):
transaction.set_autocommit(False, using="postgres")
user = User.objects.db_manager("postgres").create_user(
username="user1",
)
transaction.commit(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"):
user = User.objects.db_manager("postgres").create_user(
username="user1",
)
return HttpResponse("ok {}".format(user))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Duplicate username causes IntegrityError on repeated calls

Both postgres_insert_orm_no_autocommit and postgres_insert_orm_atomic views create users with the hardcoded username "user1". When these endpoints are called more than once (either the same endpoint twice or both endpoints once), Django raises an IntegrityError because usernames must be unique. This prevents manual testing as described in the PR description and makes the endpoints non-idempotent. The views need to either generate unique usernames, delete existing users first, or handle the integrity constraint violation.

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests should be as deterministic as possible. The sample is intended to be very minimal.



@csrf_exempt
def permission_denied_exc(*args, **kwargs):
raise PermissionDenied("bye")
Expand Down
Loading