Skip to content

Commit 48ee1f5

Browse files
saksharthakkarclaudeJosephMar
committed
feat: add ExecutionType and AgentVersion as top-level span properties
Extract executionType and agentVersion from OTEL span attributes and expose them as top-level fields on UiPathSpan for LlmOps compatibility. - Add execution_type and agent_version fields to UiPathSpan dataclass - Update to_dict() to return ExecutionType and AgentVersion keys - Extract values in otel_span_to_uipath_span() - Bump version to 2.4.16 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> Co-Authored-By: JosephMar <Joseph.Mar@uipath.com>
1 parent a51f664 commit 48ee1f5

4 files changed

Lines changed: 122 additions & 2 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath"
3-
version = "2.4.15"
3+
version = "2.4.16"
44
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

src/uipath/tracing/_utils.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ class UiPathSpan:
9999

100100
job_key: Optional[str] = field(default_factory=lambda: env.get("UIPATH_JOB_KEY"))
101101

102+
# Top-level fields for internal tracing schema
103+
execution_type: Optional[int] = None
104+
agent_version: Optional[str] = None
105+
102106
def to_dict(self, serialize_attributes: bool = True) -> Dict[str, Any]:
103107
"""Convert the Span to a dictionary suitable for JSON serialization.
104108
@@ -137,6 +141,8 @@ def to_dict(self, serialize_attributes: bool = True) -> Dict[str, Any]:
137141
"ProcessKey": self.process_key,
138142
"JobKey": self.job_key,
139143
"ReferenceId": self.reference_id,
144+
"ExecutionType": self.execution_type,
145+
"AgentVersion": self.agent_version,
140146
}
141147

142148

@@ -285,6 +291,10 @@ def otel_span_to_uipath_span(
285291
span_type_value = attributes_dict.get("span_type", "OpenTelemetry")
286292
span_type = str(span_type_value)
287293

294+
# Top-level fields for internal tracing schema
295+
execution_type = attributes_dict.get("executionType")
296+
agent_version = attributes_dict.get("agentVersion")
297+
288298
# Create UiPathSpan from OpenTelemetry span
289299
start_time = datetime.fromtimestamp(
290300
(otel_span.start_time or 0) / 1e9
@@ -310,6 +320,8 @@ def otel_span_to_uipath_span(
310320
end_time=end_time_str,
311321
status=status,
312322
span_type=span_type,
323+
execution_type=execution_type,
324+
agent_version=agent_version,
313325
)
314326

315327
@staticmethod

tests/tracing/test_span_utils.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,3 +212,111 @@ def test_otel_span_to_uipath_span_with_env_trace_id(self):
212212

213213
# Verify the trace ID is taken from environment
214214
assert str(uipath_span.trace_id) == "00000000-0000-4000-8000-000000000000"
215+
216+
@patch.dict(os.environ, {"UIPATH_ORGANIZATION_ID": "test-org"})
217+
def test_uipath_span_includes_execution_type(self):
218+
"""Test that executionType from attributes becomes top-level ExecutionType."""
219+
mock_span = Mock(spec=OTelSpan)
220+
221+
trace_id = 0x123456789ABCDEF0123456789ABCDEF0
222+
span_id = 0x0123456789ABCDEF
223+
mock_context = SpanContext(trace_id=trace_id, span_id=span_id, is_remote=False)
224+
mock_span.get_span_context.return_value = mock_context
225+
226+
mock_span.name = "test-span"
227+
mock_span.parent = None
228+
mock_span.status.status_code = StatusCode.OK
229+
mock_span.attributes = {"executionType": 0}
230+
mock_span.events = []
231+
mock_span.links = []
232+
233+
current_time_ns = int(datetime.now().timestamp() * 1e9)
234+
mock_span.start_time = current_time_ns
235+
mock_span.end_time = current_time_ns + 1000000
236+
237+
uipath_span = _SpanUtils.otel_span_to_uipath_span(mock_span)
238+
span_dict = uipath_span.to_dict()
239+
240+
assert span_dict["ExecutionType"] == 0
241+
assert uipath_span.execution_type == 0
242+
243+
@patch.dict(os.environ, {"UIPATH_ORGANIZATION_ID": "test-org"})
244+
def test_uipath_span_includes_agent_version(self):
245+
"""Test that agentVersion from attributes becomes top-level AgentVersion."""
246+
mock_span = Mock(spec=OTelSpan)
247+
248+
trace_id = 0x123456789ABCDEF0123456789ABCDEF0
249+
span_id = 0x0123456789ABCDEF
250+
mock_context = SpanContext(trace_id=trace_id, span_id=span_id, is_remote=False)
251+
mock_span.get_span_context.return_value = mock_context
252+
253+
mock_span.name = "test-span"
254+
mock_span.parent = None
255+
mock_span.status.status_code = StatusCode.OK
256+
mock_span.attributes = {"agentVersion": "2.0.0"}
257+
mock_span.events = []
258+
mock_span.links = []
259+
260+
current_time_ns = int(datetime.now().timestamp() * 1e9)
261+
mock_span.start_time = current_time_ns
262+
mock_span.end_time = current_time_ns + 1000000
263+
264+
uipath_span = _SpanUtils.otel_span_to_uipath_span(mock_span)
265+
span_dict = uipath_span.to_dict()
266+
267+
assert span_dict["AgentVersion"] == "2.0.0"
268+
assert uipath_span.agent_version == "2.0.0"
269+
270+
@patch.dict(os.environ, {"UIPATH_ORGANIZATION_ID": "test-org"})
271+
def test_uipath_span_execution_type_and_agent_version_both(self):
272+
"""Test that both executionType and agentVersion are extracted together."""
273+
mock_span = Mock(spec=OTelSpan)
274+
275+
trace_id = 0x123456789ABCDEF0123456789ABCDEF0
276+
span_id = 0x0123456789ABCDEF
277+
mock_context = SpanContext(trace_id=trace_id, span_id=span_id, is_remote=False)
278+
mock_span.get_span_context.return_value = mock_context
279+
280+
mock_span.name = "Agent run - Agent"
281+
mock_span.parent = None
282+
mock_span.status.status_code = StatusCode.OK
283+
mock_span.attributes = {"executionType": 1, "agentVersion": "1.0.0"}
284+
mock_span.events = []
285+
mock_span.links = []
286+
287+
current_time_ns = int(datetime.now().timestamp() * 1e9)
288+
mock_span.start_time = current_time_ns
289+
mock_span.end_time = current_time_ns + 1000000
290+
291+
uipath_span = _SpanUtils.otel_span_to_uipath_span(mock_span)
292+
span_dict = uipath_span.to_dict()
293+
294+
assert span_dict["ExecutionType"] == 1
295+
assert span_dict["AgentVersion"] == "1.0.0"
296+
297+
@patch.dict(os.environ, {"UIPATH_ORGANIZATION_ID": "test-org"})
298+
def test_uipath_span_missing_execution_type_and_agent_version(self):
299+
"""Test that missing executionType and agentVersion default to None."""
300+
mock_span = Mock(spec=OTelSpan)
301+
302+
trace_id = 0x123456789ABCDEF0123456789ABCDEF0
303+
span_id = 0x0123456789ABCDEF
304+
mock_context = SpanContext(trace_id=trace_id, span_id=span_id, is_remote=False)
305+
mock_span.get_span_context.return_value = mock_context
306+
307+
mock_span.name = "test-span"
308+
mock_span.parent = None
309+
mock_span.status.status_code = StatusCode.OK
310+
mock_span.attributes = {"someOtherAttr": "value"}
311+
mock_span.events = []
312+
mock_span.links = []
313+
314+
current_time_ns = int(datetime.now().timestamp() * 1e9)
315+
mock_span.start_time = current_time_ns
316+
mock_span.end_time = current_time_ns + 1000000
317+
318+
uipath_span = _SpanUtils.otel_span_to_uipath_span(mock_span)
319+
span_dict = uipath_span.to_dict()
320+
321+
assert span_dict["ExecutionType"] is None
322+
assert span_dict["AgentVersion"] is None

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)