# Padrão de implementação para clients de API externa Este documento define o padrão obrigatório para qualquer client novo que faça chamadas a APIs externas (HTTP ou outras). Ele garante que todas as requisições apareçam no Langfuse com endpoint, status code e response body, **sem que o dev precise escrever código de tracing manual**. ## 1. Use sempre `traced_async_client` / `traced_sync_client` Nunca instancie `httpx.AsyncClient` (ou `httpx.Client`) diretamente em um client de API. Use os factories de [`src/utils/http.py`](../src/utils/http.py): ```python from src.utils.http import traced_async_client async with traced_async_client(timeout=self.timeout) as client: response = await client.get(url, headers=headers) # ... ``` O factory devolve um `httpx.AsyncClient` que automaticamente: - registra o **endpoint real** (URL completa, com path e query string) da request realizada — não depende de `self.base_url` ou `self.url`; - registra o **status code** retornado; - registra o **response body** (JSON ou texto truncado em 2000 chars); - registra metadata também em **erros de rede** (timeout, connect error) com `status_code=None` e o erro como body. Argumentos comuns aceitos: `timeout`, `verify_ssl` (default `settings.VERIFY_SSL`), `headers`, `base_url`, `response_sanitizer`, e qualquer outro kwarg de `httpx.AsyncClient`. ## 2. Decore os métodos públicos com `@trace_tool` Aplique `@trace_tool` em todo método que faz chamada externa, incluindo helpers privados de autenticação (`_get_token`, `_refresh_xxx`). Cada método decorado vira um span filho próprio no Langfuse, com nome `NomeDaClasse.nome_do_metodo` — então uma classe com múltiplas APIs (ex: `SpeechAnalyticsClient._get_token` vs `.get_prediction` vs `.get_history`) não perde a granularidade. ```python from src.utils.observer import trace_tool class MyClient: @trace_tool async def get_something(self, ...) -> ...: ... ``` ## 3. Convenções de nomenclatura - **Métodos** com verbo + recurso: `get_history`, `post_prediction`, `open_service_request`. O nome do método aparece no nome do span. - **`__init__`** não precisa expor `base_url` para fins de tracing — o endpoint real vem da request. Mantenha `self.base_url` apenas se o client realmente precisa dela para construir URLs. ## 4. Sanitização de payloads sensíveis Quando a resposta contém credenciais, tokens ou dados sensíveis que não devem ir para o Langfuse, passe um `response_sanitizer` ao factory. A função recebe o body já parseado e devolve a versão filtrada: ```python def _sanitize_token_response(body): if not isinstance(body, dict): return body return { "expires_in": body.get("expires_in"), "status": body.get("status"), } async with traced_async_client( timeout=settings.SPEECH_TIMEOUT, response_sanitizer=_sanitize_token_response, ) as client: response = await client.post(url, data=...) ``` Exemplo real: [`SpeechAnalyticsClient._get_token`](../src/components/clients/speech_analytics_client.py). ## 5. Clients que não usam HTTP Para clients que não falam HTTP (ex: [`TaisKbClient`](../src/components/clients/tais_kb_client.py), que usa OCI SDK + oracledb), basta decorar o método público com `@trace_tool`. O decorator cai no fallback de logar o retorno da função como output do span. Não tente sintetizar um endpoint via `set_tool_call_metadata` — apenas adiciona ruído. ## 6. Exemplo mínimo ```python import httpx import logging from src.utils.observer import trace_tool from src.utils.http import traced_async_client from src.core.config import settings logger = logging.getLogger(__name__) class FooClient: def __init__(self) -> None: self.base_url = settings.FOO_API_BASE_URL self.token = settings.FOO_API_TOKEN self.timeout = settings.FOO_API_TIMEOUT @trace_tool async def get_foo(self, foo_id: str) -> dict: url = f"{self.base_url}/foo/{foo_id}" headers = {"Authorization": f"Bearer {self.token}"} try: async with traced_async_client(timeout=self.timeout) as client: response = await client.get(url, headers=headers) response.raise_for_status() return response.json() except httpx.HTTPStatusError as exc: logger.error(f"Foo API HTTP error {exc.response.status_code}") raise ``` Nada de `set_tool_call_metadata` espalhado pelo método. O span no Langfuse já vai conter `endpoint`, `status_code` e `response` automaticamente. ## Checklist de revisão Ao revisar um PR de novo client, valide: - [ ] Usa `traced_async_client` / `traced_sync_client` (não `httpx.AsyncClient` direto). - [ ] Métodos públicos têm `@trace_tool`. - [ ] Não há chamadas manuais a `set_tool_call_metadata`. - [ ] Respostas com dados sensíveis passam por `response_sanitizer`. - [ ] Nomes de método são `verbo_recurso` (legíveis como nome de span).