From 843acdc9f3d4c70f969719851fe970264060e653 Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Fri, 7 Nov 2025 14:32:09 +0100 Subject: [PATCH 1/8] feat(consumers): ensure that gen_ai spans get the `gen_ai.agent.name` from the ancestor span --- .../consumers/process_segments/enrichment.py | 31 ++- .../process_segments/test_enrichment.py | 180 ++++++++++++++++++ 2 files changed, 210 insertions(+), 1 deletion(-) diff --git a/src/sentry/spans/consumers/process_segments/enrichment.py b/src/sentry/spans/consumers/process_segments/enrichment.py index b2736cb92a6d9d..11fe24e4765cca 100644 --- a/src/sentry/spans/consumers/process_segments/enrichment.py +++ b/src/sentry/spans/consumers/process_segments/enrichment.py @@ -1,5 +1,5 @@ from collections import defaultdict -from collections.abc import Sequence +from collections.abc import Iterator, Sequence from typing import Any from sentry_kafka_schemas.schema_types.ingest_spans_v1 import SpanEvent @@ -81,6 +81,10 @@ def __init__(self, spans: list[SpanEvent]) -> None: interval = _span_interval(span) self._span_map.setdefault(parent_span_id, []).append(interval) + self._span_hierarchy: dict[str, SpanEvent] = { + span["span_id"]: span for span in spans if "span_id" in span + } + def _attributes(self, span: SpanEvent) -> dict[str, Any]: attributes: dict[str, Any] = {**(span.get("attributes") or {})} @@ -119,6 +123,23 @@ def get_value(key: str) -> Any: if attributes.get(key) is None: attributes[key] = value + GENAI_SPAN_OPS_AGENT_CHILDREN = [ + "gen_ai.execute_tool", + "gen_ai.handoff", + "gen_ai.run", + "gen_ai.create_agent", + ] + op = get_span_op(span) + + if op in GENAI_SPAN_OPS_AGENT_CHILDREN and "gen_ai.agent.name" not in attributes: + for ancestor in self._iter_ancestors(span): + if "gen_ai.agent.name" in ancestor.get("attributes") or {}: + attributes["gen_ai.agent.name"] = { + "type": "string", + "value": attribute_value(ancestor, "gen_ai.agent.name"), + } + break + attributes["sentry.exclusive_time_ms"] = { "type": "double", "value": self._exclusive_time(span), @@ -126,6 +147,14 @@ def get_value(key: str) -> Any: return attributes + def _iter_ancestors(self, span: SpanEvent) -> Iterator[SpanEvent]: + """ + Iterates over the ancestors of a span in order towards the root using the "parent_span_id" attribute. + """ + current = span + while current := self._span_hierarchy.get(current.get("parent_span_id")): + yield current + def _exclusive_time(self, span: SpanEvent) -> float: """ Sets the exclusive time on all spans in the list. diff --git a/tests/sentry/spans/consumers/process_segments/test_enrichment.py b/tests/sentry/spans/consumers/process_segments/test_enrichment.py index 6c9ec9071d7a04..b8c9ad059a4252 100644 --- a/tests/sentry/spans/consumers/process_segments/test_enrichment.py +++ b/tests/sentry/spans/consumers/process_segments/test_enrichment.py @@ -471,3 +471,183 @@ def _mock_performance_issue_span(is_segment, attributes, **fields) -> SpanEvent: **fields, }, ) + + +def test_enrich_gen_ai_agent_name_from_immediate_parent() -> None: + """Test that gen_ai.agent.name is inherited from the immediate parent.""" + parent_span = build_mock_span( + project_id=1, + is_segment=True, + span_id="aaaaaaaaaaaaaaaa", + start_timestamp=1609455600.0, + end_timestamp=1609455605.0, + span_op="gen_ai.create_agent", + attributes={ + "gen_ai.agent.name": {"type": "string", "value": "MyAgent"}, + }, + ) + + child_span = build_mock_span( + project_id=1, + span_id="bbbbbbbbbbbbbbbb", + parent_span_id="aaaaaaaaaaaaaaaa", + start_timestamp=1609455601.0, + end_timestamp=1609455602.0, + span_op="gen_ai.execute_tool", + ) + + spans = [parent_span, child_span] + _, enriched_spans = TreeEnricher.enrich_spans(spans) + compatible_spans = [make_compatible(span) for span in enriched_spans] + + parent, child = compatible_spans + assert attribute_value(parent, "gen_ai.agent.name") == "MyAgent" + assert attribute_value(child, "gen_ai.agent.name") == "MyAgent" + + +def test_enrich_gen_ai_agent_name_from_ancestor() -> None: + """Test that gen_ai.agent.name is inherited from a grandparent ancestor.""" + grandparent_span = build_mock_span( + project_id=1, + is_segment=True, + span_id="aaaaaaaaaaaaaaaa", + start_timestamp=1609455600.0, + end_timestamp=1609455605.0, + span_op="gen_ai.create_agent", + attributes={ + "gen_ai.agent.name": {"type": "string", "value": "GrandparentAgent"}, + }, + ) + + parent_span = build_mock_span( + project_id=1, + span_id="bbbbbbbbbbbbbbbb", + parent_span_id="aaaaaaaaaaaaaaaa", + start_timestamp=1609455601.0, + end_timestamp=1609455604.0, + span_op="some.operation", + ) + + child_span = build_mock_span( + project_id=1, + span_id="cccccccccccccccc", + parent_span_id="bbbbbbbbbbbbbbbb", + start_timestamp=1609455602.0, + end_timestamp=1609455603.0, + span_op="gen_ai.run", + ) + + spans = [grandparent_span, parent_span, child_span] + _, enriched_spans = TreeEnricher.enrich_spans(spans) + compatible_spans = [make_compatible(span) for span in enriched_spans] + + grandparent, parent, child = compatible_spans + assert attribute_value(grandparent, "gen_ai.agent.name") == "GrandparentAgent" + assert attribute_value(parent, "gen_ai.agent.name") is None + assert attribute_value(child, "gen_ai.agent.name") == "GrandparentAgent" + + +def test_enrich_gen_ai_agent_name_not_overwritten() -> None: + """Test that gen_ai.agent.name is not overwritten if already set on the child.""" + parent_span = build_mock_span( + project_id=1, + is_segment=True, + span_id="aaaaaaaaaaaaaaaa", + start_timestamp=1609455600.0, + end_timestamp=1609455605.0, + span_op="gen_ai.create_agent", + attributes={ + "gen_ai.agent.name": {"type": "string", "value": "ParentAgent"}, + }, + ) + + child_span = build_mock_span( + project_id=1, + span_id="bbbbbbbbbbbbbbbb", + parent_span_id="aaaaaaaaaaaaaaaa", + start_timestamp=1609455601.0, + end_timestamp=1609455602.0, + span_op="gen_ai.handoff", + attributes={ + "gen_ai.agent.name": {"type": "string", "value": "ChildAgent"}, + }, + ) + + spans = [parent_span, child_span] + _, enriched_spans = TreeEnricher.enrich_spans(spans) + compatible_spans = [make_compatible(span) for span in enriched_spans] + + parent, child = compatible_spans + assert attribute_value(parent, "gen_ai.agent.name") == "ParentAgent" + assert attribute_value(child, "gen_ai.agent.name") == "ChildAgent" + + +def test_enrich_gen_ai_agent_name_not_set_without_ancestor() -> None: + """Test that gen_ai.agent.name is not set if no ancestor has it.""" + parent_span = build_mock_span( + project_id=1, + is_segment=True, + span_id="aaaaaaaaaaaaaaaa", + start_timestamp=1609455600.0, + end_timestamp=1609455605.0, + span_op="some.operation", + ) + + child_span = build_mock_span( + project_id=1, + span_id="bbbbbbbbbbbbbbbb", + parent_span_id="aaaaaaaaaaaaaaaa", + start_timestamp=1609455601.0, + end_timestamp=1609455602.0, + span_op="gen_ai.execute_tool", + ) + + spans = [parent_span, child_span] + _, enriched_spans = TreeEnricher.enrich_spans(spans) + compatible_spans = [make_compatible(span) for span in enriched_spans] + + parent, child = compatible_spans + assert attribute_value(parent, "gen_ai.agent.name") is None + assert attribute_value(child, "gen_ai.agent.name") is None + + +def test_enrich_gen_ai_agent_name_not_from_sibling() -> None: + """Test that gen_ai.agent.name is not taken from a sibling span.""" + parent_span = build_mock_span( + project_id=1, + is_segment=True, + span_id="aaaaaaaaaaaaaaaa", + start_timestamp=1609455600.0, + end_timestamp=1609455605.0, + span_op="some.operation", + ) + + sibling_with_agent = build_mock_span( + project_id=1, + span_id="bbbbbbbbbbbbbbbb", + parent_span_id="aaaaaaaaaaaaaaaa", + start_timestamp=1609455601.0, + end_timestamp=1609455602.0, + span_op="gen_ai.create_agent", + attributes={ + "gen_ai.agent.name": {"type": "string", "value": "SiblingAgent"}, + }, + ) + + target_child = build_mock_span( + project_id=1, + span_id="cccccccccccccccc", + parent_span_id="aaaaaaaaaaaaaaaa", + start_timestamp=1609455602.5, + end_timestamp=1609455603.5, + span_op="gen_ai.execute_tool", + ) + + spans = [parent_span, sibling_with_agent, target_child] + _, enriched_spans = TreeEnricher.enrich_spans(spans) + compatible_spans = [make_compatible(span) for span in enriched_spans] + + parent, sibling, target = compatible_spans + assert attribute_value(parent, "gen_ai.agent.name") is None + assert attribute_value(sibling, "gen_ai.agent.name") == "SiblingAgent" + assert attribute_value(target, "gen_ai.agent.name") is None From aeff874b053758fc3042ab992f3ab7ce74ac8a66 Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Mon, 10 Nov 2025 08:53:30 +0100 Subject: [PATCH 2/8] fix: removed unnecessary additional loop over spans and fixed issue with predicate whether agent name exists as an attribute --- .../spans/consumers/process_segments/enrichment.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/sentry/spans/consumers/process_segments/enrichment.py b/src/sentry/spans/consumers/process_segments/enrichment.py index 11fe24e4765cca..ac5fb04a2dac31 100644 --- a/src/sentry/spans/consumers/process_segments/enrichment.py +++ b/src/sentry/spans/consumers/process_segments/enrichment.py @@ -76,15 +76,14 @@ def __init__(self, spans: list[SpanEvent]) -> None: self._ttfd_ts = _timestamp_by_op(spans, "ui.load.full_display") self._span_map: dict[str, list[tuple[int, int]]] = {} + self._span_hierarchy: dict[str, SpanEvent] = {} for span in spans: + if "span_id" in span: + self._span_hierarchy[span["span_id"]] = span if parent_span_id := span.get("parent_span_id"): interval = _span_interval(span) self._span_map.setdefault(parent_span_id, []).append(interval) - self._span_hierarchy: dict[str, SpanEvent] = { - span["span_id"]: span for span in spans if "span_id" in span - } - def _attributes(self, span: SpanEvent) -> dict[str, Any]: attributes: dict[str, Any] = {**(span.get("attributes") or {})} @@ -133,10 +132,10 @@ def get_value(key: str) -> Any: if op in GENAI_SPAN_OPS_AGENT_CHILDREN and "gen_ai.agent.name" not in attributes: for ancestor in self._iter_ancestors(span): - if "gen_ai.agent.name" in ancestor.get("attributes") or {}: + if (agent_name := attribute_value(ancestor, "gen_ai.agent.name")) is not None: attributes["gen_ai.agent.name"] = { "type": "string", - "value": attribute_value(ancestor, "gen_ai.agent.name"), + "value": agent_name, } break From 53e8bd9f51ec5e8bc230e92d011073191388f632 Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Mon, 10 Nov 2025 09:32:21 +0100 Subject: [PATCH 3/8] fix: resolve agent name for all gen_ai spans and only fetch it from invoke_agent ancestors --- .../consumers/process_segments/enrichment.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/sentry/spans/consumers/process_segments/enrichment.py b/src/sentry/spans/consumers/process_segments/enrichment.py index ac5fb04a2dac31..8dcd8d35e686a3 100644 --- a/src/sentry/spans/consumers/process_segments/enrichment.py +++ b/src/sentry/spans/consumers/process_segments/enrichment.py @@ -122,17 +122,13 @@ def get_value(key: str) -> Any: if attributes.get(key) is None: attributes[key] = value - GENAI_SPAN_OPS_AGENT_CHILDREN = [ - "gen_ai.execute_tool", - "gen_ai.handoff", - "gen_ai.run", - "gen_ai.create_agent", - ] - op = get_span_op(span) - - if op in GENAI_SPAN_OPS_AGENT_CHILDREN and "gen_ai.agent.name" not in attributes: + if get_span_op(span).startswith("gen_ai.") and "gen_ai.agent.name" not in attributes: for ancestor in self._iter_ancestors(span): - if (agent_name := attribute_value(ancestor, "gen_ai.agent.name")) is not None: + if ( + get_span_op(ancestor) == "gen_ai.invoke_agent" + and (agent_name := attribute_value(ancestor, "gen_ai.agent.name")) + is not None + ): attributes["gen_ai.agent.name"] = { "type": "string", "value": agent_name, From d68da1eaefaee6368e1e83c75f57dfe5482f4335 Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Mon, 10 Nov 2025 09:36:37 +0100 Subject: [PATCH 4/8] test: adding additional tests for span enrichment and fixing earlier ones. --- .../process_segments/test_enrichment.py | 57 +++++++++++++++++-- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/tests/sentry/spans/consumers/process_segments/test_enrichment.py b/tests/sentry/spans/consumers/process_segments/test_enrichment.py index b8c9ad059a4252..03006fc3c0581a 100644 --- a/tests/sentry/spans/consumers/process_segments/test_enrichment.py +++ b/tests/sentry/spans/consumers/process_segments/test_enrichment.py @@ -474,14 +474,14 @@ def _mock_performance_issue_span(is_segment, attributes, **fields) -> SpanEvent: def test_enrich_gen_ai_agent_name_from_immediate_parent() -> None: - """Test that gen_ai.agent.name is inherited from the immediate parent.""" + """Test that gen_ai.agent.name is inherited from the immediate parent with gen_ai.invoke_agent operation.""" parent_span = build_mock_span( project_id=1, is_segment=True, span_id="aaaaaaaaaaaaaaaa", start_timestamp=1609455600.0, end_timestamp=1609455605.0, - span_op="gen_ai.create_agent", + span_op="gen_ai.invoke_agent", attributes={ "gen_ai.agent.name": {"type": "string", "value": "MyAgent"}, }, @@ -506,14 +506,14 @@ def test_enrich_gen_ai_agent_name_from_immediate_parent() -> None: def test_enrich_gen_ai_agent_name_from_ancestor() -> None: - """Test that gen_ai.agent.name is inherited from a grandparent ancestor.""" + """Test that gen_ai.agent.name is inherited from a grandparent ancestor with gen_ai.invoke_agent operation.""" grandparent_span = build_mock_span( project_id=1, is_segment=True, span_id="aaaaaaaaaaaaaaaa", start_timestamp=1609455600.0, end_timestamp=1609455605.0, - span_op="gen_ai.create_agent", + span_op="gen_ai.invoke_agent", attributes={ "gen_ai.agent.name": {"type": "string", "value": "GrandparentAgent"}, }, @@ -555,7 +555,7 @@ def test_enrich_gen_ai_agent_name_not_overwritten() -> None: span_id="aaaaaaaaaaaaaaaa", start_timestamp=1609455600.0, end_timestamp=1609455605.0, - span_op="gen_ai.create_agent", + span_op="gen_ai.invoke_agent", attributes={ "gen_ai.agent.name": {"type": "string", "value": "ParentAgent"}, }, @@ -628,7 +628,7 @@ def test_enrich_gen_ai_agent_name_not_from_sibling() -> None: parent_span_id="aaaaaaaaaaaaaaaa", start_timestamp=1609455601.0, end_timestamp=1609455602.0, - span_op="gen_ai.create_agent", + span_op="gen_ai.invoke_agent", attributes={ "gen_ai.agent.name": {"type": "string", "value": "SiblingAgent"}, }, @@ -651,3 +651,48 @@ def test_enrich_gen_ai_agent_name_not_from_sibling() -> None: assert attribute_value(parent, "gen_ai.agent.name") is None assert attribute_value(sibling, "gen_ai.agent.name") == "SiblingAgent" assert attribute_value(target, "gen_ai.agent.name") is None + + +def test_enrich_gen_ai_agent_name_only_from_invoke_agent_ancestor() -> None: + """Test that gen_ai.agent.name is only inherited from ancestors with gen_ai.invoke_agent operation.""" + grandparent_span = build_mock_span( + project_id=1, + is_segment=True, + span_id="aaaaaaaaaaaaaaaa", + start_timestamp=1609455600.0, + end_timestamp=1609455605.0, + span_op="gen_ai.invoke_agent", + attributes={ + "gen_ai.agent.name": {"type": "string", "value": "InvokeAgentName"}, + }, + ) + + parent_span = build_mock_span( + project_id=1, + span_id="bbbbbbbbbbbbbbbb", + parent_span_id="aaaaaaaaaaaaaaaa", + start_timestamp=1609455601.0, + end_timestamp=1609455604.0, + span_op="gen_ai.create_agent", + attributes={ + "gen_ai.agent.name": {"type": "string", "value": "CreateAgentName"}, + }, + ) + + child_span = build_mock_span( + project_id=1, + span_id="cccccccccccccccc", + parent_span_id="bbbbbbbbbbbbbbbb", + start_timestamp=1609455602.0, + end_timestamp=1609455603.0, + span_op="gen_ai.run", + ) + + spans = [grandparent_span, parent_span, child_span] + _, enriched_spans = TreeEnricher.enrich_spans(spans) + compatible_spans = [make_compatible(span) for span in enriched_spans] + + grandparent, parent, child = compatible_spans + assert attribute_value(grandparent, "gen_ai.agent.name") == "InvokeAgentName" + assert attribute_value(parent, "gen_ai.agent.name") == "CreateAgentName" + assert attribute_value(child, "gen_ai.agent.name") == "InvokeAgentName" From 8f0ddd2ea54e80de9c31140837e7234611a1709f Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Mon, 10 Nov 2025 10:32:53 +0100 Subject: [PATCH 5/8] fix: typing checks --- .../consumers/process_segments/enrichment.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/sentry/spans/consumers/process_segments/enrichment.py b/src/sentry/spans/consumers/process_segments/enrichment.py index 8dcd8d35e686a3..7cc8997fc5f013 100644 --- a/src/sentry/spans/consumers/process_segments/enrichment.py +++ b/src/sentry/spans/consumers/process_segments/enrichment.py @@ -146,9 +146,19 @@ def _iter_ancestors(self, span: SpanEvent) -> Iterator[SpanEvent]: """ Iterates over the ancestors of a span in order towards the root using the "parent_span_id" attribute. """ - current = span - while current := self._span_hierarchy.get(current.get("parent_span_id")): - yield current + current: SpanEvent | None = span + parent_span_id: str | None = None + + while current is not None: + parent_span_id = current.get("parent_span_id") + if parent_span_id is not None: + current = self._span_hierarchy.get(parent_span_id) + else: + current = None + if current is not None: + yield current + else: + break def _exclusive_time(self, span: SpanEvent) -> float: """ From fa2af451dfebcdbc50ecebbe181f247e61acbc5e Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Mon, 10 Nov 2025 13:57:18 +0100 Subject: [PATCH 6/8] ref: rename span map variables for clarity and consistency --- .../spans/consumers/process_segments/enrichment.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/sentry/spans/consumers/process_segments/enrichment.py b/src/sentry/spans/consumers/process_segments/enrichment.py index 7cc8997fc5f013..f1adf453925262 100644 --- a/src/sentry/spans/consumers/process_segments/enrichment.py +++ b/src/sentry/spans/consumers/process_segments/enrichment.py @@ -75,14 +75,14 @@ def __init__(self, spans: list[SpanEvent]) -> None: self._ttid_ts = _timestamp_by_op(spans, "ui.load.initial_display") self._ttfd_ts = _timestamp_by_op(spans, "ui.load.full_display") - self._span_map: dict[str, list[tuple[int, int]]] = {} - self._span_hierarchy: dict[str, SpanEvent] = {} + self._span_intervals: dict[str, list[tuple[int, int]]] = {} + self._spans_by_id: dict[str, SpanEvent] = {} for span in spans: if "span_id" in span: self._span_hierarchy[span["span_id"]] = span if parent_span_id := span.get("parent_span_id"): interval = _span_interval(span) - self._span_map.setdefault(parent_span_id, []).append(interval) + self._span_intervals.setdefault(parent_span_id, []).append(interval) def _attributes(self, span: SpanEvent) -> dict[str, Any]: attributes: dict[str, Any] = {**(span.get("attributes") or {})} @@ -152,7 +152,7 @@ def _iter_ancestors(self, span: SpanEvent) -> Iterator[SpanEvent]: while current is not None: parent_span_id = current.get("parent_span_id") if parent_span_id is not None: - current = self._span_hierarchy.get(parent_span_id) + current = self._spans_by_id.get(parent_span_id) else: current = None if current is not None: @@ -168,7 +168,7 @@ def _exclusive_time(self, span: SpanEvent) -> float: of all time intervals where no child span was active. """ - intervals = self._span_map.get(span["span_id"], []) + intervals = self._span_intervals.get(span["span_id"], []) # Sort by start ASC, end DESC to skip over nested intervals efficiently intervals.sort(key=lambda x: (x[0], -x[1])) From e0239660488e63321cd533e2fb610a3d75c1b0af Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Mon, 10 Nov 2025 14:37:52 +0100 Subject: [PATCH 7/8] fix: missing rename --- src/sentry/spans/consumers/process_segments/enrichment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/spans/consumers/process_segments/enrichment.py b/src/sentry/spans/consumers/process_segments/enrichment.py index f1adf453925262..bd4c1fa72feb50 100644 --- a/src/sentry/spans/consumers/process_segments/enrichment.py +++ b/src/sentry/spans/consumers/process_segments/enrichment.py @@ -79,7 +79,7 @@ def __init__(self, spans: list[SpanEvent]) -> None: self._spans_by_id: dict[str, SpanEvent] = {} for span in spans: if "span_id" in span: - self._span_hierarchy[span["span_id"]] = span + self._spans_by_id[span["span_id"]] = span if parent_span_id := span.get("parent_span_id"): interval = _span_interval(span) self._span_intervals.setdefault(parent_span_id, []).append(interval) From e793755dd71a20f42e366e08d8719b22abae36a8 Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Tue, 11 Nov 2025 13:37:18 +0100 Subject: [PATCH 8/8] ref: update TreeEnricher to fetch from immediate parent span instead of ancestors, and adjust related tests for clarity and accuracy --- .../consumers/process_segments/enrichment.py | 9 +- .../process_segments/test_enrichment.py | 91 ++++++++----------- 2 files changed, 44 insertions(+), 56 deletions(-) diff --git a/src/sentry/spans/consumers/process_segments/enrichment.py b/src/sentry/spans/consumers/process_segments/enrichment.py index bd4c1fa72feb50..fda3ee1819d8a2 100644 --- a/src/sentry/spans/consumers/process_segments/enrichment.py +++ b/src/sentry/spans/consumers/process_segments/enrichment.py @@ -123,17 +123,18 @@ def get_value(key: str) -> Any: attributes[key] = value if get_span_op(span).startswith("gen_ai.") and "gen_ai.agent.name" not in attributes: - for ancestor in self._iter_ancestors(span): + if (parent_span_id := span.get("parent_span_id")) is not None: + parent_span = self._spans_by_id.get(parent_span_id) if ( - get_span_op(ancestor) == "gen_ai.invoke_agent" - and (agent_name := attribute_value(ancestor, "gen_ai.agent.name")) + parent_span is not None + and get_span_op(parent_span) == "gen_ai.invoke_agent" + and (agent_name := attribute_value(parent_span, "gen_ai.agent.name")) is not None ): attributes["gen_ai.agent.name"] = { "type": "string", "value": agent_name, } - break attributes["sentry.exclusive_time_ms"] = { "type": "double", diff --git a/tests/sentry/spans/consumers/process_segments/test_enrichment.py b/tests/sentry/spans/consumers/process_segments/test_enrichment.py index 03006fc3c0581a..494c4adf8b36a2 100644 --- a/tests/sentry/spans/consumers/process_segments/test_enrichment.py +++ b/tests/sentry/spans/consumers/process_segments/test_enrichment.py @@ -505,48 +505,6 @@ def test_enrich_gen_ai_agent_name_from_immediate_parent() -> None: assert attribute_value(child, "gen_ai.agent.name") == "MyAgent" -def test_enrich_gen_ai_agent_name_from_ancestor() -> None: - """Test that gen_ai.agent.name is inherited from a grandparent ancestor with gen_ai.invoke_agent operation.""" - grandparent_span = build_mock_span( - project_id=1, - is_segment=True, - span_id="aaaaaaaaaaaaaaaa", - start_timestamp=1609455600.0, - end_timestamp=1609455605.0, - span_op="gen_ai.invoke_agent", - attributes={ - "gen_ai.agent.name": {"type": "string", "value": "GrandparentAgent"}, - }, - ) - - parent_span = build_mock_span( - project_id=1, - span_id="bbbbbbbbbbbbbbbb", - parent_span_id="aaaaaaaaaaaaaaaa", - start_timestamp=1609455601.0, - end_timestamp=1609455604.0, - span_op="some.operation", - ) - - child_span = build_mock_span( - project_id=1, - span_id="cccccccccccccccc", - parent_span_id="bbbbbbbbbbbbbbbb", - start_timestamp=1609455602.0, - end_timestamp=1609455603.0, - span_op="gen_ai.run", - ) - - spans = [grandparent_span, parent_span, child_span] - _, enriched_spans = TreeEnricher.enrich_spans(spans) - compatible_spans = [make_compatible(span) for span in enriched_spans] - - grandparent, parent, child = compatible_spans - assert attribute_value(grandparent, "gen_ai.agent.name") == "GrandparentAgent" - assert attribute_value(parent, "gen_ai.agent.name") is None - assert attribute_value(child, "gen_ai.agent.name") == "GrandparentAgent" - - def test_enrich_gen_ai_agent_name_not_overwritten() -> None: """Test that gen_ai.agent.name is not overwritten if already set on the child.""" parent_span = build_mock_span( @@ -653,8 +611,40 @@ def test_enrich_gen_ai_agent_name_not_from_sibling() -> None: assert attribute_value(target, "gen_ai.agent.name") is None -def test_enrich_gen_ai_agent_name_only_from_invoke_agent_ancestor() -> None: - """Test that gen_ai.agent.name is only inherited from ancestors with gen_ai.invoke_agent operation.""" +def test_enrich_gen_ai_agent_name_only_from_invoke_agent_parent() -> None: + """Test that gen_ai.agent.name is only inherited from parent with gen_ai.invoke_agent operation.""" + parent_span = build_mock_span( + project_id=1, + is_segment=True, + span_id="aaaaaaaaaaaaaaaa", + start_timestamp=1609455600.0, + end_timestamp=1609455605.0, + span_op="gen_ai.create_agent", + attributes={ + "gen_ai.agent.name": {"type": "string", "value": "CreateAgentName"}, + }, + ) + + child_span = build_mock_span( + project_id=1, + span_id="bbbbbbbbbbbbbbbb", + parent_span_id="aaaaaaaaaaaaaaaa", + start_timestamp=1609455601.0, + end_timestamp=1609455602.0, + span_op="gen_ai.run", + ) + + spans = [parent_span, child_span] + _, enriched_spans = TreeEnricher.enrich_spans(spans) + compatible_spans = [make_compatible(span) for span in enriched_spans] + + parent, child = compatible_spans + assert attribute_value(parent, "gen_ai.agent.name") == "CreateAgentName" + assert attribute_value(child, "gen_ai.agent.name") is None + + +def test_enrich_gen_ai_agent_name_not_from_grandparent() -> None: + """Test that gen_ai.agent.name is NOT inherited from grandparent, only from immediate parent.""" grandparent_span = build_mock_span( project_id=1, is_segment=True, @@ -663,7 +653,7 @@ def test_enrich_gen_ai_agent_name_only_from_invoke_agent_ancestor() -> None: end_timestamp=1609455605.0, span_op="gen_ai.invoke_agent", attributes={ - "gen_ai.agent.name": {"type": "string", "value": "InvokeAgentName"}, + "gen_ai.agent.name": {"type": "string", "value": "GrandparentAgent"}, }, ) @@ -673,10 +663,7 @@ def test_enrich_gen_ai_agent_name_only_from_invoke_agent_ancestor() -> None: parent_span_id="aaaaaaaaaaaaaaaa", start_timestamp=1609455601.0, end_timestamp=1609455604.0, - span_op="gen_ai.create_agent", - attributes={ - "gen_ai.agent.name": {"type": "string", "value": "CreateAgentName"}, - }, + span_op="some.operation", ) child_span = build_mock_span( @@ -693,6 +680,6 @@ def test_enrich_gen_ai_agent_name_only_from_invoke_agent_ancestor() -> None: compatible_spans = [make_compatible(span) for span in enriched_spans] grandparent, parent, child = compatible_spans - assert attribute_value(grandparent, "gen_ai.agent.name") == "InvokeAgentName" - assert attribute_value(parent, "gen_ai.agent.name") == "CreateAgentName" - assert attribute_value(child, "gen_ai.agent.name") == "InvokeAgentName" + assert attribute_value(grandparent, "gen_ai.agent.name") == "GrandparentAgent" + assert attribute_value(parent, "gen_ai.agent.name") is None + assert attribute_value(child, "gen_ai.agent.name") is None