-
Notifications
You must be signed in to change notification settings - Fork 67
Implement feature flags provider #129
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| require 'mixpanel-ruby' | ||
|
|
||
| # Configuration | ||
| PROJECT_TOKEN = "" | ||
efahk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| FLAG_KEY = "sample-flag" | ||
| FLAG_FALLBACK_VARIANT = "control" | ||
| USER_CONTEXT = { "distinct_id" => "ruby-demo-user" } | ||
| API_HOST = "api.mixpanel.com" | ||
| SHOULD_POLL_CONTINUOUSLY = true | ||
| POLLING_INTERVAL_SECONDS = 15 | ||
|
|
||
| local_config = { | ||
| api_host: API_HOST, | ||
| enable_polling: SHOULD_POLL_CONTINUOUSLY, | ||
| polling_interval_in_seconds: POLLING_INTERVAL_SECONDS | ||
| } | ||
|
|
||
| tracker = Mixpanel::Tracker.new(PROJECT_TOKEN, local_flags_config: local_config) | ||
|
|
||
| tracker.local_flags.start_polling_for_definitions! | ||
|
|
||
| variant_value = tracker.local_flags.get_variant_value(FLAG_KEY, FLAG_FALLBACK_VARIANT, USER_CONTEXT) | ||
| puts "Variant value: #{variant_value}" | ||
|
|
||
| tracker.local_flags.stop_polling_for_definitions! | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,18 @@ | ||||||||||
| require 'mixpanel-ruby' | ||||||||||
|
|
||||||||||
| # Configuration | ||||||||||
| PROJECT_TOKEN = "" | ||||||||||
|
||||||||||
| PROJECT_TOKEN = "" | |
| # NOTE: Do not hardcode or commit real project tokens in source control. | |
| # For real projects, set MIXPANEL_PROJECT_TOKEN in your environment instead. | |
| PROJECT_TOKEN = ENV['MIXPANEL_PROJECT_TOKEN'] || "" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Project token is not a secret
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,8 @@ | ||
| require 'mixpanel-ruby/consumer.rb' | ||
| require 'mixpanel-ruby/tracker.rb' | ||
| require 'mixpanel-ruby/version.rb' | ||
| require 'mixpanel-ruby/flags/utils.rb' | ||
| require 'mixpanel-ruby/flags/types.rb' | ||
| require 'mixpanel-ruby/flags/flags_provider.rb' | ||
| require 'mixpanel-ruby/flags/local_flags_provider.rb' | ||
| require 'mixpanel-ruby/flags/remote_flags_provider.rb' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| require 'net/https' | ||
| require 'json' | ||
| require 'uri' | ||
| require 'mixpanel-ruby/version' | ||
| require 'mixpanel-ruby/error' | ||
| require 'mixpanel-ruby/flags/utils' | ||
| require 'mixpanel-ruby/flags/types' | ||
|
|
||
| module Mixpanel | ||
| module Flags | ||
|
|
||
| # Base class for feature flags providers | ||
| # Provides common HTTP handling and exposure event tracking | ||
| class FlagsProvider | ||
| # @param provider_config [Hash] Configuration with :token, :api_host, :request_timeout_in_seconds | ||
| # @param endpoint [String] API endpoint path (e.g., '/flags' or '/flags/definitions') | ||
| # @param tracker_callback [Proc] Function used to track events (bound tracker.track method) | ||
| # @param evaluation_mode [String] The feature flag evaluation mode. This is either 'local' or 'remote' | ||
| # @param error_handler [Mixpanel::ErrorHandler] Error handler instance | ||
| def initialize(provider_config, endpoint, tracker_callback, evaluation_mode, error_handler) | ||
| @provider_config = provider_config | ||
| @endpoint = endpoint | ||
| @tracker_callback = tracker_callback | ||
| @evaluation_mode = evaluation_mode | ||
| @error_handler = error_handler | ||
| end | ||
|
|
||
| # Make HTTP request to flags API endpoint | ||
| # @param additional_params [Hash, nil] Additional query parameters | ||
| # @return [Hash] Parsed JSON response | ||
| # @raise [Mixpanel::ConnectionError] on network errors | ||
| # @raise [Mixpanel::ServerError] on HTTP errors | ||
| def call_flags_endpoint(additional_params = nil) | ||
| common_params = Utils.prepare_common_query_params( | ||
| @provider_config[:token], | ||
| Mixpanel::VERSION | ||
| ) | ||
|
|
||
| params = common_params.merge(additional_params || {}) | ||
| query_string = URI.encode_www_form(params) | ||
|
|
||
| uri = URI::HTTPS.build( | ||
| host: @provider_config[:api_host], | ||
| path: @endpoint, | ||
| query: query_string | ||
| ) | ||
|
|
||
| http = Net::HTTP.new(uri.host, uri.port) | ||
|
|
||
| http.use_ssl = true | ||
| http.open_timeout = @provider_config[:request_timeout_in_seconds] | ||
| http.read_timeout = @provider_config[:request_timeout_in_seconds] | ||
|
|
||
| request = Net::HTTP::Get.new(uri.request_uri) | ||
|
|
||
| request.basic_auth(@provider_config[:token], '') | ||
|
|
||
| request['Content-Type'] = 'application/json' | ||
| request['traceparent'] = Utils.generate_traceparent() | ||
|
|
||
| begin | ||
| response = http.request(request) | ||
|
|
||
| unless response.code.to_i == 200 | ||
| raise ServerError.new("HTTP #{response.code}: #{response.body}") | ||
| end | ||
|
|
||
| JSON.parse(response.body) | ||
| rescue Net::OpenTimeout, Net::ReadTimeout => e | ||
| raise ConnectionError.new("Request timeout: #{e.message}") | ||
| rescue JSON::ParserError => e | ||
| raise ServerError.new("Invalid JSON response: #{e.message}") | ||
| rescue StandardError => e | ||
| raise ConnectionError.new("Network error: #{e.message}") | ||
| end | ||
| end | ||
|
|
||
| # Track exposure event to Mixpanel | ||
| # @param flag_key [String] Feature flag key | ||
| # @param selected_variant [SelectedVariant] The selected variant | ||
| # @param context [Hash] User context (must include 'distinct_id') | ||
| # @param latency_ms [Integer, nil] Optional latency in milliseconds | ||
| def track_exposure_event(flag_key, selected_variant, context, latency_ms = nil) | ||
| distinct_id = context['distinct_id'] || context[:distinct_id] | ||
|
|
||
| unless distinct_id | ||
| return | ||
| end | ||
|
|
||
| properties = { | ||
| 'distinct_id' => distinct_id, | ||
| 'Experiment name' => flag_key, | ||
| 'Variant name' => selected_variant.variant_key, | ||
| '$experiment_type' => 'feature_flag', | ||
| 'Flag evaluation mode' => @evaluation_mode | ||
| } | ||
|
|
||
| properties['Variant fetch latency (ms)'] = latency_ms if latency_ms | ||
| properties['$experiment_id'] = selected_variant.experiment_id if selected_variant.experiment_id | ||
| properties['$is_experiment_active'] = selected_variant.is_experiment_active unless selected_variant.is_experiment_active.nil? | ||
| properties['$is_qa_tester'] = selected_variant.is_qa_tester unless selected_variant.is_qa_tester.nil? | ||
|
|
||
| begin | ||
| @tracker_callback.call(distinct_id, Utils::EXPOSURE_EVENT, properties) | ||
| rescue MixpanelError => e | ||
| @error_handler.handle(e) | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
didn't add a test coverage here. i think it's better to add codecov integration here so we know it's being properly covered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added now - Needs to go into default branch but coverage looks solid
Coverage report generated for RSpec to /Users/kwameefah/mixpanel-ruby/coverage. Line Coverage: 96.45% (544 / 564) Coverage report generated for RSpec to /Users/kwameefah/mixpanel-ruby/coverage/coverage.xml. Line Coverage: 96.45% (544 / 564)