mixpanel-ruby 3.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 99f6ea359c1b7a0f4248035f2ebe9a13380ee647b20722f73f49f45b3bb62ea3
4
- data.tar.gz: c1dc8bf0e37d90a397654b263059458291eff144987b87bc3a9dcc153649c42e
3
+ metadata.gz: 6d3a5c579488b2cbe1c3b2c6e489070a0f438dca91b8e5f91d863709c517c978
4
+ data.tar.gz: '089600f43e3c29ef4f83d129dd1f8f58682ae041b1ffe776fad3ff3036140a5e'
5
5
  SHA512:
6
- metadata.gz: cee44f99c3d4000ee9eb64a0353abcee02bec2a422d0a27c9cda160c8bb50db3f6abe72c550730633c719a51eb06f92dbba5e38cc17f7266a82bf8561488005e
7
- data.tar.gz: db60ba774c9e32e2bb4199d46a3c192a40d35d3aaef84fdb87c0e29a7a66f76b4f4dbd0c6e2f28d5d7145844fadb91b9ccefb4bd82d2d9e2f287153182fcf176
6
+ metadata.gz: 81511ab17dfe9e72ffeb81a97752d82a581297dc002aa722ee50e2d12a5faa835f7dc198ea1be5b2b62cec7cb117fd8906177b427d8e8e9eddfab21f24c5d930
7
+ data.tar.gz: 97bac85d2cb9edd8dc4655f3657a42ff4a1c56fcb0f6fd983da8bb5b44baec1c684a63afd16f5612a02c545e53dd84d07853416f8aab79a0ddd00ac88dfbe2dd
@@ -22,7 +22,7 @@ jobs:
22
22
  - '3.4'
23
23
 
24
24
  steps:
25
- - uses: actions/checkout@v6
25
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
26
26
  - name: Set up Ruby
27
27
  uses: ruby/setup-ruby@d697be2f83c6234b20877c3b5eac7a7f342f0d0c # v1.269.0
28
28
  with:
@@ -31,7 +31,36 @@ jobs:
31
31
  - name: Run tests
32
32
  run: bundle exec rake
33
33
  - name: Upload coverage reports to Codecov
34
- uses: codecov/codecov-action@v5
34
+ uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5
35
35
  with:
36
36
  token: ${{ secrets.CODECOV_TOKEN }}
37
37
  slug: mixpanel/mixpanel-ruby
38
+
39
+ test-openfeature:
40
+
41
+ runs-on: ubuntu-latest
42
+ strategy:
43
+ matrix:
44
+ ruby-version:
45
+ - '3.1'
46
+ - '3.2'
47
+ - '3.3'
48
+ - '3.4'
49
+
50
+ steps:
51
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
52
+ - name: Set up Ruby
53
+ uses: ruby/setup-ruby@d697be2f83c6234b20877c3b5eac7a7f342f0d0c # v1.269.0
54
+ with:
55
+ ruby-version: ${{ matrix.ruby-version }}
56
+ - name: Install dependencies
57
+ run: cd openfeature-provider && bundle install
58
+ - name: Run OpenFeature provider tests
59
+ run: cd openfeature-provider && bundle exec rspec
60
+ - name: Upload coverage reports to Codecov
61
+ uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5
62
+ with:
63
+ token: ${{ secrets.CODECOV_TOKEN }}
64
+ slug: mixpanel/mixpanel-ruby
65
+ flags: openfeature
66
+ directory: openfeature-provider
@@ -56,25 +56,29 @@ module Mixpanel
56
56
  request.basic_auth(@provider_config[:token], '')
57
57
 
58
58
  request['Content-Type'] = 'application/json'
59
- request['traceparent'] = Utils.generate_traceparent()
59
+ request['traceparent'] = Utils.generate_traceparent
60
60
 
61
61
  begin
62
62
  response = http.request(request)
63
+ rescue Net::OpenTimeout, Net::ReadTimeout => e
64
+ raise ConnectionError.new("Request timeout: #{e.message}")
65
+ rescue StandardError => e
66
+ raise ConnectionError.new("Network error: #{e.message}")
67
+ end
63
68
 
64
- unless response.code.to_i == 200
65
- raise ServerError.new("HTTP #{response.code}: #{response.body}")
66
- end
69
+ unless response.code == '200'
70
+ raise ServerError.new("HTTP #{response.code}: #{response.body}")
71
+ end
67
72
 
73
+ begin
68
74
  JSON.parse(response.body)
69
- rescue Net::OpenTimeout, Net::ReadTimeout => e
70
- raise ConnectionError.new("Request timeout: #{e.message}")
71
75
  rescue JSON::ParserError => e
