Skip to content
Closed
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-core/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-core"
version = "0.5.22"
version = "0.5.23"
description = "UiPath Core abstractions"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
10 changes: 9 additions & 1 deletion packages/uipath-core/src/uipath/core/tracing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,19 @@
from uipath.core.tracing.decorators import traced
from uipath.core.tracing.span_utils import UiPathSpanUtils
from uipath.core.tracing.trace_manager import UiPathTraceManager
from uipath.core.tracing.types import UiPathTraceSettings
from uipath.core.tracing.types import (
EXCLUDE_SCOPES_FEATURE_FLAG,
EXCLUDED_INSTRUMENTATION_SCOPES,
UiPathTraceSettings,
is_excluded_instrumentation_scope,
)

__all__ = [
"traced",
"UiPathSpanUtils",
"UiPathTraceManager",
"UiPathTraceSettings",
"EXCLUDED_INSTRUMENTATION_SCOPES",
"EXCLUDE_SCOPES_FEATURE_FLAG",
"is_excluded_instrumentation_scope",
]
9 changes: 8 additions & 1 deletion packages/uipath-core/src/uipath/core/tracing/processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
SpanExporter,
)

from uipath.core.tracing.types import UiPathTraceSettings
from uipath.core.tracing.types import (
UiPathTraceSettings,
is_excluded_instrumentation_scope,
)


