mixpanel-ruby 2.3.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +66 -0
  3. data/Readme.rdoc +8 -2
  4. data/demo/flags/local_flags.rb +25 -0
  5. data/demo/flags/remote_flags.rb +18 -0
  6. data/lib/mixpanel-ruby/events.rb +2 -2
  7. data/lib/mixpanel-ruby/flags/flags_provider.rb +115 -0
  8. data/lib/mixpanel-ruby/flags/local_flags_provider.rb +300 -0
  9. data/lib/mixpanel-ruby/flags/remote_flags_provider.rb +134 -0
  10. data/lib/mixpanel-ruby/flags/types.rb +35 -0
  11. data/lib/mixpanel-ruby/flags/utils.rb +65 -0
  12. data/lib/mixpanel-ruby/groups.rb +1 -1
  13. data/lib/mixpanel-ruby/people.rb +1 -1
  14. data/lib/mixpanel-ruby/tracker.rb +32 -2
  15. data/lib/mixpanel-ruby/version.rb +1 -1
  16. data/lib/mixpanel-ruby.rb +5 -0
  17. data/mixpanel-ruby.gemspec +10 -3
  18. data/openfeature-provider/Gemfile +7 -0
  19. data/openfeature-provider/README.md +286 -0
  20. data/openfeature-provider/RELEASE.md +52 -0
  21. data/openfeature-provider/lib/mixpanel/openfeature/provider.rb +170 -0
  22. data/openfeature-provider/lib/mixpanel/openfeature.rb +3 -0
  23. data/openfeature-provider/mixpanel-ruby-openfeature.gemspec +23 -0
  24. data/openfeature-provider/spec/mixpanel_openfeature_provider_spec.rb +606 -0
  25. data/openfeature-provider/spec/spec_helper.rb +23 -0
  26. data/spec/mixpanel-ruby/events_spec.rb +2 -2
  27. data/spec/mixpanel-ruby/flags/local_flags_spec.rb +759 -0
  28. data/spec/mixpanel-ruby/flags/remote_flags_spec.rb +441 -0
  29. data/spec/mixpanel-ruby/flags/utils_spec.rb +110 -0
  30. data/spec/mixpanel-ruby/groups_spec.rb +10 -10
  31. data/spec/mixpanel-ruby/tracker_spec.rb +5 -5
  32. data/spec/spec_helper.rb +14 -0
  33. metadata +125 -9
  34. data/.travis.yml +0 -8