72
76
  raise ServerError.new("Invalid JSON response: #{e.message}")
73
- rescue StandardError => e
74
- raise ConnectionError.new("Network error: #{e.message}")
75
77
  end
76
78
  end
77
79
 
80
+ def shutdown; end
81
+
78
82
  # Track exposure event to Mixpanel
79
83
  # @param flag_key [String] Feature flag key
80
84
  # @param selected_variant [SelectedVariant] The selected variant
@@ -64,6 +64,10 @@ module Mixpanel
64
64
  @polling_thread = nil
65
65
  end
66
66
 
67
+ def shutdown
68
+ stop_polling_for_definitions!
69
+ end
70
+
67
71
  # Check if flag is enabled (for boolean flags)
68
72
  # @param flag_key [String] Feature flag key
69
73
  # @param context [Hash] Evaluation context (must include 'distinct_id')
@@ -107,24 +111,17 @@ module Mixpanel
107
111
 
108
112
  context_value = context[context_key] || context[context_key.to_sym]
109
113
 
110
- selected_variant = nil
114
+ selected_variant = get_variant_override_for_test_user(flag, context)
111
115
 
112
- test_variant = get_variant_override_for_test_user(flag, context)
113
- if test_variant
114
- selected_variant = test_variant
115
- else
116
+ unless selected_variant
116
117
  rollout = get_assigned_rollout(flag, context_value, context)
117
- if rollout
118
- selected_variant = get_assigned_variant(flag, context_value, flag_key, rollout)
119
- end
118
+ selected_variant = get_assigned_variant(flag, context_value, flag_key, rollout) if rollout
120
119
  end
121
120
 
122
- if selected_variant
123
- track_exposure_event(flag_key, selected_variant, context) if report_exposure
124
- return selected_variant
125
- end
121
+ return fallback_variant unless selected_variant
126
122
 
127
- fallback_variant
123
+ track_exposure_event(flag_key, selected_variant, context) if report_exposure
124
+ selected_variant
128
125
  end
129
126
 
130
127
  # Get all variants for user context
@@ -147,11 +144,9 @@ module Mixpanel
147
144
  def fetch_flag_definitions
148
145
  response = call_flags_endpoint
149
146
 
150
- new_definitions = {}
151
- (response['flags'] || []).each do |flag_data|
152
- new_definitions[flag_data['key']] = flag_data
147
+ new_definitions = (response['flags'] || []).each_with_object({}) do |flag_data, definitions|
148
+ definitions[flag_data['key']] = flag_data
153
149
  end
154
-
155
150
  @flag_definitions = new_definitions
156
151
 
157
152
  response
@@ -175,9 +170,10 @@ module Mixpanel
175
170
  end
176
171
 
177
172
  def get_matching_variant(variant_key, flag)
178
- return nil unless flag['ruleset'] && flag['ruleset']['variants']
173
+ variants = flag.dig('ruleset', 'variants')
174
+ return nil unless variants
179
175
 
180
- flag['ruleset']['variants'].each do |v|
176
+ variants.each do |v|
181
177
  if variant_key.downcase == v['key'].downcase
182
178
  return SelectedVariant.new(
183
179
  variant_key: v['key'],
@@ -191,9 +187,10 @@ module Mixpanel
191
187
  end
192
188
 
193
189
  def get_assigned_rollout(flag, context_value, context)
194
- return nil unless flag['ruleset'] && flag['ruleset']['rollout']
190
+ rollouts = flag.dig('ruleset', 'rollout')
191
+ return nil unless rollouts
195
192
 
196
- flag['ruleset']['rollout'].each_with_index do |rollout, index|
193
+ rollouts.each_with_index do |rollout, index|
197
194
  salt = if flag['hash_salt']
198
195
  "#{flag['key']}#{flag['hash_salt']}#{index}"
199
196
  else
@@ -224,7 +221,7 @@ module Mixpanel
224
221
  salt = "#{flag_key}#{stored_salt}variant"
225
222
  variant_hash = Utils.normalized_hash(context_value.to_s, salt)
226
223
 
227
- variants = flag['ruleset']['variants'].map { |v| v.dup }
224
+ variants = flag['ruleset']['variants'].map(&:dup)
228
225
  if rollout['variant_splits']
229
226
  variants.each do |v|
230
227
  v['split'] = rollout['variant_splits'][v['key']] if rollout['variant_splits'].key?(v['key'])
@@ -1,3 +1,3 @@
1
1
  module Mixpanel
2
- VERSION = '3.0.0'
2
+ VERSION = '3.1.0'
3
3
  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
@@ -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.