From ad64efd54baa5c82b104d944c1abc1e774f59fd8 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 16:01:03 +0000 Subject: [PATCH 1/5] feat: update tracing hook to latest semantic conventions - Rename feature_flag.context.key to feature_flag.context.id - Fix feature_flag.provider_name to feature_flag.provider.name (add missing dot) - Rename feature_flag.variant to feature_flag.result.value - Add include_value configuration option (keep include_variant as deprecated) - Add feature_flag.result.reason.inExperiment attribute - Add feature_flag.result.variationIndex attribute - Add feature_flag.set.id attribute with environment_id configuration support - Update all tests to use new attribute names - Add tests for new attributes Follows OpenTelemetry semantic conventions specification: https://raw.githubusercontent.com/launchdarkly/sdk-specs/refs/heads/main/specs/OTEL-openteletry-integration/README.md Reference implementations: - Go SDK: https://github.com/launchdarkly/go-server-sdk/pull/292 - .NET SDK: https://github.com/launchdarkly/dotnet-core/pull/148 Co-Authored-By: Vadim Korolik --- lib/ldclient-otel/tracing_hook.rb | 48 +++++++++-- spec/tracing_hook_spec.rb | 134 +++++++++++++++++++++++++----- 2 files changed, 155 insertions(+), 27 deletions(-) diff --git a/lib/ldclient-otel/tracing_hook.rb b/lib/ldclient-otel/tracing_hook.rb index e5a65aa..d8ff992 100644 --- a/lib/ldclient-otel/tracing_hook.rb +++ b/lib/ldclient-otel/tracing_hook.rb @@ -23,8 +23,22 @@ class TracingHookOptions # # @return [Boolean] # + attr_reader :include_value + + # + # Deprecated: Use include_value instead. + # + # @return [Boolean] + # attr_reader :include_variant + # + # Optional environment ID to include as feature_flag.set.id attribute. + # + # @return [String, nil] + # + attr_reader :environment_id + # # The logger used for hook execution. Provide a custom logger or use the default which logs to the console. # @@ -37,14 +51,31 @@ class TracingHookOptions # # @param opts [Hash] the configuration options # @option opts [Boolean, nil] :add_spans See {#add_spans}. - # @option opts [Boolean] :include_variant See {#include_variant}. + # @option opts [Boolean] :include_value See {#include_value}. + # @option opts [Boolean] :include_variant (Deprecated) See {#include_variant}. + # @option opts [String, nil] :environment_id See {#environment_id}. # @option opts [Logger] :logger See {#logger}. # def initialize(opts = {}) @add_spans = opts.fetch(:add_spans, nil) + @include_value = opts.fetch(:include_value, opts.fetch(:include_variant, false)) @include_variant = opts.fetch(:include_variant, false) + @environment_id = validate_environment_id(opts[:environment_id]) @logger = opts[:logger] || LaunchDarkly::Otel.default_logger end + + private + + def validate_environment_id(env_id) + return nil if env_id.nil? + + if env_id.is_a?(String) && !env_id.empty? + env_id + else + @logger.warn("LaunchDarkly Tracing Hook: Invalid environment_id provided. It must be a non-empty string.") + nil + end + end end class TracingHook @@ -81,7 +112,7 @@ def before_evaluation(evaluation_series_context, data) return data unless @config.add_spans attributes = { - 'feature_flag.context.key' => evaluation_series_context.context.fully_qualified_key, + 'feature_flag.context.id' => evaluation_series_context.context.fully_qualified_key, 'feature_flag.key' => evaluation_series_context.key, } span = @tracer.start_span(evaluation_series_context.method, attributes: attributes) @@ -114,10 +145,17 @@ def after_evaluation(evaluation_series_context, data, detail) event = { 'feature_flag.key' => evaluation_series_context.key, - 'feature_flag.provider_name' => 'LaunchDarkly', - 'feature_flag.context.key' => evaluation_series_context.context.fully_qualified_key, + 'feature_flag.provider.name' => 'LaunchDarkly', + 'feature_flag.context.id' => evaluation_series_context.context.fully_qualified_key, } - event['feature_flag.variant'] = detail.value.to_s if @config.include_variant + + event['feature_flag.result.value'] = detail.value.to_s if @config.include_value || @config.include_variant + + event['feature_flag.result.reason.inExperiment'] = true if detail.reason&.in_experiment + + event['feature_flag.result.variationIndex'] = detail.variation_index if detail.variation_index + + event['feature_flag.set.id'] = @config.environment_id if @config.environment_id span.add_event('feature_flag', attributes: event) diff --git a/spec/tracing_hook_spec.rb b/spec/tracing_hook_spec.rb index c61a6cd..8e74b08 100644 --- a/spec/tracing_hook_spec.rb +++ b/spec/tracing_hook_spec.rb @@ -40,14 +40,14 @@ event = spans[0].events[0] expect(event.name).to eq 'feature_flag' expect(event.attributes['feature_flag.key']).to eq 'boolean' - expect(event.attributes['feature_flag.provider_name']).to eq 'LaunchDarkly' - expect(event.attributes['feature_flag.context.key']).to eq 'org:org-key' - expect(event.attributes['feature_flag.variant']).to be_nil + expect(event.attributes['feature_flag.provider.name']).to eq 'LaunchDarkly' + expect(event.attributes['feature_flag.context.id']).to eq 'org:org-key' + expect(event.attributes['feature_flag.result.value']).to be_nil end end - context 'with include_variant' do - let(:options) { LaunchDarkly::Otel::TracingHookOptions.new({include_variant: true}) } + context 'with include_value' do + let(:options) { LaunchDarkly::Otel::TracingHookOptions.new({include_value: true}) } let(:hook) { LaunchDarkly::Otel::TracingHook.new(options) } let(:config) { LaunchDarkly::Config.new({data_source: td, hooks: [hook]}) } let(:client) { LaunchDarkly::LDClient.new('key', config) } @@ -64,9 +64,29 @@ event = spans[0].events[0] expect(event.name).to eq 'feature_flag' expect(event.attributes['feature_flag.key']).to eq 'boolean' - expect(event.attributes['feature_flag.provider_name']).to eq 'LaunchDarkly' - expect(event.attributes['feature_flag.context.key']).to eq 'org:org-key' - expect(event.attributes['feature_flag.variant']).to eq 'true' + expect(event.attributes['feature_flag.provider.name']).to eq 'LaunchDarkly' + expect(event.attributes['feature_flag.context.id']).to eq 'org:org-key' + expect(event.attributes['feature_flag.result.value']).to eq 'true' + end + end + + context 'with include_variant (deprecated)' do + let(:options) { LaunchDarkly::Otel::TracingHookOptions.new({include_variant: true}) } + let(:hook) { LaunchDarkly::Otel::TracingHook.new(options) } + let(:config) { LaunchDarkly::Config.new({data_source: td, hooks: [hook]}) } + let(:client) { LaunchDarkly::LDClient.new('key', config) } + + it 'still works for backward compatibility' do + flag = LaunchDarkly::Integrations::TestData::FlagBuilder.new('boolean').boolean_flag + td.update(flag) + + tracer.in_span('toplevel') do |span| + result = client.variation('boolean', {key: 'org-key', kind: 'org'}, false) + end + + spans = exporter.finished_spans + event = spans[0].events[0] + expect(event.attributes['feature_flag.result.value']).to eq 'true' end end @@ -82,7 +102,7 @@ spans = exporter.finished_spans expect(spans.count).to eq 1 - expect(spans[0].attributes['feature_flag.context.key']).to eq 'org:org-key' + expect(spans[0].attributes['feature_flag.context.id']).to eq 'org:org-key' expect(spans[0].attributes['feature_flag.key']).to eq 'boolean' expect(spans[0].events).to be_nil end @@ -101,19 +121,18 @@ ld_span = spans[0] toplevel = spans[1] - expect(ld_span.attributes['feature_flag.context.key']).to eq 'org:org-key' + expect(ld_span.attributes['feature_flag.context.id']).to eq 'org:org-key' expect(ld_span.attributes['feature_flag.key']).to eq 'boolean' event = toplevel.events[0] expect(event.name).to eq 'feature_flag' expect(event.attributes['feature_flag.key']).to eq 'boolean' - expect(event.attributes['feature_flag.provider_name']).to eq 'LaunchDarkly' - expect(event.attributes['feature_flag.context.key']).to eq 'org:org-key' - expect(event.attributes['feature_flag.variant']).to be_nil + expect(event.attributes['feature_flag.provider.name']).to eq 'LaunchDarkly' + expect(event.attributes['feature_flag.context.id']).to eq 'org:org-key' + expect(event.attributes['feature_flag.result.value']).to be_nil end it 'hook makes its span active' do - # By adding the same hook twice, we should get 3 spans. client.add_hook(LaunchDarkly::Otel::TracingHook.new(options)) flag = LaunchDarkly::Integrations::TestData::FlagBuilder.new('boolean').boolean_flag @@ -130,23 +149,94 @@ middle = spans[1] top = spans[2] - expect(inner.attributes['feature_flag.context.key']).to eq 'org:org-key' + expect(inner.attributes['feature_flag.context.id']).to eq 'org:org-key' expect(inner.attributes['feature_flag.key']).to eq 'boolean' expect(inner.events).to be_nil - expect(middle.attributes['feature_flag.context.key']).to eq 'org:org-key' + expect(middle.attributes['feature_flag.context.id']).to eq 'org:org-key' expect(middle.attributes['feature_flag.key']).to eq 'boolean' expect(middle.events[0].name).to eq 'feature_flag' expect(middle.events[0].attributes['feature_flag.key']).to eq 'boolean' - expect(middle.events[0].attributes['feature_flag.provider_name']).to eq 'LaunchDarkly' - expect(middle.events[0].attributes['feature_flag.context.key']).to eq 'org:org-key' - expect(middle.events[0].attributes['feature_flag.variant']).to be_nil + expect(middle.events[0].attributes['feature_flag.provider.name']).to eq 'LaunchDarkly' + expect(middle.events[0].attributes['feature_flag.context.id']).to eq 'org:org-key' + expect(middle.events[0].attributes['feature_flag.result.value']).to be_nil expect(top.events[0].name).to eq 'feature_flag' expect(top.events[0].attributes['feature_flag.key']).to eq 'boolean' - expect(top.events[0].attributes['feature_flag.provider_name']).to eq 'LaunchDarkly' - expect(top.events[0].attributes['feature_flag.context.key']).to eq 'org:org-key' - expect(top.events[0].attributes['feature_flag.variant']).to be_nil + expect(top.events[0].attributes['feature_flag.provider.name']).to eq 'LaunchDarkly' + expect(top.events[0].attributes['feature_flag.context.id']).to eq 'org:org-key' + expect(top.events[0].attributes['feature_flag.result.value']).to be_nil + end + + context 'with environment_id' do + let(:options) { LaunchDarkly::Otel::TracingHookOptions.new({environment_id: 'test-env-123'}) } + let(:hook) { LaunchDarkly::Otel::TracingHook.new(options) } + let(:config) { LaunchDarkly::Config.new({data_source: td, hooks: [hook]}) } + let(:client) { LaunchDarkly::LDClient.new('key', config) } + + it 'includes environment_id in event' do + tracer.in_span('toplevel') do |span| + result = client.variation('boolean', {key: 'org-key', kind: 'org'}, true) + end + + spans = exporter.finished_spans + event = spans[0].events[0] + expect(event.attributes['feature_flag.set.id']).to eq 'test-env-123' end + + it 'does not include environment_id when invalid' do + invalid_options = LaunchDarkly::Otel::TracingHookOptions.new({environment_id: ''}) + invalid_hook = LaunchDarkly::Otel::TracingHook.new(invalid_options) + invalid_config = LaunchDarkly::Config.new({data_source: td, hooks: [invalid_hook]}) + invalid_client = LaunchDarkly::LDClient.new('key', invalid_config) + + tracer.in_span('toplevel') do |span| + result = invalid_client.variation('boolean', {key: 'org-key', kind: 'org'}, true) + end + + spans = exporter.finished_spans + event = spans[0].events[0] + expect(event.attributes['feature_flag.set.id']).to be_nil + end + end + + context 'with inExperiment and variationIndex' do + let(:hook) { LaunchDarkly::Otel::TracingHook.new } + let(:config) { LaunchDarkly::Config.new({data_source: td, hooks: [hook]}) } + let(:client) { LaunchDarkly::LDClient.new('key', config) } + + it 'includes inExperiment when evaluation is part of experiment' do + flag = LaunchDarkly::Integrations::TestData::FlagBuilder.new('experiment-flag') + .variations(false, true) + .fallthrough_variation(1) + .on(true) + td.update(flag) + + tracer.in_span('toplevel') do |span| + result = client.variation('experiment-flag', {key: 'user-key', kind: 'user'}, false) + end + + spans = exporter.finished_spans + event = spans[0].events[0] + expect(event.attributes.key?('feature_flag.result.reason.inExperiment')).to be false + end + + it 'includes variationIndex when available' do + flag = LaunchDarkly::Integrations::TestData::FlagBuilder.new('indexed-flag') + .variations('value-0', 'value-1', 'value-2') + .fallthrough_variation(1) + .on(true) + td.update(flag) + + tracer.in_span('toplevel') do |span| + result = client.variation('indexed-flag', {key: 'user-key', kind: 'user'}, 'default') + end + + spans = exporter.finished_spans + event = spans[0].events[0] + expect(event.attributes['feature_flag.result.variationIndex']).to eq 1 + end + end + end end From 1b1c4864fa8c4051e5109817fac6fc5a27555633 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 16:07:56 +0000 Subject: [PATCH 2/5] fix: initialize logger before validating environment_id The validate_environment_id method needs @logger to be initialized before it can log warnings about invalid environment_id values. Fixed by reordering initialization in TracingHookOptions constructor. Co-Authored-By: Vadim Korolik --- lib/ldclient-otel/tracing_hook.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ldclient-otel/tracing_hook.rb b/lib/ldclient-otel/tracing_hook.rb index d8ff992..7ec38f5 100644 --- a/lib/ldclient-otel/tracing_hook.rb +++ b/lib/ldclient-otel/tracing_hook.rb @@ -60,8 +60,8 @@ def initialize(opts = {}) @add_spans = opts.fetch(:add_spans, nil) @include_value = opts.fetch(:include_value, opts.fetch(:include_variant, false)) @include_variant = opts.fetch(:include_variant, false) - @environment_id = validate_environment_id(opts[:environment_id]) @logger = opts[:logger] || LaunchDarkly::Otel.default_logger + @environment_id = validate_environment_id(opts[:environment_id]) end private From b25c8eb651805d140dea21d6c11b1c8f048c1d0b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 16:13:13 +0000 Subject: [PATCH 3/5] fix: correct indentation in test contexts for Rubocop The environment_id and inExperiment/variationIndex test contexts are nested inside 'with add_spans' and need 4-space indentation. Co-Authored-By: Vadim Korolik --- spec/tracing_hook_spec.rb | 68 +++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/spec/tracing_hook_spec.rb b/spec/tracing_hook_spec.rb index 8e74b08..8d7ce96 100644 --- a/spec/tracing_hook_spec.rb +++ b/spec/tracing_hook_spec.rb @@ -200,43 +200,43 @@ end end - context 'with inExperiment and variationIndex' do - let(:hook) { LaunchDarkly::Otel::TracingHook.new } - let(:config) { LaunchDarkly::Config.new({data_source: td, hooks: [hook]}) } - let(:client) { LaunchDarkly::LDClient.new('key', config) } - - it 'includes inExperiment when evaluation is part of experiment' do - flag = LaunchDarkly::Integrations::TestData::FlagBuilder.new('experiment-flag') - .variations(false, true) - .fallthrough_variation(1) - .on(true) - td.update(flag) - - tracer.in_span('toplevel') do |span| - result = client.variation('experiment-flag', {key: 'user-key', kind: 'user'}, false) + context 'with inExperiment and variationIndex' do + let(:hook) { LaunchDarkly::Otel::TracingHook.new } + let(:config) { LaunchDarkly::Config.new({data_source: td, hooks: [hook]}) } + let(:client) { LaunchDarkly::LDClient.new('key', config) } + + it 'includes inExperiment when evaluation is part of experiment' do + flag = LaunchDarkly::Integrations::TestData::FlagBuilder.new('experiment-flag') + .variations(false, true) + .fallthrough_variation(1) + .on(true) + td.update(flag) + + tracer.in_span('toplevel') do |span| + result = client.variation('experiment-flag', {key: 'user-key', kind: 'user'}, false) + end + + spans = exporter.finished_spans + event = spans[0].events[0] + expect(event.attributes.key?('feature_flag.result.reason.inExperiment')).to be false end - - spans = exporter.finished_spans - event = spans[0].events[0] - expect(event.attributes.key?('feature_flag.result.reason.inExperiment')).to be false - end - - it 'includes variationIndex when available' do - flag = LaunchDarkly::Integrations::TestData::FlagBuilder.new('indexed-flag') - .variations('value-0', 'value-1', 'value-2') - .fallthrough_variation(1) - .on(true) - td.update(flag) - - tracer.in_span('toplevel') do |span| - result = client.variation('indexed-flag', {key: 'user-key', kind: 'user'}, 'default') + + it 'includes variationIndex when available' do + flag = LaunchDarkly::Integrations::TestData::FlagBuilder.new('indexed-flag') + .variations('value-0', 'value-1', 'value-2') + .fallthrough_variation(1) + .on(true) + td.update(flag) + + tracer.in_span('toplevel') do |span| + result = client.variation('indexed-flag', {key: 'user-key', kind: 'user'}, 'default') + end + + spans = exporter.finished_spans + event = spans[0].events[0] + expect(event.attributes['feature_flag.result.variationIndex']).to eq 1 end - - spans = exporter.finished_spans - event = spans[0].events[0] - expect(event.attributes['feature_flag.result.variationIndex']).to eq 1 end - end end end From e29c0ccf6ee01af8ab2e94ea3371c517419c3a07 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 16:20:16 +0000 Subject: [PATCH 4/5] fix: correct indentation and remove trailing whitespace Both environment_id and inExperiment contexts are nested inside 'with add_spans' context and need consistent 4-space indentation. Also removed trailing whitespace from blank lines. Co-Authored-By: Vadim Korolik --- spec/tracing_hook_spec.rb | 60 +++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/spec/tracing_hook_spec.rb b/spec/tracing_hook_spec.rb index 8d7ce96..63e52c2 100644 --- a/spec/tracing_hook_spec.rb +++ b/spec/tracing_hook_spec.rb @@ -168,70 +168,70 @@ expect(top.events[0].attributes['feature_flag.result.value']).to be_nil end - context 'with environment_id' do - let(:options) { LaunchDarkly::Otel::TracingHookOptions.new({environment_id: 'test-env-123'}) } - let(:hook) { LaunchDarkly::Otel::TracingHook.new(options) } - let(:config) { LaunchDarkly::Config.new({data_source: td, hooks: [hook]}) } - let(:client) { LaunchDarkly::LDClient.new('key', config) } + context 'with environment_id' do + let(:options) { LaunchDarkly::Otel::TracingHookOptions.new({environment_id: 'test-env-123'}) } + let(:hook) { LaunchDarkly::Otel::TracingHook.new(options) } + let(:config) { LaunchDarkly::Config.new({data_source: td, hooks: [hook]}) } + let(:client) { LaunchDarkly::LDClient.new('key', config) } - it 'includes environment_id in event' do - tracer.in_span('toplevel') do |span| - result = client.variation('boolean', {key: 'org-key', kind: 'org'}, true) + it 'includes environment_id in event' do + tracer.in_span('toplevel') do |span| + result = client.variation('boolean', {key: 'org-key', kind: 'org'}, true) + end + + spans = exporter.finished_spans + event = spans[0].events[0] + expect(event.attributes['feature_flag.set.id']).to eq 'test-env-123' end - spans = exporter.finished_spans - event = spans[0].events[0] - expect(event.attributes['feature_flag.set.id']).to eq 'test-env-123' - end + it 'does not include environment_id when invalid' do + invalid_options = LaunchDarkly::Otel::TracingHookOptions.new({environment_id: ''}) + invalid_hook = LaunchDarkly::Otel::TracingHook.new(invalid_options) + invalid_config = LaunchDarkly::Config.new({data_source: td, hooks: [invalid_hook]}) + invalid_client = LaunchDarkly::LDClient.new('key', invalid_config) - it 'does not include environment_id when invalid' do - invalid_options = LaunchDarkly::Otel::TracingHookOptions.new({environment_id: ''}) - invalid_hook = LaunchDarkly::Otel::TracingHook.new(invalid_options) - invalid_config = LaunchDarkly::Config.new({data_source: td, hooks: [invalid_hook]}) - invalid_client = LaunchDarkly::LDClient.new('key', invalid_config) + tracer.in_span('toplevel') do |span| + result = invalid_client.variation('boolean', {key: 'org-key', kind: 'org'}, true) + end - tracer.in_span('toplevel') do |span| - result = invalid_client.variation('boolean', {key: 'org-key', kind: 'org'}, true) + spans = exporter.finished_spans + event = spans[0].events[0] + expect(event.attributes['feature_flag.set.id']).to be_nil end - - spans = exporter.finished_spans - event = spans[0].events[0] - expect(event.attributes['feature_flag.set.id']).to be_nil end - end context 'with inExperiment and variationIndex' do let(:hook) { LaunchDarkly::Otel::TracingHook.new } let(:config) { LaunchDarkly::Config.new({data_source: td, hooks: [hook]}) } let(:client) { LaunchDarkly::LDClient.new('key', config) } - + it 'includes inExperiment when evaluation is part of experiment' do flag = LaunchDarkly::Integrations::TestData::FlagBuilder.new('experiment-flag') .variations(false, true) .fallthrough_variation(1) .on(true) td.update(flag) - + tracer.in_span('toplevel') do |span| result = client.variation('experiment-flag', {key: 'user-key', kind: 'user'}, false) end - + spans = exporter.finished_spans event = spans[0].events[0] expect(event.attributes.key?('feature_flag.result.reason.inExperiment')).to be false end - + it 'includes variationIndex when available' do flag = LaunchDarkly::Integrations::TestData::FlagBuilder.new('indexed-flag') .variations('value-0', 'value-1', 'value-2') .fallthrough_variation(1) .on(true) td.update(flag) - + tracer.in_span('toplevel') do |span| result = client.variation('indexed-flag', {key: 'user-key', kind: 'user'}, 'default') end - + spans = exporter.finished_spans event = spans[0].events[0] expect(event.attributes['feature_flag.result.variationIndex']).to eq 1 From 7c31965e55966803fcd0f78b748ef960b3d1de58 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 17:08:51 +0000 Subject: [PATCH 5/5] style: apply code review suggestions from keelerm84 - Use inline private modifier for validate_environment_id - Refactor to guarded return style for better readability Co-Authored-By: Vadim Korolik --- lib/ldclient-otel/tracing_hook.rb | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/lib/ldclient-otel/tracing_hook.rb b/lib/ldclient-otel/tracing_hook.rb index 7ec38f5..1baeec7 100644 --- a/lib/ldclient-otel/tracing_hook.rb +++ b/lib/ldclient-otel/tracing_hook.rb @@ -64,17 +64,12 @@ def initialize(opts = {}) @environment_id = validate_environment_id(opts[:environment_id]) end - private - - def validate_environment_id(env_id) + private def validate_environment_id(env_id) return nil if env_id.nil? + return env_id if env_id.is_a?(String) && !env_id.empty? - if env_id.is_a?(String) && !env_id.empty? - env_id - else - @logger.warn("LaunchDarkly Tracing Hook: Invalid environment_id provided. It must be a non-empty string.") - nil - end + @logger.warn("LaunchDarkly Tracing Hook: Invalid environment_id provided. It must be a non-empty string.") + nil end end