Skip to content

Commit c614249

Browse files
committed
Merge branch 'ivana/span-first-12-is-segment' into ivana/span-first-13-tests
2 parents 2448092 + 989447d commit c614249

File tree

16 files changed

+351
-231
lines changed

16 files changed

+351
-231
lines changed

.github/workflows/ai-integration-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
token: ${{ secrets.GITHUB_TOKEN }}
3535

3636
- name: Run Python SDK Tests
37-
uses: getsentry/testing-ai-sdk-integrations@121da677853244cedfe11e95184b2b431af102eb
37+
uses: getsentry/testing-ai-sdk-integrations@285c012e522f241581534dfc89bd99ec3b1da4f6
3838
env:
3939
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4040
with:

scripts/build_aws_lambda_layer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class LayerBuilder:
1818
def __init__(
1919
self,
2020
base_dir: str,
21-
out_zip_filename: "Optional[str]"=None,
21+
out_zip_filename: "Optional[str]" = None,
2222
) -> None:
2323
self.base_dir = base_dir
2424
self.python_site_packages = os.path.join(self.base_dir, PYTHON_SITE_PACKAGES)

scripts/populate_tox/package_dependencies.jsonl

Lines changed: 17 additions & 16 deletions
Large diffs are not rendered by default.

scripts/populate_tox/populate_tox.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
See scripts/populate_tox/README.md for more info.
55
"""
66

7+
import re
78
import functools
89
import hashlib
910
import json
@@ -872,7 +873,10 @@ def get_last_updated() -> Optional[datetime]:
872873

873874

874875
def _normalize_name(package: str) -> str:
875-
return package.lower().replace("-", "_")
876+
# From https://peps.python.org/pep-0503/#normalized-names
877+
# but normalizing to underscores instead of hyphens since tox-formatted packages
878+
# use underscores.
879+
return re.sub(r"[-_.]+", "_", package).lower()
876880

877881

878882
def _extract_wheel_info_to_cache(wheel: dict):

scripts/populate_tox/releases.jsonl

Lines changed: 44 additions & 43 deletions
Large diffs are not rendered by default.

sentry_sdk/_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,7 @@ class SDKInfo(TypedDict):
351351
"max_runtime": int,
352352
"failure_issue_threshold": int,
353353
"recovery_threshold": int,
354+
"owner": str,
354355
},
355356
total=False,
356357
)

sentry_sdk/integrations/httpx.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424

2525
try:
26-
from httpx import AsyncClient, Client, Request, Response # type: ignore
26+
from httpx import AsyncClient, Client, Request, Response
2727
except ImportError:
2828
raise DidNotEnable("httpx is not installed")
2929

@@ -94,7 +94,7 @@ def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response":
9494

9595
return rv
9696

97-
Client.send = send
97+
Client.send = send # type: ignore
9898

9999

100100
def _install_httpx_async_client() -> None:
@@ -150,4 +150,4 @@ async def send(
150150

151151
return rv
152152

153-
AsyncClient.send = send
153+
AsyncClient.send = send # type: ignore

sentry_sdk/integrations/pydantic_ai/patches/tools.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ async def wrapped_execute_tool_call(
5050
call = validated.call
5151
name = call.tool_name
5252
tool = self.tools.get(name) if self.tools else None
53+
selected_tool_definition = getattr(tool, "tool_def", None)
5354

5455
# Determine tool type by checking tool.toolset
5556
tool_type = "function"
@@ -73,6 +74,7 @@ async def wrapped_execute_tool_call(
7374
args_dict,
7475
agent,
7576
tool_type=tool_type,
77+
tool_definition=selected_tool_definition,
7678
) as span:
7779
try:
7880
result = await original_execute_tool_call(
@@ -127,6 +129,7 @@ async def wrapped_call_tool(
127129
# Extract tool info before calling original
128130
name = call.tool_name
129131
tool = self.tools.get(name) if self.tools else None
132+
selected_tool_definition = getattr(tool, "tool_def", None)
130133

131134
# Determine tool type by checking tool.toolset
132135
tool_type = "function" # default
@@ -150,6 +153,7 @@ async def wrapped_call_tool(
150153
args_dict,
151154
agent,
152155
tool_type=tool_type,
156+
tool_definition=selected_tool_definition,
153157
) as span:
154158
try:
155159
result = await original_call_tool(

sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,15 @@
99

1010
if TYPE_CHECKING:
1111
from typing import Any, Optional
12+
from pydantic_ai._tool_manager import ToolDefinition # type: ignore
1213

1314

1415
def execute_tool_span(
15-
tool_name: str, tool_args: "Any", agent: "Any", tool_type: str = "function"
16+
tool_name: str,
17+
tool_args: "Any",
18+
agent: "Any",
19+
tool_type: str = "function",
20+
tool_definition: "Optional[ToolDefinition]" = None,
1621
) -> "sentry_sdk.tracing.Span":
1722
"""Create a span for tool execution.
1823
@@ -21,6 +26,7 @@ def execute_tool_span(
2126
tool_args: The arguments passed to the tool
2227
agent: The agent executing the tool
2328
tool_type: The type of tool ("function" for regular tools, "mcp" for MCP services)
29+
tool_definition: The definition of the tool, if available
2430
"""
2531
span = sentry_sdk.start_span(
2632
op=OP.GEN_AI_EXECUTE_TOOL,
@@ -32,6 +38,12 @@ def execute_tool_span(
3238
span.set_data(SPANDATA.GEN_AI_TOOL_TYPE, tool_type)
3339
span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool_name)
3440

41+
if tool_definition is not None and hasattr(tool_definition, "description"):
42+
span.set_data(
43+
SPANDATA.GEN_AI_TOOL_DESCRIPTION,
44+
tool_definition.description,
45+
)
46+
3547
_set_agent_data(span, agent)
3648

3749
if _should_send_prompts() and tool_args is not None:

sentry_sdk/traces.py

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -294,8 +294,12 @@ def __enter__(self) -> "StreamedSpan":
294294
def __exit__(
295295
self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]"
296296
) -> None:
297+
if self._timestamp is not None:
298+
# This span is already finished, ignore
299+
return
300+
297301
if value is not None and should_be_treated_as_error(ty, value):
298-
self.status = SpanStatus.ERROR
302+
self.status = SpanStatus.ERROR.value
299303

300304
self._end()
301305

@@ -335,7 +339,9 @@ def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None
335339
del self._previous_span_on_scope
336340
self._scope.span = old_span
337341

338-
# Set attributes from the segment
342+
# Set attributes from the segment. These are set on span end on purpose
343+
# so that we have the best chance to capture the segment's final name
344+
# (since it might change during its lifetime)
339345
self.set_attribute("sentry.segment.id", self._segment.span_id)
340346
self.set_attribute("sentry.segment.name", self._segment.name)
341347

@@ -440,11 +446,14 @@ def timestamp(self) -> "Optional[datetime]":
440446
return self._timestamp
441447

442448
def _is_segment(self) -> bool:
443-
return self._segment == self
449+
return self._segment is self
444450

445451

446452
class NoOpStreamedSpan(StreamedSpan):
447-
__slots__ = ("_unsampled_reason",)
453+
__slots__ = (
454+
"_finished",
455+
"_unsampled_reason",
456+
)
448457

449458
def __init__(
450459
self,
@@ -454,6 +463,8 @@ def __init__(
454463
self._scope = scope # type: ignore[assignment]
455464
self._unsampled_reason = unsampled_reason
456465

466+
self._finished = False
467+
457468
self._start()
458469

459470
def __repr__(self) -> str:
@@ -476,22 +487,28 @@ def _start(self) -> None:
476487
self._previous_span_on_scope = old_span
477488

478489
def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None:
479-
client = sentry_sdk.get_client()
480-
if client.is_active() and client.transport:
481-
logger.debug("Discarding span because sampled = False")
482-
client.transport.record_lost_event(
483-
reason=self._unsampled_reason or "sample_rate",
484-
data_category="span",
485-
quantity=1,
486-
)
487-
488-
if self._scope is None or not hasattr(self, "_previous_span_on_scope"):
490+
if self._finished:
489491
return
490492

491-
with capture_internal_exceptions():
492-
old_span = self._previous_span_on_scope
493-
del self._previous_span_on_scope
494-
self._scope.span = old_span
493+
if self._unsampled_reason is not None:
494+
client = sentry_sdk.get_client()
495+
if client.is_active() and client.transport:
496+
logger.debug(
497+
f"Discarding span because sampled=False (reason: {self._unsampled_reason})"
498+
)
499+
client.transport.record_lost_event(
500+
reason=self._unsampled_reason,
501+
data_category="span",
502+
quantity=1,
503+
)
504+
505+
if self._scope and hasattr(self, "_previous_span_on_scope"):
506+
with capture_internal_exceptions():
507+
old_span = self._previous_span_on_scope
508+
del self._previous_span_on_scope
509+
self._scope.span = old_span
510+
511+
self._finished = True
495512

496513
def end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None:
497514
self._end()
@@ -518,7 +535,7 @@ def remove_attribute(self, key: str) -> None:
518535
pass
519536

520537
def _is_segment(self) -> bool:
521-
return True if self._scope is not None else False
538+
return self._scope is not None
522539

523540
@property
524541
def status(self) -> "str":

0 commit comments

Comments
 (0)