subflag-rails 0.6.0 → 0.6.2

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: 474015cc849b73a4360fa11418161fbc78c6bbc163596b5b9add7e45929b6db4
4
- data.tar.gz: 25f595010b6722d107b0604774ac089386e9d3ced39d7fef72411964a310140c
3
+ metadata.gz: 4da7fbf4afd2e81121d78a29b650556e3c70ae35b80bee1f3edc484b25e67151
4
+ data.tar.gz: 2a8bf9240dfc0875fbb6bf7e3dde7ea608c606125aaeb00affbf84b1cf6e9851
5
5
  SHA512:
6
- metadata.gz: afbe39727f8e70a3273b77e49bf8127debc459487b4d01bc14693c682082592b11fd3506388a8db2bd5021e9bbb914767e19f279a56a56a30c114fe79f10b0f2
7
- data.tar.gz: f124c9b3103ff13453615be92dfe7639fabfeb042d514bc4232b5a73e8fa1dcbe812e4d0fb418c45a35c33ac0e71753958d37cff01c6742ff991a408e637946f
6
+ metadata.gz: a9d2171cb2ef2a919701dfc5a4e2a89bd64f498bca92dc5a77779982c5357f427f8894bd368250c94c6ff26db2b67ccc4f9f9aa787c5bccd9a4b304946413c50
7
+ data.tar.gz: 9476c50e529d665d30f5bda1355f5a21c4009756124f3ddaf1efb5e16ccae668b572c0e30b310a438f8a6cf3a0c34040e1992304370d428e29a71e1ec027fe0c
data/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.6.2] - 2026-01-22
6
+
7
+ ### Fixed
8
+
9
+ - **CSP nonce support for Admin UI**: Use `javascript_tag nonce: true` for inline scripts
10
+ - Fixes admin UI not loading on apps with strict Content Security Policy headers
11
+
5
12
  ## [0.6.0] - 2025-12-15
6
13
 
7
14
  ### Added
@@ -9,13 +16,14 @@ All notable changes to this project will be documented in this file.
9
16
  - **Percentage rollouts for ActiveRecord backend**: Gradually roll out features to a percentage of users
10
17
  - Deterministic assignment using MurmurHash3 (same user always gets the same result)
11
18
  - Combine with segment conditions (e.g., "50% of pro users")
12
- - Configure via Admin UI or targeting rules JSON
19
+ - Admin UI: percentage field in targeting rules editor
13
20
  - New dependency: `murmurhash3` gem for consistent hashing
14
21
 
15
22
  ### Changed
16
23
 
17
24
  - `TargetingEngine.evaluate` now accepts optional `flag_key:` parameter (required for percentage rollouts)
18
25
  - Targeting rules validation now accepts `percentage` as alternative to `conditions`
26
+ - Admin UI: targeting rules now saved via fetch API with proper redirect handling
19
27
 
20
28
  ## [0.5.1] - 2025-12-15
21
29
 
@@ -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; }
@@ -58,7 +60,7 @@
58
60
  .remove-btn:hover { color: #c82333; }
59
61
  </style>
60
62
 
61
- <script>
63
+ <%= javascript_tag nonce: true do %>
62
64
  const OPERATORS = [
63
65
  { value: "EQUALS", label: "equals" },
64
66
  { value: "NOT_EQUALS", label: "not equals" },
@@ -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-logic">
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: [{ attribute: "", operator: "EQUALS", value: "" }] }
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
- const form = document.createElement("form");
184
- form.method = "POST";
185
- form.action = "<%= flag_path(@flag) %>";
186
-
187
- const methodInput = document.createElement("input");
188
- methodInput.type = "hidden";
189
- methodInput.name = "_method";
190
- methodInput.value = "PATCH";
191
- form.appendChild(methodInput);
192
-
193
- const csrfInput = document.createElement("input");
194
- csrfInput.type = "hidden";
195
- csrfInput.name = "<%= request_forgery_protection_token %>";
196
- csrfInput.value = "<%= form_authenticity_token %>";
197
- form.appendChild(csrfInput);
198
-
199
- // Send all flag params to preserve other fields
200
- const keyInput = document.createElement("input");
201
- keyInput.type = "hidden";
202
- keyInput.name = "flag[key]";
203
- keyInput.value = "<%= @flag.key %>";
204
- form.appendChild(keyInput);
205
-
206
- const valueInput = document.createElement("input");
207
- valueInput.type = "hidden";
208
- valueInput.name = "flag[value]";
209
- valueInput.value = "<%= @flag.value %>";
210
- form.appendChild(valueInput);
211
-
212
- const typeInput = document.createElement("input");
213
- typeInput.type = "hidden";
214
- typeInput.name = "flag[value_type]";
215
- typeInput.value = "<%= @flag.value_type %>";
216
- form.appendChild(typeInput);
217
-
218
- const enabledInput = document.createElement("input");
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) {
@@ -238,4 +245,4 @@
238
245
  }
239
246
 
240
247
  document.addEventListener("DOMContentLoaded", init);
241
- </script>
248
+ <% end %>
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Subflag
4
4
  module Rails
5
- VERSION = "0.6.0"
5
+ VERSION = "0.6.2"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: subflag-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Subflag
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-12-15 00:00:00.000000000 Z
11
+ date: 2026-01-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: openfeature-sdk