Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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
9 changes: 9 additions & 0 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -906,6 +906,8 @@ def __init__(
error_sampler=None, # type: Optional[Callable[[Event, Hint], Union[float, bool]]]
enable_db_query_source=True, # type: bool
db_query_source_threshold_ms=100, # type: int
enable_http_request_source=False, # type: bool
http_request_source_threshold_ms=100, # type: int
spotlight=None, # type: Optional[Union[bool, str]]
cert_file=None, # type: Optional[str]
key_file=None, # type: Optional[str]
Expand Down Expand Up @@ -1261,6 +1263,13 @@ def __init__(

The query location will be added to the query for queries slower than the specified threshold.

:param enable_http_request_source: When enabled, the source location will be added to outgoing HTTP requests.

:param http_request_source_threshold_ms: The threshold in milliseconds for adding the source location to an
outgoing HTTP request.

The request location will be added to the request for requests slower than the specified threshold.

:param custom_repr: A custom `repr <https://docs.python.org/3/library/functions.html#repr>`_ function to run
while serializing an object.

Expand Down
5 changes: 4 additions & 1 deletion sentry_sdk/integrations/aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
SOURCE_FOR_STYLE,
TransactionSource,
)
from sentry_sdk.tracing_utils import should_propagate_trace
from sentry_sdk.tracing_utils import should_propagate_trace, add_http_request_source
from sentry_sdk.utils import (
capture_internal_exceptions,
ensure_integration_enabled,
Expand Down Expand Up @@ -279,6 +279,9 @@ async def on_request_end(session, trace_config_ctx, params):
span.set_data("reason", params.response.reason)
span.finish()

with capture_internal_exceptions():
add_http_request_source(span)

trace_config = TraceConfig()

trace_config.on_request_start.append(on_request_start)
Expand Down
21 changes: 16 additions & 5 deletions sentry_sdk/integrations/httpx.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import sentry_sdk
from sentry_sdk import start_span
from sentry_sdk.consts import OP, SPANDATA
from sentry_sdk.integrations import Integration, DidNotEnable
from sentry_sdk.tracing import BAGGAGE_HEADER_NAME
from sentry_sdk.tracing_utils import Baggage, should_propagate_trace
from sentry_sdk.tracing_utils import (
Baggage,
should_propagate_trace,
add_http_request_source,
)
from sentry_sdk.utils import (
SENSITIVE_DATA_SUBSTITUTE,
capture_internal_exceptions,
Expand Down Expand Up @@ -52,7 +57,7 @@ def send(self, request, **kwargs):
with capture_internal_exceptions():
parsed_url = parse_url(str(request.url), sanitize=False)

with sentry_sdk.start_span(
with start_span(
op=OP.HTTP_CLIENT,
name="%s %s"
% (
Expand Down Expand Up @@ -88,7 +93,10 @@ def send(self, request, **kwargs):
span.set_http_status(rv.status_code)
span.set_data("reason", rv.reason_phrase)

return rv
with capture_internal_exceptions():
add_http_request_source(span)

return rv

Client.send = send

Expand All @@ -106,7 +114,7 @@ async def send(self, request, **kwargs):
with capture_internal_exceptions():
parsed_url = parse_url(str(request.url), sanitize=False)

with sentry_sdk.start_span(
with start_span(
op=OP.HTTP_CLIENT,
name="%s %s"
% (
Expand Down Expand Up @@ -144,7 +152,10 @@ async def send(self, request, **kwargs):
span.set_http_status(rv.status_code)
span.set_data("reason", rv.reason_phrase)

return rv
with capture_internal_exceptions():
add_http_request_source(span)

return rv

AsyncClient.send = send

Expand Down
9 changes: 8 additions & 1 deletion sentry_sdk/integrations/stdlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
from sentry_sdk.consts import OP, SPANDATA
from sentry_sdk.integrations import Integration
from sentry_sdk.scope import add_global_event_processor
from sentry_sdk.tracing_utils import EnvironHeaders, should_propagate_trace
from sentry_sdk.tracing_utils import (
EnvironHeaders,
should_propagate_trace,
add_http_request_source,
)
from sentry_sdk.utils import (
SENSITIVE_DATA_SUBSTITUTE,
capture_internal_exceptions,
Expand Down Expand Up @@ -135,6 +139,9 @@ def getresponse(self, *args, **kwargs):
finally:
span.finish()

with capture_internal_exceptions():
add_http_request_source(span)
Copy link

Choose a reason for hiding this comment

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

Bug: Span Data Overwriting After Completion

The add_http_request_source call is placed after span.finish() in the finally block. Adding source information to an already finished span means this data might not be properly recorded, as finished spans are generally immutable.

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I placed add_http_request_source(span) after the span is finished because you need the end timestamp to determine the delay in receiving a response to the HTTP request.

It's done analogously in asyncpg and sqlalchemy.

Copy link
Contributor

Choose a reason for hiding this comment

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

Fun fact: It'd be a concern in (P)OTel as finished OTel spans can't be modified, but Sentry spans can so all good.


return rv

HTTPConnection.putrequest = putrequest # type: ignore[method-assign]
Expand Down
88 changes: 64 additions & 24 deletions sentry_sdk/tracing_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,33 +218,11 @@ def _should_be_included(
)


def add_query_source(span):
# type: (sentry_sdk.tracing.Span) -> None
def add_source(span, project_root, in_app_include, in_app_exclude):
# type: (sentry_sdk.tracing.Span, Optional[str], Optional[list[str]], Optional[list[str]]) -> None
"""
Adds OTel compatible source code information to the span
"""
client = sentry_sdk.get_client()
if not client.is_active():
return

if span.timestamp is None or span.start_timestamp is None:
return

should_add_query_source = client.options.get("enable_db_query_source", True)
if not should_add_query_source:
return

duration = span.timestamp - span.start_timestamp
threshold = client.options.get("db_query_source_threshold_ms", 0)
slow_query = duration / timedelta(milliseconds=1) > threshold

if not slow_query:
return

project_root = client.options["project_root"]
in_app_include = client.options.get("in_app_include")
in_app_exclude = client.options.get("in_app_exclude")

# Find the correct frame
frame = sys._getframe() # type: Union[FrameType, None]
while frame is not None:
Expand Down Expand Up @@ -309,6 +287,68 @@ def add_query_source(span):
span.set_data(SPANDATA.CODE_FUNCTION, frame.f_code.co_name)


def add_query_source(span):
# type: (sentry_sdk.tracing.Span) -> None
"""
Adds OTel compatible source code information to a database query span
"""
client = sentry_sdk.get_client()
if not client.is_active():
return

if span.timestamp is None or span.start_timestamp is None:
return

should_add_query_source = client.options.get("enable_db_query_source", True)
if not should_add_query_source:
return

duration = span.timestamp - span.start_timestamp
threshold = client.options.get("db_query_source_threshold_ms", 0)
slow_query = duration / timedelta(milliseconds=1) > threshold

if not slow_query:
return

add_source(
span=span,
project_root=client.options["project_root"],
in_app_include=client.options.get("in_app_include"),
in_app_exclude=client.options.get("in_app_exclude"),
)


def add_http_request_source(span):
# type: (sentry_sdk.tracing.Span) -> None
"""
Adds OTel compatible source code information to a span for an outgoing HTTP request
"""
client = sentry_sdk.get_client()
if not client.is_active():
return

if span.timestamp is None or span.start_timestamp is None:
return

should_add_request_source = client.options.get("enable_http_request_source", False)
if not should_add_request_source:
return

duration = span.timestamp - span.start_timestamp
threshold = client.options.get("http_request_source_threshold_ms", 0)
slow_query = duration / timedelta(milliseconds=1) > threshold

if not slow_query:
return

add_source(
span=span,
project_root=client.options["project_root"],
in_app_include=client.options.get("in_app_include"),
in_app_exclude=client.options.get("in_app_exclude"),
)


def extract_sentrytrace_data(header):
# type: (Optional[str]) -> Optional[Dict[str, Union[str, bool, None]]]
"""
Expand Down
6 changes: 6 additions & 0 deletions tests/integrations/aiohttp/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import os
import sys
import pytest

pytest.importorskip("aiohttp")

# Load `aiohttp_helpers` into the module search path to test request source path names relative to module. See
# `test_request_source_with_module_in_search_path`
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
Empty file.
2 changes: 2 additions & 0 deletions tests/integrations/aiohttp/aiohttp_helpers/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
async def get_request_with_client(client, url):
await client.get(url)
Loading