@@ -0,0 +1,134 @@
1
+ require 'mixpanel-ruby/flags/flags_provider'
2
+
3
+ module Mixpanel
4
+ module Flags
5
+ # Remote feature flags provider
6
+ # Evaluates flags on the server-side via HTTP API calls
7
+ class RemoteFlagsProvider < FlagsProvider
8
+ DEFAULT_CONFIG = {
9
+ api_host: 'api.mixpanel.com',
10
+ request_timeout_in_seconds: 10
11
+ }.freeze
12
+
13
+ # @param token [String] Mixpanel project token
14
+ # @param config [Hash] Remote flags configuration
15
+ # @param tracker_callback [Proc] Callback to track events
16
+ # @param error_handler [Mixpanel::ErrorHandler] Error handler
17
+ def initialize(token, config, tracker_callback, error_handler)
18
+ merged_config = DEFAULT_CONFIG.merge(config || {})
19
+
20
+ provider_config = {
21
+ token: token,
22
+ api_host: merged_config[:api_host],
23
+ request_timeout_in_seconds: merged_config[:request_timeout_in_seconds]
24
+ }
25
+
26
+ super(provider_config, '/flags', tracker_callback, 'remote', error_handler)
27
+ end
28
+
29
+ # Get variant value for a flag
30
+ # @param flag_key [String] Feature flag key
31
+ # @param fallback_value [Object] Fallback value
32
+ # @param context [Hash] Evaluation context
33
+ # @param report_exposure [Boolean] Whether to track exposure
34
+ # @return [Object] Variant value
35
+ def get_variant_value(flag_key, fallback_value, context, report_exposure: true)
36
+ selected_variant = get_variant(
37
+ flag_key,
38
+ SelectedVariant.new(variant_value: fallback_value),
39
+ context,
40
+ report_exposure: report_exposure
41
+ )
42
+ selected_variant.variant_value
43
+ rescue MixpanelError => e
44
+ @error_handler.handle(e)
45
+ fallback_value
46
+ end
47
+
48
+ # Get complete variant information
49
+ # @param flag_key [String] Feature flag key
50
+ # @param fallback_variant [SelectedVariant] Fallback variant
51
+ # @param context [Hash] Evaluation context
52
+ # @param report_exposure [Boolean] Whether to track exposure
53
+ # @return [SelectedVariant]
54
+ def get_variant(flag_key, fallback_variant, context, report_exposure: true)
55
+ start_time = Time.now
56
+ response = fetch_flags(context, flag_key)
57
+ latency_ms = ((Time.now - start_time) * 1000).to_i
58
+
59
+ flags = response['flags'] || {}
60
+ selected_variant_data = flags[flag_key]
61
+
62
+ return fallback_variant unless selected_variant_data
63
+
64
+ selected_variant = SelectedVariant.new(
65
+ variant_key: selected_variant_data['variant_key'],
66
+ variant_value: selected_variant_data['variant_value'],
67
+ experiment_id: selected_variant_data['experiment_id'],
68
+ is_experiment_active: selected_variant_data['is_experiment_active']
69
+ )
70
+
71
+ track_exposure_event(flag_key, selected_variant, context, latency_ms) if report_exposure
72
+
73
+ return selected_variant
74
+ rescue MixpanelError => e
75
+ @error_handler.handle(e)
76
+ return fallback_variant
77
+ end
78
+
79
+ # Check if flag is enabled (for boolean flags)
80
+ # This method is intended only for flags defined as Mixpanel Feature Gates (boolean flags)
81
+ # This checks that the variant value of a selected variant is concretely the boolean 'true'
82
+ # It does not coerce other truthy values.
83
+ # @param flag_key [String] Feature flag key
84
+ # @param context [Hash] Evaluation context
85
+ # @return [Boolean]
86
+ def is_enabled?(flag_key, context)
87
+ value = get_variant_value(flag_key, false, context)
88
+ value == true
89
+ rescue MixpanelError => e
90
+ @error_handler.handle(e)
91
+ false
92
+ end
93
+
94
+ # Get all variants for user context
95
+ # Exposure events NOT tracked automatically
96
+ # @param context [Hash] Evaluation context
97
+ # @return [Hash, nil] Map of flag_key => SelectedVariant, or nil on error
98
+ def get_all_variants(context)
99
+ response = fetch_flags(context)
100
+
101
+ variants = {}
102
+ (response['flags'] || {}).each do |flag_key, variant_data|
103
+ variants[flag_key] = SelectedVariant.new(
104
+ variant_key: variant_data['variant_key'],
105
+ variant_value: variant_data['variant_value'],
106
+ experiment_id: variant_data['experiment_id'],
107
+ is_experiment_active: variant_data['is_experiment_active']
108
+ )
109
+ end
110
+
111
+ variants
112
+ rescue MixpanelError => e
113
+ @error_handler.handle(e)
114
+ nil
115
+ end
116
+
117
+ private
118
+
119
+ # Fetch flags from remote API
120
+ # @param context [Hash] Evaluation context
121
+ # @param flag_key [String, nil] Optional specific flag key
122
+ # @return [Hash] API response
123
+ def fetch_flags(context, flag_key = nil)
124
+ additional_params = {
125
+ 'context' => JSON.generate(context)
126
+ }
127
+
128
+ additional_params['flag_key'] = flag_key if flag_key
129
+
130
+ call_flags_endpoint(additional_params)
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,35 @@
1
+ module Mixpanel
2
+ module Flags
3
+ # Selected variant returned from flag evaluation
4
+ class SelectedVariant
5
+ attr_accessor :variant_key, :variant_value, :experiment_id,
6
+ :is_experiment_active, :is_qa_tester
7
+
8
+ # @param variant_key [String, nil] The variant key
9
+ # @param variant_value [Object] The variant value (any type)
10
+ # @param experiment_id [String, nil] Associated experiment ID
11
+ # @param is_experiment_active [Boolean, nil] Whether experiment is active
12
+ # @param is_qa_tester [Boolean, nil] Whether user is a QA tester
13
+ def initialize(variant_key: nil, variant_value: nil, experiment_id: nil,
14
+ is_experiment_active: nil, is_qa_tester: nil)
15
+ @variant_key = variant_key
16
+ @variant_value = variant_value
17
+ @experiment_id = experiment_id
18
+ @is_experiment_active = is_experiment_active
19
+ @is_qa_tester = is_qa_tester
20
+ end
21
+
22
+ # Convert to hash representation
23
+ # @return [Hash]
24
+ def to_h
25
+ {
26
+ variant_key: @variant_key,
27
+ variant_value: @variant_value,
28
+ experiment_id: @experiment_id,
29
+ is_experiment_active: @is_experiment_active,
30
+ is_qa_tester: @is_qa_tester
31
+ }.compact
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,65 @@
1
+ require 'securerandom'
2
+
3
+ module Mixpanel
4
+ module Flags
5
+ module Utils
6
+ EXPOSURE_EVENT = '$experiment_started'.freeze
7
+
8
+ # FNV-1a 64-bit hash implementation
9
+ # Used for consistent variant assignment
10
+ #
11
+ # @param data [String] Data to hash
12
+ # @return [Integer] 64-bit hash value
13
+ def self.fnv1a_64(data)
14
+ fnv_prime = 0x100000001b3
15
+ hash_value = 0xcbf29ce484222325
16
+
17
+ data.bytes.each do |byte|
18
+ hash_value ^= byte
19
+ hash_value *= fnv_prime
20
+ hash_value &= 0xffffffffffffffff # Keep 64-bit
21
+ end
22
+
23
+ hash_value
24
+ end
25
+
26
+ # Normalized hash for variant assignment
27
+ # Returns a float in the range [0.0, 1.0) for rollout percentage matching
28
+ #
29
+ # @param key [String] Key to hash (typically distinct_id)
30
+ # @param salt [String] Salt value (flag-specific)
31
+ # @return [Float] Value between 0.0 and 1.0 (non-inclusive upper bound)
32
+ def self.normalized_hash(key, salt)
33
+ combined = key.to_s + salt.to_s
34
+ hash_value = fnv1a_64(combined)
35
+ (hash_value % 100) / 100.0
36
+ end
37
+
38
+ # Prepare common query parameters for flags API
39
+ #
40
+ # @param token [String] Mixpanel project token
41
+ # @param $lib_version [String] SDK version
42
+ # @return [Hash] Query parameters
43
+ def self.prepare_common_query_params(token, lib_version)
44
+ {
45
+ 'mp_lib' => 'ruby',
46
+ '$lib_version' => lib_version,
47
+ 'token' => token
48
+ }
49
+ end
50
+
51
+ # Generate W3C traceparent header for distributed tracing
52
+ # Format: 00-{trace-id}-{parent-id}-{trace-flags}
53
+ #
54
+ # @return [String] traceparent header value
55
+ def self.generate_traceparent
56
+ version = '00'
57
+ trace_id = SecureRandom.hex(16)
58
+ parent_id = SecureRandom.hex(8)
59
+ trace_flags = '01' # sampled
60
+
61
+ "#{version}-#{trace_id}-#{parent_id}-#{trace_flags}"
62
+ end
63
+ end
64
+ end
65
+ end
@@ -170,7 +170,7 @@ module Mixpanel
170
170
  def update(message)
