Skip to content

Commit 8818397

Browse files
refactor(ai): address review feedback
Move DEFAULT_HOST to spans.py, extract shared make_span helper to conftest.py, and parameterize span filter tests. Generated-By: PostHog Code Task-Id: 1ba1f07a-1453-4162-90a8-665958c5fe46
1 parent 35ec0de commit 8818397

7 files changed

Lines changed: 61 additions & 73 deletions

File tree

posthog/ai/otel/exporter.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@
1111
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
1212
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
1313

14-
from .spans import is_ai_span
15-
16-
DEFAULT_HOST = "https://us.i.posthog.com"
14+
from .spans import DEFAULT_HOST, is_ai_span
1715

1816

1917
class PostHogTraceExporter(SpanExporter):

posthog/ai/otel/processor.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@
1212
from opentelemetry.sdk.trace.export import BatchSpanProcessor
1313
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
1414

15-
from .spans import is_ai_span
16-
17-
DEFAULT_HOST = "https://us.i.posthog.com"
15+
from .spans import DEFAULT_HOST, is_ai_span
1816

1917

2018
class PostHogSpanProcessor(SpanProcessor):

posthog/ai/otel/spans.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
"""Shared AI span filtering logic for OpenTelemetry integration."""
1+
"""Shared AI span filtering logic and constants for OpenTelemetry integration."""
22

33
from typing import TYPE_CHECKING
44

55
if TYPE_CHECKING:
66
from opentelemetry.sdk.trace import ReadableSpan
77

8+
DEFAULT_HOST = "https://us.i.posthog.com"
9+
810
AI_SPAN_PREFIXES = ("gen_ai.", "llm.", "ai.", "traceloop.")
911

1012

posthog/test/ai/otel/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from unittest.mock import MagicMock
2+
3+
4+
def make_span(name: str = "test", attributes: dict | None = None) -> MagicMock:
5+
span = MagicMock()
6+
span.name = name
7+
span.attributes = attributes or {}
8+
return span

posthog/test/ai/otel/test_exporter.py

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,10 @@
11
import unittest
2-
from unittest.mock import MagicMock, patch
2+
from unittest.mock import patch
33

44
from opentelemetry.sdk.trace.export import SpanExportResult
55

66
from posthog.ai.otel.exporter import PostHogTraceExporter
7-
8-
9-
def _make_span(name: str = "test", attributes: dict | None = None) -> MagicMock:
10-
span = MagicMock()
11-
span.name = name
12-
span.attributes = attributes or {}
13-
return span
7+
from posthog.test.ai.otel.conftest import make_span
148

159

1610
class TestPostHogTraceExporter(unittest.TestCase):
@@ -36,7 +30,7 @@ def test_exports_ai_spans(self, mock_otlp_cls):
3630
inner = mock_otlp_cls.return_value
3731
inner.export.return_value = SpanExportResult.SUCCESS
3832

39-
spans = [_make_span("gen_ai.chat"), _make_span("llm.call")]
33+
spans = [make_span("gen_ai.chat"), make_span("llm.call")]
4034
result = exporter.export(spans)
4135

4236
self.assertEqual(result, SpanExportResult.SUCCESS)
@@ -48,8 +42,8 @@ def test_filters_out_non_ai_spans(self, mock_otlp_cls):
4842
inner = mock_otlp_cls.return_value
4943
inner.export.return_value = SpanExportResult.SUCCESS
5044

51-
ai_span = _make_span("gen_ai.chat")
52-
http_span = _make_span("http.request")
45+
ai_span = make_span("gen_ai.chat")
46+
http_span = make_span("http.request")
5347
result = exporter.export([ai_span, http_span])
5448

5549
self.assertEqual(result, SpanExportResult.SUCCESS)
@@ -60,7 +54,7 @@ def test_returns_success_when_no_ai_spans(self, mock_otlp_cls):
6054
exporter = PostHogTraceExporter(api_key="phc_test")
6155
inner = mock_otlp_cls.return_value
6256

63-
result = exporter.export([_make_span("http.request"), _make_span("db.query")])
57+
result = exporter.export([make_span("http.request"), make_span("db.query")])
6458

6559
self.assertEqual(result, SpanExportResult.SUCCESS)
6660
inner.export.assert_not_called()
@@ -81,7 +75,7 @@ def test_exports_spans_with_ai_attributes(self, mock_otlp_cls):
8175
inner = mock_otlp_cls.return_value
8276
inner.export.return_value = SpanExportResult.SUCCESS
8377

84-
span = _make_span("http.request", {"gen_ai.system": "openai"})
78+
span = make_span("http.request", {"gen_ai.system": "openai"})
8579
result = exporter.export([span])
8680

8781
self.assertEqual(result, SpanExportResult.SUCCESS)

posthog/test/ai/otel/test_processor.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,7 @@
22
from unittest.mock import MagicMock, patch
33

44
from posthog.ai.otel.processor import PostHogSpanProcessor
5-
6-
7-
def _make_span(name: str = "test", attributes: dict | None = None) -> MagicMock:
8-
span = MagicMock()
9-
span.name = name
10-
span.attributes = attributes or {}
11-
return span
5+
from posthog.test.ai.otel.conftest import make_span
126

137

148
class TestPostHogSpanProcessor(unittest.TestCase):
@@ -48,7 +42,7 @@ def test_forwards_ai_spans(self, mock_batch_cls, mock_otlp_cls):
4842
processor = PostHogSpanProcessor(api_key="phc_test")
4943
inner = mock_batch_cls.return_value
5044

