From 5937e9c401a13f552e67608f520555822ed98f90 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Wed, 4 Mar 2026 14:23:01 -0500 Subject: [PATCH 1/3] fix(utils): Avoid double serialization of strings in safe_serialize When serialize_item() already returns a string (for plain strings, callables, or objects with __dict__), skip json.dumps to prevent wrapping the value in extra quotes with escaped characters. Co-Authored-By: Claude --- sentry_sdk/utils.py | 6 +++++- tests/test_utils.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 60edafd2d1..a333467ae9 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -2005,7 +2005,11 @@ def serialize_item( try: serialized = serialize_item(data) - return json.dumps(serialized, default=str) + return ( + json.dumps(serialized, default=str) + if not isinstance(serialized, str) + else serialized + ) except Exception: return str(data) diff --git a/tests/test_utils.py b/tests/test_utils.py index d75e514212..1fc651f805 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -35,6 +35,7 @@ exc_info_from_error, get_lines_from_file, package_version, + safe_serialize, ) @@ -1062,5 +1063,36 @@ def fake_getlines(filename): assert result == expected_result +def test_safe_serialize_plain_string(): + assert safe_serialize("already a string") == "already a string" + + +def test_safe_serialize_json_string(): + assert safe_serialize('{"key": "value"}') == '{"key": "value"}' + + +def test_safe_serialize_dict(): + assert safe_serialize({"key": "value"}) == '{"key": "value"}' + + +def test_safe_serialize_callable(): + def my_func(): + pass + + result = safe_serialize(my_func) + assert result.startswith(" Date: Wed, 4 Mar 2026 14:53:28 -0500 Subject: [PATCH 2/3] test(google_genai): Update expected values after safe_serialize fix Remove wrapping double quotes from expected tool output strings in google_genai tests. The safe_serialize fix (5937e9c4) now correctly returns plain strings without passing them through json.dumps. Co-Authored-By: Claude Opus 4.6 --- tests/integrations/google_genai/test_google_genai.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integrations/google_genai/test_google_genai.py b/tests/integrations/google_genai/test_google_genai.py index fc21216be6..68ae9d234f 100644 --- a/tests/integrations/google_genai/test_google_genai.py +++ b/tests/integrations/google_genai/test_google_genai.py @@ -1625,7 +1625,7 @@ def test_generate_content_with_function_response( assert messages[0]["role"] == "tool" assert messages[0]["content"]["toolCallId"] == "call_123" assert messages[0]["content"]["toolName"] == "get_weather" - assert messages[0]["content"]["output"] == '"Sunny, 72F"' + assert messages[0]["content"]["output"] == "Sunny, 72F" def test_generate_content_with_mixed_string_and_content( @@ -1891,7 +1891,7 @@ def test_extract_contents_messages_function_response(): assert result[0]["role"] == "tool" assert result[0]["content"]["toolCallId"] == "call_123" assert result[0]["content"]["toolName"] == "get_weather" - assert result[0]["content"]["output"] == '"sunny"' + assert result[0]["content"]["output"] == "sunny" def test_extract_contents_messages_function_response_with_output_key(): @@ -1908,7 +1908,7 @@ def test_extract_contents_messages_function_response_with_output_key(): assert result[0]["content"]["toolCallId"] == "call_456" assert result[0]["content"]["toolName"] == "get_time" # Should prefer "output" key - assert result[0]["content"]["output"] == '"3:00 PM"' + assert result[0]["content"]["output"] == "3:00 PM" def test_extract_contents_messages_mixed_parts(): From b9f312cb2826a9c41e3db226a6f071fee575b6e8 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Wed, 4 Mar 2026 15:05:50 -0500 Subject: [PATCH 3/3] test(mcp): Update expected values after safe_serialize fix Remove extra quoting from string argument assertions and fix double-serialized tool result expectations. The safe_serialize fix in 5937e9c4 eliminated double serialization of strings, so test expectations need to match the new single-serialized output. Co-Authored-By: Claude --- tests/integrations/mcp/test_mcp.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/tests/integrations/mcp/test_mcp.py b/tests/integrations/mcp/test_mcp.py index 37d07e8ce4..f51d0491ae 100644 --- a/tests/integrations/mcp/test_mcp.py +++ b/tests/integrations/mcp/test_mcp.py @@ -241,15 +241,12 @@ async def test_tool_async(tool_name, arguments): assert span["data"][SPANDATA.MCP_TRANSPORT] == "http" assert span["data"][SPANDATA.MCP_REQUEST_ID] == "req-456" assert span["data"][SPANDATA.MCP_SESSION_ID] == session_id - assert span["data"]["mcp.request.argument.data"] == '"test"' + assert span["data"]["mcp.request.argument.data"] == "test" # Check PII-sensitive data if send_default_pii and include_prompts: - # TODO: Investigate why tool result is double-serialized. assert span["data"][SPANDATA.MCP_TOOL_RESULT_CONTENT] == json.dumps( - json.dumps( - {"status": "completed"}, - ) + {"status": "completed"} ) else: assert SPANDATA.MCP_TOOL_RESULT_CONTENT not in span["data"] @@ -366,8 +363,8 @@ async def test_prompt(name, arguments): assert span["data"][SPANDATA.MCP_METHOD_NAME] == "prompts/get" assert span["data"][SPANDATA.MCP_TRANSPORT] == "stdio" assert span["data"][SPANDATA.MCP_REQUEST_ID] == "req-prompt" - assert span["data"]["mcp.request.argument.name"] == '"code_help"' - assert span["data"]["mcp.request.argument.language"] == '"python"' + assert span["data"]["mcp.request.argument.name"] == "code_help" + assert span["data"]["mcp.request.argument.language"] == "python" # Message count is always captured assert span["data"][SPANDATA.MCP_PROMPT_RESULT_MESSAGE_COUNT] == 1 @@ -752,7 +749,7 @@ def test_tool_unstructured(tool_name, arguments): # Should extract and join text from content blocks only with PII if send_default_pii and include_prompts: assert ( - span["data"][SPANDATA.MCP_TOOL_RESULT_CONTENT] == '"First part Second part"' + span["data"][SPANDATA.MCP_TOOL_RESULT_CONTENT] == "First part Second part" ) else: assert SPANDATA.MCP_TOOL_RESULT_CONTENT not in span["data"] @@ -959,7 +956,7 @@ def test_tool_complex(tool_name, arguments): assert span["data"]["mcp.request.argument.nested"] == json.dumps( {"key": "value", "list": [1, 2, 3]} ) - assert span["data"]["mcp.request.argument.string"] == '"test"' + assert span["data"]["mcp.request.argument.string"] == "test" assert span["data"]["mcp.request.argument.number"] == "42"