mixpanel-ruby 3.0.0 → 3.2.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +14 -0
  3. data/.github/modules.json +18 -0
  4. data/.github/scripts/generate-changelog.sh +87 -0
  5. data/.github/workflows/pr-title-check.yml +51 -0
  6. data/.github/workflows/prepare-release.yml +189 -0
  7. data/.github/workflows/release-rubygems.yml +217 -0
  8. data/.github/workflows/ruby.yml +31 -2
  9. data/CHANGELOG.md +30 -0
  10. data/Readme.rdoc +65 -0
  11. data/lib/mixpanel-ruby/consumer.rb +54 -6
  12. data/lib/mixpanel-ruby/credentials.rb +26 -0
  13. data/lib/mixpanel-ruby/events.rb +69 -11
  14. data/lib/mixpanel-ruby/flags/flags_provider.rb +28 -10
  15. data/lib/mixpanel-ruby/flags/local_flags_provider.rb +104 -40
  16. data/lib/mixpanel-ruby/flags/remote_flags_provider.rb +4 -2
  17. data/lib/mixpanel-ruby/tracker.rb +39 -9
  18. data/lib/mixpanel-ruby/version.rb +1 -1
  19. data/lib/mixpanel-ruby.rb +1 -0
  20. data/openfeature-provider/CHANGELOG.md +5 -0
  21. data/openfeature-provider/Gemfile +7 -0
  22. data/openfeature-provider/README.md +288 -0
  23. data/openfeature-provider/RELEASE.md +52 -0
  24. data/openfeature-provider/lib/mixpanel/openfeature/provider.rb +170 -0
  25. data/openfeature-provider/lib/mixpanel/openfeature/version.rb +7 -0
  26. data/openfeature-provider/lib/mixpanel/openfeature.rb +4 -0
  27. data/openfeature-provider/mixpanel-ruby-openfeature.gemspec +25 -0
  28. data/openfeature-provider/spec/mixpanel_openfeature_provider_spec.rb +606 -0
  29. data/openfeature-provider/spec/spec_helper.rb +23 -0
  30. data/spec/mixpanel-ruby/consumer_spec.rb +81 -0
  31. data/spec/mixpanel-ruby/credentials_security_spec.rb +108 -0
  32. data/spec/mixpanel-ruby/credentials_spec.rb +54 -0
  33. data/spec/mixpanel-ruby/events_spec.rb +152 -0
  34. data/spec/mixpanel-ruby/flags/local_flags_spec.rb +209 -2
  35. data/spec/mixpanel-ruby/flags/remote_flags_spec.rb +36 -0
  36. data/spec/mixpanel-ruby/tracker_spec.rb +18 -0
  37. metadata +23 -3
@@ -62,9 +62,14 @@ module Mixpanel
62
62
  # If a block is provided, it is passed a type (one of :event or :profile_update)
63
63
  # and a string message. This same format is accepted by Mixpanel::Consumer#send!
64
64
  # and Mixpanel::BufferedConsumer#send!
65
- def initialize(token, error_handler=nil, local_flags_config: nil, remote_flags_config: nil, &block)
66
- super(token, error_handler, &block)
67
- @token = token
65
+ #
66
+ # Optional parameters:
67
+ # - credentials: ServiceAccountCredentials for authentication (used for import and feature flags)
68
+ # - local_flags_config: Configuration hash for local feature flags
69
+ # - remote_flags_config: Configuration hash for remote feature flags
70
+ def initialize(token, error_handler=nil, credentials: nil, local_flags_config: nil, remote_flags_config: nil, &block)
71
+ super(token, error_handler, credentials: credentials, &block)
72
+
68
73
  @people = People.new(token, error_handler, &block)
69
74
  @groups = Groups.new(token, error_handler, &block)
70
75
 
@@ -74,7 +79,8 @@ module Mixpanel
74
79
  token,
75
80
  local_flags_config,
76
81
  method(:track), # Pass bound method as callback
77
- error_handler || ErrorHandler.new
82
+ error_handler || ErrorHandler.new,
83
+ credentials
78
84
  )
79
85
  end
80
86
 
@@ -84,7 +90,8 @@ module Mixpanel
84
90
  token,
