diff --git a/py/src/braintrust/test_trace.py b/py/src/braintrust/test_trace.py index 346b47a1..3a572f68 100644 --- a/py/src/braintrust/test_trace.py +++ b/py/src/braintrust/test_trace.py @@ -41,6 +41,28 @@ async def fetch_fn(span_type): assert len(result) == 3 assert {s.span_id for s in result} == {"span-1", "span-2", "span-3"} + @pytest.mark.asyncio + async def test_fetch_all_after_typed_fetch_has_no_duplicates(self): + """A typed fetch followed by a full fetch must not duplicate spans.""" + all_spans = [ + make_span("fn-1", "function"), + make_span("llm-1", "llm"), + make_span("llm-2", "llm"), + ] + + async def fetch_fn(span_type): + if span_type: + return [s for s in all_spans if s.span_attributes["type"] in span_type] + return all_spans + + fetcher = CachedSpanFetcher(fetch_fn=fetch_fn) + await fetcher.get_spans(["llm"]) + result = await fetcher.get_spans() + + span_ids = [s.span_id for s in result] + assert sorted(span_ids) == ["fn-1", "llm-1", "llm-2"] + assert len(span_ids) == len(set(span_ids)), f"duplicate spans: {span_ids}" + @pytest.mark.asyncio async def test_fetch_preserves_span_result_fields(self): """Test that fetched spans preserve fields needed for full trace attachments.""" diff --git a/py/src/braintrust/trace.py b/py/src/braintrust/trace.py index ee62d671..b72b8d62 100644 --- a/py/src/braintrust/trace.py +++ b/py/src/braintrust/trace.py @@ -260,6 +260,10 @@ async def get_spans( # If no filter requested, fetch everything if not span_type or len(span_type) == 0: + # A full fetch is authoritative; reset the per-type cache first so a + # prior typed fetch's spans are not duplicated by re-fetching them + # (_fetch_spans appends). + self._span_cache = {} await self._fetch_spans(None) if self._span_cache: # Only cache if we got results self._all_fetched = True