From 809e90bcb29c4f402affb5291b8713ff6e2d59cb Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Sun, 4 Oct 2026 09:48:33 +0200 Subject: [PATCH] fix(tracing): require gen_ai. prefix in span filter --- langfuse/_client/span_filter.py | 2 +- tests/unit/test_span_filter.py | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/langfuse/_client/span_filter.py b/langfuse/_client/span_filter.py index 071ff48ce..cf3718545 100644 --- a/langfuse/_client/span_filter.py +++ b/langfuse/_client/span_filter.py @@ -78,7 +78,7 @@ def is_genai_span(span: ReadableSpan) -> bool: return False return any( - isinstance(key, str) and key.startswith("gen_ai") + isinstance(key, str) and key.startswith("gen_ai.") for key in span.attributes.keys() ) diff --git a/tests/unit/test_span_filter.py b/tests/unit/test_span_filter.py index cf9965d14..a639a534c 100644 --- a/tests/unit/test_span_filter.py +++ b/tests/unit/test_span_filter.py @@ -50,6 +50,30 @@ def test_is_genai_span_with_genai_attributes(): ) +@pytest.mark.parametrize("predicate", [is_genai_span, is_default_export_span]) +@pytest.mark.parametrize( + ("attributes", "expected"), + [ + ({"gen_ai": "value"}, False), + ({"gen_ai_custom": "value"}, False), + ({"gen_ai-request.model": "value"}, False), + ({"gen_airflow.model": "value"}, False), + ({"custom.gen_ai.request.model": "value"}, False), + ({b"gen_ai.request.model": "value"}, False), + ({}, False), + ({"gen_ai.request.model": "model"}, True), + ({"gen_ai.usage.input_tokens": 0}, True), + ({"gen_ai.custom": False}, True), + ({"gen_ai_custom": "value", "gen_ai.request.model": "model"}, True), + ], +) +def test_genai_filters_match_attribute_namespace(predicate, attributes, expected): + """Match gen_ai.* attributes without accepting lookalike namespaces.""" + span = _make_span(scope_name="unknown.scope", attributes=attributes) + + assert predicate(span) is expected + + def test_is_genai_span_ignores_non_string_keys(): """Ignore non-string keys when checking gen_ai.* attributes.""" assert (