From 7c4eade9a3a0be4089b73b769941a8e07042cfc4 Mon Sep 17 00:00:00 2001 From: Hassieb Pakzad <68423100+hassiebp@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:30:31 +0200 Subject: [PATCH 1/6] chore(client): move to LANGFUSE_BASE_URL --- langfuse/_client/client.py | 18 +++++++++++++----- langfuse/_client/environment_variables.py | 11 ++++++++++- langfuse/_client/get_client.py | 2 +- langfuse/_client/resource_manager.py | 18 +++++++++--------- langfuse/_client/span_processor.py | 6 +++--- tests/test_additional_headers_simple.py | 4 ++-- 6 files changed, 38 insertions(+), 21 deletions(-) diff --git a/langfuse/_client/client.py b/langfuse/_client/client.py index 09610567d..7b9dcfcc5 100644 --- a/langfuse/_client/client.py +++ b/langfuse/_client/client.py @@ -47,6 +47,7 @@ ) from langfuse._client.datasets import DatasetClient, DatasetItemClient from langfuse._client.environment_variables import ( + LANGFUSE_BASE_URL, LANGFUSE_DEBUG, LANGFUSE_HOST, LANGFUSE_PUBLIC_KEY, @@ -134,7 +135,8 @@ class Langfuse: Parameters: public_key (Optional[str]): Your Langfuse public API key. Can also be set via LANGFUSE_PUBLIC_KEY environment variable. secret_key (Optional[str]): Your Langfuse secret API key. Can also be set via LANGFUSE_SECRET_KEY environment variable. - host (Optional[str]): The Langfuse API host URL. Defaults to "https://cloud.langfuse.com". Can also be set via LANGFUSE_HOST environment variable. + base_url (Optional[str]): The Langfuse API base URL. Defaults to "https://cloud.langfuse.com". Can also be set via LANGFUSE_BASE_URL environment variable. + host (Optional[str]): Deprecated. Use base_url instead. The Langfuse API host URL. Defaults to "https://cloud.langfuse.com". timeout (Optional[int]): Timeout in seconds for API requests. Defaults to 5 seconds. httpx_client (Optional[httpx.Client]): Custom httpx client for making non-tracing HTTP requests. If not provided, a default client will be created. debug (bool): Enable debug logging. Defaults to False. Can also be set via LANGFUSE_DEBUG environment variable. @@ -195,6 +197,7 @@ def __init__( *, public_key: Optional[str] = None, secret_key: Optional[str] = None, + base_url: Optional[str] = None, host: Optional[str] = None, timeout: Optional[int] = None, httpx_client: Optional[httpx.Client] = None, @@ -211,7 +214,12 @@ def __init__( additional_headers: Optional[Dict[str, str]] = None, tracer_provider: Optional[TracerProvider] = None, ): - self._host = host or os.environ.get(LANGFUSE_HOST, "https://cloud.langfuse.com") + self._base_url = ( + base_url + or os.environ.get(LANGFUSE_BASE_URL) + or host + or os.environ.get(LANGFUSE_HOST, "https://cloud.langfuse.com") + ) self._environment = environment or cast( str, os.environ.get(LANGFUSE_TRACING_ENVIRONMENT) ) @@ -269,7 +277,7 @@ def __init__( self._resources = LangfuseResourceManager( public_key=public_key, secret_key=secret_key, - host=self._host, + base_url=self._base_url, timeout=timeout, environment=self._environment, release=release, @@ -2413,7 +2421,7 @@ def get_trace_url(self, *, trace_id: Optional[str] = None) -> Optional[str]: final_trace_id = trace_id or self.get_current_trace_id() return ( - f"{self._host}/project/{project_id}/traces/{final_trace_id}" + f"{self._base_url}/project/{project_id}/traces/{final_trace_id}" if project_id and final_trace_id else None ) @@ -2712,7 +2720,7 @@ async def process_item(item: ExperimentItem) -> ExperimentItemResult: project_id = self._get_project_id() if project_id: - dataset_run_url = f"{self._host}/project/{project_id}/datasets/{dataset_id}/runs/{dataset_run_id}" + dataset_run_url = f"{self._base_url}/project/{project_id}/datasets/{dataset_id}/runs/{dataset_run_id}" except Exception: pass # URL generation is optional diff --git a/langfuse/_client/environment_variables.py b/langfuse/_client/environment_variables.py index d5be09d09..6b421578a 100644 --- a/langfuse/_client/environment_variables.py +++ b/langfuse/_client/environment_variables.py @@ -35,11 +35,20 @@ Secret API key of Langfuse project """ +LANGFUSE_BASE_URL = "LANGFUSE_BASE_URL" +""" +.. envvar:: LANGFUSE_BASE_URL + +Base URL of Langfuse API. Can be set via `LANGFUSE_BASE_URL` environment variable. + +**Default value:** ``"https://cloud.langfuse.com"`` +""" + LANGFUSE_HOST = "LANGFUSE_HOST" """ .. envvar:: LANGFUSE_HOST -Host of Langfuse API. Can be set via `LANGFUSE_HOST` environment variable. +Deprecated. Use LANGFUSE_BASE_URL instead. Host of Langfuse API. Can be set via `LANGFUSE_HOST` environment variable. **Default value:** ``"https://cloud.langfuse.com"`` """ diff --git a/langfuse/_client/get_client.py b/langfuse/_client/get_client.py index 8bcdecd40..402801afd 100644 --- a/langfuse/_client/get_client.py +++ b/langfuse/_client/get_client.py @@ -40,7 +40,7 @@ def _create_client_from_instance( return Langfuse( public_key=public_key or instance.public_key, secret_key=instance.secret_key, - host=instance.host, + base_url=instance.base_url, tracing_enabled=instance.tracing_enabled, environment=instance.environment, timeout=instance.timeout, diff --git a/langfuse/_client/resource_manager.py b/langfuse/_client/resource_manager.py index 28c24e919..f0c4663d5 100644 --- a/langfuse/_client/resource_manager.py +++ b/langfuse/_client/resource_manager.py @@ -83,7 +83,7 @@ def __new__( *, public_key: str, secret_key: str, - host: str, + base_url: str, environment: Optional[str] = None, release: Optional[str] = None, timeout: Optional[int] = None, @@ -115,7 +115,7 @@ def __new__( instance._initialize_instance( public_key=public_key, secret_key=secret_key, - host=host, + base_url=base_url, timeout=timeout, environment=environment, release=release, @@ -142,7 +142,7 @@ def _initialize_instance( *, public_key: str, secret_key: str, - host: str, + base_url: str, environment: Optional[str] = None, release: Optional[str] = None, timeout: Optional[int] = None, @@ -160,7 +160,7 @@ def _initialize_instance( self.public_key = public_key self.secret_key = secret_key self.tracing_enabled = tracing_enabled - self.host = host + self.base_url = base_url self.mask = mask self.environment = environment @@ -183,7 +183,7 @@ def _initialize_instance( langfuse_processor = LangfuseSpanProcessor( public_key=self.public_key, secret_key=secret_key, - host=host, + base_url=base_url, timeout=timeout, flush_at=flush_at, flush_interval=flush_interval, @@ -212,7 +212,7 @@ def _initialize_instance( self.httpx_client = httpx.Client(timeout=timeout, headers=client_headers) self.api = FernLangfuse( - base_url=host, + base_url=base_url, username=self.public_key, password=secret_key, x_langfuse_sdk_name="python", @@ -222,7 +222,7 @@ def _initialize_instance( timeout=timeout, ) self.async_api = AsyncFernLangfuse( - base_url=host, + base_url=base_url, username=self.public_key, password=secret_key, x_langfuse_sdk_name="python", @@ -233,7 +233,7 @@ def _initialize_instance( score_ingestion_client = LangfuseClient( public_key=self.public_key, secret_key=secret_key, - base_url=host, + base_url=base_url, version=langfuse_version, timeout=timeout or 20, session=self.httpx_client, @@ -290,7 +290,7 @@ def _initialize_instance( langfuse_logger.info( f"Startup: Langfuse tracer successfully initialized | " f"public_key={self.public_key} | " - f"host={host} | " + f"base_url={base_url} | " f"environment={environment or 'default'} | " f"sample_rate={sample_rate if sample_rate is not None else 1.0} | " f"media_threads={media_upload_thread_count or 1}" diff --git a/langfuse/_client/span_processor.py b/langfuse/_client/span_processor.py index 369d5ff9e..5a2d251c0 100644 --- a/langfuse/_client/span_processor.py +++ b/langfuse/_client/span_processor.py @@ -52,7 +52,7 @@ def __init__( *, public_key: str, secret_key: str, - host: str, + base_url: str, timeout: Optional[int] = None, flush_at: Optional[int] = None, flush_interval: Optional[float] = None, @@ -94,9 +94,9 @@ def __init__( traces_export_path = os.environ.get(LANGFUSE_OTEL_TRACES_EXPORT_PATH, None) endpoint = ( - f"{host}/{traces_export_path}" + f"{base_url}/{traces_export_path}" if traces_export_path - else f"{host}/api/public/otel/v1/traces" + else f"{base_url}/api/public/otel/v1/traces" ) langfuse_span_exporter = OTLPSpanExporter( diff --git a/tests/test_additional_headers_simple.py b/tests/test_additional_headers_simple.py index 8a1d07134..1f4f836bf 100644 --- a/tests/test_additional_headers_simple.py +++ b/tests/test_additional_headers_simple.py @@ -143,7 +143,7 @@ def test_span_processor_has_additional_headers_in_otel_exporter(self): processor = LangfuseSpanProcessor( public_key="test-public-key", secret_key="test-secret-key", - host="https://mock-host.com", + base_url="https://mock-host.com", additional_headers=additional_headers, ) @@ -170,7 +170,7 @@ def test_span_processor_none_additional_headers_works(self): processor = LangfuseSpanProcessor( public_key="test-public-key", secret_key="test-secret-key", - host="https://mock-host.com", + base_url="https://mock-host.com", additional_headers=None, ) From f6f80c300637258d5885a27f46ea04b68530a750 Mon Sep 17 00:00:00 2001 From: Hassieb Pakzad <68423100+hassiebp@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:34:58 +0200 Subject: [PATCH 2/6] push --- .github/workflows/ci.yml | 2 +- tests/api_wrapper.py | 2 +- tests/test_deprecation.py | 5 +++-- tests/utils.py | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b8e0a83e..0cb4cb3d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 env: - LANGFUSE_HOST: "http://localhost:3000" + LANGFUSE_BASE_URL: "http://localhost:3000" LANGFUSE_PUBLIC_KEY: "pk-lf-1234567890" LANGFUSE_SECRET_KEY: "sk-lf-1234567890" OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} diff --git a/tests/api_wrapper.py b/tests/api_wrapper.py index 42f941550..6067e6bfa 100644 --- a/tests/api_wrapper.py +++ b/tests/api_wrapper.py @@ -9,7 +9,7 @@ def __init__(self, username=None, password=None, base_url=None): username = username if username else os.environ["LANGFUSE_PUBLIC_KEY"] password = password if password else os.environ["LANGFUSE_SECRET_KEY"] self.auth = (username, password) - self.BASE_URL = base_url if base_url else os.environ["LANGFUSE_HOST"] + self.BASE_URL = base_url if base_url else os.environ["LANGFUSE_BASE_URL"] def get_observation(self, observation_id): sleep(1) diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py index 9877f97d1..bcb2626b9 100644 --- a/tests/test_deprecation.py +++ b/tests/test_deprecation.py @@ -1,9 +1,10 @@ """Tests for deprecation warnings on deprecated functions.""" import warnings -import pytest from unittest.mock import patch +import pytest + from langfuse import Langfuse @@ -54,7 +55,7 @@ def langfuse_client(self): { "LANGFUSE_PUBLIC_KEY": "test_key", "LANGFUSE_SECRET_KEY": "test_secret", - "LANGFUSE_HOST": "http://localhost:3000", + "LANGFUSE_BASE_URL": "http://localhost:3000", }, ): return Langfuse() diff --git a/tests/utils.py b/tests/utils.py index 774c0c5c3..b6aeeb185 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -22,7 +22,7 @@ def get_api(): return FernLangfuse( username=os.environ.get("LANGFUSE_PUBLIC_KEY"), password=os.environ.get("LANGFUSE_SECRET_KEY"), - base_url=os.environ.get("LANGFUSE_HOST"), + base_url=os.environ.get("LANGFUSE_BASE_URL"), ) From 9c8e3bb52d6875d72cf225fd203058040e611065 Mon Sep 17 00:00:00 2001 From: Hassieb Pakzad <68423100+hassiebp@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:41:55 +0200 Subject: [PATCH 3/6] push --- langfuse/_client/observe.py | 28 ++++++--- langfuse/_client/span.py | 6 +- tests/test_core_sdk.py | 72 +++++++++++----------- tests/test_otel.py | 117 ++++++++++++++++++++++++------------ 4 files changed, 139 insertions(+), 84 deletions(-) diff --git a/langfuse/_client/observe.py b/langfuse/_client/observe.py index 4338641b7..c158c23b8 100644 --- a/langfuse/_client/observe.py +++ b/langfuse/_client/observe.py @@ -313,13 +313,17 @@ async def async_wrapper(*args: Tuple[Any], **kwargs: Dict[str, Any]) -> Any: ) # handle starlette.StreamingResponse - if type(result).__name__ == "StreamingResponse" and hasattr(result, "body_iterator"): + if type(result).__name__ == "StreamingResponse" and hasattr( + result, "body_iterator" + ): is_return_type_generator = True - result.body_iterator = self._wrap_async_generator_result( - langfuse_span_or_generation, - result.body_iterator, - transform_to_string, + result.body_iterator = ( + self._wrap_async_generator_result( + langfuse_span_or_generation, + result.body_iterator, + transform_to_string, + ) ) langfuse_span_or_generation.update(output=result) @@ -427,13 +431,17 @@ def sync_wrapper(*args: Any, **kwargs: Any) -> Any: ) # handle starlette.StreamingResponse - if type(result).__name__ == "StreamingResponse" and hasattr(result, "body_iterator"): + if type(result).__name__ == "StreamingResponse" and hasattr( + result, "body_iterator" + ): is_return_type_generator = True - result.body_iterator = self._wrap_async_generator_result( - langfuse_span_or_generation, - result.body_iterator, - transform_to_string, + result.body_iterator = ( + self._wrap_async_generator_result( + langfuse_span_or_generation, + result.body_iterator, + transform_to_string, + ) ) langfuse_span_or_generation.update(output=result) diff --git a/langfuse/_client/span.py b/langfuse/_client/span.py index c078d995d..866022a6e 100644 --- a/langfuse/_client/span.py +++ b/langfuse/_client/span.py @@ -190,7 +190,9 @@ def __init__( {k: v for k, v in attributes.items() if v is not None} ) # Set OTEL span status if level is ERROR - self._set_otel_span_status_if_error(level=level, status_message=status_message) + self._set_otel_span_status_if_error( + level=level, status_message=status_message + ) def end(self, *, end_time: Optional[int] = None) -> "LangfuseObservationWrapper": """End the span, marking it as completed. @@ -544,7 +546,7 @@ def _process_media_in_attribute( return data def _set_otel_span_status_if_error( - self, *, level: Optional[SpanLevel] = None, status_message: Optional[str] = None + self, *, level: Optional[SpanLevel] = None, status_message: Optional[str] = None ) -> None: """Set OpenTelemetry span status to ERROR if level is ERROR. diff --git a/tests/test_core_sdk.py b/tests/test_core_sdk.py index 26d11746c..81a874ae4 100644 --- a/tests/test_core_sdk.py +++ b/tests/test_core_sdk.py @@ -338,7 +338,7 @@ def test_create_update_current_trace(): user_id="test", metadata={"key": "value"}, public=True, - input="test_input" + input="test_input", ) # Get trace ID for later reference trace_id = span.trace_id @@ -347,7 +347,9 @@ def test_create_update_current_trace(): sleep(1) # Update trace properties using update_current_trace - langfuse.update_current_trace(metadata={"key2": "value2"}, public=False, version="1.0") + langfuse.update_current_trace( + metadata={"key2": "value2"}, public=False, version="1.0" + ) # Ensure data is sent to the API langfuse.flush() @@ -1957,9 +1959,9 @@ def test_start_as_current_observation_types(): expected_types = {obs_type.upper() for obs_type in observation_types} | { "SPAN" } # includes parent span - assert expected_types.issubset(found_types), ( - f"Missing types: {expected_types - found_types}" - ) + assert expected_types.issubset( + found_types + ), f"Missing types: {expected_types - found_types}" # Verify each specific observation exists for obs_type in observation_types: @@ -2003,25 +2005,25 @@ def test_that_generation_like_properties_are_actually_created(): ) as obs: # Verify the properties are accessible on the observation object if hasattr(obs, "model"): - assert obs.model == test_model, ( - f"{obs_type} should have model property" - ) + assert ( + obs.model == test_model + ), f"{obs_type} should have model property" if hasattr(obs, "completion_start_time"): - assert obs.completion_start_time == test_completion_start_time, ( - f"{obs_type} should have completion_start_time property" - ) + assert ( + obs.completion_start_time == test_completion_start_time + ), f"{obs_type} should have completion_start_time property" if hasattr(obs, "model_parameters"): - assert obs.model_parameters == test_model_parameters, ( - f"{obs_type} should have model_parameters property" - ) + assert ( + obs.model_parameters == test_model_parameters + ), f"{obs_type} should have model_parameters property" if hasattr(obs, "usage_details"): - assert obs.usage_details == test_usage_details, ( - f"{obs_type} should have usage_details property" - ) + assert ( + obs.usage_details == test_usage_details + ), f"{obs_type} should have usage_details property" if hasattr(obs, "cost_details"): - assert obs.cost_details == test_cost_details, ( - f"{obs_type} should have cost_details property" - ) + assert ( + obs.cost_details == test_cost_details + ), f"{obs_type} should have cost_details property" langfuse.flush() @@ -2035,28 +2037,28 @@ def test_that_generation_like_properties_are_actually_created(): for obs in trace.observations if obs.name == f"test-{obs_type}" and obs.type == obs_type.upper() ] - assert len(observations) == 1, ( - f"Expected one {obs_type.upper()} observation, but found {len(observations)}" - ) + assert ( + len(observations) == 1 + ), f"Expected one {obs_type.upper()} observation, but found {len(observations)}" obs = observations[0] assert obs.model == test_model, f"{obs_type} should have model property" - assert obs.model_parameters == test_model_parameters, ( - f"{obs_type} should have model_parameters property" - ) + assert ( + obs.model_parameters == test_model_parameters + ), f"{obs_type} should have model_parameters property" # usage_details assert hasattr(obs, "usage_details"), f"{obs_type} should have usage_details" - assert obs.usage_details == dict(test_usage_details, total=30), ( - f"{obs_type} should persist usage_details" - ) # API adds total + assert obs.usage_details == dict( + test_usage_details, total=30 + ), f"{obs_type} should persist usage_details" # API adds total - assert obs.cost_details == test_cost_details, ( - f"{obs_type} should persist cost_details" - ) + assert ( + obs.cost_details == test_cost_details + ), f"{obs_type} should persist cost_details" # completion_start_time, because of time skew not asserting time - assert obs.completion_start_time is not None, ( - f"{obs_type} should persist completion_start_time property" - ) + assert ( + obs.completion_start_time is not None + ), f"{obs_type} should persist completion_start_time property" diff --git a/tests/test_otel.py b/tests/test_otel.py index 623e866b5..be8ebafd4 100644 --- a/tests/test_otel.py +++ b/tests/test_otel.py @@ -110,7 +110,7 @@ def langfuse_client(self, monkeypatch, tracer_provider, mock_processor_init): client = Langfuse( public_key="test-public-key", secret_key="test-secret-key", - host="http://test-host", + base_url="http://test-host", tracing_enabled=True, ) @@ -134,7 +134,7 @@ def _create_client(**kwargs): client = Langfuse( public_key="test-public-key", secret_key="test-secret-key", - host="http://test-host", + base_url="http://test-host", tracing_enabled=True, **kwargs, ) @@ -950,13 +950,14 @@ def test_error_level_in_span_creation(self, langfuse_client, memory_exporter): span = langfuse_client.start_span( name="create-error-span", level="ERROR", - status_message="Initial error state" + status_message="Initial error state", ) span.end() # Get the raw OTEL spans to check the status raw_spans = [ - s for s in memory_exporter.get_finished_spans() + s + for s in memory_exporter.get_finished_spans() if s.name == "create-error-span" ] assert len(raw_spans) == 1, "Expected one span" @@ -964,6 +965,7 @@ def test_error_level_in_span_creation(self, langfuse_client, memory_exporter): # Verify OTEL span status was set to ERROR from opentelemetry.trace.status import StatusCode + assert raw_span.status.status_code == StatusCode.ERROR assert raw_span.status.description == "Initial error state" @@ -972,7 +974,10 @@ def test_error_level_in_span_creation(self, langfuse_client, memory_exporter): span_data = spans[0] attributes = span_data["attributes"] assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_LEVEL] == "ERROR" - assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_STATUS_MESSAGE] == "Initial error state" + assert ( + attributes[LangfuseOtelSpanAttributes.OBSERVATION_STATUS_MESSAGE] + == "Initial error state" + ) def test_error_level_in_span_update(self, langfuse_client, memory_exporter): """Test that OTEL span status is set to ERROR when updating spans to level='ERROR'.""" @@ -985,7 +990,8 @@ def test_error_level_in_span_update(self, langfuse_client, memory_exporter): # Get the raw OTEL spans to check the status raw_spans = [ - s for s in memory_exporter.get_finished_spans() + s + for s in memory_exporter.get_finished_spans() if s.name == "update-error-span" ] assert len(raw_spans) == 1, "Expected one span" @@ -993,6 +999,7 @@ def test_error_level_in_span_update(self, langfuse_client, memory_exporter): # Verify OTEL span status was set to ERROR from opentelemetry.trace.status import StatusCode + assert raw_span.status.status_code == StatusCode.ERROR assert raw_span.status.description == "Updated to error state" @@ -1001,7 +1008,10 @@ def test_error_level_in_span_update(self, langfuse_client, memory_exporter): span_data = spans[0] attributes = span_data["attributes"] assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_LEVEL] == "ERROR" - assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_STATUS_MESSAGE] == "Updated to error state" + assert ( + attributes[LangfuseOtelSpanAttributes.OBSERVATION_STATUS_MESSAGE] + == "Updated to error state" + ) def test_generation_error_level_in_creation(self, langfuse_client, memory_exporter): """Test that OTEL span status is set to ERROR when creating generations with level='ERROR'.""" @@ -1010,13 +1020,14 @@ def test_generation_error_level_in_creation(self, langfuse_client, memory_export name="create-error-generation", model="gpt-4", level="ERROR", - status_message="Generation failed during creation" + status_message="Generation failed during creation", ) generation.end() # Get the raw OTEL spans to check the status raw_spans = [ - s for s in memory_exporter.get_finished_spans() + s + for s in memory_exporter.get_finished_spans() if s.name == "create-error-generation" ] assert len(raw_spans) == 1, "Expected one span" @@ -1024,6 +1035,7 @@ def test_generation_error_level_in_creation(self, langfuse_client, memory_export # Verify OTEL span status was set to ERROR from opentelemetry.trace.status import StatusCode + assert raw_span.status.status_code == StatusCode.ERROR assert raw_span.status.description == "Generation failed during creation" @@ -1032,24 +1044,28 @@ def test_generation_error_level_in_creation(self, langfuse_client, memory_export span_data = spans[0] attributes = span_data["attributes"] assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_LEVEL] == "ERROR" - assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_STATUS_MESSAGE] == "Generation failed during creation" + assert ( + attributes[LangfuseOtelSpanAttributes.OBSERVATION_STATUS_MESSAGE] + == "Generation failed during creation" + ) def test_generation_error_level_in_update(self, langfuse_client, memory_exporter): """Test that OTEL span status is set to ERROR when updating generations to level='ERROR'.""" # Create a normal generation generation = langfuse_client.start_generation( - name="update-error-generation", - model="gpt-4", - level="INFO" + name="update-error-generation", model="gpt-4", level="INFO" ) # Update it to ERROR level - generation.update(level="ERROR", status_message="Generation failed during execution") + generation.update( + level="ERROR", status_message="Generation failed during execution" + ) generation.end() # Get the raw OTEL spans to check the status raw_spans = [ - s for s in memory_exporter.get_finished_spans() + s + for s in memory_exporter.get_finished_spans() if s.name == "update-error-generation" ] assert len(raw_spans) == 1, "Expected one span" @@ -1057,6 +1073,7 @@ def test_generation_error_level_in_update(self, langfuse_client, memory_exporter # Verify OTEL span status was set to ERROR from opentelemetry.trace.status import StatusCode + assert raw_span.status.status_code == StatusCode.ERROR assert raw_span.status.description == "Generation failed during execution" @@ -1065,9 +1082,14 @@ def test_generation_error_level_in_update(self, langfuse_client, memory_exporter span_data = spans[0] attributes = span_data["attributes"] assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_LEVEL] == "ERROR" - assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_STATUS_MESSAGE] == "Generation failed during execution" + assert ( + attributes[LangfuseOtelSpanAttributes.OBSERVATION_STATUS_MESSAGE] + == "Generation failed during execution" + ) - def test_non_error_levels_dont_set_otel_status(self, langfuse_client, memory_exporter): + def test_non_error_levels_dont_set_otel_status( + self, langfuse_client, memory_exporter + ): """Test that non-ERROR levels don't set OTEL span status to ERROR.""" # Test different non-error levels test_levels = ["INFO", "WARNING", "DEBUG", None] @@ -1084,16 +1106,18 @@ def test_non_error_levels_dont_set_otel_status(self, langfuse_client, memory_exp # Get the raw OTEL spans to check the status raw_spans = [ - s for s in memory_exporter.get_finished_spans() - if s.name == span_name + s for s in memory_exporter.get_finished_spans() if s.name == span_name ] assert len(raw_spans) == 1, f"Expected one span for {span_name}" raw_span = raw_spans[0] # Verify OTEL span status was NOT set to ERROR from opentelemetry.trace.status import StatusCode + # Default status should be UNSET, not ERROR - assert raw_span.status.status_code != StatusCode.ERROR, f"Level {level} should not set ERROR status" + assert ( + raw_span.status.status_code != StatusCode.ERROR + ), f"Level {level} should not set ERROR status" def test_multiple_error_updates(self, langfuse_client, memory_exporter): """Test that multiple ERROR level updates work correctly.""" @@ -1110,7 +1134,8 @@ def test_multiple_error_updates(self, langfuse_client, memory_exporter): # Get the raw OTEL spans to check the status raw_spans = [ - s for s in memory_exporter.get_finished_spans() + s + for s in memory_exporter.get_finished_spans() if s.name == "multi-error-span" ] assert len(raw_spans) == 1, "Expected one span" @@ -1118,6 +1143,7 @@ def test_multiple_error_updates(self, langfuse_client, memory_exporter): # Verify OTEL span status shows the last error message from opentelemetry.trace.status import StatusCode + assert raw_span.status.status_code == StatusCode.ERROR assert raw_span.status.description == "Second error" @@ -1129,7 +1155,8 @@ def test_error_without_status_message(self, langfuse_client, memory_exporter): # Get the raw OTEL spans to check the status raw_spans = [ - s for s in memory_exporter.get_finished_spans() + s + for s in memory_exporter.get_finished_spans() if s.name == "error-no-message-span" ] assert len(raw_spans) == 1, "Expected one span" @@ -1137,14 +1164,25 @@ def test_error_without_status_message(self, langfuse_client, memory_exporter): # Verify OTEL span status was set to ERROR even without description from opentelemetry.trace.status import StatusCode + assert raw_span.status.status_code == StatusCode.ERROR # Description should be None when no status_message provided assert raw_span.status.description is None - def test_different_observation_types_error_handling(self, langfuse_client, memory_exporter): + def test_different_observation_types_error_handling( + self, langfuse_client, memory_exporter + ): """Test that ERROR level setting works for different observation types.""" # Test different observation types - observation_types = ["agent", "tool", "chain", "retriever", "evaluator", "embedding", "guardrail"] + observation_types = [ + "agent", + "tool", + "chain", + "retriever", + "evaluator", + "embedding", + "guardrail", + ] # Create a parent span for child observations with langfuse_client.start_as_current_span(name="error-test-parent") as parent: @@ -1154,7 +1192,7 @@ def test_different_observation_types_error_handling(self, langfuse_client, memor name=f"error-{obs_type}", as_type=obs_type, level="ERROR", - status_message=f"{obs_type} failed" + status_message=f"{obs_type} failed", ) obs.end() @@ -1167,8 +1205,13 @@ def test_different_observation_types_error_handling(self, langfuse_client, memor raw_span = obs_spans[0] from opentelemetry.trace.status import StatusCode - assert raw_span.status.status_code == StatusCode.ERROR, f"{obs_type} should have ERROR status" - assert raw_span.status.description == f"{obs_type} failed", f"{obs_type} should have correct description" + + assert ( + raw_span.status.status_code == StatusCode.ERROR + ), f"{obs_type} should have ERROR status" + assert ( + raw_span.status.description == f"{obs_type} failed" + ), f"{obs_type} should have correct description" class TestAdvancedSpans(TestOTelBase): @@ -1333,7 +1376,7 @@ def test_sampling(self, monkeypatch, tracer_provider, mock_processor_init): client = Langfuse( public_key="test-public-key", secret_key="test-secret-key", - host="http://test-host", + base_url="http://test-host", tracing_enabled=True, sample_rate=0, # No sampling ) @@ -1383,7 +1426,7 @@ def test_disabled_tracing(self, monkeypatch, tracer_provider, mock_processor_ini client = Langfuse( public_key="test-public-key", secret_key="test-secret-key", - host="http://test-host", + base_url="http://test-host", tracing_enabled=False, ) @@ -1955,11 +1998,11 @@ def mock_initialize(self, **kwargs): # Initialize the two clients langfuse_project1 = Langfuse( - public_key=project1_key, secret_key="secret1", host="http://test-host" + public_key=project1_key, secret_key="secret1", base_url="http://test-host" ) langfuse_project2 = Langfuse( - public_key=project2_key, secret_key="secret2", host="http://test-host" + public_key=project2_key, secret_key="secret2", base_url="http://test-host" ) # Return the setup @@ -2313,7 +2356,7 @@ def mock_initialize(self, **kwargs): processor = LangfuseSpanProcessor( public_key=self.public_key, secret_key=self.secret_key, - host=self.host, + base_url=self.host, blocked_instrumentation_scopes=kwargs.get( "blocked_instrumentation_scopes" ), @@ -2358,7 +2401,7 @@ def test_blocked_instrumentation_scopes_export_filtering( Langfuse( public_key=instrumentation_filtering_setup["test_key"], secret_key="test-secret-key", - host="http://localhost:3000", + base_url="http://localhost:3000", blocked_instrumentation_scopes=["openai", "anthropic"], ) @@ -2423,7 +2466,7 @@ def test_no_blocked_scopes_allows_all_exports( Langfuse( public_key=instrumentation_filtering_setup["test_key"], secret_key="test-secret-key", - host="http://localhost:3000", + base_url="http://localhost:3000", blocked_instrumentation_scopes=[], ) @@ -2468,7 +2511,7 @@ def test_none_blocked_scopes_allows_all_exports( Langfuse( public_key=instrumentation_filtering_setup["test_key"], secret_key="test-secret-key", - host="http://localhost:3000", + base_url="http://localhost:3000", blocked_instrumentation_scopes=None, ) @@ -2506,7 +2549,7 @@ def test_blocking_langfuse_sdk_scope_export(self, instrumentation_filtering_setu Langfuse( public_key=instrumentation_filtering_setup["test_key"], secret_key="test-secret-key", - host="http://localhost:3000", + base_url="http://localhost:3000", blocked_instrumentation_scopes=["langfuse-sdk"], ) @@ -3147,7 +3190,7 @@ def langfuse_client(self, monkeypatch): client = Langfuse( public_key="test-public-key", secret_key="test-secret-key", - host="http://test-host", + base_url="http://test-host", ) return client From f677536261dd9996cd45eea599bec81eb1dd675e Mon Sep 17 00:00:00 2001 From: Hassieb Pakzad <68423100+hassiebp@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:50:19 +0200 Subject: [PATCH 4/6] push --- tests/test_initialization.py | 288 +++++++++++++++++++++++++++++++++++ 1 file changed, 288 insertions(+) create mode 100644 tests/test_initialization.py diff --git a/tests/test_initialization.py b/tests/test_initialization.py new file mode 100644 index 000000000..c7ba861f1 --- /dev/null +++ b/tests/test_initialization.py @@ -0,0 +1,288 @@ +"""Test suite for Langfuse client initialization with LANGFUSE_HOST and LANGFUSE_BASE_URL. + +This test suite verifies that both LANGFUSE_HOST (deprecated) and LANGFUSE_BASE_URL +environment variables work correctly for initializing the Langfuse client. +""" + +import os + +import pytest + +from langfuse import Langfuse + + +class TestClientInitialization: + """Tests for Langfuse client initialization with different URL configurations.""" + + @pytest.fixture + def cleanup_env_vars(self): + """Fixture to clean up environment variables before and after each test.""" + # Store original values + original_base_url = os.environ.get("LANGFUSE_BASE_URL") + original_host = os.environ.get("LANGFUSE_HOST") + original_public_key = os.environ.get("LANGFUSE_PUBLIC_KEY") + original_secret_key = os.environ.get("LANGFUSE_SECRET_KEY") + + # Remove them for the test + for key in ["LANGFUSE_BASE_URL", "LANGFUSE_HOST"]: + if key in os.environ: + del os.environ[key] + + yield + + # Restore original values + for key, value in [ + ("LANGFUSE_BASE_URL", original_base_url), + ("LANGFUSE_HOST", original_host), + ("LANGFUSE_PUBLIC_KEY", original_public_key), + ("LANGFUSE_SECRET_KEY", original_secret_key), + ]: + if value is not None: + os.environ[key] = value + elif key in os.environ: + del os.environ[key] + + def test_base_url_parameter_takes_precedence(self, cleanup_env_vars): + """Test that base_url parameter takes highest precedence.""" + os.environ["LANGFUSE_BASE_URL"] = "http://env-base-url.com" + os.environ["LANGFUSE_HOST"] = "http://env-host.com" + + client = Langfuse( + base_url="http://param-base-url.com", + host="http://param-host.com", + public_key="test_pk", + secret_key="test_sk", + ) + + assert client._base_url == "http://param-base-url.com" + + def test_env_base_url_takes_precedence_over_host_param(self, cleanup_env_vars): + """Test that LANGFUSE_BASE_URL env var takes precedence over host parameter.""" + os.environ["LANGFUSE_BASE_URL"] = "http://env-base-url.com" + + client = Langfuse( + host="http://param-host.com", + public_key="test_pk", + secret_key="test_sk", + ) + + assert client._base_url == "http://env-base-url.com" + + def test_host_parameter_fallback(self, cleanup_env_vars): + """Test that host parameter works as fallback when base_url is not set.""" + client = Langfuse( + host="http://param-host.com", + public_key="test_pk", + secret_key="test_sk", + ) + + assert client._base_url == "http://param-host.com" + + def test_env_host_fallback(self, cleanup_env_vars): + """Test that LANGFUSE_HOST env var works as fallback.""" + os.environ["LANGFUSE_HOST"] = "http://env-host.com" + + client = Langfuse( + public_key="test_pk", + secret_key="test_sk", + ) + + assert client._base_url == "http://env-host.com" + + def test_default_base_url(self, cleanup_env_vars): + """Test that default base_url is used when nothing is set.""" + client = Langfuse( + public_key="test_pk", + secret_key="test_sk", + ) + + assert client._base_url == "https://cloud.langfuse.com" + + def test_base_url_env_var(self, cleanup_env_vars): + """Test that LANGFUSE_BASE_URL environment variable is used correctly.""" + os.environ["LANGFUSE_BASE_URL"] = "http://test-base-url.com" + + client = Langfuse( + public_key="test_pk", + secret_key="test_sk", + ) + + assert client._base_url == "http://test-base-url.com" + + def test_host_env_var(self, cleanup_env_vars): + """Test that LANGFUSE_HOST environment variable is used correctly (deprecated).""" + os.environ["LANGFUSE_HOST"] = "http://test-host.com" + + client = Langfuse( + public_key="test_pk", + secret_key="test_sk", + ) + + assert client._base_url == "http://test-host.com" + + def test_base_url_parameter(self, cleanup_env_vars): + """Test that base_url parameter is used correctly.""" + client = Langfuse( + base_url="http://param-base-url.com", + public_key="test_pk", + secret_key="test_sk", + ) + + assert client._base_url == "http://param-base-url.com" + + def test_precedence_order_all_set(self, cleanup_env_vars): + """Test complete precedence order: base_url param > env > host param > env > default.""" + os.environ["LANGFUSE_BASE_URL"] = "http://env-base-url.com" + os.environ["LANGFUSE_HOST"] = "http://env-host.com" + + # Case 1: base_url parameter wins + client1 = Langfuse( + base_url="http://param-base-url.com", + host="http://param-host.com", + public_key="test_pk", + secret_key="test_sk", + ) + assert client1._base_url == "http://param-base-url.com" + + # Case 2: LANGFUSE_BASE_URL env var wins when base_url param not set + client2 = Langfuse( + host="http://param-host.com", + public_key="test_pk", + secret_key="test_sk", + ) + assert client2._base_url == "http://env-base-url.com" + + def test_precedence_without_base_url(self, cleanup_env_vars): + """Test precedence when base_url options are not set.""" + os.environ["LANGFUSE_HOST"] = "http://env-host.com" + + # Case 1: host parameter wins + client1 = Langfuse( + host="http://param-host.com", + public_key="test_pk", + secret_key="test_sk", + ) + assert client1._base_url == "http://param-host.com" + + # Case 2: LANGFUSE_HOST env var is used + client2 = Langfuse( + public_key="test_pk", + secret_key="test_sk", + ) + assert client2._base_url == "http://env-host.com" + + def test_url_used_in_api_client(self, cleanup_env_vars): + """Test that the resolved base_url is correctly passed to API clients.""" + test_url = "http://test-unique-api.com" + # Use a unique public key to avoid singleton conflicts + client = Langfuse( + base_url=test_url, + public_key=f"test_pk_{test_url}", + secret_key="test_sk", + ) + + # Check that the API client has the correct base_url + assert client.api._client_wrapper._base_url == test_url + assert client.async_api._client_wrapper._base_url == test_url + + def test_url_used_in_trace_url_generation(self, cleanup_env_vars): + """Test that the resolved base_url is stored correctly for trace URL generation.""" + test_url = "http://test-trace-api.com" + # Use a unique public key to avoid singleton conflicts + client = Langfuse( + base_url=test_url, + public_key=f"test_pk_{test_url}", + secret_key="test_sk", + ) + + # Verify that the base_url is stored correctly and will be used for URL generation + # We can't test the full URL generation without making network calls to get project_id + # but we can verify the base_url is correctly set + assert client._base_url == test_url + + def test_both_base_url_and_host_params(self, cleanup_env_vars): + """Test that base_url parameter takes precedence over host parameter.""" + client = Langfuse( + base_url="http://base-url.com", + host="http://host.com", + public_key="test_pk", + secret_key="test_sk", + ) + + assert client._base_url == "http://base-url.com" + + def test_both_env_vars_set(self, cleanup_env_vars): + """Test that LANGFUSE_BASE_URL takes precedence over LANGFUSE_HOST.""" + os.environ["LANGFUSE_BASE_URL"] = "http://base-url.com" + os.environ["LANGFUSE_HOST"] = "http://host.com" + + client = Langfuse( + public_key="test_pk", + secret_key="test_sk", + ) + + assert client._base_url == "http://base-url.com" + + def test_localhost_urls(self, cleanup_env_vars): + """Test that localhost URLs work correctly.""" + # Test with base_url + client1 = Langfuse( + base_url="http://localhost:3000", + public_key="test_pk", + secret_key="test_sk", + ) + assert client1._base_url == "http://localhost:3000" + + # Test with host (deprecated) + client2 = Langfuse( + host="http://localhost:3000", + public_key="test_pk", + secret_key="test_sk", + ) + assert client2._base_url == "http://localhost:3000" + + # Test with env var + os.environ["LANGFUSE_BASE_URL"] = "http://localhost:3000" + client3 = Langfuse( + public_key="test_pk", + secret_key="test_sk", + ) + assert client3._base_url == "http://localhost:3000" + + def test_trailing_slash_handling(self, cleanup_env_vars): + """Test that URLs with trailing slashes are handled correctly.""" + # URLs with trailing slashes should work + client1 = Langfuse( + base_url="http://test.com/", + public_key="test_pk", + secret_key="test_sk", + ) + # The SDK should accept the URL as-is (API client will handle normalization) + assert client1._base_url == "http://test.com/" + + def test_urls_with_paths(self, cleanup_env_vars): + """Test that URLs with paths work correctly.""" + client = Langfuse( + base_url="http://test.com/api/v1", + public_key="test_pk", + secret_key="test_sk", + ) + assert client._base_url == "http://test.com/api/v1" + + def test_https_and_http_urls(self, cleanup_env_vars): + """Test that both HTTPS and HTTP URLs work.""" + # HTTPS + client1 = Langfuse( + base_url="https://secure.com", + public_key="test_pk", + secret_key="test_sk", + ) + assert client1._base_url == "https://secure.com" + + # HTTP + client2 = Langfuse( + base_url="http://insecure.com", + public_key="test_pk", + secret_key="test_sk", + ) + assert client2._base_url == "http://insecure.com" From 56dcfe382303cec6aae473d1f9171025236a88b3 Mon Sep 17 00:00:00 2001 From: Hassieb Pakzad <68423100+hassiebp@users.noreply.github.com> Date: Wed, 22 Oct 2025 15:09:46 +0200 Subject: [PATCH 5/6] push --- tests/test_otel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_otel.py b/tests/test_otel.py index be8ebafd4..ca87691db 100644 --- a/tests/test_otel.py +++ b/tests/test_otel.py @@ -2356,7 +2356,7 @@ def mock_initialize(self, **kwargs): processor = LangfuseSpanProcessor( public_key=self.public_key, secret_key=self.secret_key, - base_url=self.host, + base_url=self.base_url, blocked_instrumentation_scopes=kwargs.get( "blocked_instrumentation_scopes" ), From fc5f2ec3dc2935a621ce2f854cc12bd9d822674b Mon Sep 17 00:00:00 2001 From: Hassieb Pakzad <68423100+hassiebp@users.noreply.github.com> Date: Wed, 22 Oct 2025 15:27:01 +0200 Subject: [PATCH 6/6] push --- tests/test_initialization.py | 43 ++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/tests/test_initialization.py b/tests/test_initialization.py index c7ba861f1..6664d318f 100644 --- a/tests/test_initialization.py +++ b/tests/test_initialization.py @@ -9,38 +9,49 @@ import pytest from langfuse import Langfuse +from langfuse._client.resource_manager import LangfuseResourceManager class TestClientInitialization: """Tests for Langfuse client initialization with different URL configurations.""" - @pytest.fixture + @pytest.fixture(autouse=True) def cleanup_env_vars(self): - """Fixture to clean up environment variables before and after each test.""" + """Fixture to clean up environment variables and singleton cache before and after each test.""" # Store original values - original_base_url = os.environ.get("LANGFUSE_BASE_URL") - original_host = os.environ.get("LANGFUSE_HOST") - original_public_key = os.environ.get("LANGFUSE_PUBLIC_KEY") - original_secret_key = os.environ.get("LANGFUSE_SECRET_KEY") - - # Remove them for the test + original_values = { + "LANGFUSE_BASE_URL": os.environ.get("LANGFUSE_BASE_URL"), + "LANGFUSE_HOST": os.environ.get("LANGFUSE_HOST"), + "LANGFUSE_PUBLIC_KEY": os.environ.get("LANGFUSE_PUBLIC_KEY"), + "LANGFUSE_SECRET_KEY": os.environ.get("LANGFUSE_SECRET_KEY"), + } + + # Remove LANGFUSE_BASE_URL and LANGFUSE_HOST for the test + # but keep PUBLIC_KEY and SECRET_KEY if they exist for key in ["LANGFUSE_BASE_URL", "LANGFUSE_HOST"]: if key in os.environ: del os.environ[key] yield - # Restore original values - for key, value in [ - ("LANGFUSE_BASE_URL", original_base_url), - ("LANGFUSE_HOST", original_host), - ("LANGFUSE_PUBLIC_KEY", original_public_key), - ("LANGFUSE_SECRET_KEY", original_secret_key), + # Clear the singleton cache to prevent test pollution + with LangfuseResourceManager._lock: + LangfuseResourceManager._instances.clear() + + # Restore original values - always remove any test values first + for key in [ + "LANGFUSE_BASE_URL", + "LANGFUSE_HOST", + "LANGFUSE_PUBLIC_KEY", + "LANGFUSE_SECRET_KEY", ]: + if key in os.environ: + del os.environ[key] + + # Then restore original values + for key, value in original_values.items(): if value is not None: os.environ[key] = value - elif key in os.environ: - del os.environ[key] def test_base_url_parameter_takes_precedence(self, cleanup_env_vars): """Test that base_url parameter takes highest precedence."""