Skip to content

Commit 087c563

Browse files
committed
Merge branch 'potel-base' into potel-base-run-all-tests
2 parents e93fa53 + 0011e22 commit 087c563

File tree

8 files changed

+149
-65
lines changed

8 files changed

+149
-65
lines changed

MIGRATION_GUIDE.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh
1010

1111
- The SDK now supports Python 3.7 and higher.
1212
- `sentry_sdk.start_span` now only takes keyword arguments.
13-
- `sentry_sdk.start_span` no longer takes an explicit `span` argument.
14-
- `sentry_sdk.start_span` no longer takes explicit `trace_id`, `span_id` or `parent_span_id` arguments.
13+
- `sentry_sdk.start_transaction`/`sentry_sdk.start_span` no longer takes the following arguments: `span`, `parent_sampled`, `trace_id`, `span_id` or `parent_span_id`.
1514
- The `Span()` constructor does not accept a `hub` parameter anymore.
1615
- `Span.finish()` does not accept a `hub` parameter anymore.
1716
- The `Profile()` constructor does not accept a `hub` parameter anymore.
@@ -21,6 +20,18 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh
2120
- clickhouse-driver integration: The query is now available under the `db.query.text` span attribute (only if `send_default_pii` is `True`).
2221
- `sentry_sdk.init` now returns `None` instead of a context manager.
2322
- The `sampling_context` argument of `traces_sampler` now additionally contains all span attributes known at span start.
23+
- The `sampling_context` argument of `traces_sampler` doesn't contain the `asgi_scope` object anymore for ASGI frameworks. Instead, the individual properties on the scope, if available, are accessible as follows:
24+
25+
| Scope property | Sampling context key(s) |
26+
| -------------- | ------------------------------- |
27+
| `type` | `network.protocol.name` |
28+
| `scheme` | `url.scheme` |
29+
| `path` | `url.path` |
30+
| `http_version` | `network.protocol.version` |
31+
| `method` | `http.request.method` |
32+
| `server` | `server.address`, `server.port` |
33+
| `client` | `client.address`, `client.port` |
34+
| full URL | `url.full` |
2435

2536
### Removed
2637

sentry_sdk/integrations/_asgi_common.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ def _get_headers(asgi_scope):
3232
return headers
3333

3434

35-
def _get_url(asgi_scope, default_scheme, host):
36-
# type: (Dict[str, Any], Literal["ws", "http"], Optional[Union[AnnotatedValue, str]]) -> str
35+
def _get_url(asgi_scope, default_scheme=None, host=None):
36+
# type: (Dict[str, Any], Optional[Literal["ws", "http"]], Optional[Union[AnnotatedValue, str]]) -> str
3737
"""
3838
Extract URL from the ASGI scope, without also including the querystring.
3939
"""

sentry_sdk/integrations/asgi.py

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from sentry_sdk.integrations._asgi_common import (
1717
_get_headers,
18+
_get_query,
1819
_get_request_data,
1920
_get_url,
2021
)
@@ -57,6 +58,14 @@
5758

5859
TRANSACTION_STYLE_VALUES = ("endpoint", "url")
5960

61+
ASGI_SCOPE_PROPERTY_TO_ATTRIBUTE = {
62+
"http_version": "network.protocol.version",
63+
"method": "http.request.method",
64+
"path": "url.path",
65+
"scheme": "url.scheme",
66+
"type": "network.protocol.name",
67+
}
68+
6069

6170
def _capture_exception(exc, mechanism_type="asgi"):
6271
# type: (Any, str) -> None
@@ -209,27 +218,25 @@ async def _run_app(self, scope, receive, send, asgi_version):
209218
name=transaction_name,
210219
source=transaction_source,
211220
origin=self.span_origin,
212-
custom_sampling_context={"asgi_scope": scope},
221+
attributes=_prepopulate_attributes(scope),
213222
)
214223
if should_trace
215224
else nullcontext()
216-
) as transaction:
217-
if transaction is not None:
218-
logger.debug(
219-
"[ASGI] Started transaction: %s", transaction
220-
)
221-
transaction.set_tag("asgi.type", ty)
225+
) as span:
226+
if span is not None:
227+
logger.debug("[ASGI] Started transaction: %s", span)
228+
span.set_tag("asgi.type", ty)
222229
try:
223230

