diff --git a/src/sentry/api/endpoints/project_trace_item_details.py b/src/sentry/api/endpoints/project_trace_item_details.py index b2c5363c6644cd..78dd7d8b5ced7d 100644 --- a/src/sentry/api/endpoints/project_trace_item_details.py +++ b/src/sentry/api/endpoints/project_trace_item_details.py @@ -170,11 +170,6 @@ def convert_rpc_attribute_to_json( if external_name in seen_sentry_conventions: continue seen_sentry_conventions.add(external_name) - else: - if external_name and is_sentry_convention_replacement_attribute( - external_name, trace_item_type - ): - continue if trace_item_type == SupportedTraceItemType.SPANS and internal_name.startswith("sentry."): internal_name = internal_name.replace("sentry.", "", count=1) diff --git a/src/sentry/search/eap/utils.py b/src/sentry/search/eap/utils.py index 483be7ee0e277e..f6d61014606d7d 100644 --- a/src/sentry/search/eap/utils.py +++ b/src/sentry/search/eap/utils.py @@ -128,6 +128,17 @@ def add_start_end_conditions( } +SENTRY_CONVENTIONS_REVERSE_REPLACEMENT_MAP: dict[SupportedTraceItemType, dict[str, set[str]]] = {} +for _item_type, _replacement_map in SENTRY_CONVENTIONS_REPLACEMENT_MAPPINGS.items(): + _internal_mapping = PUBLIC_ALIAS_TO_INTERNAL_MAPPING.get(_item_type, {}) + _reverse: dict[str, set[str]] = {} + for _deprecated_alias, _replacement in _replacement_map.items(): + _resolved = _internal_mapping.get(_deprecated_alias) + _internal_name = _resolved.internal_name if _resolved else _deprecated_alias + _reverse.setdefault(_replacement, set()).add(_internal_name) + SENTRY_CONVENTIONS_REVERSE_REPLACEMENT_MAP[_item_type] = _reverse + + INTERNAL_TO_SECONDARY_ALIASES: dict[SupportedTraceItemType, dict[str, set[str]]] = { SupportedTraceItemType.SPANS: SPAN_INTERNAL_TO_SECONDARY_ALIASES_MAPPING, SupportedTraceItemType.LOGS: LOGS_INTERNAL_TO_SECONDARY_ALIASES_MAPPING, @@ -232,6 +243,12 @@ def is_sentry_convention_replacement_attribute( return public_alias in SENTRY_CONVENTIONS_REPLACEMENT_ATTRIBUTES.get(item_type, {}) +def get_deprecated_source_internal_names( + replacement: str, item_type: SupportedTraceItemType +) -> set[str]: + return SENTRY_CONVENTIONS_REVERSE_REPLACEMENT_MAP.get(item_type, {}).get(replacement, set()) + + def translate_to_sentry_conventions(public_alias: str, item_type: SupportedTraceItemType) -> str: mapping = SENTRY_CONVENTIONS_REPLACEMENT_MAPPINGS.get(item_type, {}) return mapping.get(public_alias, public_alias) diff --git a/tests/sentry/api/endpoints/test_project_trace_item_details.py b/tests/sentry/api/endpoints/test_project_trace_item_details.py index add0c9809a832c..470655d767d044 100644 --- a/tests/sentry/api/endpoints/test_project_trace_item_details.py +++ b/tests/sentry/api/endpoints/test_project_trace_item_details.py @@ -1,3 +1,5 @@ +import pytest + from sentry.api.endpoints.project_trace_item_details import convert_rpc_attribute_to_json from sentry.search.eap.types import SupportedTraceItemType @@ -72,3 +74,57 @@ def test_convert_rpc_attribute_to_json_exposes_array_with_array_flag() -> None: "value": ["assistant output"], } ] + + +class TestReplacementAttributeFiltering: + """When use_sentry_conventions is off, replacement attributes should only be + hidden if a deprecated source attribute is also present in the response.""" + + @pytest.mark.parametrize( + "attr_name,attr_value", + [ + ("gen_ai.usage.input_tokens", {"valInt": "42"}), + ("gen_ai.input.messages", {"valStr": '["hello"]'}), + ("gen_ai.output.messages", {"valStr": '["world"]'}), + ], + ) + def test_replacement_attribute_shown_when_no_deprecated_source( + self, attr_name: str, attr_value: dict[str, str] + ) -> None: + result = convert_rpc_attribute_to_json( + [{"name": attr_name, "value": attr_value}], + SupportedTraceItemType.SPANS, + use_sentry_conventions=False, + ) + + assert len(result) == 1 + assert result[0]["name"] == attr_name + + def test_replacement_attribute_shown_alongside_deprecated_source(self) -> None: + result = convert_rpc_attribute_to_json( + [ + {"name": "gen_ai.usage.prompt_tokens", "value": {"valInt": "42"}}, + {"name": "gen_ai.usage.input_tokens", "value": {"valInt": "42"}}, + ], + SupportedTraceItemType.SPANS, + use_sentry_conventions=False, + ) + + names = [r["name"] for r in result] + assert "gen_ai.usage.prompt_tokens" in names + assert "gen_ai.usage.input_tokens" in names + + def test_replacement_array_shown_when_no_deprecated_source(self) -> None: + result = convert_rpc_attribute_to_json( + [ + { + "name": "gen_ai.output.messages", + "value": {"valArray": {"values": [{"valStr": "output"}]}}, + } + ], + SupportedTraceItemType.SPANS, + use_sentry_conventions=False, + ) + + assert len(result) == 1 + assert result[0]["name"] == "gen_ai.output.messages"