amplitude-experiment 1.6.0 → 1.7.1

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.
@@ -2,122 +2,124 @@
2
2
 
3
3
  require 'json'
4
4
 
5
- module Evaluation
6
- class Distribution
7
- attr_accessor :variant, :range
8
-
9
- def self.from_hash(hash)
10
- new.tap do |dist|
11
- dist.variant = hash['variant']
12
- dist.range = hash['range']
5
+ module AmplitudeExperiment
6
+ module Evaluation
7
+ class Distribution
8
+ attr_accessor :variant, :range
9
+
10
+ def self.from_hash(hash)
11
+ new.tap do |dist|
12
+ dist.variant = hash['variant']
13
+ dist.range = hash['range']
14
+ end
13
15
  end
14
16
  end
15
- end
16
17
 
17
- class Allocation
18
- attr_accessor :range, :distributions
18
+ class Allocation
19
+ attr_accessor :range, :distributions
19
20
 
20
- def self.from_hash(hash)
21
- new.tap do |alloc|
22
- alloc.range = hash['range']
23
- alloc.distributions = hash['distributions']&.map { |d| Distribution.from_hash(d) }
21
+ def self.from_hash(hash)
22
+ new.tap do |alloc|
23
+ alloc.range = hash['range']
24
+ alloc.distributions = hash['distributions']&.map { |d| Distribution.from_hash(d) }
25
+ end
24
26
  end
25
27
  end
26
- end
27
28
 
28
- class Condition
29
- attr_accessor :selector, :op, :values
29
+ class Condition
30
+ attr_accessor :selector, :op, :values
30
31
 
31
- def self.from_hash(hash)
32
- new.tap do |cond|
33
- cond.selector = hash['selector']
34
- cond.op = hash['op']
35
- cond.values = hash['values']
32
+ def self.from_hash(hash)
33
+ new.tap do |cond|
34
+ cond.selector = hash['selector']
35
+ cond.op = hash['op']
36
+ cond.values = hash['values']
37
+ end
36
38
  end
37
39
  end
38
- end
39
40
 
40
- class Bucket
41
- attr_accessor :selector, :salt, :allocations
41
+ class Bucket
42
+ attr_accessor :selector, :salt, :allocations
42
43
 
43
- def self.from_hash(hash)
44
- new.tap do |bucket|
45
- bucket.selector = hash['selector']
46
- bucket.salt = hash['salt']
47
- bucket.allocations = hash['allocations']&.map { |a| Allocation.from_hash(a) }
44
+ def self.from_hash(hash)
45
+ new.tap do |bucket|
46
+ bucket.selector = hash['selector']
47
+ bucket.salt = hash['salt']
48
+ bucket.allocations = hash['allocations']&.map { |a| Allocation.from_hash(a) }
49
+ end
48
50
  end
49
51
  end
50
- end
51
52
 
52
- class Segment
53
- attr_accessor :bucket, :conditions, :variant, :metadata
53
+ class Segment
54
+ attr_accessor :bucket, :conditions, :variant, :metadata
54
55
 
55
- def self.from_hash(hash)
56
- new.tap do |segment|
57
- segment.bucket = hash['bucket'] && Bucket.from_hash(hash['bucket'])
58
- segment.conditions = hash['conditions']&.map { |c| c.map { |inner| Condition.from_hash(inner) } }
59
- segment.variant = hash['variant']
60
- segment.metadata = hash['metadata']
56
+ def self.from_hash(hash)
57
+ new.tap do |segment|
58
+ segment.bucket = hash['bucket'] && Bucket.from_hash(hash['bucket'])
59
+ segment.conditions = hash['conditions']&.map { |c| c.map { |inner| Condition.from_hash(inner) } }
60
+ segment.variant = hash['variant']
61
+ segment.metadata = hash['metadata']
62
+ end
61
63
  end
62
64
  end
63
- end
64
65
 
65
- class Variant
66
- attr_accessor :key, :value, :payload, :metadata
66
+ class Variant
67
+ attr_accessor :key, :value, :payload, :metadata
67
68
 
68
- def [](key)
69
- instance_variable_get("@#{key}")
70
- end
69
+ def [](key)
70
+ instance_variable_get("@#{key}")
71
+ end
71
72
 
