diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index c3e51954e..512c3a940 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.2.41" +version = "0.2.42" 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/action_center/_tasks_service.py b/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py index 36e2a012e..627280deb 100644 --- a/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py +++ b/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py @@ -20,6 +20,7 @@ from ..common._execution_context import UiPathExecutionContext from ..common._folder_context import FolderContext, header_folder from ..common._models import Endpoint, RequestSpec +from ..common._span_utils import _SpanUtils from .task_schema import TaskSchema from .tasks import Task, TaskRecipient, TaskRecipientType @@ -210,6 +211,7 @@ def _apply_task_source( trace_id = UiPathConfig.trace_id if not (project_id and trace_id): return + parent_span_id = _SpanUtils.current_span_id() payload["taskSource"] = { "sourceName": source_name, "sourceId": project_id, @@ -218,6 +220,7 @@ def _apply_task_source( "FolderKey": UiPathConfig.folder_key, "JobKey": UiPathConfig.job_key, "ProcessKey": UiPathConfig.process_uuid, + **({"ParentSpanId": parent_span_id} if parent_span_id else {}), **(custom_metadata or {}), }, "jobId": UiPathConfig.job_key, 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 111dd59ec..897d6ae56 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_span_utils.py +++ b/packages/uipath-platform/src/uipath/platform/common/_span_utils.py @@ -11,8 +11,9 @@ from typing import Any, Dict, List, Optional, TypeVar from opentelemetry import context as context_api +from opentelemetry import trace from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor -from opentelemetry.trace import StatusCode +from opentelemetry.trace import StatusCode, format_span_id from pydantic import BaseModel, ConfigDict, Field from uipath.core.serialization import serialize_json @@ -371,6 +372,22 @@ def normalize_span_id(value: str) -> str: else: raise ValueError(f"Invalid span ID format: {value}") + @staticmethod + def current_span_id() -> Optional[str]: + from uipath.core.tracing.span_utils import UiPathSpanUtils + + try: + span = ( + UiPathSpanUtils.get_external_current_span() or trace.get_current_span() + ) + span_context = span.get_span_context() + span_id = span_context.span_id + if span_context.is_valid and isinstance(span_id, int) and span_id: + return format_span_id(span_id) + except Exception: + logger.debug("Could not read the current span id", exc_info=True) + return None + @staticmethod def otel_span_to_uipath_span( otel_span: ReadableSpan, diff --git a/packages/uipath-platform/tests/services/test_actions_service.py b/packages/uipath-platform/tests/services/test_actions_service.py index c70f2557f..f4505d1bd 100644 --- a/packages/uipath-platform/tests/services/test_actions_service.py +++ b/packages/uipath-platform/tests/services/test_actions_service.py @@ -1,12 +1,18 @@ import json from typing import Any +from unittest.mock import MagicMock import pytest +from opentelemetry.trace import INVALID_SPAN, NonRecordingSpan, SpanContext from pytest_httpx import HTTPXMock +from uipath.core.tracing.span_utils import UiPathSpanUtils from uipath.platform import UiPathApiConfig, UiPathExecutionContext from uipath.platform.action_center import Task -from uipath.platform.action_center._tasks_service import TasksService +from uipath.platform.action_center._tasks_service import ( + TasksService, + _apply_task_source, +) from uipath.platform.action_center.tasks import TaskRecipient, TaskRecipientType from uipath.platform.constants import HEADER_USER_AGENT @@ -1071,6 +1077,60 @@ async def test_create_async_merges_task_source_metadata( assert task_source_metadata["InstanceId"] == "trace-1" +async def test_create_quickform_async_sends_the_current_span_as_parent_span_id( + qf_runner_async: Any, jit_debug_env: None, monkeypatch: pytest.MonkeyPatch +) -> None: + span = NonRecordingSpan( + SpanContext(trace_id=1, span_id=0xA3C27222815E80CA, is_remote=False) + ) + monkeypatch.setattr(UiPathSpanUtils, "get_external_current_span", lambda: span) + monkeypatch.setenv("UIPATH_TRACE_ID", "4bf92f3577b34da6a3ce929d0e0e4736") + + _, body = await qf_runner_async() + + task_source_metadata = body["taskSource"]["taskSourceMetadata"] + assert task_source_metadata["ParentSpanId"] == "a3c27222815e80ca" + + +class _BrokenSpan: + def get_span_context(self) -> Any: + raise RuntimeError("span broke") + + +def _span_with_id(span_id: Any) -> Any: + span = MagicMock() + span.get_span_context.return_value.span_id = span_id + span.get_span_context.return_value.is_valid = True + return span + + +@pytest.mark.parametrize( + "span", + [ + INVALID_SPAN, + _BrokenSpan(), + _span_with_id(0), + _span_with_id(None), + _span_with_id(""), + _span_with_id("abc"), + _span_with_id(1.5), + NonRecordingSpan(SpanContext(trace_id=1, span_id=-1, is_remote=False)), + NonRecordingSpan(SpanContext(trace_id=1, span_id=2**64, is_remote=False)), + ], +) +def test_apply_task_source_without_a_valid_span_omits_parent_span_id( + jit_debug_env: None, monkeypatch: pytest.MonkeyPatch, span: Any +) -> None: + monkeypatch.setattr(UiPathSpanUtils, "get_external_current_span", lambda: span) + payload: dict[str, Any] = {} + + _apply_task_source(payload, "Agent") + + task_source_metadata = payload["taskSource"]["taskSourceMetadata"] + assert task_source_metadata["InstanceId"] == "trace-1" + assert "ParentSpanId" not in task_source_metadata + + def test_create_omits_extra_task_source_metadata_when_unset( httpx_mock: HTTPXMock, service: TasksService, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 5c59bdb8e..3ef58613d 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.2.41" +version = "0.2.42" source = { editable = "." } dependencies = [ { name = "anyio" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index e6ae1076e..25fa35fa2 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2762,7 +2762,7 @@ wheels = [ [[package]] name = "uipath-platform" -version = "0.2.41" +version = "0.2.42" source = { editable = "../uipath-platform" } dependencies = [ { name = "anyio" },