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,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,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = 'mixpanel-ruby-openfeature'
|
|
5
|
+
spec.version = '0.1.0'
|
|
6
|
+
spec.authors = ['Mixpanel']
|
|
7
|
+
spec.email = 'support@mixpanel.com'
|
|
8
|
+
spec.summary = 'OpenFeature provider for Mixpanel feature flags'
|
|
9
|
+
spec.description = 'An OpenFeature provider that wraps the Mixpanel Ruby SDK feature flags'
|
|
10
|
+
spec.homepage = 'https://mixpanel.com'
|
|
11
|
+
spec.license = 'Apache-2.0'
|
|
12
|
+
spec.required_ruby_version = '>= 3.1.0'
|
|
13
|
+
|
|
14
|
+
spec.files = Dir['lib/**/*.rb']
|
|
15
|
+
spec.require_paths = ['lib']
|
|
16
|
+
|
|
17
|
+
spec.add_runtime_dependency 'openfeature-sdk', '~> 0.5'
|
|
18
|
+
spec.add_runtime_dependency 'mixpanel-ruby', '~> 3.1'
|
|
19
|
+
|
|
20
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
|
21
|
+
spec.add_development_dependency 'simplecov'
|
|
22
|
+
spec.add_development_dependency 'simplecov-cobertura'
|
|
23
|
+
end
|