72
- def self.from_hash(hash)
73
- new.tap do |variant|
74
- variant.key = hash['key']
75
- variant.value = hash['value']
76
- variant.payload = hash['payload']
77
- variant.metadata = hash['metadata']
73
+ def self.from_hash(hash)
74
+ new.tap do |variant|
75
+ variant.key = hash['key']
76
+ variant.value = hash['value']
77
+ variant.payload = hash['payload']
78
+ variant.metadata = hash['metadata']
79
+ end
78
80
  end
79
81
  end
80
- end
81
82
 
82
- class Flag
83
- attr_accessor :key, :variants, :segments, :dependencies, :metadata
83
+ class Flag
84
+ attr_accessor :key, :variants, :segments, :dependencies, :metadata
85
+
86
+ def self.from_hash(hash)
87
+ new.tap do |flag|
88
+ flag.key = hash['key']
89
+ flag.variants = hash['variants'].transform_values { |v| Variant.from_hash(v) }
90
+ flag.segments = hash['segments'].map { |s| Segment.from_hash(s) }
91
+ flag.dependencies = hash['dependencies']
92
+ flag.metadata = hash['metadata']
93
+ end
94
+ end
84
95
 
85
- def self.from_hash(hash)
86
- new.tap do |flag|
87
- flag.key = hash['key']
88
- flag.variants = hash['variants'].transform_values { |v| Variant.from_hash(v) }
89
- flag.segments = hash['segments'].map { |s| Segment.from_hash(s) }
90
- flag.dependencies = hash['dependencies']
91
- flag.metadata = hash['metadata']
96
+ # Used for testing
97
+ def ==(other)
98
+ key == other.key
92
99
  end
93
100
  end
94
101
 
95
- # Used for testing
96
- def ==(other)
97
- key == other.key
102
+ module Operator
103
+ IS = 'is'
104
+ IS_NOT = 'is not'
105
+ CONTAINS = 'contains'
106
+ DOES_NOT_CONTAIN = 'does not contain'
107
+ LESS_THAN = 'less'
108
+ LESS_THAN_EQUALS = 'less or equal'
109
+ GREATER_THAN = 'greater'
110
+ GREATER_THAN_EQUALS = 'greater or equal'
111
+ VERSION_LESS_THAN = 'version less'
112
+ VERSION_LESS_THAN_EQUALS = 'version less or equal'
113
+ VERSION_GREATER_THAN = 'version greater'
114
+ VERSION_GREATER_THAN_EQUALS = 'version greater or equal'
115
+ SET_IS = 'set is'
116
+ SET_IS_NOT = 'set is not'
117
+ SET_CONTAINS = 'set contains'
118
+ SET_DOES_NOT_CONTAIN = 'set does not contain'
119
+ SET_CONTAINS_ANY = 'set contains any'
120
+ SET_DOES_NOT_CONTAIN_ANY = 'set does not contain any'
121
+ REGEX_MATCH = 'regex match'
122
+ REGEX_DOES_NOT_MATCH = 'regex does not match'
98
123
  end
99
124
  end
100
-
101
- module Operator
102
- IS = 'is'
103
- IS_NOT = 'is not'
104
- CONTAINS = 'contains'
105
- DOES_NOT_CONTAIN = 'does not contain'
106
- LESS_THAN = 'less'
107
- LESS_THAN_EQUALS = 'less or equal'
108
- GREATER_THAN = 'greater'
109
- GREATER_THAN_EQUALS = 'greater or equal'
110
- VERSION_LESS_THAN = 'version less'
111
- VERSION_LESS_THAN_EQUALS = 'version less or equal'
112
- VERSION_GREATER_THAN = 'version greater'
113
- VERSION_GREATER_THAN_EQUALS = 'version greater or equal'
114
- SET_IS = 'set is'
115
- SET_IS_NOT = 'set is not'
116
- SET_CONTAINS = 'set contains'
117
- SET_DOES_NOT_CONTAIN = 'set does not contain'
118
- SET_CONTAINS_ANY = 'set contains any'
119
- SET_DOES_NOT_CONTAIN_ANY = 'set does not contain any'
120
- REGEX_MATCH = 'regex match'
121
- REGEX_DOES_NOT_MATCH = 'regex does not match'
122
- end
123
125
  end
