|
27 | 27 | get_before_send_metric, |
28 | 28 | has_logs_enabled, |
29 | 29 | has_metrics_enabled, |
| 30 | + serialize_attribute, |
30 | 31 | ) |
31 | 32 | from sentry_sdk.serializer import serialize |
32 | 33 | from sentry_sdk.tracing import trace |
|
56 | 57 | ) |
57 | 58 | from sentry_sdk.scrubber import EventScrubber |
58 | 59 | 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 | + |
59 | 128 |
|
60 | 129 | if TYPE_CHECKING: |
61 | 130 | from typing import Any |
|
72 | 141 | from sentry_sdk.session import Session |
73 | 142 | from sentry_sdk.spotlight import SpotlightClient |
74 | 143 | from sentry_sdk.traces import StreamedSpan |
75 | | - from sentry_sdk.transport import Transport, Item |
| 144 | + from sentry_sdk.transport import Transport, Item, PayloadRef |
76 | 145 | from sentry_sdk._log_batcher import LogBatcher |
77 | 146 | from sentry_sdk._metrics_batcher import MetricsBatcher |
78 | 147 | from sentry_sdk.utils import Dsn |
@@ -912,7 +981,39 @@ def capture_event( |
912 | 981 | if is_transaction: |
913 | 982 | if isinstance(profile, Profile): |
914 | 983 | 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 | + |
916 | 1017 | elif is_checkin: |
917 | 1018 | envelope.add_checkin(event_opt) |
918 | 1019 | else: |
|
0 commit comments