From 37635b079ecd4deda539b9293a7bb43cff132fce Mon Sep 17 00:00:00 2001 From: Sakshar Thakkar Date: Wed, 1 Jul 2026 16:13:06 -0700 Subject: [PATCH 1/3] feat(tracing): derive LLM Ops URL source param from span Source field The ?source= query param is what the server persists as Trace.Source (the span-body Source is ignored on ingest), but _build_url hardcoded CodedAgents, mislabeling every non-coded-agent producer that exports through this SDK (e.g. Agent Builder, which sets uipath.source=1=Agents). Derive source from span_list[0]['Source'] (SpanSource), defaulting to CodedAgents. Re-implements the intent of stale PR #1428 on the v3 URL. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_0137aKdrnRYUdFg7iY7GmPYM --- .../src/uipath/tracing/_otel_exporters.py | 14 ++++++--- .../tests/tracing/test_otel_exporters.py | 31 ++++++++++++++++++- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/packages/uipath/src/uipath/tracing/_otel_exporters.py b/packages/uipath/src/uipath/tracing/_otel_exporters.py index fd7d01d5c..f8ec3aace 100644 --- a/packages/uipath/src/uipath/tracing/_otel_exporters.py +++ b/packages/uipath/src/uipath/tracing/_otel_exporters.py @@ -13,7 +13,7 @@ from uipath._utils._ssl_context import get_httpx_client_kwargs from uipath.platform.common import _SpanUtils -from uipath.platform.common._span_utils import SpanStatus +from uipath.platform.common._span_utils import SpanSource, SpanStatus from uipath.platform.common.retry import NON_RETRYABLE_STATUS_CODES from uipath.platform.constants import ( ENV_BASE_URL, @@ -381,11 +381,15 @@ def _process_span_attributes(self, span_data: Dict[str, Any]) -> None: span_data["Status"] = status def _build_url(self, span_list: list[Dict[str, Any]]) -> str: - """Construct the URL for the API request.""" + """Construct the URL for the API request. + + The `source` query param is what the server persists as Trace.Source + (the span-body Source is ignored on ingest), so derive it from the + span's resolved SpanSource. Falls back to CodedAgents when absent. + """ trace_id = str(span_list[0]["TraceId"]) - return ( - f"{self.base_url}/api/Traces/v3/spans?traceId={trace_id}&source=CodedAgents" - ) + source = str(span_list[0].get("Source") or SpanSource.CODED_AGENTS) + return f"{self.base_url}/api/Traces/v3/spans?traceId={trace_id}&source={source}" def _send_with_retries( self, url: str, payload: list[Dict[str, Any]], max_retries: int = 4 diff --git a/packages/uipath/tests/tracing/test_otel_exporters.py b/packages/uipath/tests/tracing/test_otel_exporters.py index bba826be5..7a51bc4e3 100644 --- a/packages/uipath/tests/tracing/test_otel_exporters.py +++ b/packages/uipath/tests/tracing/test_otel_exporters.py @@ -7,7 +7,7 @@ from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export import SpanExportResult -from uipath.platform.common._span_utils import SpanStatus +from uipath.platform.common._span_utils import SpanSource, SpanStatus from uipath.platform.constants import ( HEADER_INTERNAL_ACCOUNT_ID, HEADER_INTERNAL_TENANT_ID, @@ -290,6 +290,35 @@ def test_build_url_uses_v3_endpoint(mock_env_vars): assert "/api/Traces/spans" not in url.replace("/api/Traces/v3/spans", "") +def test_build_url_uses_span_source_agents(mock_env_vars): + """_build_url must render the span's Source (Agents), not the hardcoded CodedAgents.""" + with patch("uipath.tracing._otel_exporters.httpx.Client"): + exporter = LlmOpsHttpExporter() + span_list = [{"TraceId": "ab" * 16, "Source": SpanSource.AGENTS}] + url = exporter._build_url(span_list) + assert "&source=Agents" in url + assert "&source=CodedAgents" not in url + assert "/api/Traces/v3/spans" in url + + +def test_build_url_uses_span_source_coded_agents(mock_env_vars): + """An explicit CodedAgents Source still renders CodedAgents.""" + with patch("uipath.tracing._otel_exporters.httpx.Client"): + exporter = LlmOpsHttpExporter() + span_list = [{"TraceId": "ab" * 16, "Source": SpanSource.CODED_AGENTS}] + url = exporter._build_url(span_list) + assert "&source=CodedAgents" in url + + +def test_build_url_defaults_to_coded_agents_when_source_missing(mock_env_vars): + """When the span dict has no Source key, default to CodedAgents (back-compat).""" + with patch("uipath.tracing._otel_exporters.httpx.Client"): + exporter = LlmOpsHttpExporter() + span_list = [{"TraceId": "ab" * 16}] + url = exporter._build_url(span_list) + assert "&source=CodedAgents" in url + + def test_determine_status_ok_returns_string(mock_env_vars): with patch("uipath.tracing._otel_exporters.httpx.Client"): exporter = LlmOpsHttpExporter() From 9538bab79a6232c80d799d722b672d8847ededc3 Mon Sep 17 00:00:00 2001 From: Sakshar Thakkar Date: Wed, 1 Jul 2026 16:14:54 -0700 Subject: [PATCH 2/3] test(tracing): assert uipath.source=1 span exports as source=Agents Guards the full attribute->Source->URL path so a future change to the uipath.source mapping can't silently regress source attribution. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_0137aKdrnRYUdFg7iY7GmPYM --- .../tests/tracing/test_otel_exporters.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/packages/uipath/tests/tracing/test_otel_exporters.py b/packages/uipath/tests/tracing/test_otel_exporters.py index 7a51bc4e3..939dbb8cd 100644 --- a/packages/uipath/tests/tracing/test_otel_exporters.py +++ b/packages/uipath/tests/tracing/test_otel_exporters.py @@ -319,6 +319,48 @@ def test_build_url_defaults_to_coded_agents_when_source_missing(mock_env_vars): assert "&source=CodedAgents" in url +def test_agent_builder_span_yields_source_agents(mock_env_vars): + """A span with uipath.source=1 must flow through to &source=Agents in the URL. + + Drives a real span dict through otel_span_to_uipath_span().to_dict() rather + than hand-building it, guarding the whole attribute->Source->URL path. + """ + from opentelemetry.trace import SpanContext, StatusCode + + from uipath.platform.common import _SpanUtils + + # otel_span_to_uipath_span reads the context via get_span_context() and + # formats trace_id/span_id as hex, so provide a real SpanContext. + span = MagicMock(spec=ReadableSpan) + span.get_span_context.return_value = SpanContext( + trace_id=0xABCDEF1234567890ABCDEF1234567890, + span_id=0x1234567890ABCDEF, + is_remote=False, + ) + span.parent = None + span.name = "agent-span" + span.status.status_code = StatusCode.OK + span.status.description = None + span.attributes = { + "uipath.custom_instrumentation": True, + "uipath.source": 1, # SourceEnum.Agents + } + span.events = [] + span.links = [] + span.start_time = 0 + span.end_time = 1 + + span_dict = _SpanUtils.otel_span_to_uipath_span( + span, serialize_attributes=False + ).to_dict(serialize_attributes=False) + assert str(span_dict["Source"]) == "Agents" + + with patch("uipath.tracing._otel_exporters.httpx.Client"): + exporter = LlmOpsHttpExporter() + url = exporter._build_url([span_dict]) + assert "&source=Agents" in url + + def test_determine_status_ok_returns_string(mock_env_vars): with patch("uipath.tracing._otel_exporters.httpx.Client"): exporter = LlmOpsHttpExporter() From 244848cfbed7ca6ac112502d7b1e7e7a1b685231 Mon Sep 17 00:00:00 2001 From: Sakshar Thakkar Date: Wed, 1 Jul 2026 16:48:40 -0700 Subject: [PATCH 3/3] chore(uipath): bump version to 2.12.5 Release the LLM Ops trace source-derivation fix in _build_url. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_0137aKdrnRYUdFg7iY7GmPYM --- packages/uipath/pyproject.toml | 2 +- packages/uipath/uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index efcce771c..20bc31c4f 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.12.4" +version = "2.12.5" description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools." readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 87ca9e781..8156765aa 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.12.4" +version = "2.12.5" source = { editable = "." } dependencies = [ { name = "applicationinsights" },