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
36 changes: 23 additions & 13 deletions langfuse/_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1670,22 +1670,32 @@ def create_event(
def _create_remote_parent_span(
self, *, trace_id: str, parent_span_id: Optional[str]
) -> Any:
if not self._is_valid_trace_id(trace_id):
langfuse_logger.warning(
f"Passed trace ID '{trace_id}' is not a valid 32 lowercase hex char Langfuse trace id. Ignoring trace ID."
)

if parent_span_id and not self._is_valid_span_id(parent_span_id):
try:
int_trace_id = int(trace_id, 16)
if not self._is_valid_trace_id(trace_id):
langfuse_logger.warning(
f"Passed trace ID '{trace_id}' is not a standard 32 lowercase hex char Langfuse trace ID. It will be normalized to a standard OpenTelemetry trace ID format."
)
except (ValueError, TypeError):
langfuse_logger.warning(
f"Passed span ID '{parent_span_id}' is not a valid 16 lowercase hex char Langfuse span id. Ignoring parent span ID."
f"Passed trace ID '{trace_id}' is not parseable as a hex trace ID. Ignoring it and generating a random trace ID instead."
)
int_trace_id = RandomIdGenerator().generate_trace_id()

int_trace_id = int(trace_id, 16)
int_parent_span_id = (
int(parent_span_id, 16)
if parent_span_id
else RandomIdGenerator().generate_span_id()
)
if parent_span_id:
try:
int_parent_span_id = int(parent_span_id, 16)
if not self._is_valid_span_id(parent_span_id):
langfuse_logger.warning(
f"Passed span ID '{parent_span_id}' is not a standard 16 lowercase hex char Langfuse span ID. It will be normalized."
)
except (ValueError, TypeError):
langfuse_logger.warning(
f"Passed span ID '{parent_span_id}' is not parseable as a hex span ID. Ignoring it and generating a random span ID instead."
)
int_parent_span_id = RandomIdGenerator().generate_span_id()
else:
int_parent_span_id = RandomIdGenerator().generate_span_id()

span_context = otel_trace_api.SpanContext(
trace_id=int_trace_id,
Expand Down
42 changes: 42 additions & 0 deletions tests/unit/test_otel.py
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,48 @@ def test_custom_parent_span_id(self, langfuse_client, memory_exporter):
assert spans[0]["trace_id"] == trace_id
assert spans[0]["attributes"][LangfuseOtelSpanAttributes.AS_ROOT] is True

def test_non_hex_trace_id_does_not_crash(self, langfuse_client, memory_exporter):
"""A non-hex trace ID must not raise; it falls back to a random trace ID."""
# Previously ``int(trace_id, 16)`` raised ValueError for non-hex IDs even
# though the warning claimed the ID would be ignored.
remote_parent_span = langfuse_client._create_remote_parent_span(
trace_id="my-trace-123", parent_span_id=None
)

span_context = remote_parent_span.get_span_context()
# 128-bit trace ID -> non-zero and within range
assert 0 < span_context.trace_id < 2**128
assert 0 < span_context.span_id < 2**64

def test_non_hex_parent_span_id_does_not_crash(
self, langfuse_client, memory_exporter
):
"""A non-hex parent span ID must not raise; it falls back to a random span ID."""
remote_parent_span = langfuse_client._create_remote_parent_span(
trace_id="abcdef1234567890abcdef1234567890",
parent_span_id="not-a-hex-span",
)

span_context = remote_parent_span.get_span_context()
assert span_context.trace_id == int("abcdef1234567890abcdef1234567890", 16)
assert 0 < span_context.span_id < 2**64

def test_parseable_non_standard_trace_id_is_preserved(
self, langfuse_client, memory_exporter
):
"""A hex but non-32-char trace ID stays usable (no regression, no fallback)."""
# 31 hex chars: parseable as hex, just not zero-padded to 32.
non_standard_trace_id = "1ab" + "0" * 28 # 31 chars
assert len(non_standard_trace_id) == 31

remote_parent_span = langfuse_client._create_remote_parent_span(
trace_id=non_standard_trace_id, parent_span_id=None
)

span_context = remote_parent_span.get_span_context()
# The integer is derived directly from the passed hex, not randomized.
assert span_context.trace_id == int(non_standard_trace_id, 16)

def test_multiple_generations_in_trace(self, langfuse_client, memory_exporter):
"""Test creating multiple generation spans within the same trace."""
# Create a trace with multiple generation spans
Expand Down