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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +14 -0
- data/.github/modules.json +18 -0
- data/.github/scripts/generate-changelog.sh +87 -0
- data/.github/workflows/pr-title-check.yml +51 -0
- data/.github/workflows/prepare-release.yml +189 -0
- data/.github/workflows/release-rubygems.yml +217 -0
- data/.github/workflows/ruby.yml +31 -2
- data/CHANGELOG.md +30 -0
- data/Readme.rdoc +65 -0
- data/lib/mixpanel-ruby/consumer.rb +54 -6
- data/lib/mixpanel-ruby/credentials.rb +26 -0
- data/lib/mixpanel-ruby/events.rb +69 -11
- data/lib/mixpanel-ruby/flags/flags_provider.rb +28 -10
- data/lib/mixpanel-ruby/flags/local_flags_provider.rb +104 -40
- data/lib/mixpanel-ruby/flags/remote_flags_provider.rb +4 -2
- data/lib/mixpanel-ruby/tracker.rb +39 -9
- data/lib/mixpanel-ruby/version.rb +1 -1
- data/lib/mixpanel-ruby.rb +1 -0
- data/openfeature-provider/CHANGELOG.md +5 -0
- data/openfeature-provider/Gemfile +7 -0
- data/openfeature-provider/README.md +288 -0
- data/openfeature-provider/RELEASE.md +52 -0
- data/openfeature-provider/lib/mixpanel/openfeature/provider.rb +170 -0
- data/openfeature-provider/lib/mixpanel/openfeature/version.rb +7 -0
- data/openfeature-provider/lib/mixpanel/openfeature.rb +4 -0
- data/openfeature-provider/mixpanel-ruby-openfeature.gemspec +25 -0
- data/openfeature-provider/spec/mixpanel_openfeature_provider_spec.rb +606 -0
- data/openfeature-provider/spec/spec_helper.rb +23 -0
- data/spec/mixpanel-ruby/consumer_spec.rb +81 -0
- data/spec/mixpanel-ruby/credentials_security_spec.rb +108 -0
- data/spec/mixpanel-ruby/credentials_spec.rb +54 -0
- data/spec/mixpanel-ruby/events_spec.rb +152 -0
- data/spec/mixpanel-ruby/flags/local_flags_spec.rb +209 -2
- data/spec/mixpanel-ruby/flags/remote_flags_spec.rb +36 -0
- data/spec/mixpanel-ruby/tracker_spec.rb +18 -0
- 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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
# #
|
|
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
|
-
# #
|
|
131
|
-
#
|
|
132
|
-
# tracker.
|
|
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.
|
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,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
|
+
[](https://rubygems.org/gems/mixpanel-ruby-openfeature)
|
|
6
|
+
[](https://openfeature.dev/)
|
|
7
|
+
[](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,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
|