Skip to content

Commit aa82f34

Browse files
committed
Fix deferred tracing decisions for twp
1 parent 6afa91c commit aa82f34

File tree

7 files changed

+78
-120
lines changed

7 files changed

+78
-120
lines changed

sentry_sdk/integrations/opentelemetry/sampler.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,14 @@ def get_parent_sampled(parent_context, trace_id):
3434
# Only inherit sample rate if `traceId` is the same
3535
if is_span_context_valid and parent_context.trace_id == trace_id:
3636
# this is getSamplingDecision in JS
37+
# if there was no sampling flag, defer the decision
38+
dsc_sampled = parent_context.trace_state.get(TRACESTATE_SAMPLED_KEY)
39+
if dsc_sampled == "deferred":
40+
return None
41+
3742
if parent_context.trace_flags.sampled is not None:
3843
return parent_context.trace_flags.sampled
3944

40-
dsc_sampled = parent_context.trace_state.get(TRACESTATE_SAMPLED_KEY)
4145
if dsc_sampled == "true":
4246
return True
4347
elif dsc_sampled == "false":
@@ -53,6 +57,8 @@ def dropped_result(parent_span_context, attributes, sample_rate=None):
5357

5458
if TRACESTATE_SAMPLED_KEY not in trace_state:
5559
trace_state = trace_state.add(TRACESTATE_SAMPLED_KEY, "false")
60+
elif trace_state.get(TRACESTATE_SAMPLED_KEY) == "deferred":
61+
trace_state = trace_state.update(TRACESTATE_SAMPLED_KEY, "false")
5662

5763
if sample_rate and TRACESTATE_SAMPLE_RATE_KEY not in trace_state:
5864
trace_state = trace_state.add(TRACESTATE_SAMPLE_RATE_KEY, str(sample_rate))
@@ -88,6 +94,9 @@ def sampled_result(span_context, attributes, sample_rate):
8894

8995
if TRACESTATE_SAMPLED_KEY not in trace_state:
9096
trace_state = trace_state.add(TRACESTATE_SAMPLED_KEY, "true")
97+
elif trace_state.get(TRACESTATE_SAMPLED_KEY) == "deferred":
98+
trace_state = trace_state.update(TRACESTATE_SAMPLED_KEY, "true")
99+
91100
if TRACESTATE_SAMPLE_RATE_KEY not in trace_state:
92101
trace_state = trace_state.add(TRACESTATE_SAMPLE_RATE_KEY, str(sample_rate))
93102

sentry_sdk/integrations/opentelemetry/scope.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
SpanContext,
77
NonRecordingSpan,
88
TraceFlags,
9+
TraceState,
910
use_span,
1011
)
1112

@@ -14,6 +15,7 @@
1415
SENTRY_FORK_ISOLATION_SCOPE_KEY,
1516
SENTRY_USE_CURRENT_SCOPE_KEY,
1617
SENTRY_USE_ISOLATION_SCOPE_KEY,
18+
TRACESTATE_SAMPLED_KEY,
1719
)
1820
from sentry_sdk.integrations.opentelemetry.utils import trace_state_from_baggage
1921
from sentry_sdk.scope import Scope, ScopeType
@@ -96,10 +98,15 @@ def _incoming_otel_span_context(self):
9698
else TraceFlags.DEFAULT
9799
)
98100

99-
# TODO-neel-potel do we need parent and sampled like JS?
100-
trace_state = None
101101
if self._propagation_context.baggage:
102102
trace_state = trace_state_from_baggage(self._propagation_context.baggage)
103+
else:
104+
trace_state = TraceState()
105+
106+
# for twp to work, we also need to consider deferred sampling when the sampling
107+
# flag is not present, so the above TraceFlags are not sufficient
108+
if self._propagation_context.parent_sampled is None:
109+
trace_state = trace_state.add(TRACESTATE_SAMPLED_KEY, "deferred")
103110