224231
async def _sentry_wrapped_send(event):
225232
# type: (Dict[str, Any]) -> Any
226233
is_http_response = (
227234
event.get("type") == "http.response.start"
228-
and transaction is not None
235+
and span is not None
229236
and "status" in event
230237
)
231238
if is_http_response:
232-
transaction.set_http_status(event["status"])
239+
span.set_http_status(event["status"])
233240

234241
return await send(event)
235242

@@ -324,3 +331,35 @@ def _get_transaction_name_and_source(self, transaction_style, asgi_scope):
324331
return name, source
325332

326333
return name, source
334+
335+
336+
def _prepopulate_attributes(scope):
337+
# type: (Any) -> dict[str, Any]
338+
"""Unpack ASGI scope into serializable OTel attributes."""
339+
scope = scope or {}
340+
341+
attributes = {}
342+
for attr, key in ASGI_SCOPE_PROPERTY_TO_ATTRIBUTE.items():
343+
if scope.get(attr):
344+
attributes[key] = scope[attr]
345+
346+
for attr in ("client", "server"):
347+
if scope.get(attr):
348+
try:
349+
host, port = scope[attr]
350+
attributes[f"{attr}.address"] = host
351+
attributes[f"{attr}.port"] = port
352+
except Exception:
353+
pass
354+
355+
try:
356+
full_url = _get_url(scope)
357+
query = _get_query(scope)
358+
if query:
359+
full_url = f"{full_url}?{query}"
360+
361+
attributes["url.full"] = full_url
362+
except Exception:
363+
pass
364+
365+
return attributes

