diff --git a/packages/uipath-core/pyproject.toml b/packages/uipath-core/pyproject.toml index 5604e3938..1a5c34ab7 100644 --- a/packages/uipath-core/pyproject.toml +++ b/packages/uipath-core/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-core" -version = "0.5.10" +version = "0.5.11" description = "UiPath Core abstractions" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-core/src/uipath/core/tracing/span_utils.py b/packages/uipath-core/src/uipath/core/tracing/span_utils.py index 0e9e9fb22..af79f0f68 100644 --- a/packages/uipath-core/src/uipath/core/tracing/span_utils.py +++ b/packages/uipath-core/src/uipath/core/tracing/span_utils.py @@ -264,7 +264,14 @@ def _get_bottom_most_span( return external_span # Neither is an ancestor of the other - they're in different branches - # Use depth as tiebreaker + # If trace IDs differ, the spans belong to different tracing systems. + # Prefer the OTEL current_span which carries the agent's trace ID. + current_trace_id = current_span.get_span_context().trace_id + external_trace_id = external_span.get_span_context().trace_id + if current_trace_id != external_trace_id: + return current_span + + # Same trace ID, use depth as tiebreaker current_depth = _span_registry.calculate_depth(current_span_id) external_depth = _span_registry.calculate_depth(external_span_id) diff --git a/packages/uipath-core/tests/tracing/test_span_nesting.py b/packages/uipath-core/tests/tracing/test_span_nesting.py index 7245f648a..c35c133ae 100644 --- a/packages/uipath-core/tests/tracing/test_span_nesting.py +++ b/packages/uipath-core/tests/tracing/test_span_nesting.py @@ -508,26 +508,19 @@ def non_recording_parent(): def test_ctx_parameter_required_when_external_deeper_than_current( span_capture: SpanCapture, ): - """Test that trace.get_current_span(ctx) is required when external span is deeper. + """Test that OTEL span is preferred when trace IDs differ from external span. Scenario: - 1. Create an external span (depth 0) - 2. Create a deeper nested external span (depth 1) - this becomes current external - 3. Create an OTel span INSIDE the deepest external context - 4. Register the deepest external span as the external provider - 5. Create a non-recording span + 1. Create an external span hierarchy (depth 0 and depth 1) + 2. Create an OTel span in a separate context + 3. Register the deeper external span as the external provider + 4. Create a non-recording span - Expected behavior with trace.get_current_span(ctx): + Expected behavior: - get_parent_context() compares: current OTel span vs external span - - External span is deeper (depth 1), so it's chosen - - trace.get_current_span(ctx) gets the external span from ctx - - Non-recording span is parented to external - - Bug with trace.get_current_span() without ctx: - - trace.get_current_span() (no args) returns the OTel span (from thread-local) - - Non-recording span gets parented to OTel span (wrong!) - - This test PASSES with the fix (ctx parameter) and FAILS without it. + - They have different trace IDs (different tracing systems) + - OTEL current span is preferred to preserve the agent trace ID + - Non-recording span is parented to OTel span """ from opentelemetry import trace @@ -619,30 +612,20 @@ def recording_child_func(): "Recording child of non-recording parent should not be captured due to ParentBased sampler" ) - # Step 2: Verify the non-recording span was parented to external_deep (deeper) - # NOT to otel_span (which is current in thread-local context) - non_recording_id = None - for span_id, parent_id in _span_registry._parent_map.items(): - stored_span = _span_registry.get_span(span_id) - if stored_span is not None and parent_id == external_deep_id: - non_recording_id = span_id + # Step 2: When trace IDs differ between OTEL and external spans, + # the OTEL current span takes priority (agent trace ID preservation). + # The non-recording span should be parented to otel_span, not external_deep. + non_recording_parented_to_otel = False + for _span_id, parent_id in _span_registry._parent_map.items(): + if parent_id == otel_span_id: + non_recording_parented_to_otel = True break - assert non_recording_id is not None, ( - "CRITICAL: Non-recording span should be parented to external_deep (deeper). " - "This requires using trace.get_current_span(ctx) NOT trace.get_current_span(). " - "With trace.get_current_span() alone, it would pick the OTel span " - "(which is current in thread-local context), not the external span." + assert non_recording_parented_to_otel, ( + "When OTEL and external spans have different trace IDs, " + "the OTEL current span should be preferred to preserve the agent trace ID." ) - # Verify it's NOT parented to the OTel span - for _span_id, parent_id in _span_registry._parent_map.items(): - if parent_id == otel_span_id: - raise AssertionError( - "Non-recording span should NOT be parented to otel_span. " - "This indicates trace.get_current_span() was used instead of trace.get_current_span(ctx)." - ) - _span_registry.clear() finally: diff --git a/packages/uipath-core/uv.lock b/packages/uipath-core/uv.lock index 2544216df..e17802fc7 100644 --- a/packages/uipath-core/uv.lock +++ b/packages/uipath-core/uv.lock @@ -1007,7 +1007,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.10" +version = "0.5.11" source = { editable = "." } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 0ea4a2f63..a8cbc6db3 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1056,7 +1056,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.10" +version = "0.5.11" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 77434aaa8..a4bf97525 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2650,7 +2650,7 @@ dev = [ [[package]] name = "uipath-core" -version = "0.5.10" +version = "0.5.11" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" },