51-
ai_span = _make_span("gen_ai.chat")
45+
ai_span = make_span("gen_ai.chat")
5246
processor.on_end(ai_span)
5347
inner.on_end.assert_called_once_with(ai_span)
5448

@@ -58,7 +52,7 @@ def test_drops_non_ai_spans(self, mock_batch_cls, mock_otlp_cls):
5852
processor = PostHogSpanProcessor(api_key="phc_test")
5953
inner = mock_batch_cls.return_value
6054

61-
processor.on_end(_make_span("http.request"))
55+
processor.on_end(make_span("http.request"))
6256
inner.on_end.assert_not_called()
6357

6458
@patch("posthog.ai.otel.processor.OTLPSpanExporter")
@@ -67,7 +61,7 @@ def test_forwards_span_with_ai_attributes(self, mock_batch_cls, mock_otlp_cls):
6761
processor = PostHogSpanProcessor(api_key="phc_test")
6862
inner = mock_batch_cls.return_value
6963

70-
span = _make_span("http.request", {"gen_ai.system": "openai"})
64+
span = make_span("http.request", {"gen_ai.system": "openai"})
7165
processor.on_end(span)
7266
inner.on_end.assert_called_once_with(span)
7367

posthog/test/ai/otel/test_spans.py

Lines changed: 37 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,52 @@
11
import unittest
2-
from unittest.mock import MagicMock
3-
4-
from posthog.ai.otel.spans import is_ai_span
52

3+
from parameterized import parameterized
64

7-
def _make_span(name: str = "test", attributes: dict | None = None) -> MagicMock:
8-
span = MagicMock()
9-
span.name = name
10-
span.attributes = attributes or {}
11-
return span
5+
from posthog.ai.otel.spans import is_ai_span
6+
from posthog.test.ai.otel.conftest import make_span
127

138

149
class TestIsAISpan(unittest.TestCase):
15-
def test_matches_gen_ai_name_prefix(self):
16-
self.assertTrue(is_ai_span(_make_span("gen_ai.chat")))
17-
18-
def test_matches_llm_name_prefix(self):
19-
self.assertTrue(is_ai_span(_make_span("llm.call")))
20-
21-
def test_matches_ai_name_prefix(self):
22-
self.assertTrue(is_ai_span(_make_span("ai.completion")))
23-
24-
def test_matches_traceloop_name_prefix(self):
25-
self.assertTrue(is_ai_span(_make_span("traceloop.workflow")))
26-
27-
def test_rejects_non_ai_name(self):
28-
self.assertFalse(is_ai_span(_make_span("http.request")))
29-
self.assertFalse(is_ai_span(_make_span("db.query")))
30-
self.assertFalse(is_ai_span(_make_span("my_function")))
31-
32-
def test_matches_gen_ai_attribute_key(self):
33-
span = _make_span("http.request", {"gen_ai.system": "openai"})
34-
self.assertTrue(is_ai_span(span))
35-
36-
def test_matches_llm_attribute_key(self):
37-
span = _make_span("http.request", {"llm.model": "gpt-4"})
38-
self.assertTrue(is_ai_span(span))
39-
40-
def test_matches_ai_attribute_key(self):
41-
span = _make_span("http.request", {"ai.provider": "anthropic"})
42-
self.assertTrue(is_ai_span(span))
43-
44-
def test_matches_traceloop_attribute_key(self):
45-
span = _make_span("http.request", {"traceloop.entity.name": "chain"})
46-
self.assertTrue(is_ai_span(span))
10+
@parameterized.expand(
11+
[
12+
("gen_ai", "gen_ai.chat"),
13+
("llm", "llm.call"),
14+
("ai", "ai.completion"),
15+
("traceloop", "traceloop.workflow"),
16+
]
17+
)
18+
def test_matches_ai_name_prefix(self, _name, span_name):
19+
self.assertTrue(is_ai_span(make_span(span_name)))
20+
21+
@parameterized.expand(
22+
[
23+
("gen_ai", {"gen_ai.system": "openai"}),
24+
("llm", {"llm.model": "gpt-4"}),
25+
("ai", {"ai.provider": "anthropic"}),
26+
("traceloop", {"traceloop.entity.name": "chain"}),
27+
]
28+
)
29+
def test_matches_ai_attribute_key(self, _name, attrs):
30+
self.assertTrue(is_ai_span(make_span("http.request", attrs)))
31+
32+
@parameterized.expand(
33+
[
34+
("http", "http.request"),
35+
("db", "db.query"),
36+
("custom", "my_function"),
37+
]
38+
)
39+
def test_rejects_non_ai_name(self, _name, span_name):
40+
self.assertFalse(is_ai_span(make_span(span_name)))
4741

4842
def test_rejects_non_ai_attributes(self):
49-
span = _make_span("http.request", {"http.method": "GET", "http.url": "/"})
43+
span = make_span("http.request", {"http.method": "GET", "http.url": "/"})
5044
self.assertFalse(is_ai_span(span))
5145

5246
def test_empty_span(self):
53-
self.assertFalse(is_ai_span(_make_span("test", {})))
47+
self.assertFalse(is_ai_span(make_span("test", {})))
5448

5549
def test_none_attributes(self):
56-
span = _make_span("test")
50+
span = make_span("test")
5751
span.attributes = None
5852
self.assertFalse(is_ai_span(span))

0 commit comments

Comments
 (0)