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..b0dd9c8da 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 @@ -1,13 +1,17 @@ import asyncio +import logging import os import uuid from typing import Any, Dict, List, Optional +from opentelemetry import trace +from opentelemetry.trace import format_span_id from uipath.core.feature_flags import ( JIT_ESCALATION_APPS_FEATURE_FLAG, FeatureFlags, ) from uipath.core.tracing import traced +from uipath.core.tracing.span_utils import UiPathSpanUtils from uipath.platform.constants import ( ENV_TENANT_ID, @@ -23,6 +27,8 @@ from .task_schema import TaskSchema from .tasks import Task, TaskRecipient, TaskRecipientType +logger = logging.getLogger(__name__) + def _ensure_string_value(value: Any) -> str: """Convert any value to a string for use in field Value.""" @@ -190,6 +196,17 @@ def _apply_priority_labels_and_actionable_toggle( payload["isActionableMessageEnabled"] = is_actionable_message_enabled +def _current_span_id() -> Optional[str]: + try: + span = UiPathSpanUtils.get_external_current_span() or trace.get_current_span() + span_context = span.get_span_context() + if span_context.is_valid and span_context.span_id: + return format_span_id(span_context.span_id) + except Exception: + logger.debug("Could not read the current span id", exc_info=True) + return None + + def _apply_task_source( payload: Dict[str, Any], source_name: str, @@ -210,6 +227,7 @@ def _apply_task_source( trace_id = UiPathConfig.trace_id if not (project_id and trace_id): return + parent_span_id = _current_span_id() payload["taskSource"] = { "sourceName": source_name, "sourceId": project_id, @@ -218,6 +236,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/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" },