Skip to content
Merged
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
69 changes: 25 additions & 44 deletions sentry_sdk/integrations/sanic.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from urllib.parse import urlsplit

import sentry_sdk
from sentry_sdk import continue_trace
from sentry_sdk.consts import OP, SPANDATA
from sentry_sdk.data_collection import (
_apply_data_collection_filtering_to_query_string,
Expand All @@ -15,7 +14,6 @@
from sentry_sdk.scope import should_send_default_pii
from sentry_sdk.traces import SegmentNameSource, StreamedSpan
from sentry_sdk.tracing import TransactionSource
from sentry_sdk.tracing_utils import has_span_streaming_enabled
from sentry_sdk.utils import (
capture_internal_exceptions,
ensure_integration_enabled,
Expand Down Expand Up @@ -141,58 +139,41 @@ async def _context_enter(request: "Request") -> None:
return

client = sentry_sdk.get_client()
is_span_streaming_enabled = has_span_streaming_enabled(client.options)

weak_request = weakref.ref(request)
request.ctx._sentry_scope = sentry_sdk.isolation_scope()
scope = request.ctx._sentry_scope.__enter__()
scope.clear_breadcrumbs()
scope.add_event_processor(_make_request_processor(weak_request))

if is_span_streaming_enabled:
integration = client.get_integration(SanicIntegration)
if (
isinstance(integration, SanicIntegration)
and integration._unsampled_statuses
):
logger.warning(
"The `unsampled_statuses` option of SanicIntegration has no effect when span streaming is enabled.",
)
integration = client.get_integration(SanicIntegration)
if isinstance(integration, SanicIntegration) and integration._unsampled_statuses:
logger.warning(
"The `unsampled_statuses` option of SanicIntegration has no effect when span streaming is enabled.",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Default option warns on every request

Medium Severity

The unsampled_statuses warning now runs on every request. The default is {404}, which is truthy, so default SanicIntegration() logs this warning for every incoming request. The message also claims span streaming is enabled even when trace_lifecycle is not stream.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b386c02. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Will fix in a follow-up


sentry_sdk.traces.continue_trace(dict(request.headers))
scope.set_custom_sampling_context({"sanic_request": request})
sentry_sdk.traces.continue_trace(dict(request.headers))
scope.set_custom_sampling_context({"sanic_request": request})

if request.remote_addr:
if has_data_collection_enabled(client.options):
if client.options["data_collection"]["user_info"]:
scope.set_attribute(SPANDATA.USER_IP_ADDRESS, request.remote_addr)
elif should_send_default_pii():
if request.remote_addr:
if has_data_collection_enabled(client.options):
if client.options["data_collection"]["user_info"]:
scope.set_attribute(SPANDATA.USER_IP_ADDRESS, request.remote_addr)

span = sentry_sdk.traces.start_span(
# Unless the request results in a 404 error, the name and source
# will get overwritten in _set_transaction
name=request.path,
attributes={
"sentry.op": OP.HTTP_SERVER,
"sentry.origin": SanicIntegration.origin,
"sentry.segment.name.source": SegmentNameSource.URL.value,
},
parent_span=None,
)
request.ctx._sentry_root_span = span
else:
transaction = continue_trace(
dict(request.headers),
op=OP.HTTP_SERVER,
# Unless the request results in a 404 error, the name and source will get overwritten in _set_transaction
name=request.path,
source=TransactionSource.URL,
origin=SanicIntegration.origin,
)
request.ctx._sentry_root_span = sentry_sdk.start_transaction(
transaction
).__enter__()
elif should_send_default_pii():
scope.set_attribute(SPANDATA.USER_IP_ADDRESS, request.remote_addr)

span = sentry_sdk.traces.start_span(
# Unless the request results in a 404 error, the name and source
# will get overwritten in _set_transaction
name=request.path,
attributes={
"sentry.op": OP.HTTP_SERVER,
"sentry.origin": SanicIntegration.origin,
"sentry.segment.name.source": SegmentNameSource.URL.value,
},
parent_span=None,
)
request.ctx._sentry_root_span = span


async def _context_exit(
Expand Down
170 changes: 54 additions & 116 deletions tests/integrations/sanic/test_sanic.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ async def task(i):
"headers": {},
"version": "1.1",
"method": "GET",
"transport": None,
"transport": Mock(spec=["get_extra_info"]),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Without this, test_concurrency throws an error on teardown

}

if SANIC_VERSION >= (19,):
Expand Down Expand Up @@ -360,7 +360,6 @@ def __init__(
expected_status: int,
expected_transaction_name: "Optional[str]",
expected_source: "Optional[str]" = None,
streaming_compatible: bool = True,
) -> None:
"""
expected_transaction_name of None indicates we expect to not receive a transaction
Expand All @@ -370,11 +369,9 @@ def __init__(
self.expected_status = expected_status
self.expected_transaction_name = expected_transaction_name
self.expected_source = expected_source
self.streaming_compatible = streaming_compatible


@pytest.mark.parametrize("send_pii", [True, False])
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize(
"test_config",
[
Expand Down Expand Up @@ -402,14 +399,6 @@ def __init__(
expected_transaction_name="fivehundred",
expected_source=TransactionSource.COMPONENT,
),
TransactionTestConfig(
# By default, no transaction when we have a 404 error
integration_args=(),
url="/404",
expected_status=404,
expected_transaction_name=None,
streaming_compatible=False,
),
TransactionTestConfig(
# With no ignored HTTP statuses, we should get transactions for 404 errors
integration_args=(None,),
Expand All @@ -418,40 +407,24 @@ def __init__(
expected_transaction_name="/404",
expected_source=TransactionSource.URL,
),
TransactionTestConfig(
# Transaction can be suppressed for other HTTP statuses, too, by passing config to the integration
integration_args=({200},),
url="/message",
expected_status=200,
expected_transaction_name=None,
streaming_compatible=False,
),
],
)
def test_transactions(
test_config: "TransactionTestConfig",
sentry_init: "Any",
app: "Any",
capture_events: "Any",
capture_items: "Any",
span_streaming: bool,
send_pii: bool,
) -> None:
if span_streaming and not test_config.streaming_compatible:
pytest.skip("unsampled_statuses is not supported in span streaming mode")

# Init the SanicIntegration with the desired arguments
sentry_init(
integrations=[SanicIntegration(*test_config.integration_args)],
traces_sample_rate=1.0,
send_default_pii=send_pii,
trace_lifecycle="stream" if span_streaming else "static",
trace_lifecycle="stream",
)

if span_streaming:
items = capture_items("span")
else:
events = capture_events()
items = capture_items("span")

# Make request to the desired URL
c = get_client(app)
Expand All @@ -461,106 +434,71 @@ def test_transactions(

sentry_sdk.flush()

if span_streaming:
segments = [
i.payload
for i in items
if i.payload["attributes"].get("sentry.origin") == "auto.http.sanic"
and i.payload["is_segment"]
]
assert len(segments) <= 1
(segment, *_) = [*segments, None]

assert (segment is None) == (test_config.expected_transaction_name is None)

if segment is not None:
assert segment["name"] == test_config.expected_transaction_name
assert (
segment["attributes"]["sentry.segment.name.source"]
== test_config.expected_source
)

attrs = segment["attributes"]
assert attrs["http.request.method"] == "GET"
assert attrs["network.protocol.name"] == "http"
header_keys = {
key[len("http.request.header.") :]
for key in attrs
if key.startswith("http.request.header.")
}
assert header_keys >= {"accept", "accept-encoding", "host", "user-agent"}
assert attrs["http.response.status_code"] == test_config.expected_status
assert segment["status"] == (
"error" if test_config.expected_status >= 400 else "ok"
)

if send_pii:
assert attrs["url.full"].endswith(test_config.url)
assert attrs["url.path"] == test_config.url.split("?")[0]
if "?" in test_config.url:
assert attrs["http.query"] == test_config.url.split("?", 1)[1]

else:
assert "url.full" not in attrs
assert "url.path" not in attrs
assert "http.query" not in attrs
segments = [
i.payload
for i in items
if i.payload["attributes"].get("sentry.origin") == "auto.http.sanic"
and i.payload["is_segment"]
]
assert len(segments) <= 1
(segment, *_) = [*segments, None]

else:
# Extract the transaction events by inspecting the event types. We should at most have 1 transaction event.
transaction_events = [
e for e in events if "type" in e and e["type"] == "transaction"
]
assert len(transaction_events) <= 1

# Get the only transaction event, or set to None if there are no transaction events.
(transaction_event, *_) = [*transaction_events, None]

# We should have no transaction event if and only if we expect no transactions
assert (transaction_event is None) == (
test_config.expected_transaction_name is None
)
assert (segment is None) == (test_config.expected_transaction_name is None)

# If a transaction was expected, ensure it is correct
assert (
transaction_event is None
or transaction_event["transaction"] == test_config.expected_transaction_name
)
if segment is not None:
assert segment["name"] == test_config.expected_transaction_name
assert (
transaction_event is None
or transaction_event["transaction_info"]["source"]
segment["attributes"]["sentry.segment.name.source"]
== test_config.expected_source
)

attrs = segment["attributes"]
assert attrs["http.request.method"] == "GET"
assert attrs["network.protocol.name"] == "http"
header_keys = {
key[len("http.request.header.") :]
for key in attrs
if key.startswith("http.request.header.")
}
assert header_keys >= {"accept", "accept-encoding", "host", "user-agent"}
assert attrs["http.response.status_code"] == test_config.expected_status
assert segment["status"] == (
"error" if test_config.expected_status >= 400 else "ok"
)

if send_pii:
assert attrs["url.full"].endswith(test_config.url)
assert attrs["url.path"] == test_config.url.split("?")[0]
if "?" in test_config.url:
assert attrs["http.query"] == test_config.url.split("?", 1)[1]

else:
assert "url.full" not in attrs
assert "url.path" not in attrs
assert "http.query" not in attrs


@pytest.mark.parametrize("span_streaming", [True, False])
def test_span_origin(sentry_init, app, capture_events, capture_items, span_streaming):
def test_span_origin(sentry_init, app, capture_items):
sentry_init(
integrations=[SanicIntegration()],
traces_sample_rate=1.0,
trace_lifecycle="stream" if span_streaming else "static",
trace_lifecycle="stream",
)

if span_streaming:
items = capture_items("span")
else:
events = capture_events()
items = capture_items("span")

c = get_client(app)
with c as client:
client.get("/message?foo=bar")

sentry_sdk.flush()

if span_streaming:
(segment,) = [
i.payload
for i in items
if i.payload["attributes"].get("sentry.origin") == "auto.http.sanic"
]
assert segment["attributes"]["sentry.origin"] == "auto.http.sanic"
else:
(_, event) = events
assert event["contexts"]["trace"]["origin"] == "auto.http.sanic"
(segment,) = [
i.payload
for i in items
if i.payload["attributes"].get("sentry.origin") == "auto.http.sanic"
]
assert segment["attributes"]["sentry.origin"] == "auto.http.sanic"


@pytest.mark.parametrize("init_kwargs, expect_ip", DATA_COLLECTION_USER_INFO_CASES)
Expand Down Expand Up @@ -714,16 +652,14 @@ def test_client_address_span_attribute_data_collection(
@pytest.mark.parametrize(
"init_kwargs, expected_query", _QUERY_PARAM_DATA_COLLECTION_CASES
)
def test_url_query_data_collection_span_streaming(
def test_url_query_data_collection(
sentry_init, app, capture_items, init_kwargs, expected_query
):
init_kwargs = dict(init_kwargs)
experiments = dict(init_kwargs.pop("_experiments", {}))
experiments["trace_lifecycle"] = "stream"
sentry_init(
integrations=[SanicIntegration()],
traces_sample_rate=1.0,
_experiments=experiments,
trace_lifecycle="stream",
**init_kwargs,
)

Expand All @@ -743,7 +679,9 @@ def test_url_query_data_collection_span_streaming(
and i.payload["is_segment"]
]

data_collection_enabled = "data_collection" in experiments
data_collection_enabled = "data_collection" in (
init_kwargs.get("_experiments") or {}
)
url_attrs_expected = data_collection_enabled or init_kwargs.get(
"send_default_pii", False
)
Expand Down
Loading