lucerna 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/lib/lucerna/gates/client.rb +7 -2
- data/lib/lucerna/gates/evaluate.rb +74 -12
- data/lib/lucerna/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ab459063d160fe1c8b84f00d8c15413956871c59200850a10fd888286e338d1d
|
|
4
|
+
data.tar.gz: 1330ddac2d1636e4eafeebf3638f10fc817ff5f183bb637146ada3a3dc04991e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aa24335e33f022de606cf96fabef390f5a2722eea3983220873cb2c9f6d80f2884321ee302c19b4b9c477fe7f67c169a6587032cc53ed326cfe99e7d905c2ac2
|
|
7
|
+
data.tar.gz: 79857d8546d26c7b215faf8f09eaf80f149a0c1c88745ee51259806345dd8025839c98900f6d264f3ac86dadb2546eff5376c45936dd2f850adfd4af49e2fd01
|
data/lib/lucerna/gates/client.rb
CHANGED
|
@@ -113,7 +113,9 @@ module Lucerna
|
|
|
113
113
|
next false unless config
|
|
114
114
|
|
|
115
115
|
user_id, traits = resolve_identity(identity)
|
|
116
|
-
Evaluate.flag(
|
|
116
|
+
Evaluate.flag(
|
|
117
|
+
config, user_id: user_id, traits: traits, audiences: runtime["audiences"],
|
|
118
|
+
)[:on]
|
|
117
119
|
end
|
|
118
120
|
end
|
|
119
121
|
|
|
@@ -154,7 +156,10 @@ module Lucerna
|
|
|
154
156
|
|
|
155
157
|
if (config = runtime["flags"][key])
|
|
156
158
|
trace = []
|
|
157
|
-
decision = Evaluate.flag(
|
|
159
|
+
decision = Evaluate.flag(
|
|
160
|
+
config,
|
|
161
|
+
user_id: user_id, traits: traits, trace: trace, audiences: runtime["audiences"],
|
|
162
|
+
)
|
|
158
163
|
result[:flag] = { decision: decision, trace: trace }
|
|
159
164
|
end
|
|
160
165
|
|
|
@@ -20,8 +20,18 @@ module Lucerna
|
|
|
20
20
|
# fully understand.
|
|
21
21
|
module Evaluate
|
|
22
22
|
OPERATORS = %w[is is_not contains gt lt is_set].freeze
|
|
23
|
+
# Flag conditions may additionally reference a saved audience: the
|
|
24
|
+
# compiled condition carries the audience id in "value" ("trait" is
|
|
25
|
+
# the "audience" sentinel) and matches when the identity's traits
|
|
26
|
+
# satisfy the audience's rule set.
|
|
27
|
+
AUDIENCE_OPERATORS = %w[in_audience not_in_audience].freeze
|
|
23
28
|
OVERRIDE_BY = %w[user_id domain].freeze
|
|
24
29
|
OVERRIDE_SERVE = %w[on off].freeze
|
|
30
|
+
# Overrides can also pin by any trait the workspace enabled as an
|
|
31
|
+
# override dimension: "by" carries "trait:<key>" and matches when
|
|
32
|
+
# the identity trait equals "who" exactly (same value-based compare
|
|
33
|
+
# as the "is" operator; "domain" keeps its lowercase compare).
|
|
34
|
+
OVERRIDE_TRAIT_PREFIX = "trait:"
|
|
25
35
|
|
|
26
36
|
module_function
|
|
27
37
|
|
|
@@ -32,7 +42,11 @@ module Lucerna
|
|
|
32
42
|
# 4. percentage rollout, sticky by user id via the stored salt
|
|
33
43
|
# A rollout between 0 and 100 with no user id serves off — there is
|
|
34
44
|
# nothing to be sticky by.
|
|
35
|
-
|
|
45
|
+
#
|
|
46
|
+
# `audiences` resolves the flag's audience conditions (in_audience /
|
|
47
|
+
# not_in_audience); a referenced id that is absent evaluates the
|
|
48
|
+
# condition to no-match — never to a wider serve.
|
|
49
|
+
def flag(flag, user_id: nil, traits: nil, trace: nil, audiences: {})
|
|
36
50
|
unless flag["enabled"]
|
|
37
51
|
note(trace, "enabled", "flag is off in this environment")
|
|
38
52
|
return { on: false, reason: "flag_off" }
|
|
@@ -40,9 +54,9 @@ module Lucerna
|
|
|
40
54
|
note(trace, "enabled", "flag is on in this environment")
|
|
41
55
|
|
|
42
56
|
bad_override = flag["overrides"].find do |override|
|
|
43
|
-
!
|
|
57
|
+
!supported_override_by?(override["by"]) || !OVERRIDE_SERVE.include?(override["serve"])
|
|
44
58
|
end
|
|
45
|
-
bad_operator =
|
|
59
|
+
bad_operator = unsupported_flag_operator(flag["conditions"])
|
|
46
60
|
if bad_override || bad_operator
|
|
47
61
|
note(
|
|
48
62
|
trace,
|
|
@@ -60,10 +74,14 @@ module Lucerna
|
|
|
60
74
|
domain = identity_domain(traits)&.downcase
|
|
61
75
|
flag["overrides"].each do |override|
|
|
62
76
|
matched =
|
|
63
|
-
|
|
77
|
+
case override["by"]
|
|
78
|
+
when "user_id"
|
|
64
79
|
!user_id.nil? && override["who"] == user_id
|
|
65
|
-
|
|
80
|
+
when "domain"
|
|
66
81
|
!domain.nil? && override["who"].downcase == domain
|
|
82
|
+
else
|
|
83
|
+
raw = traits[override["by"].delete_prefix(OVERRIDE_TRAIT_PREFIX)]
|
|
84
|
+
!raw.nil? && raw == override["who"]
|
|
67
85
|
end
|
|
68
86
|
note(
|
|
69
87
|
trace,
|
|
@@ -76,12 +94,42 @@ module Lucerna
|
|
|
76
94
|
unless flag["conditions"].empty?
|
|
77
95
|
all = true
|
|
78
96
|
flag["conditions"].each do |condition|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
97
|
+
if AUDIENCE_OPERATORS.include?(condition["operator"])
|
|
98
|
+
audience = audiences[condition["value"]]
|
|
99
|
+
if audience
|
|
100
|
+
bad_audience_operator = unsupported_operator(audience["conditions"])
|
|
101
|
+
if bad_audience_operator
|
|
102
|
+
note(
|
|
103
|
+
trace,
|
|
104
|
+
"unsupported",
|
|
105
|
+
%(audience condition operator "#{bad_audience_operator}" is not supported by this engine),
|
|
106
|
+
)
|
|
107
|
+
return { on: false, reason: "unsupported_rule" }
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
# A dangling audience id never matches — for both operators the
|
|
111
|
+
# condition fails closed rather than widening the serve.
|
|
112
|
+
in_audience = !audience.nil? && matches_audience?(traits, audience)
|
|
113
|
+
matched =
|
|
114
|
+
!audience.nil? &&
|
|
115
|
+
(condition["operator"] == "in_audience" ? in_audience : !in_audience)
|
|
116
|
+
note(
|
|
117
|
+
trace,
|
|
118
|
+
"condition",
|
|
119
|
+
if audience
|
|
120
|
+
%(audience "#{condition["value"]}" #{condition["operator"] == "in_audience" ? "includes" : "excludes"} user: #{matched ? "matched" : "no match"})
|
|
121
|
+
else
|
|
122
|
+
%(audience "#{condition["value"]}" is not in the runtime: no match)
|
|
123
|
+
end,
|
|
124
|
+
)
|
|
125
|
+
else
|
|
126
|
+
matched = matches_condition?(traits, condition)
|
|
127
|
+
note(
|
|
128
|
+
trace,
|
|
129
|
+
"condition",
|
|
130
|
+
%(#{condition["trait"]} #{condition["operator"]} "#{condition["value"]}": #{matched ? "matched" : "no match"}),
|
|
131
|
+
)
|
|
132
|
+
end
|
|
85
133
|
unless matched
|
|
86
134
|
all = false
|
|
87
135
|
break
|
|
@@ -205,7 +253,7 @@ module Lucerna
|
|
|
205
253
|
{
|
|
206
254
|
kills: runtime["kills"].dup,
|
|
207
255
|
flags: runtime["flags"].to_h do |key, value|
|
|
208
|
-
[key, flag(value, user_id: user_id, traits: traits)]
|
|
256
|
+
[key, flag(value, user_id: user_id, traits: traits, audiences: runtime["audiences"])]
|
|
209
257
|
end,
|
|
210
258
|
experiments: runtime["experiments"].to_h do |key, value|
|
|
211
259
|
[key, experiment(value, runtime["audiences"], user_id: user_id, traits: traits)]
|
|
@@ -276,6 +324,20 @@ module Lucerna
|
|
|
276
324
|
conditions.find { |condition| !OPERATORS.include?(condition["operator"]) }&.fetch("operator")
|
|
277
325
|
end
|
|
278
326
|
|
|
327
|
+
def supported_override_by?(by)
|
|
328
|
+
return true if OVERRIDE_BY.include?(by)
|
|
329
|
+
|
|
330
|
+
by.start_with?(OVERRIDE_TRAIT_PREFIX) && by.length > OVERRIDE_TRAIT_PREFIX.length
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Same check for flag conditions, which may also reference audiences.
|
|
334
|
+
def unsupported_flag_operator(conditions)
|
|
335
|
+
conditions.find do |condition|
|
|
336
|
+
!OPERATORS.include?(condition["operator"]) &&
|
|
337
|
+
!AUDIENCE_OPERATORS.include?(condition["operator"])
|
|
338
|
+
end&.fetch("operator")
|
|
339
|
+
end
|
|
340
|
+
|
|
279
341
|
# The domain an override's by: "domain" matches: an explicit `domain`
|
|
280
342
|
# trait, falling back to the part of the `email` trait after the
|
|
281
343
|
# last "@".
|
data/lib/lucerna/version.rb
CHANGED