Skip to content

Commit 01f479a

Browse files
.
1 parent 2be94ca commit 01f479a

1 file changed

Lines changed: 130 additions & 83 deletions

File tree

sentry_sdk/client.py

Lines changed: 130 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import uuid
33
import random
44
import socket
5-
from collections.abc import Mapping
5+
from collections.abc import Mapping, Iterable
66
from datetime import datetime, timezone
77
from importlib import import_module
88
from typing import TYPE_CHECKING, List, Dict, cast, overload
@@ -58,104 +58,156 @@
5858
from sentry_sdk.scrubber import EventScrubber
5959
from sentry_sdk.monitor import Monitor
6060
from sentry_sdk.envelope import Item, PayloadRef
61+
from sentry_sdk.utils import datetime_from_isoformat
6162

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
6282

63-
_ISO_TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
83+
I = TypeVar("I", bound=Integration) # noqa: E741
6484

85+
_client_init_debug = ContextVar("client_init_debug")
6586

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+
}
7292

7393

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]" = {
8199
"status": "ok",
100+
"is_segment": False,
82101
}
83102

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"]
86111

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
89128

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"
93137

94138
attributes: "Dict[str, Any]" = {}
95139

96-
if span.get("op"):
140+
if "op" in span:
97141
attributes["sentry.op"] = span["op"]
98-
if span.get("origin"):
142+
if "origin" in span:
99143
attributes["sentry.origin"] = span["origin"]
100144

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:
110165
attributes["sentry.release"] = event["release"]
111-
if event.get("environment"):
166+
if "environment" in event:
112167
attributes["sentry.environment"] = event["environment"]
113-
if event.get("transaction"):
168+
if "transaction" in event:
114169
attributes["sentry.segment.name"] = event["transaction"]
115170

116-
if trace_context.get("span_id"):
171+
trace_context = event.get("contexts", {}).get("trace", {})
172+
if "span_id" in trace_context:
117173
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"]
122181

123182
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()}
125184

126-
return rv
185+
return res
127186

128187

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 [], []
137193

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
148197

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 [], []
152200

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)
153209

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
159211

160212

161213
def _get_options(*args: "Optional[str]", **kwargs: "Any") -> "Dict[str, Any]":
@@ -982,32 +1034,27 @@ def capture_event(
9821034
if isinstance(profile, Profile):
9831035
envelope.add_profile(profile.to_json(event_opt, self.options))
9841036

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)
9931038

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)
9971041

998-
if streamed_spans:
1042+
if gen_ai_spans:
9991043
envelope.add_item(
10001044
Item(
10011045
type=SpanBatcher.TYPE,
10021046
content_type=SpanBatcher.CONTENT_TYPE,
10031047
headers={
1004-
"item_count": len(streamed_spans),
1048+
"item_count": len(gen_ai_spans),
10051049
},
10061050
payload=PayloadRef(
10071051
json={
10081052
"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)
10111058
]
10121059
},
10131060
),

0 commit comments

Comments
 (0)