@@ -1,104 +1,108 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Implements 32-bit x86 MurmurHash3
4
- class Murmur3
5
- C1_32 = -0x3361d2af
6
- C2_32 = 0x1b873593
7
- R1_32 = 15
8
- R2_32 = 13
9
- M_32 = 5
10
- N_32 = -0x19ab949c
4
+ module AmplitudeExperiment
5
+ module Evaluation
6
+ class Murmur3
7
+ C1_32 = -0x3361d2af
8
+ C2_32 = 0x1b873593
9
+ R1_32 = 15
10
+ R2_32 = 13
11
+ M_32 = 5
12
+ N_32 = -0x19ab949c
11
13
 
12
- class << self
13
- def hash32x86(input, seed = 0)
14
- data = string_to_utf8_bytes(input)
15
- length = data.length
16
- n_blocks = length >> 2
17
- hash = seed
14
+ class << self
15
+ def hash32x86(input, seed = 0)
16
+ data = string_to_utf8_bytes(input)
17
+ length = data.length
18
+ n_blocks = length >> 2
19
+ hash = seed
18
20
 
19
- # Process body
20
- n_blocks.times do |i|
21
- index = i << 2
22
- k = read_int_le(data, index)
23
- hash = mix32(k, hash)
24
- end
21
+ # Process body
22
+ n_blocks.times do |i|
23
+ index = i << 2
24
+ k = read_int_le(data, index)
25
+ hash = mix32(k, hash)
26
+ end
25
27
 
26
- # Process tail
27
- index = n_blocks << 2
28
- k1 = 0
28
+ # Process tail
29
+ index = n_blocks << 2
30
+ k1 = 0
29
31
 
30
- case length - index
31
- when 3
32
- k1 ^= data[index + 2] << 16
33
- k1 ^= data[index + 1] << 8
34
- k1 ^= data[index]
35
- k1 = (k1 * C1_32) & 0xffffffff
36
- k1 = rotate_left(k1, R1_32)
37
- k1 = (k1 * C2_32) & 0xffffffff
38
- hash ^= k1
39
- when 2
40
- k1 ^= data[index + 1] << 8
41
- k1 ^= data[index]
42
- k1 = (k1 * C1_32) & 0xffffffff
43
- k1 = rotate_left(k1, R1_32)
44
- k1 = (k1 * C2_32) & 0xffffffff
45
- hash ^= k1
46
- when 1
47
- k1 ^= data[index]
48
- k1 = (k1 * C1_32) & 0xffffffff
49
- k1 = rotate_left(k1, R1_32)
50
- k1 = (k1 * C2_32) & 0xffffffff
51
- hash ^= k1
52
- end
32
+ case length - index
33
+ when 3
34
+ k1 ^= data[index + 2] << 16
35
+ k1 ^= data[index + 1] << 8
36
+ k1 ^= data[index]
37
+ k1 = (k1 * C1_32) & 0xffffffff
38
+ k1 = rotate_left(k1, R1_32)
39
+ k1 = (k1 * C2_32) & 0xffffffff
40
+ hash ^= k1
41
+ when 2
42
+ k1 ^= data[index + 1] << 8
43
+ k1 ^= data[index]
44
+ k1 = (k1 * C1_32) & 0xffffffff
45
+ k1 = rotate_left(k1, R1_32)
46
+ k1 = (k1 * C2_32) & 0xffffffff
47
+ hash ^= k1
48
+ when 1
49
+ k1 ^= data[index]
50
+ k1 = (k1 * C1_32) & 0xffffffff
51
+ k1 = rotate_left(k1, R1_32)
52
+ k1 = (k1 * C2_32) & 0xffffffff
53
+ hash ^= k1
54
+ end
53
55
 
54
- hash ^= length
55
- fmix32(hash) & 0xffffffff
56
- end
56
+ hash ^= length
57
+ fmix32(hash) & 0xffffffff
58
+ end
57
59
 
58
- private
60
+ private
59
61
 
