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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +66 -0
- data/Readme.rdoc +8 -2
- data/demo/flags/local_flags.rb +25 -0
- data/demo/flags/remote_flags.rb +18 -0
- data/lib/mixpanel-ruby/events.rb +2 -2
- data/lib/mixpanel-ruby/flags/flags_provider.rb +115 -0
- data/lib/mixpanel-ruby/flags/local_flags_provider.rb +300 -0
- data/lib/mixpanel-ruby/flags/remote_flags_provider.rb +134 -0
- data/lib/mixpanel-ruby/flags/types.rb +35 -0
- data/lib/mixpanel-ruby/flags/utils.rb +65 -0
- data/lib/mixpanel-ruby/groups.rb +1 -1
- data/lib/mixpanel-ruby/people.rb +1 -1
- data/lib/mixpanel-ruby/tracker.rb +32 -2
- data/lib/mixpanel-ruby/version.rb +1 -1
- data/lib/mixpanel-ruby.rb +5 -0
- data/mixpanel-ruby.gemspec +10 -3
- data/openfeature-provider/Gemfile +7 -0
- data/openfeature-provider/README.md +286 -0
- data/openfeature-provider/RELEASE.md +52 -0
- data/openfeature-provider/lib/mixpanel/openfeature/provider.rb +170 -0
- data/openfeature-provider/lib/mixpanel/openfeature.rb +3 -0
- data/openfeature-provider/mixpanel-ruby-openfeature.gemspec +23 -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/events_spec.rb +2 -2
- data/spec/mixpanel-ruby/flags/local_flags_spec.rb +759 -0
- data/spec/mixpanel-ruby/flags/remote_flags_spec.rb +441 -0
- data/spec/mixpanel-ruby/flags/utils_spec.rb +110 -0
- data/spec/mixpanel-ruby/groups_spec.rb +10 -10
- data/spec/mixpanel-ruby/tracker_spec.rb +5 -5
- data/spec/spec_helper.rb +14 -0
- metadata +125 -9
- 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
|
data/lib/mixpanel-ruby/groups.rb
CHANGED
data/lib/mixpanel-ruby/people.rb
CHANGED
|
@@ -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)
|
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'
|
data/mixpanel-ruby.gemspec
CHANGED
|
@@ -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 = '>=
|
|
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', '~>
|
|
21
|
+
spec.add_development_dependency 'rake', '~> 13'
|
|
19
22
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
|
20
|
-
spec.add_development_dependency 'webmock', '~>
|
|
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,286 @@
|
|
|
1
|
+
# mixpanel-ruby-openfeature
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/mixpanel-ruby-openfeature)
|
|
4
|
+
[](https://openfeature.dev/)
|
|
5
|
+
[](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
|