Skip to content

Commit 2804959

Browse files
merge master
2 parents 888806b + 53188b3 commit 2804959

9 files changed

Lines changed: 413 additions & 667 deletions

File tree

.claude/settings.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,15 @@
3030
"Bash(mv:*)",
3131
"Bash(source .venv/bin/activate)",
3232
"Bash(source tox.venv/bin/activate:*)",
33+
"Bash(source .tox/*/bin/activate:*)",
3334
"Bash(tox:*)",
3435
"Bash(tox.venv/bin/tox:*)",
3536
"Bash(.tox/*/bin/python:*)",
3637
"Bash(.tox/*/bin/pytest:*)",
37-
"Bash(.tox/*/bin/ruff:*)"
38+
"Bash(.tox/*/bin/ruff:*)",
39+
"Bash(ruff format:*)",
40+
"Bash(ruff check:*)",
41+
"Bash(mypy:*)"
3842
],
3943
"deny": []
4044
}

sentry_sdk/integrations/huey.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@
3434
except ImportError:
3535
raise DidNotEnable("Huey is not installed")
3636

37+
try:
38+
from huey.api import chord as HueyChord
39+
from huey.api import group as HueyGroup
40+
except ImportError:
41+
HueyChord = None
42+
HueyGroup = None
43+
3744

3845
HUEY_CONTROL_FLOW_EXCEPTIONS = (CancelExecution, RetryTask, TaskLockedException)
3946

@@ -53,22 +60,37 @@ def patch_enqueue() -> None:
5360

