Skip to content

Commit 8f0e049

Browse files
committed
use annotatedvalue instead of previous approach
1 parent 455136a commit 8f0e049

File tree

11 files changed

+240
-328
lines changed

11 files changed

+240
-328
lines changed

sentry_sdk/ai/utils.py

Lines changed: 11 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -109,26 +109,11 @@ def get_start_span_function():
109109

110110
def truncate_messages_by_size(messages, max_bytes=MAX_GEN_AI_MESSAGE_BYTES):
111111
# type: (List[Dict[str, Any]], int) -> List[Dict[str, Any]]
112-
"""
113-
Truncate messages by removing the oldest ones until the serialized size is within limits.
114-
If the last message is still too large, truncate its content instead of removing it entirely.
115-
116-
This function prioritizes keeping the most recent messages while ensuring the total
117-
serialized size stays under the specified byte limit. It uses the Sentry serializer
118-
to get accurate size estimates that match what will actually be sent.
119-
120-
Always preserves at least one message, even if content needs to be truncated.
121-
122-
:param messages: List of message objects (typically with 'role' and 'content' keys)
123-
:param max_bytes: Maximum allowed size in bytes for the serialized messages
124-
:returns: Truncated list of messages that fits within the size limit
125-
"""
126112
if not messages:
127113
return messages
128114

129115
truncated_messages = list(messages)
130116

