Skip to content

Commit 2be94ca

Browse files
feat: Send GenAI spans as V2 envelope items
1 parent fe7e848 commit 2be94ca

1 file changed

Lines changed: 103 additions & 2 deletions

File tree

sentry_sdk/client.py

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
get_before_send_metric,
2828
has_logs_enabled,
2929
has_metrics_enabled,
30+
serialize_attribute,
3031
)
3132
from sentry_sdk.serializer import serialize
3233
from sentry_sdk.tracing import trace
@@ -56,6 +57,74 @@
5657
)
5758
from sentry_sdk.scrubber import EventScrubber
5859
from sentry_sdk.monitor import Monitor
60+
from sentry_sdk.envelope import Item, PayloadRef
61+
62+
63+
_ISO_TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
64+
65+
66+
def _iso_to_epoch(iso_str: str) -> float:
67+
return (
68+
datetime.strptime(iso_str, _ISO_TIMESTAMP_FORMAT)
69+
.replace(tzinfo=timezone.utc)
70+
.timestamp()
71+
)
72+
73+
74+
def _v1_span_to_v2(span: "Dict[str, Any]", event: "Dict[str, Any]") -> "Dict[str, Any]":
75+
rv: "Dict[str, Any]" = {
76+
"trace_id": span["trace_id"],
77+
"span_id": span["span_id"],
78+
"name": span.get("description") or "",
79+
"is_segment": False,
80+
"start_timestamp": _iso_to_epoch(span["start_timestamp"]),
81+
"status": "ok",
82+
}
83+
84+
if span.get("timestamp"):
85+
rv["end_timestamp"] = _iso_to_epoch(span["timestamp"])
86+
87+
if span.get("parent_span_id"):
88+
rv["parent_span_id"] = span["parent_span_id"]
89+
90+
status = span.get("status")
91+
if status and status != "ok":
92+
rv["status"] = "error"
93+
94+
attributes: "Dict[str, Any]" = {}
95+
96+
if span.get("op"):
97+
attributes["sentry.op"] = span["op"]
98+
if span.get("origin"):
99+
attributes["sentry.origin"] = span["origin"]
100+
101+
for key, value in (span.get("data") or {}).items():
102+
attributes[key] = value
103+
for key, value in (span.get("tags") or {}).items():
104+
attributes[key] = value
105+
106+
trace_context = event.get("contexts", {}).get("trace", {})
107+
sdk_info = event.get("sdk", {})
108+
109+
if event.get("release"):
110+
attributes["sentry.release"] = event["release"]
111+
if event.get("environment"):
112+
attributes["sentry.environment"] = event["environment"]
113+
if event.get("transaction"):
114+
attributes["sentry.segment.name"] = event["transaction"]
115+
116+
if trace_context.get("span_id"):
117+
attributes["sentry.segment.id"] = trace_context["span_id"]
118+
if sdk_info.get("name"):
119+
attributes["sentry.sdk.name"] = sdk_info["name"]
120+
if sdk_info.get("version"):
121+
attributes["sentry.sdk.version"] = sdk_info["version"]
122+
123+
if attributes:
124+
rv["attributes"] = {k: serialize_attribute(v) for k, v in attributes.items()}
125+
126+
return rv
127+
59128

60129
if TYPE_CHECKING:
61130
from typing import Any
@@ -72,7 +141,7 @@
72141
from sentry_sdk.session import Session
73142
from sentry_sdk.spotlight import SpotlightClient
74143
from sentry_sdk.traces import StreamedSpan
75-
from sentry_sdk.transport import Transport, Item
144+
from sentry_sdk.transport import Transport, Item, PayloadRef
76145
from sentry_sdk._log_batcher import LogBatcher
77146
from sentry_sdk._metrics_batcher import MetricsBatcher
78147
from sentry_sdk.utils import Dsn
@@ -912,7 +981,39 @@ def capture_event(
912981
if is_transaction:
913982
if isinstance(profile, Profile):
914983
envelope.add_profile(profile.to_json(event_opt, self.options))
915-
envelope.add_transaction(event_opt)
984+
985+
nonstreamed_spans = []
986+
streamed_spans = []
987+
for span in event_opt.get("spans") or []:
988+
span_op = span.get("op")
989+
if span_op is not None and span_op.startswith("gen_ai."):
990+
streamed_spans.append(span)
991+
else:
992+
nonstreamed_spans.append(span)
993+
994+
if nonstreamed_spans:
995+
event_opt["spans"] = nonstreamed_spans
996+
envelope.add_transaction(event_opt)
997+
998+
if streamed_spans:
999+
envelope.add_item(
1000+
Item(
1001+
type=SpanBatcher.TYPE,
1002+
content_type=SpanBatcher.CONTENT_TYPE,
1003+
headers={
1004+
"item_count": len(streamed_spans),
1005+
},
1006+
payload=PayloadRef(
1007+
json={
1008+
"items": [
1009+
_v1_span_to_v2(span, event)
1010+
for span in streamed_spans
1011+
]
1012+
},
1013+
),
1014+
)
1015+
)
1016+
9161017
elif is_checkin:
9171018
envelope.add_checkin(event_opt)
9181019
else:

0 commit comments

Comments
 (0)