Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions sentry_sdk/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -1298,11 +1298,10 @@
# active span, this is a segment
if parent_span is None:
propagation_context = self.get_active_propagation_context()

if is_ignored_span(name, attributes):
return NoOpStreamedSpan(
scope=self,
unsampled_reason="ignored",

Check warning on line 1304 in sentry_sdk/scope.py

View check run for this annotation

@sentry/warden / warden: code-review

Ignored root-level span still generates a random `trace_id` instead of inheriting from propagation context

The `NoOpStreamedSpan` returned for an ignored root span (lines 1301–1304) is missing `trace_id=propagation_context.trace_id`, so it will generate a random trace ID instead of using the one from the propagation context — the same bug this PR fixes for the unsampled case.
)

sampled, sample_rate, sample_rand, outcome = _make_sampling_decision(
Expand All @@ -1314,12 +1313,13 @@
if sample_rate is not None:
self._update_sample_rate(sample_rate)

if sampled is False:
return NoOpStreamedSpan(
scope=self,
trace_id=propagation_context.trace_id,
unsampled_reason=outcome,
)

Check warning on line 1322 in sentry_sdk/scope.py

View check run for this annotation

@sentry/warden / warden: find-bugs

Ignored root span `NoOpStreamedSpan` not given `propagation_context.trace_id`

In `Scope._start_streamed_span`, when a root span (no parent) matches an `ignore_spans` rule, the `is_ignored_span` branch creates `NoOpStreamedSpan(scope=self, unsampled_reason="ignored")` without passing `trace_id`. Because `NoOpStreamedSpan.trace_id` lazily generates a random `uuid.uuid4().hex` when `_trace_id` is falsy, this ignored root span reports a random trace ID unrelated to the active propagation context, breaking trace continuity in the emitted traceparent. The structurally identical `sampled is False` branch just below correctly passes `trace_id=propagation_context.trace_id`, and both child-span branches pass `trace_id=parent_span.trace_id`, so only this ignored root branch is inconsistent. Fix by passing `trace_id=propagation_context.trace_id` (already fetched on the preceding line).
return StreamedSpan(
name=name,
attributes=attributes,
Expand All @@ -1338,11 +1338,15 @@
with new_scope():
if is_ignored_span(name, attributes):
return NoOpStreamedSpan(
trace_id=parent_span.trace_id,
unsampled_reason="ignored",
)

if isinstance(parent_span, NoOpStreamedSpan):
return NoOpStreamedSpan(unsampled_reason=parent_span._unsampled_reason)
return NoOpStreamedSpan(
trace_id=parent_span.trace_id,
unsampled_reason=parent_span._unsampled_reason,
)

return StreamedSpan(
name=name,
Expand Down
16 changes: 7 additions & 9 deletions sentry_sdk/traces.py
Original file line number Diff line number Diff line change
Expand Up @@ -610,18 +610,24 @@

class NoOpStreamedSpan(StreamedSpan):
__slots__ = (
"_trace_id",
"_span_id",
"_finished",
"_unsampled_reason",
)

def __init__(
self,
trace_id: "Optional[str]" = None,

Check warning on line 621 in sentry_sdk/traces.py

View check run for this annotation

@sentry/warden / warden: find-bugs

[QHD-LYK] Ignored root span `NoOpStreamedSpan` not given `propagation_context.trace_id` (additional location)

In `Scope._start_streamed_span`, when a root span (no parent) matches an `ignore_spans` rule, the `is_ignored_span` branch creates `NoOpStreamedSpan(scope=self, unsampled_reason="ignored")` without passing `trace_id`. Because `NoOpStreamedSpan.trace_id` lazily generates a random `uuid.uuid4().hex` when `_trace_id` is falsy, this ignored root span reports a random trace ID unrelated to the active propagation context, breaking trace continuity in the emitted traceparent. The structurally identical `sampled is False` branch just below correctly passes `trace_id=propagation_context.trace_id`, and both child-span branches pass `trace_id=parent_span.trace_id`, so only this ignored root branch is inconsistent. Fix by passing `trace_id=propagation_context.trace_id` (already fetched on the preceding line).
unsampled_reason: "Optional[str]" = None,
scope: "Optional[sentry_sdk.Scope]" = None,
) -> None:
self._scope = scope # type: ignore[assignment]
self._unsampled_reason = unsampled_reason

self._trace_id = trace_id
self._span_id = None

self._unsampled_reason = unsampled_reason
self._finished = False

self._start()
Expand Down Expand Up @@ -713,17 +719,9 @@
pass

@property
def active(self) -> bool:

Check warning on line 722 in sentry_sdk/traces.py

View check run for this annotation

@sentry/warden / warden: code-review

Existing unsampled-span test still asserts old hardcoded zero trace/span IDs and will now fail

This PR makes `NoOpStreamedSpan` trace-aware: `Scope._start_streamed_span` now constructs the unsampled segment span with `trace_id=propagation_context.trace_id` (scope.py:1317-1320), and `NoOpStreamedSpan` inherits `trace_id`/`span_id` from `StreamedSpan`, which returns the propagated trace_id and lazily generates a random span_id (`uuid.uuid4().hex[16:]`). The existing test `test_continue_trace_unsampled` in `tests/tracing/test_span_streaming.py:747-748` still asserts `span.trace_id == "00000000000000000000000000000000"` and `span.span_id == "0000000000000000"`. Since that test starts an unsampled segment after `continue_trace` with `trace_id="0af7651916cd43dd8448eb211c80319c"`, `span.trace_id` will now be that propagated value and `span.span_id` will be a random hex, so both assertions will fail. This test file was not updated in the PR (only `test_http_headers.py` was), so CI will break.
return True

@property
def span_id(self) -> str:
return "0000000000000000"

@property
def trace_id(self) -> str:
return "00000000000000000000000000000000"

@property
def sampled(self) -> "Optional[bool]":
return False
Expand Down
32 changes: 32 additions & 0 deletions tests/tracing/test_http_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import pytest

import sentry_sdk
from sentry_sdk.tracing import Transaction
from sentry_sdk.tracing_utils import extract_sentrytrace_data

Expand All @@ -26,6 +27,37 @@ def test_to_traceparent(sampled):
assert parts[2] == "1" if sampled is True else "0" # sampled


@pytest.mark.parametrize("sampled", ["1", "0", ""])
def test_to_traceparent_span_streaming(sentry_init, sampled):
sentry_init(
# parent sampling decision takes precedence over traces_sample_rate
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream"},
)

trace_id = "12312012123120121231201212312012"

sentry_sdk.traces.continue_trace(
{
"sentry-trace": f"{trace_id}-b7ad6b7169203331-{sampled}",
}
)

with sentry_sdk.traces.start_span(name="/interactions/other-dogs/new-dog") as span:
pass

traceparent = span._to_traceparent()

parts = traceparent.split("-")
assert parts[0] == trace_id
assert parts[1] == span.span_id
if sampled == "":
# we'll sample this since our traces_sample_rate kicks in
assert parts[2] == "1"
else:
assert parts[2] == sampled


@pytest.mark.parametrize("sampling_decision", [True, False])
def test_sentrytrace_extraction(sampling_decision):
sentrytrace_header = "12312012123120121231201212312012-0415201309082013-{}".format(
Expand Down
Loading