From 5171d5b810a60c403ef27b68708677d1508c80fa Mon Sep 17 00:00:00 2001 From: wei-juncheng Date: Sat, 4 Jul 2026 12:20:49 +0800 Subject: [PATCH] fix(tracing): avoid crash on non-hex trace_context IDs --- langfuse/_client/client.py | 36 ++++++++++++++++++++------------ tests/unit/test_otel.py | 42 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 13 deletions(-) diff --git a/langfuse/_client/client.py b/langfuse/_client/client.py index 6ce72d27a..f538ade75 100644 --- a/langfuse/_client/client.py +++ b/langfuse/_client/client.py @@ -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, diff --git a/tests/unit/test_otel.py b/tests/unit/test_otel.py index 46a085a71..80601a9e5 100644 --- a/tests/unit/test_otel.py +++ b/tests/unit/test_otel.py @@ -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