60
- def mix32(k, hash)
61
- k = (k * C1_32) & 0xffffffff
62
- k = rotate_left(k, R1_32)
63
- k = (k * C2_32) & 0xffffffff
64
- hash ^= k
65
- hash = rotate_left(hash, R2_32)
66
- ((hash * M_32) + N_32) & 0xffffffff
67
- end
62
+ def mix32(k, hash)
63
+ k = (k * C1_32) & 0xffffffff
64
+ k = rotate_left(k, R1_32)
65
+ k = (k * C2_32) & 0xffffffff
66
+ hash ^= k
67
+ hash = rotate_left(hash, R2_32)
68
+ ((hash * M_32) + N_32) & 0xffffffff
69
+ end
68
70
 
69
- def fmix32(hash)
70
- hash ^= hash >> 16
71
- hash = (hash * -0x7a143595) & 0xffffffff
72
- hash ^= hash >> 13
73
- hash = (hash * -0x3d4d51cb) & 0xffffffff
74
- hash ^= hash >> 16
75
- hash
76
- end
71
+ def fmix32(hash)
72
+ hash ^= hash >> 16
73
+ hash = (hash * -0x7a143595) & 0xffffffff
74
+ hash ^= hash >> 13
75
+ hash = (hash * -0x3d4d51cb) & 0xffffffff
76
+ hash ^= hash >> 16
77
+ hash
78
+ end
77
79
 
78
- def rotate_left(x, n, width = 32)
79
- n = n % width if n > width
80
- mask = (0xffffffff << (width - n)) & 0xffffffff
81
- r = ((x & mask) >> (width - n)) & 0xffffffff
82
- ((x << n) | r) & 0xffffffff
83
- end
80
+ def rotate_left(x, n, width = 32)
81
+ n %= width if n > width
82
+ mask = (0xffffffff << (width - n)) & 0xffffffff
83
+ r = ((x & mask) >> (width - n)) & 0xffffffff
84
+ ((x << n) | r) & 0xffffffff
85
+ end
84
86
 
85
- def read_int_le(data, index = 0)
86
- n = (data[index] << 24) |
87
- (data[index + 1] << 16) |
88
- (data[index + 2] << 8) |
89
- data[index + 3]
90
- reverse_bytes(n)
91
- end
87
+ def read_int_le(data, index = 0)
88
+ n = (data[index] << 24) |
89
+ (data[index + 1] << 16) |
90
+ (data[index + 2] << 8) |
91
+ data[index + 3]
92
+ reverse_bytes(n)
93
+ end
92
94
 
93
- def reverse_bytes(n)
94
- ((n & -0x1000000) >> 24) |
95
- ((n & 0x00ff0000) >> 8) |
96
- ((n & 0x0000ff00) << 8) |
97
- ((n & 0x000000ff) << 24)
98
- end
95
+ def reverse_bytes(n)
96
+ ((n & -0x1000000) >> 24) |
97
+ ((n & 0x00ff0000) >> 8) |
98
+ ((n & 0x0000ff00) << 8) |
99
+ ((n & 0x000000ff) << 24)
100
+ end
99
101
 
100
- def string_to_utf8_bytes(str)
101
- str.encode('UTF-8').bytes
102
+ def string_to_utf8_bytes(str)
103
+ str.encode('UTF-8').bytes
104
+ end
105
+ end
102
106
  end
103
107
  end
104
108
  end
@@ -1,16 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Selects a value from a nested object using an array of selector keys
4
- module Evaluation
5
- def self.select(selectable, selector)
6
- return nil if selector.nil? || selector.empty?
4
+ module AmplitudeExperiment
5
+ module Evaluation
6
+ def self.select(selectable, selector)
7
+ return nil if selector.nil? || selector.empty?
7
8
 
8
- selector.each do |selector_element|
9
- return nil if selector_element.nil? || selectable.nil?
9
+ selector.each do |selector_element|
10
+ return nil if selector_element.nil? || selectable.nil?
10
11
 
11
- selectable = selectable[selector_element]
12
- end
12
+ selectable = selectable[selector_element]
13
+ end
13
14
 
14
- selectable.nil? ? nil : selectable
15
+ selectable.nil? ? nil : selectable
16
+ end
15
17
  end
16
18
  end
@@ -1,52 +1,56 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class SemanticVersion
4
- include Comparable
3
+ module AmplitudeExperiment
4
+ module Evaluation
5
+ class SemanticVersion
6
+ include Comparable
5
7
 
6
- attr_reader :major, :minor, :patch, :pre_release
8
+ attr_reader :major, :minor, :patch, :pre_release
7
9
 
