Skip to content

Commit fa9a2c4

Browse files
committed
feat(gooddata-eval): name gooddata-eval in Langfuse requests and spans
Langfuse groups API traffic by User-Agent, so every gooddata-eval call showed up as python-httpx/0.28.1, indistinguishable from other callers. Its exported root spans carried only service.name, so a trace did not say which build or CI run produced it. Every client from make_http_client now sends gooddata-eval/<version> (<ci|local>; run=<GITHUB_RUN_ID>; sha=<12 chars of GITHUB_SHA>), with "local" for any value unset or unsafe in a header. The OTLP resource adds service.version and, when set, github.run_id, github.workflow, github.ref_name and github.event_name. jira: trivial risk: low
1 parent 380c3c5 commit fa9a2c4

4 files changed

Lines changed: 120 additions & 6 deletions

File tree

‎packages/gooddata-eval/src/gooddata_eval/core/langfuse/_env.py‎

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,25 @@
55

66
import base64
77
import os
8+
import re
89

910
import httpx
1011

12+
from gooddata_eval._version import __version__
13+
1114
_DEFAULT_BASE_URL = "https://us.cloud.langfuse.com"
15+
SERVICE_NAME = "gooddata-eval"
16+
17+
_LOCAL = "local"
18+
_HEADER_SAFE = re.compile(r"[A-Za-z0-9._-]+")
19+
_SHORT_SHA_LENGTH = 12
20+
# Resource attribute -> the GitHub Actions variable it is read from.
21+
_GITHUB_RESOURCE = {
22+
"github.run_id": "GITHUB_RUN_ID",
23+
"github.workflow": "GITHUB_WORKFLOW",
24+
"github.ref_name": "GITHUB_REF_NAME",
25+
"github.event_name": "GITHUB_EVENT_NAME",
26+
}
1227

1328

1429
def resolve_base_url() -> str:
@@ -30,10 +45,34 @@ def basic_auth_header() -> str:
3045
return f"Basic {creds}"
3146

3247

48+
def _header_value(name: str) -> str:
49+
"""The variable's value, or "local" when it is unset, blank or unsafe in a header."""
50+
value = os.environ.get(name, "").strip()
51+
return value if _HEADER_SAFE.fullmatch(value) else _LOCAL
52+
53+
54+
def user_agent() -> str:
55+
"""``gooddata-eval/<version> (<ci|local>; run=<GITHUB_RUN_ID>; sha=<short GITHUB_SHA>)``."""
56+
env = "ci" if os.environ.get("GITHUB_ACTIONS") == "true" else _LOCAL
57+
sha = _header_value("GITHUB_SHA")
58+
short_sha = sha[:_SHORT_SHA_LENGTH] if sha != _LOCAL else _LOCAL
59+
return f"{SERVICE_NAME}/{__version__} ({env}; run={_header_value('GITHUB_RUN_ID')}; sha={short_sha})"
60+
61+
62+
def resource_attributes() -> dict[str, str]:
63+
"""OTLP resource attributes of gooddata-eval's spans, with the GitHub run context when set."""
64+
attributes = {"service.name": SERVICE_NAME, "service.version": __version__}
65+
for key, variable in _GITHUB_RESOURCE.items():
66+
value = os.environ.get(variable, "").strip()
67+
if value:
68+
attributes[key] = value
69+
return attributes
70+
71+
3372
def make_http_client(*, timeout: float, transport: httpx.BaseTransport | None = None) -> httpx.Client:
3473
return httpx.Client(
3574
base_url=resolve_base_url(),
36-
headers={"Authorization": basic_auth_header()},
75+
headers={"Authorization": basic_auth_header(), "User-Agent": user_agent()},
3776
timeout=timeout,
3877
transport=transport,
3978
)

‎packages/gooddata-eval/src/gooddata_eval/core/langfuse/otlp.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from typing import TYPE_CHECKING, Any
1212

1313
from gooddata_eval._version import __version__
14+
from gooddata_eval.core.langfuse import _env
1415

1516
if TYPE_CHECKING:
1617
import httpx
@@ -42,7 +43,6 @@
4243
ATTR_EXPERIMENT_ITEM_EXPECTED_OUTPUT = "langfuse.experiment.item.expected_output"
4344
ATTR_EXPERIMENT_ITEM_METADATA_PREFIX = "langfuse.experiment.item.metadata"
4445

45-
_SERVICE_NAME = "gooddata-eval"
4646
_EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)
4747

4848

@@ -112,7 +112,7 @@ class Span:
112112

113113

