Skip to content

Commit e589c53

Browse files
committed
Merge branch 'ivana/span-first-7-add-trace-decorator' into ivana/span-first-8-bucket-based-limits-in-batcher
2 parents 656ef2e + 9b1e2f3 commit e589c53

File tree

9 files changed

+82
-37
lines changed

9 files changed

+82
-37
lines changed

sentry_sdk/integrations/openai_agents/patches/models.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,6 @@ async def wrapped_get_response(*args: "Any", **kwargs: "Any") -> "Any":
133133

134134
@wraps(original_stream_response)
135135
async def wrapped_stream_response(*args: "Any", **kwargs: "Any") -> "Any":
136-
# Uses explicit try/finally instead of context manager to ensure cleanup
137-
# even if the consumer abandons the stream (GeneratorExit).
138136
span_kwargs = dict(kwargs)
139137
if len(args) > 0:
140138
span_kwargs["system_instructions"] = args[0]

sentry_sdk/scope.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
normalize_incoming_data,
3434
PropagationContext,
3535
)
36-
from sentry_sdk.traces import StreamedSpan, NoOpStreamedSpan
36+
from sentry_sdk.traces import _DEFAULT_PARENT_SPAN, StreamedSpan, NoOpStreamedSpan
3737
from sentry_sdk.tracing import (
3838
BAGGAGE_HEADER_NAME,
3939
SENTRY_TRACE_HEADER_NAME,
@@ -1177,9 +1177,9 @@ def start_span(
11771177
def start_streamed_span(
11781178
self,
11791179
name: str,
1180-
attributes: "Optional[Attributes]" = None,
1181-
parent_span: "Optional[StreamedSpan]" = None,
1182-
active: bool = True,
1180+
attributes: "Optional[Attributes]",
1181+
parent_span: "Optional[StreamedSpan]",
1182+
active: bool,
11831183
) -> "StreamedSpan":
11841184
# TODO: rename to start_span once we drop the old API
11851185
if isinstance(parent_span, NoOpStreamedSpan):
@@ -1189,7 +1189,9 @@ def start_streamed_span(
11891189
"currently active span instead."
11901190
)
11911191

1192-
if parent_span is None or isinstance(parent_span, NoOpStreamedSpan):
1192+
if parent_span is _DEFAULT_PARENT_SPAN or isinstance(
1193+
parent_span, NoOpStreamedSpan
1194+
):
11931195
parent_span = self.span # type: ignore
11941196

11951197
# If no eligible parent_span was provided and there is no currently

sentry_sdk/traces.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,24 @@ def __str__(self) -> str:
6262
}
6363

6464

65+
# Sentinel value for an unset parent_span to be able to distinguish it from
66+
# a None set by the user
67+
_DEFAULT_PARENT_SPAN = object()
68+
69+
6570
def start_span(
6671
name: str,
6772
attributes: "Optional[Attributes]" = None,
68-
parent_span: "Optional[StreamedSpan]" = None,
73+
parent_span: "Optional[StreamedSpan]" = _DEFAULT_PARENT_SPAN, # type: ignore[assignment]
6974
active: bool = True,
7075
) -> "StreamedSpan":
7176
"""
7277
Start a span.
7378
7479
The span's parent, unless provided explicitly via the `parent_span` argument,
7580
will be the current active span, if any. If there is none, this span will
76-
become the root of a new span tree.
81+
become the root of a new span tree. If you explicitly want this span to be
82+
top-level without a parent, set `parent_span=None`.
7783
7884
`start_span()` can either be used as context manager or you can use the span
7985
object it returns and explicitly end it via `span.end()`. The following is
@@ -108,7 +114,8 @@ def start_span(
108114
109115
:param parent_span: A span instance that the new span should consider its
110116
parent. If not provided, the parent will be set to the currently active
111-
span, if any.
117+
span, if any. If set to `None`, this span will become a new root-level
118+
span.
112119
:type parent_span: "Optional[StreamedSpan]"
113120
114121
:param active: Controls whether spans started while this span is running

sentry_sdk/tracing_utils.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -950,7 +950,6 @@ def create_streaming_span_decorator(
950950
"""
951951
Create a span creating decorator that can wrap both sync and async functions.
952952
"""
953-
from sentry_sdk.scope import should_send_default_pii
954953

955954
def span_decorator(f: "Any") -> "Any":
956955
"""

sentry_sdk/utils.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2005,7 +2005,11 @@ def serialize_item(
20052005

20062006
try:
20072007
serialized = serialize_item(data)
2008-
return json.dumps(serialized, default=str)
2008+
return (
2009+
json.dumps(serialized, default=str)
2010+
if not isinstance(serialized, str)
2011+
else serialized
2012+
)
20092013
except Exception:
20102014
return str(data)
20112015

tests/integrations/google_genai/test_google_genai.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1625,7 +1625,7 @@ def test_generate_content_with_function_response(
16251625
assert messages[0]["role"] == "tool"
16261626
assert messages[0]["content"]["toolCallId"] == "call_123"
16271627
assert messages[0]["content"]["toolName"] == "get_weather"
1628-
assert messages[0]["content"]["output"] == '"Sunny, 72F"'
1628+
assert messages[0]["content"]["output"] == "Sunny, 72F"
16291629

16301630

16311631
def test_generate_content_with_mixed_string_and_content(
@@ -1891,7 +1891,7 @@ def test_extract_contents_messages_function_response():
18911891
assert result[0]["role"] == "tool"
18921892
assert result[0]["content"]["toolCallId"] == "call_123"
18931893
assert result[0]["content"]["toolName"] == "get_weather"
1894-
assert result[0]["content"]["output"] == '"sunny"'
1894+
assert result[0]["content"]["output"] == "sunny"
18951895

18961896

18971897
def test_extract_contents_messages_function_response_with_output_key():
@@ -1908,7 +1908,7 @@ def test_extract_contents_messages_function_response_with_output_key():
19081908
assert result[0]["content"]["toolCallId"] == "call_456"
19091909
assert result[0]["content"]["toolName"] == "get_time"
19101910
# Should prefer "output" key
1911-
assert result[0]["content"]["output"] == '"3:00 PM"'
1911+
assert result[0]["content"]["output"] == "3:00 PM"
19121912

19131913

19141914
def test_extract_contents_messages_mixed_parts():

tests/integrations/mcp/test_mcp.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -241,15 +241,12 @@ async def test_tool_async(tool_name, arguments):
241241
assert span["data"][SPANDATA.MCP_TRANSPORT] == "http"
242242
assert span["data"][SPANDATA.MCP_REQUEST_ID] == "req-456"
243243
assert span["data"][SPANDATA.MCP_SESSION_ID] == session_id
244-
assert span["data"]["mcp.request.argument.data"] == '"test"'
244+
assert span["data"]["mcp.request.argument.data"] == "test"
245245

246246
# Check PII-sensitive data
247247
if send_default_pii and include_prompts:
248-
# TODO: Investigate why tool result is double-serialized.
249248
assert span["data"][SPANDATA.MCP_TOOL_RESULT_CONTENT] == json.dumps(
250-
json.dumps(
251-
{"status": "completed"},
252-
)
249+
{"status": "completed"}
253250
)
254251
else:
255252
assert SPANDATA.MCP_TOOL_RESULT_CONTENT not in span["data"]
@@ -366,8 +363,8 @@ async def test_prompt(name, arguments):
366363
assert span["data"][SPANDATA.MCP_METHOD_NAME] == "prompts/get"
367364
assert span["data"][SPANDATA.MCP_TRANSPORT] == "stdio"
368365
assert span["data"][SPANDATA.MCP_REQUEST_ID] == "req-prompt"
369-
assert span["data"]["mcp.request.argument.name"] == '"code_help"'
370-
assert span["data"]["mcp.request.argument.language"] == '"python"'
366+
assert span["data"]["mcp.request.argument.name"] == "code_help"
367+
assert span["data"]["mcp.request.argument.language"] == "python"
371368

372369
# Message count is always captured
373370
assert span["data"][SPANDATA.MCP_PROMPT_RESULT_MESSAGE_COUNT] == 1
@@ -752,7 +749,7 @@ def test_tool_unstructured(tool_name, arguments):
752749
# Should extract and join text from content blocks only with PII
753750
if send_default_pii and include_prompts:
754751
assert (
755-
span["data"][SPANDATA.MCP_TOOL_RESULT_CONTENT] == '"First part Second part"'
752+
span["data"][SPANDATA.MCP_TOOL_RESULT_CONTENT] == "First part Second part"
756753
)
757754
else:
758755
assert SPANDATA.MCP_TOOL_RESULT_CONTENT not in span["data"]
@@ -959,7 +956,7 @@ def test_tool_complex(tool_name, arguments):
959956
assert span["data"]["mcp.request.argument.nested"] == json.dumps(
960957
{"key": "value", "list": [1, 2, 3]}
961958
)
962-
assert span["data"]["mcp.request.argument.string"] == '"test"'
959+
assert span["data"]["mcp.request.argument.string"] == "test"
963960
assert span["data"]["mcp.request.argument.number"] == "42"
964961

965962

tests/integrations/openai_agents/test_openai_agents.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from sentry_sdk.integrations.logging import LoggingIntegration
1313
from sentry_sdk.integrations.openai_agents import OpenAIAgentsIntegration
1414
from sentry_sdk.integrations.openai_agents.utils import _set_input_data, safe_serialize
15-
from sentry_sdk.utils import parse_version
15+
from sentry_sdk.utils import parse_version, package_version
1616

1717
from openai import AsyncOpenAI
1818
from agents.models.openai_responses import OpenAIResponsesModel
@@ -37,6 +37,8 @@
3737
from agents.exceptions import MaxTurnsExceeded, ModelBehaviorError
3838
from agents.version import __version__ as OPENAI_AGENTS_VERSION
3939

40+
OPENAI_VERSION = package_version("openai")
41+
4042
from openai.types.responses import (
4143
ResponseCreatedEvent,
4244
ResponseTextDeltaEvent,
@@ -1256,18 +1258,22 @@ def simple_test_tool(message: str) -> str:
12561258
assert ai_client_span1["data"]["gen_ai.usage.output_tokens"] == 5
12571259
assert ai_client_span1["data"]["gen_ai.usage.output_tokens.reasoning"] == 0
12581260
assert ai_client_span1["data"]["gen_ai.usage.total_tokens"] == 15
1259-
assert ai_client_span1["data"]["gen_ai.response.tool_calls"] == safe_serialize(
1260-
[
1261-
{
1262-
"arguments": '{"message": "hello"}',
1263-
"call_id": "call_123",
1264-
"name": "simple_test_tool",
1265-
"type": "function_call",
1266-
"id": "call_123",
1267-
"status": None,
1268-
}
1269-
]
1270-
)
1261+
1262+
tool_call = {
1263+
"arguments": '{"message": "hello"}',
1264+
"call_id": "call_123",
1265+
"name": "simple_test_tool",
1266+
"type": "function_call",
1267+
"id": "call_123",
1268+
"status": None,
1269+
}
1270+
1271+
if OPENAI_VERSION >= (2, 25, 0):
1272+
tool_call["namespace"] = None
1273+
1274+
assert json.loads(ai_client_span1["data"]["gen_ai.response.tool_calls"]) == [
1275+
tool_call
1276+
]
12711277

12721278
assert tool_span["description"] == "execute_tool simple_test_tool"
12731279
assert tool_span["data"]["gen_ai.agent.name"] == "test_agent"

tests/test_utils.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
exc_info_from_error,
3636
get_lines_from_file,
3737
package_version,
38+
safe_serialize,
3839
)
3940

4041

@@ -1062,5 +1063,36 @@ def fake_getlines(filename):
10621063
assert result == expected_result
10631064

10641065

1066+
def test_safe_serialize_plain_string():
1067+
assert safe_serialize("already a string") == "already a string"
1068+
1069+
1070+
def test_safe_serialize_json_string():
1071+
assert safe_serialize('{"key": "value"}') == '{"key": "value"}'
1072+
1073+
1074+
def test_safe_serialize_dict():
1075+
assert safe_serialize({"key": "value"}) == '{"key": "value"}'
1076+
1077+
1078+
def test_safe_serialize_callable():
1079+
def my_func():
1080+
pass
1081+
1082+
result = safe_serialize(my_func)
1083+
assert result.startswith("<function")
1084+
assert '"' not in result[:1] # no wrapping quotes from json.dumps
1085+
1086+
1087+
def test_safe_serialize_object():
1088+
class MyClass:
1089+
def __init__(self):
1090+
self.x = 1
1091+
1092+
result = safe_serialize(MyClass())
1093+
assert result.startswith("<MyClass")
1094+
assert '"' not in result[:1] # no wrapping quotes from json.dumps
1095+
1096+
10651097
def test_package_version_is_none():
10661098
assert package_version("non_existent_package") is None

0 commit comments

Comments
 (0)