Skip to content
Open
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.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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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."""
Expand Down Expand Up @@ -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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's get_external?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_external_current_span() returns the span the agents runtime registers with UiPathSpanUtils, which is the Escalation span during an escalation.

As said followed the way span_id is taken here: #1948 (comment)

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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
62 changes: 61 additions & 1 deletion packages/uipath-platform/tests/services/test_actions_service.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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,
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.

2 changes: 1 addition & 1 deletion packages/uipath/uv.lock

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

Loading