subflag-rails 0.6.4 → 0.6.6
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 +7 -0
- data/lib/subflag/rails/backends/active_record_provider.rb +2 -22
- data/lib/subflag/rails/backends/memory_provider.rb +1 -1
- data/lib/subflag/rails/client.rb +23 -8
- data/lib/subflag/rails/context_builder.rb +22 -0
- data/lib/subflag/rails/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 44e409a1256b7345aeac0b7331b0f62f0d3567a3264ecbf0b545c45536c4394e
|
|
4
|
+
data.tar.gz: 2ee8e1d0ed08753557fc1116855c56c9479870560c754b208295f9953ace4ae6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f75bc923804895371719e8ac2290440ecd75071232ca2b78905c5c13564908a9990e7122995b0bb8a0ca311a5d5f31b6e1e2d94831e86d8136b3f50bb6f5ac4f
|
|
7
|
+
data.tar.gz: 5a661e0587dc7c22373684a22a4ca753910378b36ca923dd4db6aabdc95d1cffa9254a5c00a22d90301d7cacb1733520ae09e2657b56d2ddac9784cea6c85506
|
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.6] - 2026-07-30
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`prefetch_all` / `subflag_prefetch` returned untargeted base values for the `active_record` backend**: `prefetch_from_active_record` now calls `flag.evaluate(context:)` instead of `flag.typed_value`, so prefetched values match what `subflag_enabled?` / `Subflag.flags(...).x?` return for flags with `targeting_rules` or percentage rollouts ([#1](https://github.com/subflag/sdk/issues/1))
|
|
10
|
+
- **Prefetch/request cache was effectively unhittable whenever a `user:`/`context:` was passed**: cache keys were derived from `EvaluationContext#hash`, which is identity-based (the class overrides `==` but not `hash`), so separately-built contexts with identical fields never matched. Cache keys now derive from `EvaluationContext#fields.hash`, which is content-based.
|
|
11
|
+
|
|
5
12
|
## [0.6.4] - 2026-01-23
|
|
6
13
|
|
|
7
14
|
### Changed
|
|
@@ -32,7 +32,7 @@ module Subflag
|
|
|
32
32
|
#
|
|
33
33
|
class ActiveRecordProvider
|
|
34
34
|
def metadata
|
|
35
|
-
|
|
35
|
+
OpenFeature::SDK::Provider::ProviderMetadata.new(name: "Subflag ActiveRecord Provider")
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def init; end
|
|
@@ -72,7 +72,7 @@ module Subflag
|
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
# Convert OpenFeature context to hash for targeting evaluation
|
|
75
|
-
context =
|
|
75
|
+
context = ContextBuilder.to_hash(evaluation_context)
|
|
76
76
|
value = flag.evaluate(context: context, expected_type: expected_type)
|
|
77
77
|
|
|
78
78
|
# Determine if targeting matched
|
|
@@ -80,26 +80,6 @@ module Subflag
|
|
|
80
80
|
resolution(value, reason: reason, variant: "default")
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
-
def context_to_hash(evaluation_context)
|
|
84
|
-
return nil if evaluation_context.nil?
|
|
85
|
-
|
|
86
|
-
# OpenFeature::SDK::EvaluationContext stores fields as instance variables
|
|
87
|
-
# We need to extract them into a hash for our targeting engine
|
|
88
|
-
if evaluation_context.respond_to?(:to_h)
|
|
89
|
-
evaluation_context.to_h
|
|
90
|
-
elsif evaluation_context.respond_to?(:fields)
|
|
91
|
-
evaluation_context.fields
|
|
92
|
-
else
|
|
93
|
-
# Fallback: extract instance variables
|
|
94
|
-
hash = {}
|
|
95
|
-
evaluation_context.instance_variables.each do |var|
|
|
96
|
-
key = var.to_s.delete("@")
|
|
97
|
-
hash[key] = evaluation_context.instance_variable_get(var)
|
|
98
|
-
end
|
|
99
|
-
hash
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
83
|
def resolution(value, reason:, variant: nil)
|
|
104
84
|
OpenFeature::SDK::Provider::ResolutionDetails.new(
|
|
105
85
|
value: value,
|
data/lib/subflag/rails/client.rb
CHANGED
|
@@ -132,7 +132,7 @@ module Subflag
|
|
|
132
132
|
# Prefetch flags from Subflag Cloud API
|
|
133
133
|
def prefetch_from_subflag_api(user:, context:)
|
|
134
134
|
ctx = ContextBuilder.build(user: user, context: context)
|
|
135
|
-
context_hash = ctx
|
|
135
|
+
context_hash = context_cache_key(ctx)
|
|
136
136
|
|
|
137
137
|
# Use Rails.cache for cross-request caching if enabled
|
|
138
138
|
if configuration.rails_cache_enabled?
|
|
@@ -189,29 +189,44 @@ module Subflag
|
|
|
189
189
|
return [] unless RequestCache.enabled?
|
|
190
190
|
|
|
191
191
|
ctx = ContextBuilder.build(user: user, context: context)
|
|
192
|
-
context_hash = ctx
|
|
192
|
+
context_hash = context_cache_key(ctx)
|
|
193
|
+
context_data = ContextBuilder.to_hash(ctx)
|
|
193
194
|
|
|
194
195
|
prefetched = []
|
|
195
196
|
|
|
196
197
|
Subflag::Rails::Flag.enabled.find_each do |flag|
|
|
198
|
+
value = flag.evaluate(context: context_data)
|
|
199
|
+
reason = flag.targeting_rules.present? && context_data.present? ? "TARGETING_MATCH" : "STATIC"
|
|
200
|
+
|
|
197
201
|
prefetch_key = "subflag:prefetch:#{flag.key}:#{context_hash}"
|
|
198
202
|
RequestCache.current_cache[prefetch_key] = PrefetchedFlag.new(
|
|
199
203
|
flag_key: flag.key,
|
|
200
|
-
value:
|
|
201
|
-
reason:
|
|
204
|
+
value: value,
|
|
205
|
+
reason: reason,
|
|
202
206
|
variant: "default"
|
|
203
207
|
)
|
|
204
|
-
prefetched << { flag_key: flag.key, value:
|
|
208
|
+
prefetched << { flag_key: flag.key, value: value }
|
|
205
209
|
end
|
|
206
210
|
|
|
207
211
|
prefetched
|
|
208
212
|
end
|
|
209
213
|
|
|
210
214
|
def build_cache_key(flag_key, ctx, type)
|
|
211
|
-
context_hash = ctx
|
|
215
|
+
context_hash = context_cache_key(ctx)
|
|
212
216
|
"subflag:#{flag_key}:#{context_hash}:#{type}"
|
|
213
217
|
end
|
|
214
218
|
|
|
219
|
+
# A stable cache-key fragment for an evaluation context.
|
|
220
|
+
#
|
|
221
|
+
# OpenFeature::SDK::EvaluationContext overrides #== but not #hash, so its
|
|
222
|
+
# default Object#hash is identity-based and differs across separately-built
|
|
223
|
+
# instances with identical fields. #fields is a plain Hash, whose #hash IS
|
|
224
|
+
# content-based, so two contexts built from the same user/context data
|
|
225
|
+
# produce the same cache key.
|
|
226
|
+
def context_cache_key(ctx)
|
|
227
|
+
ctx ? ctx.fields.hash : "no_context"
|
|
228
|
+
end
|
|
229
|
+
|
|
215
230
|
def openfeature_client
|
|
216
231
|
@openfeature_client ||= OpenFeature::SDK.build_client
|
|
217
232
|
end
|
|
@@ -303,7 +318,7 @@ module Subflag
|
|
|
303
318
|
def get_prefetched_value(flag_key, ctx, expected_type)
|
|
304
319
|
return nil unless RequestCache.enabled?
|
|
305
320
|
|
|
306
|
-
context_hash = ctx
|
|
321
|
+
context_hash = context_cache_key(ctx)
|
|
307
322
|
prefetch_key = "subflag:prefetch:#{flag_key}:#{context_hash}"
|
|
308
323
|
result = RequestCache.current_cache[prefetch_key]
|
|
309
324
|
|
|
@@ -317,7 +332,7 @@ module Subflag
|
|
|
317
332
|
def get_prefetched_result(flag_key, ctx)
|
|
318
333
|
return nil unless RequestCache.enabled?
|
|
319
334
|
|
|
320
|
-
context_hash = ctx
|
|
335
|
+
context_hash = context_cache_key(ctx)
|
|
321
336
|
prefetch_key = "subflag:prefetch:#{flag_key}:#{context_hash}"
|
|
322
337
|
RequestCache.current_cache[prefetch_key]
|
|
323
338
|
end
|
|
@@ -13,6 +13,28 @@ module Subflag
|
|
|
13
13
|
new(user: user, context: context).build
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
# Convert an OpenFeature EvaluationContext (or nil) into a plain Hash
|
|
17
|
+
# suitable for Flag#evaluate / TargetingEngine.
|
|
18
|
+
#
|
|
19
|
+
# @param evaluation_context [OpenFeature::SDK::EvaluationContext, nil]
|
|
20
|
+
# @return [Hash, nil]
|
|
21
|
+
def self.to_hash(evaluation_context)
|
|
22
|
+
return nil if evaluation_context.nil?
|
|
23
|
+
|
|
24
|
+
if evaluation_context.respond_to?(:to_h)
|
|
25
|
+
evaluation_context.to_h
|
|
26
|
+
elsif evaluation_context.respond_to?(:fields)
|
|
27
|
+
evaluation_context.fields
|
|
28
|
+
else
|
|
29
|
+
hash = {}
|
|
30
|
+
evaluation_context.instance_variables.each do |var|
|
|
31
|
+
key = var.to_s.delete("@")
|
|
32
|
+
hash[key] = evaluation_context.instance_variable_get(var)
|
|
33
|
+
end
|
|
34
|
+
hash
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
16
38
|
def initialize(user: nil, context: nil)
|
|
17
39
|
@user = user
|
|
18
40
|
@context = context || {}
|
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.
|
|
4
|
+
version: 0.6.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Subflag
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: openfeature-sdk
|