Featureflip Ruby SDK
Ruby SDK for Featureflip - evaluate feature flags locally with near-zero latency.
Installation
Add to your Gemfile:
gem "featureflip"Or install directly:
gem install featureflipQuick Start
require "featureflip"
# Initialize the client (blocks until flags are loaded)
client = Featureflip::Client.get("your-sdk-key")
# Evaluate a feature flag
enabled = client.bool_variation("my-feature", { "user_id" => "user-123" }, false)
if enabled
puts "Feature is enabled!"
else
puts "Feature is disabled"
end
# Clean shutdown
client.closeConfiguration
config = Featureflip::Config.new(
base_url: "https://eval.featureflip.io", # Evaluation API URL (default)
streaming: true, # Use SSE for real-time updates (default)
poll_interval: 30, # Polling interval in seconds
send_events: true, # Enable analytics event tracking (default)
flush_interval: 30, # Event flush interval in seconds
flush_batch_size: 100, # Events per batch
init_timeout: 10, # Max seconds to wait for initialization
connect_timeout: 5, # HTTP connection timeout in seconds
read_timeout: 10, # HTTP read timeout in seconds
max_stream_retries: 5, # SSE retries before falling back to polling
)
client = Featureflip::Client.get("your-sdk-key", config: config)The SDK key can also be set via the FEATUREFLIP_SDK_KEY environment variable.
To let a process that starts while the Evaluation API is unreachable serve its last known flags, pass snapshot_store: Featureflip::FileSnapshotStore.new("/path/to/snapshot.json") (and optionally max_snapshot_age_seconds:, default 86400). It is off unless you set it. See the Ruby SDK docs.
Singleton Convenience API
For applications that use a single client instance:
Featureflip.configure do |c|
c.sdk_key = "your-sdk-key"
c.base_url = "https://eval.featureflip.io"
c.streaming = true
end
enabled = Featureflip.bool_variation("my-feature", { "user_id" => "123" }, false)
# On shutdown
Featureflip.closeLifetime
Client.get returns handles backed by a shared core keyed by SDK key. Multiple calls with the same key share one set of connections and background threads:
client1 = Featureflip::Client.get("sdk-key")
client2 = Featureflip::Client.get("sdk-key")
# client1 and client2 share the same underlying resources
client1.close # Resources still alive — client2 holds a reference
client2.close # Now resources are cleaned upThis makes the SDK safe to use in dependency injection containers regardless of lifetime registration (singleton, scoped, or transient).
Evaluation
Context can use string or symbol keys:
context = { user_id: "123", email: "user@example.com" }
# or
context = { "user_id" => "123", "email" => "user@example.com" }
# Boolean flag
enabled = client.bool_variation("feature-key", context, false)
# String flag
tier = client.string_variation("pricing-tier", context, "free")
# Number flag
limit = client.number_variation("rate-limit", context, 100)
# JSON flag
config = client.json_variation("ui-config", context, { "theme" => "light" })Detailed Evaluation
detail = client.variation_detail("feature-key", { user_id: "123" }, false)
detail.value # The evaluated value
detail.reason # "RuleMatch", "Fallthrough", "FlagDisabled", "FlagNotFound"
detail.rule_id # Rule ID if reason is "RuleMatch"
detail.variation_key # Key of the matched variationReacting to flag changes
unsubscribe = client.on_update do |changed_keys|
Rails.logger.info("flags changed: #{changed_keys.join(', ')}")
end
unsubscribe.call # stop listeningThe block runs on the SDK's streaming or polling thread, so keep it short. Hand real work to a queue. The initial flag load does not fire; only later changes do. Closing the client drops its subscriptions.
Event Tracking
# Track custom events
client.track("checkout-completed", { user_id: "123" }, { total: 99.99 })
# Record an identify event for analytics (does not affect flag evaluation)
client.identify({ user_id: "123", email: "user@example.com", plan: "pro" })
# Force flush pending events
client.flushFeatureflip counts each event name, per environment. The metadata you pass is transmitted with the event but is not stored, and event counts are not surfaced in the app or API.
Testing
Use the test client for deterministic unit tests with no network calls:
client = Featureflip::Client.for_testing(
"my-feature" => true,
"pricing-tier" => "pro"
)
client.bool_variation("my-feature", {}, false) # => true
client.string_variation("pricing-tier", {}, "free") # => "pro"
client.bool_variation("unknown", {}, false) # => false (default)Rails Setup
Featureflip is framework-agnostic. For Rails, initialize in an initializer:
# config/initializers/featureflip.rb
Featureflip.configure do |c|
c.sdk_key = ENV["FEATUREFLIP_SDK_KEY"]
c.streaming = true
end
# For Puma/Unicorn/Passenger — respawn threads after fork
# config/puma.rb
on_worker_boot do
Featureflip.restart
endFeatures
- Local evaluation - Near-zero latency after initialization
- Real-time updates - SSE streaming with automatic polling fallback
- Event tracking - Automatic batching and background flushing
-
Test support -
for_testingfactory for deterministic unit tests - Thread-safe - Mutex-protected flag store and event queue
-
Fork-safe -
restartmethod for Puma/Unicorn/Passenger worker processes -
Zero runtime dependencies - Uses only Ruby stdlib (
net/http,digest/md5) - OpenFeature - use the featureflip-openfeature provider
Requirements
- Ruby 3.2+
Development
# Install dependencies
bundle install
# Run tests
bundle exec rspec
# Run specific test file
bundle exec rspec spec/featureflip/evaluation/evaluator_spec.rbLicense
Apache-2.0