sentry_sdk/integrations/opentelemetry/sampler.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,24 +120,34 @@ def should_sample(
120120
if not has_tracing_enabled(client.options):
121121
return dropped_result(parent_span_context, attributes)
122122

123+
# parent_span_context.is_valid means this span has a parent, remote or local
124+
is_root_span = not parent_span_context.is_valid or parent_span_context.is_remote
125+
123126
# Explicit sampled value provided at start_span
124127
if attributes.get(SentrySpanAttribute.CUSTOM_SAMPLED) is not None:
125-
sample_rate = float(attributes[SentrySpanAttribute.CUSTOM_SAMPLED])
126-
if sample_rate > 0:
127-
return sampled_result(parent_span_context, attributes, sample_rate)
128+
if is_root_span:
129+
sample_rate = float(attributes[SentrySpanAttribute.CUSTOM_SAMPLED])
130+
if sample_rate > 0:
131+
return sampled_result(parent_span_context, attributes, sample_rate)
132+
else:
133+
return dropped_result(parent_span_context, attributes)
128134
else:
129-
return dropped_result(parent_span_context, attributes)
135+
logger.debug(
136+
f"[Tracing] Ignoring sampled param for non-root span {name}"
137+
)
130138

131139
sample_rate = None
132140

133141
# Check if there is a traces_sampler
134142
# Traces_sampler is responsible to check parent sampled to have full transactions.
135143
has_traces_sampler = callable(client.options.get("traces_sampler"))
136-
if has_traces_sampler:
144+
145+
if is_root_span and has_traces_sampler:
137146
sampling_context = {
138147
"transaction_context": {
139148
"name": name,
140149
"op": attributes.get(SentrySpanAttribute.OP),
150+
"source": attributes.get(SentrySpanAttribute.SOURCE),
141151
},
142152
"parent_sampled": get_parent_sampled(parent_span_context, trace_id),
143153
}
@@ -161,8 +171,7 @@ def should_sample(
161171
return dropped_result(parent_span_context, attributes)
162172

163173
# Down-sample in case of back pressure monitor says so
164-
# TODO: this should only be done for transactions (aka root spans)
165-
if client.monitor:
174+
if is_root_span and client.monitor:
166175
sample_rate /= 2**client.monitor.downsample_factor
167176

168177
# Roll the dice on sample rate

sentry_sdk/tracing.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,9 +1228,11 @@ def __init__(
12281228
if otel_span is not None:
12291229
self._otel_span = otel_span
12301230
else:
1231-
skip_span = (
1232-
only_if_parent and not get_current_span().get_span_context().is_valid
1233-
)
1231+
skip_span = False
1232+
if only_if_parent:
1233+
parent_span_context = get_current_span().get_span_context()
1234+
skip_span = not parent_span_context.is_valid or parent_span_context.is_remote
1235+
12341236
if skip_span:
12351237
self._otel_span = INVALID_SPAN
12361238
else:
@@ -1250,6 +1252,7 @@ def __init__(
12501252
# Prepopulate some attrs so that they're accessible in traces_sampler
12511253
attributes = attributes or {}
12521254
attributes[SentrySpanAttribute.OP] = op
1255+
attributes[SentrySpanAttribute.SOURCE] = source
12531256
if sampled is not None:
12541257
attributes[SentrySpanAttribute.CUSTOM_SAMPLED] = sampled
12551258

@@ -1260,7 +1263,6 @@ def __init__(
12601263
self.origin = origin or DEFAULT_SPAN_ORIGIN
12611264
self.description = description
12621265
self.name = span_name
1263-
self.source = source
12641266

12651267
if status is not None:
12661268
self.set_status(status)

tests/integrations/asgi/test_asgi.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,3 +721,24 @@ async def test_custom_transaction_name(
721721
assert transaction_event["type"] == "transaction"
722722
assert transaction_event["transaction"] == "foobar"
723723
assert transaction_event["transaction_info"] == {"source": "custom"}
724+
725+
726+
@pytest.mark.asyncio
727+
async def test_asgi_scope_in_traces_sampler(sentry_init, asgi3_app):
728+
def dummy_traces_sampler(sampling_context):
729+
assert sampling_context["url.path"] == "/test"
730+
assert sampling_context["url.scheme"] == "http"
731+
assert sampling_context["url.full"] == "/test?hello=there"
732+
assert sampling_context["http.request.method"] == "GET"
733+
assert sampling_context["network.protocol.version"] == "1.1"
734+
assert sampling_context["network.protocol.name"] == "http"
735+
736+
sentry_init(
737+
traces_sampler=dummy_traces_sampler,
738+
traces_sample_rate=1.0,
739+
)
740+
741+
app = SentryAsgiMiddleware(asgi3_app)
742+
743+
async with TestClient(app) as client:
744+
await client.get("/test?hello=there")

tests/integrations/fastapi/test_fastapi.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,6 @@ async def _error(request: Request):
246246
assert event["request"]["headers"]["authorization"] == "[Filtered]"
247247

248248

249-
@pytest.mark.asyncio
250249
def test_response_status_code_ok_in_transaction_context(sentry_init, capture_envelopes):
251250
"""
252251
Tests that the response status code is added to the transaction "response" context.
@@ -275,7 +274,6 @@ def test_response_status_code_ok_in_transaction_context(sentry_init, capture_env
275274
assert transaction["contexts"]["response"]["status_code"] == 200
276275

277276

278-
@pytest.mark.asyncio
279277
def test_response_status_code_error_in_transaction_context(
280278
sentry_init,
281279
capture_envelopes,
@@ -312,7 +310,6 @@ def test_response_status_code_error_in_transaction_context(
312310
assert transaction["contexts"]["response"]["status_code"] == 500
313311

314312

315-
@pytest.mark.asyncio
316313
def test_response_status_code_not_found_in_transaction_context(
317314
sentry_init,
318315
capture_envelopes,

tests/tracing/test_sampling.py

Lines changed: 43 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,33 @@
66

77
import sentry_sdk
88
from sentry_sdk import start_span, start_transaction, capture_exception
9-
from sentry_sdk.tracing import Transaction
109
from sentry_sdk.utils import logger
1110

1211

13-
def test_sampling_decided_only_for_transactions(sentry_init, capture_events):
12+
def test_sampling_decided_only_for_root_spans(sentry_init):
1413
sentry_init(traces_sample_rate=0.5)
1514

16-
with start_transaction(name="hi") as transaction:
17-
assert transaction.sampled is not None
15+
with start_span(name="outer1") as root_span1:
16+
assert root_span1.sampled is not None
1817

19-
with start_span() as span:
20-
assert span.sampled == transaction.sampled
18+
with start_span(name="inner") as span:
19+
assert span.sampled == root_span1.sampled
2120

22-
with start_span() as span:
23-
assert span.sampled is None
21+
with start_span(name="outer2") as root_span2:
22+
assert root_span2.sampled is not None
2423

2524

2625
@pytest.mark.parametrize("sampled", [True, False])
27-
def test_nested_transaction_sampling_override(sentry_init, sampled):
26+
def test_nested_span_sampling_override(sentry_init, sampled):
2827
sentry_init(traces_sample_rate=1.0)
2928

30-
with start_transaction(name="outer", sampled=sampled) as outer_transaction:
31-
assert outer_transaction.sampled is sampled
32-
with start_transaction(
33-
name="inner", sampled=(not sampled)
34-
) as inner_transaction:
35-
assert inner_transaction.sampled is not sampled
36-
assert outer_transaction.sampled is sampled
29+
with start_span(name="outer", sampled=sampled) as outer_span:
30+
assert outer_span.sampled is sampled
31+
with start_span(name="inner", sampled=(not sampled)) as inner_span:
32+
# won't work because the child span inherits the sampling decision
33+
# from the parent
34+
assert inner_span.sampled is sampled
35+
assert outer_span.sampled is sampled
3736

3837

3938
def test_no_double_sampling(sentry_init, capture_events):
@@ -147,10 +146,17 @@ def test_ignores_inherited_sample_decision_when_traces_sampler_defined(
147146
traces_sampler = mock.Mock(return_value=not parent_sampling_decision)
148147
sentry_init(traces_sampler=traces_sampler)
149148

150-
transaction = start_transaction(
151-
name="dogpark", parent_sampled=parent_sampling_decision
149+
sentry_trace_header = (
150+
"12312012123120121231201212312012-1121201211212012-{sampled}".format(
151+
sampled=int(parent_sampling_decision)
152+
)
152153
)
153-
assert transaction.sampled is not parent_sampling_decision
154+
155+
with sentry_sdk.continue_trace({"sentry-trace": sentry_trace_header}):
156+
with sentry_sdk.start_span(name="dogpark") as span:
157+
pass
158+
159+
assert span.sampled is not parent_sampling_decision
154160

155161

156162
@pytest.mark.parametrize("explicit_decision", [True, False])
@@ -176,39 +182,38 @@ def test_inherits_parent_sampling_decision_when_traces_sampler_undefined(
176182
sentry_init(traces_sample_rate=0.5)
177183
mock_random_value = 0.25 if parent_sampling_decision is False else 0.75
178184

179-
with mock.patch.object(random, "random", return_value=mock_random_value):
180-
transaction = start_transaction(
181-
name="dogpark", parent_sampled=parent_sampling_decision
185+
sentry_trace_header = (
186+
"12312012123120121231201212312012-1121201211212012-{sampled}".format(
187+
sampled=int(parent_sampling_decision)
182188
)
183-
assert transaction.sampled is parent_sampling_decision
189+
)
190+
with mock.patch.object(random, "random", return_value=mock_random_value):
191+
with sentry_sdk.continue_trace({"sentry-trace": sentry_trace_header}):
192+
with start_span(name="dogpark") as span:
193+
pass
194+
195+
assert span.sampled is parent_sampling_decision
184196

185197

186198
@pytest.mark.parametrize("parent_sampling_decision", [True, False])
187199
def test_passes_parent_sampling_decision_in_sampling_context(
188200
sentry_init, parent_sampling_decision
189201
):
190-
sentry_init(traces_sample_rate=1.0)
202+
def dummy_traces_sampler(sampling_context):
203+
assert sampling_context["parent_sampled"] is parent_sampling_decision
204+
return 1.0
205+
206+
sentry_init(traces_sample_rate=1.0, traces_sampler=dummy_traces_sampler)
191207

192208
sentry_trace_header = (
193209
"12312012123120121231201212312012-1121201211212012-{sampled}".format(
194210
sampled=int(parent_sampling_decision)
195211
)
196212
)
197213

198-
transaction = Transaction.continue_from_headers(
199-
headers={"sentry-trace": sentry_trace_header}, name="dogpark"
200-
)
201-
spy = mock.Mock(wraps=transaction)
202-
start_transaction(transaction=spy)
203-
204-
# there's only one call (so index at 0) and kwargs are always last in a call
205-
# tuple (so index at -1)
206-
sampling_context = spy._set_initial_sampling_decision.mock_calls[0][-1][
207-
"sampling_context"
208-
]
209-
assert "parent_sampled" in sampling_context
210-
# because we passed in a spy, attribute access requires unwrapping
211-
assert sampling_context["parent_sampled"]._mock_wraps is parent_sampling_decision
214+
with sentry_sdk.continue_trace({"sentry-trace": sentry_trace_header}):
215+
with sentry_sdk.start_span(name="dogpark"):
216+
pass
212217

213218

214219
def test_passes_attributes_from_start_span_to_traces_sampler(

0 commit comments

Comments
 (0)