114114
def encode_export_request(
115-
spans: list[Span], *, scope_name: str = _SERVICE_NAME, scope_version: str = __version__
115+
spans: list[Span], *, scope_name: str = _env.SERVICE_NAME, scope_version: str = __version__
116116
) -> dict[str, Any]:
117117
"""Build the OTLP/JSON export request body for `POST /api/public/otel/v1/traces`."""
118118
otlp_spans = [
@@ -131,7 +131,7 @@ def encode_export_request(
131131
return {
132132
"resourceSpans": [
133133
{
134-
"resource": {"attributes": [{"key": "service.name", "value": {"stringValue": _SERVICE_NAME}}]},
134+
"resource": {"attributes": [otlp_attribute(k, v) for k, v in _env.resource_attributes().items()]},
135135
"scopeSpans": [{"scope": {"name": scope_name, "version": scope_version}, "spans": otlp_spans}],
136136
}
137137
]

‎packages/gooddata-eval/tests/test_langfuse_env.py‎

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,38 @@
33

44
import base64
55

6+
import httpx
67
import pytest
7-
from gooddata_eval.core.langfuse._env import basic_auth_header, credentials_present, make_http_client, resolve_base_url
8+
from gooddata_eval._version import __version__
9+
from gooddata_eval.core.langfuse._env import (
10+
basic_auth_header,
11+
credentials_present,
12+
make_http_client,
13+
resolve_base_url,
14+
resource_attributes,
15+
user_agent,
16+
)
17+
18+
_GITHUB_ENV = {
19+
"GITHUB_ACTIONS": "true",
20+
"GITHUB_RUN_ID": "18273645",
21+
"GITHUB_SHA": "0123456789abcdef0123456789abcdef01234567",
22+
"GITHUB_WORKFLOW": "AI agent tests (staging)",
23+
"GITHUB_REF_NAME": "master",
24+
"GITHUB_EVENT_NAME": "schedule",
25+
}
26+
27+
28+
@pytest.fixture
29+
def outside_ci(monkeypatch):
30+
for name in _GITHUB_ENV:
31+
monkeypatch.delenv(name, raising=False)
32+
33+
34+
@pytest.fixture
35+
def in_ci(monkeypatch):
36+
for name, value in _GITHUB_ENV.items():
37+
monkeypatch.setenv(name, value)
838

939

1040
def test_resolve_base_url_prefers_base_url_over_host(monkeypatch):
@@ -68,3 +98,47 @@ def test_make_http_client_uses_resolved_base_url_and_auth(monkeypatch):
6898
assert client.headers["Authorization"].startswith("Basic ")
6999
finally:
70100
client.close()
101+
102+
103+
def test_a_ci_run_names_the_package_its_run_and_short_sha(in_ci):
104+
assert user_agent() == f"gooddata-eval/{__version__} (ci; run=18273645; sha=0123456789ab)"
105+
106+
107+
def test_outside_ci_the_user_agent_falls_back_to_local(outside_ci):
108+
assert user_agent() == f"gooddata-eval/{__version__} (local; run=local; sha=local)"
109+
110+
111+
@pytest.mark.parametrize("value", ["", "18273645\r\nX-Injected: 1", "run id"])
112+
def test_a_blank_or_unsafe_run_id_falls_back_to_local(outside_ci, monkeypatch, value):
113+
monkeypatch.setenv("GITHUB_RUN_ID", value)
114+
assert "run=local;" in user_agent()
115+
116+
117+
def test_every_request_of_the_http_client_carries_the_user_agent(in_ci, monkeypatch):
118+
monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "pk-test")
119+
monkeypatch.setenv("LANGFUSE_SECRET_KEY", "sk-test")
120+
sent: list[httpx.Request] = []
121+
122+
def handler(request: httpx.Request) -> httpx.Response:
123+
sent.append(request)
124+
return httpx.Response(200, json={})
125+
126+
with make_http_client(timeout=5, transport=httpx.MockTransport(handler)) as client:
127+
client.get("/api/public/v2/observations")
128+
129+
assert sent[0].headers["User-Agent"] == user_agent()
130+
131+
132+
def test_a_ci_run_puts_its_github_context_on_the_resource(in_ci):
133+
assert resource_attributes() == {
134+
"service.name": "gooddata-eval",
135+
"service.version": __version__,
136+
"github.run_id": "18273645",
137+
"github.workflow": "AI agent tests (staging)",
138+
"github.ref_name": "master",
139+
"github.event_name": "schedule",
140+
}
141+
142+
143+
def test_outside_ci_the_resource_names_only_the_service(outside_ci):
144+
assert resource_attributes() == {"service.name": "gooddata-eval", "service.version": __version__}

‎packages/gooddata-eval/tests/test_langfuse_otlp.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import httpx
99
import pytest
10+
from gooddata_eval.core.langfuse._env import resource_attributes
1011
from gooddata_eval.core.langfuse.otlp import (
1112
Span,
1213
encode_export_request,
@@ -126,7 +127,7 @@ def test_encode_export_request_shape():
126127
resource_spans = request["resourceSpans"]
127128
assert len(resource_spans) == 1
128129
resource = resource_spans[0]["resource"]
129-
assert resource["attributes"] == [{"key": "service.name", "value": {"stringValue": "gooddata-eval"}}]
130+
assert {a["key"]: a["value"]["stringValue"] for a in resource["attributes"]} == resource_attributes()
130131

131132
scope_spans = resource_spans[0]["scopeSpans"]
132133
assert len(scope_spans) == 1

0 commit comments

Comments
 (0)