131-
# First, remove older messages until we're under the limit or have only one message left
132117
while len(truncated_messages) > 1:
133118
serialized = serialize(
134119
truncated_messages, is_vars=False, max_value_length=round(max_bytes * 0.8)
@@ -139,10 +124,8 @@ def truncate_messages_by_size(messages, max_bytes=MAX_GEN_AI_MESSAGE_BYTES):
139124
if current_size <= max_bytes:
140125
break
141126

142-
truncated_messages.pop(0) # Remove oldest message
127+
truncated_messages.pop(0)
143128

144-
# If we still have one message but it's too large, truncate its content
145-
# This ensures we always preserve at least one message
146129
if len(truncated_messages) == 1:
147130
serialized = serialize(
148131
truncated_messages, is_vars=False, max_value_length=round(max_bytes * 0.8)
@@ -151,7 +134,6 @@ def truncate_messages_by_size(messages, max_bytes=MAX_GEN_AI_MESSAGE_BYTES):
151134
current_size = len(serialized_json.encode("utf-8"))
152135

153136
if current_size > max_bytes:
154-
# Truncate the content of the last message
155137
last_message = truncated_messages[0].copy()
156138
content = last_message.get("content", "")
157139

@@ -162,71 +144,24 @@ def truncate_messages_by_size(messages, max_bytes=MAX_GEN_AI_MESSAGE_BYTES):
162144
return truncated_messages
163145

164146

165-
def serialize_gen_ai_messages(messages, max_bytes=MAX_GEN_AI_MESSAGE_BYTES):
166-
# type: (Optional[Any], int) -> Optional[str]
167-
"""
168-
Serialize and truncate gen_ai messages for storage in spans.
169-
170-
This function handles the complete workflow of:
171-
1. Truncating messages to fit within size limits (if not already done)
172-
2. Serializing them using Sentry's serializer (which processes AnnotatedValue for _meta)
173-
3. Converting to JSON string for storage
174-
175-
:param messages: List of message objects, AnnotatedValue, or None
176-
:param max_bytes: Maximum allowed size in bytes for the serialized messages
177-
:returns: JSON string of serialized messages or None if input was None/empty
178-
"""
147+
def truncate_and_annotate_messages(
148+
messages, span, scope, max_bytes=MAX_GEN_AI_MESSAGE_BYTES
149+
):
150+
# type: (Optional[List[Dict[str, Any]]], Any, Any, int) -> Optional[List[Dict[str, Any]]]
179151
if not messages:
180152
return None
181153

182-
if isinstance(messages, AnnotatedValue):
183-
serialized_messages = serialize(
184-
messages, is_vars=False, max_value_length=round(max_bytes * 0.8)
185-
)
186-
return json.dumps(serialized_messages, separators=(",", ":"))
187-
154+
original_count = len(messages)
188155
truncated_messages = truncate_messages_by_size(messages, max_bytes)
189-
serialized_messages = serialize(
190-
truncated_messages, is_vars=False, max_value_length=round(max_bytes * 0.8)
191-
)
192-
193-
return json.dumps(serialized_messages)
194156

195-
196-
def truncate_and_serialize_messages(messages, max_bytes=MAX_GEN_AI_MESSAGE_BYTES):
197-
# type: (Optional[List[Dict[str, Any]]], int) -> Any
198-
"""
199-
Truncate messages and return serialized string or AnnotatedValue for automatic _meta creation.
200-
201-
This function handles truncation and always returns serialized JSON strings. When truncation
202-
occurs, it wraps the serialized string in an AnnotatedValue so that Sentry's serializer can
203-
automatically create the appropriate _meta structure.
204-
205-
:param messages: List of message objects or None
206-
:param max_bytes: Maximum allowed size in bytes for the serialized messages
207-
:returns: JSON string, AnnotatedValue containing JSON string (if truncated), or None
208-
"""
209-
if not messages:
210-
return None
211-
212-
truncated_messages = truncate_messages_by_size(messages, max_bytes)
213157
if not truncated_messages:
214158
return None
215159

216-
# Always serialize to JSON string
217-
serialized_json = serialize_gen_ai_messages(truncated_messages, max_bytes)
218-
if not serialized_json:
219-
return None
220-
221-
original_count = len(messages)
222160
truncated_count = len(truncated_messages)
161+
n_removed = original_count - truncated_count
223162

224-
# If truncation occurred, wrap the serialized string in AnnotatedValue for _meta
225-
if original_count != truncated_count:
226-
return AnnotatedValue(
227-
value=serialized_json,
228-
metadata={"len": original_count},
229-
)
163+
if n_removed > 0:
164+
scope._gen_ai_messages_truncated[span.span_id] = n_removed
165+
span.set_data("_gen_ai_messages_original_count", original_count)
230166

231-
# No truncation, return plain serialized string
232-
return serialized_json
167+
return truncated_messages

sentry_sdk/client.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,32 @@ def _prepare_event(
614614
event["breadcrumbs"] = AnnotatedValue(
615615
event.get("breadcrumbs", []), {"len": previous_total_breadcrumbs}
616616
)
617+
618+
# Annotate truncated gen_ai messages in spans
619+
if scope is not None and scope._gen_ai_messages_truncated:
620+
spans = event.get("spans", [])
621+
if isinstance(spans, AnnotatedValue):
622+
spans = spans.value
623+
624+
for span in spans:
625+
if isinstance(span, dict):
626+
span_id = span.get("span_id")
627+
if span_id and span_id in scope._gen_ai_messages_truncated:
628+
span_data = span.get("data", {})
629+
original_count = span_data.pop(
630+
"_gen_ai_messages_original_count", None
631+
)
632+
if (
633+
original_count is not None
634+
and SPANDATA.GEN_AI_REQUEST_MESSAGES in span_data
635+
):
636+
span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES] = (
637+
AnnotatedValue(
638+
span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES],
639+
{"len": original_count},
640+
)
641+
)
642+
617643
# Postprocess the event here so that annotated types do
618644
# generally not surface in before_send
619645
if event is not None:

sentry_sdk/integrations/anthropic.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from sentry_sdk.ai.utils import (
77
set_data_normalized,
88
normalize_message_roles,
9-
truncate_and_serialize_messages,
9+
truncate_and_annotate_messages,
1010
get_start_span_function,
1111
)
1212
from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS
@@ -146,9 +146,12 @@ def _set_input_data(span, kwargs, integration):
146146
normalized_messages.append(message)
147147

148148
role_normalized_messages = normalize_message_roles(normalized_messages)
149-
serialized_messages = truncate_and_serialize_messages(role_normalized_messages)
150-
if serialized_messages is not None:
151-
span.set_data(SPANDATA.GEN_AI_REQUEST_MESSAGES, serialized_messages)
149+
scope = sentry_sdk.get_current_scope()
150+
messages_data = truncate_and_annotate_messages(
151+
role_normalized_messages, span, scope
152+
)
153+
if messages_data is not None:
154+
span.set_data(SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data)
152155

153156
set_data_normalized(
154157
span, SPANDATA.GEN_AI_RESPONSE_STREAMING, kwargs.get("stream", False)

sentry_sdk/integrations/langchain.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
normalize_message_roles,
1010
set_data_normalized,
1111
get_start_span_function,
12-
truncate_and_serialize_messages,
12+
truncate_and_annotate_messages,
1313
)
1414
from sentry_sdk.consts import OP, SPANDATA
1515
from sentry_sdk.integrations import DidNotEnable, Integration
@@ -222,7 +222,10 @@ def on_llm_start(
222222
}
223223
for prompt in prompts
224224
]
225-
messages_data = truncate_and_serialize_messages(normalized_messages)
225+
scope = sentry_sdk.get_current_scope()
226+
messages_data = truncate_and_annotate_messages(
227+
normalized_messages, span, scope
228+
)
226229
if messages_data is not None:
227230
span.set_data(SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data)
228231

