|
1 | 1 | # (C) 2026 GoodData Corporation |
2 | 2 | import json |
| 3 | +import logging |
| 4 | +from collections.abc import Callable |
| 5 | +from urllib.parse import unquote |
3 | 6 |
|
4 | 7 | import httpx |
5 | 8 | import pytest |
@@ -1053,3 +1056,77 @@ def handler(request): |
1053 | 1056 | ) |
1054 | 1057 | client.ask(item) |
1055 | 1058 | assert captured["body"]["userContext"] == _ATTACHMENT |
| 1059 | + |
| 1060 | + |
| 1061 | +def _baggage_of(requests: list[httpx.Request]) -> list[dict[str, str] | None]: |
| 1062 | + """Each request's `baggage` header as {key: decoded value}, or None when absent.""" |
| 1063 | + out: list[dict[str, str] | None] = [] |
| 1064 | + for r in requests: |
| 1065 | + raw = r.headers.get("baggage") |
| 1066 | + out.append(None if raw is None else {k: unquote(v) for k, v in (e.split("=", 1) for e in raw.split(","))}) |
| 1067 | + return out |
| 1068 | + |
| 1069 | + |
| 1070 | +def _record_requests(requests: list[httpx.Request]) -> Callable[[httpx.Request], httpx.Response]: |
| 1071 | + def handler(request: httpx.Request) -> httpx.Response: |
| 1072 | + requests.append(request) |
| 1073 | + if request.url.path.endswith("/conversations"): |
| 1074 | + return httpx.Response(200, json={"conversationId": "c1"}) |
| 1075 | + return httpx.Response(200, content=_OK_SSE) |
| 1076 | + |
| 1077 | + return handler |
| 1078 | + |
| 1079 | + |
| 1080 | +def test_trace_labels_ride_every_request_as_langfuse_baggage(monkeypatch: pytest.MonkeyPatch) -> None: |
| 1081 | + """gen-ai's Langfuse span processor copies `langfuse_*` baggage onto every span it starts, so |
| 1082 | + the labels reach the root generation and its cost-bearing children at creation.""" |
| 1083 | + monkeypatch.setenv("GOODDATA_EVAL_TRACE_LABELS", "model_version=gpt-5.2 openai,github_run_id=42") |
| 1084 | + requests: list[httpx.Request] = [] |
| 1085 | + client = _client_with_handler(_record_requests(requests)) |
| 1086 | + |
| 1087 | + client.send_message(client.create_conversation(), "q") |
| 1088 | + |
| 1089 | + expected = { |
| 1090 | + "langfuse_version": "gpt-5.2 openai", |
| 1091 | + "langfuse_metadata_model_version": "gpt-5.2 openai", |
| 1092 | + "langfuse_metadata_github_run_id": "42", |
| 1093 | + } |
| 1094 | + assert _baggage_of(requests) == [expected, expected] |
| 1095 | + # Encoded, not raw: a space or comma in a value would otherwise split or end the entry. |
| 1096 | + assert " " not in requests[0].headers["baggage"] |
| 1097 | + |
| 1098 | + |
| 1099 | +def test_trace_labels_without_a_model_version_set_no_langfuse_version(monkeypatch: pytest.MonkeyPatch) -> None: |
| 1100 | + monkeypatch.setenv("GOODDATA_EVAL_TRACE_LABELS", "github_run_id=42") |
| 1101 | + requests: list[httpx.Request] = [] |
| 1102 | + _client_with_handler(_record_requests(requests)).create_conversation() |
| 1103 | + |
| 1104 | + assert _baggage_of(requests) == [{"langfuse_metadata_github_run_id": "42"}] |
| 1105 | + |
| 1106 | + |
| 1107 | +@pytest.mark.parametrize("key", ["modèle", "run id", "c++"]) |
| 1108 | +def test_trace_label_with_a_non_token_key_is_skipped_with_one_warning( |
| 1109 | + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, key: str |
| 1110 | +) -> None: |
| 1111 | + """httpx refuses a non-ASCII header, gen-ai's OTel propagator drops a key with a space, and |
| 1112 | + it reads `+` as a space; each would otherwise fail the run or mislabel the trace.""" |
| 1113 | + monkeypatch.setenv("GOODDATA_EVAL_TRACE_LABELS", f"github_run_id=42,{key}=1") |
| 1114 | + requests: list[httpx.Request] = [] |
| 1115 | + |
| 1116 | + for _ in range(2): |
| 1117 | + _client_with_handler(_record_requests(requests)).create_conversation() |
| 1118 | + |
| 1119 | + assert _baggage_of(requests) == [{"langfuse_metadata_github_run_id": "42"}] * 2 |
| 1120 | + assert [r.levelno for r in caplog.records if repr(key) in r.getMessage()] == [logging.WARNING] |
| 1121 | + |
| 1122 | + |
| 1123 | +@pytest.mark.parametrize("raw", [None, "", "no-equals-sign", " , "]) |
| 1124 | +def test_no_trace_labels_send_no_baggage(monkeypatch: pytest.MonkeyPatch, raw: str | None) -> None: |
| 1125 | + if raw is None: |
| 1126 | + monkeypatch.delenv("GOODDATA_EVAL_TRACE_LABELS", raising=False) |
| 1127 | + else: |
| 1128 | + monkeypatch.setenv("GOODDATA_EVAL_TRACE_LABELS", raw) |
| 1129 | + requests: list[httpx.Request] = [] |
| 1130 | + _client_with_handler(_record_requests(requests)).create_conversation() |
| 1131 | + |
| 1132 | + assert _baggage_of(requests) == [None] |
0 commit comments