171
171
  data = {
172
172
  '$token' => @token,
173
- '$time' => Time.now.to_f,
173
+ '$time' => (Time.now.to_f * 1000).to_i,
174
174
  }.merge(message)
175
175
 
176
176
  message = {'data' => data}
@@ -223,7 +223,7 @@ module Mixpanel
223
223
  def update(message)
224
224
  data = {
225
225
  '$token' => @token,
226
- '$time' => Time.now.to_f,
226
+ '$time' => (Time.now.to_f * 1000).to_i,
227
227
  }.merge(message)
228
228
 
229
229
  message = {'data' => data}
@@ -1,6 +1,8 @@
1
1
  require 'mixpanel-ruby/events.rb'
2
2
  require 'mixpanel-ruby/people.rb'
3
3
  require 'mixpanel-ruby/groups.rb'
4
+ require 'mixpanel-ruby/flags/local_flags_provider.rb'
5
+ require 'mixpanel-ruby/flags/remote_flags_provider.rb'
4
6
 
5
7
  module Mixpanel
6
8
  # Use Mixpanel::Tracker to track events and profile updates in your application.
@@ -33,6 +35,14 @@ module Mixpanel
33
35
  # An instance of Mixpanel::Groups. Use this to send groups updates
34
36
  attr_reader :groups