85
91
  remote_flags_config,
86
92
  method(:track), # Pass bound method as callback
87
- error_handler || ErrorHandler.new
93
+ error_handler || ErrorHandler.new,
94
+ credentials
88
95
  )
89
96
  end
90
97
  end
@@ -122,14 +129,15 @@ module Mixpanel
122
129
  #
123
130
  # tracker = Mixpanel::Tracker.new(YOUR_MIXPANEL_TOKEN)
124
131
  #
125
- # # Import event that user "12345"'s credit card was declined
132
+ # # Using deprecated API key (still supported)
126
133
  # tracker.import("API_KEY", "12345", "Credit Card Declined", {
127
134
  # 'time' => 1310111365
128
135
  # })
129
136
  #
130
- # # Properties describe the circumstances of the event,
131
- # # or aspects of the source or user associated with the event
132
- # tracker.import("API_KEY", "12345", "Welcome Email Sent", {
137
+ # # Using service account credentials (recommended)
138
+ # credentials = Mixpanel::ServiceAccountCredentials.new(username, secret, project_id)
139
+ # tracker = Mixpanel::Tracker.new(YOUR_MIXPANEL_TOKEN, credentials: credentials)
140
+ # tracker.import(nil, "12345", "Welcome Email Sent", {
133
141
  # 'Email Template' => 'Pretty Pink Welcome',
134
142
  # 'User Sign-up Cohort' => 'July 2013',
135
143
  # 'time' => 1310111365
@@ -140,6 +148,28 @@ module Mixpanel
140
148
  super
141
149
  end
142
150
 
151
+ # Import an event using service account credentials from the constructor.
152
+ # This is the recommended method for importing historical events with service accounts.
153
+ #
154
+ # Service account credentials must be provided when creating the Tracker.
155
+ # This method provides a cleaner API than import() as it doesn't require
156
+ # passing nil as the first parameter.
157
+ #
158
+ # credentials = Mixpanel::ServiceAccountCredentials.new(username, secret, project_id)
159
+ # tracker = Mixpanel::Tracker.new(YOUR_MIXPANEL_TOKEN, credentials: credentials)
160
+ #
161
+ # # Import a historical event
162
+ # tracker.import_events('user123', 'Past Event', {
163
+ # 'Email Template' => 'Welcome Email',
164
+ # 'time' => 1369353600
165
+ # })
166
+ #
167
+ def import_events(distinct_id, event, properties={}, ip=nil)
168
+ # This is here strictly to allow rdoc to include the relevant
169
+ # documentation
170
+ super
171
+ end
172
+
143
173
  # Creates a distinct_id alias. \Events and updates with an alias
144
174
  # will be considered by mixpanel to have the same source, and
145
175
  # refer to the same profile.
@@ -1,3 +1,3 @@
1
1
  module Mixpanel
2
- VERSION = '3.0.0'
2
+ VERSION = '3.2.0'
3
3
  end
data/lib/mixpanel-ruby.rb CHANGED
@@ -1,6 +1,7 @@
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/credentials.rb'
4
5
  require 'mixpanel-ruby/flags/utils.rb'
5
6
  require 'mixpanel-ruby/flags/types.rb'
