diff --git a/.github/workflows/ruby-plugin.yml b/.github/workflows/ruby-plugin.yml
index 01a3ec982e..4456ef5f56 100644
--- a/.github/workflows/ruby-plugin.yml
+++ b/.github/workflows/ruby-plugin.yml
@@ -96,3 +96,32 @@ jobs:
run: bundle install
- name: Test
run: bundle exec rake
+
+ e2e-rails-legacy:
+ # Regression guard for the CardFlight failure. The other e2e apps run on
+ # Rails 7.2, which is above the floor the OTel Rails-family instrumentations
+ # enforce, so they can never catch an old-Rails compatibility break. This
+ # app pins Rails 7.0 (on current Ruby, so bundler still resolves the latest
+ # instrumentation gems) and fails if the Rails-family auto-instrumentation
+ # stops attaching — i.e. if a future dependency bump re-breaks Rails 7.0.
+ # See e2e/ruby/rails/demo-rails70/README.md.
+ name: Rails 7.0 (legacy) E2E Tests
+ runs-on: ubuntu-22.04-8core-32gb
+ defaults:
+ run:
+ working-directory: ./e2e/ruby/rails/demo-rails70
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ - name: Install Ruby
+ uses: ruby/setup-ruby@3ff19f5e2baf30647122352b96108b1fbe250c64 # v1
+ with:
+ ruby-version: '3.3'
+ # See the note above: install non-frozen to absorb release-please version bumps.
+ bundler-cache: false
+ working-directory: ./e2e/ruby/rails/demo-rails70
+ - name: Install dependencies
+ run: bundle install
+ - name: Test
+ run: bundle exec rake
diff --git a/e2e/ruby/rails/api-only/Gemfile.lock b/e2e/ruby/rails/api-only/Gemfile.lock
index d940618d81..9993fc604f 100644
--- a/e2e/ruby/rails/api-only/Gemfile.lock
+++ b/e2e/ruby/rails/api-only/Gemfile.lock
@@ -1,11 +1,29 @@
PATH
remote: ../../../../sdk/@launchdarkly/observability-ruby
specs:
- launchdarkly-observability (0.2.0)
+ launchdarkly-observability (0.2.1)
launchdarkly-server-sdk (>= 8.11.0)
opentelemetry-exporter-otlp (~> 0.28)
opentelemetry-exporter-otlp-logs (~> 0.1)
- opentelemetry-instrumentation-all (~> 0.62)
+ opentelemetry-instrumentation-action_mailer (< 0.8)
+ opentelemetry-instrumentation-action_pack (< 0.18)
+ opentelemetry-instrumentation-action_view (< 0.13)
+ opentelemetry-instrumentation-active_job (< 0.12)
+ opentelemetry-instrumentation-active_record (< 0.13)
+ opentelemetry-instrumentation-active_storage (< 0.5)
+ opentelemetry-instrumentation-active_support (< 0.12)
+ opentelemetry-instrumentation-concurrent_ruby (>= 0.21)
+ opentelemetry-instrumentation-faraday (>= 0.24)
+ opentelemetry-instrumentation-graphql (>= 0.28)
+ opentelemetry-instrumentation-http (>= 0.23)
+ opentelemetry-instrumentation-mysql2 (>= 0.28)
+ opentelemetry-instrumentation-net_http (>= 0.22)
+ opentelemetry-instrumentation-pg (>= 0.29)
+ opentelemetry-instrumentation-rack (>= 0.24)
+ opentelemetry-instrumentation-rails (>= 0.34, < 0.42)
+ opentelemetry-instrumentation-redis (>= 0.25)
+ opentelemetry-instrumentation-sidekiq (>= 0.25)
+ opentelemetry-instrumentation-sinatra (>= 0.24)
opentelemetry-logs-sdk (~> 0.1)
opentelemetry-sdk (~> 1.4)
opentelemetry-semantic_conventions (~> 1.10)
@@ -222,104 +240,32 @@ GEM
opentelemetry-helpers-sql-processor (0.5.0)
opentelemetry-api (~> 1.0)
opentelemetry-common (~> 0.21)
- opentelemetry-instrumentation-action_mailer (0.8.0)
+ opentelemetry-instrumentation-action_mailer (0.7.0)
opentelemetry-instrumentation-active_support (~> 0.10)
- opentelemetry-instrumentation-action_pack (0.18.0)
+ opentelemetry-instrumentation-action_pack (0.17.0)
opentelemetry-instrumentation-rack (~> 0.29)
- opentelemetry-instrumentation-action_view (0.13.0)
+ opentelemetry-instrumentation-action_view (0.12.0)
opentelemetry-instrumentation-active_support (~> 0.10)
- opentelemetry-instrumentation-active_job (0.12.0)
+ opentelemetry-instrumentation-active_job (0.11.0)
opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-active_model_serializers (0.25.0)
- opentelemetry-instrumentation-active_support (>= 0.7.0)
- opentelemetry-instrumentation-active_record (0.13.0)
+ opentelemetry-instrumentation-active_record (0.12.0)
opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-active_storage (0.5.0)
+ opentelemetry-instrumentation-active_storage (0.4.0)
opentelemetry-instrumentation-active_support (~> 0.10)
- opentelemetry-instrumentation-active_support (0.12.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-all (0.93.0)
- opentelemetry-instrumentation-active_model_serializers (~> 0.25.0)
- opentelemetry-instrumentation-anthropic (~> 0.5.0)
- opentelemetry-instrumentation-aws_lambda (~> 0.7.0)
- opentelemetry-instrumentation-aws_sdk (~> 0.12.0)
- opentelemetry-instrumentation-bunny (~> 0.25.0)
- opentelemetry-instrumentation-concurrent_ruby (~> 0.25.0)
- opentelemetry-instrumentation-dalli (~> 0.30.0)
- opentelemetry-instrumentation-delayed_job (~> 0.26.0)
- opentelemetry-instrumentation-ethon (~> 0.29.0)
- opentelemetry-instrumentation-excon (~> 0.29.0)
- opentelemetry-instrumentation-faraday (~> 0.33.0)
- opentelemetry-instrumentation-grape (~> 0.7.0)
- opentelemetry-instrumentation-graphql (~> 0.32.0)
- opentelemetry-instrumentation-grpc (~> 0.5.0)
- opentelemetry-instrumentation-gruf (~> 0.6.0)
- opentelemetry-instrumentation-http (~> 0.30.0)
- opentelemetry-instrumentation-http_client (~> 0.29.0)
- opentelemetry-instrumentation-httpx (~> 0.8.0)
- opentelemetry-instrumentation-koala (~> 0.24.0)
- opentelemetry-instrumentation-lmdb (~> 0.26.0)
- opentelemetry-instrumentation-mongo (~> 0.26.0)
- opentelemetry-instrumentation-mysql2 (~> 0.34.0)
- opentelemetry-instrumentation-net_http (~> 0.29.0)
- opentelemetry-instrumentation-pg (~> 0.36.0)
- opentelemetry-instrumentation-que (~> 0.13.0)
- opentelemetry-instrumentation-racecar (~> 0.7.0)
- opentelemetry-instrumentation-rack (~> 0.31.0)
- opentelemetry-instrumentation-rails (~> 0.42.0)
- opentelemetry-instrumentation-rake (~> 0.6.0)
- opentelemetry-instrumentation-rdkafka (~> 0.10.0)
- opentelemetry-instrumentation-redis (~> 0.29.0)
- opentelemetry-instrumentation-resque (~> 0.9.0)
- opentelemetry-instrumentation-restclient (~> 0.28.0)
- opentelemetry-instrumentation-ruby_kafka (~> 0.25.0)
- opentelemetry-instrumentation-sidekiq (~> 0.29.0)
- opentelemetry-instrumentation-sinatra (~> 0.30.0)
- opentelemetry-instrumentation-trilogy (~> 0.68.0)
- opentelemetry-instrumentation-anthropic (0.5.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-aws_lambda (0.7.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-aws_sdk (0.12.0)
+ opentelemetry-instrumentation-active_support (0.11.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-base (0.26.0)
opentelemetry-api (~> 1.7)
opentelemetry-common (~> 0.21)
opentelemetry-registry (~> 0.1)
- opentelemetry-instrumentation-bunny (0.25.0)
- opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-concurrent_ruby (0.25.0)
opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-dalli (0.30.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-delayed_job (0.26.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-ethon (0.29.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-excon (0.29.1)
- opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-faraday (0.33.0)
opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-grape (0.7.0)
- opentelemetry-instrumentation-rack (~> 0.29)
opentelemetry-instrumentation-graphql (0.32.0)
opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-grpc (0.5.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-gruf (0.6.0)
- opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-http (0.30.0)
opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-http_client (0.29.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-httpx (0.8.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-koala (0.24.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-lmdb (0.26.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-mongo (0.26.0)
- opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-mysql2 (0.34.0)
opentelemetry-helpers-mysql
opentelemetry-helpers-sql
@@ -331,13 +277,9 @@ GEM
opentelemetry-helpers-sql
opentelemetry-helpers-sql-processor
opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-que (0.13.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-racecar (0.7.0)
- opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-rack (0.31.0)
opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-rails (0.42.0)
+ opentelemetry-instrumentation-rails (0.41.0)
opentelemetry-instrumentation-action_mailer (~> 0.7)
opentelemetry-instrumentation-action_pack (~> 0.17)
opentelemetry-instrumentation-action_view (~> 0.12)
@@ -346,28 +288,12 @@ GEM
opentelemetry-instrumentation-active_storage (~> 0.4)
opentelemetry-instrumentation-active_support (~> 0.11)
opentelemetry-instrumentation-concurrent_ruby (~> 0.25)
- opentelemetry-instrumentation-rake (0.6.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-rdkafka (0.10.0)
- opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-redis (0.29.0)
opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-resque (0.9.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-restclient (0.28.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-ruby_kafka (0.25.0)
- opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-sidekiq (0.29.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-sinatra (0.30.0)
opentelemetry-instrumentation-rack (~> 0.29)
- opentelemetry-instrumentation-trilogy (0.68.0)
- opentelemetry-helpers-mysql
- opentelemetry-helpers-sql
- opentelemetry-helpers-sql-processor
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-semantic_conventions (>= 1.8.0)
opentelemetry-logs-api (0.4.0)
opentelemetry-api (~> 1.0)
opentelemetry-logs-sdk (0.6.0)
diff --git a/e2e/ruby/rails/demo-rails70/.gitattributes b/e2e/ruby/rails/demo-rails70/.gitattributes
new file mode 100644
index 0000000000..31eeee0b6a
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/.gitattributes
@@ -0,0 +1,7 @@
+# See https://git-scm.com/docs/gitattributes for more about git attribute files.
+
+# Mark the database schema as having been generated.
+db/schema.rb linguist-generated
+
+# Mark any vendored files as having been vendored.
+vendor/* linguist-vendored
diff --git a/e2e/ruby/rails/demo-rails70/.gitignore b/e2e/ruby/rails/demo-rails70/.gitignore
new file mode 100644
index 0000000000..886f714b42
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/.gitignore
@@ -0,0 +1,35 @@
+# See https://help.github.com/articles/ignoring-files for more about ignoring files.
+#
+# If you find yourself ignoring temporary files generated by your text editor
+# or operating system, you probably want to add a global ignore instead:
+# git config --global core.excludesfile '~/.gitignore_global'
+
+# Ignore bundler config.
+/.bundle
+
+# Ignore the default SQLite database.
+/db/*.sqlite3
+/db/*.sqlite3-*
+
+# Ignore all logfiles and tempfiles.
+/log/*
+/tmp/*
+!/log/.keep
+!/tmp/.keep
+
+# Ignore pidfiles, but keep the directory.
+/tmp/pids/*
+!/tmp/pids/
+!/tmp/pids/.keep
+
+# Ignore uploaded files in development.
+/storage/*
+!/storage/.keep
+/tmp/storage/*
+!/tmp/storage/
+!/tmp/storage/.keep
+
+/public/assets
+
+# Ignore master key for decrypting credentials and more.
+/config/master.key
diff --git a/e2e/ruby/rails/demo-rails70/.rubocop.yml b/e2e/ruby/rails/demo-rails70/.rubocop.yml
new file mode 100644
index 0000000000..c3ba33221b
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/.rubocop.yml
@@ -0,0 +1,37 @@
+inherit_from: '../../../../sdk/highlight-ruby/highlight/.rubocop.yml'
+
+AllCops:
+ SuggestExtensions: false
+ TargetRubyVersion: 3.0
+ Exclude:
+ - 'db/**/*'
+ - 'spec/**/*'
+ - 'test/**/*'
+ - 'config/**/*'
+ - 'bin/**/*'
+
+Style/FrozenStringLiteralComment:
+ Enabled: false
+
+Style/Documentation:
+ Enabled: false
+
+Metrics/MethodLength:
+ Enabled: false
+
+Naming/MethodParameterName:
+ Enabled: false
+
+Metrics/AbcSize:
+ Enabled: false
+
+Metrics/ClassLength:
+ Enabled: false
+
+Layout/LineLength:
+ Exclude:
+ - 'bin/bundle'
+
+Style/IfUnlessModifier:
+ Exclude:
+ - 'bin/bundle'
diff --git a/e2e/ruby/rails/demo-rails70/.ruby-version b/e2e/ruby/rails/demo-rails70/.ruby-version
new file mode 100644
index 0000000000..a0891f563f
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/.ruby-version
@@ -0,0 +1 @@
+3.3.4
diff --git a/e2e/ruby/rails/demo-rails70/Gemfile b/e2e/ruby/rails/demo-rails70/Gemfile
new file mode 100644
index 0000000000..0ba87cc288
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/Gemfile
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+source 'https://rubygems.org'
+git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
+ruby '~> 3.3.4'
+
+# Rails 7.0 on purpose: this app reproduces the CardFlight failure where the
+# OTel Rails-family instrumentations (opentelemetry-instrumentation-rails >= 0.35)
+# raised their floor to Rails 7.1, so on Rails 7.0 they fail their runtime
+# `compatible?` check and never attach. Ruby stays at 3.3.4 (>= 3.2) so bundler
+# resolves the LATEST opentelemetry-instrumentation-all — the same versions a
+# real Rails 7.0 customer on a current Ruby gets. (On Ruby < 3.2 bundler would
+# self-heal to an older instrumentation-all and the bug would not reproduce.)
+gem 'rails', '~> 7.0.0'
+
+# Rails 7.0's test runner (railties test_unit line filtering) is incompatible
+# with minitest 6.x (the `run` arity changed). Pin to 5.x so the suite runs.
+# Unrelated to the instrumentation bug — just a Rails-7.0 test-toolchain pin.
+gem 'minitest', '~> 5.0'
+
+# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]
+gem 'sprockets-rails'
+
+# Use sqlite3 as the database for Active Record
+gem 'sqlite3', '~> 1.4'
+
+# Use the Puma web server [https://github.com/puma/puma]
+gem 'puma', '~> 6.0'
+
+# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
+gem 'importmap-rails'
+
+# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
+gem 'turbo-rails'
+
+# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
+gem 'stimulus-rails'
+
+# Build JSON APIs with ease [https://github.com/rails/jbuilder]
+gem 'jbuilder'
+
+# Use Redis adapter to run Action Cable in production
+gem 'redis', '~> 4.0'
+
+# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis]
+# gem "kredis"
+
+# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
+# gem "bcrypt", "~> 3.1.7"
+
+# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
+gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
+
+# Reduces boot times through caching; required in config/boot.rb
+gem 'bootsnap', require: false
+
+# Use Sass to process CSS
+# gem "sassc-rails"
+
+# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
+# gem "image_processing", "~> 1.2"
+
+group :development, :test do
+ # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
+ gem 'debug', platforms: %i[mri mingw x64_mingw]
+end
+
+group :development do
+ # Use console on exceptions pages [https://github.com/rails/web-console]
+ gem 'web-console'
+
+ # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler]
+ # gem "rack-mini-profiler"
+
+ # Speed up commands on slow machines / big apps [https://github.com/rails/spring]
+ # gem "spring"
+end
+
+group :test do
+ # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
+ gem 'capybara'
+ gem 'selenium-webdriver'
+
+ # In-process OTLP sink (test/support/otlp_sink.rb) uses WEBrick, which is no
+ # longer a default gem since Ruby 3.0, so it must be declared to be available
+ # under `bundle exec`.
+ gem 'webrick'
+end
+
+# LaunchDarkly SDK and Observability Plugin
+gem 'launchdarkly-server-sdk', '~> 8.0'
+gem 'launchdarkly-observability', path: '../../../../sdk/@launchdarkly/observability-ruby'
diff --git a/e2e/ruby/rails/demo-rails70/Gemfile.lock b/e2e/ruby/rails/demo-rails70/Gemfile.lock
new file mode 100644
index 0000000000..3b1ce4551d
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/Gemfile.lock
@@ -0,0 +1,477 @@
+PATH
+ remote: ../../../../sdk/@launchdarkly/observability-ruby
+ specs:
+ launchdarkly-observability (0.2.1)
+ launchdarkly-server-sdk (>= 8.11.0)
+ opentelemetry-exporter-otlp (~> 0.28)
+ opentelemetry-exporter-otlp-logs (~> 0.1)
+ opentelemetry-instrumentation-action_mailer (< 0.8)
+ opentelemetry-instrumentation-action_pack (< 0.18)
+ opentelemetry-instrumentation-action_view (< 0.13)
+ opentelemetry-instrumentation-active_job (< 0.12)
+ opentelemetry-instrumentation-active_record (< 0.13)
+ opentelemetry-instrumentation-active_storage (< 0.5)
+ opentelemetry-instrumentation-active_support (< 0.12)
+ opentelemetry-instrumentation-concurrent_ruby (>= 0.21)
+ opentelemetry-instrumentation-faraday (>= 0.24)
+ opentelemetry-instrumentation-graphql (>= 0.28)
+ opentelemetry-instrumentation-http (>= 0.23)
+ opentelemetry-instrumentation-mysql2 (>= 0.28)
+ opentelemetry-instrumentation-net_http (>= 0.22)
+ opentelemetry-instrumentation-pg (>= 0.29)
+ opentelemetry-instrumentation-rack (>= 0.24)
+ opentelemetry-instrumentation-rails (>= 0.34, < 0.42)
+ opentelemetry-instrumentation-redis (>= 0.25)
+ opentelemetry-instrumentation-sidekiq (>= 0.25)
+ opentelemetry-instrumentation-sinatra (>= 0.24)
+ opentelemetry-logs-sdk (~> 0.1)
+ opentelemetry-sdk (~> 1.4)
+ opentelemetry-semantic_conventions (~> 1.10)
+
+GEM
+ remote: https://rubygems.org/
+ specs:
+ actioncable (7.0.10)
+ actionpack (= 7.0.10)
+ activesupport (= 7.0.10)
+ nio4r (~> 2.0)
+ websocket-driver (>= 0.6.1)
+ actionmailbox (7.0.10)
+ actionpack (= 7.0.10)
+ activejob (= 7.0.10)
+ activerecord (= 7.0.10)
+ activestorage (= 7.0.10)
+ activesupport (= 7.0.10)
+ mail (>= 2.7.1)
+ net-imap
+ net-pop
+ net-smtp
+ actionmailer (7.0.10)
+ actionpack (= 7.0.10)
+ actionview (= 7.0.10)
+ activejob (= 7.0.10)
+ activesupport (= 7.0.10)
+ mail (~> 2.5, >= 2.5.4)
+ net-imap
+ net-pop
+ net-smtp
+ rails-dom-testing (~> 2.0)
+ actionpack (7.0.10)
+ actionview (= 7.0.10)
+ activesupport (= 7.0.10)
+ racc
+ rack (~> 2.0, >= 2.2.4)
+ rack-test (>= 0.6.3)
+ rails-dom-testing (~> 2.0)
+ rails-html-sanitizer (~> 1.0, >= 1.2.0)
+ actiontext (7.0.10)
+ actionpack (= 7.0.10)
+ activerecord (= 7.0.10)
+ activestorage (= 7.0.10)
+ activesupport (= 7.0.10)
+ globalid (>= 0.6.0)
+ nokogiri (>= 1.8.5)
+ actionview (7.0.10)
+ activesupport (= 7.0.10)
+ builder (~> 3.1)
+ erubi (~> 1.4)
+ rails-dom-testing (~> 2.0)
+ rails-html-sanitizer (~> 1.1, >= 1.2.0)
+ activejob (7.0.10)
+ activesupport (= 7.0.10)
+ globalid (>= 0.3.6)
+ activemodel (7.0.10)
+ activesupport (= 7.0.10)
+ activerecord (7.0.10)
+ activemodel (= 7.0.10)
+ activesupport (= 7.0.10)
+ activestorage (7.0.10)
+ actionpack (= 7.0.10)
+ activejob (= 7.0.10)
+ activerecord (= 7.0.10)
+ activesupport (= 7.0.10)
+ marcel (~> 1.0)
+ mini_mime (>= 1.1.0)
+ activesupport (7.0.10)
+ base64
+ benchmark (>= 0.3)
+ bigdecimal
+ concurrent-ruby (~> 1.0, >= 1.0.2)
+ drb
+ i18n (>= 1.6, < 2)
+ logger (>= 1.4.2)
+ minitest (>= 5.1)
+ mutex_m
+ securerandom (>= 0.3)
+ tzinfo (~> 2.0)
+ addressable (2.9.0)
+ public_suffix (>= 2.0.2, < 8.0)
+ base64 (0.3.0)
+ benchmark (0.5.0)
+ bigdecimal (4.1.2)
+ bindex (0.8.1)
+ bootsnap (1.24.6)
+ msgpack (~> 1.2)
+ builder (3.3.0)
+ capybara (3.40.0)
+ addressable
+ matrix
+ mini_mime (>= 0.1.3)
+ nokogiri (~> 1.11)
+ rack (>= 1.6.0)
+ rack-test (>= 0.6.3)
+ regexp_parser (>= 1.5, < 3.0)
+ xpath (~> 3.2)
+ concurrent-ruby (1.3.7)
+ crass (1.0.6)
+ date (3.5.1)
+ debug (1.11.1)
+ irb (~> 1.10)
+ reline (>= 0.3.8)
+ domain_name (0.6.20240107)
+ drb (2.2.3)
+ erb (6.0.4)
+ erubi (1.13.1)
+ globalid (1.3.0)
+ activesupport (>= 6.1)
+ google-protobuf (4.35.1)
+ bigdecimal
+ rake (~> 13.3)
+ google-protobuf (4.35.1-aarch64-linux-gnu)
+ bigdecimal
+ rake (~> 13.3)
+ google-protobuf (4.35.1-aarch64-linux-musl)
+ bigdecimal
+ rake (~> 13.3)
+ google-protobuf (4.35.1-arm64-darwin)
+ bigdecimal
+ rake (~> 13.3)
+ google-protobuf (4.35.1-x86_64-darwin)
+ bigdecimal
+ rake (~> 13.3)
+ google-protobuf (4.35.1-x86_64-linux-gnu)
+ bigdecimal
+ rake (~> 13.3)
+ google-protobuf (4.35.1-x86_64-linux-musl)
+ bigdecimal
+ rake (~> 13.3)
+ googleapis-common-protos-types (1.23.0)
+ google-protobuf (~> 4.26)
+ http (6.0.3)
+ http-cookie (~> 1.0)
+ llhttp (~> 0.6.1)
+ http-cookie (1.1.6)
+ domain_name (~> 0.5)
+ i18n (1.15.1)
+ concurrent-ruby (~> 1.0)
+ importmap-rails (2.2.3)
+ actionpack (>= 6.0.0)
+ activesupport (>= 6.0.0)
+ railties (>= 6.0.0)
+ io-console (0.8.2)
+ irb (1.18.0)
+ pp (>= 0.6.0)
+ prism (>= 1.3.0)
+ rdoc (>= 4.0.0)
+ reline (>= 0.4.2)
+ jbuilder (2.15.1)
+ actionview (>= 7.0.0)
+ activesupport (>= 7.0.0)
+ json (2.19.9)
+ launchdarkly-server-sdk (8.14.0)
+ benchmark (~> 0.1, >= 0.1.1)
+ concurrent-ruby (~> 1.1)
+ http (>= 4.4.0, < 7.0.0)
+ json (~> 2.3)
+ ld-eventsource (= 2.6.0)
+ observer (~> 0.1.2)
+ openssl (>= 3.1.2, < 5.0)
+ semantic (~> 1.6)
+ zlib (~> 3.1)
+ ld-eventsource (2.6.0)
+ concurrent-ruby (~> 1.0)
+ http (>= 4.4.1, < 7.0.0)
+ llhttp (0.6.1)
+ logger (1.7.0)
+ loofah (2.25.1)
+ crass (~> 1.0.2)
+ nokogiri (>= 1.12.0)
+ mail (2.9.0)
+ logger
+ mini_mime (>= 0.1.1)
+ net-imap
+ net-pop
+ net-smtp
+ marcel (1.2.1)
+ matrix (0.4.3)
+ method_source (1.1.0)
+ mini_mime (1.1.5)
+ minitest (5.27.0)
+ msgpack (1.8.3)
+ mutex_m (0.3.0)
+ net-imap (0.6.4.1)
+ date
+ net-protocol
+ net-pop (0.1.2)
+ net-protocol
+ net-protocol (0.2.2)
+ timeout
+ net-smtp (0.5.1)
+ net-protocol
+ nio4r (2.7.5)
+ nokogiri (1.19.4-aarch64-linux-gnu)
+ racc (~> 1.4)
+ nokogiri (1.19.4-aarch64-linux-musl)
+ racc (~> 1.4)
+ nokogiri (1.19.4-arm-linux-gnu)
+ racc (~> 1.4)
+ nokogiri (1.19.4-arm-linux-musl)
+ racc (~> 1.4)
+ nokogiri (1.19.4-arm64-darwin)
+ racc (~> 1.4)
+ nokogiri (1.19.4-x86_64-darwin)
+ racc (~> 1.4)
+ nokogiri (1.19.4-x86_64-linux-gnu)
+ racc (~> 1.4)
+ nokogiri (1.19.4-x86_64-linux-musl)
+ racc (~> 1.4)
+ observer (0.1.2)
+ openssl (4.0.2)
+ opentelemetry-api (1.10.0)
+ logger
+ opentelemetry-common (0.25.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-exporter-otlp (0.34.0)
+ google-protobuf (>= 3.18)
+ googleapis-common-protos-types (~> 1.3)
+ opentelemetry-api (~> 1.1)
+ opentelemetry-common (~> 0.20)
+ opentelemetry-sdk (~> 1.10)
+ opentelemetry-semantic_conventions
+ opentelemetry-exporter-otlp-logs (0.5.1)
+ google-protobuf (>= 3.18)
+ googleapis-common-protos-types (~> 1.3)
+ opentelemetry-api (~> 1.1)
+ opentelemetry-common (~> 0.20)
+ opentelemetry-logs-api (~> 0.1)
+ opentelemetry-logs-sdk (~> 0.1)
+ opentelemetry-sdk
+ opentelemetry-semantic_conventions
+ opentelemetry-helpers-mysql (0.6.0)
+ opentelemetry-api (~> 1.7)
+ opentelemetry-common (~> 0.21)
+ opentelemetry-helpers-sql (0.4.0)
+ opentelemetry-api (~> 1.7)
+ opentelemetry-helpers-sql-processor (0.5.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-common (~> 0.21)
+ opentelemetry-instrumentation-action_mailer (0.7.0)
+ opentelemetry-instrumentation-active_support (~> 0.10)
+ opentelemetry-instrumentation-action_pack (0.17.0)
+ opentelemetry-instrumentation-rack (~> 0.29)
+ opentelemetry-instrumentation-action_view (0.12.0)
+ opentelemetry-instrumentation-active_support (~> 0.10)
+ opentelemetry-instrumentation-active_job (0.11.0)
+ opentelemetry-instrumentation-base (~> 0.25)
+ opentelemetry-instrumentation-active_record (0.12.0)
+ opentelemetry-instrumentation-base (~> 0.25)
+ opentelemetry-instrumentation-active_storage (0.4.0)
+ opentelemetry-instrumentation-active_support (~> 0.10)
+ opentelemetry-instrumentation-active_support (0.11.0)
+ opentelemetry-instrumentation-base (~> 0.25)
+ opentelemetry-instrumentation-base (0.26.1)
+ opentelemetry-api (~> 1.7)
+ opentelemetry-common (~> 0.21)
+ opentelemetry-registry (~> 0.1)
+ opentelemetry-instrumentation-concurrent_ruby (0.25.0)
+ opentelemetry-instrumentation-base (~> 0.25)
+ opentelemetry-instrumentation-faraday (0.33.0)
+ opentelemetry-instrumentation-base (~> 0.25)
+ opentelemetry-instrumentation-graphql (0.32.0)
+ opentelemetry-instrumentation-base (~> 0.25)
+ opentelemetry-instrumentation-http (0.30.0)
+ opentelemetry-instrumentation-base (~> 0.25)
+ opentelemetry-instrumentation-mysql2 (0.34.0)
+ opentelemetry-helpers-mysql
+ opentelemetry-helpers-sql
+ opentelemetry-helpers-sql-processor
+ opentelemetry-instrumentation-base (~> 0.25)
+ opentelemetry-instrumentation-net_http (0.29.0)
+ opentelemetry-instrumentation-base (~> 0.25)
+ opentelemetry-instrumentation-pg (0.36.0)
+ opentelemetry-helpers-sql
+ opentelemetry-helpers-sql-processor
+ opentelemetry-instrumentation-base (~> 0.25)
+ opentelemetry-instrumentation-rack (0.31.1)
+ opentelemetry-instrumentation-base (~> 0.25)
+ opentelemetry-instrumentation-rails (0.41.0)
+ opentelemetry-instrumentation-action_mailer (~> 0.7)
+ opentelemetry-instrumentation-action_pack (~> 0.17)
+ opentelemetry-instrumentation-action_view (~> 0.12)
+ opentelemetry-instrumentation-active_job (~> 0.11)
+ opentelemetry-instrumentation-active_record (~> 0.12)
+ opentelemetry-instrumentation-active_storage (~> 0.4)
+ opentelemetry-instrumentation-active_support (~> 0.11)
+ opentelemetry-instrumentation-concurrent_ruby (~> 0.25)
+ opentelemetry-instrumentation-redis (0.29.0)
+ opentelemetry-instrumentation-base (~> 0.25)
+ opentelemetry-instrumentation-sidekiq (0.29.0)
+ opentelemetry-instrumentation-base (~> 0.25)
+ opentelemetry-instrumentation-sinatra (0.30.0)
+ opentelemetry-instrumentation-rack (~> 0.29)
+ opentelemetry-logs-api (0.4.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-logs-sdk (0.6.0)
+ opentelemetry-api (~> 1.2)
+ opentelemetry-logs-api (~> 0.1)
+ opentelemetry-sdk (~> 1.3)
+ opentelemetry-registry (0.6.0)
+ opentelemetry-api (~> 1.1)
+ opentelemetry-sdk (1.12.0)
+ logger
+ opentelemetry-api (~> 1.1)
+ opentelemetry-common (~> 0.20)
+ opentelemetry-registry (~> 0.2)
+ opentelemetry-semantic_conventions
+ opentelemetry-semantic_conventions (1.41.0)
+ opentelemetry-api (~> 1.0)
+ pp (0.6.3)
+ prettyprint
+ prettyprint (0.2.0)
+ prism (1.9.0)
+ psych (5.4.0)
+ date
+ stringio
+ public_suffix (7.0.5)
+ puma (6.6.1)
+ nio4r (~> 2.0)
+ racc (1.8.1)
+ rack (2.2.23)
+ rack-test (2.2.0)
+ rack (>= 1.3)
+ rails (7.0.10)
+ actioncable (= 7.0.10)
+ actionmailbox (= 7.0.10)
+ actionmailer (= 7.0.10)
+ actionpack (= 7.0.10)
+ actiontext (= 7.0.10)
+ actionview (= 7.0.10)
+ activejob (= 7.0.10)
+ activemodel (= 7.0.10)
+ activerecord (= 7.0.10)
+ activestorage (= 7.0.10)
+ activesupport (= 7.0.10)
+ bundler (>= 1.15.0)
+ railties (= 7.0.10)
+ rails-dom-testing (2.3.0)
+ activesupport (>= 5.0.0)
+ minitest
+ nokogiri (>= 1.6)
+ rails-html-sanitizer (1.7.0)
+ loofah (~> 2.25)
+ nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
+ railties (7.0.10)
+ actionpack (= 7.0.10)
+ activesupport (= 7.0.10)
+ method_source
+ rake (>= 12.2)
+ thor (~> 1.0)
+ zeitwerk (~> 2.5)
+ rake (13.4.2)
+ rdoc (7.2.0)
+ erb
+ psych (>= 4.0.0)
+ tsort
+ redis (4.8.1)
+ regexp_parser (2.12.0)
+ reline (0.6.3)
+ io-console (~> 0.5)
+ rexml (3.4.4)
+ rubyzip (3.4.0)
+ securerandom (0.4.1)
+ selenium-webdriver (4.45.0)
+ base64 (~> 0.2)
+ logger (~> 1.4)
+ rexml (~> 3.2, >= 3.2.5)
+ rubyzip (>= 1.2.2, < 4.0)
+ websocket (~> 1.0)
+ semantic (1.6.1)
+ sprockets (4.2.2)
+ concurrent-ruby (~> 1.0)
+ logger
+ rack (>= 2.2.4, < 4)
+ sprockets-rails (3.5.2)
+ actionpack (>= 6.1)
+ activesupport (>= 6.1)
+ sprockets (>= 3.0.0)
+ sqlite3 (1.7.3-aarch64-linux)
+ sqlite3 (1.7.3-arm-linux)
+ sqlite3 (1.7.3-arm64-darwin)
+ sqlite3 (1.7.3-x86_64-darwin)
+ sqlite3 (1.7.3-x86_64-linux)
+ stimulus-rails (1.3.4)
+ railties (>= 6.0.0)
+ stringio (3.2.0)
+ thor (1.5.0)
+ timeout (0.6.1)
+ tsort (0.2.0)
+ turbo-rails (2.0.12)
+ actionpack (>= 6.0.0)
+ railties (>= 6.0.0)
+ tzinfo (2.0.6)
+ concurrent-ruby (~> 1.0)
+ web-console (4.2.1)
+ actionview (>= 6.0.0)
+ activemodel (>= 6.0.0)
+ bindex (>= 0.4.0)
+ railties (>= 6.0.0)
+ webrick (1.9.2)
+ websocket (1.2.11)
+ websocket-driver (0.8.1)
+ base64
+ websocket-extensions (>= 0.1.0)
+ websocket-extensions (0.1.5)
+ xpath (3.2.0)
+ nokogiri (~> 1.8)
+ zeitwerk (2.8.2)
+ zlib (3.2.3)
+
+PLATFORMS
+ aarch64-linux
+ aarch64-linux-gnu
+ aarch64-linux-musl
+ arm-linux
+ arm-linux-gnu
+ arm-linux-musl
+ arm64-darwin
+ x86_64-darwin
+ x86_64-linux
+ x86_64-linux-gnu
+ x86_64-linux-musl
+
+DEPENDENCIES
+ bootsnap
+ capybara
+ debug
+ importmap-rails
+ jbuilder
+ launchdarkly-observability!
+ launchdarkly-server-sdk (~> 8.0)
+ minitest (~> 5.0)
+ puma (~> 6.0)
+ rails (~> 7.0.0)
+ redis (~> 4.0)
+ selenium-webdriver
+ sprockets-rails
+ sqlite3 (~> 1.4)
+ stimulus-rails
+ turbo-rails
+ tzinfo-data
+ web-console
+ webrick
+
+RUBY VERSION
+ ruby 3.3.4p94
+
+BUNDLED WITH
+ 2.5.11
diff --git a/e2e/ruby/rails/demo-rails70/README.md b/e2e/ruby/rails/demo-rails70/README.md
new file mode 100644
index 0000000000..fa45d7ed0d
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/README.md
@@ -0,0 +1,53 @@
+# demo-rails70 — Rails 7.0 observability regression repro
+
+A Rails **7.0** application that pins the LaunchDarkly observability plugin to a
+known-broken scenario and guards against it regressing. It is a copy of
+[`../demo`](../demo) (Rails 7.2) with Rails pinned to `~> 7.0`.
+
+## Why this exists
+
+CardFlight runs Rails 7.0. The plugin used to depend on
+`opentelemetry-instrumentation-all`, whose Rails-family members raised their
+minimum to **Rails 7.1**. On Rails 7.0 every Rails-family instrumentation failed
+its runtime `compatible?` check, logged a flurry of `"... failed to install"`
+warnings, and never attached — so the app lost all Rails auto-instrumentation
+(no HTTP server spans, DB spans, etc.) and got only manual instrumentation.
+
+The other e2e apps run Rails 7.2, which is **above** that floor, so they can
+never catch this class of break. This app reproduces it on Rails 7.0.
+
+Ruby stays at **3.3.4** on purpose: on a current Ruby, Bundler still resolves the
+*latest* instrumentation gems, faithfully matching a real Rails 7.0 customer. (On
+Ruby < 3.2, Bundler would self-heal to an older instrumentation set via
+`required_ruby_version` and the bug would not reproduce.)
+
+## How the telemetry assertion works
+
+The OTLP exporter is pointed at a small **in-process OTLP sink**
+([`test/support/otlp_sink.rb`](test/support/otlp_sink.rb)) via
+`OTEL_EXPORTER_OTLP_ENDPOINT`. The sink decodes the real OTLP protobuf using the
+proto classes shipped with the exporter gems, so the test asserts that traces (an
+auto-instrumented HTTP **server** span), a log record, and a captured exception
+are actually **exported over the wire** — in pure Ruby, no Docker or Node, under
+`bundle exec rake`.
+
+`test/integration/otlp_export_e2e_test.rb` is the headline test:
+
+- **Before the fix** (instrumentation-all → instrumentation-rails 0.42): no server
+ span is produced or exported — the test fails.
+- **After the fix** (instrumentation-rails pinned to 0.41): the server span,
+ log, and exception all reach the sink — the test passes.
+
+## Running it
+
+```bash
+cd e2e/ruby/rails/demo-rails70
+bundle install
+bundle exec rake # runs the test suite (this is what CI runs)
+```
+
+Requires Ruby 3.3.x. The suite needs no external services: the OTLP sink runs
+in-process and a dummy `LAUNCHDARKLY_SDK_KEY` is set by `test/test_helper.rb`.
+
+In CI this app runs as the `e2e-rails-legacy` job in
+[`.github/workflows/ruby-plugin.yml`](../../../../.github/workflows/ruby-plugin.yml).
diff --git a/e2e/ruby/rails/demo-rails70/Rakefile b/e2e/ruby/rails/demo-rails70/Rakefile
new file mode 100644
index 0000000000..488c551fee
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/Rakefile
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+# Add your own tasks in files placed in lib/tasks ending in .rake,
+# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
+
+require_relative 'config/application'
+
+Rails.application.load_tasks
diff --git a/e2e/ruby/rails/demo-rails70/app/assets/config/manifest.js b/e2e/ruby/rails/demo-rails70/app/assets/config/manifest.js
new file mode 100644
index 0000000000..ddd546a0be
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/assets/config/manifest.js
@@ -0,0 +1,4 @@
+//= link_tree ../images
+//= link_directory ../stylesheets .css
+//= link_tree ../../javascript .js
+//= link_tree ../../../vendor/javascript .js
diff --git a/e2e/ruby/rails/demo-rails70/app/assets/images/.keep b/e2e/ruby/rails/demo-rails70/app/assets/images/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/e2e/ruby/rails/demo-rails70/app/assets/stylesheets/application.css b/e2e/ruby/rails/demo-rails70/app/assets/stylesheets/application.css
new file mode 100644
index 0000000000..1f8be2055a
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/assets/stylesheets/application.css
@@ -0,0 +1,458 @@
+/*
+ *= require_tree .
+ *= require_self
+ */
+
+:root {
+ --color-brand: #405bff;
+ --color-brand-dark: #3148cc;
+ --color-header-bg: #282828;
+ --color-header-text: #ffffff;
+ --color-bg: #f5f6f8;
+ --color-surface: #ffffff;
+ --color-text: #282828;
+ --color-text-muted: #6b7280;
+ --color-border: #e0e2e6;
+ --color-success: #16a34a;
+ --color-success-bg: #f0fdf4;
+ --color-danger: #dc2626;
+ --color-danger-bg: #fef2f2;
+ --color-code-bg: #f1f3f5;
+ --radius: 8px;
+ --shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04);
+ --max-width: 960px;
+}
+
+*, *::before, *::after {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+ font-size: 15px;
+ line-height: 1.6;
+ color: var(--color-text);
+ background: var(--color-bg);
+}
+
+/* --- Header / Nav --- */
+
+.site-header {
+ background: var(--color-header-bg);
+ color: var(--color-header-text);
+ padding: 0 24px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ height: 56px;
+}
+
+.site-header a {
+ color: var(--color-header-text);
+ text-decoration: none;
+}
+
+.site-logo {
+ font-weight: 700;
+ font-size: 16px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.site-nav {
+ display: flex;
+ gap: 24px;
+}
+
+.site-nav a {
+ font-size: 14px;
+ opacity: 0.8;
+ transition: opacity 0.15s;
+}
+
+.site-nav a:hover,
+.site-nav a.active {
+ opacity: 1;
+}
+
+/* --- Container --- */
+
+.container {
+ max-width: var(--max-width);
+ margin: 0 auto;
+ padding: 32px 24px 64px;
+}
+
+/* --- Typography --- */
+
+h1 {
+ font-size: 28px;
+ font-weight: 700;
+ margin: 0 0 8px;
+}
+
+h2 {
+ font-size: 20px;
+ font-weight: 600;
+ margin: 32px 0 12px;
+ padding-bottom: 8px;
+ border-bottom: 1px solid var(--color-border);
+}
+
+h3 {
+ font-size: 16px;
+ font-weight: 600;
+ margin: 24px 0 8px;
+}
+
+p {
+ margin: 0 0 16px;
+}
+
+a {
+ color: var(--color-brand);
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+.subtitle {
+ color: var(--color-text-muted);
+ margin-bottom: 24px;
+}
+
+/* --- Cards --- */
+
+.card {
+ background: var(--color-surface);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius);
+ box-shadow: var(--shadow);
+ padding: 20px 24px;
+ margin-bottom: 24px;
+}
+
+.card h2 {
+ margin-top: 0;
+ border-bottom: none;
+ padding-bottom: 0;
+}
+
+/* --- Status Badges --- */
+
+.badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 13px;
+ font-weight: 600;
+ padding: 4px 10px;
+ border-radius: 999px;
+}
+
+.badge-success {
+ background: var(--color-success-bg);
+ color: var(--color-success);
+}
+
+.badge-danger {
+ background: var(--color-danger-bg);
+ color: var(--color-danger);
+}
+
+/* --- Status Info --- */
+
+.status-list {
+ list-style: none;
+ padding: 0;
+ margin: 0 0 16px;
+}
+
+.status-list li {
+ padding: 6px 0;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.status-list li + li {
+ border-top: 1px solid var(--color-border);
+}
+
+.status-label {
+ font-weight: 600;
+ min-width: 120px;
+ color: var(--color-text-muted);
+ font-size: 13px;
+ text-transform: uppercase;
+ letter-spacing: 0.03em;
+}
+
+/* --- Tables --- */
+
+table {
+ width: 100%;
+ border-collapse: separate;
+ border-spacing: 0;
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius);
+ overflow: hidden;
+ margin-bottom: 16px;
+ background: var(--color-surface);
+}
+
+thead th {
+ background: var(--color-bg);
+ font-size: 12px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ color: var(--color-text-muted);
+ text-align: left;
+ padding: 10px 16px;
+ border-bottom: 1px solid var(--color-border);
+}
+
+tbody td {
+ padding: 10px 16px;
+ border-bottom: 1px solid var(--color-border);
+ font-size: 14px;
+}
+
+tbody tr:last-child td {
+ border-bottom: none;
+}
+
+tbody tr:hover {
+ background: #f9fafb;
+}
+
+/* --- Code / Pre --- */
+
+code {
+ font-family: "SF Mono", "Fira Code", "Fira Mono", Menlo, Consolas, monospace;
+ font-size: 13px;
+ background: var(--color-code-bg);
+ padding: 2px 6px;
+ border-radius: 4px;
+}
+
+pre {
+ background: var(--color-code-bg);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius);
+ padding: 16px;
+ overflow-x: auto;
+ font-size: 13px;
+ line-height: 1.5;
+}
+
+pre code {
+ background: none;
+ padding: 0;
+ border-radius: 0;
+}
+
+.code-block {
+ background: var(--color-code-bg);
+ padding: 12px 16px;
+ border-radius: var(--radius);
+ display: block;
+ font-size: 14px;
+}
+
+.value-block {
+ background: #eff6ff;
+ padding: 12px 16px;
+ border-radius: var(--radius);
+ display: block;
+}
+
+/* --- Buttons --- */
+
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 8px 16px;
+ font-size: 14px;
+ font-weight: 500;
+ border-radius: 6px;
+ border: 1px solid transparent;
+ cursor: pointer;
+ transition: background 0.15s, box-shadow 0.15s;
+ text-decoration: none;
+ line-height: 1.4;
+}
+
+.btn:hover {
+ text-decoration: none;
+}
+
+.btn-primary {
+ background: var(--color-brand);
+ color: #fff;
+ border-color: var(--color-brand);
+}
+
+.btn-primary:hover {
+ background: var(--color-brand-dark);
+}
+
+.btn-secondary {
+ background: var(--color-surface);
+ color: var(--color-text);
+ border-color: var(--color-border);
+}
+
+.btn-secondary:hover {
+ background: var(--color-bg);
+}
+
+.btn-danger {
+ background: var(--color-danger);
+ color: #fff;
+ border-color: var(--color-danger);
+}
+
+.btn-danger:hover {
+ background: #b91c1c;
+}
+
+.btn-group {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-top: 12px;
+}
+
+/* Style button_to forms inside button groups */
+.btn-group form {
+ display: inline-block;
+}
+
+.btn-group form button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 8px 16px;
+ font-size: 14px;
+ font-weight: 500;
+ border-radius: 6px;
+ border: 1px solid var(--color-border);
+ background: var(--color-surface);
+ color: var(--color-text);
+ cursor: pointer;
+ transition: background 0.15s;
+}
+
+.btn-group form button:hover {
+ background: var(--color-bg);
+}
+
+/* --- Action Sections --- */
+
+.action-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 16px;
+ margin-top: 16px;
+}
+
+.action-card {
+ background: var(--color-surface);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius);
+ padding: 20px;
+}
+
+.action-card h3 {
+ margin: 0 0 4px;
+ border-bottom: none;
+ padding-bottom: 0;
+ font-size: 15px;
+}
+
+.action-card p {
+ color: var(--color-text-muted);
+ font-size: 13px;
+ margin: 0 0 12px;
+}
+
+/* --- Detail Fields --- */
+
+.detail-field {
+ margin-bottom: 20px;
+}
+
+.detail-field h2 {
+ font-size: 13px;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ color: var(--color-text-muted);
+ margin: 0 0 6px;
+ padding-bottom: 0;
+ border-bottom: none;
+}
+
+/* --- Endpoint List --- */
+
+.endpoint-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.endpoint-list li {
+ padding: 8px 0;
+ border-bottom: 1px solid var(--color-border);
+ font-size: 14px;
+}
+
+.endpoint-list li:last-child {
+ border-bottom: none;
+}
+
+.endpoint-method {
+ display: inline-block;
+ background: var(--color-code-bg);
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-size: 11px;
+ font-weight: 700;
+ font-family: monospace;
+ margin-right: 6px;
+}
+
+/* --- Footer --- */
+
+.site-footer {
+ text-align: center;
+ padding: 24px;
+ color: var(--color-text-muted);
+ font-size: 13px;
+ border-top: 1px solid var(--color-border);
+ margin-top: 48px;
+}
+
+/* --- Feedback --- */
+
+.feedback {
+ display: inline-flex;
+ align-items: center;
+ font-size: 13px;
+ color: var(--color-success);
+ padding: 8px 0;
+}
+
+/* --- Links row --- */
+
+.link-row {
+ display: flex;
+ gap: 12px;
+ margin-top: 16px;
+}
diff --git a/e2e/ruby/rails/demo-rails70/app/channels/application_cable/channel.rb b/e2e/ruby/rails/demo-rails70/app/channels/application_cable/channel.rb
new file mode 100644
index 0000000000..9aec230539
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/channels/application_cable/channel.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+module ApplicationCable
+ class Channel < ActionCable::Channel::Base
+ end
+end
diff --git a/e2e/ruby/rails/demo-rails70/app/channels/application_cable/connection.rb b/e2e/ruby/rails/demo-rails70/app/channels/application_cable/connection.rb
new file mode 100644
index 0000000000..8d6c2a1bf4
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/channels/application_cable/connection.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+module ApplicationCable
+ class Connection < ActionCable::Connection::Base
+ end
+end
diff --git a/e2e/ruby/rails/demo-rails70/app/controllers/application_controller.rb b/e2e/ruby/rails/demo-rails70/app/controllers/application_controller.rb
new file mode 100644
index 0000000000..1389b9442f
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/controllers/application_controller.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class ApplicationController < ActionController::Base
+ private
+
+ def ld_client
+ Rails.configuration.ld_client
+ end
+ helper_method :ld_client
+
+ # Render a turbo-frame-compatible success message for POST action buttons
+ def render_turbo_feedback(frame_id, message)
+ timestamp = Time.current.strftime('%H:%M:%S')
+ html = %(#{message} at #{timestamp})
+ render html: html.html_safe, layout: false
+ end
+end
diff --git a/e2e/ruby/rails/demo-rails70/app/controllers/concerns/.keep b/e2e/ruby/rails/demo-rails70/app/controllers/concerns/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/e2e/ruby/rails/demo-rails70/app/controllers/errors_controller.rb b/e2e/ruby/rails/demo-rails70/app/controllers/errors_controller.rb
new file mode 100644
index 0000000000..bdc1a85776
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/controllers/errors_controller.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class ErrorsController < ApplicationController
+ # Uses the module-level LaunchDarklyObservability.record_exception
+ def create
+ LaunchDarklyObservability.in_span('error-handling-example', attributes: { 'foo' => 'bar' }) do |span|
+ begin
+ 1 / 0
+ rescue StandardError => e
+ LaunchDarklyObservability.record_exception(e)
+ Rails.logger.error "Exception occurred: #{e.message}"
+ end
+ end
+
+ render_turbo_feedback('error_feedback', 'Error recorded (module helper)')
+ end
+
+ # Uses the controller-level record_launchdarkly_exception helper
+ def create_with_helper
+ with_launchdarkly_span('error-controller-helper-example', attributes: { 'source' => 'controller_helper' }) do
+ begin
+ raise ArgumentError, 'demo error via controller helper'
+ rescue StandardError => e
+ record_launchdarkly_exception(e)
+ Rails.logger.error "Exception occurred: #{e.message}"
+ end
+ end
+
+ render_turbo_feedback('error_feedback', 'Error recorded (controller helper)')
+ end
+end
diff --git a/e2e/ruby/rails/demo-rails70/app/controllers/flags_controller.rb b/e2e/ruby/rails/demo-rails70/app/controllers/flags_controller.rb
new file mode 100644
index 0000000000..5759bf6bb5
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/controllers/flags_controller.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+# Controller to demonstrate LaunchDarkly feature flag evaluations
+# with observability instrumentation
+class FlagsController < ApplicationController
+ # GET /flags
+ # Returns all flag evaluations for the current context
+ def index
+ context = build_context
+
+ # Get all flags from LaunchDarkly
+ state = Rails.configuration.ld_client.all_flags_state(context, details_only_for_tracked_flags: false)
+ @all_flags_valid = state.valid?
+
+ # Build evaluations from all available flags
+ @evaluations = {}
+ state.values_map.each_key do |flag_key|
+ @evaluations[flag_key] = Rails.configuration.ld_client.variation_detail(flag_key, context, nil)
+ end
+
+ respond_to do |format|
+ format.html
+ format.json { render json: { valid: @all_flags_valid, evaluations: format_evaluations(@evaluations) } }
+ end
+ end
+
+ # GET /flags/:key
+ # Returns a single flag evaluation
+ def show
+ context = build_context
+ flag_key = params[:id]
+
+ detail = Rails.configuration.ld_client.variation_detail(flag_key, context, nil)
+
+ respond_to do |format|
+ format.html { @flag_key = flag_key; @detail = detail }
+ format.json { render json: format_detail(flag_key, detail) }
+ end
+ end
+
+ # POST /flags/evaluate
+ # Evaluate a flag with a custom context
+ def evaluate
+ flag_key = params[:flag_key]
+ context_data = params[:context] || {}
+
+ context = LaunchDarkly::LDContext.create({
+ key: context_data[:key] || 'anonymous',
+ kind: context_data[:kind] || 'user',
+ **context_data.except(:key, :kind).to_h.symbolize_keys
+ })
+
+ detail = Rails.configuration.ld_client.variation_detail(flag_key, context, params[:default])
+
+ render json: format_detail(flag_key, detail)
+ end
+
+ # POST /flags/batch
+ # Evaluate multiple flags at once (demonstrates multiple spans)
+ def batch
+ context = build_context
+ flag_keys = params[:flag_keys]
+
+ unless flag_keys.present?
+ return render json: { error: 'flag_keys parameter required' }, status: :bad_request
+ end
+
+ results = flag_keys.each_with_object({}) do |key, hash|
+ hash[key] = Rails.configuration.ld_client.variation(key, context, nil)
+ end
+
+ render json: { evaluations: results, context_key: context.key }
+ end
+
+ # GET /flags/all_flags
+ # Get all flag states (demonstrates all_flags_state method)
+ def all_flags
+ context = build_context
+ state = Rails.configuration.ld_client.all_flags_state(context)
+
+ render json: {
+ valid: state.valid?,
+ flags: JSON.parse(state.to_json)
+ }
+ end
+
+ private
+
+ def build_context
+ # Build context from request parameters or session
+ user_key = params[:user_key] || session.id.to_s.presence || 'anonymous'
+ user_kind = params[:user_kind] || 'user'
+
+ attrs = {
+ key: user_key,
+ kind: user_kind,
+ anonymous: user_key == 'anonymous'
+ }
+
+ # Add optional attributes
+ attrs[:email] = params[:email] if params[:email].present?
+ attrs[:name] = params[:name] if params[:name].present?
+ attrs[:plan] = params[:plan] if params[:plan].present?
+
+ LaunchDarkly::LDContext.create(attrs)
+ end
+
+ def format_evaluations(evaluations)
+ evaluations.transform_values { |detail| format_detail_hash(detail) }
+ end
+
+ def format_detail(key, detail)
+ {
+ flag_key: key,
+ **format_detail_hash(detail)
+ }
+ end
+
+ def format_detail_hash(detail)
+ {
+ value: detail.value,
+ variation_index: detail.variation_index,
+ reason: detail.reason ? JSON.parse(detail.reason.to_json) : nil
+ }
+ end
+end
diff --git a/e2e/ruby/rails/demo-rails70/app/controllers/logs_controller.rb b/e2e/ruby/rails/demo-rails70/app/controllers/logs_controller.rb
new file mode 100644
index 0000000000..b105191a08
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/controllers/logs_controller.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class LogsController < ApplicationController
+ def create
+ Rails.logger.info "hello, world! foo=bar"
+ render_turbo_feedback('log_feedback', 'Info log created')
+ end
+
+ def create_with_hash
+ Rails.logger.info(test: 'ing', foo: 'bar')
+ render_turbo_feedback('log_feedback', 'Info log (hash) created')
+ end
+
+ def create_warn
+ Rails.logger.warn "warning: something looks off level=warn"
+ render_turbo_feedback('log_feedback', 'Warn log created')
+ end
+
+ def create_error
+ Rails.logger.error "error: something went wrong level=error"
+ render_turbo_feedback('log_feedback', 'Error log created')
+ end
+end
diff --git a/e2e/ruby/rails/demo-rails70/app/controllers/pages_controller.rb b/e2e/ruby/rails/demo-rails70/app/controllers/pages_controller.rb
new file mode 100644
index 0000000000..093befaf4b
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/controllers/pages_controller.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class PagesController < ApplicationController
+ def home
+ # Create LaunchDarkly context for current user/session
+ @context = LaunchDarkly::LDContext.create({
+ key: session.id.to_s.presence || 'anonymous',
+ kind: 'user',
+ anonymous: session.id.blank?
+ })
+
+ state = ld_client.all_flags_state(@context)
+ @flags_valid = state.valid?
+ @flag_count = state.values_map.size
+ @sample_evaluations = state.values_map.first(5).to_h
+
+ # Make an HTTP request (auto-instrumented by OpenTelemetry)
+ @http_url = 'http://www.example.com/?test=1'
+ with_launchdarkly_span('pages-home-fetch', attributes: { 'custom.source' => 'demo' }) do
+ response = Net::HTTP.get_response(URI.parse(@http_url))
+ @http_status = "#{response.code} #{response.message}"
+ end
+
+ Rails.logger.info "[LaunchDarkly] Loaded #{@flag_count} flags, valid=#{@flags_valid}"
+ end
+end
diff --git a/e2e/ruby/rails/demo-rails70/app/controllers/telemetry_controller.rb b/e2e/ruby/rails/demo-rails70/app/controllers/telemetry_controller.rb
new file mode 100644
index 0000000000..c6e01bb003
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/controllers/telemetry_controller.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class TelemetryController < ApplicationController
+ def flush
+ LaunchDarklyObservability.flush
+ render_turbo_feedback('flush_feedback', 'Telemetry flushed')
+ end
+end
diff --git a/e2e/ruby/rails/demo-rails70/app/controllers/traces_controller.rb b/e2e/ruby/rails/demo-rails70/app/controllers/traces_controller.rb
new file mode 100644
index 0000000000..e84d704432
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/controllers/traces_controller.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+class TracesController < ApplicationController
+ def create
+ LaunchDarklyObservability.in_span('example-trace-outer') do |outer_span|
+ sleep(0.1)
+
+ trace = Trace.new(name: 'trace', kind: 'internal')
+
+ LaunchDarklyObservability.in_span('example-trace-inner', attributes: { 'trace.operation' => 'save' }) do |inner_span|
+ sleep(0.2)
+ trace.save!
+ end
+
+ outer_span.set_attribute('trace.operation', 'update')
+ trace.update!(name: 'trace-updated')
+ end
+
+ render_turbo_feedback('trace_feedback', 'Trace created')
+ end
+
+ # Uses the controller-level with_launchdarkly_span helper
+ def create_with_helper
+ with_launchdarkly_span('example-trace-controller-helper', attributes: { 'source' => 'controller_helper' }) do
+ sleep(0.1)
+ end
+
+ render_turbo_feedback('trace_feedback', 'Trace created (controller helper)')
+ end
+end
diff --git a/e2e/ruby/rails/demo-rails70/app/helpers/application_helper.rb b/e2e/ruby/rails/demo-rails70/app/helpers/application_helper.rb
new file mode 100644
index 0000000000..15b06f0f67
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/helpers/application_helper.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+module ApplicationHelper
+end
diff --git a/e2e/ruby/rails/demo-rails70/app/helpers/pages_helper.rb b/e2e/ruby/rails/demo-rails70/app/helpers/pages_helper.rb
new file mode 100644
index 0000000000..15d8b3e442
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/helpers/pages_helper.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+module PagesHelper
+end
diff --git a/e2e/ruby/rails/demo-rails70/app/javascript/application.js b/e2e/ruby/rails/demo-rails70/app/javascript/application.js
new file mode 100644
index 0000000000..9c394a0c73
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/javascript/application.js
@@ -0,0 +1,3 @@
+// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
+import '@hotwired/turbo-rails'
+import 'controllers'
diff --git a/e2e/ruby/rails/demo-rails70/app/javascript/controllers/application.js b/e2e/ruby/rails/demo-rails70/app/javascript/controllers/application.js
new file mode 100644
index 0000000000..c030eb8c7d
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/javascript/controllers/application.js
@@ -0,0 +1,9 @@
+import { Application } from '@hotwired/stimulus'
+
+const application = Application.start()
+
+// Configure Stimulus development experience
+application.debug = false
+window.Stimulus = application
+
+export { application }
diff --git a/e2e/ruby/rails/demo-rails70/app/javascript/controllers/hello_controller.js b/e2e/ruby/rails/demo-rails70/app/javascript/controllers/hello_controller.js
new file mode 100644
index 0000000000..184aa2d69c
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/javascript/controllers/hello_controller.js
@@ -0,0 +1,7 @@
+import { Controller } from '@hotwired/stimulus'
+
+export default class extends Controller {
+ connect() {
+ this.element.textContent = 'Hello World!'
+ }
+}
diff --git a/e2e/ruby/rails/demo-rails70/app/javascript/controllers/index.js b/e2e/ruby/rails/demo-rails70/app/javascript/controllers/index.js
new file mode 100644
index 0000000000..390177a3ce
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/javascript/controllers/index.js
@@ -0,0 +1,11 @@
+// Import and register all your controllers from the importmap under controllers/*
+
+import { application } from 'controllers/application'
+
+// Eager load all controllers defined in the import map under controllers/**/*_controller
+import { eagerLoadControllersFrom } from '@hotwired/stimulus-loading'
+eagerLoadControllersFrom('controllers', application)
+
+// Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!)
+// import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
+// lazyLoadControllersFrom("controllers", application)
diff --git a/e2e/ruby/rails/demo-rails70/app/jobs/application_job.rb b/e2e/ruby/rails/demo-rails70/app/jobs/application_job.rb
new file mode 100644
index 0000000000..bef395997d
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/jobs/application_job.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class ApplicationJob < ActiveJob::Base
+ # Automatically retry jobs that encountered a deadlock
+ # retry_on ActiveRecord::Deadlocked
+
+ # Most jobs are safe to ignore if the underlying records are no longer available
+ # discard_on ActiveJob::DeserializationError
+end
diff --git a/e2e/ruby/rails/demo-rails70/app/mailers/application_mailer.rb b/e2e/ruby/rails/demo-rails70/app/mailers/application_mailer.rb
new file mode 100644
index 0000000000..d84cb6e71e
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/mailers/application_mailer.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class ApplicationMailer < ActionMailer::Base
+ default from: 'from@example.com'
+ layout 'mailer'
+end
diff --git a/e2e/ruby/rails/demo-rails70/app/models/application_record.rb b/e2e/ruby/rails/demo-rails70/app/models/application_record.rb
new file mode 100644
index 0000000000..08dc537989
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/models/application_record.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ApplicationRecord < ActiveRecord::Base
+ primary_abstract_class
+end
diff --git a/e2e/ruby/rails/demo-rails70/app/models/concerns/.keep b/e2e/ruby/rails/demo-rails70/app/models/concerns/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/e2e/ruby/rails/demo-rails70/app/models/lazy_ld_client.rb b/e2e/ruby/rails/demo-rails70/app/models/lazy_ld_client.rb
new file mode 100644
index 0000000000..dc70f01581
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/models/lazy_ld_client.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+# Mirrors an application that creates the LaunchDarkly client lazily — e.g. from
+# a model on first use — instead of during boot in a config/initializer. Enabled
+# with LD_LAZY_INIT=1 (see config/initializers/launchdarkly.rb) and exercised by
+# the lazy-init e2e test, which verifies the Railtie installs OpenTelemetry
+# auto-instrumentation at boot even though the client is created afterward.
+class LazyLdClient
+ def self.instance
+ @instance ||= begin
+ plugin = LaunchDarklyObservability::Plugin.new(
+ otlp_endpoint: ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT', LaunchDarklyObservability::DEFAULT_ENDPOINT),
+ service_name: 'rails-demo-app',
+ service_version: '1.0.0'
+ )
+ config = LaunchDarkly::Config.new(plugins: [plugin])
+ LaunchDarkly::LDClient.new(ENV.fetch('LAUNCHDARKLY_SDK_KEY', ''), config)
+ end
+ end
+end
diff --git a/e2e/ruby/rails/demo-rails70/app/models/trace.rb b/e2e/ruby/rails/demo-rails70/app/models/trace.rb
new file mode 100644
index 0000000000..7b3b40ba9f
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/models/trace.rb
@@ -0,0 +1,2 @@
+class Trace < ApplicationRecord
+end
diff --git a/e2e/ruby/rails/demo-rails70/app/views/flags/index.html.erb b/e2e/ruby/rails/demo-rails70/app/views/flags/index.html.erb
new file mode 100644
index 0000000000..7ded0c08a6
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/views/flags/index.html.erb
@@ -0,0 +1,86 @@
+
Feature Flags
+
+ Each flag evaluation is automatically traced via OpenTelemetry.
+
+
+
+
+ -
+ Connection
+ <% if @all_flags_valid %>
+ Connected
+ <% else %>
+ Not connected
+ <% end %>
+
+ -
+ Total flags
+ <%= @evaluations.size %>
+
+
+
+
+<% if @evaluations.any? %>
+ All Flag Evaluations
+
+
+
+
+ | Flag Key |
+ Value |
+ Variation Index |
+ Reason |
+
+
+
+ <% @evaluations.each do |key, detail| %>
+
+ <%= key %> |
+ <%= detail.value.inspect %> |
+ <%= detail.variation_index %> |
+ <%= detail.reason&.kind %> |
+
+ <% end %>
+
+
+<% else %>
+
+
No flags available. Make sure LAUNCHDARKLY_SDK_KEY is set correctly.
+
+<% end %>
+
+Test Endpoints
+
+
+
GET Endpoints
+
+ - GET /flags.json — All flag evaluations as JSON
+ - GET /flags/all_flags — Raw flags state
+ - GET /flags/:flag_key — Evaluate a specific flag
+
+
+
POST Endpoints
+
# Evaluate a specific flag with custom context
+POST /flags/evaluate
+{
+ "flag_key": "your-flag-key",
+ "context": { "key": "user-123", "kind": "user" }
+}
+
+# Batch evaluate multiple flags
+POST /flags/batch
+{
+ "flag_keys": ["flag-1", "flag-2", "flag-3"]
+}
+
+
+Trace Information
+
+
+
+ -
+ Trace ID
+
<%= launchdarkly_trace_id || 'N/A' %>
+
+
+
diff --git a/e2e/ruby/rails/demo-rails70/app/views/flags/show.html.erb b/e2e/ruby/rails/demo-rails70/app/views/flags/show.html.erb
new file mode 100644
index 0000000000..9f59278b17
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/views/flags/show.html.erb
@@ -0,0 +1,30 @@
+Flag Evaluation
+
+
+
+
Flag Key
+ <%= @flag_key %>
+
+
+
+
Value
+ <%= @detail.value.inspect %>
+
+
+
+
Variation Index
+
<%= @detail.variation_index || 'N/A' %>
+
+
+ <% if @detail.reason %>
+
+
Reason
+
<%= JSON.pretty_generate(JSON.parse(@detail.reason.to_json)) %>
+
+ <% end %>
+
+
+
+ <%= link_to "← Back to All Flags".html_safe, flags_path, class: "btn btn-secondary" %>
+ <%= link_to "View JSON", flag_path(@flag_key, format: :json), class: "btn btn-primary", target: "_blank" %>
+
diff --git a/e2e/ruby/rails/demo-rails70/app/views/layouts/application.html.erb b/e2e/ruby/rails/demo-rails70/app/views/layouts/application.html.erb
new file mode 100644
index 0000000000..ba8801adea
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/views/layouts/application.html.erb
@@ -0,0 +1,35 @@
+
+
+
+ LaunchDarkly Observability Ruby Demo
+
+ <%= csrf_meta_tags %>
+ <%= csp_meta_tag %>
+ <%= launchdarkly_traceparent_meta_tag %>
+
+ <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
+ <%= javascript_importmap_tags %>
+
+
+
+
+
+
+ <%= yield %>
+
+
+
+
+
diff --git a/e2e/ruby/rails/demo-rails70/app/views/layouts/mailer.html.erb b/e2e/ruby/rails/demo-rails70/app/views/layouts/mailer.html.erb
new file mode 100644
index 0000000000..cbd34d2e9d
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/views/layouts/mailer.html.erb
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+ <%= yield %>
+
+
diff --git a/e2e/ruby/rails/demo-rails70/app/views/layouts/mailer.text.erb b/e2e/ruby/rails/demo-rails70/app/views/layouts/mailer.text.erb
new file mode 100644
index 0000000000..37f0bddbd7
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/views/layouts/mailer.text.erb
@@ -0,0 +1 @@
+<%= yield %>
diff --git a/e2e/ruby/rails/demo-rails70/app/views/pages/home.html.erb b/e2e/ruby/rails/demo-rails70/app/views/pages/home.html.erb
new file mode 100644
index 0000000000..a9e1f7530a
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/app/views/pages/home.html.erb
@@ -0,0 +1,114 @@
+Ruby Observability Demo
+
+ Connected to LaunchDarkly. Each flag evaluation creates an OpenTelemetry span.
+
+
+
+
Feature Flags
+
+
+ -
+ Status
+ <% if @flags_valid %>
+ Connected
+ <% else %>
+ Not connected
+ <% end %>
+
+ -
+ Total flags
+ <%= @flag_count %>
+
+ -
+ Context key
+
<%= @context.key %>
+
+
+
+ <% if @sample_evaluations.any? %>
+
Sample Flag Values
+
+
+
+ | Flag Key |
+ Value |
+
+
+
+ <% @sample_evaluations.each do |key, value| %>
+
+ <%= key %> |
+ <%= value.inspect %> |
+
+ <% end %>
+
+
+ <% else %>
+
No flags available. Make sure LAUNCHDARKLY_SDK_KEY is set.
+ <% end %>
+
+
View all flags →
+
+
+Observability Actions
+
+ Trigger traces, logs, and errors to verify OpenTelemetry instrumentation.
+
+
+
+
+
Traces
+
Create spans with nested operations and custom attributes.
+
+ <%= button_to 'Create Trace', traces_path, method: :post, data: { turbo_frame: "trace_feedback" } %>
+ <%= button_to 'Create Trace (Controller)', create_with_helper_traces_path, method: :post, data: { turbo_frame: "trace_feedback" } %>
+
+ <%= turbo_frame_tag "trace_feedback" %>
+
+
+
+
Logs
+
Emit log entries at different levels through Rails logger.
+
+ <%= button_to 'Info', logs_path, method: :post, data: { turbo_frame: "log_feedback" } %>
+ <%= button_to 'Info (Hash)', create_with_hash_logs_path, method: :post, data: { turbo_frame: "log_feedback" } %>
+ <%= button_to 'Warn', create_warn_logs_path, method: :post, data: { turbo_frame: "log_feedback" } %>
+ <%= button_to 'Error', create_error_logs_path, method: :post, data: { turbo_frame: "log_feedback" } %>
+
+ <%= turbo_frame_tag "log_feedback" %>
+
+
+
+
Errors
+
Record exceptions via module and controller helpers.
+
+ <%= button_to 'Record (Module)', errors_path, method: :post, data: { turbo_frame: "error_feedback" } %>
+ <%= button_to 'Record (Controller)', create_with_helper_errors_path, method: :post, data: { turbo_frame: "error_feedback" } %>
+
+ <%= turbo_frame_tag "error_feedback" %>
+
+
+
+
Flush
+
Force-flush all pending telemetry to the collector.
+
+ <%= button_to 'Flush Telemetry', flush_telemetry_path, method: :post, data: { turbo_frame: "flush_feedback" } %>
+
+ <%= turbo_frame_tag "flush_feedback" %>
+
+
+
+HTTP Request
+
+
Auto-instrumented outbound HTTP call via Net::HTTP
+
+ -
+ URL
+
<%= @http_url %>
+
+ -
+ Response
+ <%= @http_status %>
+
+
+
diff --git a/e2e/ruby/rails/demo-rails70/bin/bundle b/e2e/ruby/rails/demo-rails70/bin/bundle
new file mode 100755
index 0000000000..ef688ecc73
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/bin/bundle
@@ -0,0 +1,113 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+#
+# This file was generated by Bundler.
+#
+# The application 'bundle' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'rubygems'
+
+m = Module.new do
+ module_function
+
+ def invoked_as_script?
+ File.expand_path($PROGRAM_NAME) == File.expand_path(__FILE__)
+ end
+
+ def env_var_version
+ ENV['BUNDLER_VERSION']
+ end
+
+ def cli_arg_version
+ return unless invoked_as_script? # don't want to hijack other binstubs
+ return unless 'update'.start_with?(ARGV.first || ' ') # must be running `bundle update`
+
+ bundler_version = nil
+ update_index = nil
+ ARGV.each_with_index do |a, i|
+ bundler_version = a if update_index && update_index.succ == i && a.match?(Gem::Version::ANCHORED_VERSION_PATTERN)
+ next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
+
+ bundler_version = Regexp.last_match(1)
+ update_index = i
+ end
+ bundler_version
+ end
+
+ def gemfile
+ gemfile = ENV['BUNDLE_GEMFILE']
+ return gemfile if gemfile && !gemfile.empty?
+
+ File.expand_path('../Gemfile', __dir__)
+ end
+
+ def lockfile
+ lockfile =
+ case File.basename(gemfile)
+ when 'gems.rb' then gemfile.sub(/\.rb$/, '.locked')
+ else "#{gemfile}.lock"
+ end
+ File.expand_path(lockfile)
+ end
+
+ def lockfile_version
+ return unless File.file?(lockfile)
+
+ lockfile_contents = File.read(lockfile)
+ return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
+
+ Regexp.last_match(1)
+ end
+
+ def bundler_requirement
+ @bundler_requirement ||=
+ env_var_version ||
+ cli_arg_version ||
+ bundler_requirement_for(lockfile_version)
+ end
+
+ def bundler_requirement_for(version)
+ return "#{Gem::Requirement.default}.a" unless version
+
+ bundler_gem_version = Gem::Version.new(version)
+
+ bundler_gem_version.approximate_recommendation
+ end
+
+ def load_bundler!
+ ENV['BUNDLE_GEMFILE'] ||= gemfile
+
+ activate_bundler
+ end
+
+ def activate_bundler
+ gem_error = activation_error_handling do
+ gem 'bundler', bundler_requirement
+ end
+ return if gem_error.nil?
+
+ require_error = activation_error_handling do
+ require 'bundler/version'
+ end
+ if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION))
+ return
+ end
+
+ warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`"
+ exit 42
+ end
+
+ def activation_error_handling
+ yield
+ nil
+ rescue StandardError, LoadError => e
+ e
+ end
+end
+
+m.load_bundler!
+
+load Gem.bin_path('bundler', 'bundle') if m.invoked_as_script?
diff --git a/e2e/ruby/rails/demo-rails70/bin/importmap b/e2e/ruby/rails/demo-rails70/bin/importmap
new file mode 100755
index 0000000000..d4238647bb
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/bin/importmap
@@ -0,0 +1,5 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require_relative '../config/application'
+require 'importmap/commands'
diff --git a/e2e/ruby/rails/demo-rails70/bin/rails b/e2e/ruby/rails/demo-rails70/bin/rails
new file mode 100755
index 0000000000..a31728ab97
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/bin/rails
@@ -0,0 +1,6 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+APP_PATH = File.expand_path('../config/application', __dir__)
+require_relative '../config/boot'
+require 'rails/commands'
diff --git a/e2e/ruby/rails/demo-rails70/bin/rake b/e2e/ruby/rails/demo-rails70/bin/rake
new file mode 100755
index 0000000000..c199955006
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/bin/rake
@@ -0,0 +1,6 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require_relative '../config/boot'
+require 'rake'
+Rake.application.run
diff --git a/e2e/ruby/rails/demo-rails70/bin/setup b/e2e/ruby/rails/demo-rails70/bin/setup
new file mode 100755
index 0000000000..516b651e39
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/bin/setup
@@ -0,0 +1,35 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require 'fileutils'
+
+# path to your application root.
+APP_ROOT = File.expand_path('..', __dir__)
+
+def system!(*args)
+ system(*args) || abort("\n== Command #{args} failed ==")
+end
+
+FileUtils.chdir APP_ROOT do
+ # This script is a way to set up or update your development environment automatically.
+ # This script is idempotent, so that you can run it at any time and get an expectable outcome.
+ # Add necessary setup steps to this file.
+
+ puts '== Installing dependencies =='
+ system! 'gem install bundler --conservative'
+ system('bundle check') || system!('bundle install')
+
+ # puts "\n== Copying sample files =="
+ # unless File.exist?("config/database.yml")
+ # FileUtils.cp "config/database.yml.sample", "config/database.yml"
+ # end
+
+ puts "\n== Preparing database =="
+ system! 'bin/rails db:prepare'
+
+ puts "\n== Removing old logs and tempfiles =="
+ system! 'bin/rails log:clear tmp:clear'
+
+ puts "\n== Restarting application server =="
+ system! 'bin/rails restart'
+end
diff --git a/e2e/ruby/rails/demo-rails70/config.ru b/e2e/ruby/rails/demo-rails70/config.ru
new file mode 100644
index 0000000000..6dc8321802
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/config.ru
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+# This file is used by Rack-based servers to start the application.
+
+require_relative 'config/environment'
+
+run Rails.application
+Rails.application.load_server
diff --git a/e2e/ruby/rails/demo-rails70/config/application.rb b/e2e/ruby/rails/demo-rails70/config/application.rb
new file mode 100644
index 0000000000..7ffc784f68
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/config/application.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require_relative 'boot'
+
+require 'rails/all'
+
+# Require the gems listed in Gemfile, including any gems
+# you've limited to :test, :development, or :production.
+Bundler.require(*Rails.groups)
+
+module Demo
+ class Application < Rails::Application
+ # Initialize configuration defaults for originally generated Rails version.
+ config.load_defaults 7.0
+
+ # Configuration for the application, engines, and railties goes here.
+ #
+ # These settings can be overridden in specific environments using the files
+ # in config/environments, which are processed later.
+ #
+ # config.time_zone = "Central Time (US & Canada)"
+ # config.eager_load_paths << Rails.root.join("extras")
+ end
+end
diff --git a/e2e/ruby/rails/demo-rails70/config/boot.rb b/e2e/ruby/rails/demo-rails70/config/boot.rb
new file mode 100644
index 0000000000..18a50e22ca
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/config/boot.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+# Ruby 3.3 + Rails 7.0 boot fix: concurrent-ruby >= 1.3.5 dropped its implicit
+# `require 'logger'`, which makes Rails < 7.1 raise
+# "NameError: uninitialized constant Logger" during boot. Requiring it here (the
+# documented workaround) is unrelated to the instrumentation bug under test; it
+# just lets this Rails 7.0 app boot on the Ruby 3.3.4 toolchain.
+require 'logger'
+
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
+
+require 'bundler/setup' # Set up gems listed in the Gemfile.
+require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
diff --git a/e2e/ruby/rails/demo-rails70/config/cable.yml b/e2e/ruby/rails/demo-rails70/config/cable.yml
new file mode 100644
index 0000000000..9890b7388d
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/config/cable.yml
@@ -0,0 +1,11 @@
+development:
+ adapter: redis
+ url: redis://localhost:6379/1
+
+test:
+ adapter: test
+
+production:
+ adapter: redis
+ url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
+ channel_prefix: highlight_ruby_demo_production
diff --git a/e2e/ruby/rails/demo-rails70/config/database.yml b/e2e/ruby/rails/demo-rails70/config/database.yml
new file mode 100644
index 0000000000..06488320fe
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/config/database.yml
@@ -0,0 +1,25 @@
+# SQLite. Versions 3.8.0 and up are supported.
+# gem install sqlite3
+#
+# Ensure the SQLite 3 gem is defined in your Gemfile
+# gem "sqlite3"
+#
+default: &default
+ adapter: sqlite3
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+ timeout: 5000
+
+development:
+ <<: *default
+ database: db/development.sqlite3
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+ <<: *default
+ database: db/test.sqlite3
+
+production:
+ <<: *default
+ database: db/production.sqlite3
diff --git a/e2e/ruby/rails/demo-rails70/config/environment.rb b/e2e/ruby/rails/demo-rails70/config/environment.rb
new file mode 100644
index 0000000000..d5abe55806
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/config/environment.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+# Load the Rails application.
+require_relative 'application'
+
+# Initialize the Rails application.
+Rails.application.initialize!
diff --git a/e2e/ruby/rails/demo-rails70/config/environments/development.rb b/e2e/ruby/rails/demo-rails70/config/environments/development.rb
new file mode 100644
index 0000000000..84a57f4010
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/config/environments/development.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'active_support/core_ext/integer/time'
+
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # In the development environment your application's code is reloaded any time
+ # it changes. This slows down response time but is perfect for development
+ # since you don't have to restart the web server when you make code changes.
+ config.cache_classes = false
+
+ # Do not eager load code on boot.
+ config.eager_load = false
+
+ # Show full error reports.
+ config.consider_all_requests_local = true
+
+ # Enable server timing
+ config.server_timing = true
+
+ # Enable/disable caching. By default caching is disabled.
+ # Run rails dev:cache to toggle caching.
+ if Rails.root.join('tmp/caching-dev.txt').exist?
+ config.action_controller.perform_caching = true
+ config.action_controller.enable_fragment_cache_logging = true
+
+ config.cache_store = :memory_store
+ config.public_file_server.headers = {
+ 'Cache-Control' => "public, max-age=#{2.days.to_i}"
+ }
+ else
+ config.action_controller.perform_caching = false
+
+ config.cache_store = :null_store
+ end
+
+ # Store uploaded files on the local file system (see config/storage.yml for options).
+ config.active_storage.service = :local
+
+ # Don't care if the mailer can't send.
+ config.action_mailer.raise_delivery_errors = false
+
+ config.action_mailer.perform_caching = false
+
+ # Print deprecation notices to the Rails logger.
+ config.active_support.deprecation = :log
+
+ # Raise exceptions for disallowed deprecations.
+ config.active_support.disallowed_deprecation = :raise
+
+ # Tell Active Support which deprecation messages to disallow.
+ config.active_support.disallowed_deprecation_warnings = []
+
+ # Raise an error on page load if there are pending migrations.
+ config.active_record.migration_error = :page_load
+
+ # Highlight code that triggered database queries in logs.
+ config.active_record.verbose_query_logs = true
+
+ # Suppress logger output for asset requests.
+ config.assets.quiet = true
+
+ # Raises error for missing translations.
+ # config.i18n.raise_on_missing_translations = true
+
+ # Annotate rendered view with file names.
+ # config.action_view.annotate_rendered_view_with_filenames = true
+
+ # Uncomment if you wish to allow Action Cable access from any origin.
+ # config.action_cable.disable_request_forgery_protection = true
+end
diff --git a/e2e/ruby/rails/demo-rails70/config/environments/production.rb b/e2e/ruby/rails/demo-rails70/config/environments/production.rb
new file mode 100644
index 0000000000..5d44d657a0
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/config/environments/production.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+require 'active_support/core_ext/integer/time'
+
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # Code is not reloaded between requests.
+ config.cache_classes = true
+
+ # Eager load code on boot. This eager loads most of Rails and
+ # your application in memory, allowing both threaded web servers
+ # and those relying on copy on write to perform better.
+ # Rake tasks automatically ignore this option for performance.
+ config.eager_load = true
+
+ # Full error reports are disabled and caching is turned on.
+ config.consider_all_requests_local = false
+ config.action_controller.perform_caching = true
+
+ # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
+ # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
+ # config.require_master_key = true
+
+ # Disable serving static files from the `/public` folder by default since
+ # Apache or NGINX already handles this.
+ config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
+
+ # Compress CSS using a preprocessor.
+ # config.assets.css_compressor = :sass
+
+ # Do not fallback to assets pipeline if a precompiled asset is missed.
+ config.assets.compile = false
+
+ # Enable serving of images, stylesheets, and JavaScripts from an asset server.
+ # config.asset_host = "http://assets.example.com"
+
+ # Specifies the header that your server uses for sending files.
+ # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache
+ # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX
+
+ # Store uploaded files on the local file system (see config/storage.yml for options).
+ config.active_storage.service = :local
+
+ # Mount Action Cable outside main process or domain.
+ # config.action_cable.mount_path = nil
+ # config.action_cable.url = "wss://example.com/cable"
+ # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ]
+
+ # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
+ # config.force_ssl = true
+
+ # Include generic and useful information about system operation, but avoid logging too much
+ # information to avoid inadvertent exposure of personally identifiable information (PII).
+ config.log_level = :info
+
+ # Prepend all log lines with the following tags.
+ config.log_tags = [:request_id]
+
+ # Use a different cache store in production.
+ # config.cache_store = :mem_cache_store
+
+ # Use a real queuing backend for Active Job (and separate queues per environment).
+ # config.active_job.queue_adapter = :resque
+ # config.active_job.queue_name_prefix = "highlight_ruby_demo_production"
+
+ config.action_mailer.perform_caching = false
+
+ # Ignore bad email addresses and do not raise email delivery errors.
+ # Set this to true and configure the email server for immediate delivery to raise delivery errors.
+ # config.action_mailer.raise_delivery_errors = false
+
+ # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
+ # the I18n.default_locale when a translation cannot be found).
+ config.i18n.fallbacks = true
+
+ # Don't log any deprecations.
+ config.active_support.report_deprecations = false
+
+ # Use default logging formatter so that PID and timestamp are not suppressed.
+ config.log_formatter = ::Logger::Formatter.new
+
+ # Use a different logger for distributed setups.
+ # require "syslog/logger"
+ # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name")
+
+ if ENV['RAILS_LOG_TO_STDOUT'].present?
+ logger = ActiveSupport::Logger.new($stdout)
+ logger.formatter = config.log_formatter
+ config.logger = ActiveSupport::TaggedLogging.new(logger)
+ end
+
+ # Do not dump schema after migrations.
+ config.active_record.dump_schema_after_migration = false
+end
diff --git a/e2e/ruby/rails/demo-rails70/config/environments/test.rb b/e2e/ruby/rails/demo-rails70/config/environments/test.rb
new file mode 100644
index 0000000000..b8f9f0b906
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/config/environments/test.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'active_support/core_ext/integer/time'
+
+# The test environment is used exclusively to run your application's
+# test suite. You never need to work with it otherwise. Remember that
+# your test database is "scratch space" for the test suite and is wiped
+# and recreated between test runs. Don't rely on the data there!
+
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # Turn false under Spring and add config.action_view.cache_template_loading = true.
+ config.cache_classes = true
+
+ # Eager loading loads your whole application. When running a single test locally,
+ # this probably isn't necessary. It's a good idea to do in a continuous integration
+ # system, or in some way before deploying your code.
+ config.eager_load = ENV['CI'].present?
+
+ # Configure public file server for tests with Cache-Control for performance.
+ config.public_file_server.enabled = true
+ config.public_file_server.headers = {
+ 'Cache-Control' => "public, max-age=#{1.hour.to_i}"
+ }
+
+ # Show full error reports and disable caching.
+ config.consider_all_requests_local = true
+ config.action_controller.perform_caching = false
+ config.cache_store = :null_store
+
+ # Raise exceptions instead of rendering exception templates.
+ config.action_dispatch.show_exceptions = :none
+
+ # Disable request forgery protection in test environment.
+ config.action_controller.allow_forgery_protection = false
+
+ # Store uploaded files on the local file system in a temporary directory.
+ config.active_storage.service = :test
+
+ config.action_mailer.perform_caching = false
+
+ # Tell Action Mailer not to deliver emails to the real world.
+ # The :test delivery method accumulates sent emails in the
+ # ActionMailer::Base.deliveries array.
+ config.action_mailer.delivery_method = :test
+
+ # Print deprecation notices to the stderr.
+ config.active_support.deprecation = :stderr
+
+ # Raise exceptions for disallowed deprecations.
+ config.active_support.disallowed_deprecation = :raise
+
+ # Tell Active Support which deprecation messages to disallow.
+ config.active_support.disallowed_deprecation_warnings = []
+
+ # Raises error for missing translations.
+ # config.i18n.raise_on_missing_translations = true
+
+ # Annotate rendered view with file names.
+ # config.action_view.annotate_rendered_view_with_filenames = true
+end
diff --git a/e2e/ruby/rails/demo-rails70/config/importmap.rb b/e2e/ruby/rails/demo-rails70/config/importmap.rb
new file mode 100644
index 0000000000..15fd62707a
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/config/importmap.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+# Pin npm packages by running ./bin/importmap
+
+pin 'application'
+pin '@hotwired/turbo-rails', to: 'turbo.min.js'
+pin '@hotwired/stimulus', to: 'stimulus.min.js'
+pin '@hotwired/stimulus-loading', to: 'stimulus-loading.js'
+pin_all_from 'app/javascript/controllers', under: 'controllers'
diff --git a/e2e/ruby/rails/demo-rails70/config/initializers/assets.rb b/e2e/ruby/rails/demo-rails70/config/initializers/assets.rb
new file mode 100644
index 0000000000..bcafccdd33
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/config/initializers/assets.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# Be sure to restart your server when you modify this file.
+
+# Version of your assets, change this if you want to expire all your assets.
+Rails.application.config.assets.version = '1.0'
+
+# Add additional assets to the asset load path.
+# Rails.application.config.assets.paths << Emoji.images_path
+
+# Precompile additional assets.
+# application.js, application.css, and all non-JS/CSS in the app/assets
+# folder are already added.
+# Rails.application.config.assets.precompile += %w( admin.js admin.css )
diff --git a/e2e/ruby/rails/demo-rails70/config/initializers/content_security_policy.rb b/e2e/ruby/rails/demo-rails70/config/initializers/content_security_policy.rb
new file mode 100644
index 0000000000..53538c1498
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/config/initializers/content_security_policy.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+# Be sure to restart your server when you modify this file.
+
+# Define an application-wide content security policy.
+# See the Securing Rails Applications Guide for more information:
+# https://guides.rubyonrails.org/security.html#content-security-policy-header
+
+# Rails.application.configure do
+# config.content_security_policy do |policy|
+# policy.default_src :self, :https
+# policy.font_src :self, :https, :data
+# policy.img_src :self, :https, :data
+# policy.object_src :none
+# policy.script_src :self, :https
+# policy.style_src :self, :https
+# # Specify URI for violation reports
+# # policy.report_uri "/csp-violation-report-endpoint"
+# end
+#
+# # Generate session nonces for permitted importmap and inline scripts
+# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
+# config.content_security_policy_nonce_directives = %w(script-src)
+#
+# # Report violations without enforcing the policy.
+# # config.content_security_policy_report_only = true
+# end
diff --git a/e2e/ruby/rails/demo-rails70/config/initializers/filter_parameter_logging.rb b/e2e/ruby/rails/demo-rails70/config/initializers/filter_parameter_logging.rb
new file mode 100644
index 0000000000..3df77c5bee
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/config/initializers/filter_parameter_logging.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+# Be sure to restart your server when you modify this file.
+
+# Configure parameters to be filtered from the log file. Use this to limit dissemination of
+# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
+# notations and behaviors.
+Rails.application.config.filter_parameters += %i[
+ passw secret token _key crypt salt certificate otp ssn
+]
diff --git a/e2e/ruby/rails/demo-rails70/config/initializers/inflections.rb b/e2e/ruby/rails/demo-rails70/config/initializers/inflections.rb
new file mode 100644
index 0000000000..9e049dcc91
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/config/initializers/inflections.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+# Be sure to restart your server when you modify this file.
+
+# Add new inflection rules using the following format. Inflections
+# are locale specific, and you may define rules for as many different
+# locales as you wish. All of these examples are active by default:
+# ActiveSupport::Inflector.inflections(:en) do |inflect|
+# inflect.plural /^(ox)$/i, "\\1en"
+# inflect.singular /^(ox)en/i, "\\1"
+# inflect.irregular "person", "people"
+# inflect.uncountable %w( fish sheep )
+# end
+
+# These inflection rules are supported but not enabled by default:
+# ActiveSupport::Inflector.inflections(:en) do |inflect|
+# inflect.acronym "RESTful"
+# end
diff --git a/e2e/ruby/rails/demo-rails70/config/initializers/launchdarkly.rb b/e2e/ruby/rails/demo-rails70/config/initializers/launchdarkly.rb
new file mode 100644
index 0000000000..e67dce87fe
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/config/initializers/launchdarkly.rb
@@ -0,0 +1,22 @@
+require 'launchdarkly-server-sdk'
+require 'launchdarkly_observability'
+
+# Set LD_LAZY_INIT=1 to defer LaunchDarkly client creation until first use
+# (see app/models/lazy_ld_client.rb) instead of creating it here during boot. This
+# mirrors apps that build the client lazily — e.g. from a model on first request
+# — and exercises the Railtie's boot-time auto-instrumentation install path.
+unless ENV['LD_LAZY_INIT']
+ observability_plugin = LaunchDarklyObservability::Plugin.new(
+ otlp_endpoint: ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT', LaunchDarklyObservability::DEFAULT_ENDPOINT),
+ service_name: 'rails-demo-app',
+ service_version: '1.0.0'
+ )
+
+ sdk_key = ENV.fetch('LAUNCHDARKLY_SDK_KEY') do
+ Rails.logger.warn '[LaunchDarkly] LAUNCHDARKLY_SDK_KEY not set, client will not connect'
+ ''
+ end
+
+ config = LaunchDarkly::Config.new(plugins: [observability_plugin])
+ Rails.configuration.ld_client = LaunchDarkly::LDClient.new(sdk_key, config)
+end
diff --git a/e2e/ruby/rails/demo-rails70/config/initializers/permissions_policy.rb b/e2e/ruby/rails/demo-rails70/config/initializers/permissions_policy.rb
new file mode 100644
index 0000000000..810aadeb98
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/config/initializers/permissions_policy.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+# Define an application-wide HTTP permissions policy. For further
+# information see https://developers.google.com/web/updates/2018/06/feature-policy
+#
+# Rails.application.config.permissions_policy do |f|
+# f.camera :none
+# f.gyroscope :none
+# f.microphone :none
+# f.usb :none
+# f.fullscreen :self
+# f.payment :self, "https://secure.example.com"
+# end
diff --git a/e2e/ruby/rails/demo-rails70/config/locales/en.yml b/e2e/ruby/rails/demo-rails70/config/locales/en.yml
new file mode 100644
index 0000000000..ecf0a11878
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/config/locales/en.yml
@@ -0,0 +1,33 @@
+# Files in the config/locales directory are used for internationalization
+# and are automatically loaded by Rails. If you want to use locales other
+# than English, add the necessary files in this directory.
+#
+# To use the locales, use `I18n.t`:
+#
+# I18n.t "hello"
+#
+# In views, this is aliased to just `t`:
+#
+# <%= t("hello") %>
+#
+# To use a different locale, set it with `I18n.locale`:
+#
+# I18n.locale = :es
+#
+# This would use the information in config/locales/es.yml.
+#
+# The following keys must be escaped otherwise they will not be retrieved by
+# the default I18n backend:
+#
+# true, false, on, off, yes, no
+#
+# Instead, surround them with single quotes.
+#
+# en:
+# "true": "foo"
+#
+# To learn more, please read the Rails Internationalization guide
+# available at https://guides.rubyonrails.org/i18n.html.
+
+en:
+ hello: 'Hello world'
diff --git a/e2e/ruby/rails/demo-rails70/config/puma.rb b/e2e/ruby/rails/demo-rails70/config/puma.rb
new file mode 100644
index 0000000000..ffa06610fa
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/config/puma.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+# Puma can serve each request in a thread from an internal thread pool.
+# The `threads` method setting takes two numbers: a minimum and maximum.
+# Any libraries that use thread pools should be configured to match
+# the maximum value specified for Puma. Default is set to 5 threads for minimum
+# and maximum; this matches the default thread size of Active Record.
+#
+max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5)
+min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count }
+threads min_threads_count, max_threads_count
+
+# Specifies the `worker_timeout` threshold that Puma will use to wait before
+# terminating a worker in development environments.
+#
+worker_timeout 3600 if ENV.fetch('RAILS_ENV', 'development') == 'development'
+
+# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
+#
+port ENV.fetch('PORT', 2343)
+
+# Specifies the `environment` that Puma will run in.
+#
+environment ENV.fetch('RAILS_ENV', 'development')
+
+# Specifies the `pidfile` that Puma will use.
+pidfile ENV.fetch('PIDFILE', 'tmp/pids/server.pid')
+
+# Specifies the number of `workers` to boot in clustered mode.
+# Workers are forked web server processes. If using threads and workers together
+# the concurrency of the application would be max `threads` * `workers`.
+# Workers do not work on JRuby or Windows (both of which do not support
+# processes).
+#
+# workers ENV.fetch("WEB_CONCURRENCY") { 2 }
+
+# Use the `preload_app!` method when specifying a `workers` number.
+# This directive tells Puma to first boot the application and load code
+# before forking the application. This takes advantage of Copy On Write
+# process behavior so workers use less memory.
+#
+# preload_app!
+
+# Reinitialize LaunchDarkly client after forking workers
+# This is required when using workers (clustered mode)
+# on_worker_boot do
+# Rails.configuration.ld_client.postfork
+# end
+
+# Allow puma to be restarted by `bin/rails restart` command.
+plugin :tmp_restart
diff --git a/e2e/ruby/rails/demo-rails70/config/routes.rb b/e2e/ruby/rails/demo-rails70/config/routes.rb
new file mode 100644
index 0000000000..b1152efa9f
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/config/routes.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+Rails.application.routes.draw do
+ get 'pages/home'
+ resources :traces, only: [:create] do
+ post :create_with_helper, on: :collection
+ end
+ resources :logs, only: [:create] do
+ collection do
+ post :create_with_hash
+ post :create_warn
+ post :create_error
+ end
+ end
+ resources :errors, only: [:create] do
+ post :create_with_helper, on: :collection
+ end
+ post 'telemetry/flush', to: 'telemetry#flush', as: :flush_telemetry
+
+ # LaunchDarkly feature flag routes
+ resources :flags, only: %i[index show] do
+ collection do
+ post :evaluate
+ post :batch
+ get :all_flags
+ end
+ end
+
+ root to: 'pages#home'
+end
diff --git a/e2e/ruby/rails/demo-rails70/config/storage.yml b/e2e/ruby/rails/demo-rails70/config/storage.yml
new file mode 100644
index 0000000000..a0f8d3aa23
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/config/storage.yml
@@ -0,0 +1,33 @@
+test:
+ service: Disk
+ root: <%= Rails.root.join("tmp/storage") %>
+
+local:
+ service: Disk
+ root: <%= Rails.root.join("storage") %>
+# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
+# amazon:
+# service: S3
+# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
+# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
+# region: us-east-1
+# bucket: your_own_bucket-<%= Rails.env %>
+
+# Remember not to checkin your GCS keyfile to a repository
+# google:
+# service: GCS
+# project: your_project
+# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
+# bucket: your_own_bucket-<%= Rails.env %>
+
+# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
+# microsoft:
+# service: AzureStorage
+# storage_account_name: your_account_name
+# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
+# container: your_container_name-<%= Rails.env %>
+
+# mirror:
+# service: Mirror
+# primary: local
+# mirrors: [ amazon, google, microsoft ]
diff --git a/e2e/ruby/rails/demo-rails70/db/migrate/20240829164231_create_traces.rb b/e2e/ruby/rails/demo-rails70/db/migrate/20240829164231_create_traces.rb
new file mode 100644
index 0000000000..7a09c21362
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/db/migrate/20240829164231_create_traces.rb
@@ -0,0 +1,10 @@
+class CreateTraces < ActiveRecord::Migration[7.0]
+ def change
+ create_table :traces do |t|
+ t.string :name
+ t.string :kind
+
+ t.timestamps
+ end
+ end
+end
diff --git a/e2e/ruby/rails/demo-rails70/db/schema.rb b/e2e/ruby/rails/demo-rails70/db/schema.rb
new file mode 100644
index 0000000000..ab6ec88dca
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/db/schema.rb
@@ -0,0 +1,21 @@
+# This file is auto-generated from the current state of the database. Instead
+# of editing this file, please use the migrations feature of Active Record to
+# incrementally modify your database, and then regenerate this schema definition.
+#
+# This file is the source Rails uses to define your schema when running `bin/rails
+# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
+# be faster and is potentially less error prone than running all of your
+# migrations from scratch. Old migrations may fail to apply correctly if those
+# migrations use external dependencies or application code.
+#
+# It's strongly recommended that you check this file into your version control system.
+
+ActiveRecord::Schema[7.0].define(version: 2024_08_29_164231) do
+ create_table "traces", force: :cascade do |t|
+ t.string "name"
+ t.string "kind"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+end
diff --git a/e2e/ruby/rails/demo-rails70/db/seeds.rb b/e2e/ruby/rails/demo-rails70/db/seeds.rb
new file mode 100644
index 0000000000..6533326431
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/db/seeds.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+# This file should contain all the record creation needed to seed the database with its default values.
+# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup).
+#
+# Examples:
+#
+# movies = Movie.create([{ name: "Star Wars" }, { name: "Lord of the Rings" }])
+# Character.create(name: "Luke", movie: movies.first)
diff --git a/e2e/ruby/rails/demo-rails70/lib/assets/.keep b/e2e/ruby/rails/demo-rails70/lib/assets/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/e2e/ruby/rails/demo-rails70/lib/tasks/.keep b/e2e/ruby/rails/demo-rails70/lib/tasks/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/e2e/ruby/rails/demo-rails70/public/404.html b/e2e/ruby/rails/demo-rails70/public/404.html
new file mode 100644
index 0000000000..7e25f8ae14
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/public/404.html
@@ -0,0 +1,73 @@
+
+
+
+ The page you were looking for doesn't exist (404)
+
+
+
+
+
+
+
+
+
The page you were looking for doesn't exist.
+
+ You may have mistyped the address or the page may have
+ moved.
+
+
+
+ If you are the application owner check the logs for more
+ information.
+
+
+
+
diff --git a/e2e/ruby/rails/demo-rails70/public/422.html b/e2e/ruby/rails/demo-rails70/public/422.html
new file mode 100644
index 0000000000..d7a5b95445
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/public/422.html
@@ -0,0 +1,73 @@
+
+
+
+ The change you wanted was rejected (422)
+
+
+
+
+
+
+
+
+
The change you wanted was rejected.
+
+ Maybe you tried to change something you didn't have access
+ to.
+
+
+
+ If you are the application owner check the logs for more
+ information.
+
+
+
+
diff --git a/e2e/ruby/rails/demo-rails70/public/500.html b/e2e/ruby/rails/demo-rails70/public/500.html
new file mode 100644
index 0000000000..af124c77cc
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/public/500.html
@@ -0,0 +1,69 @@
+
+
+
+ We're sorry, but something went wrong (500)
+
+
+
+
+
+
+
+
+
We're sorry, but something went wrong.
+
+
+ If you are the application owner check the logs for more
+ information.
+
+
+
+
diff --git a/e2e/ruby/rails/demo-rails70/public/apple-touch-icon-precomposed.png b/e2e/ruby/rails/demo-rails70/public/apple-touch-icon-precomposed.png
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/e2e/ruby/rails/demo-rails70/public/apple-touch-icon.png b/e2e/ruby/rails/demo-rails70/public/apple-touch-icon.png
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/e2e/ruby/rails/demo-rails70/public/favicon.ico b/e2e/ruby/rails/demo-rails70/public/favicon.ico
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/e2e/ruby/rails/demo-rails70/public/robots.txt b/e2e/ruby/rails/demo-rails70/public/robots.txt
new file mode 100644
index 0000000000..c19f78ab68
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/public/robots.txt
@@ -0,0 +1 @@
+# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
diff --git a/e2e/ruby/rails/demo-rails70/storage/.keep b/e2e/ruby/rails/demo-rails70/storage/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/e2e/ruby/rails/demo-rails70/test/application_system_test_case.rb b/e2e/ruby/rails/demo-rails70/test/application_system_test_case.rb
new file mode 100644
index 0000000000..652febbd68
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/test/application_system_test_case.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
+ driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
+end
diff --git a/e2e/ruby/rails/demo-rails70/test/channels/application_cable/connection_test.rb b/e2e/ruby/rails/demo-rails70/test/channels/application_cable/connection_test.rb
new file mode 100644
index 0000000000..4aee9b3353
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/test/channels/application_cable/connection_test.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+module ApplicationCable
+ class ConnectionTest < ActionCable::Connection::TestCase
+ # test "connects with cookies" do
+ # cookies.signed[:user_id] = 42
+ #
+ # connect
+ #
+ # assert_equal connection.user_id, "42"
+ # end
+ end
+end
diff --git a/e2e/ruby/rails/demo-rails70/test/controllers/.keep b/e2e/ruby/rails/demo-rails70/test/controllers/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/e2e/ruby/rails/demo-rails70/test/controllers/pages_controller_test.rb b/e2e/ruby/rails/demo-rails70/test/controllers/pages_controller_test.rb
new file mode 100644
index 0000000000..819f8c03e9
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/test/controllers/pages_controller_test.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class PagesControllerTest < ActionDispatch::IntegrationTest
+ test 'should get home' do
+ get pages_home_url
+ assert_response :success
+ end
+end
diff --git a/e2e/ruby/rails/demo-rails70/test/fixtures/files/.keep b/e2e/ruby/rails/demo-rails70/test/fixtures/files/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/e2e/ruby/rails/demo-rails70/test/helpers/.keep b/e2e/ruby/rails/demo-rails70/test/helpers/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/e2e/ruby/rails/demo-rails70/test/integration/.keep b/e2e/ruby/rails/demo-rails70/test/integration/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/e2e/ruby/rails/demo-rails70/test/integration/lazy_init_instrumentation_test.rb b/e2e/ruby/rails/demo-rails70/test/integration/lazy_init_instrumentation_test.rb
new file mode 100644
index 0000000000..466e3c6854
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/test/integration/lazy_init_instrumentation_test.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+require 'open3'
+
+# Regression test for lazy LaunchDarkly client initialization.
+#
+# When the client is created lazily (after Rails has booted) rather than in a
+# config/initializer, the OTel Rails-family instrumentations used to report
+# "Instrumentation: ... failed to install" — their ActiveSupport.on_load hooks
+# had already fired by the time the plugin configured OpenTelemetry. The gem's
+# Railtie now installs auto-instrumentation during boot, independent of when the
+# client is created, so it attaches regardless.
+#
+# This must run in a SEPARATE process because instrumentation install is a
+# one-time, global side effect: the main test suite boots with the client
+# created during boot, so we boot a fresh Rails process with LD_LAZY_INIT=1 (no
+# client created at boot — see config/initializers/launchdarkly.rb) and assert
+# the Rails instrumentations are installed anyway.
+class LazyInitInstrumentationTest < ActiveSupport::TestCase
+ CHECK_SCRIPT = <<~'RUBY'
+ names = %w[Rack ActionPack ActiveRecord ActiveSupport Rails]
+ installed = names.all? do |n|
+ Object.const_get("OpenTelemetry::Instrumentation::#{n}::Instrumentation").instance.installed?
+ end
+ # Creating the client lazily (post-boot) must still work without raising.
+ LazyLdClient.instance
+ puts(installed ? 'LAZY_INSTRUMENTATION_OK' : 'LAZY_INSTRUMENTATION_FAILED')
+ RUBY
+
+ test 'rails auto-instrumentation installs at boot even when the client is created lazily' do
+ output = boot_lazy_and_run(CHECK_SCRIPT)
+
+ assert_includes output, 'LAZY_INSTRUMENTATION_OK',
+ "Rails auto-instrumentation should install at boot in lazy mode.\n--- subprocess output ---\n#{output}"
+ end
+
+ private
+
+ def boot_lazy_and_run(script)
+ env = {
+ 'LD_LAZY_INIT' => '1',
+ 'LAUNCHDARKLY_SDK_KEY' => 'sdk-test-0000000000000000000000',
+ 'RAILS_ENV' => 'test'
+ }
+ rails_bin = Rails.root.join('bin/rails').to_s
+ stdout, stderr, _status = Open3.capture3(env, rails_bin, 'runner', script, chdir: Rails.root.to_s)
+ "#{stdout}\n#{stderr}"
+ end
+end
diff --git a/e2e/ruby/rails/demo-rails70/test/integration/observability_instrumentation_test.rb b/e2e/ruby/rails/demo-rails70/test/integration/observability_instrumentation_test.rb
new file mode 100644
index 0000000000..9c79a41206
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/test/integration/observability_instrumentation_test.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+# End-to-end coverage for the LaunchDarkly observability plugin's Rails
+# auto-instrumentation.
+#
+# Background: the plugin configures OpenTelemetry from `Plugin#register`, which
+# runs when the LaunchDarkly client is created. In this app that happens in
+# `config/initializers/launchdarkly.rb` — i.e. DURING Rails boot — so the OTel
+# Rails-family instrumentations (Rack, ActionPack, ActiveRecord, ...) install
+# correctly. A customer who instead creates the client lazily AFTER boot sees
+# "Instrumentation: OpenTelemetry::Instrumentation::ActionPack failed to install"
+# because the ActiveSupport.on_load hooks those instrumentations rely on have
+# already fired. These tests pin the working boot-time behavior so a regression
+# (or a change that breaks instrumentation install) is caught in CI.
+class ObservabilityInstrumentationTest < ActionDispatch::IntegrationTest
+ # The Rails-family instrumentations that must attach during a boot-time init.
+ # These are exactly the ones that report "failed to install" on the lazy path.
+ RAILS_INSTRUMENTATIONS = %w[Rack ActionPack ActiveRecord ActiveSupport Rails].freeze
+
+ def instrumentation_instance(name)
+ Object.const_get("OpenTelemetry::Instrumentation::#{name}::Instrumentation").instance
+ end
+
+ test 'rails auto-instrumentation installed during boot' do
+ RAILS_INSTRUMENTATIONS.each do |name|
+ assert instrumentation_instance(name).installed?,
+ "#{name} instrumentation should be installed after a boot-time plugin init " \
+ '(it reports "failed to install" when the client is created lazily after boot)'
+ end
+ end
+
+ test 'http request produces a server span via the rack instrumentation' do
+ exporter = OpenTelemetry::SDK::Trace::Export::InMemorySpanExporter.new
+ processor = OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(exporter)
+ OpenTelemetry.tracer_provider.add_span_processor(processor)
+
+ get pages_home_url
+ assert_response :success
+
+ server_spans = exporter.finished_spans.select { |s| s.kind == :server }
+ refute_empty server_spans, 'expected an HTTP server span from the Rack/ActionPack instrumentation'
+ end
+end
diff --git a/e2e/ruby/rails/demo-rails70/test/integration/otlp_export_e2e_test.rb b/e2e/ruby/rails/demo-rails70/test/integration/otlp_export_e2e_test.rb
new file mode 100644
index 0000000000..8f22306243
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/test/integration/otlp_export_e2e_test.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+# End-to-end proof that the Rails 7.0 app EXPORTS telemetry over the real OTLP
+# protobuf pipeline to a local sink (test/support/otlp_sink.rb), which test_helper
+# points the exporter at via OTEL_EXPORTER_OTLP_ENDPOINT.
+#
+# This is the CardFlight repro's headline assertion. On Rails 7.0 with the
+# unpinned gem, the Rails-family OTel instrumentations "failed to install", so no
+# autoinstrumented HTTP *server* span is ever produced or exported — assertion
+# (1) below fails. After the gem is fixed to pin the Rails-family instrumentation
+# to a Rails-7.0-compatible version, the server span appears and the test passes.
+class OtlpExportE2ETest < ActionDispatch::IntegrationTest
+ # OTLP Span.kind for SERVER (proto enum symbol or its int value).
+ SERVER_KINDS = [:SPAN_KIND_SERVER, 2].freeze
+
+ def setup
+ OTLP_SINK.reset
+ end
+
+ test 'traces, a log, and a captured exception are exported to the OTLP sink' do
+ post traces_path # manual spans + an autoinstrumented HTTP server span
+ post logs_path # Rails.logger.info "hello, world! foo=bar"
+ post errors_path # 1 / 0 -> LaunchDarklyObservability.record_exception
+ flush_telemetry
+
+ # 1) TRACES — an autoinstrumented HTTP *server* span proves the Rails/Rack
+ # instrumentation attached. This is the assertion that is RED on Rails 7.0
+ # with the unpinned gem and GREEN after the fix.
+ assert wait_until { OTLP_SINK.spans.any? { |s| SERVER_KINDS.include?(s.kind) } },
+ 'expected an autoinstrumented HTTP server span at the OTLP sink ' \
+ "(got span names: #{OTLP_SINK.spans.map(&:name).inspect})"
+
+ # 2) LOGS — the info log reached the sink via the OTel log bridge.
+ assert wait_until { OTLP_SINK.logs.any? { |l| l.body.to_s.include?('hello, world! foo=bar') } },
+ "expected the info log at the OTLP sink (got log bodies: #{OTLP_SINK.logs.map(&:body).inspect})"
+
+ # 3) EXCEPTION — the ZeroDivisionError was recorded as an exception event.
+ exception_events = OTLP_SINK.spans.flat_map(&:events).select { |e| e[:name] == 'exception' }
+ refute_empty exception_events, 'expected a recorded exception event at the OTLP sink'
+ assert exception_events.any? { |e| e[:attributes]['exception.type'].to_s.include?('ZeroDivisionError') },
+ 'expected a ZeroDivisionError exception event ' \
+ "(got types: #{exception_events.map { |e| e[:attributes]['exception.type'] }.inspect})"
+ end
+
+ private
+
+ def flush_telemetry
+ OpenTelemetry.tracer_provider.force_flush
+ return unless OpenTelemetry.respond_to?(:logger_provider) &&
+ OpenTelemetry.logger_provider.respond_to?(:force_flush)
+
+ OpenTelemetry.logger_provider.force_flush
+ end
+
+ # Poll until the block is truthy or the timeout elapses; returns the final value.
+ def wait_until(timeout: 5.0)
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
+ loop do
+ result = yield
+ return result if result
+ break if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
+
+ sleep 0.1
+ end
+ yield
+ end
+end
diff --git a/e2e/ruby/rails/demo-rails70/test/mailers/.keep b/e2e/ruby/rails/demo-rails70/test/mailers/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/e2e/ruby/rails/demo-rails70/test/models/.keep b/e2e/ruby/rails/demo-rails70/test/models/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/e2e/ruby/rails/demo-rails70/test/support/otlp_sink.rb b/e2e/ruby/rails/demo-rails70/test/support/otlp_sink.rb
new file mode 100644
index 0000000000..5bfce0d5c5
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/test/support/otlp_sink.rb
@@ -0,0 +1,151 @@
+# frozen_string_literal: true
+
+require 'webrick'
+require 'zlib'
+require 'stringio'
+
+# Load the OTLP protobuf message classes. These ship with the exporter gems the
+# plugin already depends on, so we can decode the EXACT bytes the Ruby OTLP
+# exporter puts on the wire.
+require 'opentelemetry-exporter-otlp'
+require 'opentelemetry-exporter-otlp-logs'
+
+# A minimal, in-process OTLP/HTTP sink used by the E2E tests to assert that the
+# Rails app actually EXPORTED telemetry over the wire.
+#
+# Why not e2e/mock-otel-server? That server parses JSON and gates reads on a
+# browser-only `highlight.session_id` attribute. The Ruby OTLP exporter sends
+# gzip-compressed binary protobuf with no session id, so it cannot be parsed
+# there. This sink decodes the real OTLP protobuf using the proto classes
+# shipped with the exporter gems — pure Ruby, no Docker/Node — so the whole
+# repro runs under `bundle exec rake`, identically locally and in CI.
+module OtlpSink
+ # A decoded span (only the fields the tests assert on).
+ Span = Struct.new(:name, :kind, :scope, :attributes, :events, keyword_init: true)
+ # A decoded log record.
+ LogRecord = Struct.new(:body, :severity, :attributes, keyword_init: true)
+
+ class Server
+ attr_reader :port
+
+ def initialize(port: 4327)
+ @port = port
+ @spans = []
+ @logs = []
+ @mutex = Mutex.new
+ @server = WEBrick::HTTPServer.new(
+ Port: port,
+ BindAddress: '127.0.0.1',
+ Logger: WEBrick::Log.new(File::NULL),
+ AccessLog: []
+ )
+ @server.mount_proc('/v1/traces') { |req, res| ingest_traces(req); ok(res) }
+ @server.mount_proc('/v1/logs') { |req, res| ingest_logs(req); ok(res) }
+ # Respond 200 to metrics so the exporter never sees an error, even though
+ # the tests do not assert on metrics.
+ @server.mount_proc('/v1/metrics') { |_req, res| ok(res) }
+ end
+
+ def start
+ @thread = Thread.new { @server.start }
+ self
+ end
+
+ def stop
+ @server.shutdown
+ @thread&.join(2)
+ end
+
+ def spans
+ @mutex.synchronize { @spans.dup }
+ end
+
+ def logs
+ @mutex.synchronize { @logs.dup }
+ end
+
+ def reset
+ @mutex.synchronize do
+ @spans.clear
+ @logs.clear
+ end
+ end
+
+ private
+
+ def ok(res)
+ res.status = 200
+ res.body = 'OK'
+ end
+
+ def body_bytes(req)
+ raw = req.body.to_s
+ if req['content-encoding'].to_s.include?('gzip')
+ Zlib::GzipReader.new(StringIO.new(raw)).read
+ else
+ raw
+ end
+ end
+
+ def ingest_traces(req)
+ req_msg = Opentelemetry::Proto::Collector::Trace::V1::ExportTraceServiceRequest.decode(body_bytes(req))
+ parsed = []
+ req_msg.resource_spans.each do |rs|
+ rs.scope_spans.each do |ss|
+ scope = ss.scope&.name
+ ss.spans.each do |s|
+ parsed << Span.new(
+ name: s.name,
+ kind: s.kind,
+ scope: scope,
+ attributes: kv(s.attributes),
+ events: s.events.map { |e| { name: e.name, attributes: kv(e.attributes) } }
+ )
+ end
+ end
+ end
+ @mutex.synchronize { @spans.concat(parsed) }
+ rescue StandardError => e
+ warn "[OtlpSink] trace decode error: #{e.class}: #{e.message}"
+ end
+
+ def ingest_logs(req)
+ req_msg = Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.decode(body_bytes(req))
+ parsed = []
+ req_msg.resource_logs.each do |rl|
+ rl.scope_logs.each do |sl|
+ sl.log_records.each do |lr|
+ parsed << LogRecord.new(
+ body: any_value(lr.body),
+ severity: lr.severity_text,
+ attributes: kv(lr.attributes)
+ )
+ end
+ end
+ end
+ @mutex.synchronize { @logs.concat(parsed) }
+ rescue StandardError => e
+ warn "[OtlpSink] log decode error: #{e.class}: #{e.message}"
+ end
+
+ # Flatten a repeated KeyValue list into a plain Ruby hash.
+ def kv(attributes)
+ attributes.each_with_object({}) do |a, h|
+ h[a.key] = any_value(a.value)
+ end
+ end
+
+ # Extract the set field of an OTLP AnyValue.
+ def any_value(value)
+ return nil if value.nil?
+
+ case value.value
+ when :string_value then value.string_value
+ when :bool_value then value.bool_value
+ when :int_value then value.int_value
+ when :double_value then value.double_value
+ else value.string_value
+ end
+ end
+ end
+end
diff --git a/e2e/ruby/rails/demo-rails70/test/system/.keep b/e2e/ruby/rails/demo-rails70/test/system/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/e2e/ruby/rails/demo-rails70/test/test_helper.rb b/e2e/ruby/rails/demo-rails70/test/test_helper.rb
new file mode 100644
index 0000000000..5fa624cf94
--- /dev/null
+++ b/e2e/ruby/rails/demo-rails70/test/test_helper.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+ENV['RAILS_ENV'] ||= 'test'
+
+# The observability plugin only configures OpenTelemetry (and installs the Rails
+# auto-instrumentation) when the LaunchDarkly client registers it, which requires
+# a non-empty SDK key. Set a dummy key BEFORE the app boots so the instrumentation
+# attaches during initialization. The key is invalid, so the client never connects
+# (background connection attempts fail gracefully and do not affect tests).
+ENV['LAUNCHDARKLY_SDK_KEY'] ||= 'sdk-test-0000000000000000000000'
+
+# Point the OTLP exporter at the in-process sink (test/support/otlp_sink.rb)
+# BEFORE the app boots — the plugin reads this when it builds the exporters at
+# boot. This keeps the E2E test fully self-contained: no external collector,
+# no network egress, no LaunchDarkly backend.
+OTLP_SINK_PORT = (ENV['OTLP_SINK_PORT'] || '4327').to_i
+ENV['OTEL_EXPORTER_OTLP_ENDPOINT'] ||= "http://127.0.0.1:#{OTLP_SINK_PORT}"
+
+require_relative '../config/environment'
+require 'rails/test_help'
+require_relative 'support/otlp_sink'
+
+# Start the sink once for the whole suite and tear it down at exit.
+OTLP_SINK = OtlpSink::Server.new(port: OTLP_SINK_PORT).start
+Minitest.after_run { OTLP_SINK.stop }
+
+module ActiveSupport
+ class TestCase
+ # Run tests in a single process: the OTLP sink binds a port in THIS process,
+ # so forked parallel workers would not share its collected telemetry.
+ parallelize(workers: 1)
+
+ # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
+ fixtures :all
+
+ # Add more helper methods to be used by all tests here...
+ end
+end
diff --git a/e2e/ruby/rails/demo-rails70/vendor/.keep b/e2e/ruby/rails/demo-rails70/vendor/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/e2e/ruby/rails/demo-rails70/vendor/javascript/.keep b/e2e/ruby/rails/demo-rails70/vendor/javascript/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/e2e/ruby/rails/demo/Gemfile.lock b/e2e/ruby/rails/demo/Gemfile.lock
index cbc9a2361e..cbe953e931 100644
--- a/e2e/ruby/rails/demo/Gemfile.lock
+++ b/e2e/ruby/rails/demo/Gemfile.lock
@@ -1,11 +1,29 @@
PATH
remote: ../../../../sdk/@launchdarkly/observability-ruby
specs:
- launchdarkly-observability (0.2.0)
+ launchdarkly-observability (0.2.1)
launchdarkly-server-sdk (>= 8.11.0)
opentelemetry-exporter-otlp (~> 0.28)
opentelemetry-exporter-otlp-logs (~> 0.1)
- opentelemetry-instrumentation-all (~> 0.62)
+ opentelemetry-instrumentation-action_mailer (< 0.8)
+ opentelemetry-instrumentation-action_pack (< 0.18)
+ opentelemetry-instrumentation-action_view (< 0.13)
+ opentelemetry-instrumentation-active_job (< 0.12)
+ opentelemetry-instrumentation-active_record (< 0.13)
+ opentelemetry-instrumentation-active_storage (< 0.5)
+ opentelemetry-instrumentation-active_support (< 0.12)
+ opentelemetry-instrumentation-concurrent_ruby (>= 0.21)
+ opentelemetry-instrumentation-faraday (>= 0.24)
+ opentelemetry-instrumentation-graphql (>= 0.28)
+ opentelemetry-instrumentation-http (>= 0.23)
+ opentelemetry-instrumentation-mysql2 (>= 0.28)
+ opentelemetry-instrumentation-net_http (>= 0.22)
+ opentelemetry-instrumentation-pg (>= 0.29)
+ opentelemetry-instrumentation-rack (>= 0.24)
+ opentelemetry-instrumentation-rails (>= 0.34, < 0.42)
+ opentelemetry-instrumentation-redis (>= 0.25)
+ opentelemetry-instrumentation-sidekiq (>= 0.25)
+ opentelemetry-instrumentation-sinatra (>= 0.24)
opentelemetry-logs-sdk (~> 0.1)
opentelemetry-sdk (~> 1.4)
opentelemetry-semantic_conventions (~> 1.10)
@@ -242,104 +260,32 @@ GEM
opentelemetry-helpers-sql-processor (0.5.0)
opentelemetry-api (~> 1.0)
opentelemetry-common (~> 0.21)
- opentelemetry-instrumentation-action_mailer (0.8.0)
+ opentelemetry-instrumentation-action_mailer (0.7.0)
opentelemetry-instrumentation-active_support (~> 0.10)
- opentelemetry-instrumentation-action_pack (0.18.0)
+ opentelemetry-instrumentation-action_pack (0.17.0)
opentelemetry-instrumentation-rack (~> 0.29)
- opentelemetry-instrumentation-action_view (0.13.0)
+ opentelemetry-instrumentation-action_view (0.12.0)
opentelemetry-instrumentation-active_support (~> 0.10)
- opentelemetry-instrumentation-active_job (0.12.0)
+ opentelemetry-instrumentation-active_job (0.11.0)
opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-active_model_serializers (0.25.0)
- opentelemetry-instrumentation-active_support (>= 0.7.0)
- opentelemetry-instrumentation-active_record (0.13.0)
+ opentelemetry-instrumentation-active_record (0.12.0)
opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-active_storage (0.5.0)
+ opentelemetry-instrumentation-active_storage (0.4.0)
opentelemetry-instrumentation-active_support (~> 0.10)
- opentelemetry-instrumentation-active_support (0.12.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-all (0.93.0)
- opentelemetry-instrumentation-active_model_serializers (~> 0.25.0)
- opentelemetry-instrumentation-anthropic (~> 0.5.0)
- opentelemetry-instrumentation-aws_lambda (~> 0.7.0)
- opentelemetry-instrumentation-aws_sdk (~> 0.12.0)
- opentelemetry-instrumentation-bunny (~> 0.25.0)
- opentelemetry-instrumentation-concurrent_ruby (~> 0.25.0)
- opentelemetry-instrumentation-dalli (~> 0.30.0)
- opentelemetry-instrumentation-delayed_job (~> 0.26.0)
- opentelemetry-instrumentation-ethon (~> 0.29.0)
- opentelemetry-instrumentation-excon (~> 0.29.0)
- opentelemetry-instrumentation-faraday (~> 0.33.0)
- opentelemetry-instrumentation-grape (~> 0.7.0)
- opentelemetry-instrumentation-graphql (~> 0.32.0)
- opentelemetry-instrumentation-grpc (~> 0.5.0)
- opentelemetry-instrumentation-gruf (~> 0.6.0)
- opentelemetry-instrumentation-http (~> 0.30.0)
- opentelemetry-instrumentation-http_client (~> 0.29.0)
- opentelemetry-instrumentation-httpx (~> 0.8.0)
- opentelemetry-instrumentation-koala (~> 0.24.0)
- opentelemetry-instrumentation-lmdb (~> 0.26.0)
- opentelemetry-instrumentation-mongo (~> 0.26.0)
- opentelemetry-instrumentation-mysql2 (~> 0.34.0)
- opentelemetry-instrumentation-net_http (~> 0.29.0)
- opentelemetry-instrumentation-pg (~> 0.36.0)
- opentelemetry-instrumentation-que (~> 0.13.0)
- opentelemetry-instrumentation-racecar (~> 0.7.0)
- opentelemetry-instrumentation-rack (~> 0.31.0)
- opentelemetry-instrumentation-rails (~> 0.42.0)
- opentelemetry-instrumentation-rake (~> 0.6.0)
- opentelemetry-instrumentation-rdkafka (~> 0.10.0)
- opentelemetry-instrumentation-redis (~> 0.29.0)
- opentelemetry-instrumentation-resque (~> 0.9.0)
- opentelemetry-instrumentation-restclient (~> 0.28.0)
- opentelemetry-instrumentation-ruby_kafka (~> 0.25.0)
- opentelemetry-instrumentation-sidekiq (~> 0.29.0)
- opentelemetry-instrumentation-sinatra (~> 0.30.0)
- opentelemetry-instrumentation-trilogy (~> 0.68.0)
- opentelemetry-instrumentation-anthropic (0.5.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-aws_lambda (0.7.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-aws_sdk (0.12.0)
+ opentelemetry-instrumentation-active_support (0.11.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-base (0.26.0)
opentelemetry-api (~> 1.7)
opentelemetry-common (~> 0.21)
opentelemetry-registry (~> 0.1)
- opentelemetry-instrumentation-bunny (0.25.0)
- opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-concurrent_ruby (0.25.0)
opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-dalli (0.30.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-delayed_job (0.26.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-ethon (0.29.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-excon (0.29.1)
- opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-faraday (0.33.0)
opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-grape (0.7.0)
- opentelemetry-instrumentation-rack (~> 0.29)
opentelemetry-instrumentation-graphql (0.32.0)
opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-grpc (0.5.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-gruf (0.6.0)
- opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-http (0.30.0)
opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-http_client (0.29.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-httpx (0.8.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-koala (0.24.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-lmdb (0.26.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-mongo (0.26.0)
- opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-mysql2 (0.34.0)
opentelemetry-helpers-mysql
opentelemetry-helpers-sql
@@ -351,13 +297,9 @@ GEM
opentelemetry-helpers-sql
opentelemetry-helpers-sql-processor
opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-que (0.13.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-racecar (0.7.0)
- opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-rack (0.31.0)
opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-rails (0.42.0)
+ opentelemetry-instrumentation-rails (0.41.0)
opentelemetry-instrumentation-action_mailer (~> 0.7)
opentelemetry-instrumentation-action_pack (~> 0.17)
opentelemetry-instrumentation-action_view (~> 0.12)
@@ -366,28 +308,12 @@ GEM
opentelemetry-instrumentation-active_storage (~> 0.4)
opentelemetry-instrumentation-active_support (~> 0.11)
opentelemetry-instrumentation-concurrent_ruby (~> 0.25)
- opentelemetry-instrumentation-rake (0.6.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-rdkafka (0.10.0)
- opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-redis (0.29.0)
opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-resque (0.9.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-restclient (0.28.0)
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-instrumentation-ruby_kafka (0.25.0)
- opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-sidekiq (0.29.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-sinatra (0.30.0)
opentelemetry-instrumentation-rack (~> 0.29)
- opentelemetry-instrumentation-trilogy (0.68.0)
- opentelemetry-helpers-mysql
- opentelemetry-helpers-sql
- opentelemetry-helpers-sql-processor
- opentelemetry-instrumentation-base (~> 0.25)
- opentelemetry-semantic_conventions (>= 1.8.0)
opentelemetry-logs-api (0.4.0)
opentelemetry-api (~> 1.0)
opentelemetry-logs-sdk (0.6.0)
diff --git a/sdk/@launchdarkly/observability-ruby/CHANGELOG.md b/sdk/@launchdarkly/observability-ruby/CHANGELOG.md
index 624331ac54..a42fe0f333 100644
--- a/sdk/@launchdarkly/observability-ruby/CHANGELOG.md
+++ b/sdk/@launchdarkly/observability-ruby/CHANGELOG.md
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## Unreleased
+
+### Bug Fixes
+
+* **ruby:** keep OpenTelemetry auto-instrumentation working on Rails 7.0. The plugin now depends on individual `opentelemetry-instrumentation-*` gems instead of `opentelemetry-instrumentation-all`, pinning the Rails-family instrumentations below the releases that require Rails 7.1 (so Rails 7.0 keeps working) while every other instrumentation tracks the latest. Instrumentations that cannot attach now produce a single actionable warning instead of a flurry of "failed to install" lines.
+
## [0.2.1](https://github.com/launchdarkly/observability-sdk/compare/launchdarkly-observability-ruby/0.2.0...launchdarkly-observability-ruby/0.2.1) (2026-06-02)
diff --git a/sdk/@launchdarkly/observability-ruby/README.md b/sdk/@launchdarkly/observability-ruby/README.md
index 3602ba3e80..a16574d38d 100644
--- a/sdk/@launchdarkly/observability-ruby/README.md
+++ b/sdk/@launchdarkly/observability-ruby/README.md
@@ -38,13 +38,43 @@ The gem includes everything needed for traces and logs out of the box:
- `launchdarkly-server-sdk` >= 8.11.0 (plugin support was added in 8.11.0)
- `opentelemetry-sdk` ~> 1.4
- `opentelemetry-exporter-otlp` ~> 0.28
-- `opentelemetry-instrumentation-all` ~> 0.62
- `opentelemetry-logs-sdk` ~> 0.1
- `opentelemetry-exporter-otlp-logs` ~> 0.1
+- A curated set of individual `opentelemetry-instrumentation-*` gems (see [Ruby & Rails compatibility](#ruby--rails-compatibility))
For metrics support (optional):
- `opentelemetry-metrics-sdk` ~> 0.1
+### Ruby & Rails compatibility
+
+| Component | Supported |
+|-----------|-----------|
+| Ruby | >= 3.0 |
+| Rails | >= 7.0 (auto-instrumentation); Rack / Sinatra / other Rack apps are unaffected by Rails version |
+
+The plugin depends on **individual** `opentelemetry-instrumentation-*` gems rather
+than the `opentelemetry-instrumentation-all` meta-gem. The meta-gem couples every
+instrumentation to a single version, so when the Rails-family instrumentations
+raised their minimum to Rails 7.1, Rails 7.0 apps silently lost *all*
+auto-instrumentation. Listing gems individually keeps the Rails family
+(`opentelemetry-instrumentation-rails`, `-action_pack`, `-active_record`,
+`-active_support`, `-action_view`, `-active_job`, `-action_mailer`,
+`-active_storage`) pinned below the releases that require Rails 7.1, while every
+other instrumentation tracks the latest version. Those pinned releases are still
+compatible with Rails 7.1+, so modern apps are unaffected.
+
+If an instrumentation cannot attach on your framework version, the plugin emits a
+**single actionable warning** (instead of a flurry of "failed to install" lines)
+and keeps everything else working — flag-eval spans, manual instrumentation, logs,
+and error capture are unaffected. You can add any other
+`opentelemetry-instrumentation-*` gem to your own Gemfile and it is picked up
+automatically (the plugin activates every instrumentation that is loaded), or pin
+one to a framework-compatible release, e.g.:
+
+```ruby
+gem 'opentelemetry-instrumentation-rails', '~> 0.41'
+```
+
## Quick Start
### Basic Usage (Non-Rails)
@@ -319,12 +349,17 @@ This generates:
By default, the plugin enables OpenTelemetry auto-instrumentation for common Ruby libraries:
-- **Rails**: Request tracing, route recognition
-- **ActiveRecord**: Database query tracing
-- **Net::HTTP**: Outbound HTTP request tracing
+- **Rails** (and the Action/Active family): Request tracing, route recognition, DB queries, view rendering, jobs
+- **Net::HTTP**, **HTTP**, **Faraday**: Outbound HTTP request tracing
- **Rack**: Request/response tracing
+- **PG**, **MySQL2**: Database query tracing
- **Redis**: Cache operation tracing
- **Sidekiq**: Background job tracing
+- **GraphQL**: Query/field tracing
+- **Sinatra**: Request tracing
+
+Need another library instrumented? Add its `opentelemetry-instrumentation-*` gem
+to your Gemfile — the plugin activates every loaded instrumentation automatically.
### Customizing Instrumentations
diff --git a/sdk/@launchdarkly/observability-ruby/launchdarkly-observability.gemspec b/sdk/@launchdarkly/observability-ruby/launchdarkly-observability.gemspec
index 15ee881f51..1a3533fdab 100644
--- a/sdk/@launchdarkly/observability-ruby/launchdarkly-observability.gemspec
+++ b/sdk/@launchdarkly/observability-ruby/launchdarkly-observability.gemspec
@@ -32,8 +32,49 @@ Gem::Specification.new do |spec|
# versions raise "uninitialized constant LaunchDarkly::Interfaces::Plugins" on require.
spec.add_dependency 'launchdarkly-server-sdk', '>= 8.11.0'
spec.add_dependency 'opentelemetry-exporter-otlp', '~> 0.28'
- spec.add_dependency 'opentelemetry-instrumentation-all', '~> 0.62'
spec.add_dependency 'opentelemetry-sdk', '~> 1.4'
+
+ # OpenTelemetry auto-instrumentation.
+ #
+ # We depend on INDIVIDUAL instrumentation gems instead of the
+ # opentelemetry-instrumentation-all meta-gem on purpose. The meta-gem couples
+ # every instrumentation to one version, so when the Rails-family
+ # instrumentations raised their floor to Rails 7.1
+ # (opentelemetry-instrumentation-rails 0.42.0), the whole bundle moved with
+ # them and Rails 7.0 apps silently lost ALL auto-instrumentation. Listing gems
+ # individually keeps the Rails family on a Rails-7.0-compatible release while
+ # everything else tracks the latest. See lib/launchdarkly_observability/
+ # instrumentations.rb.
+ #
+ # Rails family. Each of these gems independently enforces a Rails 7.1 floor in
+ # its latest release (the coordinated "Min Rails 7.1 enforced" bump), so the
+ # meta gem (opentelemetry-instrumentation-rails) is NOT enough — each member
+ # must be capped below its enforcing version to keep attaching on Rails 7.0.
+ # These releases are still compatible with Rails 7.1+, so modern apps are
+ # unaffected. Revisit when the plugin drops Rails 7.0 support.
+ spec.add_dependency 'opentelemetry-instrumentation-action_mailer', '< 0.8'
+ spec.add_dependency 'opentelemetry-instrumentation-action_pack', '< 0.18'
+ spec.add_dependency 'opentelemetry-instrumentation-action_view', '< 0.13'
+ spec.add_dependency 'opentelemetry-instrumentation-active_job', '< 0.12'
+ spec.add_dependency 'opentelemetry-instrumentation-active_record', '< 0.13'
+ spec.add_dependency 'opentelemetry-instrumentation-active_storage', '< 0.5'
+ spec.add_dependency 'opentelemetry-instrumentation-active_support', '< 0.12'
+ spec.add_dependency 'opentelemetry-instrumentation-rails', '>= 0.34', '< 0.42'
+
+ # Non-Rails instrumentations: latest. Consumers can add any other
+ # opentelemetry-instrumentation-* gem to their Gemfile and it is picked up
+ # automatically (the plugin activates every loaded instrumentation).
+ spec.add_dependency 'opentelemetry-instrumentation-concurrent_ruby', '>= 0.21'
+ spec.add_dependency 'opentelemetry-instrumentation-faraday', '>= 0.24'
+ spec.add_dependency 'opentelemetry-instrumentation-graphql', '>= 0.28'
+ spec.add_dependency 'opentelemetry-instrumentation-http', '>= 0.23'
+ spec.add_dependency 'opentelemetry-instrumentation-mysql2', '>= 0.28'
+ spec.add_dependency 'opentelemetry-instrumentation-net_http', '>= 0.22'
+ spec.add_dependency 'opentelemetry-instrumentation-pg', '>= 0.29'
+ spec.add_dependency 'opentelemetry-instrumentation-rack', '>= 0.24'
+ spec.add_dependency 'opentelemetry-instrumentation-redis', '>= 0.25'
+ spec.add_dependency 'opentelemetry-instrumentation-sidekiq', '>= 0.25'
+ spec.add_dependency 'opentelemetry-instrumentation-sinatra', '>= 0.24'
spec.add_dependency 'opentelemetry-semantic_conventions', '~> 1.10'
# Logs support (included by default for out-of-box DX; opt out via enable_logs: false)
diff --git a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability.rb b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability.rb
index 364dcd0914..6b9929c6da 100644
--- a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability.rb
+++ b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability.rb
@@ -2,9 +2,13 @@
require 'opentelemetry/sdk'
require 'opentelemetry/exporter/otlp'
-require 'opentelemetry/instrumentation/all'
require 'opentelemetry/semantic_conventions'
+# Loads the individual OpenTelemetry instrumentation gems (not the
+# opentelemetry-instrumentation-all meta-gem) so the Rails family can be pinned
+# for old-Rails compatibility independently of everything else.
+require_relative 'launchdarkly_observability/instrumentations'
+
require_relative 'launchdarkly_observability/version'
require_relative 'launchdarkly_observability/hook'
require_relative 'launchdarkly_observability/opentelemetry_config'
diff --git a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/instrumentation_log_filter.rb b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/instrumentation_log_filter.rb
new file mode 100644
index 0000000000..76611b52a5
--- /dev/null
+++ b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/instrumentation_log_filter.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+module LaunchDarklyObservability
+ # Wraps the OpenTelemetry logger to suppress the per-instrumentation install
+ # chatter ("... was successfully installed" / "... failed to install") and
+ # record the names of instrumentations that failed to install. Everything else
+ # (including level / level=) is delegated to the real logger.
+ #
+ # OpenTelemetryConfig uses this to replace the SDK's flurry of
+ # per-instrumentation warnings with a single actionable summary.
+ class InstrumentationLogFilter
+ FAILED_PATTERN = /Instrumentation: (\S+) failed to install/
+
+ # Run the block with OpenTelemetry.logger swapped for a filter that
+ # suppresses per-instrumentation install chatter, returning the names of any
+ # instrumentations that reported "failed to install". The SDK installs
+ # instrumentations after a configure block returns, so wrap the whole
+ # OpenTelemetry::SDK.configure call — not just use_all.
+ def self.capture_failures
+ original = OpenTelemetry.logger
+ failed = []
+ OpenTelemetry.logger = new(original, failed)
+ yield
+ failed
+ ensure
+ OpenTelemetry.logger = original
+ end
+
+ # Build ONE actionable warning naming the instrumentations that could not
+ # attach and how to resolve it. Telemetry that does not depend on those
+ # instrumentations (flag-eval spans, manual instrumentation, logs, errors)
+ # keeps working regardless.
+ def self.failure_warning(failed)
+ names = failed.map { |n| n.sub('OpenTelemetry::Instrumentation::', '') }.uniq
+ rails = defined?(::Rails) && ::Rails.respond_to?(:version) ? " on Rails #{::Rails.version}" : ''
+ "[LaunchDarklyObservability] #{names.size} OpenTelemetry instrumentation(s) could not attach" \
+ "#{rails} (Ruby #{RUBY_VERSION}): #{names.join(', ')}. Those libraries will not be " \
+ 'auto-instrumented; flag-eval spans, manual instrumentation, logs and error capture are ' \
+ 'unaffected. This usually means an instrumentation gem dropped support for your framework ' \
+ 'version — upgrade the framework, or pin the instrumentation gem to a compatible release ' \
+ '(e.g. gem "opentelemetry-instrumentation-rails", "~> 0.41").'
+ end
+
+ def initialize(delegate, failed)
+ @delegate = delegate
+ @failed = failed
+ end
+
+ # OTel logs installs via OpenTelemetry.logger.info / .warn, so intercept the
+ # level methods (not just #add) — otherwise the calls fall through to
+ # method_missing and bypass the filter.
+ %i[debug info warn error fatal unknown].each do |level|
+ define_method(level) do |message = nil, &block|
+ forward?(message || block&.call) ? @delegate.public_send(level, message, &block) : true
+ end
+ end
+
+ def add(severity, message = nil, progname = nil, &block)
+ forward?(message || progname || block&.call) ? @delegate.add(severity, message, progname, &block) : true
+ end
+
+ def method_missing(name, ...)
+ @delegate.send(name, ...)
+ end
+
+ def respond_to_missing?(name, include_private = false)
+ @delegate.respond_to?(name, include_private) || super
+ end
+
+ private
+
+ # Returns false when the message is install chatter that should be suppressed
+ # (recording failed-instrumentation names as a side effect), true when it
+ # should be forwarded to the real logger.
+ def forward?(message)
+ text = message.to_s
+ if (match = text.match(FAILED_PATTERN))
+ @failed << match[1]
+ return false
+ end
+
+ !text.include?('was successfully installed')
+ end
+ end
+end
diff --git a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/instrumentations.rb b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/instrumentations.rb
new file mode 100644
index 0000000000..919df28927
--- /dev/null
+++ b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/instrumentations.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+# Loads the OpenTelemetry auto-instrumentations the plugin enables by default.
+#
+# We require INDIVIDUAL instrumentation gems instead of
+# `opentelemetry/instrumentation/all` on purpose. The meta-gem couples every
+# instrumentation to a single version, so when the Rails-family instrumentations
+# raised their minimum to Rails 7.1 (opentelemetry-instrumentation-rails 0.42.0),
+# the whole bundle moved with them and Rails 7.0 apps silently lost ALL
+# auto-instrumentation. Requiring gems individually lets the Rails family stay on
+# a Rails-7.0-compatible release (pinned in the gemspec) while everything else
+# tracks the latest.
+#
+# `OpenTelemetry::SDK#use_all` activates every instrumentation that has been
+# loaded, so any additional `opentelemetry-instrumentation-*` gem a consumer adds
+# to their own Gemfile is picked up automatically alongside these defaults.
+
+# Rails family. Requiring this pulls action_pack, active_record, active_support,
+# action_view and active_job.
+require 'opentelemetry/instrumentation/rails'
+
+# Common non-Rails instrumentations (latest; see gemspec for version policy).
+# Note the require paths differ from gem names in places, e.g. the
+# opentelemetry-instrumentation-net_http gem is required as 'net/http'.
+%w[
+ concurrent_ruby
+ faraday
+ graphql
+ http
+ mysql2
+ net/http
+ pg
+ rack
+ redis
+ sidekiq
+ sinatra
+].each do |path|
+ require "opentelemetry/instrumentation/#{path}"
+rescue LoadError => e
+ # A default instrumentation gem is unexpectedly absent. Don't abort the whole
+ # plugin over one missing instrumentation; the rest still load.
+ warn "[LaunchDarklyObservability] optional instrumentation '#{path}' not loaded: #{e.message}"
+end
diff --git a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb
index 268418f3c6..3e96b8f659 100644
--- a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb
+++ b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/opentelemetry_config.rb
@@ -2,8 +2,9 @@
require 'opentelemetry/sdk'
require 'opentelemetry/exporter/otlp'
-require 'opentelemetry/instrumentation/all'
require 'opentelemetry/semantic_conventions'
+require_relative 'instrumentations'
+require_relative 'instrumentation_log_filter'
module LaunchDarklyObservability
# Configures OpenTelemetry SDK with appropriate providers and exporters
@@ -78,7 +79,7 @@ def configure
# #register / #configure — is created lazily afterward. Exporters are added
# later by #configure_traces when the client registers the plugin.
def install_instrumentation_only
- OpenTelemetry::SDK.configure do |c|
+ configure_sdk_capturing_failures do |c|
c.resource = create_resource
configure_instrumentations(c)
end
@@ -118,7 +119,7 @@ def configure_traces
return
end
- OpenTelemetry::SDK.configure do |c|
+ configure_sdk_capturing_failures do |c|
c.resource = create_resource
c.add_span_processor(create_batch_span_processor)
@@ -168,6 +169,20 @@ def configure_instrumentations(config)
warn "[LaunchDarklyObservability] Error configuring instrumentations: #{e.message}"
end
+ # Run an OpenTelemetry::SDK.configure block, replacing the SDK's
+ # per-instrumentation install logging (a flurry of "Instrumentation:
+ # failed to install" WARN lines when instrumentations are incompatible with
+ # the framework version — e.g. the Rails family below its Rails floor) with a
+ # single actionable summary. The SDK installs the instrumentations AFTER the
+ # configure block returns (use_all only queues them), so the filter wraps the
+ # whole call, not just use_all.
+ def configure_sdk_capturing_failures(&block)
+ failed = InstrumentationLogFilter.capture_failures do
+ OpenTelemetry::SDK.configure(&block)
+ end
+ warn InstrumentationLogFilter.failure_warning(failed) unless failed.empty?
+ end
+
# Configure OpenTelemetry logs with OTLP exporter.
# The log gems are runtime dependencies, so require should always succeed.
# If anything goes wrong, we warn once and leave traces unaffected.
diff --git a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/rails.rb b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/rails.rb
index 90045b7df4..c92b004751 100644
--- a/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/rails.rb
+++ b/sdk/@launchdarkly/observability-ruby/lib/launchdarkly_observability/rails.rb
@@ -136,8 +136,12 @@ def attach_otel_log_bridge
# file is still loading, so the module method does not exist yet and the
# delegation raised "undefined method `otel_logger_provider_available?'".
def otel_logger_provider_available?
- defined?(OpenTelemetry::SDK::Logs::LoggerProvider) &&
- OpenTelemetry.respond_to?(:logger_provider) &&
+ # `defined?` returns nil (not false) when the constant is absent, so
+ # guard first and return an explicit boolean — callers (and the
+ # railtie test) expect true/false, never nil.
+ return false unless defined?(OpenTelemetry::SDK::Logs::LoggerProvider)
+
+ OpenTelemetry.respond_to?(:logger_provider) &&
OpenTelemetry.logger_provider.is_a?(OpenTelemetry::SDK::Logs::LoggerProvider)
end
end