diff --git a/oilpriceapi/async_client.py b/oilpriceapi/async_client.py index a42c8d0..8557306 100644 --- a/oilpriceapi/async_client.py +++ b/oilpriceapi/async_client.py @@ -119,6 +119,8 @@ def __init__( "User-Agent": f"{SDK_NAME}/{SDK_VERSION} python/{python_version}", "X-SDK-Name": SDK_NAME, "X-SDK-Version": SDK_VERSION, + "X-SDK-Language": "python", + "X-Client-Type": "sdk", } # Add optional telemetry headers (10% bonus for app_url!) diff --git a/oilpriceapi/client.py b/oilpriceapi/client.py index bcd765f..4a0760f 100644 --- a/oilpriceapi/client.py +++ b/oilpriceapi/client.py @@ -142,6 +142,8 @@ def __init__( "User-Agent": f"{SDK_NAME}/{SDK_VERSION} python/{python_version}", "X-SDK-Name": SDK_NAME, "X-SDK-Version": SDK_VERSION, + "X-SDK-Language": "python", + "X-Client-Type": "sdk", } # Add optional telemetry headers (10% bonus for app_url!) diff --git a/tests/test_telemetry_headers.py b/tests/test_telemetry_headers.py new file mode 100644 index 0000000..70e733b --- /dev/null +++ b/tests/test_telemetry_headers.py @@ -0,0 +1,107 @@ +""" +Tests for SDK telemetry headers (issue #4). + +The API tracks SDK adoption via request headers. Every SDK request must +identify itself with: + + X-SDK-Language: python + X-SDK-Version: + X-Client-Type: sdk + +Issue #4 reported that ``X-SDK-Language`` and ``X-Client-Type`` were never +reaching the API (0/166 requests), making it impossible to distinguish SDK +traffic from manual ``python-requests``/``python-httpx`` calls. These tests +encode the acceptance criteria for both the sync and async clients, asserting +the headers are both configured on the client and actually transmitted on the +wire. +""" + +import asyncio + +import httpx + +from oilpriceapi import OilPriceAPI +from oilpriceapi.async_client import AsyncOilPriceAPI +from oilpriceapi.version import SDK_VERSION + + +class TestSyncTelemetryHeaders: + """Sync client must advertise SDK telemetry headers.""" + + def test_headers_include_sdk_language(self): + client = OilPriceAPI(api_key="test_key") + assert client.headers["X-SDK-Language"] == "python" + + def test_headers_include_client_type(self): + client = OilPriceAPI(api_key="test_key") + assert client.headers["X-Client-Type"] == "sdk" + + def test_headers_include_sdk_version(self): + client = OilPriceAPI(api_key="test_key") + assert client.headers["X-SDK-Version"] == SDK_VERSION + + def test_telemetry_headers_sent_on_request(self): + """Headers must actually be transmitted, not just stored locally.""" + captured = {} + + def handler(request: httpx.Request) -> httpx.Response: + captured.update(request.headers) + return httpx.Response( + 200, + json={"data": {"price": 80.0, "commodity": "BRENT_CRUDE_USD"}}, + ) + + client = OilPriceAPI(api_key="test_key") + client._client = httpx.Client( + base_url=client.base_url, + headers=client.headers, + transport=httpx.MockTransport(handler), + ) + client.request("GET", "/v1/prices/latest") + + assert captured.get("x-sdk-language") == "python" + assert captured.get("x-client-type") == "sdk" + assert captured.get("x-sdk-version") == SDK_VERSION + + +class TestAsyncTelemetryHeaders: + """Async client must advertise SDK telemetry headers.""" + + def test_headers_include_sdk_language(self): + client = AsyncOilPriceAPI(api_key="test_key") + assert client.headers["X-SDK-Language"] == "python" + + def test_headers_include_client_type(self): + client = AsyncOilPriceAPI(api_key="test_key") + assert client.headers["X-Client-Type"] == "sdk" + + def test_headers_include_sdk_version(self): + client = AsyncOilPriceAPI(api_key="test_key") + assert client.headers["X-SDK-Version"] == SDK_VERSION + + def test_telemetry_headers_sent_on_request(self): + """Async requests must transmit telemetry headers on the wire.""" + captured = {} + + def handler(request: httpx.Request) -> httpx.Response: + captured.update(request.headers) + return httpx.Response( + 200, + json={"data": {"price": 80.0, "commodity": "BRENT_CRUDE_USD"}}, + ) + + async def run(): + client = AsyncOilPriceAPI(api_key="test_key") + client._client = httpx.AsyncClient( + base_url=client.base_url, + headers=client.headers, + transport=httpx.MockTransport(handler), + ) + await client.request("GET", "/v1/prices/latest") + await client._client.aclose() + + asyncio.run(run()) + + assert captured.get("x-sdk-language") == "python" + assert captured.get("x-client-type") == "sdk" + assert captured.get("x-sdk-version") == SDK_VERSION