-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add initial Ruby OpenFeature integration #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
c698581
Add FeatBit OpenFeature provider
deleteLater a69cf80
Add console example for evaluating FeatBit flags
deleteLater 54239f4
updated console example
deleteLater 2ddd871
pr feedback
deleteLater 92bf49b
Use actions/checkout@v6
deleteLater f78ca41
Update .github/workflows/ci.yml
deleteLater File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| name: CI | ||
|
|
||
| on: | ||
| push: | ||
| pull_request: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| test: | ||
| runs-on: ${{ matrix.os }} | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| os: [ubuntu-latest, windows-latest] | ||
| ruby: ['3.4', '4.0'] | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| with: | ||
| persist-credentials: false | ||
| - uses: ruby/setup-ruby@v1 | ||
| with: | ||
| ruby-version: ${{ matrix.ruby }} | ||
| bundler-cache: true | ||
| - run: bundle exec rake | ||
| - run: gem build featbit-openfeature-provider.gemspec | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| /.bundle/ | ||
| /vendor/ | ||
| /Gemfile.lock | ||
| /*.gem | ||
| /pkg/ | ||
| /coverage/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| # Changelog | ||
|
|
||
| ## Unreleased | ||
|
|
||
| - Add FeatBit evaluation through OpenFeature, including typed values, context mapping, | ||
| resolution details, lifecycle events, and experiment tracking. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| source "https://rubygems.org" | ||
| gemspec | ||
|
|
||
| gem "rake", "~> 13.0" | ||
| gem "rspec", "~> 3.13" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| # FeatBit OpenFeature Provider for Ruby | ||
|
|
||
| Use [FeatBit](https://featbit.co) feature flags through the | ||
| [OpenFeature Ruby SDK](https://openfeature.dev/docs/reference/sdks/server/ruby/). | ||
| Evaluation and analytics are handled by the [FeatBit Ruby Server SDK](https://github.com/featbit/featbit-ruby-server-sdk). | ||
|
|
||
| ## Requirements and installation | ||
|
|
||
| Ruby 3.4 or newer. This provider uses `openfeature-sdk ~> 0.6.5` and | ||
| `featbit-server-sdk ~> 0.1.0`. | ||
|
|
||
| Until the first RubyGems release, add the repository to your Gemfile: | ||
|
|
||
| ```ruby | ||
| gem "featbit-openfeature-provider", git: "https://github.com/featbit/openfeature-provider-ruby-server" | ||
| ``` | ||
|
|
||
| Run `bundle install`. For local development, use `path:` pointing at this checkout. | ||
|
|
||
| ## Quick start | ||
|
|
||
| ```ruby | ||
| require "featbit/openfeature" | ||
|
|
||
| provider = FeatBit::OpenFeature::Provider.new( | ||
| FeatBit::Options.new( | ||
| env_secret: ENV.fetch("FEATBIT_ENV_SECRET"), | ||
| streaming_url: ENV.fetch("FEATBIT_STREAMING_URL", "wss://app-eval.featbit.co"), | ||
| event_url: ENV.fetch("FEATBIT_EVENT_URL", "https://app-eval.featbit.co"), | ||
| start_wait: 5 | ||
| ) | ||
| ) | ||
|
|
||
| # Raises OpenFeature::SDK::ProviderInitializationError if FeatBit is not ready. | ||
| OpenFeature::SDK.configure { |config| config.set_provider_and_wait(provider) } | ||
| client = OpenFeature::SDK.build_client | ||
| context = OpenFeature::SDK::EvaluationContext.new( | ||
| targeting_key: "user-123", name: "Alice", country: "FR", plan: "pro" | ||
| ) | ||
|
|
||
| enabled = client.fetch_boolean_value( | ||
| flag_key: "new-checkout", default_value: false, evaluation_context: context | ||
| ) | ||
|
|
||
| details = client.fetch_boolean_details( | ||
| flag_key: "new-checkout", default_value: false, evaluation_context: context | ||
| ) | ||
| puts "value=#{enabled} variant=#{details.variant} reason=#{details.reason} error=#{details.error_code}" | ||
|
|
||
| # Call during graceful application shutdown to close connections and flush events. | ||
| OpenFeature::SDK.shutdown | ||
| ``` | ||
|
|
||
| Create one provider per application process and reuse the OpenFeature client. | ||
| For prefork servers, create the provider in each worker after fork. | ||
| The FeatBit client starts during provider construction and waits up to `start_wait`. | ||
| OpenFeature calls `init` to check readiness; `config.set_provider(provider)` offers | ||
| nonblocking OpenFeature registration if you do not need to wait for registration. | ||
|
|
||
| An existing client can be supplied with | ||
| `FeatBit::OpenFeature::Provider.new(client: featbit_client)`. The provider takes | ||
| ownership: replacement or OpenFeature shutdown closes that client. Do not share | ||
| one FeatBit client between independently managed providers. A closed provider | ||
| cannot restart; construct a new instance. | ||
|
|
||
| ## Evaluation context | ||
|
|
||
| | OpenFeature field | FeatBit user field | | ||
| | --- | --- | | ||
| | `targeting_key` | Required non-empty string user key | | ||
| | `name` | Optional string user name | | ||
| | Other fields | Custom attributes, with their values preserved | | ||
|
|
||
| Missing or empty targeting keys return `TARGETING_KEY_MISSING`. Non-string keys, | ||
| non-string names, and unsupported context objects return `INVALID_CONTEXT`. | ||
| The provider does not invent anonymous identities or use `key` as a fallback. | ||
| Built-in FeatBit user properties retain the SDK's precedence over custom attributes. | ||
| Contexts are not modified. Custom attribute matching and analytics serialization | ||
| follow the FeatBit SDK's behavior; use scalar attributes for targeting rules. | ||
|
|
||
| OpenFeature merges API, client, transaction, and invocation contexts before calling | ||
| the provider. A targeting key may therefore be set at any of those scopes. | ||
|
|
||
| ## Values and resolution details | ||
|
|
||
| The provider supports `fetch_boolean_value`, `fetch_string_value`, | ||
| `fetch_number_value`, `fetch_integer_value`, `fetch_float_value`, and | ||
| `fetch_object_value`, plus OpenFeature's corresponding `fetch_*_details` methods. | ||
| Objects may be hashes or arrays. Integer evaluation truncates numeric values toward | ||
| zero; float evaluation converts numeric values to Float. Incompatible types return | ||
| the caller's default with `TYPE_MISMATCH`. | ||
|
|
||
| Successful details include the FeatBit variation ID as `variant`, plus | ||
| `featbit.reason`, `featbit.flag_name` (when available), and `featbit.in_experiment` | ||
| in flag metadata. | ||
|
|
||
| | FeatBit reason/error | OpenFeature reason/error | | ||
| | --- | --- | | ||
| | Flag off | `DISABLED` | | ||
| | User target or rule match | `TARGETING_MATCH` | | ||
| | Fallthrough | `DEFAULT` | | ||
| | Client not ready | `ERROR` / `PROVIDER_NOT_READY` | | ||
| | Flag not found | `ERROR` / `FLAG_NOT_FOUND` | | ||
| | Wrong type | `ERROR` / `TYPE_MISMATCH` | | ||
| | User not specified | `ERROR` / `TARGETING_KEY_MISSING` | | ||
| | Other evaluation errors | `ERROR` / `GENERAL` | | ||
|
|
||
| Errors return the caller's default without a variant. FeatBit does not expose a | ||
| separate parse error category, so malformed flag evaluation errors map to `GENERAL`. | ||
| Evaluation analytics are produced by FeatBit's `variation_detail`; a successful | ||
| FeatBit evaluation can record an event even if the provider subsequently rejects | ||
| its type. | ||
|
|
||
| ## Events and tracking | ||
|
|
||
| The provider forwards FeatBit readiness, interrupted synchronization, and flag | ||
| changes as `PROVIDER_READY`, `PROVIDER_STALE`, and | ||
| `PROVIDER_CONFIGURATION_CHANGED` (with `flags_changed`). Failed or closed clients | ||
| emit `PROVIDER_ERROR`. Cached flags remain available during synchronization | ||
| interruptions. Shutdown removes the provider's listeners. | ||
|
|
||
| ```ruby | ||
| client.add_handler(OpenFeature::SDK::ProviderEvent::PROVIDER_CONFIGURATION_CHANGED) do |event| | ||
| puts event[:flags_changed].inspect | ||
| end | ||
|
|
||
| client.track( | ||
| "purchase", | ||
| evaluation_context: context, | ||
| tracking_event_details: OpenFeature::SDK::TrackingEventDetails.new(value: 19.95) | ||
| ) | ||
| ``` | ||
|
|
||
| Tracking requires a valid targeting key. Omitted metric values default to `1.0`. | ||
| Extra tracking fields are ignored because the FeatBit tracking API accepts only a | ||
| user, event name, and numeric value. Invalid tracking contexts produce no event. | ||
|
|
||
| ## Offline use | ||
|
|
||
| ```ruby | ||
| require "json" | ||
|
|
||
| provider = FeatBit::OpenFeature::Provider.new( | ||
| FeatBit::Options.new(offline: true, bootstrap: JSON.parse(File.read("bootstrap.json"))) | ||
| ) | ||
| ``` | ||
|
|
||
| Supply a full FeatBit data-sync payload containing feature flags and segments. | ||
| Offline mode disables networking and analytics. Without initialized bootstrap | ||
| data the provider reports `PROVIDER_NOT_READY`. | ||
|
|
||
| ## Console example | ||
|
|
||
| The console example evaluates a boolean flag against a FeatBit environment: | ||
|
|
||
| ```sh | ||
| FEATBIT_ENV_SECRET="your-environment-secret" bundle exec ruby examples/console.rb | ||
| ``` | ||
|
|
||
| Enter a boolean flag key at the prompt to evaluate it. The prompt repeats so | ||
| multiple flags can be evaluated in one session. Enter `exit`, `quit`, or `q` | ||
| to stop the application. | ||
|
|
||
| Set `FEATBIT_TARGETING_KEY` to evaluate for a different user. The streaming and | ||
| event URLs default to FeatBit Cloud and can be overridden with | ||
| `FEATBIT_STREAMING_URL` and `FEATBIT_EVENT_URL`. | ||
|
|
||
| ## Development | ||
|
|
||
| ```sh | ||
| bundle install | ||
| bundle exec rake | ||
| gem build featbit-openfeature-provider.gemspec | ||
| ``` | ||
|
|
||
| Tests run against the released SDK gems with real offline FeatBit evaluation, | ||
| including OpenFeature context merging, defaults, details, events, and tracking. | ||
| No FeatBit service or environment secret is required. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require "bundler/gem_tasks" | ||
| require "rspec/core/rake_task" | ||
|
|
||
| RSpec::Core::RakeTask.new(:spec) | ||
| task default: :spec |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require "featbit/openfeature" | ||
|
|
||
| env_secret = ENV.fetch("FEATBIT_ENV_SECRET") | ||
|
|
||
| provider = FeatBit::OpenFeature::Provider.new( | ||
| FeatBit::Options.new( | ||
| env_secret: env_secret, | ||
| streaming_url: ENV.fetch("FEATBIT_STREAMING_URL", "wss://app-eval.featbit.co"), | ||
| event_url: ENV.fetch("FEATBIT_EVENT_URL", "https://app-eval.featbit.co"), | ||
| start_wait: 5 | ||
| ) | ||
| ) | ||
|
|
||
| begin | ||
| OpenFeature::SDK.configure { |config| config.set_provider_and_wait(provider) } | ||
| client = OpenFeature::SDK.build_client | ||
| context = OpenFeature::SDK::EvaluationContext.new( | ||
| targeting_key: ENV.fetch("FEATBIT_TARGETING_KEY", "console-user"), | ||
| name: ENV.fetch("FEATBIT_USER_NAME", "Console User") | ||
| ) | ||
|
|
||
| loop do | ||
| print "Enter a boolean flag key (or 'exit' to quit): " | ||
| input = $stdin.gets | ||
| break if input.nil? | ||
|
|
||
| flag_key = input.strip | ||
| break if %w[exit quit q].include?(flag_key.downcase) | ||
|
|
||
| if flag_key.empty? | ||
| puts "Flag key cannot be empty." | ||
| next | ||
| end | ||
|
|
||
| details = client.fetch_boolean_details( | ||
| flag_key: flag_key, | ||
| default_value: false, | ||
| evaluation_context: context | ||
| ) | ||
|
|
||
| puts "value: #{details.value.inspect}" | ||
| puts "variant: #{details.variant.inspect}" | ||
| puts "reason: #{details.reason.inspect}" | ||
| puts "error_code: #{details.error_code.inspect}" | ||
| puts "error_message: #{details.error_message.inspect}" | ||
| puts "flag_metadata: #{details.flag_metadata.inspect}" | ||
| puts | ||
| end | ||
| ensure | ||
| OpenFeature::SDK.shutdown | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require_relative "lib/featbit/openfeature/version" | ||
|
|
||
| source_code_uri = "https://github.com/featbit/openfeature-provider-ruby-server" | ||
|
|
||
| Gem::Specification.new do |spec| | ||
| spec.name = "featbit-openfeature-provider" | ||
| spec.version = FeatBit::OpenFeature::VERSION | ||
| spec.authors = ["FeatBit"] | ||
| spec.email = ["contact@featbit.co"] | ||
| spec.summary = "FeatBit OpenFeature provider for Ruby server applications" | ||
| spec.description = "Evaluate FeatBit feature flags through the OpenFeature Ruby SDK." | ||
| spec.homepage = "https://www.featbit.co/" | ||
| spec.license = "Apache-2.0" | ||
| spec.required_ruby_version = ">= 3.4" | ||
| spec.metadata["source_code_uri"] = source_code_uri | ||
| spec.metadata["bug_tracker_uri"] = "#{source_code_uri}/issues" | ||
| spec.metadata["rubygems_mfa_required"] = "true" | ||
| spec.files = Dir["lib/**/*.rb", "README.md", "LICENSE", "CHANGELOG.md"] | ||
| spec.require_paths = ["lib"] | ||
| spec.add_dependency "featbit-server-sdk", "~> 0.1.0" | ||
| spec.add_dependency "openfeature-sdk", "~> 0.6.5" | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require "featbit" | ||
| require "open_feature/sdk" | ||
| require_relative "openfeature/version" | ||
| require_relative "openfeature/context_converter" | ||
| require_relative "openfeature/details_converter" | ||
| require_relative "openfeature/provider" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module FeatBit | ||
| module OpenFeature | ||
| class ContextConverter | ||
| class InvalidContext < StandardError | ||
| attr_reader :error_code | ||
|
|
||
| def initialize(error_code, message) | ||
| @error_code = error_code | ||
| super(message) | ||
| end | ||
| end | ||
|
|
||
| def self.convert(context) | ||
| codes = ::OpenFeature::SDK::Provider::ErrorCode | ||
| unless context.nil? || context.is_a?(::OpenFeature::SDK::EvaluationContext) | ||
| raise InvalidContext.new(codes::INVALID_CONTEXT, "Expected an OpenFeature evaluation context") | ||
| end | ||
|
|
||
| key = context&.targeting_key | ||
| if key.nil? || key == "" | ||
| raise InvalidContext.new(codes::TARGETING_KEY_MISSING, "A non-empty targeting_key is required") | ||
| end | ||
| unless key.is_a?(String) | ||
| raise InvalidContext.new(codes::INVALID_CONTEXT, "targeting_key must be a string") | ||
| end | ||
|
|
||
| name = context.field("name") | ||
| unless name.nil? || name.is_a?(String) | ||
| raise InvalidContext.new(codes::INVALID_CONTEXT, "name must be a string") | ||
| end | ||
|
|
||
| custom = context.fields.reject { |field, _| %w[targeting_key name].include?(field) } | ||
| FeatBit::User.new(key, name: name, custom: custom) | ||
| end | ||
| end | ||
| end | ||
| end |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.