Uh oh!
There was an error while loading. Please reload this page.
feat(django): Support span streaming - #6248
Conversation
Codecov Results 📊✅ 282 passed | Total: 282 | Pass Rate: 100% | Execution Time: 43.82s All tests are passing successfully. ❌ Patch coverage is 0.95%. Project has 14983 uncovered lines. Files with missing lines (11)
Generated by Codecov Action |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Span thread.id assertion will crash: UnwrappedItem isn't subscriptable and span payload has no contexts key (tests/integrations/django/asgi/test_asgi.py:231)
In tests/integrations/django/asgi/test_asgi.py (around L231–L235) the span_streaming branch does:
spans= [itemforiteminitemsifitem.type=="span"]
forspaninspans:
trace_context=span["contexts"]["trace"]
assertstr(data["active"]) ==trace_context["attributes"]["thread.id"]Two problems:
capture_items(tests/conftest.py ~L340) returnsUnwrappedItemdataclass instances (fieldstype,payload) with no__getitem__, sospan["contexts"]raisesTypeError: 'UnwrappedItem' object is not subscriptable. Every other call site in the repo correctly usesitem.payload for item in items if item.type == "span".- Even after switching to
span.payload, the unwrapped span payload follows the OTel v2 span schema (trace_id,span_id,name,attributes, ... — seetests/tracing/test_span_batcher.py~L407–420). There is nocontexts.traceenvelope around it, sospan.payload["contexts"]would raiseKeyError.
As long as at least one span is emitted for /sync/thread_ids / /async/thread_ids (which is the point of the test), the assertion crashes before it can validate the streaming thread-id behavior, defeating the purpose of the new test parametrization.
Verification
Inspected tests/conftest.py ~L340 (UnwrappedItem is a plain @dataclass with type/payload, no __getitem__) and tests/conftest.py ~L367–L371 (for span items it stores the per-item dict from item.payload.json["items"] directly into payload, not under a contexts key). Confirmed via tests/tracing/test_span_batcher.py ~L407 that streaming span payloads are flat OTel-style (trace_id, span_id, attributes, ...). All other span-iterating tests in the repo (anthropic, sqlalchemy, fastmcp, openai, tracing) use item.payload for item in items if item.type == "span" and access attributes directly.
Test loop may pass vacuously when no spans are captured (tests/integrations/django/asgi/test_asgi.py:233)
There is no assertion that spans is non-empty before the for span in spans loop, so if the streaming path emits no spans the assertions inside the loop are never executed and the test passes silently — unlike the else branch which has assert len(transactions) == 1.
Verification
In the else branch (lines 248, 263) assert len(profiles) == 1 and assert len(transactions) == 1 guard both loops. The span_streaming=True branch has no equivalent guard, so an empty spans list produces a vacuously-passing test.
test_sql_queries and test_sql_dict_query_params never configure SDK for streaming mode (tests/integrations/django/test_basic.py:700)
Both tests parametrize span_streaming but their sentry_init call only sets _experiments={"record_sql_params": True} — the "trace_lifecycle": "stream" key is never set, so the span_streaming=True variant runs with the default (static) lifecycle and isn't testing streaming at all.
Verification
In test_queryset_repr (line ~553) the sentry_init correctly uses _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}. In test_sql_queries (line ~697) the sentry_init uses only _experiments={"record_sql_params": True} with no trace_lifecycle key regardless of the span_streaming value. Same pattern in test_sql_dict_query_params (~line 750). The span_streaming=True branch uses capture_items("event") instead of capture_events(), but because the SDK is running in static mode, behavior is identical to the False branch and actual streaming code paths are never exercised.
Identified by Warden code-review
There was a problem hiding this comment.
asyncpg streaming path sets deprecated db.system instead of db.system.name
In sentry_sdk/integrations/asyncpg.py, _set_db_data and _wrap_connect_addr both use SPANDATA.DB_SYSTEM ("db.system") in the streaming path; they should use SPANDATA.DB_SYSTEM_NAME ("db.system.name") like the sqlalchemy integration does.
Verification
asyncpg.pyline 266:set_value(SPANDATA.DB_SYSTEM, "postgresql")uses the same deprecated key for both streaming and non-streaming paths.asyncpg.pyline 213:SPANDATA.DB_SYSTEM: "postgresql"is placed directly in the streamingspan_attributesdict.sqlalchemy.py_set_db_datacorrectly branches:span.set_attribute(SPANDATA.DB_SYSTEM_NAME, ...)for StreamedSpan vsspan.set_data(SPANDATA.DB_SYSTEM, ...)for Span.- The PR description explicitly states to use
db.system.nameinstead ofdb.systemin the streaming path. - Test
test_asyncpg.pyline 1401 confirms the wrong behaviour by assertingbind_exec_span["attributes"]["db.system"] == "postgresql"in the streaming case — the tests are aligned to the bug, not the spec.
asyncpg streaming path sets deprecated db.name instead of db.namespace
In sentry_sdk/integrations/asyncpg.py, _set_db_data (line 279) and _wrap_connect_addr (line 216) both use SPANDATA.DB_NAME ("db.name") in the streaming path; they should use SPANDATA.DB_NAMESPACE ("db.namespace") like the sqlalchemy integration does.
Verification
asyncpg.pyline 279:set_value(SPANDATA.DB_NAME, database)is called with the same deprecated key for both streaming and non-streaming paths via the unifiedset_valuealias.asyncpg.pyline 216:SPANDATA.DB_NAME: databaseis placed directly in the streamingspan_attributesdict inside_wrap_connect_addr.sqlalchemy.py_set_db_datacorrectly usesspan.set_attribute(SPANDATA.DB_NAMESPACE, db_name)for StreamedSpan andspan.set_data(SPANDATA.DB_NAME, ...)for Span.- The PR description explicitly states to use
db.namespaceinstead ofdb.namein the streaming path. - Test
test_asyncpg.pyline 1406 assertsbind_exec_span["attributes"]["db.name"] == PG_NAMEin the streaming path, confirming the tests encode the wrong attribute name. consts.pymarksDB_NAME = "db.name"as deprecated with a note to useDB_NAMESPACEinstead.
Identified by Warden find-bugs
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
asyncpg streaming connect span sets deprecated db.system/db.name instead of db.system.name/db.namespace
The asyncpg streaming connect span (and _set_db_data) uses SPANDATA.DB_SYSTEM (db.system) and SPANDATA.DB_NAME (db.name) — both marked deprecated in favor of DB_SYSTEM_NAME (db.system.name) and DB_NAMESPACE (db.namespace) — while the Django integration correctly uses the new names in the same PR.
Verification
In sentry_sdk/integrations/asyncpg.py the streaming connect path (lines ~213–216) builds span_attributes with keys SPANDATA.DB_SYSTEM and SPANDATA.DB_NAME. The _set_db_data helper (lines ~266, ~279) also uses the same deprecated constants when invoked with a StreamedSpan. By contrast, sentry_sdk/integrations/django/__init__.py's _set_db_data correctly branches on isinstance(span, StreamedSpan) and uses SPANDATA.DB_SYSTEM_NAME / SPANDATA.DB_NAMESPACE for streaming spans. The test_asyncpg.py streaming tests only assert on breadcrumbs (CRUMBS_CONNECT contains db.system and db.name) and never check the span attributes, so this inconsistency is not caught by the test suite.
Identified by Warden find-bugs
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 285de05. Configure here.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
asyncpg streaming path uses deprecated db.system and db.name attributes instead of db.system.name and db.namespace
In asyncpg.py_set_db_data, set_value is bound to span.set_attribute for StreamedSpan but still uses SPANDATA.DB_SYSTEM (db.system) and SPANDATA.DB_NAME (db.name) — the deprecated keys — instead of SPANDATA.DB_SYSTEM_NAME and SPANDATA.DB_NAMESPACE required by the streaming path. The django and sqlalchemy integrations in this same PR correctly use the new keys.
Verification
Checked sentry_sdk/integrations/asyncpg.py lines 264-279 (_set_db_data): set_value = span.set_attribute if isinstance(span, StreamedSpan) else span.set_data, then set_value(SPANDATA.DB_SYSTEM, ...) (line 266) and set_value(SPANDATA.DB_NAME, ...) (line 279) — both using the deprecated key names for both streaming and non-streaming paths. SPANDATA.DB_SYSTEM = 'db.system' (deprecated, per consts.py:511-514) and SPANDATA.DB_SYSTEM_NAME = 'db.system.name' (new, per consts.py:521). Same for DB_NAME vs DB_NAMESPACE. By contrast, sqlalchemy.py_set_db_data (lines ~144-171) and django/__init__.py_set_db_data (lines 776-820) both branch on isinstance(span, StreamedSpan) and use DB_SYSTEM_NAME/DB_NAMESPACE for the streaming path. No grep match for DB_SYSTEM_NAME or DB_NAMESPACE in asyncpg.py confirms neither is used.
Identified by Warden find-bugs
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
record_sql_queries silently drops SQL debug data (db.params, db.executemany) for streaming spans
In tracing_utils.py, the data dict (containing db.params, db.paramstyle, db.executemany, db.cursor) is built then applied to the span via span.set_data() in non-streaming mode, but the streaming path yields the span without setting any of these attributes, silently losing the data even when record_sql_params is enabled.
Verification
Checked sentry_sdk/tracing_utils.py lines 141-171. The data dict is unconditionally populated (conditioned on params_list, paramstyle, executemany, record_cursor_repr). In the else branch (non-streaming), for k, v in data.items(): span.set_data(k, v) correctly applies them. In the if has_span_streaming_enabled(...) branch (streaming), the with sentry_sdk.traces.start_span(...) block immediately yields the span with no equivalent attribute-setting loop. Callers like Django's execute() and asyncpg's _record() pass params_list and executemany=True/record_cursor_repr=True expressly to have them recorded, but in streaming mode none of this data reaches the span.
Identified by Warden find-bugs
sentrivana
left a comment
There was a problem hiding this comment.
Looks good to me, including the data that we're dropping in span first (like template context) -- we can re-add if there's demand for it.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Drop `django.function_name` and `context` attributes in streaming lifecycle mode. Replace deprecated `db.operation`, `db.system` and `db.name` attributes with `db.operation.name`, `db.system.name` and `db.namespace` attributes in the streaming lifecycle mode. Use `middleware.name` and `code.function.name` instead of `django.middleware_name` and `signal` in the streaming mode.

Description
There are 3 event processors in the integration. None of these need to be ported:
process_django_templates()exits early if the event is not an exceptionasgi_request_event_processor()adds request info (out of scope for span first).wsgi_request_event_processor()adds request info (out of scope for span first).In the streaming path, use
db.namespaceinstead ofdb.namedb.system.nameinstead ofdb.systemdb.operation.nameinstead ofdb.operationmiddleware.nameinstead ofdjango.middleware_namecode.function.nameinstead ofsignalcode.filepathinstead ofcode.file.pathcode.line.numberinstead ofcode.linenoDropped attributes:
django.function_namecontextAdapting Tests
sedcommands used for converting transaction context managers:sed commands used for converting specific attributes:
sedcommands used for converting event capture:sedcommands used for convertingop:sedcommands used for converting origin:sedcommands used for convertingdescription:sedcommands used for convertingdatatoattributes:sedcommands for converting trace id:sedcommands used for converting timestamps:other test changes:
Issues
Closes#6015
Reminders
tox -e linters.feat:,fix:,ref:,meta:)