Skip to content

Commit e882029

Browse files
authored
feat(fastapi): Support span streaming in active thread tracking (#6118)
Support span-first functionality in the FastAPI integration's active thread tracking, while continuing to support the legacy transaction-based path. When the span streaming experiment (`trace_lifecycle: "stream"`) is enabled, `current_scope.transaction` is `None` and the scope instead holds a `StreamedSpan`. The existing profiler hook in `patch_get_request_handler` only called `current_scope.transaction.update_active_thread()`, so under streaming the profiler's `thread.id` was never attached to the segment. This PR detects the `StreamedSpan` case and calls `_update_active_thread()` on its segment; the legacy transaction path is preserved unchanged. Tests are parametrized across streaming and static modes where relevant, and a dedicated test asserts that the active thread id ends up on the segment's attributes when streaming is enabled. Refs PY-2322 Fixes #6020
1 parent 21f78df commit e882029

2 files changed

Lines changed: 69 additions & 9 deletions

File tree

sentry_sdk/integrations/fastapi.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import sentry_sdk
66
from sentry_sdk.integrations import DidNotEnable
77
from sentry_sdk.scope import should_send_default_pii
8+
from sentry_sdk.traces import NoOpStreamedSpan, StreamedSpan
89
from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource
910
from sentry_sdk.utils import transaction_from_function
1011

@@ -80,7 +81,14 @@ def _sentry_get_request_handler(*args: "Any", **kwargs: "Any") -> "Any":
8081
@wraps(old_call)
8182
def _sentry_call(*args: "Any", **kwargs: "Any") -> "Any":
8283
current_scope = sentry_sdk.get_current_scope()
83-
if current_scope.transaction is not None:
84+
current_span = current_scope.span
85+
86+
if isinstance(current_span, StreamedSpan) and not isinstance(
87+
current_span, NoOpStreamedSpan
88+
):
89+
segment = current_span._segment
90+
segment._update_active_thread()
91+
elif current_scope.transaction is not None:
8492
current_scope.transaction.update_active_thread()
8593

8694
sentry_scope = sentry_sdk.get_isolation_scope()

tests/integrations/fastapi/test_fastapi.py

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -220,11 +220,43 @@ def test_active_thread_id(sentry_init, capture_envelopes, teardown_profiling, en
220220
assert str(data["active"]) == trace_context["data"]["thread.id"]
221221

222222

223+
@pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"])
224+
def test_active_thread_id_span_streaming(sentry_init, capture_items, endpoint):
225+
sentry_init(
226+
auto_enabling_integrations=False, # Ensure httpx is not auto-enabled; its legacy start_span interferes with streaming mode
227+
integrations=[StarletteIntegration(), FastApiIntegration()],
228+
traces_sample_rate=1.0,
229+
_experiments={"trace_lifecycle": "stream"},
230+
)
231+
app = fastapi_app_factory()
232+
233+
items = capture_items("span")
234+
235+
client = TestClient(app)
236+
response = client.get(endpoint)
237+
assert response.status_code == 200
238+
239+
data = json.loads(response.content)
240+
241+
sentry_sdk.flush()
242+
243+
segments = [item.payload for item in items if item.payload.get("is_segment")]
244+
assert len(segments) == 1
245+
assert str(data["active"]) == segments[0]["attributes"]["thread.id"]
246+
247+
248+
@pytest.mark.parametrize("span_streaming", [True, False])
223249
@pytest.mark.asyncio
224-
async def test_original_request_not_scrubbed(sentry_init, capture_events):
250+
async def test_original_request_not_scrubbed(
251+
sentry_init, capture_events, span_streaming
252+
):
225253
sentry_init(
254+
auto_enabling_integrations=False, # Ensure httpx is not auto-enabled; its legacy start_span interferes with streaming mode
226255
integrations=[StarletteIntegration(), FastApiIntegration()],
227256
traces_sample_rate=1.0,
257+
_experiments={
258+
"trace_lifecycle": "stream" if span_streaming else "static",
259+
},
228260
)
229261

230262
app = FastAPI()
@@ -344,6 +376,7 @@ def test_response_status_code_not_found_in_transaction_context(
344376
assert transaction["contexts"]["response"]["status_code"] == 404
345377

346378

379+
@pytest.mark.parametrize("span_streaming", [True, False])
347380
@pytest.mark.parametrize(
348381
"request_url,transaction_style,expected_transaction_name,expected_transaction_source",
349382
[
@@ -368,6 +401,8 @@ def test_transaction_name(
368401
expected_transaction_name,
369402
expected_transaction_source,
370403
capture_envelopes,
404+
capture_items,
405+
span_streaming,
371406
):
372407
"""
373408
Tests that the transaction name is something meaningful.
@@ -379,22 +414,39 @@ def test_transaction_name(
379414
FastApiIntegration(transaction_style=transaction_style),
380415
],
381416
traces_sample_rate=1.0,
417+
_experiments={
418+
"trace_lifecycle": "stream" if span_streaming else "static",
419+
},
382420
)
383421

384-
envelopes = capture_envelopes()
422+
if span_streaming:
423+
items = capture_items("span")
424+
else:
425+
envelopes = capture_envelopes()
385426

386427
app = fastapi_app_factory()
387428

388429
client = TestClient(app)
389430
client.get(request_url)
390431

391-
(_, transaction_envelope) = envelopes
392-
transaction_event = transaction_envelope.get_transaction_event()
432+
if span_streaming:
433+
sentry_sdk.flush()
434+
segments = [item.payload for item in items if item.payload.get("is_segment")]
435+
assert len(segments) == 1
436+
segment = segments[0]
437+
assert segment["name"] == expected_transaction_name
438+
assert (
439+
segment["attributes"]["sentry.span.source"] == expected_transaction_source
440+
)
441+
else:
442+
(_, transaction_envelope) = envelopes
443+
transaction_event = transaction_envelope.get_transaction_event()
393444

394-
assert transaction_event["transaction"] == expected_transaction_name
395-
assert (
396-
transaction_event["transaction_info"]["source"] == expected_transaction_source
397-
)
445+
assert transaction_event["transaction"] == expected_transaction_name
446+
assert (
447+
transaction_event["transaction_info"]["source"]
448+
== expected_transaction_source
449+
)
398450

399451

400452
def test_route_endpoint_equal_dependant_call(sentry_init):

0 commit comments

Comments
 (0)