From 2be94ca479e1a46a7ee053f0f6e6d733093a463e Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 15 Apr 2026 13:14:52 +0200 Subject: [PATCH 1/9] feat: Send GenAI spans as V2 envelope items --- sentry_sdk/client.py | 105 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 9f795d2489..ed58104ec7 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -27,6 +27,7 @@ get_before_send_metric, has_logs_enabled, has_metrics_enabled, + serialize_attribute, ) from sentry_sdk.serializer import serialize from sentry_sdk.tracing import trace @@ -56,6 +57,74 @@ ) from sentry_sdk.scrubber import EventScrubber from sentry_sdk.monitor import Monitor +from sentry_sdk.envelope import Item, PayloadRef + + +_ISO_TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" + + +def _iso_to_epoch(iso_str: str) -> float: + return ( + datetime.strptime(iso_str, _ISO_TIMESTAMP_FORMAT) + .replace(tzinfo=timezone.utc) + .timestamp() + ) + + +def _v1_span_to_v2(span: "Dict[str, Any]", event: "Dict[str, Any]") -> "Dict[str, Any]": + rv: "Dict[str, Any]" = { + "trace_id": span["trace_id"], + "span_id": span["span_id"], + "name": span.get("description") or "", + "is_segment": False, + "start_timestamp": _iso_to_epoch(span["start_timestamp"]), + "status": "ok", + } + + if span.get("timestamp"): + rv["end_timestamp"] = _iso_to_epoch(span["timestamp"]) + + if span.get("parent_span_id"): + rv["parent_span_id"] = span["parent_span_id"] + + status = span.get("status") + if status and status != "ok": + rv["status"] = "error" + + attributes: "Dict[str, Any]" = {} + + if span.get("op"): + attributes["sentry.op"] = span["op"] + if span.get("origin"): + attributes["sentry.origin"] = span["origin"] + + for key, value in (span.get("data") or {}).items(): + attributes[key] = value + for key, value in (span.get("tags") or {}).items(): + attributes[key] = value + + trace_context = event.get("contexts", {}).get("trace", {}) + sdk_info = event.get("sdk", {}) + + if event.get("release"): + attributes["sentry.release"] = event["release"] + if event.get("environment"): + attributes["sentry.environment"] = event["environment"] + if event.get("transaction"): + attributes["sentry.segment.name"] = event["transaction"] + + if trace_context.get("span_id"): + attributes["sentry.segment.id"] = trace_context["span_id"] + if sdk_info.get("name"): + attributes["sentry.sdk.name"] = sdk_info["name"] + if sdk_info.get("version"): + attributes["sentry.sdk.version"] = sdk_info["version"] + + if attributes: + rv["attributes"] = {k: serialize_attribute(v) for k, v in attributes.items()} + + return rv + if TYPE_CHECKING: from typing import Any @@ -72,7 +141,7 @@ from sentry_sdk.session import Session from sentry_sdk.spotlight import SpotlightClient from sentry_sdk.traces import StreamedSpan - from sentry_sdk.transport import Transport, Item + from sentry_sdk.transport import Transport, Item, PayloadRef from sentry_sdk._log_batcher import LogBatcher from sentry_sdk._metrics_batcher import MetricsBatcher from sentry_sdk.utils import Dsn @@ -912,7 +981,39 @@ def capture_event( if is_transaction: if isinstance(profile, Profile): envelope.add_profile(profile.to_json(event_opt, self.options)) - envelope.add_transaction(event_opt) + + nonstreamed_spans = [] + streamed_spans = [] + for span in event_opt.get("spans") or []: + span_op = span.get("op") + if span_op is not None and span_op.startswith("gen_ai."): + streamed_spans.append(span) + else: + nonstreamed_spans.append(span) + + if nonstreamed_spans: + event_opt["spans"] = nonstreamed_spans + envelope.add_transaction(event_opt) + + if streamed_spans: + envelope.add_item( + Item( + type=SpanBatcher.TYPE, + content_type=SpanBatcher.CONTENT_TYPE, + headers={ + "item_count": len(streamed_spans), + }, + payload=PayloadRef( + json={ + "items": [ + _v1_span_to_v2(span, event) + for span in streamed_spans + ] + }, + ), + ) + ) + elif is_checkin: envelope.add_checkin(event_opt) else: From 01f479a09e4791082da604ba0f57cc4b74f1bf2f Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 15 Apr 2026 15:42:59 +0200 Subject: [PATCH 2/9] . --- sentry_sdk/client.py | 213 ++++++++++++++++++++++++++----------------- 1 file changed, 130 insertions(+), 83 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index ed58104ec7..8667c2b194 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -2,7 +2,7 @@ import uuid import random import socket -from collections.abc import Mapping +from collections.abc import Mapping, Iterable from datetime import datetime, timezone from importlib import import_module from typing import TYPE_CHECKING, List, Dict, cast, overload @@ -58,104 +58,156 @@ from sentry_sdk.scrubber import EventScrubber from sentry_sdk.monitor import Monitor from sentry_sdk.envelope import Item, PayloadRef +from sentry_sdk.utils import datetime_from_isoformat +if TYPE_CHECKING: + from typing import Any + from typing import Callable + from typing import Optional + from typing import Sequence + from typing import Type + from typing import Union + from typing import TypeVar + + from sentry_sdk._types import Event, Hint, SDKInfo, Log, Metric, EventDataCategory + from sentry_sdk.integrations import Integration + from sentry_sdk.scope import Scope + from sentry_sdk.session import Session + from sentry_sdk.spotlight import SpotlightClient + from sentry_sdk.traces import StreamedSpan + from sentry_sdk.transport import Transport, Item, PayloadRef + from sentry_sdk._log_batcher import LogBatcher + from sentry_sdk._metrics_batcher import MetricsBatcher + from sentry_sdk.utils import Dsn -_ISO_TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" + I = TypeVar("I", bound=Integration) # noqa: E741 +_client_init_debug = ContextVar("client_init_debug") -def _iso_to_epoch(iso_str: str) -> float: - return ( - datetime.strptime(iso_str, _ISO_TIMESTAMP_FORMAT) - .replace(tzinfo=timezone.utc) - .timestamp() - ) +SDK_INFO: "SDKInfo" = { + "name": "sentry.python", # SDK name will be overridden after integrations have been loaded with sentry_sdk.integrations.setup_integrations() + "version": VERSION, + "packages": [{"name": "pypi:sentry-sdk", "version": VERSION}], +} -def _v1_span_to_v2(span: "Dict[str, Any]", event: "Dict[str, Any]") -> "Dict[str, Any]": - rv: "Dict[str, Any]" = { - "trace_id": span["trace_id"], - "span_id": span["span_id"], - "name": span.get("description") or "", - "is_segment": False, - "start_timestamp": _iso_to_epoch(span["start_timestamp"]), +def _serialized_v1_span_to_serialized_v2_span( + span: "Dict[str, Any]", event: "Event" +) -> "dict[str, Any]": + # See SpanBatcher._to_transport_format() for analogous population of all entries except "attributes". + res: "Dict[str, Any]" = { "status": "ok", + "is_segment": False, } - if span.get("timestamp"): - rv["end_timestamp"] = _iso_to_epoch(span["timestamp"]) + if "trace_id" in span: + res["trace_id"] = span["trace_id"] + + if "span_id" in span: + res["span_id"] = span["span_id"] + + if "description" in span: + res["name"] = span["description"] - if span.get("parent_span_id"): - rv["parent_span_id"] = span["parent_span_id"] + if "start_timestamp" in span: + start_timestamp = None + try: + start_timestamp = datetime_from_isoformat(span["start_timestamp"]) + except Exception: + pass + + if start_timestamp is not None: + res["start_timestamp"] = start_timestamp.timestamp() + + if "timestamp" in span: + end_timestamp = None + try: + end_timestamp = datetime_from_isoformat(span["timestamp"]) + except Exception: + pass - status = span.get("status") - if status and status != "ok": - rv["status"] = "error" + if end_timestamp is not None: + res["end_timestamp"] = end_timestamp.timestamp() + + if "parent_span_id" in span: + res["parent_span_id"] = span["parent_span_id"] + + if "status" in span and span["status"] != "ok": + res["status"] = "error" attributes: "Dict[str, Any]" = {} - if span.get("op"): + if "op" in span: attributes["sentry.op"] = span["op"] - if span.get("origin"): + if "origin" in span: attributes["sentry.origin"] = span["origin"] - for key, value in (span.get("data") or {}).items(): - attributes[key] = value - for key, value in (span.get("tags") or {}).items(): - attributes[key] = value - - trace_context = event.get("contexts", {}).get("trace", {}) - sdk_info = event.get("sdk", {}) - - if event.get("release"): + span_data = span.get("data") + if isinstance(span_data, dict): + attributes.update(span_data) + + span_tags = span.get("tags") + if isinstance(span_tags, dict): + attributes.update(span_tags) + + # See Scope._apply_user_attributes_to_telemetry() for user attributes. + user = event.get("user") + if isinstance(user, dict): + if "id" in user: + attributes["user.id"] = user["id"] + if "username" in user: + attributes["user.name"] = user["username"] + if "email" in user: + attributes["user.email"] = user["email"] + + # See Scope.set_global_attributes() for release, environment, and SDK metadata. + if "release" in event: attributes["sentry.release"] = event["release"] - if event.get("environment"): + if "environment" in event: attributes["sentry.environment"] = event["environment"] - if event.get("transaction"): + if "transaction" in event: attributes["sentry.segment.name"] = event["transaction"] - if trace_context.get("span_id"): + trace_context = event.get("contexts", {}).get("trace", {}) + if "span_id" in trace_context: attributes["sentry.segment.id"] = trace_context["span_id"] - if sdk_info.get("name"): - attributes["sentry.sdk.name"] = sdk_info["name"] - if sdk_info.get("version"): - attributes["sentry.sdk.version"] = sdk_info["version"] + + sdk_info = event.get("sdk") + if isinstance(sdk_info, dict): + if "name" in sdk_info: + attributes["sentry.sdk.name"] = sdk_info["name"] + if "version" in sdk_info: + attributes["sentry.sdk.version"] = sdk_info["version"] if attributes: - rv["attributes"] = {k: serialize_attribute(v) for k, v in attributes.items()} + res["attributes"] = {k: serialize_attribute(v) for k, v in attributes.items()} - return rv + return res -if TYPE_CHECKING: - from typing import Any - from typing import Callable - from typing import Optional - from typing import Sequence - from typing import Type - from typing import Union - from typing import TypeVar +def _split_gen_ai_spans( + event_opt: "Event", +) -> "tuple[List[Dict[str, object]], List[Dict[str, object]]]": + if "spans" not in event_opt: + return [], [] - from sentry_sdk._types import Event, Hint, SDKInfo, Log, Metric, EventDataCategory - from sentry_sdk.integrations import Integration - from sentry_sdk.scope import Scope - from sentry_sdk.session import Session - from sentry_sdk.spotlight import SpotlightClient - from sentry_sdk.traces import StreamedSpan - from sentry_sdk.transport import Transport, Item, PayloadRef - from sentry_sdk._log_batcher import LogBatcher - from sentry_sdk._metrics_batcher import MetricsBatcher - from sentry_sdk.utils import Dsn + spans = event_opt["spans"] + if isinstance(spans, AnnotatedValue): + spans = spans.value - I = TypeVar("I", bound=Integration) # noqa: E741 - -_client_init_debug = ContextVar("client_init_debug") + if not isinstance(spans, Iterable): + return [], [] + non_gen_ai_spans = [] + gen_ai_spans = [] + for span in spans: + span_op = span.get("op") + if isinstance(span_op, str) and span_op.startswith("gen_ai."): + gen_ai_spans.append(span) + else: + non_gen_ai_spans.append(span) -SDK_INFO: "SDKInfo" = { - "name": "sentry.python", # SDK name will be overridden after integrations have been loaded with sentry_sdk.integrations.setup_integrations() - "version": VERSION, - "packages": [{"name": "pypi:sentry-sdk", "version": VERSION}], -} + return non_gen_ai_spans, gen_ai_spans def _get_options(*args: "Optional[str]", **kwargs: "Any") -> "Dict[str, Any]": @@ -982,32 +1034,27 @@ def capture_event( if isinstance(profile, Profile): envelope.add_profile(profile.to_json(event_opt, self.options)) - nonstreamed_spans = [] - streamed_spans = [] - for span in event_opt.get("spans") or []: - span_op = span.get("op") - if span_op is not None and span_op.startswith("gen_ai."): - streamed_spans.append(span) - else: - nonstreamed_spans.append(span) + non_gen_ai_spans, gen_ai_spans = _split_gen_ai_spans(event_opt) - if nonstreamed_spans: - event_opt["spans"] = nonstreamed_spans - envelope.add_transaction(event_opt) + event_opt["spans"] = non_gen_ai_spans + envelope.add_transaction(event_opt) - if streamed_spans: + if gen_ai_spans: envelope.add_item( Item( type=SpanBatcher.TYPE, content_type=SpanBatcher.CONTENT_TYPE, headers={ - "item_count": len(streamed_spans), + "item_count": len(gen_ai_spans), }, payload=PayloadRef( json={ "items": [ - _v1_span_to_v2(span, event) - for span in streamed_spans + _serialized_v1_span_to_serialized_v2_span( + span, event + ) + for span in gen_ai_spans + if isinstance(span, dict) ] }, ), From 80e6a106b8472f6a6984ab254ca56646f0d51e59 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 15 Apr 2026 15:43:59 +0200 Subject: [PATCH 3/9] . --- sentry_sdk/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 8667c2b194..41ab81c58e 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -75,7 +75,7 @@ from sentry_sdk.session import Session from sentry_sdk.spotlight import SpotlightClient from sentry_sdk.traces import StreamedSpan - from sentry_sdk.transport import Transport, Item, PayloadRef + from sentry_sdk.transport import Transport, Item from sentry_sdk._log_batcher import LogBatcher from sentry_sdk._metrics_batcher import MetricsBatcher from sentry_sdk.utils import Dsn From 0622cf410d9c6496d81d50ce163f52fa1d97eaee Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 15 Apr 2026 15:44:35 +0200 Subject: [PATCH 4/9] . --- sentry_sdk/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 41ab81c58e..2895f23436 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -84,6 +84,7 @@ _client_init_debug = ContextVar("client_init_debug") + SDK_INFO: "SDKInfo" = { "name": "sentry.python", # SDK name will be overridden after integrations have been loaded with sentry_sdk.integrations.setup_integrations() "version": VERSION, From 7c75da105649abe57a6e32946507d97c85c86123 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 15 Apr 2026 16:01:06 +0200 Subject: [PATCH 5/9] . --- sentry_sdk/client.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 2895f23436..7bb2acf7dc 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -188,16 +188,16 @@ def _serialized_v1_span_to_serialized_v2_span( def _split_gen_ai_spans( event_opt: "Event", -) -> "tuple[List[Dict[str, object]], List[Dict[str, object]]]": +) -> "Optional[tuple[List[Dict[str, object]], List[Dict[str, object]]]]": if "spans" not in event_opt: - return [], [] + return None spans = event_opt["spans"] if isinstance(spans, AnnotatedValue): spans = spans.value if not isinstance(spans, Iterable): - return [], [] + return None non_gen_ai_spans = [] gen_ai_spans = [] @@ -1035,12 +1035,15 @@ def capture_event( if isinstance(profile, Profile): envelope.add_profile(profile.to_json(event_opt, self.options)) - non_gen_ai_spans, gen_ai_spans = _split_gen_ai_spans(event_opt) + split_spans = _split_gen_ai_spans(event_opt) + if split_spans is None or not split_spans[1]: + envelope.add_transaction(event_opt) + else: + non_gen_ai_spans, gen_ai_spans = split_spans - event_opt["spans"] = non_gen_ai_spans - envelope.add_transaction(event_opt) + event_opt["spans"] = non_gen_ai_spans + envelope.add_transaction(event_opt) - if gen_ai_spans: envelope.add_item( Item( type=SpanBatcher.TYPE, From 54a9b073a5887cdc51bd2d23253014e1bcb55c0f Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 15 Apr 2026 16:08:42 +0200 Subject: [PATCH 6/9] update --- sentry_sdk/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 7bb2acf7dc..9ee225150d 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -93,10 +93,10 @@ def _serialized_v1_span_to_serialized_v2_span( - span: "Dict[str, Any]", event: "Event" + span: "dict[str, Any]", event: "Event" ) -> "dict[str, Any]": # See SpanBatcher._to_transport_format() for analogous population of all entries except "attributes". - res: "Dict[str, Any]" = { + res: "dict[str, Any]" = { "status": "ok", "is_segment": False, } From d1aa07cb2c201ab69a130e9b1b3705f2330d629b Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 15 Apr 2026 16:48:38 +0200 Subject: [PATCH 7/9] . --- sentry_sdk/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 9ee225150d..e02841d5a3 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -192,7 +192,7 @@ def _split_gen_ai_spans( if "spans" not in event_opt: return None - spans = event_opt["spans"] + spans: "Any" = event_opt["spans"] if isinstance(spans, AnnotatedValue): spans = spans.value From 117a6c9bf47342883a8cd4546582be97d39ad996 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 15 Apr 2026 18:17:04 +0200 Subject: [PATCH 8/9] . --- sentry_sdk/client.py | 62 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index e02841d5a3..7c1eb64cff 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -7,6 +7,7 @@ from importlib import import_module from typing import TYPE_CHECKING, List, Dict, cast, overload import warnings +import json from sentry_sdk._compat import check_uwsgi_thread_support from sentry_sdk._metrics_batcher import MetricsBatcher @@ -27,10 +28,10 @@ get_before_send_metric, has_logs_enabled, has_metrics_enabled, - serialize_attribute, ) from sentry_sdk.serializer import serialize from sentry_sdk.tracing import trace +from sentry_sdk.traces import SpanStatus from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.transport import ( HttpTransportCore, @@ -39,6 +40,7 @@ ) from sentry_sdk.consts import ( SPANDATA, + SPANSTATUS, DEFAULT_MAX_VALUE_LENGTH, DEFAULT_OPTIONS, INSTRUMENTER, @@ -97,7 +99,7 @@ def _serialized_v1_span_to_serialized_v2_span( ) -> "dict[str, Any]": # See SpanBatcher._to_transport_format() for analogous population of all entries except "attributes". res: "dict[str, Any]" = { - "status": "ok", + "status": SpanStatus.OK.value, "is_segment": False, } @@ -133,7 +135,7 @@ def _serialized_v1_span_to_serialized_v2_span( if "parent_span_id" in span: res["parent_span_id"] = span["parent_span_id"] - if "status" in span and span["status"] != "ok": + if "status" in span and span["status"] != SPANSTATUS.OK: res["status"] = "error" attributes: "Dict[str, Any]" = {} @@ -180,8 +182,58 @@ def _serialized_v1_span_to_serialized_v2_span( if "version" in sdk_info: attributes["sentry.sdk.version"] = sdk_info["version"] - if attributes: - res["attributes"] = {k: serialize_attribute(v) for k, v in attributes.items()} + for key, value in attributes.items(): + serialized_value = serialize(value) + if isinstance(serialized_value, bool): + res.setdefault("attributes", {})[key] = { + "value": serialized_value, + "type": "boolean", + } + continue + + if isinstance(serialized_value, int): + res.setdefault("attributes", {})[key] = { + "value": serialized_value, + "type": "integer", + } + continue + + if isinstance(serialized_value, float): + res.setdefault("attributes", {})[key] = { + "value": serialized_value, + "type": "double", + } + continue + + if isinstance(serialized_value, str): + res.setdefault("attributes", {})[key] = { + "value": serialized_value, + "type": "string", + } + continue + + if isinstance(serialized_value, list): + if not serialized_value: + res.setdefault("attributes", {})[key] = {"value": [], "type": "array"} + + ty = type(serialized_value[0]) + if ty in (int, str, bool, float) and all( + type(v) is ty for v in serialized_value + ): + res.setdefault("attributes", {})[key] = { + "value": serialized_value, + "type": "array", + } + + continue + + # Types returned when the serializer for V1 span attributes recurses into some container types. + if isinstance(serialized_value, (dict, list)): + res.setdefault("attributes", {})[key] = { + "value": json.dumps(serialized_value), + "type": "string", + } + continue return res From 83c36b54c0c46847531db66f2ddc3d6d592d8a95 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 15 Apr 2026 18:25:21 +0200 Subject: [PATCH 9/9] . --- sentry_sdk/client.py | 118 ++++++++++++++++++++++++------------------- 1 file changed, 66 insertions(+), 52 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 7c1eb64cff..c6df2f564b 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -71,7 +71,15 @@ from typing import Union from typing import TypeVar - from sentry_sdk._types import Event, Hint, SDKInfo, Log, Metric, EventDataCategory + from sentry_sdk._types import ( + Event, + Hint, + SDKInfo, + Log, + Metric, + EventDataCategory, + SerializedAttributeValue, + ) from sentry_sdk.integrations import Integration from sentry_sdk.scope import Scope from sentry_sdk.session import Session @@ -94,6 +102,56 @@ } +def _serialized_v1_attribute_to_serialized_v2_attribute( + attribute_value: "Any", +) -> "Optional[SerializedAttributeValue]": + if isinstance(attribute_value, bool): + return { + "value": attribute_value, + "type": "boolean", + } + + if isinstance(attribute_value, int): + return { + "value": attribute_value, + "type": "integer", + } + + if isinstance(attribute_value, float): + return { + "value": attribute_value, + "type": "double", + } + + if isinstance(attribute_value, str): + return { + "value": attribute_value, + "type": "string", + } + + if isinstance(attribute_value, list): + if not attribute_value: + return {"value": [], "type": "array"} + + ty = type(attribute_value[0]) + if ty in (int, str, bool, float) and all( + type(v) is ty for v in attribute_value + ): + return { + "value": attribute_value, + "type": "array", + } + + # Types returned when the serializer for V1 span attributes recurses into some container types. + if isinstance(attribute_value, (dict, list)): + return { + "value": json.dumps(attribute_value), + "type": "string", + } + + return None + + def _serialized_v1_span_to_serialized_v2_span( span: "dict[str, Any]", event: "Event" ) -> "dict[str, Any]": @@ -182,58 +240,14 @@ def _serialized_v1_span_to_serialized_v2_span( if "version" in sdk_info: attributes["sentry.sdk.version"] = sdk_info["version"] - for key, value in attributes.items(): - serialized_value = serialize(value) - if isinstance(serialized_value, bool): - res.setdefault("attributes", {})[key] = { - "value": serialized_value, - "type": "boolean", - } - continue - - if isinstance(serialized_value, int): - res.setdefault("attributes", {})[key] = { - "value": serialized_value, - "type": "integer", - } - continue - - if isinstance(serialized_value, float): - res.setdefault("attributes", {})[key] = { - "value": serialized_value, - "type": "double", - } - continue - - if isinstance(serialized_value, str): - res.setdefault("attributes", {})[key] = { - "value": serialized_value, - "type": "string", - } - continue - - if isinstance(serialized_value, list): - if not serialized_value: - res.setdefault("attributes", {})[key] = {"value": [], "type": "array"} - - ty = type(serialized_value[0]) - if ty in (int, str, bool, float) and all( - type(v) is ty for v in serialized_value - ): - res.setdefault("attributes", {})[key] = { - "value": serialized_value, - "type": "array", - } - - continue + if not attributes: + return res - # Types returned when the serializer for V1 span attributes recurses into some container types. - if isinstance(serialized_value, (dict, list)): - res.setdefault("attributes", {})[key] = { - "value": json.dumps(serialized_value), - "type": "string", - } - continue + res["attributes"] = {} + for key, value in attributes.items(): + res["attributes"][key] = _serialized_v1_attribute_to_serialized_v2_attribute( + value + ) return res