35
37
 
38
+ # An instance of Mixpanel::Flags::LocalFlagsProvider. Use this for
39
+ # client-side feature flag evaluation
40
+ attr_reader :local_flags
41
+
42
+ # An instance of Mixpanel::Flags::RemoteFlagsProvider. Use this for
43
+ # server-side feature flag evaluation
44
+ attr_reader :remote_flags
45
+
36
46
  # Takes your Mixpanel project token, as a string.
37
47
  #
38
48
  # tracker = Mixpanel::Tracker.new(YOUR_MIXPANEL_TOKEN)
@@ -52,11 +62,31 @@ module Mixpanel
52
62
  # If a block is provided, it is passed a type (one of :event or :profile_update)
53
63
  # and a string message. This same format is accepted by Mixpanel::Consumer#send!
54
64
  # and Mixpanel::BufferedConsumer#send!
55
- def initialize(token, error_handler=nil, &block)
65
+ def initialize(token, error_handler=nil, local_flags_config: nil, remote_flags_config: nil, &block)
56
66
  super(token, error_handler, &block)
57
67
  @token = token
58
68
  @people = People.new(token, error_handler, &block)
59
69
  @groups = Groups.new(token, error_handler, &block)
70
+
71
+ # Initialize local flags if config provided
72
+ if local_flags_config
73
+ @local_flags = Flags::LocalFlagsProvider.new(
74
+ token,
75
+ local_flags_config,
76
+ method(:track), # Pass bound method as callback
77
+ error_handler || ErrorHandler.new
78
+ )
79
+ end
80
+
81
+ # Initialize remote flags if config provided
82
+ if remote_flags_config
83
+ @remote_flags = Flags::RemoteFlagsProvider.new(
84
+ token,
85
+ remote_flags_config,
86
+ method(:track), # Pass bound method as callback
87
+ error_handler || ErrorHandler.new
88
+ )
89
+ end
60
90
  end
61
91
 
62
92
  # A call to #track is a report that an event has occurred. #track
@@ -165,7 +195,7 @@ module Mixpanel
165
195
  properties = {
166
196
  'distinct_id' => distinct_id,
167
197
  'token' => @token,
168
- 'time' => Time.now.to_f,
198
+ 'time' => (Time.now.to_f * 1000).to_i,
169
199
  'mp_lib' => 'ruby',
170
200
  '$lib_version' => Mixpanel::VERSION,
171
201
  }.merge(properties)
@@ -1,3 +1,3 @@
1
1
  module Mixpanel
2
- VERSION = '2.3.0'
2
+ VERSION = '3.1.0'
3
3
  end
data/lib/mixpanel-ruby.rb CHANGED
@@ -1,3 +1,8 @@
1
1
  require 'mixpanel-ruby/consumer.rb'
2
2
  require 'mixpanel-ruby/tracker.rb'
3
3
  require 'mixpanel-ruby/version.rb'
4
+ require 'mixpanel-ruby/flags/utils.rb'
5
+ require 'mixpanel-ruby/flags/types.rb'
6
+ require 'mixpanel-ruby/flags/flags_provider.rb'
7
+ require 'mixpanel-ruby/flags/local_flags_provider.rb'
8
+ require 'mixpanel-ruby/flags/remote_flags_provider.rb'
@@ -12,10 +12,17 @@ spec = Gem::Specification.new do |spec|
12
12
  spec.homepage = 'https://mixpanel.com/help/reference/ruby'
13
13
  spec.license = 'Apache License 2.0'
14
14
 
15
- spec.required_ruby_version = '>= 2.0.0'
15
+ spec.required_ruby_version = '>= 3.0.0'
16
+ spec.add_runtime_dependency 'mutex_m'
17
+ spec.add_runtime_dependency "base64"
18
+ spec.add_runtime_dependency 'json-logic-rb', '~> 0.1.5'
16
19
 
