Skip to content

feat(django): Support span streaming#6248

Draft
alexander-alderman-webb wants to merge 14 commits into
masterfrom
webb/django/span-first
Draft

feat(django): Support span streaming#6248
alexander-alderman-webb wants to merge 14 commits into
masterfrom
webb/django/span-first

Conversation

@alexander-alderman-webb
Copy link
Copy Markdown
Contributor

@alexander-alderman-webb alexander-alderman-webb commented May 11, 2026

Description

In the streaming path, use

  • db.namespace instead of db.name
  • db.system.name instead of db.system
  • db.operation.name instead of db.operation
  • middleware.name instead of django.middleware_name
  • code.function.name instead of signal
  • code.filepath instead of code.file.path
  • code.line.number instead of code.lineno

Dropped attributes:

  • django.function_name
  • context

Adapting Tests

sed commands used for converting transaction context managers:

  • sed -i '' 's/with sentry_sdk.start_transaction(name="test_transaction"):/with sentry_sdk.traces.start_span(name="custom parent"):/g'
  • sed -i '' 's/with start_transaction(name="test_transaction"):/with sentry_sdk.traces.start_span(name="custom parent"):/g'

sed commands used for converting specific attributes:

  • sed -i '' 's/["request"]["method"]/["attributes"][SPANDATA.HTTP_REQUEST_METHOD]/g'
  • sed -i '' 's/["active_thread_id"]/["attributes"]["thread.id"]/g'
  • sed -i '' 's/CODE_LINENO/CODE_LINE_NUMBER/g'
  • sed -i '' 's/CODE_FILEPATH/CODE_FILE_PATH/g'
  • sed -i '' 's/SPANDATA.DB_OPERATION/SPANDATA.DB_OPERATION_NAME/g'
  • sed -i '' 's/SPANDATA.DB_SYSTEM/SPANDATA.DB_SYSTEM_NAME/g'
  • sed -i '' 's/SPANDATA.DB_NAME/SPANDATA.DB_NAMESPACE/g'

sed commands used for converting event capture:

  • sed -i '' 's/capture_events,/capture_items,/g'
  • sed -i '' 's/capture_envelopes,/capture_items,/g'
  • sed -i '' 's/events = capture_events()/items = capture_items("event", "transaction", "span")/g'
  • sed -i '' 's/envelopes = capture_envelopes()/items = capture_items("event", "transaction", "span")/g'
  • sed -i '' '/event = envelope.get_event()/d'
  • sed -i '' 's/event["spans"]/spans/g'
  • sed -i '' 's/transaction["spans"]/spans/g'
  • sed -i '' 's/(msg_event, error_event, transaction_event) = events/(msg_event, error_event, ) = (item.payload for item in items if item.type == "event")/g'
  • sed -i '' 's/(event,) = events/(event, ) = (item.payload for item in items if item.type == "event")/g'
  • sed -i '' 's/(transaction,) = events/spans = [item.payload for item in items if item.type == "span"]/g'
  • sed -i '' 's/message, transaction = events/message, = (item.payload for item in items if item.type == "span")/g'
  • sed -i '' 's/error_event, transaction_event = events/error_event, = (item.payload for item in items if item.type == "event")/g'
  • sed -i '' 's/transaction = events[0]/spans = [item.payload for item in items if item.type == "span"]/g'

sed commands used for converting op:

  • sed -i '' 's/["op"]/["attributes"]["sentry.op"]/g'
  • sed -i '' 's/.get("op")/["attributes"].get("sentry.op")/g'
  • sed -i '' 's/- op/- sentry.op/g'

sed commands used for converting origin:

  • sed -i '' 's/["contexts"]["trace"]["origin"]/["attributes"]["sentry.origin"]/g'
  • sed -i '' 's/["origin"]/["attributes"]["sentry.origin"]/g'

sed commands used for converting description:

  • sed -i '' 's/description/name/g'

sed commands used for converting data to attributes:

  • sed -i '' 's/["data"]/["attributes"]/g'
  • sed -i '' 's/data = span.get("data", {})/attributes = span.get("attributes", {})/g'
  • sed -i '' 's/in data/in attributes/g'
  • sed -i '' 's/data.get/attributes.get/g'

sed commands for converting trace id:

  • sed -i '' 's/["contexts"]["trace"]["trace_id"]/["trace_id"]/g'