104111
span_context = SpanContext(
105112
trace_id=int(self._propagation_context.trace_id, 16), # type: ignore

sentry_sdk/tracing.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1311,6 +1311,8 @@ def __exit__(self, ty, value, tb):
13111311
# type: (Optional[Any], Optional[Any], Optional[Any]) -> None
13121312
if value is not None:
13131313
self.set_status(SPANSTATUS.INTERNAL_ERROR)
1314+
else:
1315+
self.set_status(SPANSTATUS.OK)
13141316

13151317
self.finish()
13161318
context.detach(self._ctx_token)

tests/integrations/opentelemetry/test_compat.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def test_transaction_name_span_description_compat(
99

1010
events = capture_events()
1111

12-
with sentry_sdk.start_transaction(
12+
with sentry_sdk.start_span(
1313
name="trx-name",
1414
op="trx-op",
1515
) as trx:
@@ -33,13 +33,12 @@ def test_transaction_name_span_description_compat(
3333
assert spn.__class__.__name__ == "POTelSpan"
3434
assert spn.op == "span-op"
3535
assert spn.description == "span-desc"
36-
assert spn.name is None
36+
assert spn.name == "span-desc"
3737

3838
assert spn._otel_span is not None
3939
assert spn._otel_span.name == "span-desc"
4040
assert spn._otel_span.attributes["sentry.op"] == "span-op"
4141
assert spn._otel_span.attributes["sentry.description"] == "span-desc"
42-
assert "sentry.name" not in spn._otel_span.attributes
4342

4443
transaction = events[0]
4544
assert transaction["transaction"] == "trx-name"
@@ -53,4 +52,3 @@ def test_transaction_name_span_description_compat(
5352
assert span["op"] == "span-op"
5453
assert span["data"]["sentry.op"] == "span-op"
5554
assert span["data"]["sentry.description"] == "span-desc"
56-
assert "sentry.name" not in span["data"]

tests/integrations/opentelemetry/test_experimental.py

Lines changed: 0 additions & 47 deletions
This file was deleted.

tests/integrations/opentelemetry/test_potel.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import pytest
2-
32
from opentelemetry import trace
43

54
import sentry_sdk
5+
from tests.conftest import ApproxDict
66

77

88
tracer = trace.get_tracer(__name__)
@@ -43,7 +43,6 @@ def test_root_span_transaction_payload_started_with_otel_only(capture_envelopes)
4343
assert "span_id" in trace_context
4444
assert trace_context["origin"] == "manual"
4545
assert trace_context["op"] == "request"
46-
assert trace_context["status"] == "ok"
4746

4847
assert payload["spans"] == []
4948

@@ -63,7 +62,6 @@ def test_child_span_payload_started_with_otel_only(capture_envelopes):
6362
assert span["op"] == "db"
6463
assert span["description"] == "db"
6564
assert span["origin"] == "manual"
66-
assert span["status"] == "ok"
6765
assert span["span_id"] is not None
6866
assert span["trace_id"] == payload["contexts"]["trace"]["trace_id"]
6967
assert span["parent_span_id"] == payload["contexts"]["trace"]["span_id"]
@@ -222,8 +220,8 @@ def test_span_attributes_in_data_started_with_otel(capture_envelopes):
222220
(item,) = envelope.items
223221
payload = item.payload.json
224222

225-
assert payload["contexts"]["trace"]["data"] == {"foo": "bar", "baz": 42}
226-
assert payload["spans"][0]["data"] == {"abc": 99, "def": "moo"}
223+
assert payload["contexts"]["trace"]["data"] == ApproxDict({"foo": "bar", "baz": 42})
224+
assert payload["spans"][0]["data"] == ApproxDict({"abc": 99, "def": "moo"})
227225

228226

229227
def test_span_data_started_with_sentry(capture_envelopes):
@@ -238,18 +236,22 @@ def test_span_data_started_with_sentry(capture_envelopes):
238236
(item,) = envelope.items
239237
payload = item.payload.json
240238

241-
assert payload["contexts"]["trace"]["data"] == {
242-
"foo": "bar",
243-
"sentry.origin": "manual",
244-
"sentry.description": "request",
245-
"sentry.op": "http",
246-
}
247-
assert payload["spans"][0]["data"] == {
248-
"baz": 42,
249-
"sentry.origin": "manual",
250-
"sentry.description": "statement",
251-
"sentry.op": "db",
252-
}
239+
assert payload["contexts"]["trace"]["data"] == ApproxDict(
240+
{
241+
"foo": "bar",
242+
"sentry.origin": "manual",
243+
"sentry.description": "request",
244+
"sentry.op": "http",
245+
}
246+
)
247+
assert payload["spans"][0]["data"] == ApproxDict(
248+
{
249+
"baz": 42,
250+
"sentry.origin": "manual",
251+
"sentry.description": "statement",
252+
"sentry.op": "db",
253+
}
254+
)
253255

254256

255257
def test_transaction_tags_started_with_otel(capture_envelopes):

tests/integrations/opentelemetry/test_sampler.py

Lines changed: 36 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,13 @@ def test_sampling_traces_sample_rate_50(sentry_init, capture_envelopes):
7171

7272
envelopes = capture_envelopes()
7373

74-
with mock.patch(
75-
"sentry_sdk.integrations.opentelemetry.sampler.random", return_value=0.2
76-
): # drop
74+
with mock.patch("random.random", return_value=0.2): # drop
7775
with sentry_sdk.start_span(description="request a"):
7876
with sentry_sdk.start_span(description="cache a"):
7977
with sentry_sdk.start_span(description="db a"):
8078
...
8179

82-
with mock.patch(
83-
"sentry_sdk.integrations.opentelemetry.sampler.random", return_value=0.7
84-
): # keep
80+
with mock.patch("random.random", return_value=0.7): # keep
8581
with sentry_sdk.start_span(description="request b"):
8682
with sentry_sdk.start_span(description="cache b"):
8783
with sentry_sdk.start_span(description="db b"):
@@ -101,46 +97,34 @@ def test_sampling_traces_sample_rate_50(sentry_init, capture_envelopes):
10197
def test_sampling_traces_sampler(sentry_init, capture_envelopes):
10298
def keep_only_a(sampling_context):
10399
if " a" in sampling_context["transaction_context"]["name"]:
104-
return 0.05
100+
return 1
105101
else:
106102
return 0
107103

108-
sentry_init(
109-
traces_sample_rate=1.0,
110-
traces_sampler=keep_only_a,
111-
)
104+
sentry_init(traces_sampler=keep_only_a)
112105

113106
envelopes = capture_envelopes()
114107

115-
# Make sure random() always returns the same values
116-
with mock.patch(
117-
"sentry_sdk.integrations.opentelemetry.sampler.random",
118-
side_effect=[0.04 for _ in range(12)],
119-
):
120-
121-
with sentry_sdk.start_span(description="request a"): # keep
122-
with sentry_sdk.start_span(description="cache a"): # keep
123-
with sentry_sdk.start_span(description="db a"): # keep
124-
...
108+
# children inherit from root spans
109+
with sentry_sdk.start_span(description="request a"): # keep
110+
with sentry_sdk.start_span(description="cache a"):
111+
with sentry_sdk.start_span(description="db a"):
112+
...
125113

126-
with sentry_sdk.start_span(description="request b"): # drop
127-
with sentry_sdk.start_span(description="cache b"): # drop
128-
with sentry_sdk.start_span(description="db b"): # drop
129-
...
114+
with sentry_sdk.start_span(description="request b"): # drop
115+
with sentry_sdk.start_span(description="cache b"):
116+
with sentry_sdk.start_span(description="db b"):
117+
...
130118

131-
with sentry_sdk.start_span(description="request c"): # drop
132-
with sentry_sdk.start_span(
133-
description="cache a c"
134-
): # keep (but trx dropped, so not collected)
135-
with sentry_sdk.start_span(
136-
description="db a c"
137-
): # keep (but trx dropped, so not collected)
138-
...
119+
with sentry_sdk.start_span(description="request c"): # drop
120+
with sentry_sdk.start_span(description="cache a c"):
121+
with sentry_sdk.start_span(description="db a c"):
122+
...
139123

140-
with sentry_sdk.start_span(description="new a c"): # keep
141-
with sentry_sdk.start_span(description="cache c"): # drop
142-
with sentry_sdk.start_span(description="db c"): # drop
143-
...
124+
with sentry_sdk.start_span(description="new a c"): # keep
125+
with sentry_sdk.start_span(description="cache c"):
126+
with sentry_sdk.start_span(description="db c"):
127+
...
144128

145129
assert len(envelopes) == 2
146130
(envelope1, envelope2) = envelopes
@@ -150,7 +134,7 @@ def keep_only_a(sampling_context):
150134
assert transaction1["transaction"] == "request a"
151135
assert len(transaction1["spans"]) == 2
152136
assert transaction2["transaction"] == "new a c"
153-
assert len(transaction2["spans"]) == 0
137+
assert len(transaction2["spans"]) == 2
154138

155139

156140
def test_sampling_traces_sampler_boolean(sentry_init, capture_envelopes):
@@ -168,21 +152,21 @@ def keep_only_a(sampling_context):
168152
envelopes = capture_envelopes()
169153

170154
with sentry_sdk.start_span(description="request a"): # keep
171-
with sentry_sdk.start_span(description="cache a"): # keep
172-
with sentry_sdk.start_span(description="db X"): # drop
155+
with sentry_sdk.start_span(description="cache a"):
156+
with sentry_sdk.start_span(description="db X"):
173157
...
174158

175159
with sentry_sdk.start_span(description="request b"): # drop
176-
with sentry_sdk.start_span(description="cache b"): # drop
177-
with sentry_sdk.start_span(description="db b"): # drop
160+
with sentry_sdk.start_span(description="cache b"):
161+
with sentry_sdk.start_span(description="db b"):
178162
...
179163

180164
assert len(envelopes) == 1
181165
(envelope,) = envelopes
182166
transaction = envelope.items[0].payload.json
183167

184168
assert transaction["transaction"] == "request a"
185-
assert len(transaction["spans"]) == 1
169+
assert len(transaction["spans"]) == 2
186170

187171

188172
@pytest.mark.parametrize(
@@ -237,21 +221,24 @@ def test_sampling_parent_sampled(
237221

238222

239223
@pytest.mark.parametrize(
240-
"traces_sample_rate, expected_num_of_envelopes",
224+
"traces_sample_rate, upstream_sampled, expected_num_of_envelopes",
241225
[
242226
# special case for testing, do not pass any traces_sample_rate to init() (the default traces_sample_rate=None will be used)
243-
(-1, 0),
227+
(-1, 0, 0),
244228
# traces_sample_rate=None means do not create new traces, and also do not continue incoming traces. So, no envelopes at all.
245-
(None, 0),
229+
(None, 1, 0),
246230
# traces_sample_rate=0 means do not create new traces (0% of the requests), but continue incoming traces. So envelopes will be created only if there is an incoming trace.
247-
(0, 0),
231+
(0, 0, 0),
232+
(0, 1, 1),
248233
# traces_sample_rate=1 means create new traces for 100% of requests (and also continue incoming traces, of course).
249-
(1, 1),
234+
(1, 0, 0),
235+
(1, 1, 1),
250236
],
251237
)
252238
def test_sampling_parent_dropped(
253239
sentry_init,
254240
traces_sample_rate,
241+
upstream_sampled,
255242
expected_num_of_envelopes,
256243
capture_envelopes,
257244
):
@@ -265,7 +252,7 @@ def test_sampling_parent_dropped(
265252

266253
# The upstream service has dropped the request
267254
headers = {
268-
"sentry-trace": "771a43a4192642f0b136d5159a501700-1234567890abcdef-0",
255+
"sentry-trace": f"771a43a4192642f0b136d5159a501700-1234567890abcdef-{upstream_sampled}",
269256
}
270257
with sentry_sdk.continue_trace(headers):
271258
with sentry_sdk.start_span(description="request a"):

0 commit comments

Comments
 (0)