From 2b31e95351f4d8c71e16ec148105910e0a36959d Mon Sep 17 00:00:00 2001 From: cotovanu-cristian Date: Wed, 13 May 2026 13:08:47 +0300 Subject: [PATCH 1/6] feat(platform): promote verbosityLevel attribute to top-level VerbosityLevel field Add a `verbosity_level: Optional[int]` field on `UiPathSpan` that surfaces as `"VerbosityLevel"` in `to_dict()`. `_SpanUtils.otel_span_to_uipath_span` now promotes a `verbosityLevel` OTEL span attribute into this top-level field, following the same pattern as `executionType`, `agentVersion`, and `referenceId`. Lets producers tag a span as internal-only (e.g. VerbosityLevel=OFF=6) so LLMOps can filter it from user-facing trace UIs. Bumps uipath-platform 0.1.48 -> 0.1.49. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/uipath-platform/pyproject.toml | 2 +- .../src/uipath/platform/common/_span_utils.py | 4 ++ .../tests/services/test_span_utils.py | 54 +++++++++++++++++++ packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 5 files changed, 61 insertions(+), 3 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 80546ec3a..bb97d8c38 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.48" +version = "0.1.49" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/common/_span_utils.py b/packages/uipath-platform/src/uipath/platform/common/_span_utils.py index cd7e15e23..c57fdeebd 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_span_utils.py +++ b/packages/uipath-platform/src/uipath/platform/common/_span_utils.py @@ -87,6 +87,7 @@ class UiPathSpan: # Top-level fields for internal tracing schema execution_type: Optional[int] = None agent_version: Optional[str] = None + verbosity_level: Optional[int] = None attachments: Optional[List[SpanAttachment]] = None def to_dict(self, serialize_attributes: bool = True) -> Dict[str, Any]: @@ -136,6 +137,7 @@ def to_dict(self, serialize_attributes: bool = True) -> Dict[str, Any]: "ReferenceId": self.reference_id, "ExecutionType": self.execution_type, "AgentVersion": self.agent_version, + "VerbosityLevel": self.verbosity_level, "Attachments": attachments_out, } @@ -284,6 +286,7 @@ def otel_span_to_uipath_span( execution_type = attributes_dict.get("executionType") agent_version = attributes_dict.get("agentVersion") reference_id = attributes_dict.get("referenceId") + verbosity_level = attributes_dict.get("verbosityLevel") # Source: override via uipath.source attribute, else DEFAULT_SOURCE uipath_source = attributes_dict.get("uipath.source") @@ -334,6 +337,7 @@ def otel_span_to_uipath_span( span_type=span_type, execution_type=execution_type, agent_version=agent_version, + verbosity_level=verbosity_level, reference_id=reference_id, source=source, attachments=attachments, diff --git a/packages/uipath-platform/tests/services/test_span_utils.py b/packages/uipath-platform/tests/services/test_span_utils.py index 80cd0d2db..560b5d59c 100644 --- a/packages/uipath-platform/tests/services/test_span_utils.py +++ b/packages/uipath-platform/tests/services/test_span_utils.py @@ -362,6 +362,60 @@ def test_uipath_span_missing_execution_type_and_agent_version(self): assert span_dict["ExecutionType"] is None assert span_dict["AgentVersion"] is None + @patch.dict(os.environ, {"UIPATH_ORGANIZATION_ID": "test-org"}) + def test_uipath_span_promotes_verbosity_level_attribute(self): + """Test that uipath.verbosity_level attribute promotes to top-level VerbosityLevel.""" + mock_span = Mock(spec=OTelSpan) + + trace_id = 0x123456789ABCDEF0123456789ABCDEF0 + span_id = 0x0123456789ABCDEF + mock_context = SpanContext(trace_id=trace_id, span_id=span_id, is_remote=False) + mock_span.get_span_context.return_value = mock_context + + mock_span.name = "AgentDefinition" + mock_span.parent = None + mock_span.status.status_code = StatusCode.OK + mock_span.attributes = {"verbosityLevel": 6} + mock_span.events = [] + mock_span.links = [] + + current_time_ns = int(datetime.now().timestamp() * 1e9) + mock_span.start_time = current_time_ns + mock_span.end_time = current_time_ns + 1000000 + + uipath_span = _SpanUtils.otel_span_to_uipath_span(mock_span) + span_dict = uipath_span.to_dict() + + assert span_dict["VerbosityLevel"] == 6 + assert uipath_span.verbosity_level == 6 + + @patch.dict(os.environ, {"UIPATH_ORGANIZATION_ID": "test-org"}) + def test_uipath_span_missing_verbosity_level_defaults_none(self): + """Test that absent uipath.verbosity_level leaves VerbosityLevel=None.""" + mock_span = Mock(spec=OTelSpan) + + trace_id = 0x123456789ABCDEF0123456789ABCDEF0 + span_id = 0x0123456789ABCDEF + mock_context = SpanContext(trace_id=trace_id, span_id=span_id, is_remote=False) + mock_span.get_span_context.return_value = mock_context + + mock_span.name = "Agent run" + mock_span.parent = None + mock_span.status.status_code = StatusCode.OK + mock_span.attributes = {"someOtherAttr": "value"} + mock_span.events = [] + mock_span.links = [] + + current_time_ns = int(datetime.now().timestamp() * 1e9) + mock_span.start_time = current_time_ns + mock_span.end_time = current_time_ns + 1000000 + + uipath_span = _SpanUtils.otel_span_to_uipath_span(mock_span) + span_dict = uipath_span.to_dict() + + assert span_dict["VerbosityLevel"] is None + assert uipath_span.verbosity_level is None + @patch.dict(os.environ, {"UIPATH_ORGANIZATION_ID": "test-org"}) def test_uipath_span_source_defaults_to_robots(self): """Test that Source defaults to 4 (Robots) and ignores attributes.source.""" diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 1e7878b10..3320bcd9d 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.48" +version = "0.1.49" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index c51486b96..25d0364a5 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.48" +version = "0.1.49" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 25f14380c53bbf2976c942e55cb31e341e03c693 Mon Sep 17 00:00:00 2001 From: cotovanu-cristian Date: Wed, 13 May 2026 13:24:05 +0300 Subject: [PATCH 2/6] feat(platform): expose VerbosityLevel enum mirroring LLMOps backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `VerbosityLevel` IntEnum (Verbose=0, Trace=1, Information=2, Warning=3, Error=4, Critical=5, Off=6) in `uipath.platform.common._span_utils`, and re-export it via `uipath.tracing` for public consumption — same pattern as `AttachmentDirection`, `AttachmentProvider`, `SpanAttachment`. Values match the C# `UiPath.LLMOps.DataAccess.Models.VerbosityLevelEnum`. Producers can now set `verbosityLevel=VerbosityLevel.OFF` on a span attribute to keep it out of user-facing trace UIs. Bumps uipath 2.10.63 -> 2.10.64, tightens uipath-platform lower bound to 0.1.49 so the new uipath release also pulls the field plumbing in. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/uipath/platform/common/_span_utils.py | 10 ++++++++++ .../tests/services/test_span_utils.py | 15 +++++++++++++++ packages/uipath/pyproject.toml | 4 ++-- packages/uipath/src/uipath/tracing/__init__.py | 2 ++ .../uipath/tests/tracing/test_otel_exporters.py | 13 +++++++++++++ packages/uipath/uv.lock | 2 +- 6 files changed, 43 insertions(+), 3 deletions(-) diff --git a/packages/uipath-platform/src/uipath/platform/common/_span_utils.py b/packages/uipath-platform/src/uipath/platform/common/_span_utils.py index c57fdeebd..1ec9e060f 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_span_utils.py +++ b/packages/uipath-platform/src/uipath/platform/common/_span_utils.py @@ -29,6 +29,16 @@ class AttachmentDirection(IntEnum): OUT = 2 +class VerbosityLevel(IntEnum): + VERBOSE = 0 + TRACE = 1 + INFORMATION = 2 + WARNING = 3 + ERROR = 4 + CRITICAL = 5 + OFF = 6 + + class SpanAttachment(BaseModel): """Represents an attachment in the UiPath tracing system.""" diff --git a/packages/uipath-platform/tests/services/test_span_utils.py b/packages/uipath-platform/tests/services/test_span_utils.py index 560b5d59c..781916159 100644 --- a/packages/uipath-platform/tests/services/test_span_utils.py +++ b/packages/uipath-platform/tests/services/test_span_utils.py @@ -10,6 +10,21 @@ from uipath.platform.common import UiPathSpan, _SpanUtils +class TestVerbosityLevel: + """VerbosityLevel enum mirrors the LLMOps backend enum.""" + + def test_values_match_backend_enum(self) -> None: + from uipath.platform.common._span_utils import VerbosityLevel + + assert VerbosityLevel.VERBOSE == 0 + assert VerbosityLevel.TRACE == 1 + assert VerbosityLevel.INFORMATION == 2 + assert VerbosityLevel.WARNING == 3 + assert VerbosityLevel.ERROR == 4 + assert VerbosityLevel.CRITICAL == 5 + assert VerbosityLevel.OFF == 6 + + class TestNormalizeIds: """Tests for OTEL ID normalization functions.""" diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index aba6ae877..4155766e5 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "uipath" -version = "2.10.63" +version = "2.10.64" 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" dependencies = [ "uipath-core>=0.5.8, <0.6.0", "uipath-runtime>=0.10.1, <0.11.0", - "uipath-platform>=0.1.47, <0.2.0", + "uipath-platform>=0.1.49, <0.2.0", "click>=8.3.1", "httpx>=0.28.1", "pyjwt>=2.10.1", diff --git a/packages/uipath/src/uipath/tracing/__init__.py b/packages/uipath/src/uipath/tracing/__init__.py index aaef6328c..e6c37bc99 100644 --- a/packages/uipath/src/uipath/tracing/__init__.py +++ b/packages/uipath/src/uipath/tracing/__init__.py @@ -5,6 +5,7 @@ AttachmentDirection, AttachmentProvider, SpanAttachment, + VerbosityLevel, ) from ._live_tracking_processor import LiveTrackingSpanProcessor @@ -23,4 +24,5 @@ "AttachmentDirection", "AttachmentProvider", "SpanAttachment", + "VerbosityLevel", ] diff --git a/packages/uipath/tests/tracing/test_otel_exporters.py b/packages/uipath/tests/tracing/test_otel_exporters.py index a55fa5d60..8d2761c8a 100644 --- a/packages/uipath/tests/tracing/test_otel_exporters.py +++ b/packages/uipath/tests/tracing/test_otel_exporters.py @@ -810,5 +810,18 @@ def test_none_stays_none(self, mock_env_vars, mock_span): assert payload["ProcessKey"] is None +class TestVerbosityLevelReexport: + """VerbosityLevel from uipath-platform is re-exported via uipath.tracing.""" + + def test_uipath_tracing_reexports_verbosity_level(self) -> None: + from uipath.platform.common._span_utils import ( + VerbosityLevel as _CommonVerbosity, + ) + from uipath.tracing import VerbosityLevel as _TracingVerbosity + + assert _TracingVerbosity is _CommonVerbosity + assert _TracingVerbosity.OFF == 6 + + if __name__ == "__main__": unittest.main() diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 25d0364a5..a184861d5 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.63" +version = "2.10.64" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From aa0ce1bb5bde83b6856922e655fb99167fffe566 Mon Sep 17 00:00:00 2001 From: cotovanu-cristian Date: Wed, 13 May 2026 13:32:18 +0300 Subject: [PATCH 3/6] test(platform): consolidate VerbosityLevel tests into single promotion test Replace the tautological `test_values_match_backend_enum` (which just read back source values) and the two verbosity-specific promotion tests with a single table-driven test in `TestVerbosityLevel` that documents which OTEL attributes are promoted to top-level `UiPathSpan` fields. The test serves as both regression guard and documentation: adding a row means the attribute is newly promoted, removing one signals a contract break for downstream consumers. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../tests/services/test_span_utils.py | 105 +++++++----------- 1 file changed, 41 insertions(+), 64 deletions(-) diff --git a/packages/uipath-platform/tests/services/test_span_utils.py b/packages/uipath-platform/tests/services/test_span_utils.py index 781916159..70870c90c 100644 --- a/packages/uipath-platform/tests/services/test_span_utils.py +++ b/packages/uipath-platform/tests/services/test_span_utils.py @@ -11,18 +11,49 @@ class TestVerbosityLevel: - """VerbosityLevel enum mirrors the LLMOps backend enum.""" + """Top-level UiPathSpan fields populated by attribute promotion. + + `_SpanUtils.otel_span_to_uipath_span` promotes a small set of OTEL + attributes onto dedicated `UiPathSpan` fields surfaced under + `to_dict()`. This test documents that contract — adding a new row + means the attribute is newly promoted, removing one breaks + downstream consumers. + """ + + PROMOTIONS = [ + ("executionType", "execution_type", "ExecutionType", 1), + ("agentVersion", "agent_version", "AgentVersion", "1.2.3"), + ("referenceId", "reference_id", "ReferenceId", "ref-abc"), + ("verbosityLevel", "verbosity_level", "VerbosityLevel", 6), + ] - def test_values_match_backend_enum(self) -> None: - from uipath.platform.common._span_utils import VerbosityLevel + @patch.dict(os.environ, {"UIPATH_ORGANIZATION_ID": "test-org"}) + def test_attributes_are_promoted_to_top_level_fields(self) -> None: + attrs = {otel_attr: value for otel_attr, _, _, value in self.PROMOTIONS} + + mock_span = Mock(spec=OTelSpan) + mock_context = SpanContext( + trace_id=0x123456789ABCDEF0123456789ABCDEF0, + span_id=0x0123456789ABCDEF, + is_remote=False, + ) + mock_span.get_span_context.return_value = mock_context + mock_span.name = "test-span" + mock_span.parent = None + mock_span.status.status_code = StatusCode.OK + mock_span.attributes = attrs + mock_span.events = [] + mock_span.links = [] + now_ns = int(datetime.now().timestamp() * 1e9) + mock_span.start_time = now_ns + mock_span.end_time = now_ns + 1_000_000 + + uipath_span = _SpanUtils.otel_span_to_uipath_span(mock_span) + span_dict = uipath_span.to_dict() - assert VerbosityLevel.VERBOSE == 0 - assert VerbosityLevel.TRACE == 1 - assert VerbosityLevel.INFORMATION == 2 - assert VerbosityLevel.WARNING == 3 - assert VerbosityLevel.ERROR == 4 - assert VerbosityLevel.CRITICAL == 5 - assert VerbosityLevel.OFF == 6 + for _, span_field, top_level_key, value in self.PROMOTIONS: + assert getattr(uipath_span, span_field) == value, span_field + assert span_dict[top_level_key] == value, top_level_key class TestNormalizeIds: @@ -377,60 +408,6 @@ def test_uipath_span_missing_execution_type_and_agent_version(self): assert span_dict["ExecutionType"] is None assert span_dict["AgentVersion"] is None - @patch.dict(os.environ, {"UIPATH_ORGANIZATION_ID": "test-org"}) - def test_uipath_span_promotes_verbosity_level_attribute(self): - """Test that uipath.verbosity_level attribute promotes to top-level VerbosityLevel.""" - mock_span = Mock(spec=OTelSpan) - - trace_id = 0x123456789ABCDEF0123456789ABCDEF0 - span_id = 0x0123456789ABCDEF - mock_context = SpanContext(trace_id=trace_id, span_id=span_id, is_remote=False) - mock_span.get_span_context.return_value = mock_context - - mock_span.name = "AgentDefinition" - mock_span.parent = None - mock_span.status.status_code = StatusCode.OK - mock_span.attributes = {"verbosityLevel": 6} - mock_span.events = [] - mock_span.links = [] - - current_time_ns = int(datetime.now().timestamp() * 1e9) - mock_span.start_time = current_time_ns - mock_span.end_time = current_time_ns + 1000000 - - uipath_span = _SpanUtils.otel_span_to_uipath_span(mock_span) - span_dict = uipath_span.to_dict() - - assert span_dict["VerbosityLevel"] == 6 - assert uipath_span.verbosity_level == 6 - - @patch.dict(os.environ, {"UIPATH_ORGANIZATION_ID": "test-org"}) - def test_uipath_span_missing_verbosity_level_defaults_none(self): - """Test that absent uipath.verbosity_level leaves VerbosityLevel=None.""" - mock_span = Mock(spec=OTelSpan) - - trace_id = 0x123456789ABCDEF0123456789ABCDEF0 - span_id = 0x0123456789ABCDEF - mock_context = SpanContext(trace_id=trace_id, span_id=span_id, is_remote=False) - mock_span.get_span_context.return_value = mock_context - - mock_span.name = "Agent run" - mock_span.parent = None - mock_span.status.status_code = StatusCode.OK - mock_span.attributes = {"someOtherAttr": "value"} - mock_span.events = [] - mock_span.links = [] - - current_time_ns = int(datetime.now().timestamp() * 1e9) - mock_span.start_time = current_time_ns - mock_span.end_time = current_time_ns + 1000000 - - uipath_span = _SpanUtils.otel_span_to_uipath_span(mock_span) - span_dict = uipath_span.to_dict() - - assert span_dict["VerbosityLevel"] is None - assert uipath_span.verbosity_level is None - @patch.dict(os.environ, {"UIPATH_ORGANIZATION_ID": "test-org"}) def test_uipath_span_source_defaults_to_robots(self): """Test that Source defaults to 4 (Robots) and ignores attributes.source.""" From f2f01e044f3e1c342339cf74041903f4d6a958b0 Mon Sep 17 00:00:00 2001 From: cotovanu-cristian Date: Wed, 13 May 2026 14:02:36 +0300 Subject: [PATCH 4/6] test(platform): rename TestVerbosityLevel -> TestPromotedAttributes The class checks promotion of all attributes (executionType, agentVersion, referenceId, verbosityLevel), not just verbosity level. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/uipath-platform/tests/services/test_span_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/uipath-platform/tests/services/test_span_utils.py b/packages/uipath-platform/tests/services/test_span_utils.py index 70870c90c..04313531d 100644 --- a/packages/uipath-platform/tests/services/test_span_utils.py +++ b/packages/uipath-platform/tests/services/test_span_utils.py @@ -10,8 +10,8 @@ from uipath.platform.common import UiPathSpan, _SpanUtils -class TestVerbosityLevel: - """Top-level UiPathSpan fields populated by attribute promotion. +class TestPromotedAttributes: + """OTEL attributes promoted to top-level UiPathSpan fields. `_SpanUtils.otel_span_to_uipath_span` promotes a small set of OTEL attributes onto dedicated `UiPathSpan` fields surfaced under From 85d9ee816e5edd891c833bc35b511460f7161e0f Mon Sep 17 00:00:00 2001 From: cotovanu-cristian Date: Wed, 13 May 2026 14:23:11 +0300 Subject: [PATCH 5/6] test(platform): rename TestPromotedAttributes -> TestOTelToUiPathSpan Names the test class after the conversion function it exercises (`_SpanUtils.otel_span_to_uipath_span`). Drops "promotion" terminology in favour of "OTel attribute -> UiPathSpan field mapping". Co-Authored-By: Claude Opus 4.7 (1M context) --- .../tests/services/test_span_utils.py | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/uipath-platform/tests/services/test_span_utils.py b/packages/uipath-platform/tests/services/test_span_utils.py index 04313531d..f720944df 100644 --- a/packages/uipath-platform/tests/services/test_span_utils.py +++ b/packages/uipath-platform/tests/services/test_span_utils.py @@ -10,17 +10,17 @@ from uipath.platform.common import UiPathSpan, _SpanUtils -class TestPromotedAttributes: - """OTEL attributes promoted to top-level UiPathSpan fields. +class TestOTelToUiPathSpan: + """OTEL attribute -> top-level UiPathSpan field mapping. - `_SpanUtils.otel_span_to_uipath_span` promotes a small set of OTEL - attributes onto dedicated `UiPathSpan` fields surfaced under - `to_dict()`. This test documents that contract — adding a new row - means the attribute is newly promoted, removing one breaks + `_SpanUtils.otel_span_to_uipath_span` lifts a small set of OTEL + span attributes onto dedicated `UiPathSpan` fields surfaced under + `to_dict()`. This test documents that mapping — adding a new row + means the attribute is newly mapped, removing one breaks downstream consumers. """ - PROMOTIONS = [ + ATTRIBUTE_FIELD_MAP = [ ("executionType", "execution_type", "ExecutionType", 1), ("agentVersion", "agent_version", "AgentVersion", "1.2.3"), ("referenceId", "reference_id", "ReferenceId", "ref-abc"), @@ -28,8 +28,10 @@ class TestPromotedAttributes: ] @patch.dict(os.environ, {"UIPATH_ORGANIZATION_ID": "test-org"}) - def test_attributes_are_promoted_to_top_level_fields(self) -> None: - attrs = {otel_attr: value for otel_attr, _, _, value in self.PROMOTIONS} + def test_attributes_map_to_top_level_fields(self) -> None: + attrs = { + otel_attr: value for otel_attr, _, _, value in self.ATTRIBUTE_FIELD_MAP + } mock_span = Mock(spec=OTelSpan) mock_context = SpanContext( @@ -51,7 +53,7 @@ def test_attributes_are_promoted_to_top_level_fields(self) -> None: uipath_span = _SpanUtils.otel_span_to_uipath_span(mock_span) span_dict = uipath_span.to_dict() - for _, span_field, top_level_key, value in self.PROMOTIONS: + for _, span_field, top_level_key, value in self.ATTRIBUTE_FIELD_MAP: assert getattr(uipath_span, span_field) == value, span_field assert span_dict[top_level_key] == value, top_level_key From 48431b0b38ad29825a8f2e6f9bceec36bcbf6a5c Mon Sep 17 00:00:00 2001 From: cotovanu-cristian Date: Wed, 13 May 2026 14:38:25 +0300 Subject: [PATCH 6/6] fix(platform): omit VerbosityLevel from to_dict() when unset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backwards compat: pre-existing spans never emitted a VerbosityLevel field on the wire — the LLMOps backend applies its own default (Information=2) for spans that don't carry it. Unconditionally adding `"VerbosityLevel": null` to every span's JSON would change the wire format for every existing span in every existing agent. Make the key conditional in `UiPathSpan.to_dict()`: emit only when the producer explicitly opted in (e.g. AgentDefinition span with VerbosityLevel.OFF). Spans that don't set it produce byte-for-byte identical JSON to pre-change, leaving the backend's default path untouched. Adds `test_verbosity_level_omitted_when_unset` to lock the contract. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/uipath/platform/common/_span_utils.py | 10 ++++-- .../tests/services/test_span_utils.py | 31 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/uipath-platform/src/uipath/platform/common/_span_utils.py b/packages/uipath-platform/src/uipath/platform/common/_span_utils.py index 1ec9e060f..3b607c515 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_span_utils.py +++ b/packages/uipath-platform/src/uipath/platform/common/_span_utils.py @@ -125,7 +125,7 @@ def to_dict(self, serialize_attributes: bool = True) -> Dict[str, Any]: for att in self.attachments ] - return { + result: Dict[str, Any] = { "Id": self.id, "TraceId": self.trace_id, "ParentId": self.parent_id, @@ -147,9 +147,15 @@ def to_dict(self, serialize_attributes: bool = True) -> Dict[str, Any]: "ReferenceId": self.reference_id, "ExecutionType": self.execution_type, "AgentVersion": self.agent_version, - "VerbosityLevel": self.verbosity_level, "Attachments": attachments_out, } + # Backwards compat: only include VerbosityLevel when the producer + # opted in. Spans that don't set verbosity_level produce the same + # wire bytes as before this field existed, so the LLMOps backend + # applies its existing default (Information=2) untouched. + if self.verbosity_level is not None: + result["VerbosityLevel"] = self.verbosity_level + return result class _SpanUtils: diff --git a/packages/uipath-platform/tests/services/test_span_utils.py b/packages/uipath-platform/tests/services/test_span_utils.py index f720944df..a7f51dd14 100644 --- a/packages/uipath-platform/tests/services/test_span_utils.py +++ b/packages/uipath-platform/tests/services/test_span_utils.py @@ -57,6 +57,37 @@ def test_attributes_map_to_top_level_fields(self) -> None: assert getattr(uipath_span, span_field) == value, span_field assert span_dict[top_level_key] == value, top_level_key + @patch.dict(os.environ, {"UIPATH_ORGANIZATION_ID": "test-org"}) + def test_verbosity_level_omitted_when_unset(self) -> None: + """Spans that don't set verbosityLevel must not carry the key on the wire. + + Backwards compat: pre-existing spans never emitted VerbosityLevel; the + LLMOps backend applies its own default. Adding `"VerbosityLevel": null` + unconditionally would change the wire format for every existing span. + """ + mock_span = Mock(spec=OTelSpan) + mock_context = SpanContext( + trace_id=0x123456789ABCDEF0123456789ABCDEF0, + span_id=0x0123456789ABCDEF, + is_remote=False, + ) + mock_span.get_span_context.return_value = mock_context + mock_span.name = "legacy-span" + mock_span.parent = None + mock_span.status.status_code = StatusCode.OK + mock_span.attributes = {"someOtherAttr": "value"} + mock_span.events = [] + mock_span.links = [] + now_ns = int(datetime.now().timestamp() * 1e9) + mock_span.start_time = now_ns + mock_span.end_time = now_ns + 1_000_000 + + uipath_span = _SpanUtils.otel_span_to_uipath_span(mock_span) + span_dict = uipath_span.to_dict() + + assert uipath_span.verbosity_level is None + assert "VerbosityLevel" not in span_dict + class TestNormalizeIds: """Tests for OTEL ID normalization functions."""