@@ -276,7 +279,10 @@ def on_chat_model_start(self, serialized, messages, *, run_id, **kwargs):
276279
self._normalize_langchain_message(message)
277280
)
278281
normalized_messages = normalize_message_roles(normalized_messages)
279-
messages_data = truncate_and_serialize_messages(normalized_messages)
282+
scope = sentry_sdk.get_current_scope()
283+
messages_data = truncate_and_annotate_messages(
284+
normalized_messages, span, scope
285+
)
280286
if messages_data is not None:
281287
span.set_data(SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data)
282288

@@ -752,7 +758,10 @@ def new_invoke(self, *args, **kwargs):
752758
and integration.include_prompts
753759
):
754760
normalized_messages = normalize_message_roles([input])
755-
messages_data = truncate_and_serialize_messages(normalized_messages)
761+
scope = sentry_sdk.get_current_scope()
762+
messages_data = truncate_and_annotate_messages(
763+
normalized_messages, span, scope
764+
)
756765
if messages_data is not None:
757766
span.set_data(SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data)
758767

@@ -804,7 +813,9 @@ def new_stream(self, *args, **kwargs):
804813
and integration.include_prompts
805814
):
806815
normalized_messages = normalize_message_roles([input])
807-
messages_data = truncate_and_serialize_messages(normalized_messages)
816+
messages_data = truncate_and_annotate_messages(
817+
normalized_messages, span, sentry_sdk.get_current_scope()
818+
)
808819
if messages_data is not None:
809820
span.set_data(SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data)
810821

sentry_sdk/integrations/langgraph.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from sentry_sdk.ai.utils import (
66
set_data_normalized,
77
normalize_message_roles,
8-
truncate_and_serialize_messages,
8+
truncate_and_annotate_messages,
99
)
1010
from sentry_sdk.consts import OP, SPANDATA
1111
from sentry_sdk.integrations import DidNotEnable, Integration
@@ -185,8 +185,9 @@ def new_invoke(self, *args, **kwargs):
185185
input_messages = _parse_langgraph_messages(args[0])
186186
if input_messages:
187187
normalized_input_messages = normalize_message_roles(input_messages)
188-
messages_data = truncate_and_serialize_messages(
189-
normalized_input_messages
188+
scope = sentry_sdk.get_current_scope()
189+
messages_data = truncate_and_annotate_messages(
190+
normalized_input_messages, span, scope
190191
)
191192
if messages_data is not None:
192193
span.set_data(SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data)
@@ -235,8 +236,9 @@ async def new_ainvoke(self, *args, **kwargs):
235236
input_messages = _parse_langgraph_messages(args[0])
236237
if input_messages:
237238
normalized_input_messages = normalize_message_roles(input_messages)
238-
messages_data = truncate_and_serialize_messages(
239-
normalized_input_messages
239+
scope = sentry_sdk.get_current_scope()
240+
messages_data = truncate_and_annotate_messages(
241+
normalized_input_messages, span, scope
240242
)
241243
if messages_data is not None:
242244
span.set_data(SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data)

sentry_sdk/integrations/litellm.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from sentry_sdk.ai.utils import (
77
get_start_span_function,
88
set_data_normalized,
9-
truncate_and_serialize_messages,
9+
truncate_and_annotate_messages,
10+
normalize_message_roles,
1011
)
1112
from sentry_sdk.consts import SPANDATA
1213
from sentry_sdk.integrations import DidNotEnable, Integration
@@ -76,7 +77,9 @@ def _input_callback(kwargs):
7677

7778
# Record messages if allowed
7879
if messages and should_send_default_pii() and integration.include_prompts:
79-
messages_data = truncate_and_serialize_messages(messages)
80+
normalized_messages = normalize_message_roles(messages)
81+
scope = sentry_sdk.get_current_scope()
82+
messages_data = truncate_and_annotate_messages(normalized_messages, span, scope)
8083
if messages_data is not None:
8184
span.set_data(SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data)
8285

sentry_sdk/integrations/openai.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from sentry_sdk.ai.utils import (
77
set_data_normalized,
88
normalize_message_roles,
9-
truncate_and_serialize_messages,
9+
truncate_and_annotate_messages,
1010
)
1111
from sentry_sdk.consts import SPANDATA
1212
from sentry_sdk.integrations import DidNotEnable, Integration
@@ -187,7 +187,8 @@ def _set_input_data(span, kwargs, operation, integration):
187187
and integration.include_prompts
188188
):
189189
normalized_messages = normalize_message_roles(messages)
190-
messages_data = truncate_and_serialize_messages(normalized_messages)
190+
scope = sentry_sdk.get_current_scope()
191+
messages_data = truncate_and_annotate_messages(normalized_messages, span, scope)
191192
if messages_data is not None:
192193
span.set_data(SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data)
193194

sentry_sdk/integrations/openai_agents/spans/invoke_agent.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
get_start_span_function,
44
set_data_normalized,
55
normalize_message_roles,
6-
truncate_and_serialize_messages,
6+
truncate_and_annotate_messages,
77
)
88
from sentry_sdk.consts import OP, SPANDATA
99
from sentry_sdk.scope import should_send_default_pii
@@ -62,7 +62,10 @@ def invoke_agent_span(context, agent, kwargs):
6262

