-
Notifications
You must be signed in to change notification settings - Fork 606
feat: Send GenAI spans as V2 envelope items #6079
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
2be94ca
01f479a
80e6a10
0622cf4
7c75da1
54a9b07
d1aa07c
117a6c9
83c36b5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,11 +2,12 @@ | |||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||
| import warnings | ||||||||||||||||||||||||||||||
| import json | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| from sentry_sdk._compat import check_uwsgi_thread_support | ||||||||||||||||||||||||||||||
| from sentry_sdk._metrics_batcher import MetricsBatcher | ||||||||||||||||||||||||||||||
|
|
@@ -30,6 +31,7 @@ | |||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| 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, | ||||||||||||||||||||||||||||||
|
|
@@ -38,6 +40,7 @@ | |||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| from sentry_sdk.consts import ( | ||||||||||||||||||||||||||||||
| SPANDATA, | ||||||||||||||||||||||||||||||
| SPANSTATUS, | ||||||||||||||||||||||||||||||
| DEFAULT_MAX_VALUE_LENGTH, | ||||||||||||||||||||||||||||||
| DEFAULT_OPTIONS, | ||||||||||||||||||||||||||||||
| INSTRUMENTER, | ||||||||||||||||||||||||||||||
|
|
@@ -56,6 +59,8 @@ | |||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||
|
|
@@ -66,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 | ||||||||||||||||||||||||||||||
|
|
@@ -89,6 +102,181 @@ | |||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| 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", | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
Check failure on line 125 in sentry_sdk/client.py
|
||||||||||||||||||||||||||||||
| 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]": | ||||||||||||||||||||||||||||||
| # See SpanBatcher._to_transport_format() for analogous population of all entries except "attributes". | ||||||||||||||||||||||||||||||
| res: "dict[str, Any]" = { | ||||||||||||||||||||||||||||||
| "status": SpanStatus.OK.value, | ||||||||||||||||||||||||||||||
| "is_segment": False, | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| 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 "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 | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| 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"] != SPANSTATUS.OK: | ||||||||||||||||||||||||||||||
| res["status"] = "error" | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| attributes: "Dict[str, Any]" = {} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if "op" in span: | ||||||||||||||||||||||||||||||
| attributes["sentry.op"] = span["op"] | ||||||||||||||||||||||||||||||
| if "origin" in span: | ||||||||||||||||||||||||||||||
| attributes["sentry.origin"] = span["origin"] | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| 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 "environment" in event: | ||||||||||||||||||||||||||||||
| attributes["sentry.environment"] = event["environment"] | ||||||||||||||||||||||||||||||
| if "transaction" in event: | ||||||||||||||||||||||||||||||
| attributes["sentry.segment.name"] = event["transaction"] | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| trace_context = event.get("contexts", {}).get("trace", {}) | ||||||||||||||||||||||||||||||
| if "span_id" in trace_context: | ||||||||||||||||||||||||||||||
| attributes["sentry.segment.id"] = trace_context["span_id"] | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| 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 not attributes: | ||||||||||||||||||||||||||||||
| return res | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| res["attributes"] = {} | ||||||||||||||||||||||||||||||
| for key, value in attributes.items(): | ||||||||||||||||||||||||||||||
| res["attributes"][key] = _serialized_v1_attribute_to_serialized_v2_attribute( | ||||||||||||||||||||||||||||||
| value | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return res | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| def _split_gen_ai_spans( | ||||||||||||||||||||||||||||||
| event_opt: "Event", | ||||||||||||||||||||||||||||||
| ) -> "Optional[tuple[List[Dict[str, object]], List[Dict[str, object]]]]": | ||||||||||||||||||||||||||||||
| if "spans" not in event_opt: | ||||||||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| spans: "Any" = event_opt["spans"] | ||||||||||||||||||||||||||||||
| if isinstance(spans, AnnotatedValue): | ||||||||||||||||||||||||||||||
| spans = spans.value | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if not isinstance(spans, Iterable): | ||||||||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| non_gen_ai_spans = [] | ||||||||||||||||||||||||||||||
| gen_ai_spans = [] | ||||||||||||||||||||||||||||||
| for span in spans: | ||||||||||||||||||||||||||||||
| span_op = span.get("op") | ||||||||||||||||||||||||||||||
|
Check warning on line 271 in sentry_sdk/client.py
|
||||||||||||||||||||||||||||||
| if isinstance(span_op, str) and span_op.startswith("gen_ai."): | ||||||||||||||||||||||||||||||
| gen_ai_spans.append(span) | ||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||
| non_gen_ai_spans.append(span) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return non_gen_ai_spans, gen_ai_spans | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| def _get_options(*args: "Optional[str]", **kwargs: "Any") -> "Dict[str, Any]": | ||||||||||||||||||||||||||||||
| if args and (isinstance(args[0], (bytes, str)) or args[0] is None): | ||||||||||||||||||||||||||||||
| dsn: "Optional[str]" = args[0] | ||||||||||||||||||||||||||||||
|
|
@@ -912,7 +1100,37 @@ | |||||||||||||||||||||||||||||
| if is_transaction: | ||||||||||||||||||||||||||||||
| if isinstance(profile, Profile): | ||||||||||||||||||||||||||||||
| envelope.add_profile(profile.to_json(event_opt, self.options)) | ||||||||||||||||||||||||||||||
| envelope.add_transaction(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) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| envelope.add_item( | ||||||||||||||||||||||||||||||
| Item( | ||||||||||||||||||||||||||||||
| type=SpanBatcher.TYPE, | ||||||||||||||||||||||||||||||
| content_type=SpanBatcher.CONTENT_TYPE, | ||||||||||||||||||||||||||||||
| headers={ | ||||||||||||||||||||||||||||||
| "item_count": len(gen_ai_spans), | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
| payload=PayloadRef( | ||||||||||||||||||||||||||||||
| json={ | ||||||||||||||||||||||||||||||
| "items": [ | ||||||||||||||||||||||||||||||
| _serialized_v1_span_to_serialized_v2_span( | ||||||||||||||||||||||||||||||
| span, event | ||||||||||||||||||||||||||||||
|
Check warning on line 1124 in sentry_sdk/client.py
|
||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
Check failure on line 1125 in sentry_sdk/client.py
|
||||||||||||||||||||||||||||||
| for span in gen_ai_spans | ||||||||||||||||||||||||||||||
| if isinstance(span, dict) | ||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||
|
Check warning on line 1128 in sentry_sdk/client.py
|
||||||||||||||||||||||||||||||
|
Comment on lines
+1118
to
+1128
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. item_count header may not match actual items due to isinstance filtering The VerificationVerified by examining: (1) Suggested fix: Either filter gen_ai_spans before calculating item_count, or compute item_count from the filtered list.
Suggested change
Identified by Warden |
||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| elif is_checkin: | ||||||||||||||||||||||||||||||
| envelope.add_checkin(event_opt) | ||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.