Skip to content

Span data is always be a primitive data type #4643

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 30, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 5 additions & 1 deletion sentry_sdk/ai/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@ def _normalize_data(data):
return list(_normalize_data(x) for x in data)
if isinstance(data, dict):
return {k: _normalize_data(v) for (k, v) in data.items()}

return data


def set_data_normalized(span, key, value):
# type: (Span, str, Any) -> None
normalized = _normalize_data(value)
span.set_data(key, normalized)
if isinstance(normalized, (int, float, bool, str)):
span.set_data(key, normalized)
else:
span.set_data(key, str(normalized))
Copy link

Choose a reason for hiding this comment

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

Bug: Unexpected Stringification of None and Non-Collection Types

The isinstance check for primitive types (int, float, bool, str) excludes None, causing None values to be stringified to "None". This is unexpected, as None is typically preserved as a primitive/null value. More broadly, this logic stringifies any non-primitive type, which may be overly broad if the intent was to only stringify collections (lists, tuples, dicts).

Locations (1)
Fix in Cursor Fix in Web

20 changes: 16 additions & 4 deletions tests/integrations/cohere/test_cohere.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,14 @@ def test_nonstreaming_chat(
assert span["data"][SPANDATA.AI_MODEL_ID] == "some-model"

if send_default_pii and include_prompts:
assert "some context" in span["data"][SPANDATA.AI_INPUT_MESSAGES][0]["content"]
assert "hello" in span["data"][SPANDATA.AI_INPUT_MESSAGES][1]["content"]
assert (
"{'role': 'system', 'content': 'some context'}"
in span["data"][SPANDATA.AI_INPUT_MESSAGES]
)
assert (
"{'role': 'user', 'content': 'hello'}"
in span["data"][SPANDATA.AI_INPUT_MESSAGES]
)
Copy link

Choose a reason for hiding this comment

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

Bug: Test Assertions Fail Due to Unstable Dictionary String Representation

The test assertions now depend on exact string representations of dictionaries (e.g., "{'role': 'system', 'content': 'some context'}"). This is brittle because dictionary string representation can vary between Python versions and the order of keys is not guaranteed, making tests prone to false failures even if the underlying functionality is correct.

Locations (2)
Fix in Cursor Fix in Web

assert "the model response" in span["data"][SPANDATA.AI_RESPONSES]
else:
assert SPANDATA.AI_INPUT_MESSAGES not in span["data"]
Expand Down Expand Up @@ -128,8 +134,14 @@ def test_streaming_chat(sentry_init, capture_events, send_default_pii, include_p
assert span["data"][SPANDATA.AI_MODEL_ID] == "some-model"

if send_default_pii and include_prompts:
assert "some context" in span["data"][SPANDATA.AI_INPUT_MESSAGES][0]["content"]
assert "hello" in span["data"][SPANDATA.AI_INPUT_MESSAGES][1]["content"]
assert (
"{'role': 'system', 'content': 'some context'}"
in span["data"][SPANDATA.AI_INPUT_MESSAGES]
)
assert (
"{'role': 'user', 'content': 'hello'}"
in span["data"][SPANDATA.AI_INPUT_MESSAGES]
)
assert "the model response" in span["data"][SPANDATA.AI_RESPONSES]
else:
assert SPANDATA.AI_INPUT_MESSAGES not in span["data"]
Expand Down
6 changes: 2 additions & 4 deletions tests/integrations/langchain/test_langchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,15 +196,13 @@ def test_langchain_agent(

if send_default_pii and include_prompts:
assert (
"You are very powerful"
in chat_spans[0]["data"][SPANDATA.AI_INPUT_MESSAGES][0]["content"]
"You are very powerful" in chat_spans[0]["data"][SPANDATA.AI_INPUT_MESSAGES]
)
assert "5" in chat_spans[0]["data"][SPANDATA.AI_RESPONSES]
assert "word" in tool_exec_span["data"][SPANDATA.AI_INPUT_MESSAGES]
assert 5 == int(tool_exec_span["data"][SPANDATA.AI_RESPONSES])
assert (
"You are very powerful"
in chat_spans[1]["data"][SPANDATA.AI_INPUT_MESSAGES][0]["content"]
"You are very powerful" in chat_spans[1]["data"][SPANDATA.AI_INPUT_MESSAGES]
)
assert "5" in chat_spans[1]["data"][SPANDATA.AI_RESPONSES]
else:
Expand Down
19 changes: 6 additions & 13 deletions tests/integrations/openai/test_openai.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import json
import pytest
from openai import AsyncOpenAI, OpenAI, AsyncStream, Stream, OpenAIError
from openai.types import CompletionUsage, CreateEmbeddingResponse, Embedding
Expand Down Expand Up @@ -144,11 +143,8 @@ def test_nonstreaming_chat_completion(
assert span["op"] == "gen_ai.chat"

if send_default_pii and include_prompts:
assert "hello" in span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]["content"]
assert (
"the model response"
in json.loads(span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT])[0]["content"]
)
assert "hello" in span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
assert "the model response" in span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT]
else:
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"]
Expand Down Expand Up @@ -189,11 +185,8 @@ async def test_nonstreaming_chat_completion_async(
assert span["op"] == "gen_ai.chat"

if send_default_pii and include_prompts:
assert "hello" in span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]["content"]
assert (
"the model response"
in json.loads(span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT])[0]["content"]
)
assert "hello" in span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
assert "the model response" in span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT]
else:
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"]
Expand Down Expand Up @@ -285,7 +278,7 @@ def test_streaming_chat_completion(
assert span["op"] == "gen_ai.chat"

if send_default_pii and include_prompts:
assert "hello" in span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]["content"]
assert "hello" in span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
assert "hello world" in span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT]
else:
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"]
Expand Down Expand Up @@ -381,7 +374,7 @@ async def test_streaming_chat_completion_async(
assert span["op"] == "gen_ai.chat"

if send_default_pii and include_prompts:
assert "hello" in span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]["content"]
assert "hello" in span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
assert "hello world" in span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT]
else:
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"]
Expand Down
Loading