From 57734fbce09b882915c4528de0b407267c911e2e Mon Sep 17 00:00:00 2001 From: Alan Marx Date: Thu, 23 Jul 2026 14:58:55 -0700 Subject: [PATCH 1/7] feat(config): add Config#valid? and Langfuse.configured? Adds a non-raising Config#valid? predicate that delegates to validate!, and a top-level Langfuse.configured? convenience method. Replaces the internal tracing_config_ready? helper with direct calls to configuration.valid? now that Config owns that logic. Co-Authored-By: Claude Sonnet 4.6 --- lib/langfuse.rb | 25 ++++++++++--------------- lib/langfuse/config.rb | 11 +++++++++++ spec/langfuse/config_spec.rb | 19 +++++++++++++++++++ spec/langfuse_spec.rb | 12 ++++++++++++ 4 files changed, 52 insertions(+), 15 deletions(-) diff --git a/lib/langfuse.rb b/lib/langfuse.rb index 5e5e28f..39f29b8 100644 --- a/lib/langfuse.rb +++ b/lib/langfuse.rb @@ -101,6 +101,13 @@ def configure configuration end + # Returns whether the global configuration is valid + # + # @return [Boolean] +true+ if the global configuration is valid, +false+ otherwise + def configured? + configuration.valid? + end + # Returns the global singleton client # # @return [Client] the global client instance @@ -121,10 +128,8 @@ def client # # OpenTelemetry.tracer_provider = Langfuse.tracer_provider def tracer_provider - unless tracing_config_ready? - raise ConfigurationError, - "Langfuse tracing is disabled until public_key, secret_key, and base_url are configured." - end + + raise ConfigurationError, "Langfuse tracing is disabled until the configuration is valid." unless configuration.valid? OtelSetup.setup(configuration) unless OtelSetup.initialized? OtelSetup.tracer_provider @@ -550,23 +555,13 @@ def wrap_otel_span(otel_span, type_str, otel_tracer, attributes: nil) # rubocop:disable Naming/PredicateMethod def setup_tracing_if_ready return true if OtelSetup.initialized? - return false unless tracing_config_ready? + return false unless configuration.valid? OtelSetup.setup(configuration) true end # rubocop:enable Naming/PredicateMethod - def tracing_config_ready? - configured?(configuration.public_key) && - configured?(configuration.secret_key) && - configured?(configuration.base_url) - end - - def configured?(value) - !value.nil? && !value.empty? - end - def warn_tracing_disabled_once return if @tracing_disabled_warning_emitted diff --git a/lib/langfuse/config.rb b/lib/langfuse/config.rb index 3d98c15..90dc6d8 100644 --- a/lib/langfuse/config.rb +++ b/lib/langfuse/config.rb @@ -200,6 +200,17 @@ def validate! end # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + # Check whether the configuration is valid without raising. + # + # @return [Boolean] +true+ if valid, +false+ otherwise + def valid? + validate! + + true + rescue Langfuse::ConfigurationError + false + end + # Normalize stale_ttl value # # Converts :indefinite to 1000 years in seconds for practical "never expire" diff --git a/spec/langfuse/config_spec.rb b/spec/langfuse/config_spec.rb index 40b89cf..0ff43ea 100644 --- a/spec/langfuse/config_spec.rb +++ b/spec/langfuse/config_spec.rb @@ -455,6 +455,25 @@ end end + describe "#valid?" do + let(:config) do + described_class.new do |c| + c.public_key = "pk_test" + c.secret_key = "sk_test" + end + end + + it "returns true when configuration is valid" do + expect(config.valid?).to be true + end + + it "returns false without raising when configuration is invalid" do + config.public_key = nil + + expect(config.valid?).to be false + end + end + describe "attribute setters" do let(:config) { described_class.new } diff --git a/spec/langfuse_spec.rb b/spec/langfuse_spec.rb index 423de02..56324b7 100644 --- a/spec/langfuse_spec.rb +++ b/spec/langfuse_spec.rb @@ -56,6 +56,18 @@ end end + describe ".configured?" do + it "returns true when configuration is valid" do + expect(described_class.configured?).to be true + end + + it "returns false when configuration is invalid" do + described_class.reset! + + expect(described_class.configured?).to be false + end + end + describe ".client" do before do described_class.configure do |config| From 060a0f3a324f3506e0a2bcc8538ae069ef326acc Mon Sep 17 00:00:00 2001 From: Alan Marx Date: Thu, 23 Jul 2026 14:59:24 -0700 Subject: [PATCH 2/7] chore(release): bump version to 0.11.0 Update CHANGELOG and CONFIGURATION docs for Config#valid? and Langfuse.configured?. Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 9 +++++++++ docs/CONFIGURATION.md | 17 +++++++++++++++++ lib/langfuse/version.rb | 2 +- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5a2a4f..7ac98cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.11.0] - 2026-07-23 + +### Added +- `Config#valid?` — non-raising predicate that returns `true`/`false` instead of raising `ConfigurationError` +- `Langfuse.configured?` — module-level convenience method delegating to `configuration.valid?` + +### Changed +- Internal tracing readiness check now uses `configuration.valid?` (full config validation) rather than checking only `public_key`, `secret_key`, and `base_url` + ## [0.10.1] - 2026-05-05 ### Changed diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 4d06e41..1b72bff 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -614,6 +614,23 @@ Langfuse.client # => Raises Langfuse::ConfigurationError: "public_key is required" ``` +To check validity without raising, use `Config#valid?` or the module-level `Langfuse.configured?`: + +```ruby +Langfuse.configured? # => false (before configure) + +Langfuse.configure do |config| + config.public_key = ENV["LANGFUSE_PUBLIC_KEY"] + config.secret_key = ENV["LANGFUSE_SECRET_KEY"] +end + +Langfuse.configured? # => true + +# Or directly on a config object: +config = Langfuse::Config.new +config.valid? # => false (no keys set) +``` + Validation rules: - `public_key` must be present diff --git a/lib/langfuse/version.rb b/lib/langfuse/version.rb index 0e2adc9..ba30d2e 100644 --- a/lib/langfuse/version.rb +++ b/lib/langfuse/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Langfuse - VERSION = "0.10.1" + VERSION = "0.11.0" end From da205cfbb6446e05ed9295b7cf9080ccb9f93862 Mon Sep 17 00:00:00 2001 From: Alan Marx Date: Thu, 23 Jul 2026 15:06:51 -0700 Subject: [PATCH 3/7] docs: document configured? and Config#valid? in API_REFERENCE and ERROR_HANDLING Co-Authored-By: Claude Sonnet 4.6 --- docs/API_REFERENCE.md | 27 ++++++++++++++++++++++++++- docs/ERROR_HANDLING.md | 6 ++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index b6a2107..24cd908 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -93,6 +93,31 @@ config = Langfuse.configuration puts config.cache_ttl # => 60 ``` +### `Langfuse.configured?` + +Returns whether the global configuration is valid without raising. + +**Signature:** + +```ruby +Langfuse.configured? # => Boolean +``` + +**Returns:** `true` if configuration passes full validation, `false` otherwise + +**Example:** + +```ruby +Langfuse.configured? # => false + +Langfuse.configure do |config| + config.public_key = ENV["LANGFUSE_PUBLIC_KEY"] + config.secret_key = ENV["LANGFUSE_SECRET_KEY"] +end + +Langfuse.configured? # => true +``` + ### `Langfuse.reset!` Reset configuration, caches, and client instance. Primarily for testing. @@ -121,7 +146,7 @@ Return Langfuse's internal tracer provider so you can explicitly install it as t Langfuse.tracer_provider # => OpenTelemetry::SDK::Trace::TracerProvider ``` -**Raises:** `ConfigurationError` if `public_key`, `secret_key`, or `base_url` are not configured +**Raises:** `ConfigurationError` if configuration is invalid `Langfuse.configure` does not call this for you. This is the explicit global-install seam. If you also want another OpenTelemetry backend or custom propagation, that remains application-owned setup. diff --git a/docs/ERROR_HANDLING.md b/docs/ERROR_HANDLING.md index 82130ad..9ae5943 100644 --- a/docs/ERROR_HANDLING.md +++ b/docs/ERROR_HANDLING.md @@ -34,6 +34,9 @@ Langfuse.configure do |config| # Oops, forgot to set keys end +Langfuse.configured? +# => false + Langfuse.client # => Langfuse::ConfigurationError: public_key is required ``` @@ -45,6 +48,9 @@ Langfuse.configure do |config| config.public_key = ENV['LANGFUSE_PUBLIC_KEY'] config.secret_key = ENV['LANGFUSE_SECRET_KEY'] end + +Langfuse.configured? +# => true ``` **Validation checklist:** From 26deb3a35cd68574be7a603e4acce4a1788d3b66 Mon Sep 17 00:00:00 2001 From: Alan Marx Date: Thu, 23 Jul 2026 15:22:38 -0700 Subject: [PATCH 4/7] fix: update stale warning message in warn_tracing_disabled_once Co-Authored-By: Claude Sonnet 4.6 --- lib/langfuse.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/langfuse.rb b/lib/langfuse.rb index 39f29b8..1b4f605 100644 --- a/lib/langfuse.rb +++ b/lib/langfuse.rb @@ -569,7 +569,7 @@ def warn_tracing_disabled_once return if @tracing_disabled_warning_emitted configuration.logger.warn( - "Langfuse tracing is disabled until public_key, secret_key, and base_url are configured." + "Langfuse tracing is disabled until the configuration is valid." ) @tracing_disabled_warning_emitted = true end From d887e031332e8621f0f8490b1aff4ac81e534c11 Mon Sep 17 00:00:00 2001 From: kadekillary Date: Mon, 27 Jul 2026 10:47:53 -0700 Subject: [PATCH 5/7] fix(tracing): decouple tracing readiness from full config validity Restore tracing-specific gating via Config#validate_tracing! so invalid cache/client settings can never disable tracing (parity with the Python and JS SDKs). Type-safe validators stop ArgumentError/NoMethodError leaking through Config#valid?, and batch_size/flush_interval are now validated before SpanProcessor consumes them. Reverts the in-PR version bump to keep the release flow with the release tooling. AAI-338 --- CHANGELOG.md | 10 +- docs/API_REFERENCE.md | 4 +- docs/CONFIGURATION.md | 17 +++- docs/ERROR_HANDLING.md | 6 +- lib/langfuse.rb | 28 +++--- lib/langfuse/config.rb | 83 +++++++++++++---- lib/langfuse/otel_setup.rb | 15 +-- lib/langfuse/version.rb | 2 +- spec/langfuse/config_spec.rb | 152 +++++++++++++++++++++++++++++++ spec/langfuse/otel_setup_spec.rb | 18 +++- spec/langfuse_spec.rb | 32 ++++++- 11 files changed, 308 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ac98cb..59ca3a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,14 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [0.11.0] - 2026-07-23 - ### Added -- `Config#valid?` — non-raising predicate that returns `true`/`false` instead of raising `ConfigurationError` -- `Langfuse.configured?` — module-level convenience method delegating to `configuration.valid?` +- `Config#valid?` and `Langfuse.configured?` — non-raising client-config validity predicates. Note: these reflect client configuration validity, not tracing readiness. ### Changed -- Internal tracing readiness check now uses `configuration.valid?` (full config validation) rather than checking only `public_key`, `secret_key`, and `base_url` +- Tracing readiness now also validates `batch_size` (positive Integer) and `flush_interval` (positive number); invalid values disable tracing with a one-time specific warning instead of crashing span-processor setup. `Langfuse.tracer_provider` errors and the tracing-disabled warning now name the specific invalid setting. + +### Fixed +- `Config#validate!` raises `ConfigurationError` instead of leaking `ArgumentError`/`NoMethodError` for non-numeric numeric fields and non-string credentials; string/bogus `cache_stale_ttl` values no longer silently pass validation. ## [0.10.1] - 2026-05-05 diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index 24cd908..a8f5556 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -118,6 +118,8 @@ end Langfuse.configured? # => true ``` +`configured?` reflects client configuration validity (credentials, cache, and client settings), not tracing readiness. Tracing settings (`batch_size`, `flush_interval`, `should_export_span`) are validated separately and lazily — if they are invalid, tracing degrades to no-op spans with a one-time warning instead of raising, and `configured?` can still return `true`. + ### `Langfuse.reset!` Reset configuration, caches, and client instance. Primarily for testing. @@ -146,7 +148,7 @@ Return Langfuse's internal tracer provider so you can explicitly install it as t Langfuse.tracer_provider # => OpenTelemetry::SDK::Trace::TracerProvider ``` -**Raises:** `ConfigurationError` if configuration is invalid +**Raises:** `ConfigurationError` for missing credentials, a non-callable `should_export_span`, or an invalid `batch_size`/`flush_interval`; the message names the offending setting `Langfuse.configure` does not call this for you. This is the explicit global-install seam. If you also want another OpenTelemetry backend or custom propagation, that remains application-owned setup. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 1b72bff..91349af 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -631,16 +631,29 @@ config = Langfuse::Config.new config.valid? # => false (no keys set) ``` +Note: `configured?` and `valid?` reflect client configuration validity, not tracing readiness. Tracing settings (`batch_size`, `flush_interval`, `should_export_span`) are validated separately when tracing is set up — invalid values disable tracing with a one-time warning, while `configured?` can still return `true`. + Validation rules: -- `public_key` must be present -- `secret_key` must be present +- `public_key` must be a non-empty String +- `secret_key` must be a non-empty String +- `base_url` must be a non-empty String +- `timeout`, `cache_max_size`, `cache_lock_timeout`, and `cache_refresh_threads` must be positive numbers +- `cache_ttl` must be a non-negative number +- `cache_stale_ttl` must be a non-negative number or `:indefinite` - `cache_backend` must be `:memory`, `:rails`, or `:auto` - If `:rails` is selected, or `:auto` resolves to `:rails`, Rails and `Rails.cache` must be available - `prompt_cache_observer` must respond to `#call` (if set) - `should_export_span` must respond to `#call` (if set) - `mask` must respond to `#call` (if set) +Tracing readiness (checked lazily when tracing starts, and by `Langfuse.tracer_provider`): + +- `public_key`, `secret_key`, and `base_url` must be non-empty Strings +- `should_export_span` must respond to `#call` (if set) +- `batch_size` must be a positive Integer +- `flush_interval` must be a positive number + ## Accessing Current Configuration ```ruby diff --git a/docs/ERROR_HANDLING.md b/docs/ERROR_HANDLING.md index 9ae5943..b7e1019 100644 --- a/docs/ERROR_HANDLING.md +++ b/docs/ERROR_HANDLING.md @@ -34,8 +34,8 @@ Langfuse.configure do |config| # Oops, forgot to set keys end -Langfuse.configured? -# => false +Langfuse.configured? +# => false Langfuse.client # => Langfuse::ConfigurationError: public_key is required @@ -53,6 +53,8 @@ Langfuse.configured? # => true ``` +Tracing misconfiguration never raises from `Langfuse.observe` or `Langfuse.start_observation` — invalid tracing settings (missing credentials, bad `batch_size`/`flush_interval`, non-callable `should_export_span`) degrade to no-op spans with a one-time warning naming the offending setting. Only `Langfuse.tracer_provider` and `Client.new` raise `ConfigurationError`. + **Validation checklist:** - `public_key` present and starts with `pk-lf-` - `secret_key` present and starts with `sk-lf-` diff --git a/lib/langfuse.rb b/lib/langfuse.rb index 1b4f605..a93e090 100644 --- a/lib/langfuse.rb +++ b/lib/langfuse.rb @@ -118,7 +118,8 @@ def client # Return Langfuse's internal tracer provider for explicit global OpenTelemetry installation. # # @return [OpenTelemetry::SDK::Trace::TracerProvider] - # @raise [ConfigurationError] if tracing is not fully configured + # @raise [ConfigurationError] if tracing configuration is invalid; the message + # names the offending tracing setting # # @example # Langfuse.configure do |config| @@ -128,11 +129,11 @@ def client # # OpenTelemetry.tracer_provider = Langfuse.tracer_provider def tracer_provider - - raise ConfigurationError, "Langfuse tracing is disabled until the configuration is valid." unless configuration.valid? - + configuration.validate_tracing! OtelSetup.setup(configuration) unless OtelSetup.initialized? OtelSetup.tracer_provider + rescue ConfigurationError => e + raise ConfigurationError, tracing_disabled_message(e.message) end # Shutdown Langfuse and flush any pending traces and scores @@ -514,7 +515,6 @@ def valid_observation_type?(type) def otel_tracer return tracer_provider.tracer(LANGFUSE_TRACER_NAME, Langfuse::VERSION) if setup_tracing_if_ready - warn_tracing_disabled_once noop_tracer end @@ -552,29 +552,33 @@ def wrap_otel_span(otel_span, type_str, otel_tracer, attributes: nil) observation_class.new(otel_span, otel_tracer, attributes: attributes) end - # rubocop:disable Naming/PredicateMethod def setup_tracing_if_ready return true if OtelSetup.initialized? - return false unless configuration.valid? + configuration.validate_tracing! OtelSetup.setup(configuration) true + rescue ConfigurationError => e + warn_tracing_disabled_once(e.message) + false end - # rubocop:enable Naming/PredicateMethod - def warn_tracing_disabled_once + def warn_tracing_disabled_once(detail) return if @tracing_disabled_warning_emitted tracing_warning_mutex.synchronize do return if @tracing_disabled_warning_emitted - configuration.logger.warn( - "Langfuse tracing is disabled until the configuration is valid." - ) + # logger is unvalidated and this path fires precisely when config is known-bad + configuration.logger&.warn(tracing_disabled_message(detail)) @tracing_disabled_warning_emitted = true end end + def tracing_disabled_message(detail) + "Langfuse tracing is disabled: #{detail}" + end + def tracing_warning_mutex @tracing_warning_mutex ||= Mutex.new end diff --git a/lib/langfuse/config.rb b/lib/langfuse/config.rb index 90dc6d8..aa2359f 100644 --- a/lib/langfuse/config.rb +++ b/lib/langfuse/config.rb @@ -176,29 +176,21 @@ def initialize # # @raise [ConfigurationError] if configuration is invalid # @return [void] - # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def validate! - raise ConfigurationError, "public_key is required" if public_key.nil? || public_key.empty? - raise ConfigurationError, "secret_key is required" if secret_key.nil? || secret_key.empty? - raise ConfigurationError, "base_url cannot be empty" if base_url.nil? || base_url.empty? - raise ConfigurationError, "timeout must be positive" if timeout.nil? || timeout <= 0 - raise ConfigurationError, "cache_ttl must be non-negative" if cache_ttl.nil? || cache_ttl.negative? - raise ConfigurationError, "cache_max_size must be positive" if cache_max_size.nil? || cache_max_size <= 0 - - if cache_lock_timeout.nil? || cache_lock_timeout <= 0 - raise ConfigurationError, - "cache_lock_timeout must be positive" - end - + validate_required_string!("public_key", public_key) + validate_required_string!("secret_key", secret_key) + validate_required_string!("base_url", base_url, message: "base_url cannot be empty") + validate_positive_number!("timeout", timeout) + validate_non_negative_number!("cache_ttl", cache_ttl) + validate_positive_number!("cache_max_size", cache_max_size) + validate_positive_number!("cache_lock_timeout", cache_lock_timeout) validate_swr_config! - validate_cache_backend! validate_prompt_cache_observer! validate_sample_rate! validate_should_export_span! validate_mask! end - # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # Check whether the configuration is valid without raising. # @@ -211,6 +203,24 @@ def valid? false end + # Validate only the settings tracing setup and export consume. + # + # Deliberately narrower than {#validate!}: invalid cache/client settings + # must never disable tracing (parity with the Python and JS SDKs), and + # tracing-only settings like batch_size never fail client validation. + # + # @api private + # @raise [ConfigurationError] with a message naming the offending tracing setting + # @return [void] + def validate_tracing! + validate_required_string!("public_key", public_key) + validate_required_string!("secret_key", secret_key) + validate_required_string!("base_url", base_url, message: "base_url cannot be empty") + validate_should_export_span! + validate_batch_size! + validate_flush_interval! + end + # Normalize stale_ttl value # # Converts :indefinite to 1000 years in seconds for practical "never expire" @@ -255,6 +265,40 @@ def initialize_tracing_defaults @mask = nil end + # Type checks precede comparisons so bad types raise ConfigurationError + # instead of leaking ArgumentError/NoMethodError through #valid?. + def validate_required_string!(name, value, message: "#{name} is required") + return if value.is_a?(String) && !value.empty? + + raise ConfigurationError, message + end + + def validate_positive_number!(name, value) + return if value.is_a?(Numeric) && value.positive? + + raise ConfigurationError, "#{name} must be positive" + end + + def validate_non_negative_number!(name, value) + return if value.is_a?(Numeric) && !value.negative? + + raise ConfigurationError, "#{name} must be non-negative" + end + + # SpanProcessor computes batch_size * 2 and flush_interval * 1000, so both + # must be type-checked before tracing setup to avoid crashes mid-export. + def validate_batch_size! + return if batch_size.is_a?(Integer) && batch_size.positive? + + raise ConfigurationError, "batch_size must be a positive Integer" + end + + def validate_flush_interval! + return if flush_interval.is_a?(Numeric) && flush_interval.positive? + + raise ConfigurationError, "flush_interval must be a positive number" + end + def validate_cache_backend! valid_backends = %i[memory rails auto] return if valid_backends.include?(cache_backend) @@ -288,17 +332,16 @@ def validate_swr_stale_ttl! "cache_stale_ttl must be non-negative or :indefinite" end - # Validate numeric values are non-negative - return unless cache_stale_ttl.is_a?(Integer) && cache_stale_ttl.negative? + # Accept :indefinite or any non-negative numeric value; reject everything else + return if cache_stale_ttl == :indefinite + return if cache_stale_ttl.is_a?(Numeric) && !cache_stale_ttl.negative? raise ConfigurationError, "cache_stale_ttl must be non-negative or :indefinite" end def validate_refresh_threads! - return unless cache_refresh_threads.nil? || cache_refresh_threads <= 0 - - raise ConfigurationError, "cache_refresh_threads must be positive" + validate_positive_number!("cache_refresh_threads", cache_refresh_threads) end def validate_sample_rate! diff --git a/lib/langfuse/otel_setup.rb b/lib/langfuse/otel_setup.rb index e4bfaa1..2e4344f 100644 --- a/lib/langfuse/otel_setup.rb +++ b/lib/langfuse/otel_setup.rb @@ -31,7 +31,7 @@ class << self # @param config [Langfuse::Config] The Langfuse configuration # @return [OpenTelemetry::SDK::Trace::TracerProvider] def setup(config) - validate_tracing_config!(config) + config.validate_tracing! return existing_provider_for(config) if initialized? candidate_provider = nil @@ -149,15 +149,6 @@ def log_initialized(config) config.logger.info("Langfuse tracing initialized with OpenTelemetry (#{mode} mode)") end - def validate_tracing_config!(config) - raise ConfigurationError, "public_key is required" if blank?(config.public_key) - raise ConfigurationError, "secret_key is required" if blank?(config.secret_key) - raise ConfigurationError, "base_url cannot be empty" if blank?(config.base_url) - return if config.should_export_span.nil? || config.should_export_span.respond_to?(:call) - - raise ConfigurationError, "should_export_span must respond to #call" - end - def tracing_config_snapshot(config) TRACING_CONFIG_FIELDS.to_h { |field| [field, config.public_send(field)] }.freeze end @@ -166,10 +157,6 @@ def setup_mutex @setup_mutex ||= Mutex.new end - def blank?(value) - value.nil? || value.empty? - end - def build_headers(public_key, secret_key) credentials = "#{public_key}:#{secret_key}" encoded = Base64.strict_encode64(credentials) diff --git a/lib/langfuse/version.rb b/lib/langfuse/version.rb index ba30d2e..0e2adc9 100644 --- a/lib/langfuse/version.rb +++ b/lib/langfuse/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Langfuse - VERSION = "0.11.0" + VERSION = "0.10.1" end diff --git a/spec/langfuse/config_spec.rb b/spec/langfuse/config_spec.rb index 0ff43ea..ca02623 100644 --- a/spec/langfuse/config_spec.rb +++ b/spec/langfuse/config_spec.rb @@ -209,6 +209,32 @@ "timeout must be positive" ) end + + it "raises ConfigurationError instead of ArgumentError when timeout is a String" do + config.timeout = "5" + expect { config.validate! }.to raise_error( + Langfuse::ConfigurationError, + "timeout must be positive" + ) + end + end + + context "when fields have invalid types" do + it "raises ConfigurationError when cache_ttl is a Symbol" do + config.cache_ttl = :sixty + expect { config.validate! }.to raise_error( + Langfuse::ConfigurationError, + "cache_ttl must be non-negative" + ) + end + + it "raises ConfigurationError when public_key is not a String" do + config.public_key = 123 + expect { config.validate! }.to raise_error( + Langfuse::ConfigurationError, + "public_key is required" + ) + end end context "when should_export_span is invalid" do @@ -386,6 +412,19 @@ config.cache_stale_ttl = :indefinite expect { config.validate! }.not_to raise_error end + + it "raises ConfigurationError when a String" do + config.cache_stale_ttl = "abc" + expect { config.validate! }.to raise_error( + Langfuse::ConfigurationError, + /cache_stale_ttl/ + ) + end + + it "allows non-negative Float values" do + config.cache_stale_ttl = 30.5 + expect { config.validate! }.not_to raise_error + end end context "when cache_refresh_threads is invalid" do @@ -472,6 +511,119 @@ expect(config.valid?).to be false end + + it "returns false without raising when timeout is a String" do + config.timeout = "5" + + expect(config.valid?).to be false + end + + it "returns false without raising when cache_refresh_threads is a Symbol" do + config.cache_refresh_threads = :many + + expect(config.valid?).to be false + end + + it "stays true when batch_size is invalid (tracing-only setting)" do + config.batch_size = nil + + expect(config.valid?).to be true + end + end + + describe "#validate_tracing!" do + let(:config) do + described_class.new do |c| + c.public_key = "pk_test" + c.secret_key = "sk_test" + end + end + + it "passes with valid credentials and default tracing settings" do + expect { config.validate_tracing! }.not_to raise_error + end + + it "raises when public_key is missing" do + config.public_key = nil + expect { config.validate_tracing! }.to raise_error( + Langfuse::ConfigurationError, + "public_key is required" + ) + end + + it "raises when secret_key is empty" do + config.secret_key = "" + expect { config.validate_tracing! }.to raise_error( + Langfuse::ConfigurationError, + "secret_key is required" + ) + end + + it "raises when base_url is empty" do + config.base_url = "" + expect { config.validate_tracing! }.to raise_error( + Langfuse::ConfigurationError, + "base_url cannot be empty" + ) + end + + it "raises when should_export_span is not callable" do + config.should_export_span = "not callable" + expect { config.validate_tracing! }.to raise_error( + Langfuse::ConfigurationError, + "should_export_span must respond to #call" + ) + end + + it "raises when batch_size is nil" do + config.batch_size = nil + expect { config.validate_tracing! }.to raise_error( + Langfuse::ConfigurationError, + "batch_size must be a positive Integer" + ) + end + + it "raises when batch_size is a String" do + config.batch_size = "50" + expect { config.validate_tracing! }.to raise_error( + Langfuse::ConfigurationError, + "batch_size must be a positive Integer" + ) + end + + it "raises when batch_size is zero" do + config.batch_size = 0 + expect { config.validate_tracing! }.to raise_error( + Langfuse::ConfigurationError, + "batch_size must be a positive Integer" + ) + end + + it "raises when flush_interval is nil" do + config.flush_interval = nil + expect { config.validate_tracing! }.to raise_error( + Langfuse::ConfigurationError, + "flush_interval must be a positive number" + ) + end + + it "raises when flush_interval is negative" do + config.flush_interval = -1 + expect { config.validate_tracing! }.to raise_error( + Langfuse::ConfigurationError, + "flush_interval must be a positive number" + ) + end + + it "does not raise for an invalid cache_backend (tracing is orthogonal)" do + config.cache_backend = :bogus + expect { config.validate_tracing! }.not_to raise_error + end + + it "does not raise for an invalid timeout (tracing is orthogonal)" do + config.timeout = "5" + expect { config.validate_tracing! }.not_to raise_error + end end describe "attribute setters" do diff --git a/spec/langfuse/otel_setup_spec.rb b/spec/langfuse/otel_setup_spec.rb index 7f76798..cef73bb 100644 --- a/spec/langfuse/otel_setup_spec.rb +++ b/spec/langfuse/otel_setup_spec.rb @@ -183,7 +183,23 @@ expect { Langfuse.tracer_provider }.to raise_error( Langfuse::ConfigurationError, - /Langfuse tracing is disabled/ + /Langfuse tracing is disabled: public_key is required/ + ) + end + + it "raises from Langfuse.tracer_provider when batch_size is invalid" do + Langfuse.reset! + Langfuse.configure do |c| + c.public_key = config.public_key + c.secret_key = config.secret_key + c.base_url = config.base_url + c.batch_size = nil + c.logger = logger + end + + expect { Langfuse.tracer_provider }.to raise_error( + Langfuse::ConfigurationError, + /batch_size/ ) end diff --git a/spec/langfuse_spec.rb b/spec/langfuse_spec.rb index 56324b7..2a6fb33 100644 --- a/spec/langfuse_spec.rb +++ b/spec/langfuse_spec.rb @@ -468,7 +468,7 @@ config.logger = logger end - expect(logger).to receive(:warn).once.with(/Langfuse tracing is disabled/) + expect(logger).to receive(:warn).once.with(/Langfuse tracing is disabled: public_key is required/) first = described_class.observe("first") second = described_class.observe("second") @@ -477,6 +477,36 @@ expect(second.otel_span.recording?).to be(false) end + it "warns once and uses a no-op tracer when batch_size is invalid" do + described_class.reset! + logger = instance_double(Logger, warn: nil) + described_class.configure do |config| + config.public_key = "pk_test" + config.secret_key = "sk_test" + config.batch_size = nil + config.logger = logger + end + + expect(logger).to receive(:warn).once.with(/batch_size must be a positive Integer/) + + observation = described_class.observe("test") + + expect(observation.otel_span.recording?).to be(false) + end + + it "keeps tracing enabled when only client config is invalid" do + described_class.reset! + described_class.configure do |config| + config.public_key = "pk_test" + config.secret_key = "sk_test" + config.cache_backend = :bogus + end + + observation = described_class.observe("x") + + expect(observation.otel_span.recording?).to be(true) + end + it "creates and returns observation without block" do observation = described_class.observe("test", input: { data: "test" }) expect(observation).to be_a(Langfuse::Span) From 9688bf120cfbc024e793b9a76bb7ee9a07a99ab9 Mon Sep 17 00:00:00 2001 From: kadekillary Date: Mon, 27 Jul 2026 11:00:23 -0700 Subject: [PATCH 6/7] refactor(config): extract shared connection-settings validation Deduplicate the public_key/secret_key/base_url trio shared by validate! and validate_tracing!, drop the redundant pre-validation in setup_tracing_if_ready (OtelSetup.setup already validates), and note the configured?-vs-tracing-readiness contract in YARD. --- lib/langfuse.rb | 4 +++- lib/langfuse/config.rb | 16 ++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/langfuse.rb b/lib/langfuse.rb index a93e090..6af6199 100644 --- a/lib/langfuse.rb +++ b/lib/langfuse.rb @@ -103,6 +103,8 @@ def configure # Returns whether the global configuration is valid # + # @note Reflects client configuration validity, not tracing readiness — + # tracing settings are validated separately and lazily by {.tracer_provider}. # @return [Boolean] +true+ if the global configuration is valid, +false+ otherwise def configured? configuration.valid? @@ -555,7 +557,7 @@ def wrap_otel_span(otel_span, type_str, otel_tracer, attributes: nil) def setup_tracing_if_ready return true if OtelSetup.initialized? - configuration.validate_tracing! + # OtelSetup.setup validates tracing config before touching provider state OtelSetup.setup(configuration) true rescue ConfigurationError => e diff --git a/lib/langfuse/config.rb b/lib/langfuse/config.rb index aa2359f..59d9455 100644 --- a/lib/langfuse/config.rb +++ b/lib/langfuse/config.rb @@ -177,9 +177,7 @@ def initialize # @raise [ConfigurationError] if configuration is invalid # @return [void] def validate! - validate_required_string!("public_key", public_key) - validate_required_string!("secret_key", secret_key) - validate_required_string!("base_url", base_url, message: "base_url cannot be empty") + validate_connection_settings! validate_positive_number!("timeout", timeout) validate_non_negative_number!("cache_ttl", cache_ttl) validate_positive_number!("cache_max_size", cache_max_size) @@ -213,9 +211,7 @@ def valid? # @raise [ConfigurationError] with a message naming the offending tracing setting # @return [void] def validate_tracing! - validate_required_string!("public_key", public_key) - validate_required_string!("secret_key", secret_key) - validate_required_string!("base_url", base_url, message: "base_url cannot be empty") + validate_connection_settings! validate_should_export_span! validate_batch_size! validate_flush_interval! @@ -265,6 +261,14 @@ def initialize_tracing_defaults @mask = nil end + # The keys and URL identifying the Langfuse backend — required by both + # client validation and tracing readiness. + def validate_connection_settings! + validate_required_string!("public_key", public_key) + validate_required_string!("secret_key", secret_key) + validate_required_string!("base_url", base_url, message: "base_url cannot be empty") + end + # Type checks precede comparisons so bad types raise ConfigurationError # instead of leaking ArgumentError/NoMethodError through #valid?. def validate_required_string!(name, value, message: "#{name} is required") From 15911b734613e759f767186b770fccd78ff2fb67 Mon Sep 17 00:00:00 2001 From: kadekillary Date: Mon, 27 Jul 2026 12:16:22 -0700 Subject: [PATCH 7/7] fix(tracing): tolerate nil logger across tracing setup paths OtelSetup's setup/reuse logging now uses safe navigation, matching the guard in warn_tracing_disabled_once, so a nil logger can never crash tracing initialization or its degradation warning. --- lib/langfuse/otel_setup.rb | 6 +++--- spec/langfuse/otel_setup_spec.rb | 6 ++++++ spec/langfuse_spec.rb | 12 ++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/langfuse/otel_setup.rb b/lib/langfuse/otel_setup.rb index 2e4344f..906bab1 100644 --- a/lib/langfuse/otel_setup.rb +++ b/lib/langfuse/otel_setup.rb @@ -85,9 +85,9 @@ def initialized? def existing_provider_for(config) snapshot = tracing_config_snapshot(config) if @config_snapshot == snapshot - config.logger.debug("Langfuse tracing already initialized; reusing existing tracer provider") + config.logger&.debug("Langfuse tracing already initialized; reusing existing tracer provider") else - config.logger.warn( + config.logger&.warn( "Langfuse tracing is already initialized. Changes to #{TRACING_CONFIG_FIELDS.join(', ')} " \ "require Langfuse.reset! before they take effect." ) @@ -146,7 +146,7 @@ def build_exporter(config) def log_initialized(config) mode = config.tracing_async ? "async" : "sync" - config.logger.info("Langfuse tracing initialized with OpenTelemetry (#{mode} mode)") + config.logger&.info("Langfuse tracing initialized with OpenTelemetry (#{mode} mode)") end def tracing_config_snapshot(config) diff --git a/spec/langfuse/otel_setup_spec.rb b/spec/langfuse/otel_setup_spec.rb index cef73bb..ea27396 100644 --- a/spec/langfuse/otel_setup_spec.rb +++ b/spec/langfuse/otel_setup_spec.rb @@ -79,6 +79,12 @@ expect(described_class.setup(config)).to equal(existing_provider) end + it "tolerates a nil logger during setup" do + config.logger = nil + + expect { described_class.setup(config) }.not_to raise_error + end + it "validates should_export_span in setup" do config.should_export_span = "bad" diff --git a/spec/langfuse_spec.rb b/spec/langfuse_spec.rb index 2a6fb33..c035bea 100644 --- a/spec/langfuse_spec.rb +++ b/spec/langfuse_spec.rb @@ -494,6 +494,18 @@ expect(observation.otel_span.recording?).to be(false) end + it "degrades to a no-op tracer without crashing when logger is nil" do + described_class.reset! + described_class.configure do |config| + config.public_key = nil + config.logger = nil + end + + observation = described_class.observe("test") + + expect(observation.otel_span.recording?).to be(false) + end + it "keeps tracing enabled when only client config is invalid" do described_class.reset! described_class.configure do |config|