6
7
  require 'mixpanel-ruby/flags/flags_provider.rb'
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## [openfeature/v0.1.0](https://github.com/mixpanel/mixpanel-ruby/tree/openfeature/v0.1.0) (2026-05-13)
4
+
5
+ Initial release of the Mixpanel OpenFeature provider for Ruby.
@@ -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,288 @@
1
+ # mixpanel-ruby-openfeature
2
+
3
+ ##### _May 13, 2026_ - [openfeature/v0.1.0](https://github.com/mixpanel/mixpanel-ruby/releases/tag/openfeature/v0.1.0)
4
+
5
+ [![Gem Version](https://img.shields.io/gem/v/mixpanel-ruby-openfeature.svg)](https://rubygems.org/gems/mixpanel-ruby-openfeature)
6
+ [![OpenFeature](https://img.shields.io/badge/OpenFeature-compatible-green)](https://openfeature.dev/)
7
+ [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](https://github.com/mixpanel/mixpanel-ruby/blob/master/LICENSE)
8
+
9
+ 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.
10
+
11
+ ## Overview
12
+
13
+ This gem provides a bridge between Mixpanel's native feature flags implementation and the OpenFeature specification. By using this provider, you can:
14
+
15
+ - Leverage Mixpanel's powerful feature flag and experimentation platform
16
+ - Use OpenFeature's standardized API for flag evaluation
17
+ - Easily switch between feature flag providers without changing your application code
18
+ - Integrate with OpenFeature's ecosystem of tools and frameworks
19
+
20
+ ## Installation
21
+
22
+ Add these gems to your `Gemfile`:
23
+
24
+ ```ruby
25
+ gem 'mixpanel-ruby-openfeature'
26
+ gem 'openfeature-sdk'
27
+ gem 'mixpanel-ruby'
28
+ ```
29
+
30
+ Then run:
31
+
32
+ ```bash
33
+ bundle install
34
+ ```
35
+
36
+ Or install directly:
37
+
38
+ ```bash
39
+ gem install mixpanel-ruby-openfeature
40
+ ```
41
+
42
+ ## Quick Start
43
+
44
+ ### Local Evaluation (Recommended)
45
+
46
+ Local evaluation downloads flag definitions and evaluates them locally, providing fast, synchronous flag checks with no per-evaluation network requests.
47
+
48
+ ```ruby
49
+ require 'mixpanel-ruby'
50
+ require 'mixpanel/openfeature'
51
+
52
+ # 1. Create the provider with local evaluation
53
+ provider = Mixpanel::OpenFeature::Provider.from_local(
54
+ 'YOUR_PROJECT_TOKEN',
55
+ { poll_interval: 300 } # poll for updated definitions every 300 seconds
56
+ )
57
+
58
+ # 2. Register the provider with OpenFeature
59
+ OpenFeature::SDK.configure do |config|
60
+ config.set_provider(provider)
61
+ end
62
+
63
+ # 3. Get a client and evaluate flags
64
+ client = OpenFeature::SDK.build_client
65
+
66
+ show_new_feature = client.fetch_boolean_value(flag_key: 'new-feature-flag', default_value: false)
67
+
68
+ if show_new_feature
69
+ puts 'New feature is enabled!'
70
+ end
71
+ ```
72
+
73
+ ### Remote Evaluation
74
+
75
+ 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.
76
+
77
+ ```ruby
78
+ require 'mixpanel-ruby'
79
+ require 'mixpanel/openfeature'
80
+
81
+ # 1. Create the provider with remote evaluation
82
+ provider = Mixpanel::OpenFeature::Provider.from_remote(
83
+ 'YOUR_PROJECT_TOKEN',
84
+ {} # remote config options
85
+ )
86
+
87
+ # 2. Register the provider with OpenFeature
88
+ OpenFeature::SDK.configure do |config|
89
+ config.set_provider(provider)
90
+ end
91
+
92
+ # 3. Evaluate flags the same way
93
+ client = OpenFeature::SDK.build_client
94
+ value = client.fetch_string_value(flag_key: 'button-color-test', default_value: 'blue')
95
+ ```
96
+
97
+ ### Manual Initialization
98
+
99
+ If you already have a Mixpanel `Tracker` instance, you can pass its flags provider directly:
100
+
101
+ ```ruby
102
+ tracker = Mixpanel::Tracker.new('YOUR_PROJECT_TOKEN', nil, local_flags_config: { poll_interval: 300 })
103
+ flags_provider = tracker.local_flags
104
+ flags_provider.start_polling_for_definitions!
105
+
106
+ provider = Mixpanel::OpenFeature::Provider.new(flags_provider)
107
+ ```
108
+
109
+ ## Usage Examples
110
+
111
+ ### Basic Boolean Flag
112
+
113
+ ```ruby
114
+ client = OpenFeature::SDK.build_client
115
+
116
+ is_feature_enabled = client.fetch_boolean_value(flag_key: 'my-feature', default_value: false)
117
+
118
+ if is_feature_enabled
119
+ # Show the new feature
120
+ end
121
+ ```
122
+
123
+ ### Mixpanel Flag Types and OpenFeature Evaluation Methods
124
+
125
+ Mixpanel feature flags support three flag types. Use the corresponding OpenFeature evaluation method based on your flag's variant values:
126
+
127
+ | Mixpanel Flag Type | Variant Values | OpenFeature Method |
128
+ |---|---|---|
129
+ | Feature Gate | `true` / `false` | `fetch_boolean_value` |
130
+ | Experiment | boolean, string, number, or JSON object | `fetch_boolean_value`, `fetch_string_value`, `fetch_number_value`, or `fetch_object_value` |
131
+ | Dynamic Config | JSON object | `fetch_object_value` |
132
+
133
+ ```ruby
134
+ client = OpenFeature::SDK.build_client
135
+
136
+ # Feature Gate - boolean variants
137
+ is_feature_on = client.fetch_boolean_value(flag_key: 'new-checkout', default_value: false)
138
+
139
+ # Experiment with string variants
140
+ button_color = client.fetch_string_value(flag_key: 'button-color-test', default_value: 'blue')
141
+
142
+ # Experiment with number variants
143
+ max_items = client.fetch_number_value(flag_key: 'max-items', default_value: 10)
144
+
145
+ # Dynamic Config - JSON object variants
146
+ feature_config = client.fetch_object_value(
147
+ flag_key: 'homepage-layout',
148
+ default_value: { 'layout' => 'grid', 'items_per_row' => 3 }
149
+ )
150
+ ```
151
+
152
+ ### Getting Full Resolution Details
153
+
154
+ If you need additional metadata about the flag evaluation:
155
+
156
+ ```ruby
157
+ client = OpenFeature::SDK.build_client
158
+
159
+ details = client.fetch_boolean_details(flag_key: 'my-feature', default_value: false)
160
+
161
+ puts details.value # The resolved value
162
+ puts details.variant # The variant key from Mixpanel
163
+ puts details.reason # Why this value was returned
164
+ puts details.error_code # Error code if evaluation failed
165
+ ```
166
+
167
+ ### Passing Evaluation Context
168
+
169
+ You can pass evaluation context to provide additional properties for flag evaluation:
170
+
171
+ ```ruby
172
+ context = OpenFeature::SDK::EvaluationContext.new(
173
+ targeting_key: 'user-123',
174
+ email: 'user@example.com',
175
+ plan: 'premium'
176
+ )
177
+
178
+ value = client.fetch_boolean_value(
179
+ flag_key: 'premium-feature',
180
+ default_value: false,
181
+ evaluation_context: context
182
+ )
183
+ ```
184
+
185
+ ### Accessing the Mixpanel Tracker
186
+
187
+ When using the `from_local` or `from_remote` class methods, you can access the underlying Mixpanel tracker for tracking events:
188
+
189
+ ```ruby
190
+ provider = Mixpanel::OpenFeature::Provider.from_local('YOUR_PROJECT_TOKEN', {})
191
+
192
+ # Access the tracker for event tracking
193
+ provider.mixpanel.track('user-123', 'Page View', { 'page' => '/home' })
194
+ ```
195
+
196
+ ## Cleanup
197
+
198
+ When you are done using the provider, shut it down to stop any background polling:
199
+
200
+ ```ruby
201
+ provider.shutdown
202
+ ```
203
+
204
+ ## Error Handling
205
+
206
+ The provider uses OpenFeature's standard error codes to indicate issues during flag evaluation:
207
+
208
+ ### PROVIDER_NOT_READY
209
+
210
+ Returned when flags are evaluated before the provider has finished initializing (e.g., before flag definitions have been fetched for local evaluation).
211
+
212
+ ### FLAG_NOT_FOUND
213
+
214
+ Returned when the requested flag does not exist in Mixpanel.
215
+
216
+ ```ruby
217
+ details = client.fetch_boolean_details(flag_key: 'nonexistent-flag', default_value: false)
218
+
219
+ if details.error_code == OpenFeature::SDK::Provider::ErrorCode::FLAG_NOT_FOUND
220
+ puts 'Flag does not exist, using default value'
221
+ end
222
+ ```
223
+
224
+ ### TYPE_MISMATCH
225
+
226
+ Returned when the flag value type does not match the requested type.
227
+
228
+ ```ruby
229
+ # If 'my-flag' returns a string in Mixpanel but you request a boolean...
230
+ details = client.fetch_boolean_details(flag_key: 'my-flag', default_value: false)
231
+
232
+ if details.error_code == OpenFeature::SDK::Provider::ErrorCode::TYPE_MISMATCH
233
+ puts 'Flag is not a boolean, using default value'
234
+ end
235
+ ```
236
+
237
+ ## FAQ
238
+
239
+ ### What Ruby versions are supported?
240
+
241
+ Ruby 3.1.0 or later is required.
242
+
243
+ ### What is the difference between local and remote evaluation?
244
+
245
+ **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.
246
+
247
+ **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.
248
+
249
+ ### Does targetingKey have special meaning?
250
+
251
+ 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.
252
+
253
+ ### Does the provider call mixpanel.identify()?
254
+
255
+ No. User identity should be managed separately through the Mixpanel tracker's `track` or `people` methods. The provider only handles feature flag evaluation.
256
+
257
+ ### How are exposure events tracked?
258
+
259
+ 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.
260
+
261
+ ### Can I use this with Rails?
262
+
263
+ Yes. A common pattern is to initialize the provider in an initializer:
264
+
265
+ ```ruby
266
+ # config/initializers/openfeature.rb
267
+ provider = Mixpanel::OpenFeature::Provider.from_local(
268
+ ENV['MIXPANEL_TOKEN'],
269
+ { poll_interval: 300 }
270
+ )
271
+
272
+ OpenFeature::SDK.configure do |config|
273
+ config.set_provider(provider)
274
+ end
275
+
276
+ at_exit { provider.shutdown }
277
+ ```
278
+
279
+ Then use it in controllers or services:
280
+
281
+ ```ruby
282
+ client = OpenFeature::SDK.build_client
283
+ show_banner = client.fetch_boolean_value(flag_key: 'show-banner', default_value: false)
284
+ ```
285
+
286
+ ## License
287
+
288
+ Apache-2.0
@@ -0,0 +1,52 @@
1
+ # Releasing the OpenFeature Provider
2
+
3
+ The OpenFeature provider (`mixpanel-ruby-openfeature`) is published to RubyGems independently from the core SDK.
4
+
5
+ ## Release Order
6
+
7
+ The OpenFeature provider depends on features added to `mixpanel-ruby` in version 3.1.0+ (e.g., `shutdown` methods on flags providers). You **must** publish the core SDK first:
8
+
9
+ 1. Publish `mixpanel-ruby` (bump version in `lib/mixpanel-ruby/version.rb`, build, push)
10
+ 2. Verify the new version is live on https://rubygems.org/gems/mixpanel-ruby
11
+ 3. Then publish `mixpanel-ruby-openfeature` (steps below)
12
+
13
+ If you update the core SDK version, update the dependency constraint in `mixpanel-ruby-openfeature.gemspec` to match.
14
+
15
+ ## Prerequisites
16
+
17
+ - Ruby 3.1+
18
+ - A RubyGems account with permission to push to the `mixpanel-ruby-openfeature` gem
19
+ - For the first upload, you'll need owner access or to create the gem under the Mixpanel org
20
+ - Sign in and get your API key at https://rubygems.org/profile/edit
21
+
22
+ ## Releasing
23
+
24
+ 1. Update the version in `mixpanel-ruby-openfeature.gemspec`
25
+
26
+ 2. Build the gem:
27
+ ```bash
28
+ cd openfeature-provider
29
+ gem build mixpanel-ruby-openfeature.gemspec
30
+ ```
31
+
32
+ 3. Verify the built artifact:
33
+ ```bash
34
+ ls *.gem
35
+ # Should show: mixpanel-ruby-openfeature-<version>.gem
36
+ ```
37
+
38
+ 4. Push to RubyGems:
39
+ ```bash
40
+ gem push mixpanel-ruby-openfeature-<version>.gem
41
+ ```
42
+ You'll be prompted for your RubyGems credentials on first push. Alternatively, configure `~/.gem/credentials` with your API key:
43
+ ```yaml
44
+ ---
45
+ :rubygems_api_key: rubygems_<your-key>
46
+ ```
47
+
48
+ 5. Verify at https://rubygems.org/gems/mixpanel-ruby-openfeature
49
+
50
+ ## Versioning
51
+
52
+ The OpenFeature provider is versioned independently from the core SDK. The core SDK dependency is declared in the gemspec (`mixpanel-ruby ~> 3.0`) — update it when the provider needs features from a newer core SDK release.
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open_feature/sdk'
4
+
5
+ module Mixpanel
6
+ module OpenFeature
7
+ class Provider
8
+ attr_reader :metadata, :mixpanel
9
+
10
+ def self.from_local(token, config, error_handler: nil)
11
+ tracker = ::Mixpanel::Tracker.new(token, error_handler, local_flags_config: config)
12
+ flags_provider = tracker.local_flags
13
+ flags_provider.start_polling_for_definitions!
14
+ provider = new(flags_provider)
15
+ provider.instance_variable_set(:@mixpanel, tracker)
16
+ provider
17
+ end
18
+
19
+ def self.from_remote(token, config, error_handler: nil)
20
+ tracker = ::Mixpanel::Tracker.new(token, error_handler, remote_flags_config: config)
21
+ flags_provider = tracker.remote_flags
22
+ provider = new(flags_provider)
23
+ provider.instance_variable_set(:@mixpanel, tracker)
24
+ provider
25
+ end
26
+
27
+ def initialize(flags_provider)
28
+ @flags_provider = flags_provider
29
+ @metadata = ::OpenFeature::SDK::Provider::ProviderMetadata.new(name: 'mixpanel-provider').freeze
30
+ end
31
+
32
+ def shutdown
33
+ @flags_provider.shutdown if @flags_provider.respond_to?(:shutdown)
34
+ end
35
+
36
+ def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
37
+ resolve(flag_key, default_value, :boolean, evaluation_context)
38
+ end
39
+
40
+ def fetch_string_value(flag_key:, default_value:, evaluation_context: nil)
41
+ resolve(flag_key, default_value, :string, evaluation_context)
42
+ end
43
+
44
+ def fetch_number_value(flag_key:, default_value:, evaluation_context: nil)
45
+ resolve(flag_key, default_value, :number, evaluation_context)
46
+ end
47
+
48
+ def fetch_integer_value(flag_key:, default_value:, evaluation_context: nil)
49
+ resolve(flag_key, default_value, :integer, evaluation_context)
50
+ end
51
+
52
+ def fetch_float_value(flag_key:, default_value:, evaluation_context: nil)
53
+ resolve(flag_key, default_value, :float, evaluation_context)
54
+ end
55
+
56
+ def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
57
+ resolve(flag_key, default_value, :object, evaluation_context)
58
+ end
59
+
60
+ private
61
+
62
+ def resolve(flag_key, default_value, expected_type, evaluation_context)
63
+ unless flags_ready?
64
+ return error_result(default_value, ::OpenFeature::SDK::Provider::ErrorCode::PROVIDER_NOT_READY)
65
+ end
66
+
67
+ context = build_context(evaluation_context)
68
+ fallback = ::Mixpanel::Flags::SelectedVariant.new(variant_value: default_value)
69
+
70
+ begin
71
+ result = @flags_provider.get_variant(flag_key, fallback, context, report_exposure: true)
72
+ rescue StandardError
73
+ return error_result(default_value, ::OpenFeature::SDK::Provider::ErrorCode::GENERAL)
74
+ end
75
+
76
+ if result.equal?(fallback)
77
+ return ::OpenFeature::SDK::Provider::ResolutionDetails.new(
78
+ value: default_value,
79
+ error_code: ::OpenFeature::SDK::Provider::ErrorCode::FLAG_NOT_FOUND,
80
+ reason: ::OpenFeature::SDK::Provider::Reason::DEFAULT
81
+ )
82
+ end
83
+
84
+ value = result.variant_value
85
+
86
+ if value.nil? && expected_type != :object
87
+ return error_result(default_value, ::OpenFeature::SDK::Provider::ErrorCode::TYPE_MISMATCH)
88
+ end
89
+
90
+ coerced = coerce_value(value, expected_type)
91
+ if coerced.nil? && !value.nil?
92
+ return error_result(default_value, ::OpenFeature::SDK::Provider::ErrorCode::TYPE_MISMATCH)
93
+ end
94
+
95
+ ::OpenFeature::SDK::Provider::ResolutionDetails.new(
96
+ value: coerced.nil? ? value : coerced,
97
+ variant: result.variant_key,
98
+ reason: ::OpenFeature::SDK::Provider::Reason::TARGETING_MATCH
99
+ )
100
+ end
101
+
102
+ def coerce_value(value, expected_type)
103
+ case expected_type
104
+ when :boolean
105
+ value == true || value == false ? value : nil
106
+ when :string
107
+ value.is_a?(String) ? value : nil
108
+ when :integer
109
+ if value.is_a?(Integer)
110
+ value
111
+ elsif value.is_a?(Float) && value.finite? && value == value.floor
112
+ value.to_i
113
+ end
114
+ when :float
115
+ if value.is_a?(Float)
116
+ value
117
+ elsif value.is_a?(Integer)
118
+ value.to_f
119
+ end
120
+ when :number
121
+ value.is_a?(Numeric) ? value : nil
122
+ when :object
123
+ value
124
+ end
125
+ end
126
+
127
+ def build_context(evaluation_context)
128
+ return {} if evaluation_context.nil?
129
+
130
+ ctx = {}
131
+ if evaluation_context.respond_to?(:fields)
132
+ evaluation_context.fields.each { |k, v| ctx[k] = unwrap_value(v) }
133
+ end
134
+ if evaluation_context.respond_to?(:targeting_key) && evaluation_context.targeting_key
135
+ ctx['targetingKey'] = unwrap_value(evaluation_context.targeting_key)
136
+ end
137
+ ctx
138
+ end
139
+
140
+ def unwrap_value(value)
141
+ case value
142
+ when Float
143
+ value.finite? && value == value.floor ? value.to_i : value
144
+ when Array
145
+ value.map { |v| unwrap_value(v) }
146
+ when Hash
147
+ value.transform_values { |v| unwrap_value(v) }
148
+ else
149
+ value
150
+ end
151
+ end
152
+
153
+ def flags_ready?
154
+ if @flags_provider.respond_to?(:are_flags_ready)
155
+ @flags_provider.are_flags_ready
156
+ else
157
+ true
158
+ end
159
+ end
160
+
161
+ def error_result(default_value, error_code)
162
+ ::OpenFeature::SDK::Provider::ResolutionDetails.new(
163
+ value: default_value,
164
+ error_code: error_code,
165
+ reason: ::OpenFeature::SDK::Provider::Reason::ERROR
166
+ )
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mixpanel
4
+ module OpenFeature
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'openfeature/version'
4
+ require_relative 'openfeature/provider'
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.join(File.dirname(__FILE__), 'lib/mixpanel/openfeature/version.rb')
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'mixpanel-ruby-openfeature'
7
+ spec.version = Mixpanel::OpenFeature::VERSION
8
+ spec.authors = ['Mixpanel']
9
+ spec.email = 'support@mixpanel.com'
10
+ spec.summary = 'OpenFeature provider for Mixpanel feature flags'
11
+ spec.description = 'An OpenFeature provider that wraps the Mixpanel Ruby SDK feature flags'
12
+ spec.homepage = 'https://mixpanel.com'
13
+ spec.license = 'Apache-2.0'
14
+ spec.required_ruby_version = '>= 3.1.0'
15
+
16
+ spec.files = Dir['lib/**/*.rb']
17
+ spec.require_paths = ['lib']
18
+
19
+ spec.add_runtime_dependency 'openfeature-sdk', '~> 0.5'
20
+ spec.add_runtime_dependency 'mixpanel-ruby', '~> 3.1'
21
+
22
+ spec.add_development_dependency 'rspec', '~> 3.0'
23
+ spec.add_development_dependency 'simplecov'
24
+ spec.add_development_dependency 'simplecov-cobertura'
25
+ end