sed commands used for converting timestamps:

  • sed -i '' 's/span.timestamp/span._end_timestamp/g'
  • sed -i '' 's/span.start_timestamp/span._start_timestamp/g'

other test changes:

  • sed -i '' '/assert transaction_event["type"] == "transaction"/d'
  • sed -i '' '/assert len(transactions) == 1/d'

Issues

Closes #6015

Reminders


@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 11, 2026

Codecov Results 📊

88 passed | Total: 88 | Pass Rate: 100% | Execution Time: 19.65s

All tests are passing successfully.

❌ Patch coverage is 2.19%. Project has 16093 uncovered lines.

Files with missing lines (13)
File Patch % Lines
tracing_utils.py 41.43% ⚠️ 451 Missing and 57 partials
__init__.py 5.01% ⚠️ 398 Missing
wsgi.py 17.74% ⚠️ 153 Missing
caching.py 0.00% ⚠️ 143 Missing
asyncpg.py 9.87% ⚠️ 137 Missing
asgi.py 0.00% ⚠️ 114 Missing
templates.py 0.00% ⚠️ 110 Missing
middleware.py 0.00% ⚠️ 95 Missing
sqlalchemy.py 10.00% ⚠️ 90 Missing
views.py 0.00% ⚠️ 59 Missing
signals_handlers.py 0.00% ⚠️ 50 Missing
tasks.py 0.00% ⚠️ 28 Missing
consts.py 99.49% ⚠️ 2 Missing

Generated by Codecov Action

Comment thread sentry_sdk/integrations/django/middleware.py
Comment thread tests/integrations/django/test_db_transactions.py
Comment thread tests/integrations/django/test_db_transactions.py
Comment thread tests/integrations/django/test_basic.py
Comment thread tests/integrations/django/test_cache_module.py Outdated
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()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

add_query_source called after StreamedSpan is already captured in Django execute/executemany

In django/__init__.py execute (line 648) and executemany (line 669), add_query_source(span) is called outside the with record_sql_queries(...) block. For StreamedSpan, _end()_capture_span() fires when the with block exits, so those source-code attributes (code.file.path, code.line.number, code.function) are set on an already-captured span and silently lost. The asyncpg integration correctly calls add_query_source inside the with block only when isinstance(span, StreamedSpan).

Verification

Traced: StreamedSpan.__exit___end()self._scope._capture_span(self) (traces.py ~412) serialises and queues the span. set_attribute after this point mutates the local dict but the span is already sent. add_query_source checks isinstance(span, Span): if span.timestamp is None: return but skips that guard for StreamedSpan, so it proceeds and calls span.set_attribute(...) on the finished span. asyncpg (_wrap_execute, _wrap_connection_method, _wrap_cursor_method) avoids this by conditionally calling add_query_source inside the with block when isinstance(span, StreamedSpan). Django execute/executemany lack this conditional, so query source attributes are always dropped in streaming mode.

Identified by Warden find-bugs · YUR-B3G

Comment on lines +2362 to +2363

assert spans[4]["attributes"][SPANDATA.HTTP_REQUEST_METHOD] == "OPTIONS"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Contradictory hardcoded span indices in test_transaction_http_method_custom streaming path

The assertions spans[4][...] == "OPTIONS" and spans[7][...] == "HEAD" are mutually inconsistent: the first implies OPTIONS produces 5 spans (root at index 4), while the second implies OPTIONS produces only 3 spans (root at index 7 = 5+2). Both requests hit the same /nomessage view with the same integration config (no middleware/cache/signal spans), so they must produce equal span counts — making at least one of these index assertions check the wrong span or raise IndexError.

Verification

GET is excluded from capture (only OPTIONS/HEAD are captured), so spans contains only spans from the OPTIONS and HEAD requests. Both go to the same nomessage view (trivially returns HttpResponse). DjangoIntegration() uses middleware_spans=False (default). If each request produces N spans (root last), OPTIONS root is at index N-1 and HEAD root is at index 2N-1. Solving: N-1=4 gives N=5; 2N-1=7 gives N=4. These are contradictory — N cannot simultaneously be 4 and 5. At least one assertion must be checking the wrong span.

Identified by Warden find-bugs · RT7-BXG

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate django to span first

1 participant