5461
@ensure_integration_enabled(HueyIntegration, old_enqueue)
5562
def _sentry_enqueue(
56-
self: "Huey", task: "Task"
63+
self: "Huey", item: "Union[Task, HueyGroup, HueyChord]"
5764
) -> "Optional[Union[Result, ResultGroup]]":
65+
if HueyChord is not None and isinstance(item, HueyChord):
66+
span_name = "Huey Chord"
67+
elif HueyGroup is not None and isinstance(item, HueyGroup):
68+
span_name = "Huey Task Group"
69+
else:
70+
span_name = item.name
71+
5872
with sentry_sdk.start_span(
5973
op=OP.QUEUE_SUBMIT_HUEY,
60-
name=task.name,
74+
name=span_name,
6175
origin=HueyIntegration.origin,
6276
):
63-
if not isinstance(task, PeriodicTask):
77+
if (
78+
not isinstance(item, PeriodicTask)
79+
and not (HueyGroup is not None and isinstance(item, HueyGroup))
80+
and not (HueyChord is not None and isinstance(item, HueyChord))
81+
):
6482
# Attach trace propagation data to task kwargs. We do
6583
# not do this for periodic tasks, as these don't
6684
# really have an originating transaction.
67-
task.kwargs["sentry_headers"] = {
85+
# Additionally, we do not do this for Huey groups or chords, as enqueue will
86+
# recursively call this method for each task within the list, resulting
87+
# in the trace propagation data being attached to each task individually
88+
# (which we want)
89+
item.kwargs["sentry_headers"] = {
6890
BAGGAGE_HEADER_NAME: get_baggage(),
6991
SENTRY_TRACE_HEADER_NAME: get_traceparent(),
7092
}
71-
return old_enqueue(self, task)
93+
return old_enqueue(self, item)
7294

7395
Huey.enqueue = _sentry_enqueue
7496

sentry_sdk/integrations/openai_agents/spans/ai_client.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
from ..consts import SPAN_ORIGIN
77
from ..utils import (
8-
_create_mcp_execute_tool_spans,
98
_set_agent_data,
109
_set_input_data,
1110
_set_output_data,
@@ -55,7 +54,6 @@ def update_ai_client_span(
5554

5655
if hasattr(response, "output") and response.output:
5756
_set_output_data(span, response)
58-
_create_mcp_execute_tool_spans(span, response)
5957

6058
if response_model is not None:
6159
span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model)

sentry_sdk/integrations/openai_agents/utils.py

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
set_data_normalized,
1515
truncate_and_annotate_messages,
1616
)
17-
from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS
17+
from sentry_sdk.consts import SPANDATA
1818
from sentry_sdk.integrations import DidNotEnable
1919
from sentry_sdk.scope import should_send_default_pii
2020
from sentry_sdk.utils import event_from_exception, safe_serialize
@@ -215,25 +215,3 @@ def _set_output_data(span: "sentry_sdk.tracing.Span", result: "Any") -> None:
215215
set_data_normalized(
216216
span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"]
217217
)
218-
219-
220-
def _create_mcp_execute_tool_spans(
221-
span: "sentry_sdk.tracing.Span", result: "agents.Result"
222-
) -> None:
223-
for output in result.output:
224-
if output.__class__.__name__ == "McpCall":
225-
with sentry_sdk.start_span(
226-
op=OP.GEN_AI_EXECUTE_TOOL,
227-
name=f"execute_tool {output.name}",
228-
start_timestamp=span.start_timestamp,
229-
) as execute_tool_span:
230-
execute_tool_span.set_data(SPANDATA.GEN_AI_TOOL_NAME, output.name)
231-
if should_send_default_pii():
232-
execute_tool_span.set_data(
233-
SPANDATA.GEN_AI_TOOL_INPUT, output.arguments
234-
)
235-
execute_tool_span.set_data(
236-
SPANDATA.GEN_AI_TOOL_OUTPUT, output.output
237-
)
238-
if output.error:
239-
execute_tool_span.set_status(SPANSTATUS.INTERNAL_ERROR)

sentry_sdk/integrations/sanic.py

Lines changed: 83 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@
66

77
import sentry_sdk
88
from sentry_sdk import continue_trace
9-
from sentry_sdk.consts import OP
9+
from sentry_sdk.consts import OP, SPANDATA
1010
from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version
1111
from sentry_sdk.integrations._wsgi_common import RequestExtractor, _filter_headers
1212
from sentry_sdk.integrations.logging import ignore_logger
13+
from sentry_sdk.scope import should_send_default_pii
14+
from sentry_sdk.traces import SegmentSource, StreamedSpan
1315
from sentry_sdk.tracing import TransactionSource
16+
from sentry_sdk.tracing_utils import has_span_streaming_enabled
1417
from sentry_sdk.utils import (
1518
CONTEXTVARS_ERROR_MESSAGE,
1619
HAS_REAL_CONTEXTVARS,
@@ -165,23 +168,43 @@ async def _context_enter(request: "Request") -> None:
165168
if not request.ctx._sentry_do_integration:
166169
return
167170

171+
client = sentry_sdk.get_client()
172+
is_span_streaming_enabled = has_span_streaming_enabled(client.options)
173+
168174
weak_request = weakref.ref(request)
169175
request.ctx._sentry_scope = sentry_sdk.isolation_scope()
170176
scope = request.ctx._sentry_scope.__enter__()
171177
scope.clear_breadcrumbs()
172178
scope.add_event_processor(_make_request_processor(weak_request))
173179

174-
transaction = continue_trace(
175-
dict(request.headers),
176-
op=OP.HTTP_SERVER,
177-
# Unless the request results in a 404 error, the name and source will get overwritten in _set_transaction
178-
name=request.path,
179-
source=TransactionSource.URL,
180-
origin=SanicIntegration.origin,
181-
)
182-
request.ctx._sentry_transaction = sentry_sdk.start_transaction(
183-
transaction
184-
).__enter__()
180+
if is_span_streaming_enabled:
181+
sentry_sdk.traces.continue_trace(dict(request.headers))
182+
scope.set_custom_sampling_context({"sanic_request": request})
183+
184+
span = sentry_sdk.traces.start_span(
185+
# Unless the request results in a 404 error, the name and source
186+
# will get overwritten in _set_transaction
187+
name=request.path,
188+
attributes={
189+
"sentry.op": OP.HTTP_SERVER,
190+
"sentry.origin": SanicIntegration.origin,
191+
"sentry.span.source": SegmentSource.URL.value,
192+
},
193+
parent_span=None,
194+
)
195+
request.ctx._sentry_root_span = span
196+
else:
197+
transaction = continue_trace(
198+
dict(request.headers),
199+
op=OP.HTTP_SERVER,
200+
# Unless the request results in a 404 error, the name and source will get overwritten in _set_transaction
201+
name=request.path,
202+
source=TransactionSource.URL,
203+
origin=SanicIntegration.origin,
204+
)
205+
request.ctx._sentry_root_span = sentry_sdk.start_transaction(
206+
transaction
207+
).__enter__()
185208

186209

187210
async def _context_exit(
@@ -198,12 +221,24 @@ async def _context_exit(
198221
# This capture_internal_exceptions block has been intentionally nested here, so that in case an exception
199222
# happens while trying to end the transaction, we still attempt to exit the hub.
200223
with capture_internal_exceptions():
201-
request.ctx._sentry_transaction.set_http_status(response_status)
202-
request.ctx._sentry_transaction.sampled &= (
203-
isinstance(integration, SanicIntegration)
204-
and response_status not in integration._unsampled_statuses
205-
)
206-
request.ctx._sentry_transaction.__exit__(None, None, None)
224+
span = request.ctx._sentry_root_span
225+
if isinstance(span, StreamedSpan):
226+
with capture_internal_exceptions():
227+
for attr, value in _get_request_attributes(request).items():
228+
span.set_attribute(attr, value)
229+
if response_status is not None:
230+
span.set_attribute(SPANDATA.HTTP_STATUS_CODE, response_status)
231+
span.status = "error" if response_status >= 400 else "ok"
232+
233+
span.end()
234+
else:
235+
span.set_http_status(response_status)
236+
span.sampled &= (
237+
isinstance(integration, SanicIntegration)
238+
and response_status not in integration._unsampled_statuses
239+
)
240+
241+
span.__exit__(None, None, None)
207242

208243
request.ctx._sentry_scope.__exit__(None, None, None)
209244

@@ -315,6 +350,36 @@ def _capture_exception(exception: "Union[ExcInfo, BaseException]") -> None:
315350
sentry_sdk.capture_event(event, hint=hint)
316351

317352

353+
def _get_request_attributes(request: "Request") -> "Dict[str, Any]":
354+
"""
355+
Return span attributes related to the HTTP request from a Sanic request.
356+
"""
357+
attributes = {} # type: Dict[str, Any]
358+
359+
if request.method:
360+
attributes[SPANDATA.HTTP_REQUEST_METHOD] = request.method.upper()
361+
362+
headers = _filter_headers(dict(request.headers), use_annotated_value=False)
363+
for header, value in headers.items():
364+
attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = value
365+
366+
urlparts = urlsplit(request.url)
367+
368+
if urlparts.query:
369+
attributes[SPANDATA.HTTP_QUERY] = urlparts.query
370+
371+
attributes[SPANDATA.URL_FULL] = request.url
372+
373+
if urlparts.scheme:
374+
attributes[SPANDATA.NETWORK_PROTOCOL_NAME] = urlparts.scheme
375+
376+
if should_send_default_pii() and request.remote_addr:
377+
attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_addr
378+
attributes[SPANDATA.USER_IP_ADDRESS] = request.remote_addr
379+
380+
return attributes
381+
382+
318383
def _make_request_processor(weak_request: "Callable[[], Request]") -> "EventProcessor":
319384
def sanic_processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]":
320385
try:

0 commit comments

Comments
 (0)