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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf8a610323ba01503dfe245ea129a8cdedfc2d9e916977fd34ce7cc1c85d04d3
4
- data.tar.gz: 9c4c0c931b3bc9326a696d849873cd0741d546194eacdd4d038152e994bdb168
3
+ metadata.gz: ab459063d160fe1c8b84f00d8c15413956871c59200850a10fd888286e338d1d
4
+ data.tar.gz: 1330ddac2d1636e4eafeebf3638f10fc817ff5f183bb637146ada3a3dc04991e
5
5
  SHA512:
6
- metadata.gz: d3cedc4e00127c55d42fb9436acb15570d38f206f1aa2d2cce1c578fdf691a713b19d6940164b2bf35de6c012550b9894f432ca275027d7f74bf68825b7e6a59
7
- data.tar.gz: 25c3407e10189105c8cbf1f37ac7b4513303308e669fcfed5409c24a949f913c273c36c1ac1684d3d8d186c25e528a0e7596d96821a07a00297b5510408926cc
6
+ metadata.gz: aa24335e33f022de606cf96fabef390f5a2722eea3983220873cb2c9f6d80f2884321ee302c19b4b9c477fe7f67c169a6587032cc53ed326cfe99e7d905c2ac2
7
+ data.tar.gz: 79857d8546d26c7b215faf8f09eaf80f149a0c1c88745ee51259806345dd8025839c98900f6d264f3ac86dadb2546eff5376c45936dd2f850adfd4af49e2fd01
@@ -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(config, user_id: user_id, traits: traits)[:on]
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(config, user_id: user_id, traits: traits, trace: trace)
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
- def flag(flag, user_id: nil, traits: nil, trace: nil)
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
- !OVERRIDE_BY.include?(override["by"]) || !OVERRIDE_SERVE.include?(override["serve"])
57
+ !supported_override_by?(override["by"]) || !OVERRIDE_SERVE.include?(override["serve"])
44
58
  end
45
- bad_operator = unsupported_operator(flag["conditions"])
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
- if override["by"] == "user_id"
77
+ case override["by"]
78
+ when "user_id"
64
79
  !user_id.nil? && override["who"] == user_id
65
- else
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
- matched = matches_condition?(traits, condition)
80
- note(
81
- trace,
82
- "condition",
83
- %(#{condition["trait"]} #{condition["operator"]} "#{condition["value"]}": #{matched ? "matched" : "no match"}),
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 "@".
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lucerna
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
 
6
6
  # Value of the x-lucerna-sdk header on every request.
7
7
  SDK_IDENTITY = "ruby/#{VERSION}"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lucerna
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lucerna