8
- MAJOR_MINOR_REGEX = '(\d+)\.(\d+)'
9
- PATCH_REGEX = '(\d+)'
10
- PRERELEASE_REGEX = '(-(([-\w]+\.?)*))?'
11
- VERSION_PATTERN = /^#{MAJOR_MINOR_REGEX}(\.#{PATCH_REGEX}#{PRERELEASE_REGEX})?$/.freeze
10
+ MAJOR_MINOR_REGEX = '(\d+)\.(\d+)'
11
+ PATCH_REGEX = '(\d+)'
12
+ PRERELEASE_REGEX = '(-(([-\w]+\.?)*))?'
13
+ VERSION_PATTERN = /^#{MAJOR_MINOR_REGEX}(\.#{PATCH_REGEX}#{PRERELEASE_REGEX})?$/.freeze
12
14
 
13
- def initialize(major, minor, patch, pre_release = nil)
14
- @major = major
15
- @minor = minor
16
- @patch = patch
17
- @pre_release = pre_release
18
- end
15
+ def initialize(major, minor, patch, pre_release = nil)
16
+ @major = major
17
+ @minor = minor
18
+ @patch = patch
19
+ @pre_release = pre_release
20
+ end
19
21
 
20
- def self.parse(version)
21
- return nil if version.nil?
22
+ def self.parse(version)
23
+ return nil if version.nil?
22
24
 
23
- match = VERSION_PATTERN.match(version)
24
- return nil unless match
25
+ match = VERSION_PATTERN.match(version)
26
+ return nil unless match
25
27
 
26
- major = match[1].to_i
27
- minor = match[2].to_i
28
- patch = match[4]&.to_i || 0
29
- pre_release = match[5]
28
+ major = match[1].to_i
29
+ minor = match[2].to_i
30
+ patch = match[4]&.to_i || 0
31
+ pre_release = match[5]
30
32
 
31
- new(major, minor, patch, pre_release)
32
- end
33
+ new(major, minor, patch, pre_release)
34
+ end
33
35
 
34
- def <=>(other)
35
- return nil unless other.is_a?(SemanticVersion)
36
+ def <=>(other)
37
+ return nil unless other.is_a?(SemanticVersion)
36
38
 
37
- result = major <=> other.major
38
- return result unless result.zero?
39
+ result = major <=> other.major
40
+ return result unless result.zero?
39
41
 
40
- result = minor <=> other.minor
41
- return result unless result.zero?
42
+ result = minor <=> other.minor
43
+ return result unless result.zero?
42
44
 
43
- result = patch <=> other.patch
44
- return result unless result.zero?
45
+ result = patch <=> other.patch
46
+ return result unless result.zero?
45
47
 
46
- return 1 if !pre_release && other.pre_release
47
- return -1 if pre_release && !other.pre_release
48
- return 0 if !pre_release && !other.pre_release
48
+ return 1 if !pre_release && other.pre_release
49
+ return -1 if pre_release && !other.pre_release
50
+ return 0 if !pre_release && !other.pre_release
49
51
 
50
- pre_release <=> other.pre_release
52
+ pre_release <=> other.pre_release
53
+ end
54
+ end
51
55
  end
52
56
  end
@@ -1,56 +1,53 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class CycleError < StandardError
4
- attr_accessor :path
5
-
6
- def initialize(path)
7
- super("Detected a cycle between flags #{path}")
8
- self.path = path
9
- end
10
- end
3
+ require_relative '../error'
11
4
 
12
5
  # Performs topological sorting of feature flags based on their dependencies
