diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index d05b617e4c..0a40a659de 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -137,6 +137,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - The `enable_tracing` `init` option has been removed. Configure `traces_sample_rate` directly. - The `propagate_traces` `init` option has been removed. Use `trace_propagation_targets` instead. - The `custom_sampling_context` parameter of `start_transaction` has been removed. Use `attributes` instead to set key-value pairs of data that should be accessible in the traces sampler. Note that span attributes need to conform to the [OpenTelemetry specification](https://opentelemetry.io/docs/concepts/signals/traces/#attributes), meaning only certain types can be set as values. +- `set_measurement` has been removed. - The PyMongo integration no longer sets tags. The data is still accessible via span attributes. - The PyMongo integration doesn't set `operation_ids` anymore. The individual IDs (`operation_id`, `request_id`, `session_id`) are now accessible as separate span attributes. - `sentry_sdk.metrics` and associated metrics APIs have been removed as Sentry no longer accepts metrics data in this form. See https://sentry.zendesk.com/hc/en-us/articles/26369339769883-Upcoming-API-Changes-to-Metrics diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py index 1529de592c..b35c446dc0 100644 --- a/sentry_sdk/__init__.py +++ b/sentry_sdk/__init__.py @@ -36,7 +36,6 @@ "set_context", "set_extra", "set_level", - "set_measurement", "set_tag", "set_tags", "set_user", diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index 9b320966ea..79260e3431 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -107,7 +107,6 @@ def substituted_because_contains_sensitive_data(cls): from typing import Callable from typing import Dict from typing import Mapping - from typing import NotRequired from typing import Optional from typing import Type from typing_extensions import Literal, TypedDict @@ -120,45 +119,6 @@ class SDKInfo(TypedDict): # "critical" is an alias of "fatal" recognized by Relay LogLevelStr = Literal["fatal", "critical", "error", "warning", "info", "debug"] - DurationUnit = Literal[ - "nanosecond", - "microsecond", - "millisecond", - "second", - "minute", - "hour", - "day", - "week", - ] - - InformationUnit = Literal[ - "bit", - "byte", - "kilobyte", - "kibibyte", - "megabyte", - "mebibyte", - "gigabyte", - "gibibyte", - "terabyte", - "tebibyte", - "petabyte", - "pebibyte", - "exabyte", - "exbibyte", - ] - - FractionUnit = Literal["ratio", "percent"] - MeasurementUnit = Union[DurationUnit, InformationUnit, FractionUnit, str] - - MeasurementValue = TypedDict( - "MeasurementValue", - { - "value": float, - "unit": NotRequired[Optional[MeasurementUnit]], - }, - ) - Event = TypedDict( "Event", { @@ -180,7 +140,6 @@ class SDKInfo(TypedDict): "level": LogLevelStr, "logentry": Mapping[str, object], "logger": str, - "measurements": dict[str, MeasurementValue], "message": str, "modules": dict[str, str], "monitor_config": Mapping[str, object], diff --git a/sentry_sdk/ai/monitoring.py b/sentry_sdk/ai/monitoring.py index 08b6482da5..2b6a1cdf72 100644 --- a/sentry_sdk/ai/monitoring.py +++ b/sentry_sdk/ai/monitoring.py @@ -106,9 +106,9 @@ def record_token_usage( if ai_pipeline_name: span.set_attribute("ai.pipeline.name", ai_pipeline_name) if prompt_tokens is not None: - span.set_measurement("ai_prompt_tokens_used", value=prompt_tokens) + span.set_attribute("ai.prompt_tokens.used", prompt_tokens) if completion_tokens is not None: - span.set_measurement("ai_completion_tokens_used", value=completion_tokens) + span.set_attribute("ai.completion_tokens.used", completion_tokens) if ( total_tokens is None and prompt_tokens is not None @@ -116,4 +116,4 @@ def record_token_usage( ): total_tokens = prompt_tokens + completion_tokens if total_tokens is not None: - span.set_measurement("ai_total_tokens_used", total_tokens) + span.set_attribute("ai.total_tokens.used", total_tokens) diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 2ded31ee48..b8a2498d5d 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -59,7 +59,6 @@ "set_context", "set_extra", "set_level", - "set_measurement", "set_tag", "set_tags", "set_user", @@ -287,13 +286,6 @@ def start_transaction( ) -def set_measurement(name, value, unit=""): - # type: (str, float, sentry_sdk._types.MeasurementUnit) -> None - transaction = get_current_scope().root_span - if transaction is not None: - transaction.set_measurement(name, value, unit) - - def get_current_span(scope=None): # type: (Optional[Scope]) -> Optional[sentry_sdk.tracing.Span] """ diff --git a/sentry_sdk/opentelemetry/consts.py b/sentry_sdk/opentelemetry/consts.py index 0e3cb54948..7f7afce9e2 100644 --- a/sentry_sdk/opentelemetry/consts.py +++ b/sentry_sdk/opentelemetry/consts.py @@ -26,7 +26,6 @@ class SentrySpanAttribute: DESCRIPTION = "sentry.description" OP = "sentry.op" ORIGIN = "sentry.origin" - MEASUREMENT = "sentry.measurement" TAG = "sentry.tag" NAME = "sentry.name" SOURCE = "sentry.source" diff --git a/sentry_sdk/opentelemetry/span_processor.py b/sentry_sdk/opentelemetry/span_processor.py index 6da616ed87..abfb712a89 100644 --- a/sentry_sdk/opentelemetry/span_processor.py +++ b/sentry_sdk/opentelemetry/span_processor.py @@ -304,10 +304,6 @@ def _common_span_transaction_attributes_as_json(self, span): "timestamp": convert_from_otel_timestamp(span.end_time), } # type: Event - measurements = extract_span_attributes(span, SentrySpanAttribute.MEASUREMENT) - if measurements: - common_json["measurements"] = measurements - tags = extract_span_attributes(span, SentrySpanAttribute.TAG) if tags: common_json["tags"] = tags diff --git a/sentry_sdk/opentelemetry/utils.py b/sentry_sdk/opentelemetry/utils.py index aa10e849ac..b9dbbd5f09 100644 --- a/sentry_sdk/opentelemetry/utils.py +++ b/sentry_sdk/opentelemetry/utils.py @@ -309,15 +309,7 @@ def extract_span_attributes(span, namespace): for attr, value in (span.attributes or {}).items(): if attr.startswith(namespace): key = attr[len(namespace) + 1 :] - - if namespace == SentrySpanAttribute.MEASUREMENT: - value = cast("tuple[str, str]", value) - extracted_attrs[key] = { - "value": float(value[0]), - "unit": value[1], - } - else: - extracted_attrs[key] = value + extracted_attrs[key] = value return extracted_attrs diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 7b8004c8b5..388cf38cef 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -65,7 +65,6 @@ R = TypeVar("R") from sentry_sdk._types import ( - MeasurementUnit, SamplingContext, ) @@ -150,10 +149,6 @@ def finish( # type: (...) -> None pass - def set_measurement(self, name, value, unit=""): - # type: (str, float, MeasurementUnit) -> None - pass - def set_context(self, key, value): # type: (str, dict[str, Any]) -> None pass @@ -540,13 +535,6 @@ def set_status(self, status): else: self._otel_span.set_status(Status(otel_status, otel_description)) - def set_measurement(self, name, value, unit=""): - # type: (str, float, MeasurementUnit) -> None - # Stringify value here since OTel expects all seq items to be of one type - self.set_attribute( - f"{SentrySpanAttribute.MEASUREMENT}.{name}", (str(value), unit) - ) - def set_thread(self, thread_id, thread_name): # type: (Optional[int], Optional[str]) -> None if thread_id is not None: diff --git a/tests/integrations/anthropic/test_anthropic.py b/tests/integrations/anthropic/test_anthropic.py index c318331972..5da9b870eb 100644 --- a/tests/integrations/anthropic/test_anthropic.py +++ b/tests/integrations/anthropic/test_anthropic.py @@ -127,9 +127,9 @@ def test_nonstreaming_create_message( assert SPANDATA.AI_INPUT_MESSAGES not in span["data"] assert SPANDATA.AI_RESPONSES not in span["data"] - assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 10 - assert span["measurements"]["ai_completion_tokens_used"]["value"] == 20 - assert span["measurements"]["ai_total_tokens_used"]["value"] == 30 + assert span["data"]["ai.prompt_tokens.used"] == 10 + assert span["data"]["ai.completion_tokens.used"] == 20 + assert span["data"]["ai.total_tokens.used"] == 30 assert span["data"]["ai.streaming"] is False @@ -197,9 +197,9 @@ async def test_nonstreaming_create_message_async( assert SPANDATA.AI_INPUT_MESSAGES not in span["data"] assert SPANDATA.AI_RESPONSES not in span["data"] - assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 10 - assert span["measurements"]["ai_completion_tokens_used"]["value"] == 20 - assert span["measurements"]["ai_total_tokens_used"]["value"] == 30 + assert span["data"]["ai.prompt_tokens.used"] == 10 + assert span["data"]["ai.completion_tokens.used"] == 20 + assert span["data"]["ai.total_tokens.used"] == 30 assert span["data"]["ai.streaming"] is False @@ -299,9 +299,9 @@ def test_streaming_create_message( assert SPANDATA.AI_INPUT_MESSAGES not in span["data"] assert SPANDATA.AI_RESPONSES not in span["data"] - assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 10 - assert span["measurements"]["ai_completion_tokens_used"]["value"] == 30 - assert span["measurements"]["ai_total_tokens_used"]["value"] == 40 + assert span["data"]["ai.prompt_tokens.used"] == 10 + assert span["data"]["ai.completion_tokens.used"] == 30 + assert span["data"]["ai.total_tokens.used"] == 40 assert span["data"]["ai.streaming"] is True @@ -404,9 +404,9 @@ async def test_streaming_create_message_async( assert SPANDATA.AI_INPUT_MESSAGES not in span["data"] assert SPANDATA.AI_RESPONSES not in span["data"] - assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 10 - assert span["measurements"]["ai_completion_tokens_used"]["value"] == 30 - assert span["measurements"]["ai_total_tokens_used"]["value"] == 40 + assert span["data"]["ai.prompt_tokens.used"] == 10 + assert span["data"]["ai.completion_tokens.used"] == 30 + assert span["data"]["ai.total_tokens.used"] == 40 assert span["data"]["ai.streaming"] is True @@ -536,9 +536,9 @@ def test_streaming_create_message_with_input_json_delta( assert SPANDATA.AI_INPUT_MESSAGES not in span["data"] assert SPANDATA.AI_RESPONSES not in span["data"] - assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 366 - assert span["measurements"]["ai_completion_tokens_used"]["value"] == 51 - assert span["measurements"]["ai_total_tokens_used"]["value"] == 417 + assert span["data"]["ai.prompt_tokens.used"] == 366 + assert span["data"]["ai.completion_tokens.used"] == 51 + assert span["data"]["ai.total_tokens.used"] == 417 assert span["data"]["ai.streaming"] is True @@ -675,9 +675,9 @@ async def test_streaming_create_message_with_input_json_delta_async( assert SPANDATA.AI_INPUT_MESSAGES not in span["data"] assert SPANDATA.AI_RESPONSES not in span["data"] - assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 366 - assert span["measurements"]["ai_completion_tokens_used"]["value"] == 51 - assert span["measurements"]["ai_total_tokens_used"]["value"] == 417 + assert span["data"]["ai.prompt_tokens.used"] == 366 + assert span["data"]["ai.completion_tokens.used"] == 51 + assert span["data"]["ai.total_tokens.used"] == 417 assert span["data"]["ai.streaming"] is True @@ -822,11 +822,6 @@ def test_add_ai_data_to_span_with_input_json_delta(sentry_init, capture_events): content_blocks=["{'test': 'data',", "'more': 'json'}"], ) - # assert span._data.get("ai.streaming") is True - # assert span._measurements.get("ai_prompt_tokens_used")["value"] == 10 - # assert span._measurements.get("ai_completion_tokens_used")["value"] == 20 - # assert span._measurements.get("ai_total_tokens_used")["value"] == 30 - (event,) = events assert len(event["spans"]) == 1 @@ -836,6 +831,6 @@ def test_add_ai_data_to_span_with_input_json_delta(sentry_init, capture_events): [{"type": "text", "text": "{'test': 'data','more': 'json'}"}] ) assert span["data"]["ai.streaming"] is True - assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 10 - assert span["measurements"]["ai_completion_tokens_used"]["value"] == 20 - assert span["measurements"]["ai_total_tokens_used"]["value"] == 30 + assert span["data"]["ai.prompt_tokens.used"] == 10 + assert span["data"]["ai.completion_tokens.used"] == 20 + assert span["data"]["ai.total_tokens.used"] == 30 diff --git a/tests/integrations/cohere/test_cohere.py b/tests/integrations/cohere/test_cohere.py index ff41ceba11..25d1c30cf4 100644 --- a/tests/integrations/cohere/test_cohere.py +++ b/tests/integrations/cohere/test_cohere.py @@ -64,9 +64,9 @@ def test_nonstreaming_chat( assert "ai.input_messages" not in span["data"] assert "ai.responses" not in span["data"] - assert span["measurements"]["ai_completion_tokens_used"]["value"] == 10 - assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 20 - assert span["measurements"]["ai_total_tokens_used"]["value"] == 30 + assert span["data"]["ai.completion_tokens.used"] == 10 + assert span["data"]["ai.prompt_tokens.used"] == 20 + assert span["data"]["ai.total_tokens.used"] == 30 # noinspection PyTypeChecker @@ -136,9 +136,9 @@ def test_streaming_chat(sentry_init, capture_events, send_default_pii, include_p assert "ai.input_messages" not in span["data"] assert "ai.responses" not in span["data"] - assert span["measurements"]["ai_completion_tokens_used"]["value"] == 10 - assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 20 - assert span["measurements"]["ai_total_tokens_used"]["value"] == 30 + assert span["data"]["ai.completion_tokens.used"] == 10 + assert span["data"]["ai.prompt_tokens.used"] == 20 + assert span["data"]["ai.total_tokens.used"] == 30 def test_bad_chat(sentry_init, capture_events): @@ -200,8 +200,8 @@ def test_embed(sentry_init, capture_events, send_default_pii, include_prompts): else: assert "ai.input_messages" not in span["data"] - assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 10 - assert span["measurements"]["ai_total_tokens_used"]["value"] == 10 + assert span["data"]["ai.prompt_tokens.used"] == 10 + assert span["data"]["ai.total_tokens.used"] == 10 def test_span_origin_chat(sentry_init, capture_events): diff --git a/tests/integrations/huggingface_hub/test_huggingface_hub.py b/tests/integrations/huggingface_hub/test_huggingface_hub.py index 17df29c331..9a867e718b 100644 --- a/tests/integrations/huggingface_hub/test_huggingface_hub.py +++ b/tests/integrations/huggingface_hub/test_huggingface_hub.py @@ -74,7 +74,7 @@ def test_nonstreaming_chat_completion( assert "ai.responses" not in span["data"] if details_arg: - assert span["measurements"]["ai_total_tokens_used"]["value"] == 10 + assert span["data"]["ai.total_tokens.used"] == 10 @pytest.mark.parametrize( @@ -133,7 +133,7 @@ def test_streaming_chat_completion( assert "ai.responses" not in span["data"] if details_arg: - assert span["measurements"]["ai_total_tokens_used"]["value"] == 10 + assert span["data"]["ai.total_tokens.used"] == 10 def test_bad_chat_completion(sentry_init, capture_events): diff --git a/tests/integrations/langchain/test_langchain.py b/tests/integrations/langchain/test_langchain.py index f8ab30054d..62f3eac04a 100644 --- a/tests/integrations/langchain/test_langchain.py +++ b/tests/integrations/langchain/test_langchain.py @@ -179,12 +179,13 @@ def test_langchain_agent( assert len(list(x for x in tx["spans"] if x["op"] == "ai.run.langchain")) > 0 if use_unknown_llm_type: - assert "ai_prompt_tokens_used" in chat_spans[0]["measurements"] - assert "ai_total_tokens_used" in chat_spans[0]["measurements"] + assert "ai.prompt_tokens.used" in chat_spans[0]["data"] + assert "ai.total_tokens.used" in chat_spans[0]["data"] else: # important: to avoid double counting, we do *not* measure # tokens used if we have an explicit integration (e.g. OpenAI) - assert "measurements" not in chat_spans[0] + assert "ai.prompt_tokens.used" not in chat_spans[0]["data"] + assert "ai.total_tokens.used" not in chat_spans[0]["data"] if send_default_pii and include_prompts: assert "You are very powerful" in chat_spans[0]["data"]["ai.input_messages"] diff --git a/tests/integrations/openai/test_openai.py b/tests/integrations/openai/test_openai.py index 0508d7d056..85ff95f377 100644 --- a/tests/integrations/openai/test_openai.py +++ b/tests/integrations/openai/test_openai.py @@ -89,9 +89,9 @@ def test_nonstreaming_chat_completion( assert "ai.input_messages" not in span["data"] assert "ai.responses" not in span["data"] - assert span["measurements"]["ai_completion_tokens_used"]["value"] == 10 - assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 20 - assert span["measurements"]["ai_total_tokens_used"]["value"] == 30 + assert span["data"]["ai.completion_tokens.used"] == 10 + assert span["data"]["ai.prompt_tokens.used"] == 20 + assert span["data"]["ai.total_tokens.used"] == 30 @pytest.mark.asyncio @@ -131,9 +131,9 @@ async def test_nonstreaming_chat_completion_async( assert "ai.input_messages" not in span["data"] assert "ai.responses" not in span["data"] - assert span["measurements"]["ai_completion_tokens_used"]["value"] == 10 - assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 20 - assert span["measurements"]["ai_total_tokens_used"]["value"] == 30 + assert span["data"]["ai.completion_tokens.used"] == 10 + assert span["data"]["ai.prompt_tokens.used"] == 20 + assert span["data"]["ai.total_tokens.used"] == 30 def tiktoken_encoding_if_installed(): @@ -227,9 +227,9 @@ def test_streaming_chat_completion( try: import tiktoken # type: ignore # noqa # pylint: disable=unused-import - assert span["measurements"]["ai_completion_tokens_used"]["value"] == 2 - assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 1 - assert span["measurements"]["ai_total_tokens_used"]["value"] == 3 + assert span["data"]["ai.completion_tokens.used"] == 2 + assert span["data"]["ai.prompt_tokens.used"] == 1 + assert span["data"]["ai.total_tokens.used"] == 3 except ImportError: pass # if tiktoken is not installed, we can't guarantee token usage will be calculated properly @@ -323,9 +323,9 @@ async def test_streaming_chat_completion_async( try: import tiktoken # type: ignore # noqa # pylint: disable=unused-import - assert span["measurements"]["ai_completion_tokens_used"]["value"] == 2 - assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 1 - assert span["measurements"]["ai_total_tokens_used"]["value"] == 3 + assert span["data"]["ai.completion_tokens.used"] == 2 + assert span["data"]["ai.prompt_tokens.used"] == 1 + assert span["data"]["ai.total_tokens.used"] == 3 except ImportError: pass # if tiktoken is not installed, we can't guarantee token usage will be calculated properly @@ -409,8 +409,8 @@ def test_embeddings_create( else: assert "ai.input_messages" not in span["data"] - assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 20 - assert span["measurements"]["ai_total_tokens_used"]["value"] == 30 + assert span["data"]["ai.prompt_tokens.used"] == 20 + assert span["data"]["ai.total_tokens.used"] == 30 @pytest.mark.asyncio @@ -457,8 +457,8 @@ async def test_embeddings_create_async( else: assert "ai.input_messages" not in span["data"] - assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 20 - assert span["measurements"]["ai_total_tokens_used"]["value"] == 30 + assert span["data"]["ai.prompt_tokens.used"] == 20 + assert span["data"]["ai.total_tokens.used"] == 30 @pytest.mark.forked diff --git a/tests/tracing/test_misc.py b/tests/tracing/test_misc.py index 5b0213d6c6..4d85594324 100644 --- a/tests/tracing/test_misc.py +++ b/tests/tracing/test_misc.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock import sentry_sdk -from sentry_sdk import start_span, set_measurement, get_current_scope +from sentry_sdk import start_span, get_current_scope from sentry_sdk.consts import MATCH_ALL from sentry_sdk.tracing_utils import should_propagate_trace from sentry_sdk.utils import Dsn @@ -115,46 +115,6 @@ def test_finds_spans_on_scope(sentry_init): assert child_span.root_span == root_span -def test_set_measurement(sentry_init, capture_events): - sentry_init(traces_sample_rate=1.0) - - events = capture_events() - - with start_span(name="measuring stuff") as span: - - with pytest.raises(TypeError): - span.set_measurement() - - with pytest.raises(TypeError): - span.set_measurement("metric.foo") - - span.set_measurement("metric.foo", 123) - span.set_measurement("metric.bar", 456, unit="second") - span.set_measurement("metric.baz", 420.69, unit="custom") - span.set_measurement("metric.foobar", 12, unit="percent") - span.set_measurement("metric.foobar", 17.99, unit="percent") - - (event,) = events - assert event["measurements"]["metric.foo"] == {"value": 123, "unit": ""} - assert event["measurements"]["metric.bar"] == {"value": 456, "unit": "second"} - assert event["measurements"]["metric.baz"] == {"value": 420.69, "unit": "custom"} - assert event["measurements"]["metric.foobar"] == {"value": 17.99, "unit": "percent"} - - -def test_set_measurement_public_api(sentry_init, capture_events): - sentry_init(traces_sample_rate=1.0) - - events = capture_events() - - with start_span(name="measuring stuff"): - set_measurement("metric.foo", 123) - set_measurement("metric.bar", 456, unit="second") - - (event,) = events - assert event["measurements"]["metric.foo"] == {"value": 123, "unit": ""} - assert event["measurements"]["metric.bar"] == {"value": 456, "unit": "second"} - - @pytest.mark.parametrize( "trace_propagation_targets,url,expected_propagation_decision", [