class UiPathExecutionTraceProcessorMixin:
Expand All @@ -34,6 +37,10 @@ def on_start(self, span: Span, parent_context: context_api.Context | None = None

def on_end(self, span: ReadableSpan):
"""Called when a span ends. Filters before delegating to parent."""
# Always drop third-party instrumentation noise, then apply the
# optional span filter.
if is_excluded_instrumentation_scope(span):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[GR-4] New span-exclusion behavior is enabled unconditionally

The new always-on drop of a2a-python-sdk spans runs for every agent (coded and low-code) with no kill switch. New behavior should default OFF behind an env-var feature flag (UIPATH_FEATURE_) on a patch version, then flip the default on later.

Fix: Gate is_excluded_instrumentation_scope behind an env-var feature flag (default off), or document why this is a safe deterministic noise filter exempt from the flag policy.

return
span_filter = self._settings.span_filter if self._settings else None
if span_filter is None or span_filter(span):
parent = cast(SpanProcessor, super())
Expand Down
30 changes: 30 additions & 0 deletions packages/uipath-core/src/uipath/core/tracing/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,36 @@
from opentelemetry.sdk.trace import ReadableSpan
from pydantic import BaseModel, Field

from uipath.core.feature_flags import FeatureFlags

# Instrumentation scopes whose spans are third-party internal noise and should
# not be exported to LLM Observability. The a2a-sdk auto-instruments its
# JSON-RPC transport under the "a2a-python-sdk" scope (one span per transport
# method), which surfaces as unparented, low-value nodes in the execution trace;
# meaningful A2A activity is emitted as the caller's own span instead. Dropped at
# the span-processor layer so the exclusion applies to every agent (coded and
# low-code), independent of any optional UiPathTraceSettings.span_filter.
EXCLUDED_INSTRUMENTATION_SCOPES: frozenset[str] = frozenset({"a2a-python-sdk"})

# Feature flag gating the exclusion. Ships default-off (set
# ``UIPATH_FEATURE_ExcludeThirdPartyTraceScopes=true`` to opt in); the default is
# flipped on in a later release once the noise filtering is validated in the field.
EXCLUDE_SCOPES_FEATURE_FLAG = "ExcludeThirdPartyTraceScopes"


def is_excluded_instrumentation_scope(span: ReadableSpan) -> bool:
"""Return True when the span comes from an excluded instrumentation scope.

Gated behind the ``UIPATH_FEATURE_ExcludeThirdPartyTraceScopes`` feature flag
(default off). Used by the UiPath span processors to drop third-party
instrumentation noise (see ``EXCLUDED_INSTRUMENTATION_SCOPES``) before export,
independent of any configured ``span_filter``.
"""
if not FeatureFlags.is_flag_enabled(EXCLUDE_SCOPES_FEATURE_FLAG):
return False
scope = getattr(span, "instrumentation_scope", None)
return scope is not None and scope.name in EXCLUDED_INSTRUMENTATION_SCOPES


class UiPathTraceSettings(BaseModel):
"""Trace settings for UiPath SDK."""
Expand Down
84 changes: 84 additions & 0 deletions packages/uipath-core/tests/tracing/test_span_filtering.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,90 @@ def test_filter_with_empty_attributes(self):
assert "has-required" in exported_names
assert "no-attrs" not in exported_names

def test_excluded_instrumentation_scope_dropped(self, monkeypatch):
"""With the feature flag on, excluded third-party scopes (a2a-sdk) are
dropped even with no span_filter — the coded-agent path."""
from unittest.mock import MagicMock

from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult

monkeypatch.setenv("UIPATH_FEATURE_ExcludeThirdPartyTraceScopes", "true")

mock_exporter = MagicMock(spec=SpanExporter)
mock_exporter.export.return_value = SpanExportResult.SUCCESS

settings = UiPathTraceSettings(span_filter=None)
trace_manager = UiPathTraceManager()
trace_manager.add_span_exporter(mock_exporter, batch=False, settings=settings)

a2a_tracer = trace.get_tracer("a2a-python-sdk")
normal_tracer = trace.get_tracer("uipath.test.agent")
with a2a_tracer.start_as_current_span("JsonRpcTransport.send_message"):
pass
with normal_tracer.start_as_current_span("finance-agent"):
pass

trace_manager.flush_spans()

exported_spans = []
for call in mock_exporter.export.call_args_list:
exported_spans.extend(call[0][0])

exported_names = {s.name for s in exported_spans}
assert "finance-agent" in exported_names
assert "JsonRpcTransport.send_message" not in exported_names

def test_excluded_scope_kept_when_flag_disabled(self):
"""With the feature flag off (default), excluded-scope spans are still
exported — the change ships dark."""
from unittest.mock import MagicMock

from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult

mock_exporter = MagicMock(spec=SpanExporter)
mock_exporter.export.return_value = SpanExportResult.SUCCESS

settings = UiPathTraceSettings(span_filter=None)
trace_manager = UiPathTraceManager()
trace_manager.add_span_exporter(mock_exporter, batch=False, settings=settings)

a2a_tracer = trace.get_tracer("a2a-python-sdk")
with a2a_tracer.start_as_current_span("JsonRpcTransport.send_message"):
pass

trace_manager.flush_spans()

exported_names = {
s.name for call in mock_exporter.export.call_args_list for s in call[0][0]
}
assert "JsonRpcTransport.send_message" in exported_names

def test_is_excluded_instrumentation_scope_helper(self, monkeypatch):
"""The scope helper is a no-op when the flag is off, and (flag on) matches
a2a-sdk spans while ignoring normal spans."""
from typing import cast

from opentelemetry.sdk.trace import ReadableSpan, TracerProvider

from uipath.core.tracing.types import is_excluded_instrumentation_scope

provider = TracerProvider()
a2a = provider.get_tracer("a2a-python-sdk").start_span("x")
a2a.end()
normal = provider.get_tracer("uipath.agent").start_span("y")
normal.end()
# start_span is typed as the API Span; the runtime object is a ReadableSpan.
a2a_span = cast(ReadableSpan, a2a)
normal_span = cast(ReadableSpan, normal)

# Flag off (default): no exclusion.
assert is_excluded_instrumentation_scope(a2a_span) is False

# Flag on: a2a-sdk excluded, normal scope kept.
monkeypatch.setenv("UIPATH_FEATURE_ExcludeThirdPartyTraceScopes", "true")
assert is_excluded_instrumentation_scope(a2a_span) is True
assert is_excluded_instrumentation_scope(normal_span) is False

def test_different_filters_per_exporter(self):
"""Test that different exporters can have different filters."""
from unittest.mock import MagicMock
Expand Down
2 changes: 1 addition & 1 deletion packages/uipath-core/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-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,11 +1,11 @@
[project]
name = "uipath"
version = "2.11.12"
version = "2.11.13"
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.21, <0.6.0",
"uipath-core>=0.5.23, <0.6.0",
"uipath-runtime>=0.11.0, <0.12.0",
"uipath-platform>=0.1.76, <0.2.0",
"click>=8.3.1",
Expand Down
13 changes: 10 additions & 3 deletions packages/uipath/src/uipath/tracing/_live_tracking_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
from opentelemetry import context as context_api
from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor

from uipath.core.tracing import UiPathTraceSettings
from uipath.core.tracing import (
UiPathTraceSettings,
is_excluded_instrumentation_scope,
)
from uipath.tracing._otel_exporters import LlmOpsHttpExporter, SpanStatus

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -72,13 +75,17 @@ def on_start(
self, span: Span, parent_context: context_api.Context | None = None
) -> None:
"""Called when span starts - upsert with RUNNING status (non-blocking)."""
# Apply factory span filter if configured
# Drop third-party instrumentation noise, then apply the optional filter.
if is_excluded_instrumentation_scope(span):
return
if self.span_filter is None or self.span_filter(span):
self._upsert_span_async(span, status_override=SpanStatus.RUNNING)

def on_end(self, span: ReadableSpan) -> None:
"""Called when span ends - upsert with final status (non-blocking)."""
# Apply factory span filter if configured
# Drop third-party instrumentation noise, then apply the optional filter.
if is_excluded_instrumentation_scope(span):
return
if self.span_filter is None or self.span_filter(span):
self._upsert_span_async(span)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,34 @@ def test_on_end_no_filter_accepts_all(self, processor_no_filter, mock_exporter):

mock_exporter.upsert_span.assert_called_once_with(span)

# Tests for excluded third-party instrumentation scopes (a2a-sdk)

def test_on_start_skips_excluded_instrumentation_scope(
self, processor_no_filter, mock_exporter, monkeypatch
):
"""With the flag on, on_start drops excluded-scope spans even with no filter."""
monkeypatch.setenv("UIPATH_FEATURE_ExcludeThirdPartyTraceScopes", "true")
span = self.create_mock_span({"span_type": "agent"})
span.instrumentation_scope = Mock()
span.instrumentation_scope.name = "a2a-python-sdk"

processor_no_filter.on_start(span, None)

mock_exporter.upsert_span.assert_not_called()

def test_on_end_skips_excluded_instrumentation_scope(
self, processor_no_filter, mock_exporter, monkeypatch
):
"""With the flag on, on_end drops excluded-scope spans even with no filter."""
monkeypatch.setenv("UIPATH_FEATURE_ExcludeThirdPartyTraceScopes", "true")
span = self.create_mock_readable_span({"span_type": "agent"})
span.instrumentation_scope = Mock()
span.instrumentation_scope.name = "a2a-python-sdk"

processor_no_filter.on_end(span)

mock_exporter.upsert_span.assert_not_called()

# Tests for custom instrumentation filter

def test_on_start_with_filter_accepts_matching(
Expand Down
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