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.
@@ -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