subflag-rails 0.6.0 → 0.6.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/CHANGELOG.md +2 -1
- data/app/views/subflag/rails/flags/edit.html.erb +58 -51
- data/lib/subflag/rails/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: 67d1ea6917f303ab8a78a5ffa90a9d5cfc3d6c30b7c13cd039f89cdd20365402
|
|
4
|
+
data.tar.gz: f8e5269abd5ef6ec77d8a4288b8a5ce91d114dcf96905d56c7aab5db368a1833
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7ba49356001ccb5676eccb9417e54bafc382bbc5ff0af19ad95a7567f4ac7a1fc1c2bd50505fad9d40ca4528e26e18c71900b423de149f91395c41686ce30b17
|
|
7
|
+
data.tar.gz: 18a47da01c9c44396faf29d4fe2234e4132f2479ff247aa5764f09fe0b27b361ceec05c65057f8cb8e5380ed2b9a319aa8a6fb7499882aa52ee38dcf7f0dcfab
|
data/CHANGELOG.md
CHANGED
|
@@ -9,13 +9,14 @@ All notable changes to this project will be documented in this file.
|
|
|
9
9
|
- **Percentage rollouts for ActiveRecord backend**: Gradually roll out features to a percentage of users
|
|
10
10
|
- Deterministic assignment using MurmurHash3 (same user always gets the same result)
|
|
11
11
|
- Combine with segment conditions (e.g., "50% of pro users")
|
|
12
|
-
-
|
|
12
|
+
- Admin UI: percentage field in targeting rules editor
|
|
13
13
|
- New dependency: `murmurhash3` gem for consistent hashing
|
|
14
14
|
|
|
15
15
|
### Changed
|
|
16
16
|
|
|
17
17
|
- `TargetingEngine.evaluate` now accepts optional `flag_key:` parameter (required for percentage rollouts)
|
|
18
18
|
- Targeting rules validation now accepts `percentage` as alternative to `conditions`
|
|
19
|
+
- Admin UI: targeting rules now saved via fetch API with proper redirect handling
|
|
19
20
|
|
|
20
21
|
## [0.5.1] - 2025-12-15
|
|
21
22
|
|
|
@@ -49,6 +49,8 @@
|
|
|
49
49
|
.rule-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
|
|
50
50
|
.rule-value { display: flex; gap: 10px; align-items: center; margin-bottom: 10px; }
|
|
51
51
|
.rule-value input { width: 200px; }
|
|
52
|
+
.rule-percentage { display: flex; gap: 10px; align-items: center; margin-bottom: 10px; }
|
|
53
|
+
.rule-percentage input { width: 80px; }
|
|
52
54
|
.rule-logic { margin-bottom: 10px; }
|
|
53
55
|
.rule-logic label { margin-right: 15px; }
|
|
54
56
|
.condition { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; }
|
|
@@ -94,6 +96,8 @@
|
|
|
94
96
|
function renderRule(rule, ruleIndex) {
|
|
95
97
|
const conditions = rule.conditions?.conditions || [];
|
|
96
98
|
const logicType = rule.conditions?.type || "AND";
|
|
99
|
+
const percentage = rule.percentage;
|
|
100
|
+
const hasConditions = conditions.length > 0;
|
|
97
101
|
|
|
98
102
|
return `
|
|
99
103
|
<div class="rule" data-rule-index="${ruleIndex}">
|
|
@@ -105,7 +109,13 @@
|
|
|
105
109
|
<label>Return value:</label>
|
|
106
110
|
<input type="text" value="${escapeHtml(rule.value || '')}" onchange="updateRuleValue(${ruleIndex}, this.value)" placeholder="value when matched">
|
|
107
111
|
</div>
|
|
108
|
-
<div class="rule-
|
|
112
|
+
<div class="rule-percentage">
|
|
113
|
+
<label>Percentage rollout:</label>
|
|
114
|
+
<input type="number" min="0" max="100" value="${percentage ?? ''}" onchange="updateRulePercentage(${ruleIndex}, this.value)" placeholder="">
|
|
115
|
+
<span>% of users</span>
|
|
116
|
+
<small style="color: #666;">(leave empty to match all)</small>
|
|
117
|
+
</div>
|
|
118
|
+
<div class="rule-logic" ${!hasConditions && percentage ? 'style="opacity: 0.5;"' : ''}>
|
|
109
119
|
<label><input type="radio" name="logic-${ruleIndex}" value="AND" ${logicType === 'AND' ? 'checked' : ''} onchange="updateLogic(${ruleIndex}, 'AND')"> ALL match</label>
|
|
110
120
|
<label><input type="radio" name="logic-${ruleIndex}" value="OR" ${logicType === 'OR' ? 'checked' : ''} onchange="updateLogic(${ruleIndex}, 'OR')"> ANY match</label>
|
|
111
121
|
</div>
|
|
@@ -113,6 +123,7 @@
|
|
|
113
123
|
${conditions.map((c, cIndex) => renderCondition(c, ruleIndex, cIndex)).join("")}
|
|
114
124
|
</div>
|
|
115
125
|
<button type="button" class="btn btn-sm" onclick="addCondition(${ruleIndex})">+ Add Condition</button>
|
|
126
|
+
<small style="display: block; margin-top: 5px; color: #666;">${hasConditions && percentage ? 'User must match conditions AND be in percentage' : ''}</small>
|
|
116
127
|
</div>
|
|
117
128
|
`;
|
|
118
129
|
}
|
|
@@ -139,7 +150,7 @@
|
|
|
139
150
|
function addRule() {
|
|
140
151
|
rules.push({
|
|
141
152
|
value: "",
|
|
142
|
-
conditions: { type: "AND", conditions: [
|
|
153
|
+
conditions: { type: "AND", conditions: [] }
|
|
143
154
|
});
|
|
144
155
|
renderRules();
|
|
145
156
|
}
|
|
@@ -153,6 +164,15 @@
|
|
|
153
164
|
rules[ruleIndex].value = value;
|
|
154
165
|
}
|
|
155
166
|
|
|
167
|
+
function updateRulePercentage(ruleIndex, value) {
|
|
168
|
+
if (value === '' || value === null) {
|
|
169
|
+
delete rules[ruleIndex].percentage;
|
|
170
|
+
} else {
|
|
171
|
+
rules[ruleIndex].percentage = parseInt(value, 10);
|
|
172
|
+
}
|
|
173
|
+
renderRules();
|
|
174
|
+
}
|
|
175
|
+
|
|
156
176
|
function updateLogic(ruleIndex, type) {
|
|
157
177
|
rules[ruleIndex].conditions.type = type;
|
|
158
178
|
}
|
|
@@ -180,55 +200,42 @@
|
|
|
180
200
|
}
|
|
181
201
|
|
|
182
202
|
function saveRules() {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
enabledInput.type = "hidden";
|
|
220
|
-
enabledInput.name = "flag[enabled]";
|
|
221
|
-
enabledInput.value = "<%= @flag.enabled? %>";
|
|
222
|
-
form.appendChild(enabledInput);
|
|
223
|
-
|
|
224
|
-
const rulesInput = document.createElement("input");
|
|
225
|
-
rulesInput.type = "hidden";
|
|
226
|
-
rulesInput.name = "flag[targeting_rules]";
|
|
227
|
-
rulesInput.value = JSON.stringify(rules);
|
|
228
|
-
form.appendChild(rulesInput);
|
|
229
|
-
|
|
230
|
-
document.body.appendChild(form);
|
|
231
|
-
form.submit();
|
|
203
|
+
// Clean up rules before saving - filter out invalid rules
|
|
204
|
+
const cleanedRules = rules.map(rule => {
|
|
205
|
+
const cleaned = { value: rule.value };
|
|
206
|
+
if (rule.percentage !== undefined && rule.percentage !== null && rule.percentage !== '') {
|
|
207
|
+
cleaned.percentage = rule.percentage;
|
|
208
|
+
}
|
|
209
|
+
const conditions = rule.conditions?.conditions?.filter(c => c.attribute && c.operator);
|
|
210
|
+
if (conditions && conditions.length > 0) {
|
|
211
|
+
cleaned.conditions = { type: rule.conditions?.type || "AND", conditions };
|
|
212
|
+
}
|
|
213
|
+
return cleaned;
|
|
214
|
+
}).filter(rule => rule.conditions || rule.percentage !== undefined);
|
|
215
|
+
|
|
216
|
+
const formData = new FormData();
|
|
217
|
+
formData.append("flag[key]", "<%= @flag.key %>");
|
|
218
|
+
formData.append("flag[value]", "<%= @flag.value %>");
|
|
219
|
+
formData.append("flag[value_type]", "<%= @flag.value_type %>");
|
|
220
|
+
formData.append("flag[enabled]", "<%= @flag.enabled? %>");
|
|
221
|
+
formData.append("flag[targeting_rules]", JSON.stringify(cleanedRules));
|
|
222
|
+
|
|
223
|
+
fetch("<%= flag_path(@flag) %>", {
|
|
224
|
+
method: "PATCH",
|
|
225
|
+
headers: {
|
|
226
|
+
"X-CSRF-Token": "<%= form_authenticity_token %>",
|
|
227
|
+
},
|
|
228
|
+
body: formData,
|
|
229
|
+
redirect: "manual"
|
|
230
|
+
}).then(response => {
|
|
231
|
+
if (response.ok || response.type === "opaqueredirect" || response.status === 302) {
|
|
232
|
+
window.location.reload();
|
|
233
|
+
} else {
|
|
234
|
+
alert("Failed to save rules: " + response.status);
|
|
235
|
+
}
|
|
236
|
+
}).catch(err => {
|
|
237
|
+
alert("Error saving rules: " + err.message);
|
|
238
|
+
});
|
|
232
239
|
}
|
|
233
240
|
|
|
234
241
|
function escapeHtml(str) {
|