This is the Optimizely Ruby SDK for A/B testing and feature flag management. Key architectural patterns:
Optimizely::Project(lib/optimizely.rb): Main client class, entry point for all SDK operationsDecisionService(lib/optimizely/decision_service.rb): Core bucketing logic with 7-step decision flow (status→forced→whitelist→sticky→audience→CMAB→hash)ProjectConfig(lib/optimizely/config/): Manages datafile parsing and experiment/feature configuration- Config Managers (
lib/optimizely/config_manager/): Handle datafile fetching -HTTPProjectConfigManagerfor polling,StaticProjectConfigManagerfor static files
- User Context:
OptimizelyUserContextwraps user ID + attributes for decision APIs - Decision Pipeline: All feature flags/experiments flow through
DecisionService.get_variation_for_feature() - Event Processing:
BatchEventProcessorqueues and batches impression/conversion events - CMAB Integration: Contextual Multi-Armed Bandit system in
lib/optimizely/cmab/for advanced optimization
# Run all tests
bundle exec rake spec
# Run specific test file
bundle exec rspec spec/decision_service_spec.rb
# Run linting
bundle exec rubocop# Build gem locally
rake build # outputs to /pkg/
# Run benchmarks
rake benchmark- Frozen string literals: All files start with
# frozen_string_literal: true - Module namespacing: Everything under
Optimizely::module - Struct for data: Use
Struct.new()for decision results (e.g.,DecisionService::Decision) - Factory pattern:
OptimizelyFactoryprovides preset SDK configurations
- Experiment/Feature keys: String identifiers from Optimizely dashboard
- User bucketing:
bucketing_idvsuser_id(can be different for sticky bucketing) - Decision sources:
'EXPERIMENT','FEATURE_TEST','ROLLOUT'constants inDecisionService
- Graceful degradation: Invalid configs return
nil/default values, never crash - Logging levels: Use
Logger::ERROR,Logger::INFO,Logger::DEBUGconsistently - User input validation:
Helpers::Validatorvalidates all public API inputs
- HTTPProjectConfigManager: Polls Optimizely CDN every 5 minutes by default
- Notification system: Subscribe to
OPTIMIZELY_CONFIG_UPDATEfor datafile changes - ODP integration: On datafile update, call
update_odp_config_on_datafile_update()
- BatchEventProcessor: Default 10 events/batch, 30s flush interval
- Thread safety: SDK spawns background threads for polling and event processing
- Graceful shutdown: Always call
optimizely.close()to flush events
- New pattern: Use
decide()API, not legacyis_feature_enabled() - Decision options:
OptimizelyDecideOptionconstants control behavior (exclude variables, include reasons, etc.) - Forced decisions: Override bucketing via
set_forced_variation()or user profile service
- Spec location: One spec file per class in
/spec/directory - WebMock: Mock HTTP requests in tests (
require 'webmock/rspec') - Test data: Use
spec_params.rbfor shared test constants - Coverage: Coveralls integration via GitHub Actions
- Thread spawning: Initialize SDK after forking processes in web servers
- Bucketing consistency: Use same
bucketing_idacross SDK calls for sticky behavior - CMAB caching: Context-aware ML decisions cache by user - use appropriate cache invalidation options
- ODP events: Require at least one identifier, auto-add default event data
# ❌ MEMORY LEAK - Creates new threads every request
def get_optimizely_client
Optimizely::Project.new(datafile: fetch_datafile)
end
# ✅ CORRECT - Singleton pattern with proper cleanup
class OptimizelyService
def self.instance
@instance ||= Optimizely::Project.new(datafile: fetch_datafile)
end
def self.reset!
@instance&.close # Critical: stops background threads
@instance = nil
end
end- Background threads:
Project.new()spawns multiple threads (BatchEventProcessor,OdpEventManager, config polling) - Memory accumulation: Each initialization creates new threads that persist until explicitly stopped
- Proper cleanup: Always call
optimizely_instance.close()before dereferencing - Rails deployment: Use singleton pattern or application-level initialization, never per-request
# Application initialization (config/initializers/optimizely.rb)
OPTIMIZELY_CLIENT = Optimizely::Project.new(
sdk_key: ENV['OPTIMIZELY_SDK_KEY']
)
# Graceful shutdown (config/application.rb)
at_exit { OPTIMIZELY_CLIENT&.close }