Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/uipath-platform/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down Expand Up @@ -87,6 +97,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]:
Expand Down Expand Up @@ -114,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,
Expand All @@ -138,6 +149,13 @@ def to_dict(self, serialize_attributes: bool = True) -> Dict[str, Any]:
"AgentVersion": self.agent_version,
"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:
Expand Down Expand Up @@ -284,6 +302,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")
Expand Down Expand Up @@ -334,6 +353,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,
Expand Down
79 changes: 79 additions & 0 deletions packages/uipath-platform/tests/services/test_span_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,85 @@
from uipath.platform.common import UiPathSpan, _SpanUtils


class TestOTelToUiPathSpan:
"""OTEL attribute -> top-level UiPathSpan field mapping.

`_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.
"""

ATTRIBUTE_FIELD_MAP = [
("executionType", "execution_type", "ExecutionType", 1),
("agentVersion", "agent_version", "AgentVersion", "1.2.3"),
("referenceId", "reference_id", "ReferenceId", "ref-abc"),
("verbosityLevel", "verbosity_level", "VerbosityLevel", 6),
]

@patch.dict(os.environ, {"UIPATH_ORGANIZATION_ID": "test-org"})
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(
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()

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

@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."""

Expand Down
2 changes: 1 addition & 1 deletion packages/uipath-platform/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/uipath/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 2 additions & 0 deletions packages/uipath/src/uipath/tracing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
AttachmentDirection,
AttachmentProvider,
SpanAttachment,
VerbosityLevel,
)

from ._live_tracking_processor import LiveTrackingSpanProcessor
Expand All @@ -23,4 +24,5 @@
"AttachmentDirection",
"AttachmentProvider",
"SpanAttachment",
"VerbosityLevel",
]
13 changes: 13 additions & 0 deletions packages/uipath/tests/tracing/test_otel_exporters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
4 changes: 2 additions & 2 deletions packages/uipath/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading