featureflip 2.5.0 → 2.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f3e88364434f9dced8db82624524bf1ffe905b35b5ae8a062896bfb6c02dc5e6
4
- data.tar.gz: 87d5bdaa62bb9f6e8eb0685f235315b31adfb5cd0dbdaac478965144a09709ff
3
+ metadata.gz: a41a0e3edb8d4325e4a0784930973f07ad8339e7b25b127f076bc7cfade0d948
4
+ data.tar.gz: 6ceb86141220f142710e6a0937af742270c61961ff87f583a2d727bab1c6ee65
5
5
  SHA512:
6
- metadata.gz: 7b9c0cbfce3e7451f4218dc5a0274a28181d770a5689427d5fba20899702c5a580413b6adcb99482c717708cc2b60cccd5e9ee6dfb423351ff82024befae0477
7
- data.tar.gz: d4c066e87770edfa6881cf000d36cc8e7f9f476cc3afebc8b44e5842f2c5946805af79a47dbceb575bd4e50496e56d024b3485fa083d414fb52bf8809b25eff2
6
+ metadata.gz: 013af1bacae587f2dd832bd7004ac4a94a45e8d635c64204d8ea892d4e62c80112d07856856d097f03393f4bbf7c3aef7d54c378672db9c61623d9053b8e3e3b
7
+ data.tar.gz: 822537c99bfdd30d8f2f4b16b9b8592d2cd23fdaccaf2cea0333fccdf68f0998877bf124da0060ec63abc0fb347f6506ad10f0fc235d8f22291de50a68f20a33
@@ -32,6 +32,14 @@ module Featureflip
32
32
  targets = condition.values.map(&:to_s)
33
33
 
34
34
  result = evaluate_operator(condition.operator, str_value, targets)
35
+
36
+ # Issue #2262: an unrecognised operator fails CLOSED. `!nil` is `true`
37
+ # in Ruby, so without this guard a negated unknown operator would match
38
+ # every user and roll the flag out to 100% of traffic. The realistic
39
+ # trigger is a new operator shipped server-side reaching an SDK pinned
40
+ # to an older version.
41
+ return false if result.nil?
42
+
35
43
  condition.negate ? !result : result
36
44
  end
37
45
 
@@ -150,7 +158,10 @@ module Featureflip
150
158
  when "SemverLessThanOrEqual"
151
159
  targets.any? { |t| compare_semver(value, t, :<=) }
152
160
  else
153
- false
161
+ # Unrecognised operator. `nil` — NOT `false` — so the caller can tell
162
+ # "cannot evaluate" apart from "evaluated, did not match"; only the
163
+ # latter may be inverted by `negate` (#2262).
164
+ nil
154
165
  end
155
166
  end
156
167
 
@@ -241,25 +241,29 @@ module Featureflip
241
241
  return unless @event_processor
242
242
 
243
243
  context = normalize_context(context)
244
- @event_processor.queue_event({
244
+ @event_processor.queue_event(compact_event({
245
245
  type: "Custom",
246
246
  flagKey: event_key,
247
- userId: context["user_id"]&.to_s,
248
- metadata: metadata || {},
247
+ userId: resolve_event_user_id(context),
248
+ metadata: metadata,
249
249
  timestamp: Time.now.utc.iso8601
250
- })
250
+ }))
251
251
  end
252
252
 
253
253
  def identify(context)
254
254
  return unless @event_processor
255
255
 
256
256
  context = normalize_context(context)
257
- @event_processor.queue_event({
257
+ # Strip both identity spellings so the id is carried once, at the top
258
+ # level, and not duplicated inside the attribute bag.
259
+ attributes = context.reject { |k, _| IDENTITY_KEYS.include?(k) }
260
+ @event_processor.queue_event(compact_event({
258
261
  type: "Identify",
259
262
  flagKey: "$identify",
260
- userId: context["user_id"]&.to_s,
263
+ userId: resolve_event_user_id(context),
264
+ metadata: attributes,
261
265
  timestamp: Time.now.utc.iso8601
262
- })
266
+ }))
263
267
  end
264
268
 
265
269
  def flush
@@ -417,16 +421,43 @@ module Featureflip
417
421
  context.transform_keys(&:to_s)
418
422
  end
419
423
 
424
+ # Both spellings of the identity are accepted on an event context; the
425
+ # canonical +user_id+ wins when a caller supplies both. The evaluator
426
+ # already aliases these for bucketing, so events have to as well or an
427
+ # alias caller gets every event attributed to nil.
428
+ IDENTITY_KEYS = %w[user_id userId].freeze
429
+
430
+ def resolve_event_user_id(context)
431
+ raw = context["user_id"]
432
+ raw = context["userId"] if raw.nil?
433
+ raw&.to_s
434
+ end
435
+
436
+ # +userId+ and +metadata+ are optional on the wire. Omit them rather than
437
+ # sending nulls or empty bags, matching the other SDKs.
438
+ #
439
+ # +metadata+ is caller-supplied and untyped, so the emptiness check is
440
+ # guarded: a caller passing a non-collection must not take a NoMethodError
441
+ # into their request path just to record an analytics event.
442
+ def compact_event(event)
443
+ event.delete(:userId) if event[:userId].nil?
444
+ metadata = event[:metadata]
445
+ if metadata.nil? || (metadata.respond_to?(:empty?) && metadata.empty?)
446
+ event.delete(:metadata)
447
+ end
448
+ event
449
+ end
450
+
420
451
  def record_evaluation(key, context, variation_key)
421
452
  return unless @event_processor
422
453
 
423
- @event_processor.queue_event({
454
+ @event_processor.queue_event(compact_event({
424
455
  type: "Evaluation",
425
456
  flagKey: key,
426
- userId: context["user_id"]&.to_s,
457
+ userId: resolve_event_user_id(context),
427
458
  variation: variation_key,
428
459
  timestamp: Time.now.utc.iso8601
429
- })
460
+ }))
430
461
  end
431
462
 
432
463
  # Fire the registered evaluation inspectors. Called once per variation call
@@ -1,3 +1,3 @@
1
1
  module Featureflip
2
- VERSION = "2.5.0"
2
+ VERSION = "2.5.1"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: featureflip
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.0
4
+ version: 2.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Featureflip
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-20 00:00:00.000000000 Z
11
+ date: 2026-08-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logger