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.
- checksums.yaml +4 -4
- data/amplitude-experiment.gemspec +1 -1
- data/lib/amplitude/processor.rb +1 -1
- data/lib/amplitude/timeline.rb +1 -1
- data/lib/amplitude-experiment.rb +1 -6
- data/lib/experiment/evaluation/evaluation.rb +250 -248
- data/lib/experiment/evaluation/flag.rb +90 -88
- data/lib/experiment/evaluation/murmur3.rb +90 -86
- data/lib/experiment/evaluation/select.rb +10 -8
- data/lib/experiment/evaluation/semantic_version.rb +39 -35
- data/lib/experiment/evaluation/topological_sort.rb +46 -49
- data/lib/experiment/evaluation.rb +6 -0
- data/lib/experiment/local/client.rb +4 -7
- data/lib/experiment/local/config.rb +21 -3
- data/lib/experiment/remote/client.rb +1 -6
- data/lib/experiment/remote/config.rb +24 -4
- data/lib/experiment/version.rb +1 -1
- metadata +5 -4
|
@@ -2,122 +2,124 @@
|
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
|
|
5
|
-
module
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
18
|
-
|
|
18
|
+
class Allocation
|
|
19
|
+
attr_accessor :range, :distributions
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
29
|
-
|
|
29
|
+
class Condition
|
|
30
|
+
attr_accessor :selector, :op, :values
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
41
|
-
|
|
41
|
+
class Bucket
|
|
42
|
+
attr_accessor :selector, :salt, :allocations
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
53
|
-
|
|
53
|
+
class Segment
|
|
54
|
+
attr_accessor :bucket, :conditions, :variant, :metadata
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
66
|
-
|
|
66
|
+
class Variant
|
|
67
|
+
attr_accessor :key, :value, :payload, :metadata
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
def [](key)
|
|
70
|
+
instance_variable_get("@#{key}")
|
|
71
|
+
end
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
# Process tail
|
|
29
|
+
index = n_blocks << 2
|
|
30
|
+
k1 = 0
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
hash ^= length
|
|
57
|
+
fmix32(hash) & 0xffffffff
|
|
58
|
+
end
|
|
57
59
|
|
|
58
|
-
|
|
60
|
+
private
|
|
59
61
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
module AmplitudeExperiment
|
|
5
|
+
module Evaluation
|
|
6
|
+
def self.select(selectable, selector)
|
|
7
|
+
return nil if selector.nil? || selector.empty?
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
selector.each do |selector_element|
|
|
10
|
+
return nil if selector_element.nil? || selectable.nil?
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
selectable = selectable[selector_element]
|
|
13
|
+
end
|
|
13
14
|
|
|
14
|
-
|
|
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
|
-
|
|
4
|
-
|
|
3
|
+
module AmplitudeExperiment
|
|
4
|
+
module Evaluation
|
|
5
|
+
class SemanticVersion
|
|
6
|
+
include Comparable
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
attr_reader :major, :minor, :patch, :pre_release
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
+
def self.parse(version)
|
|
23
|
+
return nil if version.nil?
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
match = VERSION_PATTERN.match(version)
|
|
26
|
+
return nil unless match
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
+
new(major, minor, patch, pre_release)
|
|
34
|
+
end
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
def <=>(other)
|
|
37
|
+
return nil unless other.is_a?(SemanticVersion)
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
result = major <=> other.major
|
|
40
|
+
return result unless result.zero?
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
result = minor <=> other.minor
|
|
43
|
+
return result unless result.zero?
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
result = patch <=> other.patch
|
|
46
|
+
return result unless result.zero?
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
@@ -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)
|