17
20
  spec.add_development_dependency 'activesupport', '~> 4.0'
18
- spec.add_development_dependency 'rake', '~> 0'
21
+ spec.add_development_dependency 'rake', '~> 13'
19
22
  spec.add_development_dependency 'rspec', '~> 3.0'
20
- spec.add_development_dependency 'webmock', '~> 1.18'
23
+ spec.add_development_dependency 'webmock', '~> 3.16.2'
24
+ spec.add_development_dependency 'debug'
25
+ spec.add_development_dependency 'ruby-lsp-rspec'
26
+ spec.add_development_dependency 'simplecov'
27
+ spec.add_development_dependency 'simplecov-cobertura'
21
28
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ gem 'mixpanel-ruby', path: '..'
@@ -0,0 +1,286 @@
1
+ # mixpanel-ruby-openfeature
2
+
3
+ [![Gem Version](https://img.shields.io/gem/v/mixpanel-ruby-openfeature.svg)](https://rubygems.org/gems/mixpanel-ruby-openfeature)
4
+ [![OpenFeature](https://img.shields.io/badge/OpenFeature-compatible-green)](https://openfeature.dev/)
5
+ [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](https://github.com/mixpanel/mixpanel-ruby/blob/master/LICENSE)
6
+
7
+ An [OpenFeature](https://openfeature.dev/) provider that wraps Mixpanel's feature flags for use with the OpenFeature Ruby SDK. This allows you to use Mixpanel's feature flagging capabilities through OpenFeature's standardized, vendor-agnostic API.
8
+
9
+ ## Overview
10
+
11
+ This gem provides a bridge between Mixpanel's native feature flags implementation and the OpenFeature specification. By using this provider, you can:
12
+
13
+ - Leverage Mixpanel's powerful feature flag and experimentation platform
14
+ - Use OpenFeature's standardized API for flag evaluation
15
+ - Easily switch between feature flag providers without changing your application code
16
+ - Integrate with OpenFeature's ecosystem of tools and frameworks
17
+
18
+ ## Installation
19
+
20
+ Add these gems to your `Gemfile`:
21
+
22
+ ```ruby
23
+ gem 'mixpanel-ruby-openfeature'
24
+ gem 'openfeature-sdk'
25
+ gem 'mixpanel-ruby'
26
+ ```
27
+
28
+ Then run:
29
+
30
+ ```bash
31
+ bundle install
32
+ ```
33
+
34
+ Or install directly:
35
+
36
+ ```bash
37
+ gem install mixpanel-ruby-openfeature
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ### Local Evaluation (Recommended)
43
+
44
+ Local evaluation downloads flag definitions and evaluates them locally, providing fast, synchronous flag checks with no per-evaluation network requests.
45
+
46
+ ```ruby
47
+ require 'mixpanel-ruby'
48
+ require 'mixpanel/openfeature'
49
+
50
+ # 1. Create the provider with local evaluation
51
+ provider = Mixpanel::OpenFeature::Provider.from_local(
52
+ 'YOUR_PROJECT_TOKEN',
53
+ { poll_interval: 300 } # poll for updated definitions every 300 seconds
54
+ )
55
+
56
+ # 2. Register the provider with OpenFeature
57
+ OpenFeature::SDK.configure do |config|
58
+ config.set_provider(provider)
59
+ end
60
+
61
+ # 3. Get a client and evaluate flags
62
+ client = OpenFeature::SDK.build_client
63
+
64
+ show_new_feature = client.fetch_boolean_value(flag_key: 'new-feature-flag', default_value: false)
65
+
66
+ if show_new_feature
67
+ puts 'New feature is enabled!'
68
+ end
69
+ ```
70
+
71
+ ### Remote Evaluation
72
+
73
+ Remote evaluation sends each flag check to Mixpanel's servers, which is useful when you need server-side targeting or cannot download flag definitions locally.
74
+
75
+ ```ruby
76
+ require 'mixpanel-ruby'
77
+ require 'mixpanel/openfeature'
78
+
79
+ # 1. Create the provider with remote evaluation
80
+ provider = Mixpanel::OpenFeature::Provider.from_remote(
81
+ 'YOUR_PROJECT_TOKEN',
82
+ {} # remote config options
83
+ )
84
+
85
+ # 2. Register the provider with OpenFeature
86
+ OpenFeature::SDK.configure do |config|
87
+ config.set_provider(provider)
88
+ end
89
+
90
+ # 3. Evaluate flags the same way
91
+ client = OpenFeature::SDK.build_client
92
+ value = client.fetch_string_value(flag_key: 'button-color-test', default_value: 'blue')
93
+ ```
94
+
95
+ ### Manual Initialization
96
+
97
+ If you already have a Mixpanel `Tracker` instance, you can pass its flags provider directly:
98
+
99
+ ```ruby
100
+ tracker = Mixpanel::Tracker.new('YOUR_PROJECT_TOKEN', nil, local_flags_config: { poll_interval: 300 })
101
+ flags_provider = tracker.local_flags
102
+ flags_provider.start_polling_for_definitions!
103
+
104
+ provider = Mixpanel::OpenFeature::Provider.new(flags_provider)
105
+ ```
106
+
107
+ ## Usage Examples
108
+
109
+ ### Basic Boolean Flag
110
+
111
+ ```ruby
112
+ client = OpenFeature::SDK.build_client
113
+
114
+ is_feature_enabled = client.fetch_boolean_value(flag_key: 'my-feature', default_value: false)
115
+
116
+ if is_feature_enabled
117
+ # Show the new feature
118
+ end
119
+ ```
120
+
121
+ ### Mixpanel Flag Types and OpenFeature Evaluation Methods
122
+
123
+ Mixpanel feature flags support three flag types. Use the corresponding OpenFeature evaluation method based on your flag's variant values:
124
+
125
+ | Mixpanel Flag Type | Variant Values | OpenFeature Method |
126
+ |---|---|---|
127
+ | Feature Gate | `true` / `false` | `fetch_boolean_value` |
128
+ | Experiment | boolean, string, number, or JSON object | `fetch_boolean_value`, `fetch_string_value`, `fetch_number_value`, or `fetch_object_value` |
129
+ | Dynamic Config | JSON object | `fetch_object_value` |
130
+
131
+ ```ruby
132
+ client = OpenFeature::SDK.build_client
133
+
134
+ # Feature Gate - boolean variants
135
+ is_feature_on = client.fetch_boolean_value(flag_key: 'new-checkout', default_value: false)
136
+
137
+ # Experiment with string variants
138
+ button_color = client.fetch_string_value(flag_key: 'button-color-test', default_value: 'blue')
139
+
140
+ # Experiment with number variants
141
+ max_items = client.fetch_number_value(flag_key: 'max-items', default_value: 10)
142
+
143
+ # Dynamic Config - JSON object variants
144
+ feature_config = client.fetch_object_value(
145
+ flag_key: 'homepage-layout',
146
+ default_value: { 'layout' => 'grid', 'items_per_row' => 3 }
147
+ )
148
+ ```
149
+
150
+ ### Getting Full Resolution Details
151
+
152
+ If you need additional metadata about the flag evaluation:
153
+
154
+ ```ruby
155
+ client = OpenFeature::SDK.build_client
156
+
157
+ details = client.fetch_boolean_details(flag_key: 'my-feature', default_value: false)
158
+
159
+ puts details.value # The resolved value
160
+ puts details.variant # The variant key from Mixpanel
161
+ puts details.reason # Why this value was returned
162
+ puts details.error_code # Error code if evaluation failed
163
+ ```
164
+
165
+ ### Passing Evaluation Context
166
+
167
+ You can pass evaluation context to provide additional properties for flag evaluation:
168
+
169
+ ```ruby
170
+ context = OpenFeature::SDK::EvaluationContext.new(
171
+ targeting_key: 'user-123',
172
+ email: 'user@example.com',
173
+ plan: 'premium'
174
+ )
175
+
176
+ value = client.fetch_boolean_value(
177
+ flag_key: 'premium-feature',
178
+ default_value: false,
179
+ evaluation_context: context
180
+ )
181
+ ```
182
+
183
+ ### Accessing the Mixpanel Tracker
184
+
185
+ When using the `from_local` or `from_remote` class methods, you can access the underlying Mixpanel tracker for tracking events:
186
+
187
+ ```ruby
188
+ provider = Mixpanel::OpenFeature::Provider.from_local('YOUR_PROJECT_TOKEN', {})
189
+
190
+ # Access the tracker for event tracking
191
+ provider.mixpanel.track('user-123', 'Page View', { 'page' => '/home' })
192
+ ```
193
+
194
+ ## Cleanup
195
+
196
+ When you are done using the provider, shut it down to stop any background polling:
197
+
198
+ ```ruby
199
+ provider.shutdown
200
+ ```
201
+
202
+ ## Error Handling
203
+
204
+ The provider uses OpenFeature's standard error codes to indicate issues during flag evaluation:
205
+
206
+ ### PROVIDER_NOT_READY
207
+
208
+ Returned when flags are evaluated before the provider has finished initializing (e.g., before flag definitions have been fetched for local evaluation).
209
+
210
+ ### FLAG_NOT_FOUND
211
+
212
+ Returned when the requested flag does not exist in Mixpanel.
213
+
214
+ ```ruby
215
+ details = client.fetch_boolean_details(flag_key: 'nonexistent-flag', default_value: false)
216
+
217
+ if details.error_code == OpenFeature::SDK::Provider::ErrorCode::FLAG_NOT_FOUND
218
+ puts 'Flag does not exist, using default value'
219
+ end
220
+ ```
221
+
222
+ ### TYPE_MISMATCH
223
+
224
+ Returned when the flag value type does not match the requested type.
225
+
226
+ ```ruby
227
+ # If 'my-flag' returns a string in Mixpanel but you request a boolean...
228
+ details = client.fetch_boolean_details(flag_key: 'my-flag', default_value: false)
229
+
230
+ if details.error_code == OpenFeature::SDK::Provider::ErrorCode::TYPE_MISMATCH
231
+ puts 'Flag is not a boolean, using default value'
232
+ end
233
+ ```
234
+
235
+ ## FAQ
236
+
237
+ ### What Ruby versions are supported?
238
+
239
+ Ruby 3.1.0 or later is required.
240
+
241
+ ### What is the difference between local and remote evaluation?
242
+
243
+ **Local evaluation** downloads all flag definitions upfront and evaluates them in-process. This is faster (no network round-trip per evaluation) and works offline after the initial fetch. Use `from_local` for this mode.
244
+
245
+ **Remote evaluation** sends each flag check to Mixpanel's servers. This ensures you always have the latest flag values and supports server-side-only targeting rules. Use `from_remote` for this mode.
246
+
247
+ ### Does targetingKey have special meaning?
248
+
249
+ Unlike some feature flag providers, `targetingKey` is not used as a special bucketing key in Mixpanel. It is passed as another context property alongside all other fields. Mixpanel's server-side configuration determines which properties are used for targeting rules and bucketing.
250
+
251
+ ### Does the provider call mixpanel.identify()?
252
+
253
+ No. User identity should be managed separately through the Mixpanel tracker's `track` or `people` methods. The provider only handles feature flag evaluation.
254
+
255
+ ### How are exposure events tracked?
256
+
257
+ When a flag is successfully resolved, the provider automatically reports an exposure event via `report_exposure: true`. This tracks `$experiment_started` events in Mixpanel for analytics and experimentation reporting.
258
+
259
+ ### Can I use this with Rails?
260
+
261
+ Yes. A common pattern is to initialize the provider in an initializer:
262
+
263
+ ```ruby
264
+ # config/initializers/openfeature.rb
265
+ provider = Mixpanel::OpenFeature::Provider.from_local(
266
+ ENV['MIXPANEL_TOKEN'],
267
+ { poll_interval: 300 }
268
+ )
269
+
270
+ OpenFeature::SDK.configure do |config|
271
+ config.set_provider(provider)
272
+ end
273
+
274
+ at_exit { provider.shutdown }
275
+ ```
276
+
277
+ Then use it in controllers or services:
278
+
279
+ ```ruby
280
+ client = OpenFeature::SDK.build_client
281
+ show_banner = client.fetch_boolean_value(flag_key: 'show-banner', default_value: false)
282
+ ```
283
+
284
+ ## License
285
+
286
+ Apache-2.0