subflag-rails 0.5.1 → 0.6.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/CHANGELOG.md +15 -0
- data/README.md +12 -0
- data/lib/subflag/rails/models/flag.rb +15 -2
- data/lib/subflag/rails/targeting_engine.rb +43 -6
- data/lib/subflag/rails/version.rb +1 -1
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 474015cc849b73a4360fa11418161fbc78c6bbc163596b5b9add7e45929b6db4
|
|
4
|
+
data.tar.gz: 25f595010b6722d107b0604774ac089386e9d3ced39d7fef72411964a310140c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: afbe39727f8e70a3273b77e49bf8127debc459487b4d01bc14693c682082592b11fd3506388a8db2bd5021e9bbb914767e19f279a56a56a30c114fe79f10b0f2
|
|
7
|
+
data.tar.gz: f124c9b3103ff13453615be92dfe7639fabfeb042d514bc4232b5a73e8fa1dcbe812e4d0fb418c45a35c33ac0e71753958d37cff01c6742ff991a408e637946f
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.6.0] - 2025-12-15
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Percentage rollouts for ActiveRecord backend**: Gradually roll out features to a percentage of users
|
|
10
|
+
- Deterministic assignment using MurmurHash3 (same user always gets the same result)
|
|
11
|
+
- Combine with segment conditions (e.g., "50% of pro users")
|
|
12
|
+
- Configure via Admin UI or targeting rules JSON
|
|
13
|
+
- New dependency: `murmurhash3` gem for consistent hashing
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- `TargetingEngine.evaluate` now accepts optional `flag_key:` parameter (required for percentage rollouts)
|
|
18
|
+
- Targeting rules validation now accepts `percentage` as alternative to `conditions`
|
|
19
|
+
|
|
5
20
|
## [0.5.1] - 2025-12-15
|
|
6
21
|
|
|
7
22
|
### Fixed
|
data/README.md
CHANGED
|
@@ -482,6 +482,18 @@ Subflag::Rails::Flag.create!(
|
|
|
482
482
|
}
|
|
483
483
|
```
|
|
484
484
|
|
|
485
|
+
**Percentage rollouts:**
|
|
486
|
+
|
|
487
|
+
Gradually roll out features to a percentage of users using the Admin UI:
|
|
488
|
+
|
|
489
|
+
1. Create or edit a flag at `/subflag`
|
|
490
|
+
2. Add a targeting rule with a **percentage** (0-100)
|
|
491
|
+
3. Optionally combine with conditions (e.g., "50% of pro users")
|
|
492
|
+
|
|
493
|
+
Assignment is deterministic — the same user always gets the same result for the same flag.
|
|
494
|
+
|
|
495
|
+
> **Note:** Percentage rollouts require `targeting_key` in your user context (typically the user ID).
|
|
496
|
+
|
|
485
497
|
**How evaluation works:**
|
|
486
498
|
|
|
487
499
|
1. Flag disabled? → return code default
|
|
@@ -70,7 +70,7 @@ module Subflag
|
|
|
70
70
|
def evaluate(context: nil, expected_type: nil)
|
|
71
71
|
rules = parsed_targeting_rules
|
|
72
72
|
raw_value = if rules.present? && context.present?
|
|
73
|
-
matched = TargetingEngine.evaluate(rules, context)
|
|
73
|
+
matched = TargetingEngine.evaluate(rules, context, flag_key: key)
|
|
74
74
|
matched || value
|
|
75
75
|
else
|
|
76
76
|
value
|
|
@@ -146,7 +146,20 @@ module Subflag
|
|
|
146
146
|
|
|
147
147
|
rule = rule.transform_keys(&:to_s)
|
|
148
148
|
errors.add(:targeting_rules, "rule #{index} must have a 'value' key") unless rule.key?("value")
|
|
149
|
-
|
|
149
|
+
|
|
150
|
+
has_conditions = rule.key?("conditions") && rule["conditions"].present?
|
|
151
|
+
has_percentage = rule.key?("percentage")
|
|
152
|
+
|
|
153
|
+
unless has_conditions || has_percentage
|
|
154
|
+
errors.add(:targeting_rules, "rule #{index} must have 'conditions' and/or 'percentage'")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
if has_percentage
|
|
158
|
+
pct = rule["percentage"]
|
|
159
|
+
unless pct.is_a?(Numeric) && pct >= 0 && pct <= 100
|
|
160
|
+
errors.add(:targeting_rules, "rule #{index} percentage must be a number between 0 and 100")
|
|
161
|
+
end
|
|
162
|
+
end
|
|
150
163
|
end
|
|
151
164
|
end
|
|
152
165
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "murmurhash3"
|
|
4
|
+
|
|
3
5
|
module Subflag
|
|
4
6
|
module Rails
|
|
5
7
|
# Evaluates targeting rules against evaluation contexts.
|
|
@@ -34,25 +36,56 @@ module Subflag
|
|
|
34
36
|
#
|
|
35
37
|
# @param rules [Array<Hash>, nil] Array of targeting rules with values
|
|
36
38
|
# @param context [Hash, nil] Evaluation context with attributes
|
|
39
|
+
# @param flag_key [String, nil] The flag key (required for percentage rollouts)
|
|
37
40
|
# @return [String, nil] The matched rule's value, or nil if no match
|
|
38
|
-
def evaluate(rules, context)
|
|
41
|
+
def evaluate(rules, context, flag_key: nil)
|
|
39
42
|
return nil if rules.nil? || rules.empty?
|
|
40
43
|
return nil if context.nil? || context.empty?
|
|
41
44
|
|
|
42
|
-
# Normalize context keys to strings for comparison
|
|
43
45
|
normalized_context = normalize_context(context)
|
|
46
|
+
targeting_key = extract_targeting_key(normalized_context)
|
|
44
47
|
|
|
45
48
|
rules.each do |rule|
|
|
46
49
|
rule = rule.transform_keys(&:to_s)
|
|
47
50
|
conditions = rule["conditions"]
|
|
48
|
-
|
|
51
|
+
percentage = rule["percentage"]
|
|
52
|
+
|
|
53
|
+
segment_matches = if conditions.nil? || conditions.empty?
|
|
54
|
+
true
|
|
55
|
+
else
|
|
56
|
+
evaluate_rule(conditions, normalized_context)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
next unless segment_matches
|
|
49
60
|
|
|
50
|
-
if
|
|
51
|
-
|
|
61
|
+
if percentage
|
|
62
|
+
next unless targeting_key && flag_key
|
|
63
|
+
next unless evaluate_percentage(targeting_key, flag_key, percentage)
|
|
52
64
|
end
|
|
65
|
+
|
|
66
|
+
return rule["value"]
|
|
53
67
|
end
|
|
54
68
|
|
|
55
|
-
nil
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Evaluate percentage rollout using MurmurHash3
|
|
73
|
+
#
|
|
74
|
+
# @param targeting_key [String] Unique identifier for the context
|
|
75
|
+
# @param flag_key [String] The flag being evaluated
|
|
76
|
+
# @param percentage [Integer] Target percentage (0-100)
|
|
77
|
+
# @return [Boolean] true if context falls within percentage
|
|
78
|
+
def evaluate_percentage(targeting_key, flag_key, percentage)
|
|
79
|
+
percentage = percentage.to_i
|
|
80
|
+
return false if percentage <= 0
|
|
81
|
+
return true if percentage >= 100
|
|
82
|
+
|
|
83
|
+
hash_input = "#{targeting_key}:#{flag_key}"
|
|
84
|
+
hash_bytes = MurmurHash3::V128.str_hash(hash_input)
|
|
85
|
+
hash_code = [hash_bytes[0]].pack("L").unpack1("l")
|
|
86
|
+
bucket = hash_code.abs % 100
|
|
87
|
+
|
|
88
|
+
bucket < percentage
|
|
56
89
|
end
|
|
57
90
|
|
|
58
91
|
private
|
|
@@ -63,6 +96,10 @@ module Subflag
|
|
|
63
96
|
end
|
|
64
97
|
end
|
|
65
98
|
|
|
99
|
+
def extract_targeting_key(context)
|
|
100
|
+
context["targeting_key"] || context["targetingKey"]
|
|
101
|
+
end
|
|
102
|
+
|
|
66
103
|
# Evaluate an AND/OR rule block
|
|
67
104
|
def evaluate_rule(rule, context)
|
|
68
105
|
rule = rule.transform_keys(&:to_s)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: subflag-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Subflag
|
|
@@ -58,6 +58,20 @@ dependencies:
|
|
|
58
58
|
- - ">="
|
|
59
59
|
- !ruby/object:Gem::Version
|
|
60
60
|
version: '6.1'
|
|
61
|
+
- !ruby/object:Gem::Dependency
|
|
62
|
+
name: murmurhash3
|
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: 0.1.6
|
|
68
|
+
type: :runtime
|
|
69
|
+
prerelease: false
|
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: 0.1.6
|
|
61
75
|
- !ruby/object:Gem::Dependency
|
|
62
76
|
name: bundler
|
|
63
77
|
requirement: !ruby/object:Gem::Requirement
|