13
- class TopologicalSort
14
- # Sort flags topologically based on their dependencies
15
- def self.sort(flags, flag_keys = nil)
16
- available = flags.clone
17
- result = []
18
- starting_keys = flag_keys.nil? || flag_keys.empty? ? flags.keys : flag_keys
19
-
20
- starting_keys.each do |flag_key|
21
- traversal = parent_traversal(flag_key, available)
22
- result.concat(traversal) if traversal
23
- end
24
-
25
- result
26
- end
27
-
28
- # Perform depth-first traversal of flag dependencies
29
- def self.parent_traversal(flag_key, available, path = [])
30
- flag = available[flag_key]
31
- return nil unless flag
32
-
33
- # No dependencies - return flag and remove from available
34
- if !flag.dependencies || flag.dependencies.empty?
35
- available.delete(flag.key)
36
- return [flag]
6
+ module AmplitudeExperiment
7
+ module Evaluation
8
+ class TopologicalSort
9
+ # Sort flags topologically based on their dependencies
10
+ def self.sort(flags, flag_keys = nil)
11
+ available = flags.clone
12
+ result = []
13
+ starting_keys = flag_keys.nil? || flag_keys.empty? ? flags.keys : flag_keys
14
+
15
+ starting_keys.each do |flag_key|
16
+ traversal = parent_traversal(flag_key, available)
17
+ result.concat(traversal) if traversal
18
+ end
19
+
20
+ result
21
+ end
22
+
23
+ # Perform depth-first traversal of flag dependencies
24
+ def self.parent_traversal(flag_key, available, path = [])
25
+ flag = available[flag_key]
26
+ return nil unless flag
27
+
28
+ # No dependencies - return flag and remove from available
29
+ if !flag.dependencies || flag.dependencies.empty?
30
+ available.delete(flag.key)
31
+ return [flag]
32
+ end
33
+
34
+ # Check for cycles
35
+ path.push(flag.key)
36
+ result = []
37
+
38
+ flag.dependencies.each do |parent_key|
39
+ raise CycleError, path if path.any? { |p| p == parent_key }
40
+
41
+ traversal = parent_traversal(parent_key, available, path)
42
+ result.concat(traversal) if traversal
43
+ end
44
+
45
+ result.push(flag)
46
+ path.pop
47
+ available.delete(flag.key)
48
+
49
+ result
50
+ end
37
51
  end
38
-
39
- # Check for cycles
40
- path.push(flag.key)
41
- result = []
42
-
43
- flag.dependencies.each do |parent_key|
44
- raise CycleError, path if path.any? { |p| p == parent_key }
45
-
46
- traversal = parent_traversal(parent_key, available, path)
47
- result.concat(traversal) if traversal
48
- end
49
-
50
- result.push(flag)
51
- path.pop
52
- available.delete(flag.key)
53
-
54
- result
55
52
  end
56
53
  end
@@ -0,0 +1,6 @@
1
+ require 'experiment/evaluation/evaluation'
2
+ require 'experiment/evaluation/flag'
3
+ require 'experiment/evaluation/murmur3'
4
+ require 'experiment/evaluation/select'
5
+ require 'experiment/evaluation/semantic_version'
6
+ require 'experiment/evaluation/topological_sort'
@@ -1,6 +1,8 @@
1
1
  require 'uri'
2
2
  require 'logger'
3
+
3
4
  require_relative '../../amplitude'
5
+ require_relative '../evaluation'
4
6
 
5
7
  module AmplitudeExperiment
6
8
  FLAG_TYPE_MUTUAL_EXCLUSION_GROUP = 'mutual-exclusion-group'.freeze
@@ -14,14 +16,9 @@ module AmplitudeExperiment
14
16
  def initialize(api_key, config = nil)
15
17
  @api_key = api_key
16
18
  @config = config || LocalEvaluationConfig.new
19
+ @logger = @config.logger
17
20
  @flags = nil
18
21
  @flags_mutex = Mutex.new
19
- @logger = Logger.new($stdout)
20
- @logger.level = if @config.debug
21
- Logger::DEBUG
22
- else
23
- Logger::INFO
24
- end
25
22
  raise ArgumentError, 'Experiment API key is empty' if @api_key.nil? || @api_key.empty?
26
23
 
27
24
  @engine = Evaluation::Engine.new
@@ -68,7 +65,7 @@ module AmplitudeExperiment
68
65
  flags = @flag_config_storage.flag_configs
69
66
  return {} if flags.nil?
70
67
 
71
- sorted_flags = TopologicalSort.sort(flags, flag_keys)
68
+ sorted_flags = Evaluation::TopologicalSort.sort(flags, flag_keys)
72
69
  required_cohorts_in_storage(sorted_flags)
73
70
  user = enrich_user_with_cohorts(user, flags) if @config.cohort_sync_config
74
71
  context = AmplitudeExperiment.user_to_evaluation_context(user)