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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +66 -0
  3. data/Readme.rdoc +8 -2
  4. data/demo/flags/local_flags.rb +25 -0
  5. data/demo/flags/remote_flags.rb +18 -0
  6. data/lib/mixpanel-ruby/events.rb +2 -2
  7. data/lib/mixpanel-ruby/flags/flags_provider.rb +115 -0
  8. data/lib/mixpanel-ruby/flags/local_flags_provider.rb +300 -0
  9. data/lib/mixpanel-ruby/flags/remote_flags_provider.rb +134 -0
  10. data/lib/mixpanel-ruby/flags/types.rb +35 -0
  11. data/lib/mixpanel-ruby/flags/utils.rb +65 -0
  12. data/lib/mixpanel-ruby/groups.rb +1 -1
  13. data/lib/mixpanel-ruby/people.rb +1 -1
  14. data/lib/mixpanel-ruby/tracker.rb +32 -2
  15. data/lib/mixpanel-ruby/version.rb +1 -1
  16. data/lib/mixpanel-ruby.rb +5 -0
  17. data/mixpanel-ruby.gemspec +10 -3
  18. data/openfeature-provider/Gemfile +7 -0
  19. data/openfeature-provider/README.md +286 -0
  20. data/openfeature-provider/RELEASE.md +52 -0
  21. data/openfeature-provider/lib/mixpanel/openfeature/provider.rb +170 -0
  22. data/openfeature-provider/lib/mixpanel/openfeature.rb +3 -0
  23. data/openfeature-provider/mixpanel-ruby-openfeature.gemspec +23 -0
  24. data/openfeature-provider/spec/mixpanel_openfeature_provider_spec.rb +606 -0
  25. data/openfeature-provider/spec/spec_helper.rb +23 -0
  26. data/spec/mixpanel-ruby/events_spec.rb +2 -2
  27. data/spec/mixpanel-ruby/flags/local_flags_spec.rb +759 -0
  28. data/spec/mixpanel-ruby/flags/remote_flags_spec.rb +441 -0
  29. data/spec/mixpanel-ruby/flags/utils_spec.rb +110 -0
  30. data/spec/mixpanel-ruby/groups_spec.rb +10 -10
  31. data/spec/mixpanel-ruby/tracker_spec.rb +5 -5
  32. data/spec/spec_helper.rb +14 -0
  33. metadata +125 -9
  34. 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,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'openfeature/provider'
@@ -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