6363
if len(messages) > 0:
6464
normalized_messages = normalize_message_roles(messages)
65-
messages_data = truncate_and_serialize_messages(normalized_messages)
65+
scope = sentry_sdk.get_current_scope()
66+
messages_data = truncate_and_annotate_messages(
67+
normalized_messages, span, scope
68+
)
6669
if messages_data is not None:
6770
span.set_data(SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data)
6871

sentry_sdk/integrations/openai_agents/utils.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
normalize_message_roles,
55
set_data_normalized,
66
normalize_message_role,
7-
truncate_and_serialize_messages,
7+
truncate_and_annotate_messages,
88
)
99
from sentry_sdk.consts import SPANDATA, SPANSTATUS, OP
1010
from sentry_sdk.integrations import DidNotEnable
@@ -137,7 +137,10 @@ def _set_input_data(span, get_response_kwargs):
137137
)
138138

139139
role_normalized_messages = normalize_message_roles(request_messages)
140-
messages_data = truncate_and_serialize_messages(role_normalized_messages)
140+
scope = sentry_sdk.get_current_scope()
141+
messages_data = truncate_and_annotate_messages(
142+
role_normalized_messages, span, scope
143+
)
141144
if messages_data is not None:
142145
span.set_data(SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data)
143146

sentry_sdk/scope.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ class Scope:
188188
"_extras",
189189
"_breadcrumbs",
190190
"_n_breadcrumbs_truncated",
191+
"_gen_ai_messages_truncated",
191192
"_event_processors",
192193
"_error_processors",
193194
"_should_capture",
@@ -213,6 +214,7 @@ def __init__(self, ty=None, client=None):
213214
self._name = None # type: Optional[str]
214215
self._propagation_context = None # type: Optional[PropagationContext]
215216
self._n_breadcrumbs_truncated = 0 # type: int
217+
self._gen_ai_messages_truncated = {} # type: Dict[str, int]
216218

217219
self.client = NonRecordingClient() # type: sentry_sdk.client.BaseClient
218220

@@ -247,6 +249,7 @@ def __copy__(self):
247249

248250
rv._breadcrumbs = copy(self._breadcrumbs)
249251
rv._n_breadcrumbs_truncated = self._n_breadcrumbs_truncated
252+
rv._gen_ai_messages_truncated = self._gen_ai_messages_truncated.copy()
250253
rv._event_processors = self._event_processors.copy()
251254
rv._error_processors = self._error_processors.copy()
252255
rv._propagation_context = self._propagation_context
@@ -1583,6 +1586,8 @@ def update_from_scope(self, scope):
15831586
self._n_breadcrumbs_truncated = (
15841587
self._n_breadcrumbs_truncated + scope._n_breadcrumbs_truncated
15851588
)
1589+
if scope._gen_ai_messages_truncated:
1590+
self._gen_ai_messages_truncated.update(scope._gen_ai_messages_truncated)
15861591
if scope._span:
15871592
self._span = scope._span
15881593
if scope._attachments:

0 commit comments

Comments
 (0)