From 6aed030eb6c6a8525d7b0abf566ad0b0a98acdbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Brun?= Date: Fri, 3 Jul 2026 19:14:51 -0300 Subject: [PATCH 1/5] label internal sdk calls --- src/sap_cloud_sdk/agentgateway/_fragments.py | 6 +- src/sap_cloud_sdk/agentgateway/_lob.py | 6 +- src/sap_cloud_sdk/agentgateway/agw_client.py | 12 +++- .../core/auditlog_ng/__init__.py | 5 +- .../data_anonymization/_http_transport.py | 9 ++- src/sap_cloud_sdk/destination/__init__.py | 15 ++++- .../destination/certificate_client.py | 8 ++- src/sap_cloud_sdk/destination/client.py | 9 ++- .../destination/fragment_client.py | 8 ++- .../extensibility/_ums_transport.py | 4 +- src/sap_cloud_sdk/extensibility/client.py | 4 +- tests/agentgateway/unit/test_agw_client.py | 21 ++++++ tests/agentgateway/unit/test_lob.py | 34 +++++++++- .../auditlog_ng/unit/test_create_client.py | 20 ++++-- .../data_anonymization/test_http_transport.py | 8 +-- tests/destination/unit/test_init.py | 64 +++++++++++++++++++ .../extensibility/unit/test_ums_transport.py | 6 +- 17 files changed, 215 insertions(+), 24 deletions(-) diff --git a/src/sap_cloud_sdk/agentgateway/_fragments.py b/src/sap_cloud_sdk/agentgateway/_fragments.py index fcd48d59..43a69cfd 100644 --- a/src/sap_cloud_sdk/agentgateway/_fragments.py +++ b/src/sap_cloud_sdk/agentgateway/_fragments.py @@ -16,6 +16,7 @@ ) from sap_cloud_sdk.agentgateway.exceptions import MCPServerNotFoundError +from sap_cloud_sdk.core.telemetry import Module logger = logging.getLogger(__name__) @@ -35,7 +36,10 @@ class FragmentLabel(str, Enum): def _list_fragments_by_label(label: FragmentLabel, tenant_subdomain: str) -> list: - client = create_fragment_client(instance=_DESTINATION_INSTANCE) + client = create_fragment_client( + instance=_DESTINATION_INSTANCE, + _telemetry_source=Module.AGENTGATEWAY, + ) return client.list_instance_fragments( filter=ListOptions(filter_labels=[Label(key=LABEL_KEY, values=[label.value])]), tenant=tenant_subdomain, diff --git a/src/sap_cloud_sdk/agentgateway/_lob.py b/src/sap_cloud_sdk/agentgateway/_lob.py index 0ada5b63..19e23eab 100644 --- a/src/sap_cloud_sdk/agentgateway/_lob.py +++ b/src/sap_cloud_sdk/agentgateway/_lob.py @@ -18,6 +18,7 @@ ConsumptionLevel, ConsumptionOptions, ) +from sap_cloud_sdk.core.telemetry import Module from sap_cloud_sdk.agentgateway._fragments import ( LABEL_KEY, @@ -88,7 +89,10 @@ def _fetch_auth_token( Raises: MCPServerNotFoundError: If no auth token is returned. """ - client = create_destination_client(instance=_DESTINATION_INSTANCE) + client = create_destination_client( + instance=_DESTINATION_INSTANCE, + _telemetry_source=Module.AGENTGATEWAY, + ) dest = client.get_destination( dest_name, level=ConsumptionLevel.PROVIDER_SUBACCOUNT, diff --git a/src/sap_cloud_sdk/agentgateway/agw_client.py b/src/sap_cloud_sdk/agentgateway/agw_client.py index 411ff4a8..033cbb91 100644 --- a/src/sap_cloud_sdk/agentgateway/agw_client.py +++ b/src/sap_cloud_sdk/agentgateway/agw_client.py @@ -106,6 +106,7 @@ def __init__( self, tenant_subdomain: str | Callable[[], str] | None = None, config: ClientConfig | None = None, + _telemetry_source: Module | None = None, ): """Initialize the Agent Gateway client. @@ -114,11 +115,13 @@ def __init__( Can be a string or a callable returning a string. Required for LoB agents, ignored for Customer agents. config: Client configuration. Uses defaults if not provided. + _telemetry_source: Internal telemetry source identifier. Not intended for external use. """ self._tenant_subdomain = tenant_subdomain self._config = config or ClientConfig() self._token_cache = _TokenCache(self._config) self._gateway_url_cache = _GatewayUrlCache() + self._telemetry_source = _telemetry_source @staticmethod def _resolve_value( @@ -545,6 +548,8 @@ def _unwrap_exception_group(exc: BaseException) -> BaseException: def create_client( tenant_subdomain: str | Callable[[], str] | None = None, config: ClientConfig | None = None, + *, + _telemetry_source: Module | None = None, ) -> AgentGatewayClient: """Create an Agent Gateway client for discovering and invoking MCP tools. @@ -556,6 +561,7 @@ def create_client( Can be a string or a callable returning a string. Required for LoB agents, ignored for Customer agents. config: Client configuration. Uses defaults if not provided. + _telemetry_source: Internal telemetry source identifier. Not intended for external use. Returns: AgentGatewayClient instance. @@ -609,4 +615,8 @@ def create_client( user_auth = await agw_client.get_user_auth(user_token="user-jwt") ``` """ - return AgentGatewayClient(tenant_subdomain=tenant_subdomain, config=config) + return AgentGatewayClient( + tenant_subdomain=tenant_subdomain, + config=config, + _telemetry_source=_telemetry_source, + ) diff --git a/src/sap_cloud_sdk/core/auditlog_ng/__init__.py b/src/sap_cloud_sdk/core/auditlog_ng/__init__.py index 5273d000..c262b608 100644 --- a/src/sap_cloud_sdk/core/auditlog_ng/__init__.py +++ b/src/sap_cloud_sdk/core/auditlog_ng/__init__.py @@ -116,7 +116,10 @@ def _get_config_from_destination( create_client as _dest_create_client, ) - dest_client = _dest_create_client(instance=destination_instance) + dest_client = _dest_create_client( + instance=destination_instance, + _telemetry_source=Module.AUDITLOG_NG, + ) options = ( ConsumptionOptions( fragment_name=fragment_name, fragment_level=ConsumptionLevel.SUBACCOUNT diff --git a/src/sap_cloud_sdk/core/data_anonymization/_http_transport.py b/src/sap_cloud_sdk/core/data_anonymization/_http_transport.py index 360c7c27..80c77e1e 100644 --- a/src/sap_cloud_sdk/core/data_anonymization/_http_transport.py +++ b/src/sap_cloud_sdk/core/data_anonymization/_http_transport.py @@ -357,13 +357,16 @@ def _cert_from_inline_values( def _cert_from_destination(self, name: str) -> str: """Resolve a combined PEM bundle via Destination and write it to a temp file.""" try: + from sap_cloud_sdk.core.telemetry import Module from sap_cloud_sdk.destination import ( AccessStrategy, create_certificate_client, create_client, ) - destination_client = create_client() + destination_client = create_client( + _telemetry_source=Module.DATA_ANONYMIZATION, + ) destination = destination_client.get_instance_destination(name) if destination is None: destination = destination_client.get_subaccount_destination( @@ -376,7 +379,9 @@ def _cert_from_destination(self, name: str) -> str: key_store_location = self._get_destination_keystore_location( destination, name ) - cert_client = create_certificate_client() + cert_client = create_certificate_client( + _telemetry_source=Module.DATA_ANONYMIZATION, + ) cert = cert_client.get_instance_certificate(key_store_location) if cert is None: cert = cert_client.get_subaccount_certificate( diff --git a/src/sap_cloud_sdk/destination/__init__.py b/src/sap_cloud_sdk/destination/__init__.py index d4e6b7e9..6073a98e 100644 --- a/src/sap_cloud_sdk/destination/__init__.py +++ b/src/sap_cloud_sdk/destination/__init__.py @@ -26,6 +26,7 @@ import os from typing import Optional +from sap_cloud_sdk.core.telemetry import Module from sap_cloud_sdk.destination._models import ( Destination, AuthToken, @@ -83,6 +84,7 @@ def create_client( instance: Optional[str] = None, config: Optional[DestinationConfig] = None, use_default_proxy: bool = False, + _telemetry_source: Optional[Module] = None, ): """Creates a Destination client with local/cloud detection. @@ -98,6 +100,7 @@ def create_client( will attempt to load transparent proxy configuration from APPFND_CONHOS_TRANSP_PROXY environment variable. To use a custom proxy, use client.set_proxy() after creation. Defaults to False. + _telemetry_source: Internal telemetry source identifier. Not intended for external use. Returns: DestinationClient or LocalDevDestinationClient: Client implementing the Destination interface. @@ -118,7 +121,9 @@ def create_client( tp = TokenProvider(binding) http = DestinationHttp(config=binding, token_provider=tp) - return DestinationClient(http, use_default_proxy) + return DestinationClient( + http, use_default_proxy, _telemetry_source=_telemetry_source + ) except Exception as e: raise ClientCreationError(f"failed to create destination client: {e}") @@ -128,6 +133,7 @@ def create_fragment_client( *, instance: Optional[str] = None, config: Optional[DestinationConfig] = None, + _telemetry_source: Optional[Module] = None, ): """Creates a Fragment client with local/cloud detection. @@ -139,6 +145,7 @@ def create_fragment_client( Args: instance: Instance name used for secret resolution in cloud mode. Defaults to "default". config: Optional explicit DestinationConfig. + _telemetry_source: Internal telemetry source identifier. Not intended for external use. Returns: FragmentClient or LocalDevFragmentClient: Client for managing destination fragments. @@ -159,7 +166,7 @@ def create_fragment_client( tp = TokenProvider(binding) http = DestinationHttp(config=binding, token_provider=tp) - return FragmentClient(http) + return FragmentClient(http, _telemetry_source=_telemetry_source) except Exception as e: raise ClientCreationError(f"failed to create fragment client: {e}") @@ -169,6 +176,7 @@ def create_certificate_client( *, instance: Optional[str] = None, config: Optional[DestinationConfig] = None, + _telemetry_source: Optional[Module] = None, ): """Creates a Certificate client with local/cloud detection. @@ -180,6 +188,7 @@ def create_certificate_client( Args: instance: Instance name used for secret resolution in cloud mode. Defaults to "default". config: Optional explicit DestinationConfig. + _telemetry_source: Internal telemetry source identifier. Not intended for external use. Returns: CertificateClient or LocalDevCertificateClient: Client for managing certificates. @@ -200,7 +209,7 @@ def create_certificate_client( tp = TokenProvider(binding) http = DestinationHttp(config=binding, token_provider=tp) - return CertificateClient(http) + return CertificateClient(http, _telemetry_source=_telemetry_source) except Exception as e: raise ClientCreationError(f"failed to create certificate client: {e}") diff --git a/src/sap_cloud_sdk/destination/certificate_client.py b/src/sap_cloud_sdk/destination/certificate_client.py index de7589c6..aec3cfb7 100644 --- a/src/sap_cloud_sdk/destination/certificate_client.py +++ b/src/sap_cloud_sdk/destination/certificate_client.py @@ -61,16 +61,22 @@ class CertificateClient: ``` """ - def __init__(self, http: DestinationHttp) -> None: + def __init__( + self, + http: DestinationHttp, + _telemetry_source: Optional[Module] = None, + ) -> None: """Initialize CertificateClient with dependency injection. Args: http: Configured HTTP transport for the Destination Service. + _telemetry_source: Internal telemetry source identifier. Not intended for external use. Raises: DestinationOperationError: If initialization fails. """ self._http = http + self._telemetry_source = _telemetry_source # ---------- Read operations ---------- diff --git a/src/sap_cloud_sdk/destination/client.py b/src/sap_cloud_sdk/destination/client.py index 20bf3c60..17521ad5 100644 --- a/src/sap_cloud_sdk/destination/client.py +++ b/src/sap_cloud_sdk/destination/client.py @@ -90,7 +90,12 @@ class DestinationClient: ``` """ - def __init__(self, http: DestinationHttp, use_default_proxy: bool = False) -> None: + def __init__( + self, + http: DestinationHttp, + use_default_proxy: bool = False, + _telemetry_source: Optional[Module] = None, + ) -> None: """Initialize DestinationClient with dependency injection. Note: @@ -103,6 +108,7 @@ def __init__(self, http: DestinationHttp, use_default_proxy: bool = False) -> No use_default_proxy: Whether to use the default transparent proxy for all get operations. When True, will attempt to load transparent proxy configuration from APPFND_CONHOS_TRANSP_PROXY environment variable. Defaults to False. + _telemetry_source: Internal telemetry source identifier. Not intended for external use. Raises: DestinationOperationError: If initialization fails. @@ -110,6 +116,7 @@ def __init__(self, http: DestinationHttp, use_default_proxy: bool = False) -> No self._http = http self._client_proxy_enabled = use_default_proxy self._transparent_proxy = load_transparent_proxy() + self._telemetry_source = _telemetry_source def set_proxy(self, transparent_proxy: TransparentProxy) -> None: """Set or update the transparent proxy configuration for this client. diff --git a/src/sap_cloud_sdk/destination/fragment_client.py b/src/sap_cloud_sdk/destination/fragment_client.py index bd870ef9..a377ce16 100644 --- a/src/sap_cloud_sdk/destination/fragment_client.py +++ b/src/sap_cloud_sdk/destination/fragment_client.py @@ -57,16 +57,22 @@ class FragmentClient: ``` """ - def __init__(self, http: DestinationHttp) -> None: + def __init__( + self, + http: DestinationHttp, + _telemetry_source: Optional[Module] = None, + ) -> None: """Initialize FragmentClient with dependency injection. Args: http: Configured HTTP transport for the Destination Service. + _telemetry_source: Internal telemetry source identifier. Not intended for external use. Raises: DestinationOperationError: If initialization fails. """ self._http = http + self._telemetry_source = _telemetry_source # ---------- Read operations ---------- diff --git a/src/sap_cloud_sdk/extensibility/_ums_transport.py b/src/sap_cloud_sdk/extensibility/_ums_transport.py index 49bcfdf0..cbacd083 100644 --- a/src/sap_cloud_sdk/extensibility/_ums_transport.py +++ b/src/sap_cloud_sdk/extensibility/_ums_transport.py @@ -23,6 +23,7 @@ import httpx +from sap_cloud_sdk.core.telemetry import Module from sap_cloud_sdk.destination import ConsumptionLevel from sap_cloud_sdk.destination import create_client as create_destination_client from sap_cloud_sdk.extensibility._models import ( @@ -450,7 +451,8 @@ def __init__(self, agent_ord_id: str, config: ExtensibilityConfig) -> None: self._config = config self._destination_name = _ums_destination_name(config.destination_name) self._dest_client = create_destination_client( - instance=config.destination_instance + instance=config.destination_instance, + _telemetry_source=Module.EXTENSIBILITY, ) self._cache: collections.OrderedDict[ tuple[str, str], diff --git a/src/sap_cloud_sdk/extensibility/client.py b/src/sap_cloud_sdk/extensibility/client.py index e34cfc2b..3e317236 100644 --- a/src/sap_cloud_sdk/extensibility/client.py +++ b/src/sap_cloud_sdk/extensibility/client.py @@ -612,7 +612,9 @@ async def call_hook_agw( ) ``` """ - agw_client = create_agw_client(tenant_subdomain) + agw_client = create_agw_client( + tenant_subdomain, _telemetry_source=Module.EXTENSIBILITY + ) execute_tool, get_exec_tool = await self._discover_n8n_tools( agw_client, user_token ) diff --git a/tests/agentgateway/unit/test_agw_client.py b/tests/agentgateway/unit/test_agw_client.py index b1541fa5..9d8424ad 100644 --- a/tests/agentgateway/unit/test_agw_client.py +++ b/tests/agentgateway/unit/test_agw_client.py @@ -14,6 +14,7 @@ MCPTool, AgentGatewaySDKError, ) +from sap_cloud_sdk.core.telemetry import Module # ============================================================ @@ -980,3 +981,23 @@ async def test_wraps_unexpected_error(self): agw_client = create_client(tenant_subdomain="my-tenant") with pytest.raises(AgentGatewaySDKError, match="Agent card discovery failed"): await agw_client.list_agent_cards() + + +# ============================================================ +# Test: _telemetry_source wiring +# ============================================================ + + +class TestCreateClientTelemetrySource: + """Verify _telemetry_source kwarg is stored on the AgentGatewayClient.""" + + def test_default_source_is_none(self): + agw_client = create_client(tenant_subdomain="my-tenant") + assert agw_client._telemetry_source is None + + def test_explicit_source_is_stored(self): + agw_client = create_client( + tenant_subdomain="my-tenant", + _telemetry_source=Module.EXTENSIBILITY, + ) + assert agw_client._telemetry_source is Module.EXTENSIBILITY diff --git a/tests/agentgateway/unit/test_lob.py b/tests/agentgateway/unit/test_lob.py index 39c036e5..509a5ff4 100644 --- a/tests/agentgateway/unit/test_lob.py +++ b/tests/agentgateway/unit/test_lob.py @@ -28,6 +28,7 @@ from sap_cloud_sdk.agentgateway._token_cache import _GatewayUrlCache, _TokenCache from sap_cloud_sdk.agentgateway.config import ClientConfig from sap_cloud_sdk.agentgateway.exceptions import AgentGatewaySDKError, MCPServerNotFoundError +from sap_cloud_sdk.core.telemetry import Module from sap_cloud_sdk.destination import ConsumptionLevel # Aliases for use in existing test assertions @@ -213,7 +214,10 @@ def test_uses_correct_filter_labels(self): list_mcp_fragments("tenant-sub") - mock_client.assert_called_once_with(instance="default") + mock_client.assert_called_once_with( + instance="default", + _telemetry_source=Module.AGENTGATEWAY, + ) call_args = mock_client.return_value.list_instance_fragments.call_args filter_opt = call_args.kwargs.get("filter") assert filter_opt is not None @@ -1011,3 +1015,31 @@ async def _selective_fetch(fragment_url, token, timeout): assert len(result) == 1 assert result[0].ord_id == "ord-ok" + + +# ============================================================ +# Test: _fetch_auth_token passes source kwarg to the destination factory +# ============================================================ + + +class TestFetchAuthTokenTelemetrySource: + """Verify _fetch_auth_token calls create_destination_client with + _telemetry_source=Module.AGENTGATEWAY (call site: agentgateway/_lob.py:92).""" + + def test_passes_agentgateway_as_source(self): + mock_dest = MagicMock() + mock_dest.auth_tokens = [MagicMock()] + mock_dest.auth_tokens[0].http_header = {"value": "Bearer x"} + mock_dest.url = "https://agw.example.com" + + with patch( + "sap_cloud_sdk.agentgateway._lob.create_destination_client" + ) as mock_client: + mock_client.return_value.get_destination.return_value = mock_dest + + _fetch_auth_token("dest-name", "tenant-sub") + + mock_client.assert_called_once_with( + instance="default", + _telemetry_source=Module.AGENTGATEWAY, + ) diff --git a/tests/core/unit/auditlog_ng/unit/test_create_client.py b/tests/core/unit/auditlog_ng/unit/test_create_client.py index 14a303d9..550537ff 100644 --- a/tests/core/unit/auditlog_ng/unit/test_create_client.py +++ b/tests/core/unit/auditlog_ng/unit/test_create_client.py @@ -250,7 +250,10 @@ def test_destination_triggered_by_tenant( ) as mock_dest_factory: client = create_client(tenant="my-tenant", insecure=True) - mock_dest_factory.assert_called_once_with(instance="default") + mock_dest_factory.assert_called_once_with( + instance="default", + _telemetry_source=Module.AUDITLOG_NG, + ) dest_client.get_destination.assert_called_once() call_kwargs = dest_client.get_destination.call_args.kwargs assert call_kwargs["name"] == "AuditLogV3_Destination" @@ -329,7 +332,10 @@ def test_destination_create_client_called_with_instance(self, mock_provider_cls, insecure=True, ) - mock_dest_factory.assert_called_once_with(instance="my-instance") + mock_dest_factory.assert_called_once_with( + instance="my-instance", + _telemetry_source=Module.AUDITLOG_NG, + ) def test_destination_fragment_name_forwarded(self, mock_provider_cls, mock_exporter_fn): """fragment_name is always wrapped in ConsumptionOptions and forwarded to get_destination.""" @@ -384,7 +390,10 @@ def test_destination_name_alone_enters_destination_path( insecure=True, ) - mock_dest_factory.assert_called_once_with(instance="default") + mock_dest_factory.assert_called_once_with( + instance="default", + _telemetry_source=Module.AUDITLOG_NG, + ) assert isinstance(client, AuditClient) def test_destination_name_without_fragment_uses_destination_path( @@ -439,7 +448,10 @@ def test_destination_name_without_instance_uses_default_instance( insecure=True, ) - mock_dest_factory.assert_called_once_with(instance="default") + mock_dest_factory.assert_called_once_with( + instance="default", + _telemetry_source=Module.AUDITLOG_NG, + ) assert isinstance(client, AuditClient) # ------------------------------------------------------------------ diff --git a/tests/core/unit/data_anonymization/test_http_transport.py b/tests/core/unit/data_anonymization/test_http_transport.py index afd67c8f..ec64a7d7 100644 --- a/tests/core/unit/data_anonymization/test_http_transport.py +++ b/tests/core/unit/data_anonymization/test_http_transport.py @@ -262,12 +262,12 @@ def test_resolve_cert_from_destination( monkeypatch.setattr( destination_module, "create_client", - lambda: destination_client, + lambda **_: destination_client, ) monkeypatch.setattr( destination_module, "create_certificate_client", - lambda: cert_client, + lambda **_: cert_client, ) transport = object.__new__(HttpTransport) @@ -312,11 +312,11 @@ def test_resolve_cert_from_destination_with_base64_bundle( cert_client = types.SimpleNamespace( get_instance_certificate=lambda name: certificate ) - monkeypatch.setattr(destination_module, "create_client", lambda: destination_client) + monkeypatch.setattr(destination_module, "create_client", lambda **_: destination_client) monkeypatch.setattr( destination_module, "create_certificate_client", - lambda: cert_client, + lambda **_: cert_client, ) transport = object.__new__(HttpTransport) diff --git a/tests/destination/unit/test_init.py b/tests/destination/unit/test_init.py index 6958d5f0..901d39de 100644 --- a/tests/destination/unit/test_init.py +++ b/tests/destination/unit/test_init.py @@ -17,6 +17,7 @@ from sap_cloud_sdk.destination.local_certificate_client import LocalDevCertificateClient from sap_cloud_sdk.destination.config import DestinationConfig from sap_cloud_sdk.destination.exceptions import ClientCreationError +from sap_cloud_sdk.core.telemetry import Module _NO_MOCK_FILE = patch("sap_cloud_sdk.destination.os.path.isfile", new=lambda _: False) @@ -364,3 +365,66 @@ def test_falls_through_to_cloud_when_no_mock_file(self, mock_load_config, mock_h mock_http.return_value = Mock() client = create_certificate_client() assert isinstance(client, CertificateClient) + + +class TestCreateClientTelemetrySource: + """Verify _telemetry_source kwarg is stored on the client.""" + + @_NO_MOCK_FILE + @patch("sap_cloud_sdk.destination.load_from_env_or_mount") + @patch("sap_cloud_sdk.destination.TokenProvider") + @patch("sap_cloud_sdk.destination.DestinationHttp") + def test_default_source_is_none(self, mock_http, mock_tp, mock_load_config): + mock_load_config.return_value = Mock(spec=DestinationConfig) + assert create_client()._telemetry_source is None + + @_NO_MOCK_FILE + @patch("sap_cloud_sdk.destination.load_from_env_or_mount") + @patch("sap_cloud_sdk.destination.TokenProvider") + @patch("sap_cloud_sdk.destination.DestinationHttp") + def test_explicit_source_is_stored(self, mock_http, mock_tp, mock_load_config): + mock_load_config.return_value = Mock(spec=DestinationConfig) + client = create_client(_telemetry_source=Module.AGENTGATEWAY) + assert client._telemetry_source is Module.AGENTGATEWAY + + +class TestCreateFragmentClientTelemetrySource: + """Verify _telemetry_source kwarg is stored on the fragment client.""" + + @_NO_MOCK_FILE + @patch("sap_cloud_sdk.destination.load_from_env_or_mount") + @patch("sap_cloud_sdk.destination.TokenProvider") + @patch("sap_cloud_sdk.destination.DestinationHttp") + def test_default_source_is_none(self, mock_http, mock_tp, mock_load_config): + mock_load_config.return_value = Mock(spec=DestinationConfig) + assert create_fragment_client()._telemetry_source is None + + @_NO_MOCK_FILE + @patch("sap_cloud_sdk.destination.load_from_env_or_mount") + @patch("sap_cloud_sdk.destination.TokenProvider") + @patch("sap_cloud_sdk.destination.DestinationHttp") + def test_explicit_source_is_stored(self, mock_http, mock_tp, mock_load_config): + mock_load_config.return_value = Mock(spec=DestinationConfig) + client = create_fragment_client(_telemetry_source=Module.AGENTGATEWAY) + assert client._telemetry_source is Module.AGENTGATEWAY + + +class TestCreateCertificateClientTelemetrySource: + """Verify _telemetry_source kwarg is stored on the certificate client.""" + + @_NO_MOCK_FILE + @patch("sap_cloud_sdk.destination.load_from_env_or_mount") + @patch("sap_cloud_sdk.destination.TokenProvider") + @patch("sap_cloud_sdk.destination.DestinationHttp") + def test_default_source_is_none(self, mock_http, mock_tp, mock_load_config): + mock_load_config.return_value = Mock(spec=DestinationConfig) + assert create_certificate_client()._telemetry_source is None + + @_NO_MOCK_FILE + @patch("sap_cloud_sdk.destination.load_from_env_or_mount") + @patch("sap_cloud_sdk.destination.TokenProvider") + @patch("sap_cloud_sdk.destination.DestinationHttp") + def test_explicit_source_is_stored(self, mock_http, mock_tp, mock_load_config): + mock_load_config.return_value = Mock(spec=DestinationConfig) + client = create_certificate_client(_telemetry_source=Module.DATA_ANONYMIZATION) + assert client._telemetry_source is Module.DATA_ANONYMIZATION diff --git a/tests/extensibility/unit/test_ums_transport.py b/tests/extensibility/unit/test_ums_transport.py index f150e97e..2d8ec8a9 100644 --- a/tests/extensibility/unit/test_ums_transport.py +++ b/tests/extensibility/unit/test_ums_transport.py @@ -10,6 +10,7 @@ from sap_cloud_sdk.extensibility._models import ( ExtensionCapabilityImplementation, ) +from sap_cloud_sdk.core.telemetry import Module from sap_cloud_sdk.extensibility._ums_transport import ( UmsTransport, _ums_destination_name, @@ -104,7 +105,10 @@ def test_valid_config(self, mock_dest_client, monkeypatch): transport = UmsTransport(AGENT_ORD_ID, config) assert transport._config is config assert transport._destination_name == "sap-managed-runtime-ums-exttest-dev-eu12" - mock_dest_client.assert_called_once_with(instance="default") + mock_dest_client.assert_called_once_with( + instance="default", + _telemetry_source=Module.EXTENSIBILITY, + ) @patch("sap_cloud_sdk.extensibility._ums_transport.create_destination_client") def test_destination_name_none_when_env_not_set( From de8fba39368a4d6ea66b470a6c902f532eaea022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Brun?= Date: Fri, 3 Jul 2026 20:22:53 -0300 Subject: [PATCH 2/5] remove unnecessary tests and change assertions --- tests/agentgateway/unit/test_lob.py | 35 ++----------------- .../auditlog_ng/unit/test_create_client.py | 24 +++++-------- .../extensibility/unit/test_ums_transport.py | 7 ++-- 3 files changed, 12 insertions(+), 54 deletions(-) diff --git a/tests/agentgateway/unit/test_lob.py b/tests/agentgateway/unit/test_lob.py index 509a5ff4..345e5845 100644 --- a/tests/agentgateway/unit/test_lob.py +++ b/tests/agentgateway/unit/test_lob.py @@ -28,7 +28,6 @@ from sap_cloud_sdk.agentgateway._token_cache import _GatewayUrlCache, _TokenCache from sap_cloud_sdk.agentgateway.config import ClientConfig from sap_cloud_sdk.agentgateway.exceptions import AgentGatewaySDKError, MCPServerNotFoundError -from sap_cloud_sdk.core.telemetry import Module from sap_cloud_sdk.destination import ConsumptionLevel # Aliases for use in existing test assertions @@ -214,10 +213,8 @@ def test_uses_correct_filter_labels(self): list_mcp_fragments("tenant-sub") - mock_client.assert_called_once_with( - instance="default", - _telemetry_source=Module.AGENTGATEWAY, - ) + mock_client.assert_called_once() + assert mock_client.call_args.kwargs["instance"] == "default" call_args = mock_client.return_value.list_instance_fragments.call_args filter_opt = call_args.kwargs.get("filter") assert filter_opt is not None @@ -1015,31 +1012,3 @@ async def _selective_fetch(fragment_url, token, timeout): assert len(result) == 1 assert result[0].ord_id == "ord-ok" - - -# ============================================================ -# Test: _fetch_auth_token passes source kwarg to the destination factory -# ============================================================ - - -class TestFetchAuthTokenTelemetrySource: - """Verify _fetch_auth_token calls create_destination_client with - _telemetry_source=Module.AGENTGATEWAY (call site: agentgateway/_lob.py:92).""" - - def test_passes_agentgateway_as_source(self): - mock_dest = MagicMock() - mock_dest.auth_tokens = [MagicMock()] - mock_dest.auth_tokens[0].http_header = {"value": "Bearer x"} - mock_dest.url = "https://agw.example.com" - - with patch( - "sap_cloud_sdk.agentgateway._lob.create_destination_client" - ) as mock_client: - mock_client.return_value.get_destination.return_value = mock_dest - - _fetch_auth_token("dest-name", "tenant-sub") - - mock_client.assert_called_once_with( - instance="default", - _telemetry_source=Module.AGENTGATEWAY, - ) diff --git a/tests/core/unit/auditlog_ng/unit/test_create_client.py b/tests/core/unit/auditlog_ng/unit/test_create_client.py index 550537ff..94d8b55c 100644 --- a/tests/core/unit/auditlog_ng/unit/test_create_client.py +++ b/tests/core/unit/auditlog_ng/unit/test_create_client.py @@ -250,10 +250,8 @@ def test_destination_triggered_by_tenant( ) as mock_dest_factory: client = create_client(tenant="my-tenant", insecure=True) - mock_dest_factory.assert_called_once_with( - instance="default", - _telemetry_source=Module.AUDITLOG_NG, - ) + mock_dest_factory.assert_called_once() + assert mock_dest_factory.call_args.kwargs["instance"] == "default" dest_client.get_destination.assert_called_once() call_kwargs = dest_client.get_destination.call_args.kwargs assert call_kwargs["name"] == "AuditLogV3_Destination" @@ -332,10 +330,8 @@ def test_destination_create_client_called_with_instance(self, mock_provider_cls, insecure=True, ) - mock_dest_factory.assert_called_once_with( - instance="my-instance", - _telemetry_source=Module.AUDITLOG_NG, - ) + mock_dest_factory.assert_called_once() + assert mock_dest_factory.call_args.kwargs["instance"] == "my-instance" def test_destination_fragment_name_forwarded(self, mock_provider_cls, mock_exporter_fn): """fragment_name is always wrapped in ConsumptionOptions and forwarded to get_destination.""" @@ -390,10 +386,8 @@ def test_destination_name_alone_enters_destination_path( insecure=True, ) - mock_dest_factory.assert_called_once_with( - instance="default", - _telemetry_source=Module.AUDITLOG_NG, - ) + mock_dest_factory.assert_called_once() + assert mock_dest_factory.call_args.kwargs["instance"] == "default" assert isinstance(client, AuditClient) def test_destination_name_without_fragment_uses_destination_path( @@ -448,10 +442,8 @@ def test_destination_name_without_instance_uses_default_instance( insecure=True, ) - mock_dest_factory.assert_called_once_with( - instance="default", - _telemetry_source=Module.AUDITLOG_NG, - ) + mock_dest_factory.assert_called_once() + assert mock_dest_factory.call_args.kwargs["instance"] == "default" assert isinstance(client, AuditClient) # ------------------------------------------------------------------ diff --git a/tests/extensibility/unit/test_ums_transport.py b/tests/extensibility/unit/test_ums_transport.py index 2d8ec8a9..f725e348 100644 --- a/tests/extensibility/unit/test_ums_transport.py +++ b/tests/extensibility/unit/test_ums_transport.py @@ -10,7 +10,6 @@ from sap_cloud_sdk.extensibility._models import ( ExtensionCapabilityImplementation, ) -from sap_cloud_sdk.core.telemetry import Module from sap_cloud_sdk.extensibility._ums_transport import ( UmsTransport, _ums_destination_name, @@ -105,10 +104,8 @@ def test_valid_config(self, mock_dest_client, monkeypatch): transport = UmsTransport(AGENT_ORD_ID, config) assert transport._config is config assert transport._destination_name == "sap-managed-runtime-ums-exttest-dev-eu12" - mock_dest_client.assert_called_once_with( - instance="default", - _telemetry_source=Module.EXTENSIBILITY, - ) + mock_dest_client.assert_called_once() + assert mock_dest_client.call_args.kwargs["instance"] == "default" @patch("sap_cloud_sdk.extensibility._ums_transport.create_destination_client") def test_destination_name_none_when_env_not_set( From aacbd09184da081ed569f8a7557e180c26e464c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Brun?= Date: Mon, 6 Jul 2026 09:18:57 -0300 Subject: [PATCH 3/5] bump version --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index aa778f3e..02ddcf5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sap-cloud-sdk" -version = "0.32.0" +version = "0.32.2" description = "SAP Cloud SDK for Python" readme = "README.md" license = "Apache-2.0" diff --git a/uv.lock b/uv.lock index be9e3cfa..b5070674 100644 --- a/uv.lock +++ b/uv.lock @@ -3713,7 +3713,7 @@ wheels = [ [[package]] name = "sap-cloud-sdk" -version = "0.32.0" +version = "0.32.2" source = { editable = "." } dependencies = [ { name = "grpcio" }, From 31e46495f6990187e2826461df94f8be1396b3a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Brun?= Date: Mon, 6 Jul 2026 16:23:53 -0300 Subject: [PATCH 4/5] chore: fix trailing whitespace in test_agw_client --- tests/agentgateway/unit/test_agw_client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/agentgateway/unit/test_agw_client.py b/tests/agentgateway/unit/test_agw_client.py index 5b361614..3e777d85 100644 --- a/tests/agentgateway/unit/test_agw_client.py +++ b/tests/agentgateway/unit/test_agw_client.py @@ -1001,8 +1001,9 @@ def test_explicit_source_is_stored(self): _telemetry_source=Module.EXTENSIBILITY, ) assert agw_client._telemetry_source is Module.EXTENSIBILITY - - + + +# ============================================================ # Test: get_ias_client_id # ============================================================ From e05f20f32d543e356d4ac7c258b13d3222031738 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Brun?= Date: Thu, 9 Jul 2026 11:20:34 -0300 Subject: [PATCH 5/5] add missing _telemtry_source and bump version --- pyproject.toml | 2 +- src/sap_cloud_sdk/agentgateway/_lob.py | 5 ++++- uv.lock | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 398667e9..186be223 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sap-cloud-sdk" -version = "0.33.1" +version = "0.33.2" description = "SAP Cloud SDK for Python" readme = "README.md" license = "Apache-2.0" diff --git a/src/sap_cloud_sdk/agentgateway/_lob.py b/src/sap_cloud_sdk/agentgateway/_lob.py index c43ceefb..83ad4ebd 100644 --- a/src/sap_cloud_sdk/agentgateway/_lob.py +++ b/src/sap_cloud_sdk/agentgateway/_lob.py @@ -134,7 +134,10 @@ def get_ias_client_id_lob() -> str: Any exception raised by the destination client. """ dest_name = _ias_dest_name() - client = create_destination_client(instance=_DESTINATION_INSTANCE) + client = create_destination_client( + instance=_DESTINATION_INSTANCE, + _telemetry_source=Module.AGENTGATEWAY, + ) dest = client.get_destination( dest_name, level=ConsumptionLevel.PROVIDER_SUBACCOUNT, diff --git a/uv.lock b/uv.lock index a74e5b97..838f2be1 100644 --- a/uv.lock +++ b/uv.lock @@ -3713,7 +3713,7 @@ wheels = [ [[package]] name = "sap-cloud-sdk" -version = "0.33.1" +version = "0.33.2" source = { editable = "." } dependencies = [ { name = "grpcio" },