From b5a427fa3b314b61e0d81211f624a796ec2e531a Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 11 May 2026 08:54:52 +0200 Subject: [PATCH 1/9] feat(django): Support span streaming --- sentry_sdk/integrations/django/__init__.py | 131 +- sentry_sdk/integrations/django/asgi.py | 26 +- sentry_sdk/integrations/django/caching.py | 158 +- sentry_sdk/integrations/django/middleware.py | 30 +- .../integrations/django/signals_handlers.py | 30 +- sentry_sdk/integrations/django/tasks.py | 22 +- sentry_sdk/integrations/django/templates.py | 55 +- sentry_sdk/integrations/django/views.py | 51 +- tests/integrations/django/asgi/test_asgi.py | 642 ++++-- tests/integrations/django/test_basic.py | 1905 ++++++++++++---- .../integrations/django/test_cache_module.py | 1031 ++++++--- .../django/test_data_scrubbing.py | 42 +- .../integrations/django/test_db_query_data.py | 922 ++++++-- .../django/test_db_transactions.py | 1968 ++++++++++++----- tests/integrations/django/test_tasks.py | 261 ++- 15 files changed, 5405 insertions(+), 1869 deletions(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index b25944c8ea..5a4ea6febd 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -9,7 +9,12 @@ 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 -from sentry_sdk.tracing_utils import add_query_source, record_sql_queries +from sentry_sdk.traces import StreamedSpan +from sentry_sdk.tracing_utils import ( + add_query_source, + record_sql_queries_supporting_streaming, + has_span_streaming_enabled, +) from sentry_sdk.utils import ( AnnotatedValue, HAS_REAL_CONTEXTVARS, @@ -86,6 +91,7 @@ from django.utils.datastructures import MultiValueDict from sentry_sdk.tracing import Span + from sentry_sdk.traces import StreamedSpan from sentry_sdk.integrations.wsgi import _ScopedResponse from sentry_sdk._types import Event, Hint, EventProcessor, NotImplementedType @@ -633,7 +639,7 @@ def install_sql_hook() -> None: def execute( self: "CursorWrapper", sql: "Any", params: "Optional[Any]" = None ) -> "Any": - with record_sql_queries( + with record_sql_queries_supporting_streaming( cursor=self.cursor, query=sql, params_list=params, @@ -653,7 +659,7 @@ def execute( def executemany( self: "CursorWrapper", sql: "Any", param_list: "List[Any]" ) -> "Any": - with record_sql_queries( + with record_sql_queries_supporting_streaming( cursor=self.cursor, query=sql, params_list=param_list, @@ -675,13 +681,27 @@ def connect(self: "BaseDatabaseWrapper") -> None: with capture_internal_exceptions(): sentry_sdk.add_breadcrumb(message="connect", category="query") - with sentry_sdk.start_span( - op=OP.DB, - name="connect", - origin=DjangoIntegration.origin_db, - ) as span: - _set_db_data(span, self) - return real_connect(self) + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + if span_streaming: + with sentry_sdk.traces.start_span( + name="connect", + attributes={ + "sentry.op": OP.DB, + "sentry.origin": DjangoIntegration.origin_db, + }, + ) as span: + _set_db_data(span, self) + return real_connect(self) + else: + with sentry_sdk.start_span( + op=OP.DB, + name="connect", + origin=DjangoIntegration.origin_db, + ) as span: + _set_db_data(span, self) + return real_connect(self) def _commit(self: "BaseDatabaseWrapper") -> None: integration = sentry_sdk.get_client().get_integration(DjangoIntegration) @@ -689,13 +709,27 @@ def _commit(self: "BaseDatabaseWrapper") -> None: if integration is None or not integration.db_transaction_spans: return real_commit(self) - with sentry_sdk.start_span( - op=OP.DB, - name=SPANNAME.DB_COMMIT, - origin=DjangoIntegration.origin_db, - ) as span: - _set_db_data(span, self, SPANNAME.DB_COMMIT) - return real_commit(self) + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + if span_streaming: + with sentry_sdk.traces.start_span( + name=SPANNAME.DB_COMMIT, + attributes={ + "sentry.op": OP.DB, + "sentry.origin": DjangoIntegration.origin_db, + }, + ) as span: + _set_db_data(span, self, SPANNAME.DB_COMMIT) + return real_commit(self) + else: + with sentry_sdk.start_span( + op=OP.DB, + name=SPANNAME.DB_COMMIT, + origin=DjangoIntegration.origin_db, + ) as span: + _set_db_data(span, self, SPANNAME.DB_COMMIT) + return real_commit(self) def _rollback(self: "BaseDatabaseWrapper") -> None: integration = sentry_sdk.get_client().get_integration(DjangoIntegration) @@ -703,13 +737,27 @@ def _rollback(self: "BaseDatabaseWrapper") -> None: if integration is None or not integration.db_transaction_spans: return real_rollback(self) - with sentry_sdk.start_span( - op=OP.DB, - name=SPANNAME.DB_ROLLBACK, - origin=DjangoIntegration.origin_db, - ) as span: - _set_db_data(span, self, SPANNAME.DB_ROLLBACK) - return real_rollback(self) + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + if span_streaming: + with sentry_sdk.traces.start_span( + name=SPANNAME.DB_ROLLBACK, + attributes={ + "sentry.op": OP.DB, + "sentry.origin": DjangoIntegration.origin_db, + }, + ) as span: + _set_db_data(span, self, SPANNAME.DB_ROLLBACK) + return real_rollback(self) + else: + with sentry_sdk.start_span( + op=OP.DB, + name=SPANNAME.DB_ROLLBACK, + origin=DjangoIntegration.origin_db, + ) as span: + _set_db_data(span, self, SPANNAME.DB_ROLLBACK) + return real_rollback(self) CursorWrapper.execute = execute CursorWrapper.executemany = executemany @@ -720,14 +768,22 @@ def _rollback(self: "BaseDatabaseWrapper") -> None: def _set_db_data( - span: "Span", cursor_or_db: "Any", db_operation: "Optional[str]" = None + span: "Union[Span, StreamedSpan]", + cursor_or_db: "Any", + db_operation: "Optional[str]" = None, ) -> 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 isinstance(span, StreamedSpan): + span.set_attribute(SPANDATA.DB_SYSTEM_NAME, vendor) + + if db_operation is not None: + span.set_attribute(SPANDATA.DB_OPERATION_NAME, db_operation) + else: + span.set_data(SPANDATA.DB_SYSTEM, vendor) - if db_operation is not None: - span.set_data(SPANDATA.DB_OPERATION, db_operation) + 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` @@ -759,20 +815,29 @@ def _set_db_data( connection_params = db.get_connection_params() db_name = connection_params.get("dbname") or connection_params.get("database") - if db_name is not None: - span.set_data(SPANDATA.DB_NAME, db_name) + + if isinstance(span, StreamedSpan): + if db_name is not None: + span.set_attribute(SPANDATA.DB_NAMESPACE, db_name) + + set_on_span = span.set_attribute + else: + if db_name is not None: + span.set_data(SPANDATA.DB_NAME, db_name) + + set_on_span = span.set_data server_address = connection_params.get("host") if server_address is not None: - span.set_data(SPANDATA.SERVER_ADDRESS, server_address) + set_on_span(SPANDATA.SERVER_ADDRESS, server_address) server_port = connection_params.get("port") if server_port is not None: - span.set_data(SPANDATA.SERVER_PORT, str(server_port)) + set_on_span(SPANDATA.SERVER_PORT, str(server_port)) server_socket_address = connection_params.get("unix_socket") if server_socket_address is not None: - span.set_data(SPANDATA.SERVER_SOCKET_ADDRESS, server_socket_address) + set_on_span(SPANDATA.SERVER_SOCKET_ADDRESS, server_socket_address) def add_template_context_repr_sequence() -> None: diff --git a/sentry_sdk/integrations/django/asgi.py b/sentry_sdk/integrations/django/asgi.py index 3e15bf592e..a9adf87f20 100644 --- a/sentry_sdk/integrations/django/asgi.py +++ b/sentry_sdk/integrations/django/asgi.py @@ -21,6 +21,7 @@ capture_internal_exceptions, ensure_integration_enabled, ) +from sentry_sdk.tracing_utils import has_span_streaming_enabled from typing import TYPE_CHECKING @@ -182,12 +183,25 @@ async def sentry_wrapped_callback( if not integration or not integration.middleware_spans: return await callback(request, *args, **kwargs) - with sentry_sdk.start_span( - op=OP.VIEW_RENDER, - name=request.resolver_match.view_name, - origin=DjangoIntegration.origin, - ): - return await callback(request, *args, **kwargs) + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + if span_streaming: + with sentry_sdk.traces.start_span( + name=request.resolver_match.view_name, + attributes={ + "sentry.op": OP.VIEW_RENDER, + "sentry.origin": DjangoIntegration.origin, + }, + ): + return await callback(request, *args, **kwargs) + else: + with sentry_sdk.start_span( + op=OP.VIEW_RENDER, + name=request.resolver_match.view_name, + origin=DjangoIntegration.origin, + ): + return await callback(request, *args, **kwargs) return sentry_wrapped_callback diff --git a/sentry_sdk/integrations/django/caching.py b/sentry_sdk/integrations/django/caching.py index 2ea49a2fa1..ce1139fa16 100644 --- a/sentry_sdk/integrations/django/caching.py +++ b/sentry_sdk/integrations/django/caching.py @@ -12,6 +12,7 @@ capture_internal_exceptions, ensure_integration_enabled, ) +from sentry_sdk.tracing_utils import has_span_streaming_enabled if TYPE_CHECKING: @@ -61,56 +62,113 @@ def _instrument_call( op = OP.CACHE_PUT if is_set_operation else OP.CACHE_GET description = _get_span_description(method_name, args, kwargs) - with sentry_sdk.start_span( - op=op, - name=description, - origin=DjangoIntegration.origin, - ) as span: - value = original_method(*args, **kwargs) - - with capture_internal_exceptions(): - if address is not None: - span.set_data(SPANDATA.NETWORK_PEER_ADDRESS, address) - - if port is not None: - span.set_data(SPANDATA.NETWORK_PEER_PORT, port) - - key = _get_safe_key(method_name, args, kwargs) - if key is not None: - span.set_data(SPANDATA.CACHE_KEY, key) - - item_size = None - if is_get_many_method: - if value != {}: - item_size = len(str(value)) - span.set_data(SPANDATA.CACHE_HIT, True) - else: - span.set_data(SPANDATA.CACHE_HIT, False) - elif is_get_method: - default_value = None - if len(args) >= 2: - default_value = args[1] - elif "default" in kwargs: - default_value = kwargs["default"] - - if value != default_value: - item_size = len(str(value)) - span.set_data(SPANDATA.CACHE_HIT, True) - else: - span.set_data(SPANDATA.CACHE_HIT, False) - else: # TODO: We don't handle `get_or_set` which we should - arg_count = len(args) - if arg_count >= 2: - # 'set' command - item_size = len(str(args[1])) - elif arg_count == 1: - # 'set_many' command - item_size = len(str(args[0])) - - if item_size is not None: - span.set_data(SPANDATA.CACHE_ITEM_SIZE, item_size) - - return value + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + if span_streaming: + with sentry_sdk.traces.start_span( + name=description, + attributes={ + "sentry.op": op, + "sentry.origin": DjangoIntegration.origin, + }, + ) as span: + value = original_method(*args, **kwargs) + + with capture_internal_exceptions(): + if address is not None: + span.set_attribute(SPANDATA.NETWORK_PEER_ADDRESS, address) + + if port is not None: + span.set_attribute(SPANDATA.NETWORK_PEER_PORT, port) + + key = _get_safe_key(method_name, args, kwargs) + if key is not None: + span.set_attribute(SPANDATA.CACHE_KEY, key) + + item_size = None + if is_get_many_method: + if value != {}: + item_size = len(str(value)) + span.set_attribute(SPANDATA.CACHE_HIT, True) + else: + span.set_attribute(SPANDATA.CACHE_HIT, False) + elif is_get_method: + default_value = None + if len(args) >= 2: + default_value = args[1] + elif "default" in kwargs: + default_value = kwargs["default"] + + if value != default_value: + item_size = len(str(value)) + span.set_attribute(SPANDATA.CACHE_HIT, True) + else: + span.set_attribute(SPANDATA.CACHE_HIT, False) + else: # TODO: We don't handle `get_or_set` which we should + arg_count = len(args) + if arg_count >= 2: + # 'set' command + item_size = len(str(args[1])) + elif arg_count == 1: + # 'set_many' command + item_size = len(str(args[0])) + + if item_size is not None: + span.set_attribute(SPANDATA.CACHE_ITEM_SIZE, item_size) + + return value + else: + with sentry_sdk.start_span( + op=op, + name=description, + origin=DjangoIntegration.origin, + ) as span: + value = original_method(*args, **kwargs) + + with capture_internal_exceptions(): + if address is not None: + span.set_data(SPANDATA.NETWORK_PEER_ADDRESS, address) + + if port is not None: + span.set_data(SPANDATA.NETWORK_PEER_PORT, port) + + key = _get_safe_key(method_name, args, kwargs) + if key is not None: + span.set_data(SPANDATA.CACHE_KEY, key) + + item_size = None + if is_get_many_method: + if value != {}: + item_size = len(str(value)) + span.set_data(SPANDATA.CACHE_HIT, True) + else: + span.set_data(SPANDATA.CACHE_HIT, False) + elif is_get_method: + default_value = None + if len(args) >= 2: + default_value = args[1] + elif "default" in kwargs: + default_value = kwargs["default"] + + if value != default_value: + item_size = len(str(value)) + span.set_data(SPANDATA.CACHE_HIT, True) + else: + span.set_data(SPANDATA.CACHE_HIT, False) + else: # TODO: We don't handle `get_or_set` which we should + arg_count = len(args) + if arg_count >= 2: + # 'set' command + item_size = len(str(args[1])) + elif arg_count == 1: + # 'set_many' command + item_size = len(str(args[0])) + + if item_size is not None: + span.set_data(SPANDATA.CACHE_ITEM_SIZE, item_size) + + return value @functools.wraps(original_method) def sentry_method(*args: "Any", **kwargs: "Any") -> "Any": diff --git a/sentry_sdk/integrations/django/middleware.py b/sentry_sdk/integrations/django/middleware.py index 94c0decf87..d04941005e 100644 --- a/sentry_sdk/integrations/django/middleware.py +++ b/sentry_sdk/integrations/django/middleware.py @@ -7,12 +7,13 @@ from django import VERSION as DJANGO_VERSION import sentry_sdk -from sentry_sdk.consts import OP +from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.utils import ( ContextVar, transaction_from_function, capture_internal_exceptions, ) +from sentry_sdk.tracing_utils import has_span_streaming_enabled from typing import TYPE_CHECKING @@ -80,13 +81,26 @@ def _check_middleware_span(old_method: "Callable[..., Any]") -> "Optional[Span]" if function_basename: description = "{}.{}".format(description, function_basename) - middleware_span = sentry_sdk.start_span( - op=OP.MIDDLEWARE_DJANGO, - name=description, - origin=DjangoIntegration.origin, - ) - middleware_span.set_tag("django.function_name", function_name) - middleware_span.set_tag("django.middleware_name", middleware_name) + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + if span_streaming: + middleware_span = sentry_sdk.traces.start_span( + name=description, + attributes={ + "sentry.op": OP.MIDDLEWARE_DJANGO, + "sentry.origin": DjangoIntegration.origin, + }, + ) + middleware_span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name) + else: + middleware_span = sentry_sdk.start_span( + op=OP.MIDDLEWARE_DJANGO, + name=description, + origin=DjangoIntegration.origin, + ) + middleware_span.set_tag("django.function_name", function_name) + middleware_span.set_tag("django.middleware_name", middleware_name) return middleware_span diff --git a/sentry_sdk/integrations/django/signals_handlers.py b/sentry_sdk/integrations/django/signals_handlers.py index 0c834ff8c6..c540da6062 100644 --- a/sentry_sdk/integrations/django/signals_handlers.py +++ b/sentry_sdk/integrations/django/signals_handlers.py @@ -5,6 +5,7 @@ import sentry_sdk from sentry_sdk.consts import OP from sentry_sdk.integrations.django import DJANGO_VERSION +from sentry_sdk.tracing_utils import has_span_streaming_enabled from typing import TYPE_CHECKING @@ -63,13 +64,28 @@ def sentry_sync_receiver_wrapper( @wraps(receiver) def wrapper(*args: "Any", **kwargs: "Any") -> "Any": signal_name = _get_receiver_name(receiver) - with sentry_sdk.start_span( - op=OP.EVENT_DJANGO, - name=signal_name, - origin=DjangoIntegration.origin, - ) as span: - span.set_data("signal", signal_name) - return receiver(*args, **kwargs) + + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + if span_streaming: + with sentry_sdk.traces.start_span( + name=signal_name, + attributes={ + "sentry.op": OP.EVENT_DJANGO, + "sentry.origin": DjangoIntegration.origin, + }, + ) as span: + span.set_attribute("signal", signal_name) + return receiver(*args, **kwargs) + else: + with sentry_sdk.start_span( + op=OP.EVENT_DJANGO, + name=signal_name, + origin=DjangoIntegration.origin, + ) as span: + span.set_data("signal", signal_name) + return receiver(*args, **kwargs) return wrapper diff --git a/sentry_sdk/integrations/django/tasks.py b/sentry_sdk/integrations/django/tasks.py index e6adb914e8..3e3063a288 100644 --- a/sentry_sdk/integrations/django/tasks.py +++ b/sentry_sdk/integrations/django/tasks.py @@ -3,6 +3,7 @@ import sentry_sdk from sentry_sdk.consts import OP from sentry_sdk.utils import qualname_from_function +from sentry_sdk.tracing_utils import has_span_streaming_enabled try: # django.tasks were added in Django 6.0 @@ -32,9 +33,22 @@ def _sentry_enqueue(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": name = qualname_from_function(self.func) or "" - with sentry_sdk.start_span( - op=OP.QUEUE_SUBMIT_DJANGO, name=name, origin=DjangoIntegration.origin - ): - return old_task_enqueue(self, *args, **kwargs) + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + if span_streaming: + with sentry_sdk.traces.start_span( + name=name, + attributes={ + "sentry.op": OP.QUEUE_SUBMIT_DJANGO, + "sentry.origin": DjangoIntegration.origin, + }, + ): + return old_task_enqueue(self, *args, **kwargs) + else: + with sentry_sdk.start_span( + op=OP.QUEUE_SUBMIT_DJANGO, name=name, origin=DjangoIntegration.origin + ): + return old_task_enqueue(self, *args, **kwargs) Task.enqueue = _sentry_enqueue diff --git a/sentry_sdk/integrations/django/templates.py b/sentry_sdk/integrations/django/templates.py index c8ca6682fe..a2e6bda978 100644 --- a/sentry_sdk/integrations/django/templates.py +++ b/sentry_sdk/integrations/django/templates.py @@ -7,6 +7,7 @@ import sentry_sdk from sentry_sdk.consts import OP from sentry_sdk.utils import ensure_integration_enabled +from sentry_sdk.tracing_utils import has_span_streaming_enabled from typing import TYPE_CHECKING @@ -65,13 +66,26 @@ def patch_templates() -> None: @property # type: ignore @ensure_integration_enabled(DjangoIntegration, real_rendered_content.fget) def rendered_content(self: "SimpleTemplateResponse") -> str: - with sentry_sdk.start_span( - op=OP.TEMPLATE_RENDER, - name=_get_template_name_description(self.template_name), - origin=DjangoIntegration.origin, - ) as span: - span.set_data("context", self.context_data) - return real_rendered_content.fget(self) + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + if span_streaming: + with sentry_sdk.traces.start_span( + name=_get_template_name_description(self.template_name), + attributes={ + "sentry.op": OP.TEMPLATE_RENDER, + "sentry.origin": DjangoIntegration.origin, + }, + ) as span: + return real_rendered_content.fget(self) + else: + with sentry_sdk.start_span( + op=OP.TEMPLATE_RENDER, + name=_get_template_name_description(self.template_name), + origin=DjangoIntegration.origin, + ) as span: + span.set_data("context", self.context_data) + return real_rendered_content.fget(self) SimpleTemplateResponse.rendered_content = rendered_content @@ -97,13 +111,26 @@ def render( sentry_sdk.get_current_scope().trace_propagation_meta() ) - with sentry_sdk.start_span( - op=OP.TEMPLATE_RENDER, - name=_get_template_name_description(template_name), - origin=DjangoIntegration.origin, - ) as span: - span.set_data("context", context) - return real_render(request, template_name, context, *args, **kwargs) + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + if span_streaming: + with sentry_sdk.traces.start_span( + name=_get_template_name_description(template_name), + attributes={ + "sentry.op": OP.TEMPLATE_RENDER, + "sentry.origin": DjangoIntegration.origin, + }, + ) as span: + return real_render(request, template_name, context, *args, **kwargs) + else: + with sentry_sdk.start_span( + op=OP.TEMPLATE_RENDER, + name=_get_template_name_description(template_name), + origin=DjangoIntegration.origin, + ) as span: + span.set_data("context", context) + return real_render(request, template_name, context, *args, **kwargs) django.shortcuts.render = render diff --git a/sentry_sdk/integrations/django/views.py b/sentry_sdk/integrations/django/views.py index c9e370029e..a13599a4be 100644 --- a/sentry_sdk/integrations/django/views.py +++ b/sentry_sdk/integrations/django/views.py @@ -2,6 +2,7 @@ import sentry_sdk from sentry_sdk.consts import OP +from sentry_sdk.tracing_utils import has_span_streaming_enabled from typing import TYPE_CHECKING @@ -30,12 +31,25 @@ def patch_views() -> None: old_render = SimpleTemplateResponse.render def sentry_patched_render(self: "SimpleTemplateResponse") -> "Any": - with sentry_sdk.start_span( - op=OP.VIEW_RESPONSE_RENDER, - name="serialize response", - origin=DjangoIntegration.origin, - ): - return old_render(self) + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + if span_streaming: + with sentry_sdk.traces.start_span( + name="serialize response", + attributes={ + "sentry.op": OP.VIEW_RESPONSE_RENDER, + "sentry.origin": DjangoIntegration.origin, + }, + ): + return old_render(self) + else: + with sentry_sdk.start_span( + op=OP.VIEW_RESPONSE_RENDER, + name="serialize response", + origin=DjangoIntegration.origin, + ): + return old_render(self) @functools.wraps(old_make_view_atomic) def sentry_patched_make_view_atomic( @@ -86,11 +100,24 @@ def sentry_wrapped_callback(request: "Any", *args: "Any", **kwargs: "Any") -> "A if not integration or not integration.middleware_spans: return callback(request, *args, **kwargs) - with sentry_sdk.start_span( - op=OP.VIEW_RENDER, - name=request.resolver_match.view_name, - origin=DjangoIntegration.origin, - ): - return callback(request, *args, **kwargs) + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + + if span_streaming: + with sentry_sdk.traces.start_span( + name=request.resolver_match.view_name, + attributes={ + "sentry.op": OP.VIEW_RENDER, + "sentry.origin": DjangoIntegration.origin, + }, + ): + return callback(request, *args, **kwargs) + else: + with sentry_sdk.start_span( + op=OP.VIEW_RENDER, + name=request.resolver_match.view_name, + origin=DjangoIntegration.origin, + ): + return callback(request, *args, **kwargs) return sentry_wrapped_callback diff --git a/tests/integrations/django/asgi/test_asgi.py b/tests/integrations/django/asgi/test_asgi.py index 174ceb77bb..b7515f300f 100644 --- a/tests/integrations/django/asgi/test_asgi.py +++ b/tests/integrations/django/asgi/test_asgi.py @@ -9,7 +9,10 @@ import django import pytest from channels.testing import HttpCommunicator + +import sentry_sdk from sentry_sdk import capture_message +from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.django.asgi import _asgi_middleware_mixin_factory from tests.integrations.django.myapp.asgi import channels_application @@ -33,54 +36,105 @@ @pytest.mark.skipif( django.VERSION < (3, 0), reason="Django ASGI support shipped in 3.0" ) -async def test_basic(sentry_init, capture_events, application): +@pytest.mark.parametrize("span_streaming", [True, False]) +async def test_basic( + sentry_init, + capture_events, + capture_items, + application, + span_streaming, +): sentry_init( integrations=[DjangoIntegration()], send_default_pii=True, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() - import channels # type: ignore[import-not-found] - if ( - sys.version_info < (3, 9) - and channels.__version__ < "4.0.0" - and django.VERSION >= (3, 0) - and django.VERSION < (4, 0) - ): - # We emit a UserWarning for channels 2.x and 3.x on Python 3.8 and older - # because the async support was not really good back then and there is a known issue. - # See the TreadingIntegration for details. - with pytest.warns(UserWarning): + if span_streaming: + items = capture_items("event") + + if ( + sys.version_info < (3, 9) + and channels.__version__ < "4.0.0" + and django.VERSION >= (3, 0) + and django.VERSION < (4, 0) + ): + # We emit a UserWarning for channels 2.x and 3.x on Python 3.8 and older + # because the async support was not really good back then and there is a known issue. + # See the TreadingIntegration for details. + with pytest.warns(UserWarning): + comm = HttpCommunicator(application, "GET", "/view-exc?test=query") + response = await comm.get_response() + await comm.wait() + else: comm = HttpCommunicator(application, "GET", "/view-exc?test=query") response = await comm.get_response() await comm.wait() + + assert response["status"] == 500 + + (event,) = (item.payload for item in items if item.type == "event") + + (exception,) = event["exception"]["values"] + assert exception["type"] == "ZeroDivisionError" + + # Test that the ASGI middleware got set up correctly. Right now this needs + # to be installed manually (see myapp/asgi.py) + assert event["transaction"] == "/view-exc" + assert event["request"] == { + "cookies": {}, + "headers": {}, + "method": "GET", + "query_string": "test=query", + "url": "/view-exc", + } + + capture_message("hi") + event = items[-1].payload else: - comm = HttpCommunicator(application, "GET", "/view-exc?test=query") - response = await comm.get_response() - await comm.wait() + events = capture_events() + + if ( + sys.version_info < (3, 9) + and channels.__version__ < "4.0.0" + and django.VERSION >= (3, 0) + and django.VERSION < (4, 0) + ): + # We emit a UserWarning for channels 2.x and 3.x on Python 3.8 and older + # because the async support was not really good back then and there is a known issue. + # See the TreadingIntegration for details. + with pytest.warns(UserWarning): + comm = HttpCommunicator(application, "GET", "/view-exc?test=query") + response = await comm.get_response() + await comm.wait() + else: + comm = HttpCommunicator(application, "GET", "/view-exc?test=query") + response = await comm.get_response() + await comm.wait() - assert response["status"] == 500 + assert response["status"] == 500 - (event,) = events + (event,) = events - (exception,) = event["exception"]["values"] - assert exception["type"] == "ZeroDivisionError" + (exception,) = event["exception"]["values"] + assert exception["type"] == "ZeroDivisionError" - # Test that the ASGI middleware got set up correctly. Right now this needs - # to be installed manually (see myapp/asgi.py) - assert event["transaction"] == "/view-exc" - assert event["request"] == { - "cookies": {}, - "headers": {}, - "method": "GET", - "query_string": "test=query", - "url": "/view-exc", - } + # Test that the ASGI middleware got set up correctly. Right now this needs + # to be installed manually (see myapp/asgi.py) + assert event["transaction"] == "/view-exc" + assert event["request"] == { + "cookies": {}, + "headers": {}, + "method": "GET", + "query_string": "test=query", + "url": "/view-exc", + } + + capture_message("hi") + event = events[-1] - capture_message("hi") - event = events[-1] assert "request" not in event @@ -90,21 +144,39 @@ async def test_basic(sentry_init, capture_events, application): @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) -async def test_async_views(sentry_init, capture_events, application): +@pytest.mark.parametrize("span_streaming", [True, False]) +async def test_async_views( + sentry_init, + capture_events, + capture_items, + application, + span_streaming, +): sentry_init( integrations=[DjangoIntegration()], send_default_pii=True, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() - comm = HttpCommunicator(application, "GET", "/async_message") - response = await comm.get_response() - await comm.wait() + if span_streaming: + items = capture_items("event") + + response = await comm.get_response() + await comm.wait() + + assert response["status"] == 200 - assert response["status"] == 200 + (event,) = (item.payload for item in items if item.type == "event") + else: + events = capture_events() + + response = await comm.get_response() + await comm.wait() + + assert response["status"] == 200 - (event,) = events + (event,) = events assert event["transaction"] == "/async_message" assert event["request"] == { @@ -124,50 +196,74 @@ async def test_async_views(sentry_init, capture_events, application): @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) +@pytest.mark.parametrize("span_streaming", [True, False]) async def test_active_thread_id( sentry_init, capture_envelopes, + capture_items, teardown_profiling, endpoint, application, middleware_spans, + span_streaming, ): - with mock.patch( - "sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0 - ): - sentry_init( - integrations=[DjangoIntegration(middleware_spans=middleware_spans)], - traces_sample_rate=1.0, - profiles_sample_rate=1.0, - ) + sentry_init( + integrations=[DjangoIntegration(middleware_spans=middleware_spans)], + traces_sample_rate=1.0, + profiles_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) - envelopes = capture_envelopes() + comm = HttpCommunicator(application, "GET", endpoint) - comm = HttpCommunicator(application, "GET", endpoint) - response = await comm.get_response() - await comm.wait() + if span_streaming: + with mock.patch( + "sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0 + ): + items = capture_items("span") + response = await comm.get_response() + await comm.wait() + + assert response["status"] == 200, response["body"] + + data = json.loads(response["body"]) + + spans = [item for item in items if item.type == "span"] - assert response["status"] == 200, response["body"] + for span in spans: + trace_context = span["contexts"]["trace"] + assert str(data["active"]) == trace_context["attributes"]["thread.id"] + else: + with mock.patch( + "sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0 + ): + envelopes = capture_envelopes() + response = await comm.get_response() + await comm.wait() - assert len(envelopes) == 1 + assert response["status"] == 200, response["body"] - profiles = [item for item in envelopes[0].items if item.type == "profile"] - assert len(profiles) == 1 + assert len(envelopes) == 1 - data = json.loads(response["body"]) + profiles = [item for item in envelopes[0].items if item.type == "profile"] + assert len(profiles) == 1 - for item in profiles: - transactions = item.payload.json["transactions"] - assert len(transactions) == 1 - assert str(data["active"]) == transactions[0]["active_thread_id"] + data = json.loads(response["body"]) + + for item in profiles: + transactions = item.payload.json["transactions"] + assert len(transactions) == 1 + assert str(data["active"]) == transactions[0]["active_thread_id"] - transactions = [item for item in envelopes[0].items if item.type == "transaction"] - assert len(transactions) == 1 + transactions = [ + item for item in envelopes[0].items if item.type == "transaction" + ] + assert len(transactions) == 1 - for item in transactions: - transaction = item.payload.json - trace_context = transaction["contexts"]["trace"] - assert str(data["active"]) == trace_context["data"]["thread.id"] + for item in transactions: + transaction = item.payload.json + trace_context = transaction["contexts"]["trace"] + assert str(data["active"]) == trace_context["data"]["thread.id"] @pytest.mark.asyncio @@ -265,8 +361,14 @@ async def test_async_middleware_that_is_function_concurrent_execution( @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) +@pytest.mark.parametrize("span_streaming", [True, False]) async def test_async_middleware_spans( - sentry_init, render_span_tree, capture_events, settings + sentry_init, + render_span_tree, + capture_events, + capture_items, + settings, + span_streaming, ): settings.MIDDLEWARE = [ "django.contrib.sessions.middleware.SessionMiddleware", @@ -279,23 +381,54 @@ async def test_async_middleware_spans( sentry_init( integrations=[DjangoIntegration(middleware_spans=True)], traces_sample_rate=1.0, - _experiments={"record_sql_params": True}, + _experiments={ + "record_sql_params": True, + "trace_lifecycle": "stream" if span_streaming else "static", + }, ) - events = capture_events() - comm = HttpCommunicator(asgi_application, "GET", "/simple_async_view") - response = await comm.get_response() - await comm.wait() + if span_streaming: + items = capture_items("span") - assert response["status"] == 200 + response = await comm.get_response() + await comm.wait() - (transaction,) = events + assert response["status"] == 200 + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + + assert ( + render_span_tree(spans) + == """\ +- sentry.op="http.server": name="/simple_async_view" + - sentry.op="event.django": name="django.db.reset_queries" + - sentry.op="event.django": name="django.db.close_old_connections" + - sentry.op="middleware.django": name="django.contrib.sessions.middleware.SessionMiddleware.__acall__" + - sentry.op="middleware.django": name="django.contrib.auth.middleware.AuthenticationMiddleware.__acall__" + - sentry.op="middleware.django": name="django.middleware.csrf.CsrfViewMiddleware.__acall__" + - sentry.op="middleware.django": name="tests.integrations.django.myapp.settings.TestMiddleware.__acall__" + - sentry.op="middleware.django": name="django.middleware.csrf.CsrfViewMiddleware.process_view" + - sentry.op="view.render": name="simple_async_view" + - sentry.op="event.django": name="django.db.close_old_connections" + - sentry.op="event.django": name="django.core.cache.close_caches" + - sentry.op="event.django": name="django.core.handlers.base.reset_urlconf\"""" + ) + else: + events = capture_events() - assert transaction["type"] == "transaction" - assert ( - render_span_tree(transaction["spans"], transaction["contexts"]["trace"]) - == """\ + response = await comm.get_response() + await comm.wait() + + assert response["status"] == 200 + + (transaction,) = events + + assert transaction["type"] == "transaction" + assert ( + render_span_tree(transaction["spans"], transaction["contexts"]["trace"]) + == """\ - op="http.server": description=null - op="event.django": description="django.db.reset_queries" - op="event.django": description="django.db.close_old_connections" @@ -308,7 +441,7 @@ async def test_async_middleware_spans( - op="event.django": description="django.db.close_old_connections" - op="event.django": description="django.core.cache.close_caches" - op="event.django": description="django.core.handlers.base.reset_urlconf\"""" - ) + ) @pytest.mark.asyncio @@ -316,27 +449,57 @@ async def test_async_middleware_spans( @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) -async def test_has_trace_if_performance_enabled(sentry_init, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +async def test_has_trace_if_performance_enabled( + sentry_init, + capture_events, + capture_items, + span_streaming, +): sentry_init( integrations=[DjangoIntegration()], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() - comm = HttpCommunicator(asgi_application, "GET", "/view-exc-with-msg") - response = await comm.get_response() - await comm.wait() + if span_streaming: + items = capture_items("event", "span") - assert response["status"] == 500 + response = await comm.get_response() + await comm.wait() - (msg_event, error_event, transaction_event) = events + assert response["status"] == 500 - assert ( - msg_event["contexts"]["trace"]["trace_id"] - == error_event["contexts"]["trace"]["trace_id"] - == transaction_event["contexts"]["trace"]["trace_id"] - ) + ( + msg_event, + error_event, + ) = (item.payload for item in items if item.type == "event") + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + assert spans[6]["is_segment"] is True + + assert ( + msg_event["contexts"]["trace"]["trace_id"] + == error_event["contexts"]["trace"]["trace_id"] + == spans[6]["trace_id"] + ) + else: + events = capture_events() + + response = await comm.get_response() + await comm.wait() + + assert response["status"] == 500 + + (msg_event, error_event, transaction_event) = events + + assert ( + msg_event["contexts"]["trace"]["trace_id"] + == error_event["contexts"]["trace"]["trace_id"] + == transaction_event["contexts"]["trace"]["trace_id"] + ) @pytest.mark.asyncio @@ -344,20 +507,40 @@ async def test_has_trace_if_performance_enabled(sentry_init, capture_events): @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) -async def test_has_trace_if_performance_disabled(sentry_init, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +async def test_has_trace_if_performance_disabled( + sentry_init, + capture_events, + capture_items, + span_streaming, +): sentry_init( integrations=[DjangoIntegration()], + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() - comm = HttpCommunicator(asgi_application, "GET", "/view-exc-with-msg") - response = await comm.get_response() - await comm.wait() + if span_streaming: + items = capture_items("event") - assert response["status"] == 500 + response = await comm.get_response() + await comm.wait() - (msg_event, error_event) = events + assert response["status"] == 500 + + ( + msg_event, + error_event, + ) = (item.payload for item in items if item.type == "event") + else: + events = capture_events() + + response = await comm.get_response() + await comm.wait() + + assert response["status"] == 500 + + (msg_event, error_event) = events assert msg_event["contexts"]["trace"] assert "trace_id" in msg_event["contexts"]["trace"] @@ -375,14 +558,19 @@ async def test_has_trace_if_performance_disabled(sentry_init, capture_events): @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) -async def test_trace_from_headers_if_performance_enabled(sentry_init, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +async def test_trace_from_headers_if_performance_enabled( + sentry_init, + capture_events, + capture_items, + span_streaming, +): sentry_init( integrations=[DjangoIntegration()], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() - trace_id = "582b43a4192642f0b136d5159a501701" sentry_trace_header = "{}-{}-{}".format(trace_id, "6e8f22c393e68f19", 1) @@ -392,16 +580,40 @@ async def test_trace_from_headers_if_performance_enabled(sentry_init, capture_ev "/view-exc-with-msg", headers=[(b"sentry-trace", sentry_trace_header.encode())], ) - response = await comm.get_response() - await comm.wait() - assert response["status"] == 500 + if span_streaming: + items = capture_items("event", "span") - (msg_event, error_event, transaction_event) = events + response = await comm.get_response() + await comm.wait() - assert msg_event["contexts"]["trace"]["trace_id"] == trace_id - assert error_event["contexts"]["trace"]["trace_id"] == trace_id - assert transaction_event["contexts"]["trace"]["trace_id"] == trace_id + assert response["status"] == 500 + + ( + msg_event, + error_event, + ) = (item.payload for item in items if item.type == "event") + + assert msg_event["contexts"]["trace"]["trace_id"] == trace_id + assert error_event["contexts"]["trace"]["trace_id"] == trace_id + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + assert spans[6]["is_segment"] is True + assert spans[6]["trace_id"] == trace_id + else: + events = capture_events() + + response = await comm.get_response() + await comm.wait() + + assert response["status"] == 500 + + (msg_event, error_event, transaction_event) = events + + assert msg_event["contexts"]["trace"]["trace_id"] == trace_id + assert error_event["contexts"]["trace"]["trace_id"] == trace_id + assert transaction_event["contexts"]["trace"]["trace_id"] == trace_id @pytest.mark.asyncio @@ -409,13 +621,18 @@ async def test_trace_from_headers_if_performance_enabled(sentry_init, capture_ev @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) -async def test_trace_from_headers_if_performance_disabled(sentry_init, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +async def test_trace_from_headers_if_performance_disabled( + sentry_init, + capture_events, + capture_items, + span_streaming, +): sentry_init( integrations=[DjangoIntegration()], + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() - trace_id = "582b43a4192642f0b136d5159a501701" sentry_trace_header = "{}-{}-{}".format(trace_id, "6e8f22c393e68f19", 1) @@ -425,12 +642,27 @@ async def test_trace_from_headers_if_performance_disabled(sentry_init, capture_e "/view-exc-with-msg", headers=[(b"sentry-trace", sentry_trace_header.encode())], ) - response = await comm.get_response() - await comm.wait() - assert response["status"] == 500 + if span_streaming: + items = capture_items("event") + + response = await comm.get_response() + await comm.wait() + + assert response["status"] == 500 - (msg_event, error_event) = events + (msg_event, error_event) = ( + item.payload for item in items if item.type == "event" + ) + else: + events = capture_events() + + response = await comm.get_response() + await comm.wait() + + assert response["status"] == 500 + + (msg_event, error_event) = events assert msg_event["contexts"]["trace"]["trace_id"] == trace_id assert error_event["contexts"]["trace"]["trace_id"] == trace_id @@ -536,13 +768,14 @@ async def test_trace_from_headers_if_performance_disabled(sentry_init, capture_e ], ) @pytest.mark.asyncio -@pytest.mark.forked @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) +@pytest.mark.parametrize("span_streaming", [True, False]) async def test_asgi_request_body( sentry_init, capture_envelopes, + capture_items, application, send_default_pii, method, @@ -550,14 +783,14 @@ async def test_asgi_request_body( url_name, body, expected_data, + span_streaming, ): sentry_init( integrations=[DjangoIntegration()], send_default_pii=send_default_pii, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - envelopes = capture_envelopes() - comm = HttpCommunicator( application, method=method, @@ -565,14 +798,29 @@ async def test_asgi_request_body( path=reverse(url_name), body=body, ) - response = await comm.get_response() - await comm.wait() - assert response["status"] == 200 - assert response["body"] == body + if span_streaming: + items = capture_items("event") - (envelope,) = envelopes - event = envelope.get_event() + response = await comm.get_response() + await comm.wait() + + assert response["status"] == 200 + assert response["body"] == body + + sentry_sdk.flush() + (event,) = (item.payload for item in items if item.type == "event") + else: + envelopes = capture_envelopes() + + response = await comm.get_response() + await comm.wait() + + assert response["status"] == 200 + assert response["body"] == body + + (envelope,) = envelopes + event = envelope.get_event() if expected_data is not None: assert event["request"]["data"] == expected_data @@ -650,21 +898,39 @@ def get_response(): ... @pytest.mark.skipif( django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" ) -async def test_async_view(sentry_init, capture_events, application): +@pytest.mark.parametrize("span_streaming", [True, False]) +async def test_async_view( + sentry_init, + capture_events, + capture_items, + application, + span_streaming, +): sentry_init( integrations=[DjangoIntegration()], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() - comm = HttpCommunicator(application, "GET", "/simple_async_view") - await comm.get_response() - await comm.wait() + if span_streaming: + items = capture_items("span") - (event,) = events - assert event["type"] == "transaction" - assert event["transaction"] == "/simple_async_view" + await comm.get_response() + await comm.wait() + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + assert spans[5]["name"] == "/simple_async_view" + else: + events = capture_events() + + await comm.get_response() + await comm.wait() + + (event,) = events + assert event["type"] == "transaction" + assert event["transaction"] == "/simple_async_view" @pytest.mark.parametrize("application", APPS) @@ -672,8 +938,13 @@ async def test_async_view(sentry_init, capture_events, application): @pytest.mark.skipif( django.VERSION < (3, 0), reason="Django ASGI support shipped in 3.0" ) +@pytest.mark.parametrize("span_streaming", [True, False]) async def test_transaction_http_method_default( - sentry_init, capture_events, application + sentry_init, + capture_events, + capture_items, + application, + span_streaming, ): """ By default OPTIONS and HEAD requests do not create a transaction. @@ -681,25 +952,45 @@ async def test_transaction_http_method_default( sentry_init( integrations=[DjangoIntegration()], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() + if span_streaming: + items = capture_items("span") - comm = HttpCommunicator(application, "GET", "/simple_async_view") - await comm.get_response() - await comm.wait() + comm = HttpCommunicator(application, "GET", "/simple_async_view") + await comm.get_response() + await comm.wait() - comm = HttpCommunicator(application, "OPTIONS", "/simple_async_view") - await comm.get_response() - await comm.wait() + comm = HttpCommunicator(application, "OPTIONS", "/simple_async_view") + await comm.get_response() + await comm.wait() - comm = HttpCommunicator(application, "HEAD", "/simple_async_view") - await comm.get_response() - await comm.wait() + comm = HttpCommunicator(application, "HEAD", "/simple_async_view") + await comm.get_response() + await comm.wait() - (event,) = events + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + assert spans[5]["attributes"]["http.request.method"] == "GET" + else: + events = capture_events() - assert len(events) == 1 - assert event["request"]["method"] == "GET" + comm = HttpCommunicator(application, "GET", "/simple_async_view") + await comm.get_response() + await comm.wait() + + comm = HttpCommunicator(application, "OPTIONS", "/simple_async_view") + await comm.get_response() + await comm.wait() + + comm = HttpCommunicator(application, "HEAD", "/simple_async_view") + await comm.get_response() + await comm.wait() + + (event,) = events + + assert len(events) == 1 + assert event["request"]["method"] == "GET" @pytest.mark.parametrize("application", APPS) @@ -707,7 +998,14 @@ async def test_transaction_http_method_default( @pytest.mark.skipif( django.VERSION < (3, 0), reason="Django ASGI support shipped in 3.0" ) -async def test_transaction_http_method_custom(sentry_init, capture_events, application): +@pytest.mark.parametrize("span_streaming", [True, False]) +async def test_transaction_http_method_custom( + sentry_init, + capture_events, + capture_items, + application, + span_streaming, +): sentry_init( integrations=[ DjangoIntegration( @@ -718,23 +1016,45 @@ async def test_transaction_http_method_custom(sentry_init, capture_events, appli ) ], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() + if span_streaming: + items = capture_items("span") - comm = HttpCommunicator(application, "GET", "/simple_async_view") - await comm.get_response() - await comm.wait() + comm = HttpCommunicator(application, "GET", "/simple_async_view") + await comm.get_response() + await comm.wait() - comm = HttpCommunicator(application, "OPTIONS", "/simple_async_view") - await comm.get_response() - await comm.wait() + comm = HttpCommunicator(application, "OPTIONS", "/simple_async_view") + await comm.get_response() + await comm.wait() + + comm = HttpCommunicator(application, "HEAD", "/simple_async_view") + await comm.get_response() + await comm.wait() + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + + assert spans[10]["attributes"][SPANDATA.HTTP_REQUEST_METHOD] == "OPTIONS" + assert spans[16]["attributes"][SPANDATA.HTTP_REQUEST_METHOD] == "HEAD" + else: + events = capture_events() - comm = HttpCommunicator(application, "HEAD", "/simple_async_view") - await comm.get_response() - await comm.wait() + comm = HttpCommunicator(application, "GET", "/simple_async_view") + await comm.get_response() + await comm.wait() + + comm = HttpCommunicator(application, "OPTIONS", "/simple_async_view") + await comm.get_response() + await comm.wait() + + comm = HttpCommunicator(application, "HEAD", "/simple_async_view") + await comm.get_response() + await comm.wait() - assert len(events) == 2 + assert len(events) == 2 - (event1, event2) = events - assert event1["request"]["method"] == "OPTIONS" - assert event2["request"]["method"] == "HEAD" + (event1, event2) = events + assert event1["request"]["method"] == "OPTIONS" + assert event2["request"]["method"] == "HEAD" diff --git a/tests/integrations/django/test_basic.py b/tests/integrations/django/test_basic.py index f14be92ceb..b2cac58d90 100644 --- a/tests/integrations/django/test_basic.py +++ b/tests/integrations/django/test_basic.py @@ -50,21 +50,52 @@ def client(): return Client(application) -def test_view_exceptions(sentry_init, client, capture_exceptions, capture_events): - sentry_init(integrations=[DjangoIntegration()], send_default_pii=True) +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_view_exceptions( + sentry_init, + client, + capture_exceptions, + capture_events, + capture_items, + span_streaming, +): + sentry_init( + integrations=[DjangoIntegration()], + send_default_pii=True, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) exceptions = capture_exceptions() - events = capture_events() - client.get(reverse("view_exc")) + if span_streaming: + items = capture_items("event") - (error,) = exceptions - assert isinstance(error, ZeroDivisionError) + client.get(reverse("view_exc")) + + (error,) = exceptions + assert isinstance(error, ZeroDivisionError) + + (event,) = (item.payload for item in items if item.type == "event") + else: + events = capture_events() + + client.get(reverse("view_exc")) + + (error,) = exceptions + assert isinstance(error, ZeroDivisionError) + + (event,) = events - (event,) = events assert event["exception"]["values"][0]["mechanism"]["type"] == "django" +@pytest.mark.parametrize("span_streaming", [True, False]) def test_ensures_x_forwarded_header_is_honored_in_sdk_when_enabled_in_django( - sentry_init, client, capture_exceptions, capture_events, settings + sentry_init, + client, + capture_exceptions, + capture_events, + capture_items, + settings, + span_streaming, ): """ Test that ensures if django settings.USE_X_FORWARDED_HOST is set to True @@ -72,34 +103,66 @@ def test_ensures_x_forwarded_header_is_honored_in_sdk_when_enabled_in_django( """ settings.USE_X_FORWARDED_HOST = True - sentry_init(integrations=[DjangoIntegration()], send_default_pii=True) + sentry_init( + integrations=[DjangoIntegration()], + send_default_pii=True, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) exceptions = capture_exceptions() - events = capture_events() - client.get(reverse("view_exc"), headers={"X_FORWARDED_HOST": "example.com"}) + if span_streaming: + items = capture_items("event") + client.get(reverse("view_exc"), headers={"X_FORWARDED_HOST": "example.com"}) - (error,) = exceptions - assert isinstance(error, ZeroDivisionError) + (error,) = exceptions + assert isinstance(error, ZeroDivisionError) + + (event,) = (item.payload for item in items if item.type == "event") + else: + events = capture_events() + client.get(reverse("view_exc"), headers={"X_FORWARDED_HOST": "example.com"}) + + (error,) = exceptions + assert isinstance(error, ZeroDivisionError) + + (event,) = events - (event,) = events assert event["request"]["url"] == "http://example.com/view-exc" +@pytest.mark.parametrize("span_streaming", [True, False]) def test_ensures_x_forwarded_header_is_not_honored_when_unenabled_in_django( - sentry_init, client, capture_exceptions, capture_events + sentry_init, + client, + capture_exceptions, + capture_events, + capture_items, + span_streaming, ): """ Test that ensures if django settings.USE_X_FORWARDED_HOST is set to False then the SDK sets the request url to the `HTTP_POST` """ - sentry_init(integrations=[DjangoIntegration()], send_default_pii=True) + sentry_init( + integrations=[DjangoIntegration()], + send_default_pii=True, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) exceptions = capture_exceptions() - events = capture_events() - client.get(reverse("view_exc"), headers={"X_FORWARDED_HOST": "example.com"}) + if span_streaming: + items = capture_items("event") + client.get(reverse("view_exc"), headers={"X_FORWARDED_HOST": "example.com"}) - (error,) = exceptions - assert isinstance(error, ZeroDivisionError) + (error,) = exceptions + assert isinstance(error, ZeroDivisionError) + (event,) = (item.payload for item in items if item.type == "event") + else: + events = capture_events() + client.get(reverse("view_exc"), headers={"X_FORWARDED_HOST": "example.com"}) + + (error,) = exceptions + assert isinstance(error, ZeroDivisionError) + (event,) = events - (event,) = events assert event["request"]["url"] == "http://localhost/view-exc" @@ -112,37 +175,90 @@ def test_middleware_exceptions(sentry_init, client, capture_exceptions): assert isinstance(error, ZeroDivisionError) -def test_request_captured(sentry_init, client, capture_events): - sentry_init(integrations=[DjangoIntegration()], send_default_pii=True) - events = capture_events() - content, status, headers = unpack_werkzeug_response(client.get(reverse("message"))) +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_request_captured( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): + sentry_init( + integrations=[DjangoIntegration()], + send_default_pii=True, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) + if span_streaming: + items = capture_items("event") + content, status, headers = unpack_werkzeug_response( + client.get(reverse("message")) + ) - assert content == b"ok" + assert content == b"ok" - (event,) = events - assert event["transaction"] == "/message" - assert event["request"] == { - "cookies": {}, - "env": {"SERVER_NAME": "localhost", "SERVER_PORT": "80"}, - "headers": {"Host": "localhost"}, - "method": "GET", - "query_string": "", - "url": "http://localhost/message", - } + (event,) = (item.payload for item in items if item.type == "event") + assert event["transaction"] == "/message" + assert event["request"] == { + "cookies": {}, + "env": {"SERVER_NAME": "localhost", "SERVER_PORT": "80"}, + "headers": {"Host": "localhost"}, + "method": "GET", + "query_string": "", + "url": "http://localhost/message", + } + else: + events = capture_events() + content, status, headers = unpack_werkzeug_response( + client.get(reverse("message")) + ) + + assert content == b"ok" -def test_transaction_with_class_view(sentry_init, client, capture_events): + (event,) = events + + assert event["transaction"] == "/message" + assert event["request"] == { + "cookies": {}, + "env": {"SERVER_NAME": "localhost", "SERVER_PORT": "80"}, + "headers": {"Host": "localhost"}, + "method": "GET", + "query_string": "", + "url": "http://localhost/message", + } + + +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_transaction_with_class_view( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): sentry_init( integrations=[DjangoIntegration(transaction_style="function_name")], send_default_pii=True, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() - content, status, headers = unpack_werkzeug_response( - client.head(reverse("classbased")) - ) - assert status.lower() == "200 ok" + if span_streaming: + items = capture_items("event") - (event,) = events + content, status, headers = unpack_werkzeug_response( + client.head(reverse("classbased")) + ) + assert status.lower() == "200 ok" + + (event,) = (item.payload for item in items if item.type == "event") + else: + events = capture_events() + + content, status, headers = unpack_werkzeug_response( + client.head(reverse("classbased")) + ) + assert status.lower() == "200 ok" + + (event,) = events assert ( event["transaction"] == "tests.integrations.django.myapp.views.ClassBasedView" @@ -150,7 +266,14 @@ def test_transaction_with_class_view(sentry_init, client, capture_events): assert event["message"] == "hi" -def test_has_trace_if_performance_enabled(sentry_init, client, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_has_trace_if_performance_enabled( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): sentry_init( integrations=[ DjangoIntegration( @@ -158,36 +281,80 @@ def test_has_trace_if_performance_enabled(sentry_init, client, capture_events): ) ], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() - client.head(reverse("view_exc_with_msg")) + if span_streaming: + items = capture_items("event", "span") + client.head(reverse("view_exc_with_msg")) - (msg_event, error_event, transaction_event) = events + ( + msg_event, + error_event, + ) = (item.payload for item in items if item.type == "event") - assert msg_event["contexts"]["trace"] - assert "trace_id" in msg_event["contexts"]["trace"] + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + assert spans[3]["is_segment"] is True + assert "trace_id" in spans[3] - assert error_event["contexts"]["trace"] - assert "trace_id" in error_event["contexts"]["trace"] + assert msg_event["contexts"]["trace"] + assert "trace_id" in msg_event["contexts"]["trace"] - assert transaction_event["contexts"]["trace"] - assert "trace_id" in transaction_event["contexts"]["trace"] + assert error_event["contexts"]["trace"] + assert "trace_id" in error_event["contexts"]["trace"] - assert ( - msg_event["contexts"]["trace"]["trace_id"] - == error_event["contexts"]["trace"]["trace_id"] - == transaction_event["contexts"]["trace"]["trace_id"] - ) + assert ( + msg_event["contexts"]["trace"]["trace_id"] + == error_event["contexts"]["trace"]["trace_id"] + == spans[3]["trace_id"] + ) + else: + events = capture_events() + client.head(reverse("view_exc_with_msg")) + + (msg_event, error_event, transaction_event) = events + + assert transaction_event["contexts"]["trace"] + assert "trace_id" in transaction_event["contexts"]["trace"] + + assert msg_event["contexts"]["trace"] + assert "trace_id" in msg_event["contexts"]["trace"] + + assert error_event["contexts"]["trace"] + assert "trace_id" in error_event["contexts"]["trace"] + + assert ( + msg_event["contexts"]["trace"]["trace_id"] + == error_event["contexts"]["trace"]["trace_id"] + == transaction_event["contexts"]["trace"]["trace_id"] + ) -def test_has_trace_if_performance_disabled(sentry_init, client, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_has_trace_if_performance_disabled( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): sentry_init( integrations=[DjangoIntegration()], + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() - client.head(reverse("view_exc_with_msg")) + if span_streaming: + items = capture_items("event") + client.head(reverse("view_exc_with_msg")) - (msg_event, error_event) = events + ( + msg_event, + error_event, + ) = (item.payload for item in items if item.type == "event") + else: + events = capture_events() + client.head(reverse("view_exc_with_msg")) + + (msg_event, error_event) = events assert msg_event["contexts"]["trace"] assert "trace_id" in msg_event["contexts"]["trace"] @@ -201,7 +368,14 @@ def test_has_trace_if_performance_disabled(sentry_init, client, capture_events): ) -def test_trace_from_headers_if_performance_enabled(sentry_init, client, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_trace_from_headers_if_performance_enabled( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): sentry_init( integrations=[ DjangoIntegration( @@ -209,35 +383,68 @@ def test_trace_from_headers_if_performance_enabled(sentry_init, client, capture_ ) ], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() - trace_id = "582b43a4192642f0b136d5159a501701" sentry_trace_header = "{}-{}-{}".format(trace_id, "6e8f22c393e68f19", 1) - client.head( - reverse("view_exc_with_msg"), headers={"sentry-trace": sentry_trace_header} - ) + if span_streaming: + items = capture_items("event", "span") - (msg_event, error_event, transaction_event) = events + client.head( + reverse("view_exc_with_msg"), headers={"sentry-trace": sentry_trace_header} + ) - assert msg_event["contexts"]["trace"] - assert "trace_id" in msg_event["contexts"]["trace"] + ( + msg_event, + error_event, + ) = (item.payload for item in items if item.type == "event") - assert error_event["contexts"]["trace"] - assert "trace_id" in error_event["contexts"]["trace"] + assert msg_event["contexts"]["trace"] + assert "trace_id" in msg_event["contexts"]["trace"] - assert transaction_event["contexts"]["trace"] - assert "trace_id" in transaction_event["contexts"]["trace"] + assert error_event["contexts"]["trace"] + assert "trace_id" in error_event["contexts"]["trace"] - assert msg_event["contexts"]["trace"]["trace_id"] == trace_id - assert error_event["contexts"]["trace"]["trace_id"] == trace_id - assert transaction_event["contexts"]["trace"]["trace_id"] == trace_id + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + assert spans[3]["is_segment"] is True + assert "trace_id" in spans[3] + + assert msg_event["contexts"]["trace"]["trace_id"] == trace_id + assert error_event["contexts"]["trace"]["trace_id"] == trace_id + assert spans[3]["trace_id"] == trace_id + else: + events = capture_events() + + client.head( + reverse("view_exc_with_msg"), headers={"sentry-trace": sentry_trace_header} + ) + + (msg_event, error_event, transaction_event) = events + + assert msg_event["contexts"]["trace"] + assert "trace_id" in msg_event["contexts"]["trace"] + assert error_event["contexts"]["trace"] + assert "trace_id" in error_event["contexts"]["trace"] + assert transaction_event["contexts"]["trace"] + assert "trace_id" in transaction_event["contexts"]["trace"] + + assert msg_event["contexts"]["trace"]["trace_id"] == trace_id + assert error_event["contexts"]["trace"]["trace_id"] == trace_id + assert transaction_event["contexts"]["trace"]["trace_id"] == trace_id + + +@pytest.mark.parametrize("span_streaming", [True, False]) def test_trace_from_headers_if_performance_disabled( - sentry_init, client, capture_events + sentry_init, + client, + capture_events, + capture_items, + span_streaming, ): sentry_init( integrations=[ @@ -245,18 +452,31 @@ def test_trace_from_headers_if_performance_disabled( http_methods_to_capture=("HEAD",), ) ], + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() - trace_id = "582b43a4192642f0b136d5159a501701" sentry_trace_header = "{}-{}-{}".format(trace_id, "6e8f22c393e68f19", 1) - client.head( - reverse("view_exc_with_msg"), headers={"sentry-trace": sentry_trace_header} - ) + if span_streaming: + items = capture_items("event") + + client.head( + reverse("view_exc_with_msg"), headers={"sentry-trace": sentry_trace_header} + ) + + ( + msg_event, + error_event, + ) = (item.payload for item in items if item.type == "event") + else: + events = capture_events() + + client.head( + reverse("view_exc_with_msg"), headers={"sentry-trace": sentry_trace_header} + ) - (msg_event, error_event) = events + (msg_event, error_event) = events assert msg_event["contexts"]["trace"] assert "trace_id" in msg_event["contexts"]["trace"] @@ -270,18 +490,52 @@ def test_trace_from_headers_if_performance_disabled( @pytest.mark.forked @pytest_mark_django_db_decorator() -def test_user_captured(sentry_init, client, capture_events): - sentry_init(integrations=[DjangoIntegration()], send_default_pii=True) - events = capture_events() - content, status, headers = unpack_werkzeug_response(client.get(reverse("mylogin"))) - assert content == b"ok" +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_user_captured( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): + sentry_init( + integrations=[DjangoIntegration()], + send_default_pii=True, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) + if span_streaming: + items = capture_items("event") - assert not events + content, status, headers = unpack_werkzeug_response( + client.get(reverse("mylogin")) + ) + assert content == b"ok" - content, status, headers = unpack_werkzeug_response(client.get(reverse("message"))) - assert content == b"ok" + sentry_sdk.flush() + assert not items - (event,) = events + content, status, headers = unpack_werkzeug_response( + client.get(reverse("message")) + ) + assert content == b"ok" + + (event,) = (item.payload for item in items if item.type == "event") + else: + events = capture_events() + + content, status, headers = unpack_werkzeug_response( + client.get(reverse("mylogin")) + ) + assert content == b"ok" + + assert not events + + content, status, headers = unpack_werkzeug_response( + client.get(reverse("message")) + ) + assert content == b"ok" + + (event,) = events assert event["user"] == { "email": "lennon@thebeatles.com", @@ -292,18 +546,39 @@ def test_user_captured(sentry_init, client, capture_events): @pytest.mark.forked @pytest_mark_django_db_decorator() -def test_queryset_repr(sentry_init, capture_events): - sentry_init(integrations=[DjangoIntegration()]) - events = capture_events() +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_queryset_repr( + sentry_init, + capture_events, + capture_items, + span_streaming, +): + sentry_init( + integrations=[DjangoIntegration()], + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) + User.objects.create_user("john", "lennon@thebeatles.com", "johnpassword") + if span_streaming: + items = capture_items("event") - try: - my_queryset = User.objects.all() # noqa - 1 / 0 - except Exception: - capture_exception() + try: + my_queryset = User.objects.all() # noqa + 1 / 0 + except Exception: + capture_exception() + + (event,) = (item.payload for item in items if item.type == "event") + else: + events = capture_events() + + try: + my_queryset = User.objects.all() # noqa + 1 / 0 + except Exception: + capture_exception() - (event,) = events + (event,) = events (exception,) = event["exception"]["values"] assert exception["type"] == "ZeroDivisionError" @@ -315,18 +590,38 @@ def test_queryset_repr(sentry_init, capture_events): @pytest.mark.forked @pytest_mark_django_db_decorator() -def test_context_nested_queryset_repr(sentry_init, capture_events): - sentry_init(integrations=[DjangoIntegration()]) - events = capture_events() +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_context_nested_queryset_repr( + sentry_init, + capture_events, + capture_items, + span_streaming, +): + sentry_init( + integrations=[DjangoIntegration()], + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) User.objects.create_user("john", "lennon@thebeatles.com", "johnpassword") + if span_streaming: + items = capture_items("event") - try: - context = make_context({"entries": User.objects.all()}) # noqa - 1 / 0 - except Exception: - capture_exception() + try: + context = make_context({"entries": User.objects.all()}) # noqa + 1 / 0 + except Exception: + capture_exception() - (event,) = events + (event,) = (item.payload for item in items if item.type == "event") + else: + events = capture_events() + + try: + context = make_context({"entries": User.objects.all()}) # noqa + 1 / 0 + except Exception: + capture_exception() + + (event,) = events (exception,) = event["exception"]["values"] assert exception["type"] == "ZeroDivisionError" @@ -334,13 +629,32 @@ def test_context_nested_queryset_repr(sentry_init, capture_events): assert "= (1, 7): - views_tests.append( + if span_streaming: + views_tests = [ ( - reverse("template_test"), - '- op="template.render": description="user_name.html"', + reverse("template_test2"), + '- sentry.op="template.render": name="[user_name.html, ...]"', ), - ) + ] + if DJANGO_VERSION >= (1, 7): + views_tests.append( + ( + reverse("template_test"), + '- sentry.op="template.render": name="user_name.html"', + ), + ) - for url, expected_line in views_tests: - events = capture_events() - client.get(url) - transaction = events[0] - assert expected_line in render_span_tree( - transaction["spans"], transaction["contexts"]["trace"] - ) + for url, expected_line in views_tests: + items = capture_items("span") + client.get(url) + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + assert expected_line in render_span_tree(spans) + else: + views_tests = [ + ( + reverse("template_test2"), + '- op="template.render": description="[user_name.html, ...]"', + ), + ] + if DJANGO_VERSION >= (1, 7): + views_tests.append( + ( + reverse("template_test"), + '- op="template.render": description="user_name.html"', + ), + ) + + for url, expected_line in views_tests: + events = capture_events() + client.get(url) + transaction = events[0] + assert expected_line in render_span_tree( + transaction["spans"], transaction["contexts"]["trace"] + ) @pytest.mark.skipif(DJANGO_VERSION < (1, 9), reason="Requires Django >= 1.9") @@ -999,8 +1759,67 @@ def test_render_spans_queryset_in_data(sentry_init, client, capture_events): ) -if DJANGO_VERSION >= (1, 10): - EXPECTED_MIDDLEWARE_SPANS = """\ +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_middleware_spans( + sentry_init, + client, + capture_events, + capture_items, + render_span_tree, + span_streaming, +): + sentry_init( + integrations=[ + DjangoIntegration(middleware_spans=True, signals_spans=False), + ], + traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) + if span_streaming: + items = capture_items("event", "span") + + client.get(reverse("message")) + + (message,) = (item.payload for item in items if item.type == "event") + assert message["message"] == "hi" + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + + if DJANGO_VERSION >= (1, 10): + EXPECTED_MIDDLEWARE_SPANS = """\ +- sentry.op="http.server": name="/message" + - sentry.op="middleware.django": name="django.contrib.sessions.middleware.SessionMiddleware.__call__" + - sentry.op="middleware.django": name="django.contrib.auth.middleware.AuthenticationMiddleware.__call__" + - sentry.op="middleware.django": name="django.middleware.csrf.CsrfViewMiddleware.__call__" + - sentry.op="middleware.django": name="tests.integrations.django.myapp.settings.TestMiddleware.__call__" + - sentry.op="middleware.django": name="tests.integrations.django.myapp.settings.TestFunctionMiddleware.__call__" + - sentry.op="middleware.django": name="django.middleware.csrf.CsrfViewMiddleware.process_view" + - sentry.op="view.render": name="message"\ +""" + else: + EXPECTED_MIDDLEWARE_SPANS = """\ +- sentry.op="http.server": name="/message" + - sentry.op="middleware.django": name="django.contrib.sessions.middleware.SessionMiddleware.process_request" + - sentry.op="middleware.django": name="django.contrib.auth.middleware.AuthenticationMiddleware.process_request" + - sentry.op="middleware.django": name="tests.integrations.django.myapp.settings.TestMiddleware.process_request" + - sentry.op="middleware.django": name="django.middleware.csrf.CsrfViewMiddleware.process_view" + - sentry.op="view.render": name="message" + - sentry.op="middleware.django": name="tests.integrations.django.myapp.settings.TestMiddleware.process_response" + - sentry.op="middleware.django": name="django.middleware.csrf.CsrfViewMiddleware.process_response" + - sentry.op="middleware.django": name="django.contrib.sessions.middleware.SessionMiddleware.process_response"\ +""" + assert render_span_tree(spans) == EXPECTED_MIDDLEWARE_SPANS + else: + events = capture_events() + + client.get(reverse("message")) + + message, transaction = events + + assert message["message"] == "hi" + if DJANGO_VERSION >= (1, 10): + EXPECTED_MIDDLEWARE_SPANS = """\ - op="http.server": description=null - op="middleware.django": description="django.contrib.sessions.middleware.SessionMiddleware.__call__" - op="middleware.django": description="django.contrib.auth.middleware.AuthenticationMiddleware.__call__" @@ -1010,8 +1829,8 @@ def test_render_spans_queryset_in_data(sentry_init, client, capture_events): - op="middleware.django": description="django.middleware.csrf.CsrfViewMiddleware.process_view" - op="view.render": description="message"\ """ -else: - EXPECTED_MIDDLEWARE_SPANS = """\ + else: + EXPECTED_MIDDLEWARE_SPANS = """\ - op="http.server": description=null - op="middleware.django": description="django.contrib.sessions.middleware.SessionMiddleware.process_request" - op="middleware.django": description="django.contrib.auth.middleware.AuthenticationMiddleware.process_request" @@ -1022,104 +1841,165 @@ def test_render_spans_queryset_in_data(sentry_init, client, capture_events): - op="middleware.django": description="django.middleware.csrf.CsrfViewMiddleware.process_response" - op="middleware.django": description="django.contrib.sessions.middleware.SessionMiddleware.process_response"\ """ + assert ( + render_span_tree(transaction["spans"], transaction["contexts"]["trace"]) + == EXPECTED_MIDDLEWARE_SPANS + ) -def test_middleware_spans(sentry_init, client, capture_events, render_span_tree): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_middleware_spans_disabled( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): sentry_init( integrations=[ - DjangoIntegration(middleware_spans=True, signals_spans=False), + DjangoIntegration(signals_spans=False), ], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() + if span_streaming: + items = capture_items("event", "span") - client.get(reverse("message")) + client.get(reverse("message")) - message, transaction = events + (message,) = (item.payload for item in items if item.type == "event") - assert message["message"] == "hi" - assert ( - render_span_tree(transaction["spans"], transaction["contexts"]["trace"]) - == EXPECTED_MIDDLEWARE_SPANS - ) + assert message["message"] == "hi" + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + assert len(spans) == 1 + else: + events = capture_events() + + client.get(reverse("message")) + message, transaction = events -def test_middleware_spans_disabled(sentry_init, client, capture_events): + assert message["message"] == "hi" + assert not len(transaction["spans"]) + + +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_signals_spans( + sentry_init, + client, + capture_events, + capture_items, + render_span_tree, + span_streaming, +): sentry_init( integrations=[ - DjangoIntegration(signals_spans=False), + DjangoIntegration(middleware_spans=False), ], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() - - client.get(reverse("message")) + if span_streaming: + items = capture_items("event", "span") - message, transaction = events + client.get(reverse("message")) - assert message["message"] == "hi" - assert not len(transaction["spans"]) + (message,) = (item.payload for item in items if item.type == "event") + assert message["message"] == "hi" -EXPECTED_SIGNALS_SPANS = """\ -- op="http.server": description=null - - op="event.django": description="django.db.reset_queries" - - op="event.django": description="django.db.close_old_connections"\ + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + assert ( + render_span_tree(spans) + == """\ +- sentry.op="http.server": name="/message" + - sentry.op="event.django": name="django.db.reset_queries" + - sentry.op="event.django": name="django.db.close_old_connections"\ """ + ) + assert spans[0]["attributes"]["sentry.op"] == "event.django" + assert spans[0]["name"] == "django.db.reset_queries" -def test_signals_spans(sentry_init, client, capture_events, render_span_tree): - sentry_init( - integrations=[ - DjangoIntegration(middleware_spans=False), - ], - traces_sample_rate=1.0, - ) - events = capture_events() + assert spans[1]["attributes"]["sentry.op"] == "event.django" + assert spans[1]["name"] == "django.db.close_old_connections" + else: + events = capture_events() - client.get(reverse("message")) + client.get(reverse("message")) - message, transaction = events + message, transaction = events - assert message["message"] == "hi" - assert ( - render_span_tree(transaction["spans"], transaction["contexts"]["trace"]) - == EXPECTED_SIGNALS_SPANS - ) + assert message["message"] == "hi" + assert ( + render_span_tree(transaction["spans"], transaction["contexts"]["trace"]) + == """\ +- op="http.server": description=null + - op="event.django": description="django.db.reset_queries" + - op="event.django": description="django.db.close_old_connections"\ +""" + ) - assert transaction["spans"][0]["op"] == "event.django" - assert transaction["spans"][0]["description"] == "django.db.reset_queries" + assert transaction["spans"][0]["op"] == "event.django" + assert transaction["spans"][0]["description"] == "django.db.reset_queries" - assert transaction["spans"][1]["op"] == "event.django" - assert transaction["spans"][1]["description"] == "django.db.close_old_connections" + assert transaction["spans"][1]["op"] == "event.django" + assert ( + transaction["spans"][1]["description"] == "django.db.close_old_connections" + ) -def test_signals_spans_disabled(sentry_init, client, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_signals_spans_disabled( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): sentry_init( integrations=[ DjangoIntegration(middleware_spans=False, signals_spans=False), ], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() + if span_streaming: + items = capture_items("event", "span") - client.get(reverse("message")) + client.get(reverse("message")) - message, transaction = events + sentry_sdk.flush() + (message,) = (item.payload for item in items if item.type == "event") - assert message["message"] == "hi" - assert not transaction["spans"] + assert message["message"] == "hi" + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + assert len(spans) == 1 + else: + events = capture_events() -EXPECTED_SIGNALS_SPANS_FILTERED = """\ -- op="http.server": description=null - - op="event.django": description="django.db.reset_queries" - - op="event.django": description="django.db.close_old_connections" - - op="event.django": description="tests.integrations.django.myapp.signals.signal_handler"\ -""" + client.get(reverse("message")) + + message, transaction = events + assert message["message"] == "hi" + assert not transaction["spans"] -def test_signals_spans_filtering(sentry_init, client, capture_events, render_span_tree): + +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_signals_spans_filtering( + sentry_init, + client, + capture_events, + capture_items, + render_span_tree, + span_streaming, +): sentry_init( integrations=[ DjangoIntegration( @@ -1130,29 +2010,65 @@ def test_signals_spans_filtering(sentry_init, client, capture_events, render_spa ), ], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() + if span_streaming: + items = capture_items("span") + + client.get(reverse("send_myapp_custom_signal")) + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + assert ( + render_span_tree(spans) + == """\ +- sentry.op="http.server": name="/send-myapp-custom-signal" + - sentry.op="event.django": name="django.db.reset_queries" + - sentry.op="event.django": name="django.db.close_old_connections" + - sentry.op="event.django": name="tests.integrations.django.myapp.signals.signal_handler"\ +""" + ) - client.get(reverse("send_myapp_custom_signal")) + assert spans[0]["attributes"]["sentry.op"] == "event.django" + assert spans[0]["name"] == "django.db.reset_queries" - (transaction,) = events + assert spans[1]["attributes"]["sentry.op"] == "event.django" + assert spans[1]["name"] == "django.db.close_old_connections" - assert ( - render_span_tree(transaction["spans"], transaction["contexts"]["trace"]) - == EXPECTED_SIGNALS_SPANS_FILTERED - ) + assert spans[2]["attributes"]["sentry.op"] == "event.django" + assert ( + spans[2]["name"] == "tests.integrations.django.myapp.signals.signal_handler" + ) + else: + events = capture_events() - assert transaction["spans"][0]["op"] == "event.django" - assert transaction["spans"][0]["description"] == "django.db.reset_queries" + client.get(reverse("send_myapp_custom_signal")) - assert transaction["spans"][1]["op"] == "event.django" - assert transaction["spans"][1]["description"] == "django.db.close_old_connections" + (transaction,) = events - assert transaction["spans"][2]["op"] == "event.django" - assert ( - transaction["spans"][2]["description"] - == "tests.integrations.django.myapp.signals.signal_handler" - ) + assert ( + render_span_tree(transaction["spans"], transaction["contexts"]["trace"]) + == """\ +- op="http.server": description=null + - op="event.django": description="django.db.reset_queries" + - op="event.django": description="django.db.close_old_connections" + - op="event.django": description="tests.integrations.django.myapp.signals.signal_handler"\ +""" + ) + + assert transaction["spans"][0]["op"] == "event.django" + assert transaction["spans"][0]["description"] == "django.db.reset_queries" + + assert transaction["spans"][1]["op"] == "event.django" + assert ( + transaction["spans"][1]["description"] == "django.db.close_old_connections" + ) + + assert transaction["spans"][2]["op"] == "event.django" + assert ( + transaction["spans"][2]["description"] + == "tests.integrations.django.myapp.signals.signal_handler" + ) def test_csrf(sentry_init, client): @@ -1194,8 +2110,16 @@ def test_csrf(sentry_init, client): @pytest.mark.skipif(DJANGO_VERSION < (2, 0), reason="Requires Django > 2.0") @pytest.mark.parametrize("middleware_spans", [False, True]) +@pytest.mark.parametrize("span_streaming", [True, False]) def test_custom_urlconf_middleware( - settings, sentry_init, client, capture_events, render_span_tree, middleware_spans + settings, + sentry_init, + client, + capture_events, + capture_items, + render_span_tree, + middleware_spans, + span_streaming, ): """ Some middlewares (for instance in django-tenants) overwrite request.urlconf. @@ -1209,33 +2133,68 @@ def test_custom_urlconf_middleware( sentry_init( integrations=[DjangoIntegration(middleware_spans=middleware_spans)], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() - content, status, _headers = unpack_werkzeug_response(client.get("/custom/ok")) - assert status.lower() == "200 ok" - assert content == b"custom ok" + if span_streaming: + items = capture_items("event", "span") - event = events.pop(0) - assert event["transaction"] == "/custom/ok" - if middleware_spans: - assert "custom_urlconf_middleware" in render_span_tree( - event["spans"], event["contexts"]["trace"] - ) + content, status, _headers = unpack_werkzeug_response(client.get("/custom/ok")) + assert status.lower() == "200 ok" + assert content == b"custom ok" - _content, status, _headers = unpack_werkzeug_response(client.get("/custom/exc")) - assert status.lower() == "500 internal server error" + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] - error_event, transaction_event = events - assert error_event["transaction"] == "/custom/exc" - assert error_event["exception"]["values"][-1]["mechanism"]["type"] == "django" - assert transaction_event["transaction"] == "/custom/exc" - if middleware_spans: - assert "custom_urlconf_middleware" in render_span_tree( - transaction_event["spans"], transaction_event["contexts"]["trace"] - ) + if middleware_spans: + assert spans[10]["name"] == "/custom/ok" + assert "custom_urlconf_middleware" in render_span_tree(spans) + else: + assert spans[2]["name"] == "/custom/ok" + + _content, status, _headers = unpack_werkzeug_response(client.get("/custom/exc")) + assert status.lower() == "500 internal server error" + + (error_event,) = (item.payload for item in items if item.type == "event") + assert error_event["transaction"] == "/custom/exc" + assert error_event["exception"]["values"][-1]["mechanism"]["type"] == "django" + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + + if middleware_spans: + assert spans[22]["name"] == "/custom/exc" + assert "custom_urlconf_middleware" in render_span_tree(spans) + else: + assert spans[6]["name"] == "/custom/exc" + else: + events = capture_events() + + content, status, _headers = unpack_werkzeug_response(client.get("/custom/ok")) + assert status.lower() == "200 ok" + assert content == b"custom ok" + + event = events.pop(0) + assert event["transaction"] == "/custom/ok" + if middleware_spans: + assert "custom_urlconf_middleware" in render_span_tree( + event["spans"], event["contexts"]["trace"] + ) + + _content, status, _headers = unpack_werkzeug_response(client.get("/custom/exc")) + assert status.lower() == "500 internal server error" + + error_event, transaction_event = events + assert error_event["transaction"] == "/custom/exc" + assert error_event["exception"]["values"][-1]["mechanism"]["type"] == "django" + assert transaction_event["transaction"] == "/custom/exc" + if middleware_spans: + assert "custom_urlconf_middleware" in render_span_tree( + transaction_event["spans"], transaction_event["contexts"]["trace"] + ) settings.MIDDLEWARE.pop(0) + client.application.load_middleware() def test_get_receiver_name(): @@ -1258,7 +2217,14 @@ def dummy(a, b): @pytest.mark.skipif(DJANGO_VERSION <= (1, 11), reason="Requires Django > 1.11") -def test_span_origin(sentry_init, client, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_span_origin( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): sentry_init( integrations=[ DjangoIntegration( @@ -1268,45 +2234,88 @@ def test_span_origin(sentry_init, client, capture_events): ) ], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() + signal_span_found = False + if span_streaming: + items = capture_items("span") - client.get(reverse("view_with_signal")) + client.get(reverse("view_with_signal")) - (transaction,) = events + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] - assert transaction["contexts"]["trace"]["origin"] == "auto.http.django" + assert spans[-1]["attributes"]["sentry.origin"] == "auto.http.django" - signal_span_found = False - for span in transaction["spans"]: - assert span["origin"] == "auto.http.django" - if span["op"] == "event.django": - signal_span_found = True + for span in spans: + assert span["attributes"]["sentry.origin"] == "auto.http.django" + if span["attributes"]["sentry.op"] == "event.django": + signal_span_found = True + else: + events = capture_events() + + client.get(reverse("view_with_signal")) + + (transaction,) = events + + assert transaction["contexts"]["trace"]["origin"] == "auto.http.django" + + for span in transaction["spans"]: + assert span["origin"] == "auto.http.django" + if span["op"] == "event.django": + signal_span_found = True assert signal_span_found -def test_transaction_http_method_default(sentry_init, client, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_transaction_http_method_default( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): """ By default OPTIONS and HEAD requests do not create a transaction. """ sentry_init( integrations=[DjangoIntegration()], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() + if span_streaming: + items = capture_items("span") - client.get("/nomessage") - client.options("/nomessage") - client.head("/nomessage") + client.get("/nomessage") + client.options("/nomessage") + client.head("/nomessage") - (event,) = events + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] - assert len(events) == 1 - assert event["request"]["method"] == "GET" + assert spans[2]["attributes"][SPANDATA.HTTP_REQUEST_METHOD] == "GET" + else: + events = capture_events() + + client.get("/nomessage") + client.options("/nomessage") + client.head("/nomessage") + + (event,) = events + + assert len(events) == 1 + assert event["request"]["method"] == "GET" -def test_transaction_http_method_custom(sentry_init, client, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_transaction_http_method_custom( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): sentry_init( integrations=[ DjangoIntegration( @@ -1317,18 +2326,32 @@ def test_transaction_http_method_custom(sentry_init, client, capture_events): ) ], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() + if span_streaming: + items = capture_items("span") + + client.get("/nomessage") + client.options("/nomessage") + client.head("/nomessage") + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + + assert spans[4]["attributes"][SPANDATA.HTTP_REQUEST_METHOD] == "OPTIONS" + assert spans[7]["attributes"][SPANDATA.HTTP_REQUEST_METHOD] == "HEAD" + else: + events = capture_events() - client.get("/nomessage") - client.options("/nomessage") - client.head("/nomessage") + client.get("/nomessage") + client.options("/nomessage") + client.head("/nomessage") - assert len(events) == 2 + assert len(events) == 2 - (event1, event2) = events - assert event1["request"]["method"] == "OPTIONS" - assert event2["request"]["method"] == "HEAD" + (event1, event2) = events + assert event1["request"]["method"] == "OPTIONS" + assert event2["request"]["method"] == "HEAD" def test_ensures_spotlight_middleware_when_spotlight_is_enabled(sentry_init, settings): diff --git a/tests/integrations/django/test_cache_module.py b/tests/integrations/django/test_cache_module.py index 01b97c1302..fc7983619c 100644 --- a/tests/integrations/django/test_cache_module.py +++ b/tests/integrations/django/test_cache_module.py @@ -93,8 +93,14 @@ def use_django_caching_with_cluster(settings): @pytest.mark.forked @pytest_mark_django_db_decorator() @pytest.mark.skipif(DJANGO_VERSION < (1, 9), reason="Requires Django >= 1.9") +@pytest.mark.parametrize("span_streaming", [True, False]) def test_cache_spans_disabled_middleware( - sentry_init, client, capture_events, use_django_caching_with_middlewares + sentry_init, + client, + capture_events, + capture_items, + use_django_caching_with_middlewares, + span_streaming, ): sentry_init( integrations=[ @@ -105,22 +111,39 @@ def test_cache_spans_disabled_middleware( ) ], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() + if span_streaming: + items = capture_items("span") - client.get(reverse("not_cached_view")) - client.get(reverse("not_cached_view")) + client.get(reverse("not_cached_view")) + client.get(reverse("not_cached_view")) - (first_event, second_event) = events - assert len(first_event["spans"]) == 0 - assert len(second_event["spans"]) == 0 + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + assert len(spans) == 2 + else: + events = capture_events() + + client.get(reverse("not_cached_view")) + client.get(reverse("not_cached_view")) + + (first_event, second_event) = events + assert len(first_event["spans"]) == 0 + assert len(second_event["spans"]) == 0 @pytest.mark.forked @pytest_mark_django_db_decorator() @pytest.mark.skipif(DJANGO_VERSION < (1, 9), reason="Requires Django >= 1.9") +@pytest.mark.parametrize("span_streaming", [True, False]) def test_cache_spans_disabled_decorator( - sentry_init, client, capture_events, use_django_caching + sentry_init, + client, + capture_events, + capture_items, + use_django_caching, + span_streaming, ): sentry_init( integrations=[ @@ -131,22 +154,39 @@ def test_cache_spans_disabled_decorator( ) ], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() + if span_streaming: + items = capture_items("span") + + client.get(reverse("cached_view")) + client.get(reverse("cached_view")) + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + assert len(spans) == 2 + else: + events = capture_events() - client.get(reverse("cached_view")) - client.get(reverse("cached_view")) + client.get(reverse("cached_view")) + client.get(reverse("cached_view")) - (first_event, second_event) = events - assert len(first_event["spans"]) == 0 - assert len(second_event["spans"]) == 0 + (first_event, second_event) = events + assert len(first_event["spans"]) == 0 + assert len(second_event["spans"]) == 0 @pytest.mark.forked @pytest_mark_django_db_decorator() @pytest.mark.skipif(DJANGO_VERSION < (1, 9), reason="Requires Django >= 1.9") +@pytest.mark.parametrize("span_streaming", [True, False]) def test_cache_spans_disabled_templatetag( - sentry_init, client, capture_events, use_django_caching + sentry_init, + client, + capture_events, + capture_items, + use_django_caching, + span_streaming, ): sentry_init( integrations=[ @@ -157,22 +197,39 @@ def test_cache_spans_disabled_templatetag( ) ], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() + if span_streaming: + items = capture_items("span") - client.get(reverse("view_with_cached_template_fragment")) - client.get(reverse("view_with_cached_template_fragment")) + client.get(reverse("view_with_cached_template_fragment")) + client.get(reverse("view_with_cached_template_fragment")) - (first_event, second_event) = events - assert len(first_event["spans"]) == 0 - assert len(second_event["spans"]) == 0 + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + assert len(spans) == 2 + else: + events = capture_events() + + client.get(reverse("view_with_cached_template_fragment")) + client.get(reverse("view_with_cached_template_fragment")) + + (first_event, second_event) = events + assert len(first_event["spans"]) == 0 + assert len(second_event["spans"]) == 0 @pytest.mark.forked @pytest_mark_django_db_decorator() @pytest.mark.skipif(DJANGO_VERSION < (1, 9), reason="Requires Django >= 1.9") +@pytest.mark.parametrize("span_streaming", [True, False]) def test_cache_spans_middleware( - sentry_init, client, capture_events, use_django_caching_with_middlewares + sentry_init, + client, + capture_events, + capture_items, + use_django_caching_with_middlewares, + span_streaming, ): sentry_init( integrations=[ @@ -183,65 +240,119 @@ def test_cache_spans_middleware( ) ], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) client.application.load_middleware() - events = capture_events() - - client.get(reverse("not_cached_view")) - client.get(reverse("not_cached_view")) - - (first_event, second_event) = events - # first_event - cache.get - assert first_event["spans"][0]["op"] == "cache.get" - assert first_event["spans"][0]["description"].startswith( - "views.decorators.cache.cache_header." - ) - assert first_event["spans"][0]["data"]["network.peer.address"] is not None - assert first_event["spans"][0]["data"]["cache.key"][0].startswith( - "views.decorators.cache.cache_header." - ) - assert not first_event["spans"][0]["data"]["cache.hit"] - assert "cache.item_size" not in first_event["spans"][0]["data"] - # first_event - cache.put - assert first_event["spans"][1]["op"] == "cache.put" - assert first_event["spans"][1]["description"].startswith( - "views.decorators.cache.cache_header." - ) - assert first_event["spans"][1]["data"]["network.peer.address"] is not None - assert first_event["spans"][1]["data"]["cache.key"][0].startswith( - "views.decorators.cache.cache_header." - ) - assert "cache.hit" not in first_event["spans"][1]["data"] - assert first_event["spans"][1]["data"]["cache.item_size"] == 2 - # second_event - cache.get - assert second_event["spans"][0]["op"] == "cache.get" - assert second_event["spans"][0]["description"].startswith( - "views.decorators.cache.cache_header." - ) - assert second_event["spans"][0]["data"]["network.peer.address"] is not None - assert second_event["spans"][0]["data"]["cache.key"][0].startswith( - "views.decorators.cache.cache_header." - ) - assert second_event["spans"][0]["data"]["cache.hit"] - assert second_event["spans"][0]["data"]["cache.item_size"] == 2 - # second_event - cache.get 2 - assert second_event["spans"][1]["op"] == "cache.get" - assert second_event["spans"][1]["description"].startswith( - "views.decorators.cache.cache_page." - ) - assert second_event["spans"][1]["data"]["network.peer.address"] is not None - assert second_event["spans"][1]["data"]["cache.key"][0].startswith( - "views.decorators.cache.cache_page." - ) - assert second_event["spans"][1]["data"]["cache.hit"] - assert second_event["spans"][1]["data"]["cache.item_size"] == 58 + if span_streaming: + items = capture_items("span") + + client.get(reverse("not_cached_view")) + client.get(reverse("not_cached_view")) + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + # first_event - cache.get + assert spans[0]["attributes"]["sentry.op"] == "cache.get" + assert spans[0]["name"].startswith("views.decorators.cache.cache_header.") + assert spans[0]["attributes"]["network.peer.address"] is not None + assert spans[0]["attributes"]["cache.key"][0].startswith( + "views.decorators.cache.cache_header." + ) + assert not spans[0]["attributes"]["cache.hit"] + assert "cache.item_size" not in spans[0]["attributes"] + # first_event - cache.put + assert spans[1]["attributes"]["sentry.op"] == "cache.put" + assert spans[1]["name"].startswith("views.decorators.cache.cache_header.") + assert spans[1]["attributes"]["network.peer.address"] is not None + assert spans[1]["attributes"]["cache.key"][0].startswith( + "views.decorators.cache.cache_header." + ) + assert "cache.hit" not in spans[1]["attributes"] + assert spans[1]["attributes"]["cache.item_size"] == 2 + # second_event - cache.get + assert spans[4]["attributes"]["sentry.op"] == "cache.get" + assert spans[4]["name"].startswith("views.decorators.cache.cache_header.") + assert spans[4]["attributes"]["network.peer.address"] is not None + assert spans[4]["attributes"]["cache.key"][0].startswith( + "views.decorators.cache.cache_header." + ) + assert spans[4]["attributes"]["cache.hit"] + assert spans[4]["attributes"]["cache.item_size"] == 2 + # second_event - cache.get 2 + assert spans[5]["attributes"]["sentry.op"] == "cache.get" + assert spans[5]["name"].startswith("views.decorators.cache.cache_page.") + assert spans[5]["attributes"]["network.peer.address"] is not None + assert spans[5]["attributes"]["cache.key"][0].startswith( + "views.decorators.cache.cache_page." + ) + assert spans[5]["attributes"]["cache.hit"] + assert spans[5]["attributes"]["cache.item_size"] == 58 + else: + events = capture_events() + + client.get(reverse("not_cached_view")) + client.get(reverse("not_cached_view")) + + (first_event, second_event) = events + # first_event - cache.get + assert first_event["spans"][0]["op"] == "cache.get" + assert first_event["spans"][0]["description"].startswith( + "views.decorators.cache.cache_header." + ) + assert first_event["spans"][0]["data"]["network.peer.address"] is not None + assert first_event["spans"][0]["data"]["cache.key"][0].startswith( + "views.decorators.cache.cache_header." + ) + assert not first_event["spans"][0]["data"]["cache.hit"] + assert "cache.item_size" not in first_event["spans"][0]["data"] + # first_event - cache.put + assert first_event["spans"][1]["op"] == "cache.put" + assert first_event["spans"][1]["description"].startswith( + "views.decorators.cache.cache_header." + ) + assert first_event["spans"][1]["data"]["network.peer.address"] is not None + assert first_event["spans"][1]["data"]["cache.key"][0].startswith( + "views.decorators.cache.cache_header." + ) + assert "cache.hit" not in first_event["spans"][1]["data"] + assert first_event["spans"][1]["data"]["cache.item_size"] == 2 + # second_event - cache.get + assert second_event["spans"][0]["op"] == "cache.get" + assert second_event["spans"][0]["description"].startswith( + "views.decorators.cache.cache_header." + ) + assert second_event["spans"][0]["data"]["network.peer.address"] is not None + assert second_event["spans"][0]["data"]["cache.key"][0].startswith( + "views.decorators.cache.cache_header." + ) + assert second_event["spans"][0]["data"]["cache.hit"] + assert second_event["spans"][0]["data"]["cache.item_size"] == 2 + # second_event - cache.get 2 + assert second_event["spans"][1]["op"] == "cache.get" + assert second_event["spans"][1]["description"].startswith( + "views.decorators.cache.cache_page." + ) + assert second_event["spans"][1]["data"]["network.peer.address"] is not None + assert second_event["spans"][1]["data"]["cache.key"][0].startswith( + "views.decorators.cache.cache_page." + ) + assert second_event["spans"][1]["data"]["cache.hit"] + assert second_event["spans"][1]["data"]["cache.item_size"] == 58 @pytest.mark.forked @pytest_mark_django_db_decorator() @pytest.mark.skipif(DJANGO_VERSION < (1, 9), reason="Requires Django >= 1.9") -def test_cache_spans_decorator(sentry_init, client, capture_events, use_django_caching): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_cache_spans_decorator( + sentry_init, + client, + capture_events, + capture_items, + use_django_caching, + span_streaming, +): sentry_init( integrations=[ DjangoIntegration( @@ -251,53 +362,96 @@ def test_cache_spans_decorator(sentry_init, client, capture_events, use_django_c ) ], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() - - client.get(reverse("cached_view")) - client.get(reverse("cached_view")) - - (first_event, second_event) = events - # first_event - cache.get - assert first_event["spans"][0]["op"] == "cache.get" - assert first_event["spans"][0]["description"].startswith( - "views.decorators.cache.cache_header." - ) - assert first_event["spans"][0]["data"]["network.peer.address"] is not None - assert first_event["spans"][0]["data"]["cache.key"][0].startswith( - "views.decorators.cache.cache_header." - ) - assert not first_event["spans"][0]["data"]["cache.hit"] - assert "cache.item_size" not in first_event["spans"][0]["data"] - # first_event - cache.put - assert first_event["spans"][1]["op"] == "cache.put" - assert first_event["spans"][1]["description"].startswith( - "views.decorators.cache.cache_header." - ) - assert first_event["spans"][1]["data"]["network.peer.address"] is not None - assert first_event["spans"][1]["data"]["cache.key"][0].startswith( - "views.decorators.cache.cache_header." - ) - assert "cache.hit" not in first_event["spans"][1]["data"] - assert first_event["spans"][1]["data"]["cache.item_size"] == 2 - # second_event - cache.get - assert second_event["spans"][1]["op"] == "cache.get" - assert second_event["spans"][1]["description"].startswith( - "views.decorators.cache.cache_page." - ) - assert second_event["spans"][1]["data"]["network.peer.address"] is not None - assert second_event["spans"][1]["data"]["cache.key"][0].startswith( - "views.decorators.cache.cache_page." - ) - assert second_event["spans"][1]["data"]["cache.hit"] - assert second_event["spans"][1]["data"]["cache.item_size"] == 58 + if span_streaming: + items = capture_items("span") + + client.get(reverse("cached_view")) + client.get(reverse("cached_view")) + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + # first_event - cache.get + assert spans[0]["attributes"]["sentry.op"] == "cache.get" + assert spans[0]["name"].startswith("views.decorators.cache.cache_header.") + assert spans[0]["attributes"]["network.peer.address"] is not None + assert spans[0]["attributes"]["cache.key"][0].startswith( + "views.decorators.cache.cache_header." + ) + assert not spans[0]["attributes"]["cache.hit"] + assert "cache.item_size" not in spans[0]["attributes"] + # first_event - cache.put + assert spans[1]["attributes"]["sentry.op"] == "cache.put" + assert spans[1]["name"].startswith("views.decorators.cache.cache_header.") + assert spans[1]["attributes"]["network.peer.address"] is not None + assert spans[1]["attributes"]["cache.key"][0].startswith( + "views.decorators.cache.cache_header." + ) + assert "cache.hit" not in spans[1]["attributes"] + assert spans[1]["attributes"]["cache.item_size"] == 2 + # second_event - cache.get + assert spans[5]["attributes"]["sentry.op"] == "cache.get" + assert spans[5]["name"].startswith("views.decorators.cache.cache_page.") + assert spans[5]["attributes"]["network.peer.address"] is not None + assert spans[5]["attributes"]["cache.key"][0].startswith( + "views.decorators.cache.cache_page." + ) + assert spans[5]["attributes"]["cache.hit"] + assert spans[5]["attributes"]["cache.item_size"] == 58 + else: + events = capture_events() + + client.get(reverse("cached_view")) + client.get(reverse("cached_view")) + + (first_event, second_event) = events + # first_event - cache.get + assert first_event["spans"][0]["op"] == "cache.get" + assert first_event["spans"][0]["description"].startswith( + "views.decorators.cache.cache_header." + ) + assert first_event["spans"][0]["data"]["network.peer.address"] is not None + assert first_event["spans"][0]["data"]["cache.key"][0].startswith( + "views.decorators.cache.cache_header." + ) + assert not first_event["spans"][0]["data"]["cache.hit"] + assert "cache.item_size" not in first_event["spans"][0]["data"] + # first_event - cache.put + assert first_event["spans"][1]["op"] == "cache.put" + assert first_event["spans"][1]["description"].startswith( + "views.decorators.cache.cache_header." + ) + assert first_event["spans"][1]["data"]["network.peer.address"] is not None + assert first_event["spans"][1]["data"]["cache.key"][0].startswith( + "views.decorators.cache.cache_header." + ) + assert "cache.hit" not in first_event["spans"][1]["data"] + assert first_event["spans"][1]["data"]["cache.item_size"] == 2 + # second_event - cache.get + assert second_event["spans"][1]["op"] == "cache.get" + assert second_event["spans"][1]["description"].startswith( + "views.decorators.cache.cache_page." + ) + assert second_event["spans"][1]["data"]["network.peer.address"] is not None + assert second_event["spans"][1]["data"]["cache.key"][0].startswith( + "views.decorators.cache.cache_page." + ) + assert second_event["spans"][1]["data"]["cache.hit"] + assert second_event["spans"][1]["data"]["cache.item_size"] == 58 @pytest.mark.forked @pytest_mark_django_db_decorator() @pytest.mark.skipif(DJANGO_VERSION < (1, 9), reason="Requires Django >= 1.9") +@pytest.mark.parametrize("span_streaming", [True, False]) def test_cache_spans_templatetag( - sentry_init, client, capture_events, use_django_caching + sentry_init, + client, + capture_events, + capture_items, + use_django_caching, + span_streaming, ): sentry_init( integrations=[ @@ -308,51 +462,89 @@ def test_cache_spans_templatetag( ) ], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() - - client.get(reverse("view_with_cached_template_fragment")) - client.get(reverse("view_with_cached_template_fragment")) - - (first_event, second_event) = events - assert len(first_event["spans"]) == 2 - # first_event - cache.get - assert first_event["spans"][0]["op"] == "cache.get" - assert first_event["spans"][0]["description"].startswith( - "template.cache.some_identifier." - ) - assert first_event["spans"][0]["data"]["network.peer.address"] is not None - assert first_event["spans"][0]["data"]["cache.key"][0].startswith( - "template.cache.some_identifier." - ) - assert not first_event["spans"][0]["data"]["cache.hit"] - assert "cache.item_size" not in first_event["spans"][0]["data"] - # first_event - cache.put - assert first_event["spans"][1]["op"] == "cache.put" - assert first_event["spans"][1]["description"].startswith( - "template.cache.some_identifier." - ) - assert first_event["spans"][1]["data"]["network.peer.address"] is not None - assert first_event["spans"][1]["data"]["cache.key"][0].startswith( - "template.cache.some_identifier." - ) - assert "cache.hit" not in first_event["spans"][1]["data"] - assert first_event["spans"][1]["data"]["cache.item_size"] == 51 - # second_event - cache.get - assert second_event["spans"][0]["op"] == "cache.get" - assert second_event["spans"][0]["description"].startswith( - "template.cache.some_identifier." - ) - assert second_event["spans"][0]["data"]["network.peer.address"] is not None - assert second_event["spans"][0]["data"]["cache.key"][0].startswith( - "template.cache.some_identifier." - ) - assert second_event["spans"][0]["data"]["cache.hit"] - assert second_event["spans"][0]["data"]["cache.item_size"] == 51 + if span_streaming: + items = capture_items("span") + + client.get(reverse("view_with_cached_template_fragment")) + client.get(reverse("view_with_cached_template_fragment")) + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + assert len(spans) == 5 + # first_event - cache.get + assert spans[0]["attributes"]["sentry.op"] == "cache.get" + assert spans[0]["name"].startswith("template.cache.some_identifier.") + assert spans[0]["attributes"]["network.peer.address"] is not None + assert spans[0]["attributes"]["cache.key"][0].startswith( + "template.cache.some_identifier." + ) + assert not spans[0]["attributes"]["cache.hit"] + assert "cache.item_size" not in spans[0]["attributes"] + # first_event - cache.put + assert spans[1]["attributes"]["sentry.op"] == "cache.put" + assert spans[1]["name"].startswith("template.cache.some_identifier.") + assert spans[1]["attributes"]["network.peer.address"] is not None + assert spans[1]["attributes"]["cache.key"][0].startswith( + "template.cache.some_identifier." + ) + assert "cache.hit" not in spans[1]["attributes"] + assert spans[1]["attributes"]["cache.item_size"] == 51 + # second_event - cache.get + assert spans[3]["attributes"]["sentry.op"] == "cache.get" + assert spans[3]["name"].startswith("template.cache.some_identifier.") + assert spans[3]["attributes"]["network.peer.address"] is not None + assert spans[3]["attributes"]["cache.key"][0].startswith( + "template.cache.some_identifier." + ) + assert spans[3]["attributes"]["cache.hit"] + assert spans[3]["attributes"]["cache.item_size"] == 51 + else: + events = capture_events() + + client.get(reverse("view_with_cached_template_fragment")) + client.get(reverse("view_with_cached_template_fragment")) + + (first_event, second_event) = events + assert len(first_event["spans"]) == 2 + # first_event - cache.get + assert first_event["spans"][0]["op"] == "cache.get" + assert first_event["spans"][0]["description"].startswith( + "template.cache.some_identifier." + ) + assert first_event["spans"][0]["data"]["network.peer.address"] is not None + assert first_event["spans"][0]["data"]["cache.key"][0].startswith( + "template.cache.some_identifier." + ) + assert not first_event["spans"][0]["data"]["cache.hit"] + assert "cache.item_size" not in first_event["spans"][0]["data"] + # first_event - cache.put + assert first_event["spans"][1]["op"] == "cache.put" + assert first_event["spans"][1]["description"].startswith( + "template.cache.some_identifier." + ) + assert first_event["spans"][1]["data"]["network.peer.address"] is not None + assert first_event["spans"][1]["data"]["cache.key"][0].startswith( + "template.cache.some_identifier." + ) + assert "cache.hit" not in first_event["spans"][1]["data"] + assert first_event["spans"][1]["data"]["cache.item_size"] == 51 + # second_event - cache.get + assert second_event["spans"][0]["op"] == "cache.get" + assert second_event["spans"][0]["description"].startswith( + "template.cache.some_identifier." + ) + assert second_event["spans"][0]["data"]["network.peer.address"] is not None + assert second_event["spans"][0]["data"]["cache.key"][0].startswith( + "template.cache.some_identifier." + ) + assert second_event["spans"][0]["data"]["cache.hit"] + assert second_event["spans"][0]["data"]["cache.item_size"] == 51 @pytest.mark.parametrize( - "method_name, args, kwargs, expected_description", + "method_name, args, kwargs, expected_name", [ (None, None, None, ""), ("get", None, None, ""), @@ -380,16 +572,23 @@ def test_cache_spans_templatetag( ), # this case should never happen, just making sure that we are not raising an exception in that case. ], ) -def test_cache_spans_get_span_description( - method_name, args, kwargs, expected_description +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_cache_spans_get_span_name( + method_name, args, kwargs, expected_name, span_streaming ): - assert _get_span_description(method_name, args, kwargs) == expected_description + assert _get_span_description(method_name, args, kwargs) == expected_name @pytest.mark.forked @pytest_mark_django_db_decorator() +@pytest.mark.parametrize("span_streaming", [True, False]) def test_cache_spans_location_with_port( - sentry_init, client, capture_events, use_django_caching_with_port + sentry_init, + client, + capture_events, + capture_items, + use_django_caching_with_port, + span_streaming, ): sentry_init( integrations=[ @@ -400,24 +599,49 @@ def test_cache_spans_location_with_port( ) ], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() + if span_streaming: + items = capture_items("span") - client.get(reverse("cached_view")) - client.get(reverse("cached_view")) + client.get(reverse("cached_view")) + client.get(reverse("cached_view")) + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + + for span in spans: + if span["is_segment"] is True: + continue - for event in events: - for span in event["spans"]: assert ( - span["data"]["network.peer.address"] == "redis://127.0.0.1" + span["attributes"]["network.peer.address"] == "redis://127.0.0.1" ) # Note: the username/password are not included in the address - assert span["data"]["network.peer.port"] == 6379 + assert span["attributes"]["network.peer.port"] == 6379 + else: + events = capture_events() + + client.get(reverse("cached_view")) + client.get(reverse("cached_view")) + + for event in events: + for span in event["spans"]: + assert ( + span["data"]["network.peer.address"] == "redis://127.0.0.1" + ) # Note: the username/password are not included in the address + assert span["data"]["network.peer.port"] == 6379 @pytest.mark.forked @pytest_mark_django_db_decorator() +@pytest.mark.parametrize("span_streaming", [True, False]) def test_cache_spans_location_without_port( - sentry_init, client, capture_events, use_django_caching_without_port + sentry_init, + client, + capture_events, + capture_items, + use_django_caching_without_port, + span_streaming, ): sentry_init( integrations=[ @@ -428,22 +652,45 @@ def test_cache_spans_location_without_port( ) ], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() + if span_streaming: + items = capture_items("span") + + client.get(reverse("cached_view")) + client.get(reverse("cached_view")) + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + + for span in spans: + if span["is_segment"] is True: + continue + + assert span["attributes"]["network.peer.address"] == "redis://example.com" + assert "network.peer.port" not in span["attributes"] + else: + events = capture_events() - client.get(reverse("cached_view")) - client.get(reverse("cached_view")) + client.get(reverse("cached_view")) + client.get(reverse("cached_view")) - for event in events: - for span in event["spans"]: - assert span["data"]["network.peer.address"] == "redis://example.com" - assert "network.peer.port" not in span["data"] + for event in events: + for span in event["spans"]: + assert span["data"]["network.peer.address"] == "redis://example.com" + assert "network.peer.port" not in span["data"] @pytest.mark.forked @pytest_mark_django_db_decorator() +@pytest.mark.parametrize("span_streaming", [True, False]) def test_cache_spans_location_with_cluster( - sentry_init, client, capture_events, use_django_caching_with_cluster + sentry_init, + client, + capture_events, + capture_items, + use_django_caching_with_cluster, + span_streaming, ): sentry_init( integrations=[ @@ -454,22 +701,45 @@ def test_cache_spans_location_with_cluster( ) ], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() + if span_streaming: + items = capture_items("span") + + client.get(reverse("cached_view")) + client.get(reverse("cached_view")) - client.get(reverse("cached_view")) - client.get(reverse("cached_view")) + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] - for event in events: - for span in event["spans"]: + for span in spans: # because it is a cluster we do not know what host is actually accessed, so we omit the data - assert "network.peer.address" not in span["data"].keys() - assert "network.peer.port" not in span["data"].keys() + assert "network.peer.address" not in span["attributes"].keys() + assert "network.peer.port" not in span["attributes"].keys() + else: + events = capture_events() + + client.get(reverse("cached_view")) + client.get(reverse("cached_view")) + + for event in events: + for span in event["spans"]: + # because it is a cluster we do not know what host is actually accessed, so we omit the data + assert "network.peer.address" not in span["data"].keys() + assert "network.peer.port" not in span["data"].keys() @pytest.mark.forked @pytest_mark_django_db_decorator() -def test_cache_spans_item_size(sentry_init, client, capture_events, use_django_caching): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_cache_spans_item_size( + sentry_init, + client, + capture_events, + capture_items, + use_django_caching, + span_streaming, +): sentry_init( integrations=[ DjangoIntegration( @@ -479,40 +749,75 @@ def test_cache_spans_item_size(sentry_init, client, capture_events, use_django_c ) ], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() + if span_streaming: + items = capture_items("span") + + client.get(reverse("cached_view")) + client.get(reverse("cached_view")) + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + assert len(spans) == 7 + assert spans[0]["attributes"]["sentry.op"] == "cache.get" + assert not spans[0]["attributes"]["cache.hit"] + assert "cache.item_size" not in spans[0]["attributes"] + + assert spans[1]["attributes"]["sentry.op"] == "cache.put" + assert "cache.hit" not in spans[1]["attributes"] + assert spans[1]["attributes"]["cache.item_size"] == 2 + + assert spans[2]["attributes"]["sentry.op"] == "cache.put" + assert "cache.hit" not in spans[2]["attributes"] + assert spans[2]["attributes"]["cache.item_size"] == 58 + + assert spans[4]["attributes"]["sentry.op"] == "cache.get" + assert spans[4]["attributes"]["cache.hit"] + assert spans[4]["attributes"]["cache.item_size"] == 2 + + assert spans[5]["attributes"]["sentry.op"] == "cache.get" + assert spans[5]["attributes"]["cache.hit"] + assert spans[5]["attributes"]["cache.item_size"] == 58 + else: + events = capture_events() - client.get(reverse("cached_view")) - client.get(reverse("cached_view")) + client.get(reverse("cached_view")) + client.get(reverse("cached_view")) - (first_event, second_event) = events - assert len(first_event["spans"]) == 3 - assert first_event["spans"][0]["op"] == "cache.get" - assert not first_event["spans"][0]["data"]["cache.hit"] - assert "cache.item_size" not in first_event["spans"][0]["data"] + (first_event, second_event) = events + assert len(first_event["spans"]) == 3 + assert first_event["spans"][0]["op"] == "cache.get" + assert not first_event["spans"][0]["data"]["cache.hit"] + assert "cache.item_size" not in first_event["spans"][0]["data"] - assert first_event["spans"][1]["op"] == "cache.put" - assert "cache.hit" not in first_event["spans"][1]["data"] - assert first_event["spans"][1]["data"]["cache.item_size"] == 2 + assert first_event["spans"][1]["op"] == "cache.put" + assert "cache.hit" not in first_event["spans"][1]["data"] + assert first_event["spans"][1]["data"]["cache.item_size"] == 2 - assert first_event["spans"][2]["op"] == "cache.put" - assert "cache.hit" not in first_event["spans"][2]["data"] - assert first_event["spans"][2]["data"]["cache.item_size"] == 58 + assert first_event["spans"][2]["op"] == "cache.put" + assert "cache.hit" not in first_event["spans"][2]["data"] + assert first_event["spans"][2]["data"]["cache.item_size"] == 58 - assert len(second_event["spans"]) == 2 - assert second_event["spans"][0]["op"] == "cache.get" - assert second_event["spans"][0]["data"]["cache.hit"] - assert second_event["spans"][0]["data"]["cache.item_size"] == 2 + assert len(second_event["spans"]) == 2 + assert second_event["spans"][0]["op"] == "cache.get" + assert second_event["spans"][0]["data"]["cache.hit"] + assert second_event["spans"][0]["data"]["cache.item_size"] == 2 - assert second_event["spans"][1]["op"] == "cache.get" - assert second_event["spans"][1]["data"]["cache.hit"] - assert second_event["spans"][1]["data"]["cache.item_size"] == 58 + assert second_event["spans"][1]["op"] == "cache.get" + assert second_event["spans"][1]["data"]["cache.hit"] + assert second_event["spans"][1]["data"]["cache.item_size"] == 58 @pytest.mark.forked @pytest_mark_django_db_decorator() +@pytest.mark.parametrize("span_streaming", [True, False]) def test_cache_spans_get_custom_default( - sentry_init, capture_events, use_django_caching + sentry_init, + capture_events, + capture_items, + use_django_caching, + span_streaming, ): sentry_init( integrations=[ @@ -523,57 +828,110 @@ def test_cache_spans_get_custom_default( ) ], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() id = os.getpid() from django.core.cache import cache - with sentry_sdk.start_transaction(): - cache.set(f"S{id}", "Sensitive1") - cache.set(f"S{id + 1}", "") + if span_streaming: + items = capture_items("span") + + with sentry_sdk.traces.start_span(name="custom parent"): + cache.set(f"S{id}", "Sensitive1") + cache.set(f"S{id + 1}", "") + + cache.get(f"S{id}", "null") + cache.get(f"S{id}", default="null") + + cache.get(f"S{id + 1}", "null") + cache.get(f"S{id + 1}", default="null") + + cache.get(f"S{id + 2}", "null") + cache.get(f"S{id + 2}", default="null") + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + assert len(spans) == 9 - cache.get(f"S{id}", "null") - cache.get(f"S{id}", default="null") + assert spans[0]["attributes"]["sentry.op"] == "cache.put" + assert spans[0]["name"] == f"S{id}" - cache.get(f"S{id + 1}", "null") - cache.get(f"S{id + 1}", default="null") + assert spans[1]["attributes"]["sentry.op"] == "cache.put" + assert spans[1]["name"] == f"S{id + 1}" - cache.get(f"S{id + 2}", "null") - cache.get(f"S{id + 2}", default="null") + for span in (spans[2], spans[3]): + assert span["attributes"]["sentry.op"] == "cache.get" + assert span["name"] == f"S{id}" + assert span["attributes"]["cache.hit"] + assert span["attributes"]["cache.item_size"] == 10 - (transaction,) = events - assert len(transaction["spans"]) == 8 + for span in (spans[4], spans[5]): + assert span["attributes"]["sentry.op"] == "cache.get" + assert span["name"] == f"S{id + 1}" + assert span["attributes"]["cache.hit"] + assert span["attributes"]["cache.item_size"] == 0 - assert transaction["spans"][0]["op"] == "cache.put" - assert transaction["spans"][0]["description"] == f"S{id}" + for span in (spans[6], spans[7]): + assert span["attributes"]["sentry.op"] == "cache.get" + assert span["name"] == f"S{id + 2}" + assert not span["attributes"]["cache.hit"] + assert "cache.item_size" not in span["attributes"] + else: + events = capture_events() + + with sentry_sdk.start_transaction(): + cache.set(f"S{id}", "Sensitive1") + cache.set(f"S{id + 1}", "") + + cache.get(f"S{id}", "null") + cache.get(f"S{id}", default="null") + + cache.get(f"S{id + 1}", "null") + cache.get(f"S{id + 1}", default="null") + + cache.get(f"S{id + 2}", "null") + cache.get(f"S{id + 2}", default="null") + + (transaction,) = events + assert len(transaction["spans"]) == 8 - assert transaction["spans"][1]["op"] == "cache.put" - assert transaction["spans"][1]["description"] == f"S{id + 1}" + assert transaction["spans"][0]["op"] == "cache.put" + assert transaction["spans"][0]["description"] == f"S{id}" - for span in (transaction["spans"][2], transaction["spans"][3]): - assert span["op"] == "cache.get" - assert span["description"] == f"S{id}" - assert span["data"]["cache.hit"] - assert span["data"]["cache.item_size"] == 10 + assert transaction["spans"][1]["op"] == "cache.put" + assert transaction["spans"][1]["description"] == f"S{id + 1}" - for span in (transaction["spans"][4], transaction["spans"][5]): - assert span["op"] == "cache.get" - assert span["description"] == f"S{id + 1}" - assert span["data"]["cache.hit"] - assert span["data"]["cache.item_size"] == 0 + for span in (transaction["spans"][2], transaction["spans"][3]): + assert span["op"] == "cache.get" + assert span["description"] == f"S{id}" + assert span["data"]["cache.hit"] + assert span["data"]["cache.item_size"] == 10 - for span in (transaction["spans"][6], transaction["spans"][7]): - assert span["op"] == "cache.get" - assert span["description"] == f"S{id + 2}" - assert not span["data"]["cache.hit"] - assert "cache.item_size" not in span["data"] + for span in (transaction["spans"][4], transaction["spans"][5]): + assert span["op"] == "cache.get" + assert span["description"] == f"S{id + 1}" + assert span["data"]["cache.hit"] + assert span["data"]["cache.item_size"] == 0 + + for span in (transaction["spans"][6], transaction["spans"][7]): + assert span["op"] == "cache.get" + assert span["description"] == f"S{id + 2}" + assert not span["data"]["cache.hit"] + assert "cache.item_size" not in span["data"] @pytest.mark.forked @pytest_mark_django_db_decorator() -def test_cache_spans_get_many(sentry_init, capture_events, use_django_caching): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_cache_spans_get_many( + sentry_init, + capture_events, + capture_items, + use_django_caching, + span_streaming, +): sentry_init( integrations=[ DjangoIntegration( @@ -583,52 +941,100 @@ def test_cache_spans_get_many(sentry_init, capture_events, use_django_caching): ) ], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() id = os.getpid() from django.core.cache import cache - with sentry_sdk.start_transaction(): - cache.get_many([f"S{id}", f"S{id + 1}"]) - cache.set(f"S{id}", "Sensitive1") - cache.get_many([f"S{id}", f"S{id + 1}"]) + if span_streaming: + items = capture_items("span") + + with sentry_sdk.traces.start_span(name="custom parent"): + cache.get_many([f"S{id}", f"S{id + 1}"]) + cache.set(f"S{id}", "Sensitive1") + cache.get_many([f"S{id}", f"S{id + 1}"]) + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + assert len(spans) == 8 + + assert spans[2]["attributes"]["sentry.op"] == "cache.get" + assert spans[2]["name"] == f"S{id}, S{id + 1}" + assert not spans[0]["attributes"]["cache.hit"] + + assert spans[0]["attributes"]["sentry.op"] == "cache.get" + assert spans[0]["name"] == f"S{id}" + assert not spans[1]["attributes"]["cache.hit"] - (transaction,) = events - assert len(transaction["spans"]) == 7 + assert spans[1]["attributes"]["sentry.op"] == "cache.get" + assert spans[1]["name"] == f"S{id + 1}" + assert not spans[2]["attributes"]["cache.hit"] - assert transaction["spans"][0]["op"] == "cache.get" - assert transaction["spans"][0]["description"] == f"S{id}, S{id + 1}" - assert not transaction["spans"][0]["data"]["cache.hit"] + assert spans[3]["attributes"]["sentry.op"] == "cache.put" + assert spans[3]["name"] == f"S{id}" - assert transaction["spans"][1]["op"] == "cache.get" - assert transaction["spans"][1]["description"] == f"S{id}" - assert not transaction["spans"][1]["data"]["cache.hit"] + assert spans[6]["attributes"]["sentry.op"] == "cache.get" + assert spans[6]["name"] == f"S{id}, S{id + 1}" + assert spans[6]["attributes"]["cache.hit"] - assert transaction["spans"][2]["op"] == "cache.get" - assert transaction["spans"][2]["description"] == f"S{id + 1}" - assert not transaction["spans"][2]["data"]["cache.hit"] + assert spans[4]["attributes"]["sentry.op"] == "cache.get" + assert spans[4]["name"] == f"S{id}" + assert spans[4]["attributes"]["cache.hit"] - assert transaction["spans"][3]["op"] == "cache.put" - assert transaction["spans"][3]["description"] == f"S{id}" + assert spans[5]["attributes"]["sentry.op"] == "cache.get" + assert spans[5]["name"] == f"S{id + 1}" + assert not spans[5]["attributes"]["cache.hit"] + else: + events = capture_events() + + with sentry_sdk.start_transaction(): + cache.get_many([f"S{id}", f"S{id + 1}"]) + cache.set(f"S{id}", "Sensitive1") + cache.get_many([f"S{id}", f"S{id + 1}"]) + + (transaction,) = events + assert len(transaction["spans"]) == 7 + + assert transaction["spans"][0]["op"] == "cache.get" + assert transaction["spans"][0]["description"] == f"S{id}, S{id + 1}" + assert not transaction["spans"][0]["data"]["cache.hit"] + + assert transaction["spans"][1]["op"] == "cache.get" + assert transaction["spans"][1]["description"] == f"S{id}" + assert not transaction["spans"][1]["data"]["cache.hit"] + + assert transaction["spans"][2]["op"] == "cache.get" + assert transaction["spans"][2]["description"] == f"S{id + 1}" + assert not transaction["spans"][2]["data"]["cache.hit"] - assert transaction["spans"][4]["op"] == "cache.get" - assert transaction["spans"][4]["description"] == f"S{id}, S{id + 1}" - assert transaction["spans"][4]["data"]["cache.hit"] + assert transaction["spans"][3]["op"] == "cache.put" + assert transaction["spans"][3]["description"] == f"S{id}" - assert transaction["spans"][5]["op"] == "cache.get" - assert transaction["spans"][5]["description"] == f"S{id}" - assert transaction["spans"][5]["data"]["cache.hit"] + assert transaction["spans"][4]["op"] == "cache.get" + assert transaction["spans"][4]["description"] == f"S{id}, S{id + 1}" + assert transaction["spans"][4]["data"]["cache.hit"] - assert transaction["spans"][6]["op"] == "cache.get" - assert transaction["spans"][6]["description"] == f"S{id + 1}" - assert not transaction["spans"][6]["data"]["cache.hit"] + assert transaction["spans"][5]["op"] == "cache.get" + assert transaction["spans"][5]["description"] == f"S{id}" + assert transaction["spans"][5]["data"]["cache.hit"] + + assert transaction["spans"][6]["op"] == "cache.get" + assert transaction["spans"][6]["description"] == f"S{id + 1}" + assert not transaction["spans"][6]["data"]["cache.hit"] @pytest.mark.forked @pytest_mark_django_db_decorator() -def test_cache_spans_set_many(sentry_init, capture_events, use_django_caching): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_cache_spans_set_many( + sentry_init, + capture_events, + capture_items, + use_django_caching, + span_streaming, +): sentry_init( integrations=[ DjangoIntegration( @@ -638,37 +1044,70 @@ def test_cache_spans_set_many(sentry_init, capture_events, use_django_caching): ) ], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() id = os.getpid() from django.core.cache import cache - with sentry_sdk.start_transaction(): - cache.set_many({f"S{id}": "Sensitive1", f"S{id + 1}": "Sensitive2"}) - cache.get(f"S{id}") + if span_streaming: + items = capture_items("span") + + with sentry_sdk.traces.start_span(name="custom parent"): + cache.set_many({f"S{id}": "Sensitive1", f"S{id + 1}": "Sensitive2"}) + cache.get(f"S{id}") + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + assert len(spans) == 5 + + assert spans[2]["attributes"]["sentry.op"] == "cache.put" + assert spans[2]["name"] == f"S{id}, S{id + 1}" + + assert spans[0]["attributes"]["sentry.op"] == "cache.put" + assert spans[0]["name"] == f"S{id}" - (transaction,) = events - assert len(transaction["spans"]) == 4 + assert spans[1]["attributes"]["sentry.op"] == "cache.put" + assert spans[1]["name"] == f"S{id + 1}" - assert transaction["spans"][0]["op"] == "cache.put" - assert transaction["spans"][0]["description"] == f"S{id}, S{id + 1}" + assert spans[3]["attributes"]["sentry.op"] == "cache.get" + assert spans[3]["name"] == f"S{id}" + else: + events = capture_events() + + with sentry_sdk.start_transaction(): + cache.set_many({f"S{id}": "Sensitive1", f"S{id + 1}": "Sensitive2"}) + cache.get(f"S{id}") + + (transaction,) = events + assert len(transaction["spans"]) == 4 - assert transaction["spans"][1]["op"] == "cache.put" - assert transaction["spans"][1]["description"] == f"S{id}" + assert transaction["spans"][0]["op"] == "cache.put" + assert transaction["spans"][0]["description"] == f"S{id}, S{id + 1}" - assert transaction["spans"][2]["op"] == "cache.put" - assert transaction["spans"][2]["description"] == f"S{id + 1}" + assert transaction["spans"][1]["op"] == "cache.put" + assert transaction["spans"][1]["description"] == f"S{id}" - assert transaction["spans"][3]["op"] == "cache.get" - assert transaction["spans"][3]["description"] == f"S{id}" + assert transaction["spans"][2]["op"] == "cache.put" + assert transaction["spans"][2]["description"] == f"S{id + 1}" + + assert transaction["spans"][3]["op"] == "cache.get" + assert transaction["spans"][3]["description"] == f"S{id}" @pytest.mark.forked @pytest_mark_django_db_decorator() @pytest.mark.skipif(DJANGO_VERSION <= (1, 11), reason="Requires Django > 1.11") -def test_span_origin_cache(sentry_init, client, capture_events, use_django_caching): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_span_origin_cache( + sentry_init, + client, + capture_events, + capture_items, + use_django_caching, + span_streaming, +): sentry_init( integrations=[ DjangoIntegration( @@ -678,19 +1117,35 @@ def test_span_origin_cache(sentry_init, client, capture_events, use_django_cachi ) ], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() + cache_span_found = False + if span_streaming: + items = capture_items("span") - client.get(reverse("cached_view")) + client.get(reverse("cached_view")) - (transaction,) = events + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] - assert transaction["contexts"]["trace"]["origin"] == "auto.http.django" + assert spans[1]["attributes"]["sentry.origin"] == "auto.http.django" - cache_span_found = False - for span in transaction["spans"]: - assert span["origin"] == "auto.http.django" - if span["op"].startswith("cache."): - cache_span_found = True + for span in spans: + assert span["attributes"]["sentry.origin"] == "auto.http.django" + if span["attributes"]["sentry.op"].startswith("cache."): + cache_span_found = True + else: + events = capture_events() + + client.get(reverse("cached_view")) + + (transaction,) = events + + assert transaction["contexts"]["trace"]["origin"] == "auto.http.django" + + for span in transaction["spans"]: + assert span["origin"] == "auto.http.django" + if span["op"].startswith("cache."): + cache_span_found = True assert cache_span_found diff --git a/tests/integrations/django/test_data_scrubbing.py b/tests/integrations/django/test_data_scrubbing.py index 128da9b97e..97fca2868d 100644 --- a/tests/integrations/django/test_data_scrubbing.py +++ b/tests/integrations/django/test_data_scrubbing.py @@ -20,37 +20,49 @@ def client(): @pytest.mark.forked @pytest_mark_django_db_decorator() +@pytest.mark.parametrize("span_streaming", [True, False]) def test_scrub_django_session_cookies_removed( sentry_init, client, - capture_events, + capture_items, + span_streaming, ): - sentry_init(integrations=[DjangoIntegration()], send_default_pii=False) - events = capture_events() + sentry_init( + integrations=[DjangoIntegration()], + send_default_pii=False, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) + items = capture_items("event") werkzeug_set_cookie(client, "localhost", "sessionid", "123") werkzeug_set_cookie(client, "localhost", "csrftoken", "456") werkzeug_set_cookie(client, "localhost", "foo", "bar") client.get(reverse("view_exc")) - (event,) = events + (event,) = (item.payload for item in items if item.type == "event") assert "cookies" not in event["request"] @pytest.mark.forked @pytest_mark_django_db_decorator() +@pytest.mark.parametrize("span_streaming", [True, False]) def test_scrub_django_session_cookies_filtered( sentry_init, client, - capture_events, + capture_items, + span_streaming, ): - sentry_init(integrations=[DjangoIntegration()], send_default_pii=True) - events = capture_events() + sentry_init( + integrations=[DjangoIntegration()], + send_default_pii=True, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) + items = capture_items("event") werkzeug_set_cookie(client, "localhost", "sessionid", "123") werkzeug_set_cookie(client, "localhost", "csrftoken", "456") werkzeug_set_cookie(client, "localhost", "foo", "bar") client.get(reverse("view_exc")) - (event,) = events + (event,) = (item.payload for item in items if item.type == "event") assert event["request"]["cookies"] == { "sessionid": "[Filtered]", "csrftoken": "[Filtered]", @@ -60,23 +72,29 @@ def test_scrub_django_session_cookies_filtered( @pytest.mark.forked @pytest_mark_django_db_decorator() +@pytest.mark.parametrize("span_streaming", [True, False]) def test_scrub_django_custom_session_cookies_filtered( sentry_init, client, - capture_events, + capture_items, settings, + span_streaming, ): settings.SESSION_COOKIE_NAME = "my_sess" settings.CSRF_COOKIE_NAME = "csrf_secret" - sentry_init(integrations=[DjangoIntegration()], send_default_pii=True) - events = capture_events() + sentry_init( + integrations=[DjangoIntegration()], + send_default_pii=True, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) + items = capture_items("event") werkzeug_set_cookie(client, "localhost", "my_sess", "123") werkzeug_set_cookie(client, "localhost", "csrf_secret", "456") werkzeug_set_cookie(client, "localhost", "foo", "bar") client.get(reverse("view_exc")) - (event,) = events + (event,) = (item.payload for item in items if item.type == "event") assert event["request"]["cookies"] == { "my_sess": "[Filtered]", "csrf_secret": "[Filtered]", diff --git a/tests/integrations/django/test_db_query_data.py b/tests/integrations/django/test_db_query_data.py index 41ad9d5e1c..6ccd7f2607 100644 --- a/tests/integrations/django/test_db_query_data.py +++ b/tests/integrations/django/test_db_query_data.py @@ -14,10 +14,14 @@ from werkzeug.test import Client +import sentry_sdk from sentry_sdk import start_transaction from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations.django import DjangoIntegration -from sentry_sdk.tracing_utils import record_sql_queries +from sentry_sdk.tracing_utils import ( + record_sql_queries, + record_sql_queries_supporting_streaming, +) from tests.conftest import unpack_werkzeug_response from tests.integrations.django.utils import pytest_mark_django_db_decorator @@ -31,13 +35,21 @@ def client(): @pytest.mark.forked @pytest_mark_django_db_decorator(transaction=True) -def test_query_source_disabled(sentry_init, client, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_query_source_disabled( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): sentry_options = { "integrations": [DjangoIntegration()], "send_default_pii": True, "traces_sample_rate": 1.0, "enable_db_query_source": False, "db_query_source_threshold_ms": 0, + "_experiments": {"trace_lifecycle": "stream" if span_streaming else "static"}, } sentry_init(**sentry_options) @@ -48,36 +60,70 @@ def test_query_source_disabled(sentry_init, client, capture_events): # trigger Django to open a new connection by marking the existing one as None. connections["postgres"].connection = None - events = capture_events() - - _, status, _ = unpack_werkzeug_response(client.get(reverse("postgres_select_orm"))) - assert status == "200 OK" - - (event,) = events - for span in event["spans"]: - if span.get("op") == "db" and "auth_user" in span.get("description"): - data = span.get("data", {}) + if span_streaming: + items = capture_items("span") - assert SPANDATA.CODE_LINENO not in data - assert SPANDATA.CODE_NAMESPACE not in data - assert SPANDATA.CODE_FILEPATH not in data - assert SPANDATA.CODE_FUNCTION not in data - break + _, status, _ = unpack_werkzeug_response( + client.get(reverse("postgres_select_orm")) + ) + assert status == "200 OK" + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + + for span in spans: + if span["attributes"].get("sentry.op") == "db" and "auth_user" in span.get( + "name" + ): + attributes = span.get("attributes", {}) + + assert SPANDATA.CODE_LINE_NUMBER not in attributes + assert SPANDATA.CODE_NAMESPACE not in attributes + assert SPANDATA.CODE_FILE_PATH not in attributes + assert SPANDATA.CODE_FUNCTION not in attributes + break + else: + raise AssertionError("No db span found") else: - raise AssertionError("No db span found") + events = capture_events() + + _, status, _ = unpack_werkzeug_response( + client.get(reverse("postgres_select_orm")) + ) + assert status == "200 OK" + + (event,) = events + for span in event["spans"]: + if span.get("op") == "db" and "auth_user" in span.get("description"): + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO not in data + assert SPANDATA.CODE_NAMESPACE not in data + assert SPANDATA.CODE_FILEPATH not in data + assert SPANDATA.CODE_FUNCTION not in data + break + else: + raise AssertionError("No db span found") @pytest.mark.forked @pytest_mark_django_db_decorator(transaction=True) @pytest.mark.parametrize("enable_db_query_source", [None, True]) +@pytest.mark.parametrize("span_streaming", [True, False]) def test_query_source_enabled( - sentry_init, client, capture_events, enable_db_query_source + sentry_init, + client, + capture_events, + capture_items, + enable_db_query_source, + span_streaming, ): sentry_options = { "integrations": [DjangoIntegration()], "send_default_pii": True, "traces_sample_rate": 1.0, "db_query_source_threshold_ms": 0, + "_experiments": {"trace_lifecycle": "stream" if span_streaming else "static"}, } if enable_db_query_source is not None: @@ -91,35 +137,69 @@ def test_query_source_enabled( # trigger Django to open a new connection by marking the existing one as None. connections["postgres"].connection = None - events = capture_events() + if span_streaming: + items = capture_items("span") - _, status, _ = unpack_werkzeug_response(client.get(reverse("postgres_select_orm"))) - assert status == "200 OK" - - (event,) = events - for span in event["spans"]: - if span.get("op") == "db" and "auth_user" in span.get("description"): - data = span.get("data", {}) - - assert SPANDATA.CODE_LINENO in data - assert SPANDATA.CODE_NAMESPACE in data - assert SPANDATA.CODE_FILEPATH in data - assert SPANDATA.CODE_FUNCTION in data - - break + _, status, _ = unpack_werkzeug_response( + client.get(reverse("postgres_select_orm")) + ) + assert status == "200 OK" + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + + for span in spans: + if span["attributes"].get("sentry.op") == "db" and "auth_user" in span.get( + "name" + ): + attributes = span.get("attributes", {}) + + assert SPANDATA.CODE_LINE_NUMBER in attributes + assert SPANDATA.CODE_NAMESPACE in attributes + assert SPANDATA.CODE_FILE_PATH in attributes + assert SPANDATA.CODE_FUNCTION in attributes + break + else: + raise AssertionError("No db span found") else: - raise AssertionError("No db span found") + events = capture_events() + + _, status, _ = unpack_werkzeug_response( + client.get(reverse("postgres_select_orm")) + ) + assert status == "200 OK" + + (event,) = events + for span in event["spans"]: + if span.get("op") == "db" and "auth_user" in span.get("description"): + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO in data + assert SPANDATA.CODE_NAMESPACE in data + assert SPANDATA.CODE_FILEPATH in data + assert SPANDATA.CODE_FUNCTION in data + break + else: + raise AssertionError("No db span found") @pytest.mark.forked @pytest_mark_django_db_decorator(transaction=True) -def test_query_source(sentry_init, client, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_query_source( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): sentry_init( integrations=[DjangoIntegration()], send_default_pii=True, traces_sample_rate=1.0, enable_db_query_source=True, db_query_source_threshold_ms=0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) if "postgres" not in connections: @@ -128,45 +208,94 @@ def test_query_source(sentry_init, client, capture_events): # trigger Django to open a new connection by marking the existing one as None. connections["postgres"].connection = None - events = capture_events() + if span_streaming: + items = capture_items("span") - _, status, _ = unpack_werkzeug_response(client.get(reverse("postgres_select_orm"))) - assert status == "200 OK" + _, status, _ = unpack_werkzeug_response( + client.get(reverse("postgres_select_orm")) + ) + assert status == "200 OK" - (event,) = events - for span in event["spans"]: - if span.get("op") == "db" and "auth_user" in span.get("description"): - data = span.get("data", {}) + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] - assert SPANDATA.CODE_LINENO in data - assert SPANDATA.CODE_NAMESPACE in data - assert SPANDATA.CODE_FILEPATH in data - assert SPANDATA.CODE_FUNCTION in data + for span in spans: + if span["attributes"].get("sentry.op") == "db" and "auth_user" in span.get( + "name" + ): + attributes = span.get("attributes", {}) - assert type(data.get(SPANDATA.CODE_LINENO)) == int - assert data.get(SPANDATA.CODE_LINENO) > 0 + assert SPANDATA.CODE_LINE_NUMBER in attributes + assert SPANDATA.CODE_NAMESPACE in attributes + assert SPANDATA.CODE_FILE_PATH in attributes + assert SPANDATA.CODE_FUNCTION in attributes - assert ( - data.get(SPANDATA.CODE_NAMESPACE) - == "tests.integrations.django.myapp.views" - ) - assert data.get(SPANDATA.CODE_FILEPATH).endswith( - "tests/integrations/django/myapp/views.py" - ) + assert type(attributes.get(SPANDATA.CODE_LINE_NUMBER)) == int + assert attributes.get(SPANDATA.CODE_LINE_NUMBER) > 0 - is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep - assert is_relative_path + assert ( + attributes.get(SPANDATA.CODE_NAMESPACE) + == "tests.integrations.django.myapp.views" + ) + assert attributes.get(SPANDATA.CODE_FILE_PATH).endswith( + "tests/integrations/django/myapp/views.py" + ) - assert data.get(SPANDATA.CODE_FUNCTION) == "postgres_select_orm" + is_relative_path = attributes.get(SPANDATA.CODE_FILE_PATH)[0] != os.sep + assert is_relative_path - break + assert attributes.get(SPANDATA.CODE_FUNCTION) == "postgres_select_orm" + break + else: + raise AssertionError("No db span found") else: - raise AssertionError("No db span found") + events = capture_events() + + _, status, _ = unpack_werkzeug_response( + client.get(reverse("postgres_select_orm")) + ) + assert status == "200 OK" + + (event,) = events + for span in event["spans"]: + if span.get("op") == "db" and "auth_user" in span.get("description"): + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO in data + assert SPANDATA.CODE_NAMESPACE in data + assert SPANDATA.CODE_FILEPATH in data + assert SPANDATA.CODE_FUNCTION in data + + assert type(data.get(SPANDATA.CODE_LINENO)) == int + assert data.get(SPANDATA.CODE_LINENO) > 0 + + assert ( + data.get(SPANDATA.CODE_NAMESPACE) + == "tests.integrations.django.myapp.views" + ) + assert data.get(SPANDATA.CODE_FILEPATH).endswith( + "tests/integrations/django/myapp/views.py" + ) + + is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep + assert is_relative_path + + assert data.get(SPANDATA.CODE_FUNCTION) == "postgres_select_orm" + break + else: + raise AssertionError("No db span found") @pytest.mark.forked @pytest_mark_django_db_decorator(transaction=True) -def test_query_source_with_module_in_search_path(sentry_init, client, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_query_source_with_module_in_search_path( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): """ Test that query source is relative to the path of the module it ran in """ @@ -178,6 +307,7 @@ def test_query_source_with_module_in_search_path(sentry_init, client, capture_ev traces_sample_rate=1.0, enable_db_query_source=True, db_query_source_threshold_ms=0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) if "postgres" not in connections: @@ -186,41 +316,84 @@ def test_query_source_with_module_in_search_path(sentry_init, client, capture_ev # trigger Django to open a new connection by marking the existing one as None. connections["postgres"].connection = None - events = capture_events() + if span_streaming: + items = capture_items("span") - _, status, _ = unpack_werkzeug_response( - client.get(reverse("postgres_select_slow_from_supplement")) - ) - assert status == "200 OK" + _, status, _ = unpack_werkzeug_response( + client.get(reverse("postgres_select_slow_from_supplement")) + ) + assert status == "200 OK" - (event,) = events - for span in event["spans"]: - if span.get("op") == "db" and "auth_user" in span.get("description"): - data = span.get("data", {}) + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] - assert SPANDATA.CODE_LINENO in data - assert SPANDATA.CODE_NAMESPACE in data - assert SPANDATA.CODE_FILEPATH in data - assert SPANDATA.CODE_FUNCTION in data + for span in spans: + if span["attributes"].get("sentry.op") == "db" and "auth_user" in span.get( + "name" + ): + attributes = span.get("attributes", {}) - assert type(data.get(SPANDATA.CODE_LINENO)) == int - assert data.get(SPANDATA.CODE_LINENO) > 0 - assert data.get(SPANDATA.CODE_NAMESPACE) == "django_helpers.views" - assert data.get(SPANDATA.CODE_FILEPATH) == "django_helpers/views.py" + assert SPANDATA.CODE_LINE_NUMBER in attributes + assert SPANDATA.CODE_NAMESPACE in attributes + assert SPANDATA.CODE_FILE_PATH in attributes + assert SPANDATA.CODE_FUNCTION in attributes - is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep - assert is_relative_path + assert type(attributes.get(SPANDATA.CODE_LINE_NUMBER)) == int + assert attributes.get(SPANDATA.CODE_LINE_NUMBER) > 0 + assert attributes.get(SPANDATA.CODE_NAMESPACE) == "django_helpers.views" + assert ( + attributes.get(SPANDATA.CODE_FILE_PATH) == "django_helpers/views.py" + ) - assert data.get(SPANDATA.CODE_FUNCTION) == "postgres_select_orm" + is_relative_path = attributes.get(SPANDATA.CODE_FILE_PATH)[0] != os.sep + assert is_relative_path - break + assert attributes.get(SPANDATA.CODE_FUNCTION) == "postgres_select_orm" + break + else: + raise AssertionError("No db span found") else: - raise AssertionError("No db span found") + events = capture_events() + + _, status, _ = unpack_werkzeug_response( + client.get(reverse("postgres_select_slow_from_supplement")) + ) + assert status == "200 OK" + + (event,) = events + for span in event["spans"]: + if span.get("op") == "db" and "auth_user" in span.get("description"): + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO in data + assert SPANDATA.CODE_NAMESPACE in data + assert SPANDATA.CODE_FILEPATH in data + assert SPANDATA.CODE_FUNCTION in data + + assert type(data.get(SPANDATA.CODE_LINENO)) == int + assert data.get(SPANDATA.CODE_LINENO) > 0 + assert data.get(SPANDATA.CODE_NAMESPACE) == "django_helpers.views" + assert data.get(SPANDATA.CODE_FILEPATH) == "django_helpers/views.py" + + is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep + assert is_relative_path + + assert data.get(SPANDATA.CODE_FUNCTION) == "postgres_select_orm" + break + else: + raise AssertionError("No db span found") @pytest.mark.forked @pytest_mark_django_db_decorator(transaction=True) -def test_query_source_with_in_app_exclude(sentry_init, client, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_query_source_with_in_app_exclude( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): sentry_init( integrations=[DjangoIntegration()], send_default_pii=True, @@ -228,6 +401,7 @@ def test_query_source_with_in_app_exclude(sentry_init, client, capture_events): enable_db_query_source=True, db_query_source_threshold_ms=0, in_app_exclude=["tests.integrations.django.myapp.views"], + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) if "postgres" not in connections: @@ -236,54 +410,112 @@ def test_query_source_with_in_app_exclude(sentry_init, client, capture_events): # trigger Django to open a new connection by marking the existing one as None. connections["postgres"].connection = None - events = capture_events() - - _, status, _ = unpack_werkzeug_response(client.get(reverse("postgres_select_orm"))) - assert status == "200 OK" + if span_streaming: + items = capture_items("span") - (event,) = events - for span in event["spans"]: - if span.get("op") == "db" and "auth_user" in span.get("description"): - data = span.get("data", {}) - - assert SPANDATA.CODE_LINENO in data - assert SPANDATA.CODE_NAMESPACE in data - assert SPANDATA.CODE_FILEPATH in data - assert SPANDATA.CODE_FUNCTION in data - - assert type(data.get(SPANDATA.CODE_LINENO)) == int - assert data.get(SPANDATA.CODE_LINENO) > 0 - - if DJANGO_VERSION >= (1, 11): - assert ( - data.get(SPANDATA.CODE_NAMESPACE) - == "tests.integrations.django.myapp.settings" - ) - assert data.get(SPANDATA.CODE_FILEPATH).endswith( - "tests/integrations/django/myapp/settings.py" - ) - assert data.get(SPANDATA.CODE_FUNCTION) == "middleware" - else: - assert ( - data.get(SPANDATA.CODE_NAMESPACE) - == "tests.integrations.django.test_db_query_data" - ) - assert data.get(SPANDATA.CODE_FILEPATH).endswith( - "tests/integrations/django/test_db_query_data.py" - ) - assert ( - data.get(SPANDATA.CODE_FUNCTION) - == "test_query_source_with_in_app_exclude" - ) - - break + _, status, _ = unpack_werkzeug_response( + client.get(reverse("postgres_select_orm")) + ) + assert status == "200 OK" + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + + for span in spans: + if span["attributes"].get("sentry.op") == "db" and "auth_user" in span.get( + "name" + ): + attributes = span.get("attributes", {}) + + assert SPANDATA.CODE_LINE_NUMBER in attributes + assert SPANDATA.CODE_NAMESPACE in attributes + assert SPANDATA.CODE_FILE_PATH in attributes + assert SPANDATA.CODE_FUNCTION in attributes + + assert type(attributes.get(SPANDATA.CODE_LINE_NUMBER)) == int + assert attributes.get(SPANDATA.CODE_LINE_NUMBER) > 0 + + if DJANGO_VERSION >= (1, 11): + assert ( + attributes.get(SPANDATA.CODE_NAMESPACE) + == "tests.integrations.django.myapp.settings" + ) + assert attributes.get(SPANDATA.CODE_FILE_PATH).endswith( + "tests/integrations/django/myapp/settings.py" + ) + assert attributes.get(SPANDATA.CODE_FUNCTION) == "middleware" + else: + assert ( + attributes.get(SPANDATA.CODE_NAMESPACE) + == "tests.integrations.django.test_db_query_data" + ) + assert attributes.get(SPANDATA.CODE_FILE_PATH).endswith( + "tests/integrations/django/test_db_query_data.py" + ) + assert ( + attributes.get(SPANDATA.CODE_FUNCTION) + == "test_query_source_with_in_app_exclude" + ) + break + else: + raise AssertionError("No db span found") else: - raise AssertionError("No db span found") + events = capture_events() + + _, status, _ = unpack_werkzeug_response( + client.get(reverse("postgres_select_orm")) + ) + assert status == "200 OK" + + (event,) = events + for span in event["spans"]: + if span.get("op") == "db" and "auth_user" in span.get("description"): + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO in data + assert SPANDATA.CODE_NAMESPACE in data + assert SPANDATA.CODE_FILEPATH in data + assert SPANDATA.CODE_FUNCTION in data + + assert type(data.get(SPANDATA.CODE_LINENO)) == int + assert data.get(SPANDATA.CODE_LINENO) > 0 + + if DJANGO_VERSION >= (1, 11): + assert ( + data.get(SPANDATA.CODE_NAMESPACE) + == "tests.integrations.django.myapp.settings" + ) + assert data.get(SPANDATA.CODE_FILEPATH).endswith( + "tests/integrations/django/myapp/settings.py" + ) + assert data.get(SPANDATA.CODE_FUNCTION) == "middleware" + else: + assert ( + data.get(SPANDATA.CODE_NAMESPACE) + == "tests.integrations.django.test_db_query_data" + ) + assert data.get(SPANDATA.CODE_FILEPATH).endswith( + "tests/integrations/django/test_db_query_data.py" + ) + assert ( + data.get(SPANDATA.CODE_FUNCTION) + == "test_query_source_with_in_app_exclude" + ) + break + else: + raise AssertionError("No db span found") @pytest.mark.forked @pytest_mark_django_db_decorator(transaction=True) -def test_query_source_with_in_app_include(sentry_init, client, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_query_source_with_in_app_include( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): sentry_init( integrations=[DjangoIntegration()], send_default_pii=True, @@ -291,6 +523,7 @@ def test_query_source_with_in_app_include(sentry_init, client, capture_events): enable_db_query_source=True, db_query_source_threshold_ms=0, in_app_include=["django"], + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) if "postgres" not in connections: @@ -299,43 +532,92 @@ def test_query_source_with_in_app_include(sentry_init, client, capture_events): # trigger Django to open a new connection by marking the existing one as None. connections["postgres"].connection = None - events = capture_events() + if span_streaming: + items = capture_items("span") + + _, status, _ = unpack_werkzeug_response( + client.get(reverse("postgres_select_orm")) + ) + assert status == "200 OK" - _, status, _ = unpack_werkzeug_response(client.get(reverse("postgres_select_orm"))) - assert status == "200 OK" + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] - (event,) = events - for span in event["spans"]: - if span.get("op") == "db" and "auth_user" in span.get("description"): - data = span.get("data", {}) + for span in spans: + if span["attributes"].get("sentry.op") == "db" and "auth_user" in span.get( + "name" + ): + attributes = span.get("attributes", {}) - assert SPANDATA.CODE_LINENO in data - assert SPANDATA.CODE_NAMESPACE in data - assert SPANDATA.CODE_FILEPATH in data - assert SPANDATA.CODE_FUNCTION in data + assert SPANDATA.CODE_LINE_NUMBER in attributes + assert SPANDATA.CODE_NAMESPACE in attributes + assert SPANDATA.CODE_FILE_PATH in attributes + assert SPANDATA.CODE_FUNCTION in attributes - assert type(data.get(SPANDATA.CODE_LINENO)) == int - assert data.get(SPANDATA.CODE_LINENO) > 0 + assert type(attributes.get(SPANDATA.CODE_LINE_NUMBER)) == int + assert attributes.get(SPANDATA.CODE_LINE_NUMBER) > 0 - assert data.get(SPANDATA.CODE_NAMESPACE) == "django.db.models.sql.compiler" - assert data.get(SPANDATA.CODE_FILEPATH).endswith( - "django/db/models/sql/compiler.py" - ) - assert data.get(SPANDATA.CODE_FUNCTION) == "execute_sql" - break + assert ( + attributes.get(SPANDATA.CODE_NAMESPACE) + == "django.db.models.sql.compiler" + ) + assert attributes.get(SPANDATA.CODE_FILE_PATH).endswith( + "django/db/models/sql/compiler.py" + ) + assert attributes.get(SPANDATA.CODE_FUNCTION) == "execute_sql" + break + else: + raise AssertionError("No db span found") else: - raise AssertionError("No db span found") + events = capture_events() + + _, status, _ = unpack_werkzeug_response( + client.get(reverse("postgres_select_orm")) + ) + assert status == "200 OK" + + (event,) = events + for span in event["spans"]: + if span.get("op") == "db" and "auth_user" in span.get("description"): + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO in data + assert SPANDATA.CODE_NAMESPACE in data + assert SPANDATA.CODE_FILEPATH in data + assert SPANDATA.CODE_FUNCTION in data + + assert type(data.get(SPANDATA.CODE_LINENO)) == int + assert data.get(SPANDATA.CODE_LINENO) > 0 + + assert ( + data.get(SPANDATA.CODE_NAMESPACE) == "django.db.models.sql.compiler" + ) + assert data.get(SPANDATA.CODE_FILEPATH).endswith( + "django/db/models/sql/compiler.py" + ) + assert data.get(SPANDATA.CODE_FUNCTION) == "execute_sql" + break + else: + raise AssertionError("No db span found") @pytest.mark.forked @pytest_mark_django_db_decorator(transaction=True) -def test_no_query_source_if_duration_too_short(sentry_init, client, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_no_query_source_if_duration_too_short( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): sentry_init( integrations=[DjangoIntegration()], send_default_pii=True, traces_sample_rate=1.0, enable_db_query_source=True, db_query_source_threshold_ms=100, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) if "postgres" not in connections: @@ -344,15 +626,20 @@ def test_no_query_source_if_duration_too_short(sentry_init, client, capture_even # trigger Django to open a new connection by marking the existing one as None. connections["postgres"].connection = None - events = capture_events() - class fake_record_sql_queries: # noqa: N801 def __init__(self, *args, **kwargs): - with record_sql_queries(*args, **kwargs) as span: - self.span = span + if span_streaming: + with record_sql_queries_supporting_streaming(*args, **kwargs) as span: + self.span = span - self.span.start_timestamp = datetime(2024, 1, 1, microsecond=0) - self.span.timestamp = datetime(2024, 1, 1, microsecond=99999) + self.span._start_timestamp = datetime(2024, 1, 1, microsecond=0) + self.span._end_timestamp = datetime(2024, 1, 1, microsecond=99999) + else: + with record_sql_queries(*args, **kwargs) as span: + self.span = span + + self.span.start_timestamp = datetime(2024, 1, 1, microsecond=0) + self.span.timestamp = datetime(2024, 1, 1, microsecond=99999) def __enter__(self): return self.span @@ -360,40 +647,79 @@ def __enter__(self): def __exit__(self, type, value, traceback): pass - with mock.patch( - "sentry_sdk.integrations.django.record_sql_queries", - fake_record_sql_queries, - ): - _, status, _ = unpack_werkzeug_response( - client.get(reverse("postgres_select_orm")) - ) + if span_streaming: + items = capture_items("span") - assert status == "200 OK" + with mock.patch( + "sentry_sdk.integrations.django.record_sql_queries_supporting_streaming", + fake_record_sql_queries, + ): + _, status, _ = unpack_werkzeug_response( + client.get(reverse("postgres_select_orm")) + ) + + assert status == "200 OK" - (event,) = events - for span in event["spans"]: - if span.get("op") == "db" and "auth_user" in span.get("description"): - data = span.get("data", {}) + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] - assert SPANDATA.CODE_LINENO not in data - assert SPANDATA.CODE_NAMESPACE not in data - assert SPANDATA.CODE_FILEPATH not in data - assert SPANDATA.CODE_FUNCTION not in data + for span in spans: + if span["attributes"].get("sentry.op") == "db" and "auth_user" in span.get( + "name" + ): + attributes = span.get("attributes", {}) - break + assert SPANDATA.CODE_LINE_NUMBER not in attributes + assert SPANDATA.CODE_NAMESPACE not in attributes + assert SPANDATA.CODE_FILE_PATH not in attributes + assert SPANDATA.CODE_FUNCTION not in attributes + break + else: + raise AssertionError("No db span found") else: - raise AssertionError("No db span found") + events = capture_events() + + with mock.patch( + "sentry_sdk.integrations.django.record_sql_queries_supporting_streaming", + fake_record_sql_queries, + ): + _, status, _ = unpack_werkzeug_response( + client.get(reverse("postgres_select_orm")) + ) + + assert status == "200 OK" + + (event,) = events + for span in event["spans"]: + if span.get("op") == "db" and "auth_user" in span.get("description"): + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO not in data + assert SPANDATA.CODE_NAMESPACE not in data + assert SPANDATA.CODE_FILEPATH not in data + assert SPANDATA.CODE_FUNCTION not in data + break + else: + raise AssertionError("No db span found") @pytest.mark.forked @pytest_mark_django_db_decorator(transaction=True) -def test_query_source_if_duration_over_threshold(sentry_init, client, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_query_source_if_duration_over_threshold( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): sentry_init( integrations=[DjangoIntegration()], send_default_pii=True, traces_sample_rate=1.0, enable_db_query_source=True, db_query_source_threshold_ms=100, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) if "postgres" not in connections: @@ -402,15 +728,20 @@ def test_query_source_if_duration_over_threshold(sentry_init, client, capture_ev # trigger Django to open a new connection by marking the existing one as None. connections["postgres"].connection = None - events = capture_events() - class fake_record_sql_queries: # noqa: N801 def __init__(self, *args, **kwargs): - with record_sql_queries(*args, **kwargs) as span: - self.span = span + if span_streaming: + with record_sql_queries_supporting_streaming(*args, **kwargs) as span: + self.span = span + + self.span._start_timestamp = datetime(2024, 1, 1, microsecond=0) + self.span._end_timestamp = datetime(2024, 1, 1, microsecond=101000) + else: + with record_sql_queries(*args, **kwargs) as span: + self.span = span - self.span.start_timestamp = datetime(2024, 1, 1, microsecond=0) - self.span.timestamp = datetime(2024, 1, 1, microsecond=101000) + self.span.start_timestamp = datetime(2024, 1, 1, microsecond=0) + self.span.timestamp = datetime(2024, 1, 1, microsecond=101000) def __enter__(self): return self.span @@ -418,52 +749,108 @@ def __enter__(self): def __exit__(self, type, value, traceback): pass - with mock.patch( - "sentry_sdk.integrations.django.record_sql_queries", - fake_record_sql_queries, - ): - _, status, _ = unpack_werkzeug_response( - client.get(reverse("postgres_select_orm")) - ) + if span_streaming: + items = capture_items("span") - assert status == "200 OK" + with mock.patch( + "sentry_sdk.integrations.django.record_sql_queries_supporting_streaming", + fake_record_sql_queries, + ): + _, status, _ = unpack_werkzeug_response( + client.get(reverse("postgres_select_orm")) + ) - (event,) = events - for span in event["spans"]: - if span.get("op") == "db" and "auth_user" in span.get("description"): - data = span.get("data", {}) + assert status == "200 OK" - assert SPANDATA.CODE_LINENO in data - assert SPANDATA.CODE_NAMESPACE in data - assert SPANDATA.CODE_FILEPATH in data - assert SPANDATA.CODE_FUNCTION in data + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] - assert type(data.get(SPANDATA.CODE_LINENO)) == int - assert data.get(SPANDATA.CODE_LINENO) > 0 + for span in spans: + if span["attributes"].get("sentry.op") == "db" and "auth_user" in span.get( + "name" + ): + attributes = span.get("attributes", {}) - assert ( - data.get(SPANDATA.CODE_NAMESPACE) - == "tests.integrations.django.myapp.views" - ) - assert data.get(SPANDATA.CODE_FILEPATH).endswith( - "tests/integrations/django/myapp/views.py" - ) + assert SPANDATA.CODE_LINE_NUMBER in attributes + assert SPANDATA.CODE_NAMESPACE in attributes + assert SPANDATA.CODE_FILE_PATH in attributes + assert SPANDATA.CODE_FUNCTION in attributes + + assert type(attributes.get(SPANDATA.CODE_LINE_NUMBER)) == int + assert attributes.get(SPANDATA.CODE_LINE_NUMBER) > 0 + + assert ( + attributes.get(SPANDATA.CODE_NAMESPACE) + == "tests.integrations.django.myapp.views" + ) + assert attributes.get(SPANDATA.CODE_FILE_PATH).endswith( + "tests/integrations/django/myapp/views.py" + ) - is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep - assert is_relative_path + is_relative_path = attributes.get(SPANDATA.CODE_FILE_PATH)[0] != os.sep + assert is_relative_path - assert data.get(SPANDATA.CODE_FUNCTION) == "postgres_select_orm" - break + assert attributes.get(SPANDATA.CODE_FUNCTION) == "postgres_select_orm" + break + else: + raise AssertionError("No db span found") else: - raise AssertionError("No db span found") + events = capture_events() + + with mock.patch( + "sentry_sdk.integrations.django.record_sql_queries_supporting_streaming", + fake_record_sql_queries, + ): + _, status, _ = unpack_werkzeug_response( + client.get(reverse("postgres_select_orm")) + ) + + assert status == "200 OK" + + (event,) = events + for span in event["spans"]: + if span.get("op") == "db" and "auth_user" in span.get("description"): + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO in data + assert SPANDATA.CODE_NAMESPACE in data + assert SPANDATA.CODE_FILEPATH in data + assert SPANDATA.CODE_FUNCTION in data + + assert type(data.get(SPANDATA.CODE_LINENO)) == int + assert data.get(SPANDATA.CODE_LINENO) > 0 + + assert ( + data.get(SPANDATA.CODE_NAMESPACE) + == "tests.integrations.django.myapp.views" + ) + assert data.get(SPANDATA.CODE_FILEPATH).endswith( + "tests/integrations/django/myapp/views.py" + ) + + is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep + assert is_relative_path + + assert data.get(SPANDATA.CODE_FUNCTION) == "postgres_select_orm" + break + else: + raise AssertionError("No db span found") @pytest.mark.forked @pytest_mark_django_db_decorator(transaction=True) -def test_db_span_origin_execute(sentry_init, client, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_db_span_origin_execute( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): sentry_init( integrations=[DjangoIntegration()], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) if "postgres" not in connections: @@ -472,55 +859,106 @@ def test_db_span_origin_execute(sentry_init, client, capture_events): # trigger Django to open a new connection by marking the existing one as None. connections["postgres"].connection = None - events = capture_events() + if span_streaming: + items = capture_items("span") - client.get(reverse("postgres_select_orm")) + client.get(reverse("postgres_select_orm")) - (event,) = events + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] - assert event["contexts"]["trace"]["origin"] == "auto.http.django" + assert spans[1]["attributes"]["sentry.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" + for span in spans: + if span["attributes"]["sentry.op"] == "db": + assert span["attributes"]["sentry.origin"] == "auto.db.django" + else: + assert span["attributes"]["sentry.origin"] == "auto.http.django" + else: + events = capture_events() + + client.get(reverse("postgres_select_orm")) + + (event,) = events + + 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" @pytest.mark.forked @pytest_mark_django_db_decorator(transaction=True) -def test_db_span_origin_executemany(sentry_init, client, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_db_span_origin_executemany( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): sentry_init( integrations=[DjangoIntegration()], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - 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 = """UPDATE auth_user SET username = %s where id = %s;""" - query_list = ( - ( - "test1", - 1, - ), - ( - "test2", - 2, - ), - ) - cursor.executemany(query, query_list) + if span_streaming: + items = capture_items("span") + with sentry_sdk.traces.start_span(name="custom parent"): + from django.db import connection, transaction + + cursor = connection.cursor() + + query = """UPDATE auth_user SET username = %s where id = %s;""" + query_list = ( + ( + "test1", + 1, + ), + ( + "test2", + 2, + ), + ) + cursor.executemany(query, query_list) + + transaction.commit() + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + + assert spans[1]["attributes"]["sentry.origin"] == "manual" + assert spans[0]["attributes"]["sentry.origin"] == "auto.db.django" + else: + events = capture_events() + with start_transaction(name="test_transaction"): + from django.db import connection, transaction + + cursor = connection.cursor() + + query = """UPDATE auth_user SET username = %s where id = %s;""" + query_list = ( + ( + "test1", + 1, + ), + ( + "test2", + 2, + ), + ) + cursor.executemany(query, query_list) - transaction.commit() + transaction.commit() - (event,) = events + (event,) = events - assert event["contexts"]["trace"]["origin"] == "manual" - assert event["spans"][0]["origin"] == "auto.db.django" + assert event["contexts"]["trace"]["origin"] == "manual" + assert event["spans"][0]["origin"] == "auto.db.django" diff --git a/tests/integrations/django/test_db_transactions.py b/tests/integrations/django/test_db_transactions.py index 2750397b0e..76da6c4951 100644 --- a/tests/integrations/django/test_db_transactions.py +++ b/tests/integrations/django/test_db_transactions.py @@ -13,6 +13,7 @@ from werkzeug.test import Client +import sentry_sdk from sentry_sdk import start_transaction from sentry_sdk.consts import SPANDATA, SPANNAME from sentry_sdk.integrations.django import DjangoIntegration @@ -28,12 +29,18 @@ def client(): @pytest.mark.forked @pytest_mark_django_db_decorator(transaction=True) +@pytest.mark.parametrize("span_streaming", [True, False]) def test_db_transaction_spans_disabled_no_autocommit( - sentry_init, client, capture_events + sentry_init, + client, + capture_events, + capture_items, + span_streaming, ): sentry_init( integrations=[DjangoIntegration()], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) if "postgres" not in connections: @@ -42,17 +49,18 @@ def test_db_transaction_spans_disabled_no_autocommit( # trigger Django to open a new connection by marking the existing one as None. connections["postgres"].connection = None - events = capture_events() + if span_streaming: + items = capture_items("span") - client.get(reverse("postgres_insert_orm_no_autocommit_rollback")) - client.get(reverse("postgres_insert_orm_no_autocommit")) + client.get(reverse("postgres_insert_orm_no_autocommit_rollback")) + client.get(reverse("postgres_insert_orm_no_autocommit")) - with start_transaction(name="test_transaction"): - from django.db import connection, transaction + with sentry_sdk.traces.start_span(name="custom parent"): + from django.db import connection, transaction - cursor = connection.cursor() + cursor = connection.cursor() - query = """INSERT INTO auth_user ( + query = """INSERT INTO auth_user ( password, is_superuser, username, @@ -65,34 +73,34 @@ def test_db_transaction_spans_disabled_no_autocommit( ) 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), - ), - ) + 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) + 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 + with sentry_sdk.traces.start_span(name="custom parent"): + from django.db import connection, transaction - cursor = connection.cursor() + cursor = connection.cursor() - query = """INSERT INTO auth_user ( + query = """INSERT INTO auth_user ( password, is_superuser, username, @@ -105,75 +113,65 @@ def test_db_transaction_spans_disabled_no_autocommit( ) 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_rollback, postgres_commit, sqlite_rollback, sqlite_commit) = events - - # Ensure operation is persisted - assert User.objects.using("postgres").exists() - - 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_rollback["spans"], - postgres_commit["spans"], - sqlite_rollback["spans"], - sqlite_commit["spans"], - ) - if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_COMMIT - or span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_ROLLBACK - ] - assert len(commit_spans) == 0 - - -@pytest.mark.forked -@pytest_mark_django_db_decorator(transaction=True) -def test_db_transaction_spans_disabled_atomic(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_atomic_rollback")) - client.get(reverse("postgres_insert_orm_atomic")) + query_list = ( + ( + "user1", + "John", + "Doe", + "user1@example.com", + datetime(1970, 1, 1), + ), + ( + "user2", + "Max", + "Mustermann", + "user2@example.com", + datetime(1970, 1, 1), + ), + ) - with start_transaction(name="test_transaction"): - from django.db import connection, transaction + transaction.set_autocommit(False) + cursor.executemany(query, query_list) + transaction.commit() + transaction.set_autocommit(True) + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + + postgres_rollback = spans[4] + assert postgres_rollback["is_segment"] is True + postgres_commit = spans[9] + assert postgres_commit["is_segment"] is True + sqlite_rollback = spans[11] + assert sqlite_rollback["is_segment"] is True + sqlite_commit = spans[13] + assert sqlite_commit["is_segment"] is True + + # Ensure operation is persisted + assert User.objects.using("postgres").exists() + + assert postgres_rollback["attributes"]["sentry.origin"] == "auto.http.django" + assert postgres_commit["attributes"]["sentry.origin"] == "auto.http.django" + assert sqlite_rollback["attributes"]["sentry.origin"] == "manual" + assert sqlite_commit["attributes"]["sentry.origin"] == "manual" + + commit_spans = [ + span + for span in spans + if span["attributes"].get(SPANDATA.DB_OPERATION_NAME) == SPANNAME.DB_COMMIT + or span["attributes"].get(SPANDATA.DB_OPERATION_NAME) + == SPANNAME.DB_ROLLBACK + ] + assert len(commit_spans) == 0 + else: + 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"): + from django.db import connection, transaction - with transaction.atomic(): cursor = connection.cursor() query = """INSERT INTO auth_user ( @@ -205,13 +203,15 @@ def test_db_transaction_spans_disabled_atomic(sentry_init, client, capture_event datetime(1970, 1, 1), ), ) + + transaction.set_autocommit(False) cursor.executemany(query, query_list) - transaction.set_rollback(True) + transaction.rollback() + transaction.set_autocommit(True) - with start_transaction(name="test_transaction"): - from django.db import connection, transaction + with start_transaction(name="test_transaction"): + from django.db import connection, transaction - with transaction.atomic(): cursor = connection.cursor() query = """INSERT INTO auth_user ( @@ -243,38 +243,50 @@ def test_db_transaction_spans_disabled_atomic(sentry_init, client, capture_event 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_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_rollback["spans"], - postgres_commit["spans"], - sqlite_rollback["spans"], - sqlite_commit["spans"], - ) - if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_COMMIT - or span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_ROLLBACK - ] - assert len(commit_spans) == 0 + transaction.set_autocommit(False) + cursor.executemany(query, query_list) + transaction.commit() + transaction.set_autocommit(True) + + (postgres_rollback, postgres_commit, sqlite_rollback, sqlite_commit) = events + + # Ensure operation is persisted + assert User.objects.using("postgres").exists() + + 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_rollback["spans"], + postgres_commit["spans"], + sqlite_rollback["spans"], + sqlite_commit["spans"], + ) + if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_COMMIT + or span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_ROLLBACK + ] + assert len(commit_spans) == 0 @pytest.mark.forked @pytest_mark_django_db_decorator(transaction=True) -def test_db_no_autocommit_execute(sentry_init, client, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_db_transaction_spans_disabled_atomic( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): sentry_init( - integrations=[DjangoIntegration(db_transaction_spans=True)], + integrations=[DjangoIntegration()], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) if "postgres" not in connections: @@ -283,66 +295,119 @@ def test_db_no_autocommit_execute(sentry_init, client, capture_events): # 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")) + if span_streaming: + items = capture_items("span") - (event,) = events + client.get(reverse("postgres_insert_orm_atomic_rollback")) + client.get(reverse("postgres_insert_orm_atomic")) - # Ensure operation is persisted - assert User.objects.using("postgres").exists() + with sentry_sdk.traces.start_span(name="custom parent"): + from django.db import connection, transaction - assert event["contexts"]["trace"]["origin"] == "auto.http.django" - - commit_spans = [ - span - for span in event["spans"] - if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_COMMIT - ] - assert len(commit_spans) == 1 - commit_span = commit_spans[0] - assert commit_span["origin"] == "auto.db.django" + with transaction.atomic(): + cursor = connection.cursor() - # 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" - ) + 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);""" - insert_spans = [ - span for span in event["spans"] if span["description"].startswith("INSERT INTO") - ] - assert len(insert_spans) == 1 - insert_span = insert_spans[0] + 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) - # Verify query and commit statements are siblings - assert commit_span["parent_span_id"] == insert_span["parent_span_id"] + with sentry_sdk.traces.start_span(name="custom parent"): + from django.db import connection, transaction + with transaction.atomic(): + cursor = connection.cursor() -@pytest.mark.forked -@pytest_mark_django_db_decorator(transaction=True) -def test_db_no_autocommit_executemany(sentry_init, client, capture_events): - sentry_init( - integrations=[DjangoIntegration(db_transaction_spans=True)], - traces_sample_rate=1.0, - ) + 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);""" - events = capture_events() + 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) - with start_transaction(name="test_transaction"): - from django.db import connection, transaction + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + + postgres_rollback = spans[4] + assert postgres_rollback["is_segment"] is True + postgres_commit = spans[9] + assert postgres_commit["is_segment"] is True + sqlite_rollback = spans[12] + assert sqlite_rollback["is_segment"] is True + sqlite_commit = spans[15] + assert sqlite_commit["is_segment"] is True + + commit_spans = [ + span + for span in spans + if span["attributes"].get(SPANDATA.DB_OPERATION_NAME) == SPANNAME.DB_COMMIT + or span["attributes"].get(SPANDATA.DB_OPERATION_NAME) + == SPANNAME.DB_ROLLBACK + ] + else: + events = capture_events() + + client.get(reverse("postgres_insert_orm_atomic_rollback")) + client.get(reverse("postgres_insert_orm_atomic")) + + with start_transaction(name="test_transaction"): + from django.db import connection, transaction - cursor = connection.cursor() + with transaction.atomic(): + cursor = connection.cursor() - query = """INSERT INTO auth_user ( + query = """INSERT INTO auth_user ( password, is_superuser, username, @@ -355,68 +420,101 @@ def test_db_no_autocommit_executemany(sentry_init, client, capture_events): ) 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), - ), - ) + 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) - transaction.set_autocommit(False) - cursor.executemany(query, query_list) - transaction.commit() - transaction.set_autocommit(True) + with start_transaction(name="test_transaction"): + from django.db import connection, transaction - (event,) = 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);""" - # Ensure operation is persisted - assert User.objects.exists() + 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) - assert event["contexts"]["trace"]["origin"] == "manual" - assert event["spans"][0]["origin"] == "auto.db.django" + (postgres_rollback, postgres_commit, sqlite_rollback, sqlite_commit) = events - commit_spans = [ - span - for span in event["spans"] - if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_COMMIT - ] - assert len(commit_spans) == 1 - commit_span = commit_spans[0] - assert commit_span["origin"] == "auto.db.django" + # Ensure operation is persisted + assert User.objects.using("postgres").exists() - # 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") + 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" - insert_spans = [ - span for span in event["spans"] if span["description"].startswith("INSERT INTO") - ] + commit_spans = [ + span + for span in itertools.chain( + postgres_rollback["spans"], + postgres_commit["spans"], + sqlite_rollback["spans"], + sqlite_commit["spans"], + ) + if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_COMMIT + or span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_ROLLBACK + ] - # Verify queries and commit statements are siblings - for insert_span in insert_spans: - assert commit_span["parent_span_id"] == insert_span["parent_span_id"] + assert len(commit_spans) == 0 @pytest.mark.forked @pytest_mark_django_db_decorator(transaction=True) -def test_db_no_autocommit_rollback_execute(sentry_init, client, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_db_no_autocommit_execute( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): sentry_init( integrations=[DjangoIntegration(db_transaction_spans=True)], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) if "postgres" not in connections: @@ -425,66 +523,120 @@ def test_db_no_autocommit_rollback_execute(sentry_init, client, capture_events): # trigger Django to open a new connection by marking the existing one as None. connections["postgres"].connection = None - events = capture_events() + if span_streaming: + items = capture_items("span") - client.get(reverse("postgres_insert_orm_no_autocommit_rollback")) + client.get(reverse("postgres_insert_orm_no_autocommit")) - (event,) = events + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] - # Ensure operation is rolled back - assert not User.objects.using("postgres").exists() + # Ensure operation is persisted + assert User.objects.using("postgres").exists() - assert event["contexts"]["trace"]["origin"] == "auto.http.django" + assert spans[5]["attributes"]["sentry.origin"] == "auto.http.django" - rollback_spans = [ - span - for span in event["spans"] - if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_ROLLBACK - ] - assert len(rollback_spans) == 1 - rollback_span = rollback_spans[0] - assert rollback_span["origin"] == "auto.db.django" + commit_spans = [ + span + for span in spans + if span["attributes"].get(SPANDATA.DB_OPERATION_NAME) == SPANNAME.DB_COMMIT + ] - # 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" - ) + assert len(commit_spans) == 1 + commit_span = commit_spans[0] + + assert commit_span["attributes"]["sentry.origin"] == "auto.db.django" + + # Verify other database attributes + assert commit_span["attributes"].get(SPANDATA.DB_SYSTEM_NAME) == "postgresql" + conn_params = connections["postgres"].get_connection_params() + assert commit_span["attributes"].get(SPANDATA.DB_NAMESPACE) is not None + assert commit_span["attributes"].get(SPANDATA.DB_NAMESPACE) == conn_params.get( + "database" + ) or conn_params.get("dbname") + assert commit_span["attributes"].get(SPANDATA.SERVER_ADDRESS) == os.environ.get( + "SENTRY_PYTHON_TEST_POSTGRES_HOST", "localhost" + ) + assert commit_span["attributes"].get(SPANDATA.SERVER_PORT) == os.environ.get( + "SENTRY_PYTHON_TEST_POSTGRES_PORT", "5432" + ) + + insert_spans = [ + span for span in spans if span["name"].startswith("INSERT INTO") + ] + else: + events = capture_events() + + client.get(reverse("postgres_insert_orm_no_autocommit")) + + (event,) = events + + # Ensure operation is persisted + assert 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) == SPANNAME.DB_COMMIT + ] + + assert len(commit_spans) == 1 + commit_span = commit_spans[0] + + assert commit_span["origin"] == "auto.db.django" - insert_spans = [ - span for span in event["spans"] if span["description"].startswith("INSERT INTO") - ] + # 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" + ) + + 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"] + # Verify query and commit statements are siblings + assert commit_span["parent_span_id"] == insert_span["parent_span_id"] @pytest.mark.forked @pytest_mark_django_db_decorator(transaction=True) -def test_db_no_autocommit_rollback_executemany(sentry_init, client, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_db_no_autocommit_executemany( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): sentry_init( integrations=[DjangoIntegration(db_transaction_spans=True)], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) + if span_streaming: + items = capture_items("span") - events = capture_events() + with sentry_sdk.traces.start_span(name="custom parent"): + from django.db import connection, transaction - with start_transaction(name="test_transaction"): - from django.db import connection, transaction - - cursor = connection.cursor() + cursor = connection.cursor() - query = """INSERT INTO auth_user ( + query = """INSERT INTO auth_user ( password, is_superuser, username, @@ -497,68 +649,154 @@ def test_db_no_autocommit_rollback_executemany(sentry_init, client, capture_even ) 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), - ), - ) + 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) + transaction.set_autocommit(False) + cursor.executemany(query, query_list) + transaction.commit() + transaction.set_autocommit(True) - (event,) = events + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] - # Ensure operation is rolled back - assert not User.objects.exists() + # Ensure operation is persisted + assert User.objects.exists() - assert event["contexts"]["trace"]["origin"] == "manual" - assert event["spans"][0]["origin"] == "auto.db.django" + assert spans[2]["attributes"]["sentry.origin"] == "manual" + assert spans[0]["attributes"]["sentry.origin"] == "auto.db.django" - rollback_spans = [ - span - for span in event["spans"] - if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_ROLLBACK - ] - assert len(rollback_spans) == 1 - rollback_span = rollback_spans[0] - assert rollback_span["origin"] == "auto.db.django" + commit_spans = [ + span + for span in spans + if span["attributes"].get(SPANDATA.DB_OPERATION_NAME) == SPANNAME.DB_COMMIT + ] - # 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") + assert len(commit_spans) == 1 + commit_span = commit_spans[0] - insert_spans = [ - span for span in event["spans"] if span["description"].startswith("INSERT INTO") - ] + assert commit_span["attributes"]["sentry.origin"] == "auto.db.django" - # Verify queries and rollback statements are siblings - for insert_span in insert_spans: - assert rollback_span["parent_span_id"] == insert_span["parent_span_id"] + # Verify other database attributes + assert commit_span["attributes"].get(SPANDATA.DB_SYSTEM_NAME) == "sqlite" + conn_params = connection.get_connection_params() + assert commit_span["attributes"].get(SPANDATA.DB_NAMESPACE) is not None + assert commit_span["attributes"].get(SPANDATA.DB_NAMESPACE) == conn_params.get( + "database" + ) or conn_params.get("dbname") + + insert_spans = [ + span for span in spans if span["name"].startswith("INSERT INTO") + ] + else: + 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.commit() + transaction.set_autocommit(True) + + (event,) = events + + # Ensure operation is persisted + assert 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) == SPANNAME.DB_COMMIT + ] + + 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") + + 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 commit_span["parent_span_id"] == insert_span["parent_span_id"] @pytest.mark.forked @pytest_mark_django_db_decorator(transaction=True) -def test_db_atomic_execute(sentry_init, client, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_db_no_autocommit_rollback_execute( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): sentry_init( integrations=[DjangoIntegration(db_transaction_spans=True)], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) if "postgres" not in connections: @@ -567,65 +805,192 @@ def test_db_atomic_execute(sentry_init, client, capture_events): # trigger Django to open a new connection by marking the existing one as None. connections["postgres"].connection = None - events = capture_events() + if span_streaming: + items = capture_items("span") - client.get(reverse("postgres_insert_orm_atomic")) + client.get(reverse("postgres_insert_orm_no_autocommit_rollback")) - (event,) = events + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] - # Ensure operation is persisted - assert User.objects.using("postgres").exists() + # Ensure operation is rolled back + assert not User.objects.using("postgres").exists() - assert event["contexts"]["trace"]["origin"] == "auto.http.django" + assert spans[5]["attributes"]["sentry.origin"] == "auto.http.django" - commit_spans = [ - span - for span in event["spans"] - if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_COMMIT - ] - assert len(commit_spans) == 1 - commit_span = commit_spans[0] - assert commit_span["origin"] == "auto.db.django" + rollback_spans = [ + span + for span in spans + if span["attributes"].get(SPANDATA.DB_OPERATION_NAME) + == SPANNAME.DB_ROLLBACK + ] - # 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" - ) + assert len(rollback_spans) == 1 + rollback_span = rollback_spans[0] + + assert rollback_span["attributes"]["sentry.origin"] == "auto.db.django" + + # Verify other database attributes + assert rollback_span["attributes"].get(SPANDATA.DB_SYSTEM_NAME) == "postgresql" + conn_params = connections["postgres"].get_connection_params() + assert rollback_span["attributes"].get(SPANDATA.DB_NAMESPACE) is not None + assert rollback_span["attributes"].get( + SPANDATA.DB_NAMESPACE + ) == conn_params.get("database") or conn_params.get("dbname") + assert rollback_span["attributes"].get( + SPANDATA.SERVER_ADDRESS + ) == os.environ.get("SENTRY_PYTHON_TEST_POSTGRES_HOST", "localhost") + assert rollback_span["attributes"].get(SPANDATA.SERVER_PORT) == os.environ.get( + "SENTRY_PYTHON_TEST_POSTGRES_PORT", "5432" + ) + + insert_spans = [ + span for span in spans if span["name"].startswith("INSERT INTO") + ] + else: + 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" - insert_spans = [ - span for span in event["spans"] if span["description"].startswith("INSERT INTO") - ] + rollback_spans = [ + span + for span in event["spans"] + if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_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 commit statements are siblings - assert commit_span["parent_span_id"] == insert_span["parent_span_id"] + # 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(sentry_init, client, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_db_no_autocommit_rollback_executemany( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): sentry_init( integrations=[DjangoIntegration(db_transaction_spans=True)], - send_default_pii=True, traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) + if span_streaming: + items = capture_items("span") + + with sentry_sdk.traces.start_span(name="custom parent"): + from django.db import connection, transaction + + cursor = connection.cursor() - events = capture_events() + 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), + ), + ) - with start_transaction(name="test_transaction"): - from django.db import connection, transaction + transaction.set_autocommit(False) + cursor.executemany(query, query_list) + transaction.rollback() + transaction.set_autocommit(True) + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + + # Ensure operation is rolled back + assert not User.objects.exists() + + assert spans[2]["attributes"]["sentry.origin"] == "manual" + assert spans[0]["attributes"]["sentry.origin"] == "auto.db.django" + + rollback_spans = [ + span + for span in spans + if span["attributes"].get(SPANDATA.DB_OPERATION_NAME) + == SPANNAME.DB_ROLLBACK + ] + assert len(rollback_spans) == 1 + rollback_span = rollback_spans[0] + + assert rollback_span["attributes"]["sentry.origin"] == "auto.db.django" + + # Verify other database attributes + assert rollback_span["attributes"].get(SPANDATA.DB_SYSTEM_NAME) == "sqlite" + conn_params = connection.get_connection_params() + assert rollback_span["attributes"].get(SPANDATA.DB_NAMESPACE) is not None + assert rollback_span["attributes"].get( + SPANDATA.DB_NAMESPACE + ) == conn_params.get("database") or conn_params.get("dbname") + + insert_spans = [ + span for span in spans if span["name"].startswith("INSERT INTO") + ] + else: + 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 ( @@ -657,48 +1022,63 @@ def test_db_atomic_executemany(sentry_init, client, capture_events): datetime(1970, 1, 1), ), ) + + transaction.set_autocommit(False) cursor.executemany(query, query_list) + transaction.rollback() + transaction.set_autocommit(True) - (event,) = events + (event,) = events - # Ensure operation is persisted - assert User.objects.exists() + # Ensure operation is rolled back + assert not User.objects.exists() - assert event["contexts"]["trace"]["origin"] == "manual" + 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) == SPANNAME.DB_COMMIT - ] - assert len(commit_spans) == 1 - commit_span = commit_spans[0] - assert commit_span["origin"] == "auto.db.django" + rollback_spans = [ + span + for span in event["spans"] + if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_ROLLBACK + ] + assert len(rollback_spans) == 1 + rollback_span = rollback_spans[0] - # 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") + assert rollback_span["origin"] == "auto.db.django" - insert_spans = [ - span for span in event["spans"] if span["description"].startswith("INSERT INTO") - ] + # 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") - # Verify queries and commit statements are siblings + 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 commit_span["parent_span_id"] == insert_span["parent_span_id"] + 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_rollback_execute(sentry_init, client, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_db_atomic_execute( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): sentry_init( integrations=[DjangoIntegration(db_transaction_spans=True)], - send_default_pii=True, traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) if "postgres" not in connections: @@ -707,68 +1087,191 @@ def test_db_atomic_rollback_execute(sentry_init, client, capture_events): # trigger Django to open a new connection by marking the existing one as None. connections["postgres"].connection = None - events = capture_events() + if span_streaming: + items = capture_items("span") - client.get(reverse("postgres_insert_orm_atomic_rollback")) + client.get(reverse("postgres_insert_orm_atomic")) - (event,) = events + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] - # Ensure operation is rolled back - assert not User.objects.using("postgres").exists() + # Ensure operation is persisted + assert User.objects.using("postgres").exists() - assert event["contexts"]["trace"]["origin"] == "auto.http.django" + assert spans[5]["attributes"]["sentry.origin"] == "auto.http.django" - rollback_spans = [ - span - for span in event["spans"] - if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_ROLLBACK - ] - assert len(rollback_spans) == 1 - rollback_span = rollback_spans[0] - assert rollback_span["origin"] == "auto.db.django" + commit_spans = [ + span + for span in spans + if span["attributes"].get(SPANDATA.DB_OPERATION_NAME) == SPANNAME.DB_COMMIT + ] - # 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" - ) + assert len(commit_spans) == 1 + commit_span = commit_spans[0] - insert_spans = [ - span for span in event["spans"] if span["description"].startswith("INSERT INTO") - ] + assert commit_span["attributes"]["sentry.origin"] == "auto.db.django" + + # Verify other database attributes + assert commit_span["attributes"].get(SPANDATA.DB_SYSTEM_NAME) == "postgresql" + conn_params = connections["postgres"].get_connection_params() + assert commit_span["attributes"].get(SPANDATA.DB_NAMESPACE) is not None + assert commit_span["attributes"].get(SPANDATA.DB_NAMESPACE) == conn_params.get( + "database" + ) or conn_params.get("dbname") + assert commit_span["attributes"].get(SPANDATA.SERVER_ADDRESS) == os.environ.get( + "SENTRY_PYTHON_TEST_POSTGRES_HOST", "localhost" + ) + assert commit_span["attributes"].get(SPANDATA.SERVER_PORT) == os.environ.get( + "SENTRY_PYTHON_TEST_POSTGRES_PORT", "5432" + ) + + insert_spans = [ + span for span in spans if span["name"].startswith("INSERT INTO") + ] + else: + events = capture_events() + + client.get(reverse("postgres_insert_orm_atomic")) + + (event,) = events + + # Ensure operation is persisted + assert 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) == SPANNAME.DB_COMMIT + ] + + 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" + ) + + 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"] + # Verify query and commit statements are siblings + assert commit_span["parent_span_id"] == insert_span["parent_span_id"] @pytest.mark.forked @pytest_mark_django_db_decorator(transaction=True) -def test_db_atomic_rollback_executemany(sentry_init, client, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_db_atomic_executemany( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): sentry_init( integrations=[DjangoIntegration(db_transaction_spans=True)], send_default_pii=True, traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) + if span_streaming: + items = capture_items("span") - events = capture_events() + with sentry_sdk.traces.start_span(name="custom parent"): + from django.db import connection, transaction - with start_transaction(name="test_transaction"): - from django.db import connection, transaction + with transaction.atomic(): + cursor = connection.cursor() - 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 = """INSERT INTO auth_user ( + 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) + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + + # Ensure operation is persisted + assert User.objects.exists() + + assert spans[3]["attributes"]["sentry.origin"] == "manual" + + commit_spans = [ + span + for span in spans + if span["attributes"].get(SPANDATA.DB_OPERATION_NAME) == SPANNAME.DB_COMMIT + ] + assert len(commit_spans) == 1 + commit_span = commit_spans[0] + + assert commit_span["attributes"]["sentry.origin"] == "auto.db.django" + + # Verify other database attributes + assert commit_span["attributes"].get(SPANDATA.DB_SYSTEM_NAME) == "sqlite" + conn_params = connection.get_connection_params() + assert commit_span["attributes"].get(SPANDATA.DB_NAMESPACE) is not None + assert commit_span["attributes"].get(SPANDATA.DB_NAMESPACE) == conn_params.get( + "database" + ) or conn_params.get("dbname") + + insert_spans = [ + span for span in spans if span["name"].startswith("INSERT INTO") + ] + else: + 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, @@ -781,65 +1284,75 @@ def test_db_atomic_rollback_executemany(sentry_init, client, capture_events): ) 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) + 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) - (event,) = events + (event,) = events - # Ensure operation is rolled back - assert not User.objects.exists() + # Ensure operation is persisted + assert User.objects.exists() - assert event["contexts"]["trace"]["origin"] == "manual" + assert event["contexts"]["trace"]["origin"] == "manual" - rollback_spans = [ - span - for span in event["spans"] - if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_ROLLBACK - ] - assert len(rollback_spans) == 1 - rollback_span = rollback_spans[0] - assert rollback_span["origin"] == "auto.db.django" + commit_spans = [ + span + for span in event["spans"] + if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_COMMIT + ] + assert len(commit_spans) == 1 + commit_span = commit_spans[0] - # 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") + assert commit_span["origin"] == "auto.db.django" - insert_spans = [ - span for span in event["spans"] if span["description"].startswith("INSERT INTO") - ] + # 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") - # Verify queries and rollback statements are siblings + 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"] + assert commit_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): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_db_atomic_rollback_execute( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): sentry_init( integrations=[DjangoIntegration(db_transaction_spans=True)], send_default_pii=True, traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) if "postgres" not in connections: @@ -848,43 +1361,91 @@ def test_db_atomic_execute_exception(sentry_init, client, capture_events): # trigger Django to open a new connection by marking the existing one as None. connections["postgres"].connection = None - events = capture_events() + if span_streaming: + items = capture_items("span") - client.get(reverse("postgres_insert_orm_atomic_exception")) + client.get(reverse("postgres_insert_orm_atomic_rollback")) - (event,) = events + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] - # Ensure operation is rolled back - assert not User.objects.using("postgres").exists() + # Ensure operation is rolled back + assert not User.objects.using("postgres").exists() - assert event["contexts"]["trace"]["origin"] == "auto.http.django" + assert spans[5]["attributes"]["sentry.origin"] == "auto.http.django" - rollback_spans = [ - span - for span in event["spans"] - if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_ROLLBACK - ] - assert len(rollback_spans) == 1 - rollback_span = rollback_spans[0] - assert rollback_span["origin"] == "auto.db.django" + rollback_spans = [ + span + for span in spans + if span["attributes"].get(SPANDATA.DB_OPERATION_NAME) + == SPANNAME.DB_ROLLBACK + ] - # 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" - ) + assert len(rollback_spans) == 1 + rollback_span = rollback_spans[0] + + assert rollback_span["attributes"]["sentry.origin"] == "auto.db.django" + + # Verify other database attributes + assert rollback_span["attributes"].get(SPANDATA.DB_SYSTEM_NAME) == "postgresql" + conn_params = connections["postgres"].get_connection_params() + assert rollback_span["attributes"].get(SPANDATA.DB_NAMESPACE) is not None + assert rollback_span["attributes"].get( + SPANDATA.DB_NAMESPACE + ) == conn_params.get("database") or conn_params.get("dbname") + assert rollback_span["attributes"].get( + SPANDATA.SERVER_ADDRESS + ) == os.environ.get("SENTRY_PYTHON_TEST_POSTGRES_HOST", "localhost") + assert rollback_span["attributes"].get(SPANDATA.SERVER_PORT) == os.environ.get( + "SENTRY_PYTHON_TEST_POSTGRES_PORT", "5432" + ) + + insert_spans = [ + span for span in spans if span["name"].startswith("INSERT INTO") + ] + else: + 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" + + rollback_spans = [ + span + for span in event["spans"] + if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_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") + ] - insert_spans = [ - span for span in event["spans"] if span["description"].startswith("INSERT INTO") - ] assert len(insert_spans) == 1 insert_span = insert_spans[0] @@ -894,19 +1455,98 @@ def test_db_atomic_execute_exception(sentry_init, client, capture_events): @pytest.mark.forked @pytest_mark_django_db_decorator(transaction=True) -def test_db_atomic_executemany_exception(sentry_init, client, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_db_atomic_rollback_executemany( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): sentry_init( integrations=[DjangoIntegration(db_transaction_spans=True)], send_default_pii=True, traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) + if span_streaming: + items = capture_items("span") + + with sentry_sdk.traces.start_span(name="custom parent"): + 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) + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + + # Ensure operation is rolled back + assert not User.objects.exists() + + assert spans[3]["attributes"]["sentry.origin"] == "manual" + + rollback_spans = [ + span + for span in spans + if span["attributes"].get(SPANDATA.DB_OPERATION_NAME) + == SPANNAME.DB_ROLLBACK + ] + + assert len(rollback_spans) == 1 + rollback_span = rollback_spans[0] + + assert rollback_span["attributes"]["sentry.origin"] == "auto.db.django" - events = capture_events() + # Verify other database attributes + assert rollback_span["attributes"].get(SPANDATA.DB_SYSTEM_NAME) == "sqlite" + conn_params = connection.get_connection_params() + assert rollback_span["attributes"].get(SPANDATA.DB_NAMESPACE) is not None + assert rollback_span["attributes"].get( + SPANDATA.DB_NAMESPACE + ) == conn_params.get("database") or conn_params.get("dbname") - with start_transaction(name="test_transaction"): - from django.db import connection, transaction + insert_spans = [ + span for span in spans if span["name"].startswith("INSERT INTO") + ] + else: + events = capture_events() + + with start_transaction(name="test_transaction"): + from django.db import connection, transaction - try: with transaction.atomic(): cursor = connection.cursor() @@ -940,37 +1580,323 @@ def test_db_atomic_executemany_exception(sentry_init, client, capture_events): ), ) 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) == SPANNAME.DB_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") - ] + transaction.set_rollback(True) + + (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) == SPANNAME.DB_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"] + + +@pytest.mark.forked +@pytest_mark_django_db_decorator(transaction=True) +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_db_atomic_execute_exception( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): + sentry_init( + integrations=[DjangoIntegration(db_transaction_spans=True)], + send_default_pii=True, + traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) + + 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 + + if span_streaming: + items = capture_items("span") + + client.get(reverse("postgres_insert_orm_atomic_exception")) + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + + # Ensure operation is rolled back + assert not User.objects.using("postgres").exists() + + assert spans[5]["attributes"]["sentry.origin"] == "auto.http.django" + + rollback_spans = [ + span + for span in spans + if span["attributes"].get(SPANDATA.DB_OPERATION_NAME) + == SPANNAME.DB_ROLLBACK + ] + + assert len(rollback_spans) == 1 + rollback_span = rollback_spans[0] + + assert rollback_span["attributes"]["sentry.origin"] == "auto.db.django" + + # Verify other database attributes + assert rollback_span["attributes"].get(SPANDATA.DB_SYSTEM_NAME) == "postgresql" + conn_params = connections["postgres"].get_connection_params() + assert rollback_span["attributes"].get(SPANDATA.DB_NAMESPACE) is not None + assert rollback_span["attributes"].get( + SPANDATA.DB_NAMESPACE + ) == conn_params.get("database") or conn_params.get("dbname") + assert rollback_span["attributes"].get( + SPANDATA.SERVER_ADDRESS + ) == os.environ.get("SENTRY_PYTHON_TEST_POSTGRES_HOST", "localhost") + assert rollback_span["attributes"].get(SPANDATA.SERVER_PORT) == os.environ.get( + "SENTRY_PYTHON_TEST_POSTGRES_PORT", "5432" + ) + + insert_spans = [ + span for span in spans if span["name"].startswith("INSERT INTO") + ] + else: + 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) == SPANNAME.DB_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) +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_db_atomic_executemany_exception( + sentry_init, + client, + capture_events, + capture_items, + span_streaming, +): + sentry_init( + integrations=[DjangoIntegration(db_transaction_spans=True)], + send_default_pii=True, + traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) + if span_streaming: + items = capture_items("span") + + with sentry_sdk.traces.start_span(name="custom parent"): + 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 + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + + # Ensure operation is rolled back + assert not User.objects.exists() + + assert spans[3]["attributes"]["sentry.origin"] == "manual" + + rollback_spans = [ + span + for span in spans + if span["attributes"].get(SPANDATA.DB_OPERATION_NAME) + == SPANNAME.DB_ROLLBACK + ] + assert len(rollback_spans) == 1 + rollback_span = rollback_spans[0] + + assert rollback_span["attributes"]["sentry.origin"] == "auto.db.django" + + # Verify other database attributes + assert rollback_span["attributes"].get(SPANDATA.DB_SYSTEM_NAME) == "sqlite" + conn_params = connection.get_connection_params() + assert rollback_span["attributes"].get(SPANDATA.DB_NAMESPACE) is not None + assert rollback_span["attributes"].get( + SPANDATA.DB_NAMESPACE + ) == conn_params.get("database") or conn_params.get("dbname") + + insert_spans = [ + span for span in spans if span["name"].startswith("INSERT INTO") + ] + else: + 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) == SPANNAME.DB_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: diff --git a/tests/integrations/django/test_tasks.py b/tests/integrations/django/test_tasks.py index 56c68b807f..8cf46959f4 100644 --- a/tests/integrations/django/test_tasks.py +++ b/tests/integrations/django/test_tasks.py @@ -52,29 +52,60 @@ def task_two(): not HAS_DJANGO_TASKS, reason="Django tasks are only available in Django 6.0+", ) -def test_task_span_is_created(sentry_init, capture_events, immediate_backend): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_task_span_is_created( + sentry_init, + capture_events, + capture_items, + immediate_backend, + span_streaming, +): """Test that the queue.submit.django span is created when a task is enqueued.""" sentry_init( integrations=[DjangoIntegration()], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() - - with sentry_sdk.start_transaction(name="test_transaction"): - simple_task.enqueue() - - (event,) = events - assert event["type"] == "transaction" - - queue_submit_spans = [ - span for span in event["spans"] if span["op"] == OP.QUEUE_SUBMIT_DJANGO - ] - assert len(queue_submit_spans) == 1 - assert ( - queue_submit_spans[0]["description"] - == "tests.integrations.django.test_tasks.simple_task" - ) - assert queue_submit_spans[0]["origin"] == "auto.http.django" + if span_streaming: + items = capture_items("span") + + with sentry_sdk.traces.start_span(name="custom parent"): + simple_task.enqueue() + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + + queue_submit_spans = [ + span + for span in spans + if span["attributes"].get("sentry.op") == OP.QUEUE_SUBMIT_DJANGO + ] + assert len(queue_submit_spans) == 1 + assert ( + queue_submit_spans[0]["name"] + == "tests.integrations.django.test_tasks.simple_task" + ) + assert ( + queue_submit_spans[0]["attributes"]["sentry.origin"] == "auto.http.django" + ) + else: + events = capture_events() + + with sentry_sdk.start_transaction(name="test_transaction"): + simple_task.enqueue() + + (event,) = events + assert event["type"] == "transaction" + + queue_submit_spans = [ + span for span in event["spans"] if span["op"] == OP.QUEUE_SUBMIT_DJANGO + ] + assert len(queue_submit_spans) == 1 + assert ( + queue_submit_spans[0]["description"] + == "tests.integrations.django.test_tasks.simple_task" + ) + assert queue_submit_spans[0]["origin"] == "auto.http.django" @pytest.mark.skipif( @@ -98,89 +129,179 @@ def test_task_enqueue_returns_result(sentry_init, immediate_backend): not HAS_DJANGO_TASKS, reason="Django tasks are only available in Django 6.0+", ) -def test_task_enqueue_with_kwargs(sentry_init, immediate_backend, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_task_enqueue_with_kwargs( + sentry_init, + immediate_backend, + capture_events, + capture_items, + span_streaming, +): """Test that task enqueuing works correctly with keyword arguments.""" sentry_init( integrations=[DjangoIntegration()], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() - - with sentry_sdk.start_transaction(name="test_transaction"): - result = greet.enqueue(name="World", greeting="Hi") - - assert result.return_value == "Hi, World!" - - (event,) = events - queue_submit_spans = [ - span for span in event["spans"] if span["op"] == OP.QUEUE_SUBMIT_DJANGO - ] - assert len(queue_submit_spans) == 1 - assert ( - queue_submit_spans[0]["description"] - == "tests.integrations.django.test_tasks.greet" - ) + if span_streaming: + items = capture_items("span") + + with sentry_sdk.traces.start_span(name="custom parent"): + result = greet.enqueue(name="World", greeting="Hi") + + assert result.return_value == "Hi, World!" + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + + queue_submit_spans = [ + span + for span in spans + if span["attributes"].get("sentry.op") == OP.QUEUE_SUBMIT_DJANGO + ] + assert len(queue_submit_spans) == 1 + assert ( + queue_submit_spans[0]["name"] + == "tests.integrations.django.test_tasks.greet" + ) + else: + events = capture_events() + + with sentry_sdk.start_transaction(name="test_transaction"): + result = greet.enqueue(name="World", greeting="Hi") + + assert result.return_value == "Hi, World!" + + (event,) = events + queue_submit_spans = [ + span for span in event["spans"] if span["op"] == OP.QUEUE_SUBMIT_DJANGO + ] + assert len(queue_submit_spans) == 1 + assert ( + queue_submit_spans[0]["description"] + == "tests.integrations.django.test_tasks.greet" + ) @pytest.mark.skipif( not HAS_DJANGO_TASKS, reason="Django tasks are only available in Django 6.0+", ) -def test_task_error_reporting(sentry_init, immediate_backend, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_task_error_reporting( + sentry_init, + immediate_backend, + capture_events, + capture_items, + span_streaming, +): """Test that errors in tasks are correctly reported and don't break the span.""" sentry_init( integrations=[DjangoIntegration()], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() - - with sentry_sdk.start_transaction(name="test_transaction"): - result = failing_task.enqueue() - - with pytest.raises(ValueError, match="Task failed"): - _ = result.return_value - - assert len(events) == 2 - transaction_event = events[-1] - assert transaction_event["type"] == "transaction" - - queue_submit_spans = [ - span - for span in transaction_event["spans"] - if span["op"] == OP.QUEUE_SUBMIT_DJANGO - ] - assert len(queue_submit_spans) == 1 - assert ( - queue_submit_spans[0]["description"] - == "tests.integrations.django.test_tasks.failing_task" - ) + if span_streaming: + items = capture_items("span") + + with sentry_sdk.traces.start_span(name="custom parent"): + result = failing_task.enqueue() + + with pytest.raises(ValueError, match="Task failed"): + _ = result.return_value + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + + queue_submit_spans = [ + span + for span in spans + if span["attributes"].get("sentry.op") == OP.QUEUE_SUBMIT_DJANGO + ] + + assert len(queue_submit_spans) == 1 + assert ( + queue_submit_spans[0]["name"] + == "tests.integrations.django.test_tasks.failing_task" + ) + else: + events = capture_events() + + with sentry_sdk.start_transaction(name="test_transaction"): + result = failing_task.enqueue() + + with pytest.raises(ValueError, match="Task failed"): + _ = result.return_value + + assert len(events) == 2 + transaction_event = events[-1] + assert transaction_event["type"] == "transaction" + + queue_submit_spans = [ + span + for span in transaction_event["spans"] + if span["op"] == OP.QUEUE_SUBMIT_DJANGO + ] + + assert len(queue_submit_spans) == 1 + assert ( + queue_submit_spans[0]["description"] + == "tests.integrations.django.test_tasks.failing_task" + ) @pytest.mark.skipif( not HAS_DJANGO_TASKS, reason="Django tasks are only available in Django 6.0+", ) +@pytest.mark.parametrize("span_streaming", [True, False]) def test_multiple_task_enqueues_create_multiple_spans( - sentry_init, capture_events, immediate_backend + sentry_init, + capture_events, + capture_items, + immediate_backend, + span_streaming, ): """Test that enqueueing multiple tasks creates multiple spans.""" sentry_init( integrations=[DjangoIntegration()], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() + if span_streaming: + items = capture_items("span") + + with sentry_sdk.traces.start_span(name="custom parent"): + task_one.enqueue() + task_two.enqueue() + task_one.enqueue() + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + + queue_submit_spans = [ + span + for span in spans + if span["attributes"].get("sentry.op") == OP.QUEUE_SUBMIT_DJANGO + ] + assert len(queue_submit_spans) == 3 + + span_names = [span["name"] for span in queue_submit_spans] + else: + events = capture_events() + + with sentry_sdk.start_transaction(name="test_transaction"): + task_one.enqueue() + task_two.enqueue() + task_one.enqueue() - with sentry_sdk.start_transaction(name="test_transaction"): - task_one.enqueue() - task_two.enqueue() - task_one.enqueue() + (event,) = events + queue_submit_spans = [ + span for span in event["spans"] if span["op"] == OP.QUEUE_SUBMIT_DJANGO + ] + assert len(queue_submit_spans) == 3 - (event,) = events - queue_submit_spans = [ - span for span in event["spans"] if span["op"] == OP.QUEUE_SUBMIT_DJANGO - ] - assert len(queue_submit_spans) == 3 + span_names = [span["description"] for span in queue_submit_spans] - span_names = [span["description"] for span in queue_submit_spans] assert span_names.count("tests.integrations.django.test_tasks.task_one") == 2 assert span_names.count("tests.integrations.django.test_tasks.task_two") == 1 From 30188d4fb8648cda04df05b18dab2c1e24bacf6d Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 11 May 2026 09:02:02 +0200 Subject: [PATCH 2/9] typing --- sentry_sdk/integrations/django/middleware.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sentry_sdk/integrations/django/middleware.py b/sentry_sdk/integrations/django/middleware.py index d04941005e..ff79c6dd35 100644 --- a/sentry_sdk/integrations/django/middleware.py +++ b/sentry_sdk/integrations/django/middleware.py @@ -22,8 +22,10 @@ from typing import Callable from typing import Optional from typing import TypeVar + from typing import Union from sentry_sdk.tracing import Span + from sentry_sdk.traces import StreamedSpan F = TypeVar("F", bound=Callable[..., Any]) @@ -84,6 +86,7 @@ def _check_middleware_span(old_method: "Callable[..., Any]") -> "Optional[Span]" client = sentry_sdk.get_client() span_streaming = has_span_streaming_enabled(client.options) + middleware_span: "Union[Span, StreamedSpan]" if span_streaming: middleware_span = sentry_sdk.traces.start_span( name=description, From acfee8af6f454bbbafee0ff0d37b310a8a2ca939 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 11 May 2026 09:13:36 +0200 Subject: [PATCH 3/9] add middleware name const --- sentry_sdk/consts.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index d2b4cd89af..277fd86f92 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -863,6 +863,12 @@ class SPANDATA: The messaging system's name, e.g. `kafka`, `aws_sqs` """ + MIDDLEWARE_NAME = "middleware.name" + """ + The name of the middleware. + Example: "AuthenticationMiddleware" + """ + NETWORK_PEER_ADDRESS = "network.peer.address" """ Peer address of the network connection - IP address or Unix domain socket name. From 703578474b45df836cc7811c7cece0abcaea3e6f Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 11 May 2026 09:36:42 +0200 Subject: [PATCH 4/9] return type annotation --- sentry_sdk/integrations/django/middleware.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/django/middleware.py b/sentry_sdk/integrations/django/middleware.py index ff79c6dd35..091e423a7e 100644 --- a/sentry_sdk/integrations/django/middleware.py +++ b/sentry_sdk/integrations/django/middleware.py @@ -71,7 +71,9 @@ def sentry_patched_load_middleware(*args: "Any", **kwargs: "Any") -> "Any": def _wrap_middleware(middleware: "Any", middleware_name: str) -> "Any": from sentry_sdk.integrations.django import DjangoIntegration - def _check_middleware_span(old_method: "Callable[..., Any]") -> "Optional[Span]": + def _check_middleware_span( + old_method: "Callable[..., Any]", + ) -> "Optional[Union[Span, StreamedSpan]]": integration = sentry_sdk.get_client().get_integration(DjangoIntegration) if integration is None or not integration.middleware_spans: return None From 9d3ed1b277fc00e9222583232d5f28089f6a04a8 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 11 May 2026 10:26:38 +0200 Subject: [PATCH 5/9] condense span streaming checks --- sentry_sdk/integrations/django/__init__.py | 12 +++--------- sentry_sdk/integrations/django/asgi.py | 4 +--- sentry_sdk/integrations/django/caching.py | 4 +--- sentry_sdk/integrations/django/middleware.py | 4 +--- sentry_sdk/integrations/django/signals_handlers.py | 6 +++--- sentry_sdk/integrations/django/tasks.py | 4 +--- sentry_sdk/integrations/django/templates.py | 4 +--- sentry_sdk/integrations/django/views.py | 8 ++------ 8 files changed, 13 insertions(+), 33 deletions(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 5a4ea6febd..7a16b14680 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -681,9 +681,7 @@ def connect(self: "BaseDatabaseWrapper") -> None: with capture_internal_exceptions(): sentry_sdk.add_breadcrumb(message="connect", category="query") - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) if span_streaming: with sentry_sdk.traces.start_span( name="connect", @@ -709,9 +707,7 @@ def _commit(self: "BaseDatabaseWrapper") -> None: if integration is None or not integration.db_transaction_spans: return real_commit(self) - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) if span_streaming: with sentry_sdk.traces.start_span( name=SPANNAME.DB_COMMIT, @@ -737,9 +733,7 @@ def _rollback(self: "BaseDatabaseWrapper") -> None: if integration is None or not integration.db_transaction_spans: return real_rollback(self) - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) if span_streaming: with sentry_sdk.traces.start_span( name=SPANNAME.DB_ROLLBACK, diff --git a/sentry_sdk/integrations/django/asgi.py b/sentry_sdk/integrations/django/asgi.py index a9adf87f20..487c88e826 100644 --- a/sentry_sdk/integrations/django/asgi.py +++ b/sentry_sdk/integrations/django/asgi.py @@ -183,9 +183,7 @@ async def sentry_wrapped_callback( if not integration or not integration.middleware_spans: return await callback(request, *args, **kwargs) - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) if span_streaming: with sentry_sdk.traces.start_span( name=request.resolver_match.view_name, diff --git a/sentry_sdk/integrations/django/caching.py b/sentry_sdk/integrations/django/caching.py index ce1139fa16..327d0b4e37 100644 --- a/sentry_sdk/integrations/django/caching.py +++ b/sentry_sdk/integrations/django/caching.py @@ -62,9 +62,7 @@ def _instrument_call( op = OP.CACHE_PUT if is_set_operation else OP.CACHE_GET description = _get_span_description(method_name, args, kwargs) - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) if span_streaming: with sentry_sdk.traces.start_span( name=description, diff --git a/sentry_sdk/integrations/django/middleware.py b/sentry_sdk/integrations/django/middleware.py index 091e423a7e..c9c98f714e 100644 --- a/sentry_sdk/integrations/django/middleware.py +++ b/sentry_sdk/integrations/django/middleware.py @@ -85,9 +85,7 @@ def _check_middleware_span( if function_basename: description = "{}.{}".format(description, function_basename) - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) middleware_span: "Union[Span, StreamedSpan]" if span_streaming: middleware_span = sentry_sdk.traces.start_span( diff --git a/sentry_sdk/integrations/django/signals_handlers.py b/sentry_sdk/integrations/django/signals_handlers.py index c540da6062..a25a8623ef 100644 --- a/sentry_sdk/integrations/django/signals_handlers.py +++ b/sentry_sdk/integrations/django/signals_handlers.py @@ -65,9 +65,9 @@ def sentry_sync_receiver_wrapper( def wrapper(*args: "Any", **kwargs: "Any") -> "Any": signal_name = _get_receiver_name(receiver) - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - + span_streaming = has_span_streaming_enabled( + sentry_sdk.get_client().options + ) if span_streaming: with sentry_sdk.traces.start_span( name=signal_name, diff --git a/sentry_sdk/integrations/django/tasks.py b/sentry_sdk/integrations/django/tasks.py index 3e3063a288..ac3e06a078 100644 --- a/sentry_sdk/integrations/django/tasks.py +++ b/sentry_sdk/integrations/django/tasks.py @@ -33,9 +33,7 @@ def _sentry_enqueue(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": name = qualname_from_function(self.func) or "" - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) if span_streaming: with sentry_sdk.traces.start_span( name=name, diff --git a/sentry_sdk/integrations/django/templates.py b/sentry_sdk/integrations/django/templates.py index a2e6bda978..577943fb5f 100644 --- a/sentry_sdk/integrations/django/templates.py +++ b/sentry_sdk/integrations/django/templates.py @@ -66,9 +66,7 @@ def patch_templates() -> None: @property # type: ignore @ensure_integration_enabled(DjangoIntegration, real_rendered_content.fget) def rendered_content(self: "SimpleTemplateResponse") -> str: - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) if span_streaming: with sentry_sdk.traces.start_span( name=_get_template_name_description(self.template_name), diff --git a/sentry_sdk/integrations/django/views.py b/sentry_sdk/integrations/django/views.py index a13599a4be..fe45c7c09f 100644 --- a/sentry_sdk/integrations/django/views.py +++ b/sentry_sdk/integrations/django/views.py @@ -31,9 +31,7 @@ def patch_views() -> None: old_render = SimpleTemplateResponse.render def sentry_patched_render(self: "SimpleTemplateResponse") -> "Any": - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) if span_streaming: with sentry_sdk.traces.start_span( name="serialize response", @@ -100,9 +98,7 @@ def sentry_wrapped_callback(request: "Any", *args: "Any", **kwargs: "Any") -> "A if not integration or not integration.middleware_spans: return callback(request, *args, **kwargs) - client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) - + span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) if span_streaming: with sentry_sdk.traces.start_span( name=request.resolver_match.view_name, From d5c2343aab42b526a1efce74ec623537792fc705 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 11 May 2026 10:32:50 +0200 Subject: [PATCH 6/9] use code.function.name instead of signal --- sentry_sdk/integrations/django/signals_handlers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/django/signals_handlers.py b/sentry_sdk/integrations/django/signals_handlers.py index a25a8623ef..d1727cabc1 100644 --- a/sentry_sdk/integrations/django/signals_handlers.py +++ b/sentry_sdk/integrations/django/signals_handlers.py @@ -3,7 +3,7 @@ from django.dispatch import Signal import sentry_sdk -from sentry_sdk.consts import OP +from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations.django import DJANGO_VERSION from sentry_sdk.tracing_utils import has_span_streaming_enabled @@ -76,7 +76,7 @@ def wrapper(*args: "Any", **kwargs: "Any") -> "Any": "sentry.origin": DjangoIntegration.origin, }, ) as span: - span.set_attribute("signal", signal_name) + span.set_attribute(SPANDATA.CODE_FUNCTION_NAME, signal_name) return receiver(*args, **kwargs) else: with sentry_sdk.start_span( From b00b7259794f481f01794073ffc64ed819727d49 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 11 May 2026 14:10:01 +0200 Subject: [PATCH 7/9] rename record_sql_queries_supporting_streaming --- sentry_sdk/integrations/sqlalchemy.py | 4 +- sentry_sdk/tracing_utils.py | 48 ------------------- .../integrations/django/test_db_query_data.py | 20 ++++---- .../sqlalchemy/test_sqlalchemy.py | 46 +++++------------- 4 files changed, 23 insertions(+), 95 deletions(-) diff --git a/sentry_sdk/integrations/sqlalchemy.py b/sentry_sdk/integrations/sqlalchemy.py index 5001404641..fa1563ba57 100644 --- a/sentry_sdk/integrations/sqlalchemy.py +++ b/sentry_sdk/integrations/sqlalchemy.py @@ -2,7 +2,7 @@ from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable from sentry_sdk.tracing_utils import ( add_query_source, - record_sql_queries_supporting_streaming, + record_sql_queries, ) from sentry_sdk.utils import ( capture_internal_exceptions, @@ -52,7 +52,7 @@ def _before_cursor_execute( executemany: bool, *args: "Any", ) -> None: - ctx_mgr = record_sql_queries_supporting_streaming( + ctx_mgr = record_sql_queries( cursor, statement, parameters, diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 8986759885..32f8d29f4c 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -133,54 +133,6 @@ def record_sql_queries( executemany: bool, record_cursor_repr: bool = False, span_origin: str = "manual", -) -> "Generator[sentry_sdk.tracing.Span, None, None]": - # TODO: Bring back capturing of params by default - if sentry_sdk.get_client().options["_experiments"].get("record_sql_params", False): - if not params_list or params_list == [None]: - params_list = None - - if paramstyle == "pyformat": - paramstyle = "format" - else: - params_list = None - paramstyle = None - - query = _format_sql(cursor, query) - - data = {} - if params_list is not None: - data["db.params"] = params_list - if paramstyle is not None: - data["db.paramstyle"] = paramstyle - if executemany: - data["db.executemany"] = True - if record_cursor_repr and cursor is not None: - data["db.cursor"] = cursor - - with capture_internal_exceptions(): - sentry_sdk.add_breadcrumb(message=query, category="query", data=data) - - with sentry_sdk.start_span( - op=OP.DB, - name=query, - origin=span_origin, - ) as span: - for k, v in data.items(): - span.set_data(k, v) - yield span - - -# Mirrors record_sql_queries() temporarily so the Django and asyncpg integrations don't crash with span streaming enabled. -# Once both are ported, remove record_sql_queries() and rename record_sql_queries_supporting_streaming() to record_sql_queries(). -@contextlib.contextmanager -def record_sql_queries_supporting_streaming( - cursor: "Any", - query: "Any", - params_list: "Any", - paramstyle: "Optional[str]", - executemany: bool, - record_cursor_repr: bool = False, - span_origin: str = "manual", ) -> "Generator[Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan], None, None]": # TODO: Bring back capturing of params by default client = sentry_sdk.get_client() diff --git a/tests/integrations/django/test_db_query_data.py b/tests/integrations/django/test_db_query_data.py index 6ccd7f2607..6d6662f4df 100644 --- a/tests/integrations/django/test_db_query_data.py +++ b/tests/integrations/django/test_db_query_data.py @@ -20,7 +20,6 @@ from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.tracing_utils import ( record_sql_queries, - record_sql_queries_supporting_streaming, ) from tests.conftest import unpack_werkzeug_response @@ -628,16 +627,13 @@ def test_no_query_source_if_duration_too_short( class fake_record_sql_queries: # noqa: N801 def __init__(self, *args, **kwargs): - if span_streaming: - with record_sql_queries_supporting_streaming(*args, **kwargs) as span: - self.span = span + with record_sql_queries(*args, **kwargs) as span: + self.span = span + if span_streaming: self.span._start_timestamp = datetime(2024, 1, 1, microsecond=0) self.span._end_timestamp = datetime(2024, 1, 1, microsecond=99999) else: - with record_sql_queries(*args, **kwargs) as span: - self.span = span - self.span.start_timestamp = datetime(2024, 1, 1, microsecond=0) self.span.timestamp = datetime(2024, 1, 1, microsecond=99999) @@ -651,7 +647,7 @@ def __exit__(self, type, value, traceback): items = capture_items("span") with mock.patch( - "sentry_sdk.integrations.django.record_sql_queries_supporting_streaming", + "sentry_sdk.integrations.django.record_sql_queries", fake_record_sql_queries, ): _, status, _ = unpack_werkzeug_response( @@ -680,7 +676,7 @@ def __exit__(self, type, value, traceback): events = capture_events() with mock.patch( - "sentry_sdk.integrations.django.record_sql_queries_supporting_streaming", + "sentry_sdk.integrations.django.record_sql_queries", fake_record_sql_queries, ): _, status, _ = unpack_werkzeug_response( @@ -731,7 +727,7 @@ def test_query_source_if_duration_over_threshold( class fake_record_sql_queries: # noqa: N801 def __init__(self, *args, **kwargs): if span_streaming: - with record_sql_queries_supporting_streaming(*args, **kwargs) as span: + with record_sql_queries(*args, **kwargs) as span: self.span = span self.span._start_timestamp = datetime(2024, 1, 1, microsecond=0) @@ -753,7 +749,7 @@ def __exit__(self, type, value, traceback): items = capture_items("span") with mock.patch( - "sentry_sdk.integrations.django.record_sql_queries_supporting_streaming", + "sentry_sdk.integrations.django.record_sql_queries", fake_record_sql_queries, ): _, status, _ = unpack_werkzeug_response( @@ -798,7 +794,7 @@ def __exit__(self, type, value, traceback): events = capture_events() with mock.patch( - "sentry_sdk.integrations.django.record_sql_queries_supporting_streaming", + "sentry_sdk.integrations.django.record_sql_queries", fake_record_sql_queries, ): _, status, _ = unpack_werkzeug_response( diff --git a/tests/integrations/sqlalchemy/test_sqlalchemy.py b/tests/integrations/sqlalchemy/test_sqlalchemy.py index d942d5fea3..ce5a023cbc 100644 --- a/tests/integrations/sqlalchemy/test_sqlalchemy.py +++ b/tests/integrations/sqlalchemy/test_sqlalchemy.py @@ -14,7 +14,7 @@ from sentry_sdk.consts import DEFAULT_MAX_VALUE_LENGTH, SPANDATA from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration from sentry_sdk.serializer import MAX_EVENT_BYTES -from sentry_sdk.tracing_utils import record_sql_queries_supporting_streaming +from sentry_sdk.tracing_utils import record_sql_queries from sentry_sdk.utils import json_dumps @@ -932,19 +932,11 @@ class Person(Base): class fake_record_sql_queries: # noqa: N801 def __init__(self, *args, **kwargs): - with record_sql_queries_supporting_streaming( - *args, **kwargs - ) as span: + with record_sql_queries(*args, **kwargs) as span: self.span = span - if span_streaming: - self.span._start_timestamp = datetime(2024, 1, 1, microsecond=0) - self.span._end_timestamp = datetime( - 2024, 1, 1, microsecond=99999 - ) - else: - self.span.start_timestamp = datetime(2024, 1, 1, microsecond=0) - self.span.timestamp = datetime(2024, 1, 1, microsecond=99999) + self.span._start_timestamp = datetime(2024, 1, 1, microsecond=0) + self.span._end_timestamp = datetime(2024, 1, 1, microsecond=99999) def __enter__(self): return self.span @@ -953,7 +945,7 @@ def __exit__(self, type, value, traceback): pass with mock.patch( - "sentry_sdk.integrations.sqlalchemy.record_sql_queries_supporting_streaming", + "sentry_sdk.integrations.sqlalchemy.record_sql_queries", fake_record_sql_queries, ): assert session.query(Person).first() == bob @@ -998,19 +990,11 @@ class Person(Base): class fake_record_sql_queries: # noqa: N801 def __init__(self, *args, **kwargs): - with record_sql_queries_supporting_streaming( - *args, **kwargs - ) as span: + with record_sql_queries(*args, **kwargs) as span: self.span = span - if span_streaming: - self.span._start_timestamp = datetime(2024, 1, 1, microsecond=0) - self.span._end_timestamp = datetime( - 2024, 1, 1, microsecond=99999 - ) - else: - self.span.start_timestamp = datetime(2024, 1, 1, microsecond=0) - self.span.timestamp = datetime(2024, 1, 1, microsecond=99999) + self.span.start_timestamp = datetime(2024, 1, 1, microsecond=0) + self.span.timestamp = datetime(2024, 1, 1, microsecond=99999) def __enter__(self): return self.span @@ -1019,7 +1003,7 @@ def __exit__(self, type, value, traceback): pass with mock.patch( - "sentry_sdk.integrations.sqlalchemy.record_sql_queries_supporting_streaming", + "sentry_sdk.integrations.sqlalchemy.record_sql_queries", fake_record_sql_queries, ): assert session.query(Person).first() == bob @@ -1080,9 +1064,7 @@ class Person(Base): class fake_record_sql_queries: # noqa: N801 def __init__(self, *args, **kwargs): - with record_sql_queries_supporting_streaming( - *args, **kwargs - ) as span: + with record_sql_queries(*args, **kwargs) as span: self.span = span self.span._start_timestamp = datetime(2024, 1, 1, microsecond=0) @@ -1095,7 +1077,7 @@ def __exit__(self, type, value, traceback): pass with mock.patch( - "sentry_sdk.integrations.sqlalchemy.record_sql_queries_supporting_streaming", + "sentry_sdk.integrations.sqlalchemy.record_sql_queries", fake_record_sql_queries, ): assert session.query(Person).first() == bob @@ -1157,9 +1139,7 @@ class Person(Base): class fake_record_sql_queries: # noqa: N801 def __init__(self, *args, **kwargs): - with record_sql_queries_supporting_streaming( - *args, **kwargs - ) as span: + with record_sql_queries(*args, **kwargs) as span: self.span = span self.span.start_timestamp = datetime(2024, 1, 1, microsecond=0) @@ -1172,7 +1152,7 @@ def __exit__(self, type, value, traceback): pass with mock.patch( - "sentry_sdk.integrations.sqlalchemy.record_sql_queries_supporting_streaming", + "sentry_sdk.integrations.sqlalchemy.record_sql_queries", fake_record_sql_queries, ): assert session.query(Person).first() == bob From 23846df4f2136ae15b8a303eee054f28866876e2 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 11 May 2026 14:13:01 +0200 Subject: [PATCH 8/9] more renaming --- sentry_sdk/integrations/asyncpg.py | 6 +++--- tests/integrations/asyncpg/test_asyncpg.py | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/sentry_sdk/integrations/asyncpg.py b/sentry_sdk/integrations/asyncpg.py index 9b37119564..32eb80f703 100644 --- a/sentry_sdk/integrations/asyncpg.py +++ b/sentry_sdk/integrations/asyncpg.py @@ -12,7 +12,7 @@ from sentry_sdk.tracing_utils import ( add_query_source, has_span_streaming_enabled, - record_sql_queries_supporting_streaming, + record_sql_queries, ) from sentry_sdk.utils import ( capture_internal_exceptions, @@ -80,7 +80,7 @@ async def _inner(*args: "Any", **kwargs: "Any") -> "T": return await f(*args, **kwargs) query = _normalize_query(args[1]) - with record_sql_queries_supporting_streaming( + with record_sql_queries( cursor=None, query=query, params_list=None, @@ -121,7 +121,7 @@ def _record( param_style = "pyformat" if params_list else None query = _normalize_query(query) - with record_sql_queries_supporting_streaming( + with record_sql_queries( cursor=cursor, query=query, params_list=params_list, diff --git a/tests/integrations/asyncpg/test_asyncpg.py b/tests/integrations/asyncpg/test_asyncpg.py index 35195e757b..0189d5e4c7 100644 --- a/tests/integrations/asyncpg/test_asyncpg.py +++ b/tests/integrations/asyncpg/test_asyncpg.py @@ -23,7 +23,7 @@ from sentry_sdk import capture_message, start_transaction from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations.asyncpg import AsyncPGIntegration -from sentry_sdk.tracing_utils import record_sql_queries_supporting_streaming +from sentry_sdk.tracing_utils import record_sql_queries from tests.conftest import ApproxDict PG_HOST = os.getenv("SENTRY_PYTHON_TEST_POSTGRES_HOST", "localhost") @@ -799,7 +799,7 @@ async def test_no_query_source_if_duration_too_short( @contextmanager def fake_record_sql_queries_streaming(*args, **kwargs): - with record_sql_queries_supporting_streaming(*args, **kwargs) as span: + with record_sql_queries(*args, **kwargs) as span: pass span._start_timestamp = datetime.datetime(2024, 1, 1, microsecond=0) if span_streaming: @@ -812,7 +812,7 @@ def fake_record_sql_queries_streaming(*args, **kwargs): conn: Connection = await connect(PG_CONNECTION_URI) with mock.patch( - "sentry_sdk.integrations.asyncpg.record_sql_queries_supporting_streaming", + "sentry_sdk.integrations.asyncpg.record_sql_queries", fake_record_sql_queries_streaming, ): await conn.execute( @@ -842,14 +842,14 @@ def fake_record_sql_queries_streaming(*args, **kwargs): @contextmanager def fake_record_sql_queries(*args, **kwargs): - with record_sql_queries_supporting_streaming(*args, **kwargs) as span: + with record_sql_queries(*args, **kwargs) as span: pass span.start_timestamp = datetime.datetime(2024, 1, 1, microsecond=0) span.timestamp = datetime.datetime(2024, 1, 1, microsecond=99999) yield span with mock.patch( - "sentry_sdk.integrations.asyncpg.record_sql_queries_supporting_streaming", + "sentry_sdk.integrations.asyncpg.record_sql_queries", fake_record_sql_queries, ): await conn.execute( @@ -886,14 +886,14 @@ async def test_query_source_if_duration_over_threshold(sentry_init, capture_even @contextmanager def fake_record_sql_queries(*args, **kwargs): - with record_sql_queries_supporting_streaming(*args, **kwargs) as span: + with record_sql_queries(*args, **kwargs) as span: pass span.start_timestamp = datetime.datetime(2024, 1, 1, microsecond=0) span.timestamp = datetime.datetime(2024, 1, 1, microsecond=100001) yield span with mock.patch( - "sentry_sdk.integrations.asyncpg.record_sql_queries_supporting_streaming", + "sentry_sdk.integrations.asyncpg.record_sql_queries", fake_record_sql_queries, ): await conn.execute( From 2d4cb1ef8a957c9f5317a1595b0cce6abb83d512 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 11 May 2026 14:14:56 +0200 Subject: [PATCH 9/9] remaining reference --- sentry_sdk/integrations/django/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 7a16b14680..3e786bc98b 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -12,7 +12,7 @@ from sentry_sdk.traces import StreamedSpan from sentry_sdk.tracing_utils import ( add_query_source, - record_sql_queries_supporting_streaming, + record_sql_queries, has_span_streaming_enabled, ) from sentry_sdk.utils import ( @@ -639,7 +639,7 @@ def install_sql_hook() -> None: def execute( self: "CursorWrapper", sql: "Any", params: "Optional[Any]" = None ) -> "Any": - with record_sql_queries_supporting_streaming( + with record_sql_queries( cursor=self.cursor, query=sql, params_list=params, @@ -659,7 +659,7 @@ def execute( def executemany( self: "CursorWrapper", sql: "Any", param_list: "List[Any]" ) -> "Any": - with record_sql_queries_supporting_streaming( + with record_sql_queries( cursor=self.cursor, query=sql, params_list=param_list,