Skip to content

Commit cb77eb2

Browse files
fix(platform): resolve correct span in build_trace_context_headers (#1679)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 81338db commit cb77eb2

2 files changed

Lines changed: 103 additions & 32 deletions

File tree

packages/uipath-platform/src/uipath/platform/chat/llm_trace_context.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,17 @@
55
from uipath.core.tracing.span_utils import UiPathSpanUtils
66

77
from ..common._config import UiPathConfig
8+
from ..common._span_utils import _SpanUtils
89

910

1011
def build_trace_context_headers(
1112
extra_baggage: list[str] | None = None,
1213
) -> dict[str, str]:
13-
"""Build W3C-style trace context headers from the current OpenTelemetry span.
14+
"""Build W3C-style trace context headers for LLM Gateway requests.
15+
16+
Resolves the current span via ``UiPathSpanUtils.get_external_current_span()``
17+
(which returns the deepest active span from the LLMOps hierarchy) with a
18+
fallback to ``trace.get_current_span()``.
1419
1520
Args:
1621
extra_baggage: Additional baggage entries (e.g. ``["source=agents"]``)
@@ -25,16 +30,17 @@ def build_trace_context_headers(
2530
headers: dict[str, str] = {}
2631
llmops_span = UiPathSpanUtils.get_external_current_span()
2732
span = llmops_span or trace.get_current_span()
33+
config_trace_id = UiPathConfig.trace_id
2834
ctx = span.get_span_context()
29-
if ctx and ctx.trace_id and ctx.span_id:
30-
trace_id = format(ctx.trace_id, "032x")
31-
span_id = format(ctx.span_id, "016x")
35+
if config_trace_id and ctx and ctx.span_id:
36+
trace_id = _SpanUtils.normalize_trace_id(config_trace_id)
37+
span_id = format(ctx.span_id, "032x")
3238
headers["x-uipath-traceparent-id"] = f"00-{trace_id}-{span_id}"
3339

3440
baggage_parts: list[str] = list(extra_baggage) if extra_baggage else []
3541
if folder_key := UiPathConfig.folder_key:
3642
baggage_parts.append(f"folderKey={folder_key}")
37-
if agent_id := UiPathConfig.process_uuid:
43+
if agent_id := UiPathConfig.agent_id:
3844
baggage_parts.append(f"agentId={agent_id}")
3945
if process_key := UiPathConfig.process_key:
4046
baggage_parts.append(f"processKey={process_key}")

packages/uipath-platform/tests/services/test_llm_trace_context.py

Lines changed: 92 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import os
44
from unittest.mock import patch
55

6-
from opentelemetry import trace
76
from opentelemetry.sdk.trace import TracerProvider
87
from opentelemetry.trace import NonRecordingSpan, SpanContext, TraceFlags
98
from uipath.core.feature_flags import FeatureFlags
@@ -13,6 +12,13 @@
1312
FEATURE_FLAG = "EnableTraceContextHeaders"
1413

1514

15+
def _make_span():
16+
"""Create a real OTEL span for testing."""
17+
provider = TracerProvider()
18+
tracer = provider.get_tracer("test")
19+
return tracer.start_span("test-span")
20+
21+
1622
class TestFeatureFlagDisabled:
1723
"""When the feature flag is off, no headers are returned."""
1824

@@ -28,48 +34,74 @@ def test_returns_empty_dict_when_explicitly_disabled(self) -> None:
2834

2935

3036
class TestTraceparentHeader:
31-
"""When enabled, x-uipath-traceparent-id is populated from the active span."""
37+
"""When enabled, x-uipath-traceparent-id is populated from config + span."""
3238

3339
def setup_method(self) -> None:
3440
FeatureFlags.reset_flags()
3541
FeatureFlags.configure_flags({FEATURE_FLAG: True})
3642

37-
def test_traceparent_from_active_span(self) -> None:
38-
provider = TracerProvider()
39-
tracer = provider.get_tracer("test")
40-
with tracer.start_as_current_span("test-span") as span:
41-
ctx = span.get_span_context()
42-
expected_trace_id = format(ctx.trace_id, "032x")
43-
expected_span_id = format(ctx.span_id, "016x")
44-
43+
def test_traceparent_from_config_and_span(self) -> None:
44+
span = _make_span()
45+
ctx = span.get_span_context()
46+
expected_span_id = format(ctx.span_id, "032x")
47+
config_trace = "abcdef1234567890abcdef1234567890"
48+
env = {"UIPATH_TRACE_ID": config_trace}
49+
with (
50+
patch.dict(os.environ, env),
51+
patch(
52+
"uipath.platform.chat.llm_trace_context.trace.get_current_span",
53+
return_value=span,
54+
),
55+
):
4556
headers = build_trace_context_headers()
4657

4758
assert "x-uipath-traceparent-id" in headers
4859
value = headers["x-uipath-traceparent-id"]
49-
assert value == f"00-{expected_trace_id}-{expected_span_id}"
50-
# Verify format: version (2) + dash + trace_id (32) + dash + span_id (16)
60+
assert value == f"00-{config_trace}-{expected_span_id}"
5161
parts = value.split("-")
5262
assert len(parts) == 3
5363
assert parts[0] == "00"
5464
assert len(parts[1]) == 32
55-
assert len(parts[2]) == 16
65+
assert len(parts[2]) == 32
66+
67+
def test_no_traceparent_without_config_trace_id(self) -> None:
68+
headers = build_trace_context_headers()
69+
assert "x-uipath-traceparent-id" not in headers
70+
71+
def test_traceparent_strips_dashes_from_config_trace_id(self) -> None:
72+
span = _make_span()
73+
uuid_trace = "abcdef12-3456-7890-abcd-ef1234567890"
74+
env = {"UIPATH_TRACE_ID": uuid_trace}
75+
with (
76+
patch.dict(os.environ, env),
77+
patch(
78+
"uipath.platform.chat.llm_trace_context.trace.get_current_span",
79+
return_value=span,
80+
),
81+
):
82+
headers = build_trace_context_headers()
5683

57-
def test_no_traceparent_without_active_span(self) -> None:
58-
# INVALID_SPAN has trace_id=0 and span_id=0
59-
from opentelemetry.context import attach, detach
84+
value = headers["x-uipath-traceparent-id"]
85+
parts = value.split("-")
86+
assert parts[1] == "abcdef1234567890abcdef1234567890"
6087

88+
def test_no_traceparent_with_invalid_span(self) -> None:
6189
ctx = SpanContext(
6290
trace_id=0,
6391
span_id=0,
6492
is_remote=False,
6593
trace_flags=TraceFlags(0),
6694
)
67-
non_recording = NonRecordingSpan(ctx)
68-
token = attach(trace.set_span_in_context(non_recording))
69-
try:
95+
span = NonRecordingSpan(ctx)
96+
env = {"UIPATH_TRACE_ID": "abcdef1234567890abcdef1234567890"}
97+
with (
98+
patch.dict(os.environ, env),
99+
patch(
100+
"uipath.platform.chat.llm_trace_context.trace.get_current_span",
101+
return_value=span,
102+
),
103+
):
70104
headers = build_trace_context_headers()
71-
finally:
72-
detach(token)
73105

74106
assert "x-uipath-traceparent-id" not in headers
75107

@@ -84,7 +116,7 @@ def setup_method(self) -> None:
84116
def test_all_env_vars_present(self) -> None:
85117
env = {
86118
"UIPATH_FOLDER_KEY": "folder-abc",
87-
"UIPATH_PROCESS_UUID": "agent-123",
119+
"UIPATH_AGENT_ID": "agent-123",
88120
"UIPATH_PROCESS_KEY": "process-789",
89121
}
90122
with patch.dict(os.environ, env, clear=True):
@@ -103,6 +135,31 @@ def test_partial_env_vars(self) -> None:
103135
baggage = headers["x-uipath-tracebaggage"]
104136
assert "folderKey=folder-only" in baggage
105137

138+
def test_agent_id_from_agent_id_env(self) -> None:
139+
env = {"UIPATH_AGENT_ID": "real-agent-id"}
140+
with patch.dict(os.environ, env, clear=True):
141+
headers = build_trace_context_headers()
142+
143+
baggage = headers["x-uipath-tracebaggage"]
144+
assert "agentId=real-agent-id" in baggage
145+
146+
def test_agent_id_falls_back_to_project_id(self) -> None:
147+
env = {"UIPATH_PROJECT_ID": "project-123"}
148+
with patch.dict(os.environ, env, clear=True):
149+
headers = build_trace_context_headers()
150+
151+
baggage = headers["x-uipath-tracebaggage"]
152+
assert "agentId=project-123" in baggage
153+
154+
def test_no_agent_id_without_env_vars(self) -> None:
155+
env = {"UIPATH_FOLDER_KEY": "f1"}
156+
with patch.dict(os.environ, env, clear=True):
157+
headers = build_trace_context_headers()
158+
159+
baggage = headers["x-uipath-tracebaggage"]
160+
assert "agentId" not in baggage
161+
assert "folderKey=f1" in baggage
162+
106163
def test_no_baggage_without_env_vars(self) -> None:
107164
with patch.dict(os.environ, {}, clear=True):
108165
headers = build_trace_context_headers()
@@ -112,7 +169,7 @@ def test_no_baggage_without_env_vars(self) -> None:
112169
def test_baggage_comma_separated(self) -> None:
113170
env = {
114171
"UIPATH_FOLDER_KEY": "f1",
115-
"UIPATH_PROCESS_UUID": "a1",
172+
"UIPATH_AGENT_ID": "a1",
116173
}
117174
with patch.dict(os.environ, env, clear=True):
118175
headers = build_trace_context_headers()
@@ -148,14 +205,22 @@ def setup_method(self) -> None:
148205
FeatureFlags.configure_flags({FEATURE_FLAG: True})
149206

150207
def test_both_headers_present(self) -> None:
151-
provider = TracerProvider()
152-
tracer = provider.get_tracer("test")
153-
env = {"UIPATH_FOLDER_KEY": "folder-abc"}
208+
span = _make_span()
209+
env = {
210+
"UIPATH_FOLDER_KEY": "folder-abc",
211+
"UIPATH_TRACE_ID": "abcdef1234567890abcdef1234567890",
212+
}
154213
with (
155-
tracer.start_as_current_span("test-span"),
156214
patch.dict(os.environ, env, clear=True),
215+
patch(
216+
"uipath.platform.chat.llm_trace_context.trace.get_current_span",
217+
return_value=span,
218+
),
157219
):
158220
headers = build_trace_context_headers()
159221

160222
assert "x-uipath-traceparent-id" in headers
223+
assert headers["x-uipath-traceparent-id"].startswith(
224+
"00-abcdef1234567890abcdef1234567890-"
225+
)
161226
assert "x-uipath-tracebaggage" in headers

0 commit comments

Comments
 (0)