flagsmith 2.0.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +2 -0
  3. data/.rubocop.yml +4 -1
  4. data/.rubocop_todo.yml +0 -1
  5. data/.ruby-version +1 -0
  6. data/Gemfile.lock +35 -6
  7. data/LICENCE +4 -5
  8. data/README.md +8 -64
  9. data/Rakefile +5 -1
  10. data/example/.env.development +5 -0
  11. data/example/.env.test +4 -0
  12. data/example/.gitignore +5 -0
  13. data/example/.hanamirc +3 -0
  14. data/example/.rspec +2 -0
  15. data/example/Gemfile +25 -0
  16. data/example/Gemfile.lock +269 -0
  17. data/example/README.md +29 -0
  18. data/example/Rakefile +9 -0
  19. data/example/apps/web/application.rb +162 -0
  20. data/example/apps/web/config/routes.rb +1 -0
  21. data/example/apps/web/controllers/.gitkeep +0 -0
  22. data/example/apps/web/controllers/home/index.rb +32 -0
  23. data/example/apps/web/templates/application.html.slim +7 -0
  24. data/example/apps/web/templates/home/index.html.slim +10 -0
  25. data/example/apps/web/views/application_layout.rb +7 -0
  26. data/example/apps/web/views/home/index.rb +29 -0
  27. data/example/config/boot.rb +2 -0
  28. data/example/config/environment.rb +17 -0
  29. data/example/config/initializers/.gitkeep +0 -0
  30. data/example/config/initializers/flagsmith.rb +9 -0
  31. data/example/config/puma.rb +15 -0
  32. data/example/config.ru +3 -0
  33. data/example/spec/example/entities/.gitkeep +0 -0
  34. data/example/spec/example/mailers/.gitkeep +0 -0
  35. data/example/spec/example/repositories/.gitkeep +0 -0
  36. data/example/spec/features_helper.rb +12 -0
  37. data/example/spec/spec_helper.rb +103 -0
  38. data/example/spec/support/.gitkeep +0 -0
  39. data/example/spec/support/capybara.rb +8 -0
  40. data/example/spec/web/controllers/.gitkeep +0 -0
  41. data/example/spec/web/controllers/home/index_spec.rb +9 -0
  42. data/example/spec/web/features/.gitkeep +0 -0
  43. data/example/spec/web/views/application_layout_spec.rb +10 -0
  44. data/example/spec/web/views/home/index_spec.rb +10 -0
  45. data/lib/flagsmith/engine/core.rb +88 -0
  46. data/lib/flagsmith/engine/environments/models.rb +61 -0
  47. data/lib/flagsmith/engine/features/models.rb +173 -0
  48. data/lib/flagsmith/engine/identities/models.rb +115 -0
  49. data/lib/flagsmith/engine/organisations/models.rb +28 -0
  50. data/lib/flagsmith/engine/projects/models.rb +31 -0
  51. data/lib/flagsmith/engine/segments/constants.rb +41 -0
  52. data/lib/flagsmith/engine/segments/evaluator.rb +68 -0
  53. data/lib/flagsmith/engine/segments/models.rb +121 -0
  54. data/lib/flagsmith/engine/utils/hash_func.rb +34 -0
  55. data/lib/flagsmith/hash_slice.rb +12 -0
  56. data/lib/flagsmith/sdk/analytics_processor.rb +39 -0
  57. data/lib/flagsmith/sdk/api_client.rb +47 -0
  58. data/lib/flagsmith/sdk/config.rb +91 -0
  59. data/lib/flagsmith/sdk/errors.rb +9 -0
  60. data/lib/flagsmith/sdk/instance_methods.rb +137 -0
  61. data/lib/flagsmith/sdk/intervals.rb +24 -0
  62. data/lib/flagsmith/sdk/models/flag.rb +62 -0
  63. data/lib/flagsmith/sdk/models/flags/collection.rb +105 -0
  64. data/lib/flagsmith/sdk/pooling_manager.rb +31 -0
  65. data/lib/flagsmith/version.rb +5 -0
  66. data/lib/flagsmith.rb +79 -101
  67. metadata +104 -6
  68. data/.gitignore +0 -57
  69. data/flagsmith.gemspec +0 -22
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../utils/hash_func'
4
+
5
+ module Flagsmith
6
+ module Engine
7
+ # FeatureStateModel
8
+ class FeatureState
9
+ include Flagsmith::Engine::Utils::HashFunc
10
+
11
+ attr_reader :feature, :enabled, :django_id, :uuid, :feature_segment
12
+ attr_accessor :multivariate_feature_state_values
13
+
14
+ def initialize(params = {})
15
+ @feature = params.fetch(:feature)
16
+ @enabled = params.fetch(:enabled)
17
+ @feature_segment = params.fetch(:feature_segment, nil)
18
+ @django_id = params.fetch(:django_id, nil)
19
+ @feature_state_value = params.fetch(:feature_state_value, nil)
20
+ @uuid = params.fetch(:uuid, SecureRandom.uuid)
21
+ @multivariate_feature_state_values = params.fetch(:multivariate_feature_state_values, [])
22
+ end
23
+
24
+ attr_writer :feature_state_value
25
+
26
+ def get_value(identity_id = nil)
27
+ return multivariate_value(identity_id) if identity_id && multivariate_feature_state_values.length.positive?
28
+
29
+ @feature_state_value
30
+ end
31
+
32
+ alias set_value feature_state_value=
33
+ alias feature_state_uuid uuid
34
+ alias enabled? enabled
35
+
36
+ def multivariate_value(identity_id)
37
+ percentage_value = hashed_percentage_for_object_ids([django_id || uuid, identity_id])
38
+
39
+ start_percentage = 0
40
+ multivariate_feature_state_values.sort.each do |multi_fs_value|
41
+ limit = multi_fs_value.percentage_allocation + start_percentage
42
+
43
+ if start_percentage <= percentage_value && percentage_value < limit
44
+ return multi_fs_value.multivariate_feature_option.value
45
+ end
46
+
47
+ start_percentage = limit
48
+ end
49
+ @feature_state_value
50
+ end
51
+
52
+ # Returns `true` if `self` is higher segment priority than `other`
53
+ # (i.e. has lower value for feature_segment.priority)
54
+ # NOTE:
55
+ # A segment will be considered higher priority only if:
56
+ # 1. `other` does not have a feature segment
57
+ # (i.e: it is an environment feature state or it's a
58
+ # feature state with feature segment but from an old document
59
+ # that does not have `feature_segment.priority`)
60
+ # but `self` does.
61
+ # 2. `other` have a feature segment with high priority
62
+ def higher_segment_priority?(other)
63
+ feature_segment.priority.to_i < (other&.feature_segment&.priority || Float::INFINITY)
64
+ rescue TypeError, NoMethodError
65
+ false
66
+ end
67
+
68
+ class << self
69
+ def build(json)
70
+ multivariate_feature_state_values = build_multivariate_values(json[:multivariate_feature_state_values])
71
+ attributes = json.slice(:uuid, :enabled, :django_id, :feature_state_value)
72
+ .merge(feature: Flagsmith::Engine::Feature.build(json[:feature]))
73
+ .merge(multivariate_feature_state_values: multivariate_feature_state_values)
74
+ if json.key?(:feature_segment) && !json[:feature_segment].nil?
75
+ attributes = attributes.merge(
76
+ feature_segment: Flagsmith::Engine::Features::Segment.new(json[:feature_segment])
77
+ )
78
+ end
79
+ new(**attributes)
80
+ end
81
+
82
+ def build_multivariate_values(multivariate_feature_state_values)
83
+ return [] unless multivariate_feature_state_values&.any?
84
+
85
+ multivariate_feature_state_values.map do |fsv|
86
+ Flagsmith::Engine::Features::MultivariateStateValue.build(fsv)
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ # FeatureModel
93
+ class Feature
94
+ attr_reader :id, :name, :type
95
+
96
+ def initialize(id:, name:, type:)
97
+ @id = id
98
+ @name = name
99
+ @type = type
100
+ end
101
+
102
+ def ==(other)
103
+ return false if other.nil?
104
+
105
+ id == other.id
106
+ end
107
+
108
+ class << self
109
+ def build(json)
110
+ new(**json.slice(:id, :name, :type))
111
+ end
112
+ end
113
+ end
114
+
115
+ module Features
116
+ # MultivariateFeatureOptionModel
117
+ class MultivariateOption
118
+ attr_reader :value, :id
119
+
120
+ def initialize(value:, id: nil)
121
+ @value = value
122
+ @id = id
123
+ end
124
+
125
+ class << self
126
+ def build(json)
127
+ new(**json.slice(:id, :value))
128
+ end
129
+ end
130
+ end
131
+
132
+ # MultivariateFeatureStateValueModel
133
+ class MultivariateStateValue
134
+ attr_reader :id, :multivariate_feature_option, :percentage_allocation, :mv_fs_value_uuid
135
+
136
+ def initialize(id:, multivariate_feature_option:, percentage_allocation:, mv_fs_value_uuid: SecureRandom.uuid)
137
+ @id = id
138
+ @percentage_allocation = percentage_allocation
139
+ @multivariate_feature_option = multivariate_feature_option
140
+ @mv_fs_value_uuid = mv_fs_value_uuid
141
+ end
142
+
143
+ def <=>(other)
144
+ return false if other.nil?
145
+
146
+ if !id.nil? && !other.id.nil?
147
+ id - other.id
148
+ else
149
+ mv_fs_value_uuid <=> other.mv_fs_value_uuid
150
+ end
151
+ end
152
+
153
+ class << self
154
+ def build(json)
155
+ new(
156
+ **json.slice(:id, :percentage_allocation, :mv_fs_value_uuid)
157
+ .merge(multivariate_feature_option: MultivariateOption.build(json[:multivariate_feature_option]))
158
+ )
159
+ end
160
+ end
161
+ end
162
+
163
+ # FeatureSegment
164
+ class Segment
165
+ attr_reader :priority
166
+
167
+ def initialize(params = {})
168
+ @priority = params[:priority].nil? ? nil : params[:priority].to_i
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flagsmith
4
+ module Engine
5
+ # IdentityModel
6
+ class Identity
7
+ attr_reader :identifier, :environment_api_key, :created_date, :identity_features,
8
+ :identity_traits, :identity_uuid, :django_id
9
+
10
+ def initialize(params)
11
+ @identity_uuid = params.fetch(:identity_uuid, SecureRandom.uuid)
12
+ @created_date = params[:created_date].is_a?(String) ? Date.parse(params[:created_date]) : params[:created_date]
13
+ @identity_traits = params.fetch(:identity_traits, [])
14
+ @identity_features = params.fetch(:identity_features, Flagsmith::Engine::Identities::FeaturesList.new)
15
+ @environment_api_key = params.fetch(:environment_api_key)
16
+ @identifier = params.fetch(:identifier)
17
+ @django_id = params.fetch(:django_id, nil)
18
+ end
19
+
20
+ def composite_key
21
+ Identity.generate_composite_key(@environment_api_key, @identifier)
22
+ end
23
+
24
+ def update_traits(traits)
25
+ existing_traits = {}
26
+ @identity_traits.each { |trait| existing_traits[trait.key] = trait }
27
+
28
+ traits.each do |trait|
29
+ if trait.value.nil?
30
+ existing_traits.delete(trait.key)
31
+ else
32
+ existing_traits[trait.key] = trait
33
+ end
34
+ end
35
+
36
+ @identity_traits = existing_traits.values
37
+ end
38
+
39
+ class << self
40
+ def generate_composite_key(env_key, identifier)
41
+ "#{env_key}_#{identifier}"
42
+ end
43
+
44
+ def build(json)
45
+ identity_features = Flagsmith::Engine::Identities::FeaturesList.build(json[:identity_features])
46
+ identity_traits = json.fetch(:identity_traits, [])
47
+ .map { |t| Flagsmith::Engine::Identities::Trait.build(t) }
48
+
49
+ Identity.new(
50
+ **json.slice(:identifier, :identity_uuid, :environment_api_key, :created_date, :django_id)
51
+ .merge(identity_features: identity_features, identity_traits: identity_traits)
52
+ )
53
+ end
54
+ end
55
+ end
56
+
57
+ module Identities
58
+ # TraitModel
59
+ class Trait
60
+ attr_reader :trait_value, :trait_key
61
+
62
+ def initialize(trait_key:, trait_value:)
63
+ @trait_key = trait_key
64
+ @trait_value = trait_value
65
+ end
66
+
67
+ alias key trait_key
68
+ alias value trait_value
69
+
70
+ class << self
71
+ def build(json)
72
+ new(**json.slice(:trait_key, :trait_value))
73
+ end
74
+ end
75
+ end
76
+
77
+ class NotUniqueFeatureState < StandardError; end
78
+
79
+ # IdentityFeaturesList
80
+ class FeaturesList
81
+ include Enumerable
82
+
83
+ def initialize(list = [])
84
+ @list = []
85
+ list.each { |item| @list << item }
86
+ end
87
+
88
+ def <<(item)
89
+ @list.each do |v|
90
+ next unless v.django_id == item.django_id
91
+
92
+ raise NotUniqueFeatureState, "Feature state for this feature already exists, django_id: #{django_id}"
93
+ end
94
+ @list << item
95
+ end
96
+
97
+ alias push <<
98
+
99
+ def each(&block)
100
+ @list.each { |item| block&.call(item) || item }
101
+ end
102
+
103
+ class << self
104
+ def build(identity_features)
105
+ return new unless identity_features&.any?
106
+
107
+ new(
108
+ identity_features.map { |f| Flagsmith::Engine::FeatureState.build(f) }
109
+ )
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flagsmith
4
+ module Engine
5
+ # OrganisationModel
6
+ class Organisation
7
+ attr_reader :id, :name, :feature_analitycs, :stop_serving_flags, :persist_trait_data
8
+
9
+ def initialize(id:, name:, stop_serving_flags:, persist_trait_data:, feature_analitycs: nil)
10
+ @id = id
11
+ @name = name
12
+ @feature_analitycs = feature_analitycs
13
+ @stop_serving_flags = stop_serving_flags
14
+ @persist_trait_data = persist_trait_data
15
+ end
16
+
17
+ def unique_slug
18
+ "#{id}-#{name}"
19
+ end
20
+
21
+ class << self
22
+ def build(json)
23
+ new(**json.slice(:id, :name, :feature_analitycs, :stop_serving_flags, :persist_trait_data))
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flagsmith
4
+ module Engine
5
+ # ProjectModel
6
+ class Project
7
+ attr_reader :id, :name, :organisation
8
+ attr_accessor :segments, :hide_disabled_flags
9
+
10
+ def initialize(id:, name:, organisation:, hide_disabled_flags:, segments: [])
11
+ @id = id
12
+ @name = name
13
+ @hide_disabled_flags = hide_disabled_flags
14
+ @organisation = organisation
15
+ @segments = segments
16
+ end
17
+
18
+ class << self
19
+ def build(json)
20
+ segments = json.fetch(:segments, []).map { |s| Flagsmith::Engine::Segment.build(s) }
21
+
22
+ new(
23
+ **json.slice(:id, :name, :hide_disabled_flags)
24
+ .merge(organisation: Flagsmith::Engine::Organisation.build(json[:organisation]))
25
+ .merge(segments: segments)
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flagsmith
4
+ module Engine
5
+ module Segments
6
+ module Constants
7
+ # Segment Rules
8
+ ALL_RULE = 'ALL'
9
+ ANY_RULE = 'ANY'
10
+ NONE_RULE = 'NONE'
11
+
12
+ RULE_TYPES = [ALL_RULE, ANY_RULE, NONE_RULE].freeze
13
+
14
+ # Segment Condition Operators
15
+ EQUAL = 'EQUAL'
16
+ GREATER_THAN = 'GREATER_THAN'
17
+ LESS_THAN = 'LESS_THAN'
18
+ LESS_THAN_INCLUSIVE = 'LESS_THAN_INCLUSIVE'
19
+ CONTAINS = 'CONTAINS'
20
+ GREATER_THAN_INCLUSIVE = 'GREATER_THAN_INCLUSIVE'
21
+ NOT_CONTAINS = 'NOT_CONTAINS'
22
+ NOT_EQUAL = 'NOT_EQUAL'
23
+ REGEX = 'REGEX'
24
+ PERCENTAGE_SPLIT = 'PERCENTAGE_SPLIT'
25
+
26
+ CONDITION_OPERATORS = [
27
+ EQUAL,
28
+ GREATER_THAN,
29
+ LESS_THAN,
30
+ LESS_THAN_INCLUSIVE,
31
+ CONTAINS,
32
+ GREATER_THAN_INCLUSIVE,
33
+ NOT_CONTAINS,
34
+ NOT_EQUAL,
35
+ REGEX,
36
+ PERCENTAGE_SPLIT
37
+ ].freeze
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'constants'
4
+ require_relative '../utils/hash_func'
5
+
6
+ module Flagsmith
7
+ module Engine
8
+ module Segments
9
+ # Evaluator methods
10
+ module Evaluator
11
+ include Flagsmith::Engine::Segments::Constants
12
+ include Flagsmith::Engine::Utils::HashFunc
13
+
14
+ def get_identity_segments(environment, identity, override_traits = nil)
15
+ environment.project.segments.select do |s|
16
+ evaluate_identity_in_segment(identity, s, override_traits)
17
+ end
18
+ end
19
+
20
+ # Evaluates whether a given identity is in the provided segment.
21
+ #
22
+ # :param identity: identity model object to evaluate
23
+ # :param segment: segment model object to evaluate
24
+ # :param override_traits: pass in a list of traits to use instead of those on the
25
+ # identity model itself
26
+ # :return: True if the identity is in the segment, False otherwise
27
+ def evaluate_identity_in_segment(identity, segment, override_traits = nil)
28
+ segment.rules&.length&.positive? &&
29
+ segment.rules.all? do |rule|
30
+ traits_match_segment_rule(
31
+ override_traits || identity.identity_traits,
32
+ rule,
33
+ segment.id,
34
+ identity.django_id || identity.composite_key
35
+ )
36
+ end
37
+ end
38
+
39
+ def traits_match_segment_rule(identity_traits, rule, segment_id, identity_id)
40
+ matching_block = lambda { |condition|
41
+ traits_match_segment_condition(identity_traits, condition, segment_id, identity_id)
42
+ }
43
+
44
+ matches_conditions =
45
+ if rule.conditions&.length&.positive?
46
+ rule.conditions.send(rule.matching_function, &matching_block)
47
+ else true
48
+ end
49
+
50
+ matches_conditions &&
51
+ rule.rules.all? { |r| traits_match_segment_rule(identity_traits, r, segment_id, identity_id) }
52
+ end
53
+
54
+ def traits_match_segment_condition(identity_traits, condition, segment_id, identity_id)
55
+ if condition.operator == PERCENTAGE_SPLIT
56
+ return hashed_percentage_for_object_ids([segment_id, identity_id]) <= condition.value.to_f
57
+ end
58
+
59
+ trait = identity_traits.find { |t| t.key == condition.property }
60
+
61
+ return condition.match_trait_value?(trait.value) if trait
62
+
63
+ false
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'constants'
4
+
5
+ module Flagsmith
6
+ module Engine
7
+ # SegmentModel
8
+ class Segment
9
+ attr_reader :id, :name
10
+ attr_accessor :rules, :feature_states
11
+
12
+ def initialize(id:, name:, rules: [], feature_states: [])
13
+ @id = id
14
+ @name = name
15
+ @rules = rules
16
+ @feature_states = feature_states
17
+ end
18
+
19
+ class << self
20
+ def build(json)
21
+ feature_states = json.fetch(:feature_states, []).map { |fs| Flagsmith::Engine::FeatureState.build(fs) }
22
+ rules = json.fetch(:rules, []).map { |rule| Flagsmith::Engine::Segments::Rule.build(rule) }
23
+
24
+ new(
25
+ **json.slice(:id, :name)
26
+ .merge(feature_states: feature_states, rules: rules)
27
+ )
28
+ end
29
+ end
30
+ end
31
+
32
+ module Segments
33
+ # SegmentConditionModel
34
+ class Condition
35
+ include Constants
36
+ attr_reader :operator, :value, :property
37
+
38
+ MATCHING_FUNCTIONS = {
39
+ EQUAL => ->(other_value, self_value) { other_value == self_value },
40
+ GREATER_THAN => ->(other_value, self_value) { other_value > self_value },
41
+ GREATER_THAN_INCLUSIVE => ->(other_value, self_value) { other_value >= self_value },
42
+ LESS_THAN => ->(other_value, self_value) { other_value < self_value },
43
+ LESS_THAN_INCLUSIVE => ->(other_value, self_value) { other_value <= self_value },
44
+ NOT_EQUAL => ->(other_value, self_value) { other_value != self_value },
45
+ CONTAINS => ->(other_value, self_value) { other_value.include? self_value },
46
+
47
+ NOT_CONTAINS => ->(other_value, self_value) { !other_value.include? self_value },
48
+ REGEX => ->(other_value, self_value) { other_value.match? self_value }
49
+ }.freeze
50
+
51
+ def initialize(operator:, value:, property: nil)
52
+ @operator = operator
53
+ @value = value
54
+ @property = property
55
+ end
56
+
57
+ def match_trait_value?(trait_value)
58
+ if @value.is_a?(String) && @value.match?(/:semver$/)
59
+ trait_value = Semantic::Version.new(trait_value.gsub(/:semver$/, ''))
60
+ end
61
+
62
+ type_as_trait_value = format_to_type_of(trait_value)
63
+ formatted_value = type_as_trait_value ? type_as_trait_value.call(@value) : @value
64
+
65
+ MATCHING_FUNCTIONS[operator]&.call(trait_value, formatted_value)
66
+ end
67
+
68
+ # rubocop:disable Metrics/AbcSize
69
+ def format_to_type_of(input)
70
+ {
71
+ 'String' => ->(v) { v.to_s },
72
+ 'Semantic::Version' => ->(v) { Semantic::Version.new(v.to_s.gsub(/:semver$/, '')) },
73
+ 'TrueClass' => ->(v) { ['True', 'true', 'TRUE', true, 1, '1'].include?(v) ? true : false },
74
+ 'FalseClass' => ->(v) { ['False', 'false', 'FALSE', false, 0, '0'].include?(v) ? false : true },
75
+ 'Integer' => ->(v) { v.to_i },
76
+ 'Float' => ->(v) { v.to_f }
77
+ }[input.class.to_s]
78
+ end
79
+ # rubocop:enable Metrics/AbcSize
80
+
81
+ class << self
82
+ def build(json)
83
+ new(**json.slice(:operator, :value).merge(property: json[:property_]))
84
+ end
85
+ end
86
+ end
87
+
88
+ # SegmentRuleModel
89
+ class Rule
90
+ include Constants
91
+ MATCHING_FUNCTIONS = {
92
+ ANY_RULE => :any?,
93
+ ALL_RULE => :all?,
94
+ NONE_RULE => :none?
95
+ }.freeze
96
+
97
+ attr_accessor :type, :rules, :conditions
98
+
99
+ def initialize(type: nil, rules: [], conditions: [])
100
+ @type = type
101
+ @rules = rules
102
+ @conditions = conditions
103
+ end
104
+
105
+ def matching_function
106
+ MATCHING_FUNCTIONS[type]
107
+ end
108
+
109
+ class << self
110
+ def build(json)
111
+ rules = json.fetch(:rules, []).map { |r| Flagsmith::Engine::Segments::Rule.build(r) }
112
+ conditions = json.fetch(:conditions, []).map { |c| Flagsmith::Engine::Segments::Condition.build(c) }
113
+ new(
114
+ type: json[:type], rules: rules, conditions: conditions
115
+ )
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+
5
+ module Flagsmith
6
+ module Engine
7
+ module Utils
8
+ # HashFunction
9
+ module HashFunc
10
+ # Given a list of object ids, get a floating point number between 0 (inclusive) and
11
+ # 100 (exclusive) based on the hash of those ids. This should give the same value
12
+ # every time for any list of ids.
13
+ #
14
+ # :param object_ids: list of object ids to calculate the hash for
15
+ # :param iterations: num times to include each id in the generated string to hash
16
+ # :return: (float) number between 0 (inclusive) and 100 (exclusive)
17
+ def hashed_percentage_for_object_ids(object_ids, iterations = 1)
18
+ to_hash = (object_ids.map(&:to_s) * iterations).flatten.join(',')
19
+
20
+ hashed_value = Digest::MD5.hexdigest(to_hash.encode('utf-8'))
21
+ hashed_value_as_int = hashed_value.to_i(16)
22
+ value = ((hashed_value_as_int % 9999).to_f / 9998) * 100
23
+
24
+ # since we want a number between 0 (inclusive) and 100 (exclusive), in the
25
+ # unlikely case that we get the exact number 100, we call the method again
26
+ # and increase the number of iterations to ensure we get a different result
27
+ return hashed_percentage_for_object_ids(object_ids, iterations + 1) if value == 100
28
+
29
+ value
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flagsmith
4
+ # Hash#slice was added in ruby version 2.5
5
+ module HashSlice
6
+ def slice(*keys)
7
+ select { |key, _value| keys.include?(key) }
8
+ end
9
+ end
10
+ end
11
+
12
+ Hash.include Flagsmith::HashSlice if RUBY_VERSION < '2.5.0'
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flagsmith
4
+ # Used to control how often we send data(in seconds)
5
+ class AnalyticsProcessor
6
+ ENDPOINT = 'analytics/flags/'
7
+ TIMER = 10
8
+ attr_reader :last_flushed, :timeout, :analytics_data
9
+
10
+ # AnalyticsProcessor is used to track how often individual Flags are evaluated within
11
+ # the Flagsmith SDK. Docs: https://docs.flagsmith.com/advanced-use/flag-analytics.
12
+ #
13
+ # data[:environment_key] environment key obtained from the Flagsmith UI
14
+ # data[:base_api_url] base api url to override when using self hosted version
15
+ # data[:timeout] used to tell requests to stop waiting for a response after a
16
+ # given number of seconds
17
+ def initialize(data)
18
+ @last_flushed = Time.now
19
+ @analytics_data = {}
20
+ @api_client = data.fetch(:api_client)
21
+ @timeout = data.fetch(:timeout, 3)
22
+ end
23
+
24
+ # Sends all the collected data to the api asynchronously and resets the timer
25
+ def flush
26
+ return if @analytics_data.empty?
27
+
28
+ @api_client.post(ENDPOINT, @analytics_data.to_json)
29
+
30
+ @analytics_data = {}
31
+ @last_flushed = Time.now
32
+ end
33
+
34
+ def track_feature(feature_id)
35
+ @analytics_data[feature_id] = @analytics_data.fetch(feature_id, 0) + 1
36
+ flush if (Time.now - @last_flushed) > TIMER * 1000
37
+ end
38
+ end
39
+ end