|
2 | 2 | import uuid |
3 | 3 | import random |
4 | 4 | import socket |
5 | | -from collections.abc import Mapping |
| 5 | +from collections.abc import Mapping, Iterable |
6 | 6 | from datetime import datetime, timezone |
7 | 7 | from importlib import import_module |
8 | 8 | from typing import TYPE_CHECKING, List, Dict, cast, overload |
|
58 | 58 | from sentry_sdk.scrubber import EventScrubber |
59 | 59 | from sentry_sdk.monitor import Monitor |
60 | 60 | from sentry_sdk.envelope import Item, PayloadRef |
| 61 | +from sentry_sdk.utils import datetime_from_isoformat |
61 | 62 |
|
| 63 | +if TYPE_CHECKING: |
| 64 | + from typing import Any |
| 65 | + from typing import Callable |
| 66 | + from typing import Optional |
| 67 | + from typing import Sequence |
| 68 | + from typing import Type |
| 69 | + from typing import Union |
| 70 | + from typing import TypeVar |
| 71 | + |
| 72 | + from sentry_sdk._types import Event, Hint, SDKInfo, Log, Metric, EventDataCategory |
| 73 | + from sentry_sdk.integrations import Integration |
| 74 | + from sentry_sdk.scope import Scope |
| 75 | + from sentry_sdk.session import Session |
| 76 | + from sentry_sdk.spotlight import SpotlightClient |
| 77 | + from sentry_sdk.traces import StreamedSpan |
| 78 | + from sentry_sdk.transport import Transport, Item, PayloadRef |
| 79 | + from sentry_sdk._log_batcher import LogBatcher |
| 80 | + from sentry_sdk._metrics_batcher import MetricsBatcher |
| 81 | + from sentry_sdk.utils import Dsn |
62 | 82 |
|
63 | | -_ISO_TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" |
| 83 | + I = TypeVar("I", bound=Integration) # noqa: E741 |
64 | 84 |
|
| 85 | +_client_init_debug = ContextVar("client_init_debug") |
65 | 86 |
|
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 | | - ) |
| 87 | +SDK_INFO: "SDKInfo" = { |
| 88 | + "name": "sentry.python", # SDK name will be overridden after integrations have been loaded with sentry_sdk.integrations.setup_integrations() |
| 89 | + "version": VERSION, |
| 90 | + "packages": [{"name": "pypi:sentry-sdk", "version": VERSION}], |
| 91 | +} |
72 | 92 |
|
73 | 93 |
|
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"]), |
| 94 | +def _serialized_v1_span_to_serialized_v2_span( |
| 95 | + span: "Dict[str, Any]", event: "Event" |
| 96 | +) -> "dict[str, Any]": |
| 97 | + # See SpanBatcher._to_transport_format() for analogous population of all entries except "attributes". |
| 98 | + res: "Dict[str, Any]" = { |
81 | 99 | "status": "ok", |
| 100 | + "is_segment": False, |
82 | 101 | } |
83 | 102 |
|
84 | | - if span.get("timestamp"): |
85 | | - rv["end_timestamp"] = _iso_to_epoch(span["timestamp"]) |
| 103 | + if "trace_id" in span: |
| 104 | + res["trace_id"] = span["trace_id"] |
| 105 | + |
| 106 | + if "span_id" in span: |
| 107 | + res["span_id"] = span["span_id"] |
| 108 | + |
| 109 | + if "description" in span: |
| 110 | + res["name"] = span["description"] |
86 | 111 |
|
87 | | - if span.get("parent_span_id"): |
88 | | - rv["parent_span_id"] = span["parent_span_id"] |
| 112 | + if "start_timestamp" in span: |
| 113 | + start_timestamp = None |
| 114 | + try: |
| 115 | + start_timestamp = datetime_from_isoformat(span["start_timestamp"]) |
| 116 | + except Exception: |
| 117 | + pass |
| 118 | + |
| 119 | + if start_timestamp is not None: |
| 120 | + res["start_timestamp"] = start_timestamp.timestamp() |
| 121 | + |
| 122 | + if "timestamp" in span: |
| 123 | + end_timestamp = None |
| 124 | + try: |
| 125 | + end_timestamp = datetime_from_isoformat(span["timestamp"]) |
| 126 | + except Exception: |
| 127 | + pass |
89 | 128 |
|
90 | | - status = span.get("status") |
91 | | - if status and status != "ok": |
92 | | - rv["status"] = "error" |
| 129 | + if end_timestamp is not None: |
| 130 | + res["end_timestamp"] = end_timestamp.timestamp() |
| 131 | + |
| 132 | + if "parent_span_id" in span: |
| 133 | + res["parent_span_id"] = span["parent_span_id"] |
| 134 | + |
| 135 | + if "status" in span and span["status"] != "ok": |
| 136 | + res["status"] = "error" |
93 | 137 |
|
94 | 138 | attributes: "Dict[str, Any]" = {} |
95 | 139 |
|
96 | | - if span.get("op"): |
| 140 | + if "op" in span: |
97 | 141 | attributes["sentry.op"] = span["op"] |
98 | | - if span.get("origin"): |
| 142 | + if "origin" in span: |
99 | 143 | attributes["sentry.origin"] = span["origin"] |
100 | 144 |
|
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"): |
| 145 | + span_data = span.get("data") |
| 146 | + if isinstance(span_data, dict): |
| 147 | + attributes.update(span_data) |
| 148 | + |
| 149 | + span_tags = span.get("tags") |
| 150 | + if isinstance(span_tags, dict): |
| 151 | + attributes.update(span_tags) |
| 152 | + |
| 153 | + # See Scope._apply_user_attributes_to_telemetry() for user attributes. |
| 154 | + user = event.get("user") |
| 155 | + if isinstance(user, dict): |
| 156 | + if "id" in user: |
| 157 | + attributes["user.id"] = user["id"] |
| 158 | + if "username" in user: |
| 159 | + attributes["user.name"] = user["username"] |
| 160 | + if "email" in user: |
| 161 | + attributes["user.email"] = user["email"] |
| 162 | + |
| 163 | + # See Scope.set_global_attributes() for release, environment, and SDK metadata. |
| 164 | + if "release" in event: |
110 | 165 | attributes["sentry.release"] = event["release"] |
111 | | - if event.get("environment"): |
| 166 | + if "environment" in event: |
112 | 167 | attributes["sentry.environment"] = event["environment"] |
113 | | - if event.get("transaction"): |
| 168 | + if "transaction" in event: |
114 | 169 | attributes["sentry.segment.name"] = event["transaction"] |
115 | 170 |
|
116 | | - if trace_context.get("span_id"): |
| 171 | + trace_context = event.get("contexts", {}).get("trace", {}) |
| 172 | + if "span_id" in trace_context: |
117 | 173 | 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"] |
| 174 | + |
| 175 | + sdk_info = event.get("sdk") |
| 176 | + if isinstance(sdk_info, dict): |
| 177 | + if "name" in sdk_info: |
| 178 | + attributes["sentry.sdk.name"] = sdk_info["name"] |
| 179 | + if "version" in sdk_info: |
| 180 | + attributes["sentry.sdk.version"] = sdk_info["version"] |
122 | 181 |
|
123 | 182 | if attributes: |
124 | | - rv["attributes"] = {k: serialize_attribute(v) for k, v in attributes.items()} |
| 183 | + res["attributes"] = {k: serialize_attribute(v) for k, v in attributes.items()} |
125 | 184 |
|
126 | | - return rv |
| 185 | + return res |
127 | 186 |
|
128 | 187 |
|
129 | | -if TYPE_CHECKING: |
130 | | - from typing import Any |
131 | | - from typing import Callable |
132 | | - from typing import Optional |
133 | | - from typing import Sequence |
134 | | - from typing import Type |
135 | | - from typing import Union |
136 | | - from typing import TypeVar |
| 188 | +def _split_gen_ai_spans( |
| 189 | + event_opt: "Event", |
| 190 | +) -> "tuple[List[Dict[str, object]], List[Dict[str, object]]]": |
| 191 | + if "spans" not in event_opt: |
| 192 | + return [], [] |
137 | 193 |
|
138 | | - from sentry_sdk._types import Event, Hint, SDKInfo, Log, Metric, EventDataCategory |
139 | | - from sentry_sdk.integrations import Integration |
140 | | - from sentry_sdk.scope import Scope |
141 | | - from sentry_sdk.session import Session |
142 | | - from sentry_sdk.spotlight import SpotlightClient |
143 | | - from sentry_sdk.traces import StreamedSpan |
144 | | - from sentry_sdk.transport import Transport, Item, PayloadRef |
145 | | - from sentry_sdk._log_batcher import LogBatcher |
146 | | - from sentry_sdk._metrics_batcher import MetricsBatcher |
147 | | - from sentry_sdk.utils import Dsn |
| 194 | + spans = event_opt["spans"] |
| 195 | + if isinstance(spans, AnnotatedValue): |
| 196 | + spans = spans.value |
148 | 197 |
|
149 | | - I = TypeVar("I", bound=Integration) # noqa: E741 |
150 | | - |
151 | | -_client_init_debug = ContextVar("client_init_debug") |
| 198 | + if not isinstance(spans, Iterable): |
| 199 | + return [], [] |
152 | 200 |
|
| 201 | + non_gen_ai_spans = [] |
| 202 | + gen_ai_spans = [] |
| 203 | + for span in spans: |
| 204 | + span_op = span.get("op") |
| 205 | + if isinstance(span_op, str) and span_op.startswith("gen_ai."): |
| 206 | + gen_ai_spans.append(span) |
| 207 | + else: |
| 208 | + non_gen_ai_spans.append(span) |
153 | 209 |
|
154 | | -SDK_INFO: "SDKInfo" = { |
155 | | - "name": "sentry.python", # SDK name will be overridden after integrations have been loaded with sentry_sdk.integrations.setup_integrations() |
156 | | - "version": VERSION, |
157 | | - "packages": [{"name": "pypi:sentry-sdk", "version": VERSION}], |
158 | | -} |
| 210 | + return non_gen_ai_spans, gen_ai_spans |
159 | 211 |
|
160 | 212 |
|
161 | 213 | def _get_options(*args: "Optional[str]", **kwargs: "Any") -> "Dict[str, Any]": |
@@ -982,32 +1034,27 @@ def capture_event( |
982 | 1034 | if isinstance(profile, Profile): |
983 | 1035 | envelope.add_profile(profile.to_json(event_opt, self.options)) |
984 | 1036 |
|
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) |
| 1037 | + non_gen_ai_spans, gen_ai_spans = _split_gen_ai_spans(event_opt) |
993 | 1038 |
|
994 | | - if nonstreamed_spans: |
995 | | - event_opt["spans"] = nonstreamed_spans |
996 | | - envelope.add_transaction(event_opt) |
| 1039 | + event_opt["spans"] = non_gen_ai_spans |
| 1040 | + envelope.add_transaction(event_opt) |
997 | 1041 |
|
998 | | - if streamed_spans: |
| 1042 | + if gen_ai_spans: |
999 | 1043 | envelope.add_item( |
1000 | 1044 | Item( |
1001 | 1045 | type=SpanBatcher.TYPE, |
1002 | 1046 | content_type=SpanBatcher.CONTENT_TYPE, |
1003 | 1047 | headers={ |
1004 | | - "item_count": len(streamed_spans), |
| 1048 | + "item_count": len(gen_ai_spans), |
1005 | 1049 | }, |
1006 | 1050 | payload=PayloadRef( |
1007 | 1051 | json={ |
1008 | 1052 | "items": [ |
1009 | | - _v1_span_to_v2(span, event) |
1010 | | - for span in streamed_spans |
| 1053 | + _serialized_v1_span_to_serialized_v2_span( |
| 1054 | + span, event |
| 1055 | + ) |
| 1056 | + for span in gen_ai_spans |
| 1057 | + if isinstance(span, dict) |
1011 | 1058 | ] |
1012 | 1059 | }, |
1013 | 1060 | ), |
|
0 commit comments