toggly 0.6.0 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f6a78a0c74623574333be32320578cdd12ee7bf1415ed199c2584716b5e98aa
4
- data.tar.gz: c38ea07fda43a2ce57af893804471bd45ac819d51019e47e082aeccf0411e157
3
+ metadata.gz: d728557cdb56574a22377e85047ec649a801532f9cea3574ae66ccba703e320e
4
+ data.tar.gz: 5054b15ad5ad1b48fde5c6fb174698c701bd8fa38b6054cc8afd6ff015e196ce
5
5
  SHA512:
6
- metadata.gz: 9e6ce90750098dd4d67642925c3239f1a39a43d5f894d6de60949614d5e6c1de662912073001b9e1c1db42cc6baf9fd39e99efbeeb629d71dd92c54420461539
7
- data.tar.gz: 3dce06734ce96188493c9fef22d030f17e538f8ae8711a128c5918a7c9c9ecdedd3786e1d6881553b1c962185940523df22a576ec059e8649c670d70053becd2
6
+ metadata.gz: 8871e715209c606026d37367ec46522c3bbcc7925633f9f511bad51179f141924cb4b7e123978212e1a1697050072d0bfacff0463e53cf02510012cb28531db1
7
+ data.tar.gz: c3536488d775b49aeefa375f72f3c6b88a993fb4b6464a19e8c5ae73bde75222c310bc820e563f94039509bd6dbbc698ddb04c33cd89de6beba2e7c1ac29dd56
data/CHANGELOG.md CHANGED
@@ -5,6 +5,53 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.1.0] - 2026-09-23
9
+
10
+ ### Added
11
+
12
+ - Ambient targeting identity for catalog-local variants: `Config#identity`,
13
+ `Client#identity` / `Client#set_identity`. When `get_variant` /
14
+ `get_variant_value` are called with a nil context or a blank
15
+ `context.identity`, the client identity is used as the allocator
16
+ `userId`. Groups still come only from the per-call context (never Config).
17
+ Prefer config / `set_identity` (or Rails ambient context) over building
18
+ `Context.new(identity: …)` at every call site.
19
+ `Config#identity=` is configuration-time only (`Toggly.configure` /
20
+ `Config.new`); after `Client` is built, use `Client#set_identity`.
21
+
22
+ ## [1.0.0] - 2026-09-23
23
+
24
+ ### Changed (Breaking)
25
+
26
+ - **Catalog-local, MF-parity feature variants**, replacing the
27
+ `enable_variants` / `evaluated-variants-signed` dual-rail from 0.6.0.
28
+ `Client#get_variant` / `#get_variant_value` now assign variants **locally**
29
+ from the same `variants` / `allocation` payload on the `definitions` /
30
+ `definitions-signed` catalog that drives `enabled?` — there is no separate
31
+ network call. Assignment matches `Microsoft.FeatureManagement` 4.7.0's
32
+ `IVariantFeatureManager` bit-for-bit (user → group → percentile → default,
33
+ `StatusOverride`, percentile SHA-256 hashing) and is verified against the
34
+ shared `variant-allocator-corpus/cases.json` gold corpus (100% pass).
35
+ - `get_variant(feature_key, context: nil)` / `get_variant_value(feature_key,
36
+ context: nil)` now take the same `context:` (`userId` + `groups`) used by
37
+ `enabled?`, instead of a client-wide `variant_identity`.
38
+ - `VariantResult` gains `enabled` (effective enabled after the assigned
39
+ variant's `StatusOverride` — `enabled?` itself stays filter-based only)
40
+ and `reason` (`"User"` | `"Group"` | `"Percentile"` |
41
+ `"DefaultWhenEnabled"` | `"DefaultWhenDisabled"`).
42
+ - New `FeatureDefinition#variants` / `#allocation` (`FeatureVariant`,
43
+ `FeatureVariantAllocation`), parsed from the `definitions` wire.
44
+ - **Removed**: `Config#enable_variants` / `#variant_identity` /
45
+ `#variant_groups` / `#variant_claims` / `#variants_endpoint`,
46
+ `Client#variant_defs` / `#set_variant_identity`, `EvaluatedVariantDef`,
47
+ and the `evaluated-variants-signed` fetch/cache rail (including the
48
+ `SnapshotProviders` `save_variants` / `load_variants` hooks — variants are
49
+ now part of the ordinary definitions snapshot via `FeatureDefinition`).
50
+
51
+ **Migration**: drop `enable_variants` / `variant_identity` /
52
+ `set_variant_identity` from your config. Pass targeting via
53
+ `get_variant(key, context: Toggly::Context.new(identity: ..., groups: ...))`.
54
+
8
55
  ## [0.6.0] - 2026-09-22
9
56
 
10
57
  ### Added
data/README.md CHANGED
@@ -38,6 +38,34 @@ if client.enabled?(:my_feature)
38
38
  end
39
39
  ```
40
40
 
41
+ ## Feature variants (catalog-local, MF-parity)
42
+
43
+ `get_variant` / `get_variant_value` assign a variant purely from the
44
+ feature's `variants` / `allocation` catalog data (no network round-trip),
45
+ matching `Microsoft.FeatureManagement`'s `IVariantFeatureManager`
46
+ bit-for-bit — verified against the shared
47
+ `variant-allocator-corpus/cases.json` gold corpus.
48
+
49
+ ```ruby
50
+ # Prefer ambient identity: Config#identity, Client#set_identity, or Rails
51
+ # feature_variant (uses toggly_context). Per-call context is an override.
52
+ client = Toggly::Client.new(app_key: '...', environment: 'Production', identity: 'user-1')
53
+ client.set_identity('user-2') # optional runtime update
54
+
55
+ variant = client.get_variant('checkout-flow') # uses client identity
56
+ variant = client.get_variant('checkout-flow', context: Toggly::Context.new(identity: 'override'))
57
+ variant&.name # assigned variant name, or nil
58
+ variant&.configuration_value # untyped configuration payload
59
+ variant&.enabled # effective enabled after StatusOverride
60
+ variant&.reason # "User" | "Group" | "Percentile" | "DefaultWhenEnabled" | "DefaultWhenDisabled"
61
+
62
+ client.get_variant_value('checkout-flow') # shortcut for configuration_value
63
+ ```
64
+
65
+ `enabled?` stays filter-based only. The `enable_variants` /
66
+ `evaluated-variants-signed` dual-rail (and `set_variant_identity`) was
67
+ removed in 1.0 — see [CHANGELOG.md](CHANGELOG.md).
68
+
41
69
  ## Usage & metrics telemetry
42
70
 
43
71
  When `app_key` is set, usage tracking and metrics default **on** (disable with
@@ -2,31 +2,22 @@
2
2
 
3
3
  module Toggly
4
4
  class Client
5
- # Durable snapshot load/save helpers for Client. Dual-rail: `definitions`
6
- # is always persisted/restored (sole source of truth for `enabled?`).
7
- # `variant_defs` is an additive rail, persisted/restored independently
8
- # only when `config.enable_variants` is true — it never replaces the
9
- # definitions snapshot.
5
+ # Durable snapshot load/save helpers for Client. Definitions (including
6
+ # `variants` / `allocation`) are the only persisted rail — there is no
7
+ # separate evaluated-variants snapshot (removed with the dual-rail).
10
8
  module SnapshotSupport
11
9
  private
12
10
 
13
- # @return [Boolean] true when any durable snapshot rail was applied
14
- # into memory (definitions and/or evaluated variants)
11
+ # @return [Boolean] true when the durable snapshot was applied into memory
15
12
  def load_snapshot
16
13
  return false unless @config.snapshot_provider
17
14
 
18
- definitions_loaded = load_definitions_snapshot
19
- variants_loaded = @config.enable_variants && load_variants_snapshot
20
-
21
- definitions_loaded || variants_loaded
15
+ load_definitions_snapshot
22
16
  rescue StandardError => e
23
17
  log_warn("Failed to load snapshot: #{e.message}")
24
18
  false
25
19
  end
26
20
 
27
- # Persist the definitions rail. Always available regardless of
28
- # `config.enable_variants` — definitions remain authoritative for
29
- # `enabled?`.
30
21
  def save_definitions_snapshot
31
22
  return unless @config.snapshot_provider
32
23
 
@@ -36,17 +27,6 @@ module Toggly
36
27
  log_warn("Failed to save snapshot: #{e.message}")
37
28
  end
38
29
 
39
- # Persist the evaluated-variants rail. Additive only; called only when
40
- # `config.enable_variants` is true.
41
- def save_variants_snapshot
42
- return unless @config.snapshot_provider
43
-
44
- @config.snapshot_provider.save_variants(@variant_defs)
45
- log_debug("Saved variants snapshot with #{@variant_defs.size} features")
46
- rescue StandardError => e
47
- log_warn("Failed to save variants snapshot: #{e.message}")
48
- end
49
-
50
30
  def load_definitions_snapshot
51
31
  data = @config.snapshot_provider.load
52
32
  return false unless data
@@ -59,20 +39,6 @@ module Toggly
59
39
  log_debug("Loaded #{@definitions.size} features from snapshot")
60
40
  true
61
41
  end
62
-
63
- def load_variants_snapshot
64
- return false unless @config.snapshot_provider.respond_to?(:load_variants)
65
-
66
- data = @config.snapshot_provider.load_variants
67
- return false unless data
68
-
69
- @mutex.synchronize do
70
- @variant_defs = data[:variants] || {}
71
- end
72
-
73
- log_debug("Loaded #{@variant_defs.size} evaluated variants from snapshot")
74
- true
75
- end
76
42
  end
77
43
  end
78
44
  end
data/lib/toggly/client.rb CHANGED
@@ -33,10 +33,6 @@ module Toggly
33
33
  # @return [Hash<String, FeatureDefinition>] Current definitions
34
34
  attr_reader :definitions
35
35
 
36
- # @return [Hash<String, EvaluatedVariantDef>] Current evaluated variants
37
- # (populated only when `config.enable_variants` is true)
38
- attr_reader :variant_defs
39
-
40
36
  # @return [Boolean] Whether the client is ready
41
37
  attr_reader :ready
42
38
 
@@ -48,10 +44,10 @@ module Toggly
48
44
  @config.validate!
49
45
 
50
46
  @definitions = {}
51
- @variant_defs = {}
52
47
  # True once a revision (including empty) or durable snapshot was applied.
53
48
  @definitions_loaded = false
54
49
  @mutex = Mutex.new
50
+ @identity = normalize_client_identity(@config.identity)
55
51
  @ready = false
56
52
  @closed = false
57
53
 
@@ -85,10 +81,9 @@ module Toggly
85
81
 
86
82
  definition = @mutex.synchronize { @definitions[key] }
87
83
 
88
- # Dual-rail: definitions/definitions-signed are the sole source of
89
- # truth for enabled? — this holds even when config.enable_variants is
90
- # true. Evaluated variants (@variant_defs) are an additive rail read
91
- # only by get_variant / get_variant_value and never override this.
84
+ # `enabled?` stays filter-based only (definitions/definitions-signed).
85
+ # It never reflects a variant's StatusOverride — see `get_variant`
86
+ # (`VariantResult#enabled`) for MF-identical effective-enabled semantics.
92
87
  result = if definition.nil?
93
88
  if !default.nil?
94
89
  default
@@ -117,52 +112,81 @@ module Toggly
117
112
  !enabled?(feature_key, context: context, default: default.nil? ? nil : !default)
118
113
  end
119
114
 
120
- # Get the assigned variant for a feature. Requires `config.enable_variants`;
121
- # returns nil when variants are disabled, unknown, or unassigned.
115
+ # Get the assigned variant for a feature, computed locally from the
116
+ # feature's catalog `variants` / `allocation` (MF-parity, bit-for-bit
117
+ # with `Microsoft.FeatureManagement`'s `IVariantFeatureManager`). Returns
118
+ # nil when the feature is unknown, has no variants configured, or no
119
+ # variant resolves for this context (see {VariantAllocator}).
120
+ #
121
+ # There is no network round-trip here — this superseded the
122
+ # `enable_variants` / `evaluated-variants-signed` dual-rail removed in
123
+ # 1.0 (see CHANGELOG).
124
+ #
125
+ # Targeting identity resolution (first non-empty wins):
126
+ # 1. per-call `context.identity`
127
+ # 2. {#identity} from Config / {#set_identity}
128
+ # 3. empty (MF anonymous)
129
+ #
130
+ # Groups come only from the per-call context (never Config). Prefer
131
+ # Rails `feature_variant` / ambient `toggly_context`, or
132
+ # `set_identity` / `Config#identity` for non-HTTP hosts — not a
133
+ # hand-built identity on every call.
122
134
  #
123
135
  # NOTE: this is the actual A/B assignment. It is unrelated to the
124
136
  # `variant:` telemetry label on `record_usage` / `record_view`, which is
125
- # a free-form usage tag (defaults to "enabled"/"disabled") and does not
126
- # reflect `evaluated-variants-signed` results.
137
+ # a free-form usage tag (defaults to "enabled"/"disabled").
127
138
  #
128
139
  # @param feature_key [String, Symbol] The feature key
140
+ # @param context [Context, nil] Optional targeting context (userId + groups)
129
141
  # @return [VariantResult, nil]
130
- def get_variant(feature_key)
131
- return nil unless @config.enable_variants
132
-
142
+ def get_variant(feature_key, context: nil)
133
143
  key = feature_key.to_s
134
- entry = @mutex.synchronize { @variant_defs[key] }
135
- return nil if entry.nil? || entry.variant.nil? || entry.variant.to_s.empty?
144
+ definition = @mutex.synchronize { @definitions[key] }
145
+ return nil if definition.nil?
146
+
147
+ enabled = @engine.evaluate(definition, context)
148
+ assignment = VariantAllocator.assign(
149
+ definition,
150
+ enabled: enabled,
151
+ identity: variant_user_id(context),
152
+ groups: context&.groups || []
153
+ )
154
+ return nil if assignment.variant_name.nil?
136
155
 
137
- VariantResult.new(name: entry.variant, configuration_value: entry.configuration_value)
156
+ VariantResult.new(
157
+ name: assignment.variant_name,
158
+ configuration_value: assignment.configuration_value,
159
+ enabled: assignment.enabled,
160
+ reason: assignment.reason
161
+ )
138
162
  end
139
163
 
140
164
  # Get the configuration value for the assigned variant, if any.
141
165
  #
142
166
  # @param feature_key [String, Symbol] The feature key
167
+ # @param context [Context, nil] Optional targeting context (userId + groups)
143
168
  # @return [Object, nil]
144
- def get_variant_value(feature_key)
145
- get_variant(feature_key)&.configuration_value
169
+ def get_variant_value(feature_key, context: nil)
170
+ get_variant(feature_key, context: context)&.configuration_value
146
171
  end
147
172
 
148
- # Update the `userId` sent to `evaluated-variants-signed` and, when
149
- # `config.enable_variants` is true, clear cached variants and refresh.
150
- # No-op (besides updating provider state) when variants are disabled.
151
- # Named as an action (not `variant_identity=`) because it also triggers
152
- # a network refresh — it is not a passive attribute writer.
173
+ # Default targeting userId used by {#get_variant} / {#get_variant_value}
174
+ # when the per-call context has no identity.
153
175
  #
154
- # @param identity [String, nil]
155
- # @return [Boolean] true if the identity changed
156
- # rubocop:disable-next Naming/AccessorMethodName
157
- def set_variant_identity(identity)
158
- changed = @provider.set_variant_identity(identity)
159
-
160
- if changed && @config.enable_variants
161
- @mutex.synchronize { @variant_defs = {} }
162
- refresh(force: true) unless @config.offline_mode?
163
- end
176
+ # @return [String, nil]
177
+ def identity
178
+ @mutex.synchronize { @identity }
179
+ end
164
180
 
165
- changed
181
+ # Set the default targeting userId (overrides Config#identity at runtime).
182
+ # Pass +nil+ or an empty string to clear.
183
+ #
184
+ # Named +set_identity+ (not +identity=+) to match Python/Java/Go SDK APIs.
185
+ #
186
+ # @param value [String, nil]
187
+ # @return [String, nil]
188
+ def set_identity(value) # rubocop:disable Naming/AccessorMethodName
189
+ @mutex.synchronize { @identity = normalize_client_identity(value) }
166
190
  end
167
191
 
168
192
  # Get detailed evaluation result
@@ -217,15 +241,7 @@ module Toggly
217
241
  end
218
242
 
219
243
  begin
220
- # Dual-rail: definitions/definitions-signed are always refreshed —
221
- # the sole source of truth for enabled?. When config.enable_variants
222
- # is true, evaluated-variants-signed is ALSO fetched on its own rail,
223
- # additive only for get_variant / get_variant_value. A failure on
224
- # either rail must never affect the other.
225
- definitions_updated = refresh_definitions_rail(force: force)
226
- variants_updated = @config.enable_variants ? refresh_variants_rail(force: force) : false
227
-
228
- definitions_updated || variants_updated
244
+ refresh_definitions_rail(force: force)
229
245
  ensure
230
246
  drain_pending = false
231
247
  @mutex.synchronize do
@@ -334,9 +350,9 @@ module Toggly
334
350
 
335
351
  private
336
352
 
337
- # Definitions rail: `definitions` / `definitions-signed` → local rule eval.
338
- # Always runs (dual-rail): the sole source of truth for `enabled?`,
339
- # regardless of `config.enable_variants`.
353
+ # `definitions` / `definitions-signed` → local rule eval. The sole
354
+ # source of truth for `enabled?`; also carries `variants` / `allocation`
355
+ # for the catalog-local `get_variant` assignment.
340
356
  def refresh_definitions_rail(force:)
341
357
  refresh_definitions(force: force)
342
358
  rescue StandardError => e
@@ -365,36 +381,6 @@ module Toggly
365
381
  end
366
382
  end
367
383
 
368
- # Variants rail: `evaluated-variants-signed` → server-evaluated assignment.
369
- # Additive only (used by get_variant / get_variant_value); runs only when
370
- # `config.enable_variants` is true and never influences `enabled?`. A
371
- # failure here must never affect the definitions rail above, and is not
372
- # counted in definition cache-hit/miss telemetry (that metric is scoped
373
- # to the definitions rail).
374
- def refresh_variants_rail(force:)
375
- refresh_variants(force: force)
376
- rescue StandardError => e
377
- log_error("Failed to refresh evaluated variants: #{e.message}")
378
- false
379
- end
380
-
381
- def refresh_variants(force:)
382
- result = @provider.fetch_variants(force: force)
383
-
384
- if result.variants
385
- @mutex.synchronize do
386
- @variant_defs = result.variants
387
- @ready = true
388
- end
389
-
390
- save_variants_snapshot
391
- log_info("Evaluated variants refreshed (#{result.variants.size} features)")
392
- true
393
- else
394
- false
395
- end
396
- end
397
-
398
384
  def initialize_definitions
399
385
  # Startup served from durable snapshot before first network — cache hit.
400
386
  # Distinct from the subsequent refresh() network outcome (no double-count
@@ -419,7 +405,7 @@ module Toggly
419
405
  log_error("Failed to initialize definitions: #{e.message}")
420
406
 
421
407
  # Use snapshot or defaults as fallback
422
- @ready = true if @definitions.any? || @variant_defs.any? || @config.defaults.any?
408
+ @ready = true if @definitions.any? || @config.defaults.any?
423
409
  end
424
410
 
425
411
  def start_background_refresh
@@ -496,6 +482,20 @@ module Toggly
496
482
  @telemetry.record_check(feature_key, enabled, identity)
497
483
  end
498
484
 
485
+ # Allocator userId: per-call context identity when present, else client identity.
486
+ def variant_user_id(context)
487
+ return context.identity if context&.identity?
488
+
489
+ identity
490
+ end
491
+
492
+ def normalize_client_identity(value)
493
+ return nil if value.nil?
494
+
495
+ text = value.to_s
496
+ text.empty? ? nil : text
497
+ end
498
+
499
499
  def log_info(message)
500
500
  @config.logger&.info("[Toggly] #{message}")
501
501
  end
data/lib/toggly/config.rb CHANGED
@@ -12,6 +12,17 @@ module Toggly
12
12
  # @return [String] Application key from Toggly dashboard
13
13
  attr_reader :app_key
14
14
 
15
+ # Default targeting userId for catalog-local +get_variant+ /
16
+ # +get_variant_value+ when neither a per-call +Context#identity+ nor
17
+ # Rails ambient context supplies one. Groups are never taken from Config.
18
+ #
19
+ # Set at configuration time only ({Config.new}, +Toggly.configure+ block).
20
+ # After a {Client} is built, mutating this value has no effect — use
21
+ # {Client#set_identity} for runtime updates.
22
+ #
23
+ # @return [String, nil]
24
+ attr_reader :identity
25
+
15
26
  # @return [String] Environment name (e.g., "Production", "Staging")
16
27
  attr_accessor :environment
17
28
 
@@ -55,25 +66,6 @@ module Toggly
55
66
  # @return [Array<String>] Allowed key IDs for signed definitions
56
67
  attr_accessor :allowed_key_ids
57
68
 
58
- # @return [Boolean] When true, additionally fetch server-evaluated variants
59
- # from `evaluated-variants-signed` on their own rail (dual-rail).
60
- # `definitions` / `definitions-signed` remain the sole source of truth
61
- # for `enabled?` regardless of this setting — evaluated variants are
62
- # additive and only feed `Client#get_variant` / `#get_variant_value`.
63
- attr_accessor :enable_variants
64
-
65
- # @return [String, nil] `userId` sent to `evaluated-variants-signed` for targeting.
66
- # Separate from the per-call `Context#identity` used for local rule evaluation.
67
- attr_accessor :variant_identity
68
-
69
- # @return [Array<String>] Application-wide group memberships sent to
70
- # `evaluated-variants-signed` (not request-local booleans).
71
- attr_accessor :variant_groups
72
-
73
- # @return [Hash<String, String>] Application-wide string claims sent to
74
- # `evaluated-variants-signed` (at most 20 on the wire).
75
- attr_accessor :variant_claims
76
-
77
69
  # @return [Logger, nil] Logger instance
78
70
  attr_accessor :logger
79
71
 
@@ -108,6 +100,7 @@ module Toggly
108
100
 
109
101
  def initialize(**options)
110
102
  @app_key = options[:app_key]
103
+ @identity = normalize_identity(options[:identity])
111
104
  @environment = options[:environment] || DEFAULT_ENVIRONMENT
112
105
  @base_url = normalize_url(options[:base_url] || DEFAULT_BASE_URL)
113
106
  @definitions_url = options[:definitions_url]
@@ -123,10 +116,6 @@ module Toggly
123
116
  @snapshot_provider = options[:snapshot_provider]
124
117
  @use_signed_definitions = options[:use_signed_definitions] || false
125
118
  @allowed_key_ids = options[:allowed_key_ids] || []
126
- @enable_variants = options[:enable_variants] || false
127
- @variant_identity = options[:variant_identity]
128
- @variant_groups = Array(options[:variant_groups])
129
- @variant_claims = options[:variant_claims] || {}
130
119
  @logger = options[:logger]
131
120
 
132
121
  @usage_tracking_explicit = options.key?(:enable_usage_tracking)
@@ -150,16 +139,6 @@ module Toggly
150
139
  "#{normalize_url(base)}#{endpoint}/#{@app_key}/#{@environment}"
151
140
  end
152
141
 
153
- # Get the evaluated-variants-signed endpoint URL. Always signed; query
154
- # params (userId/groups/claims) are added by the provider since they can
155
- # change at runtime (see `Client#set_variant_identity`).
156
- #
157
- # @return [String]
158
- def variants_endpoint
159
- base = @definitions_url || @base_url
160
- "#{normalize_url(base)}evaluated-variants-signed/#{@app_key}/#{@environment}"
161
- end
162
-
163
142
  # Validate the configuration
164
143
  #
165
144
  # @raise [ConfigError] if configuration is invalid
@@ -205,12 +184,23 @@ module Toggly
205
184
  (@app_key.nil? || @app_key.empty?) && !@defaults.empty?
206
185
  end
207
186
 
187
+ # Configuration-time identity only (e.g. inside +Toggly.configure+ before
188
+ # {Client} is constructed). Does not update an existing client's targeting
189
+ # identity — call {Client#set_identity} after initialization.
190
+ #
191
+ # @param value [String, nil]
192
+ # @return [String, nil]
193
+ def identity=(value)
194
+ @identity = normalize_identity(value)
195
+ end
196
+
208
197
  # Convert to hash
209
198
  #
210
199
  # @return [Hash]
211
200
  def to_h
212
201
  {
213
202
  app_key: @app_key,
203
+ identity: @identity,
214
204
  environment: @environment,
215
205
  base_url: @base_url,
216
206
  definitions_url: @definitions_url,
@@ -219,7 +209,6 @@ module Toggly
219
209
  enable_undefined_in_dev: @enable_undefined_in_dev,
220
210
  disable_background_refresh: @disable_background_refresh,
221
211
  enable_live_updates: @enable_live_updates,
222
- enable_variants: @enable_variants,
223
212
  app_version: @app_version,
224
213
  instance_name: @instance_name,
225
214
  use_signed_definitions: @use_signed_definitions,
@@ -242,5 +231,12 @@ module Toggly
242
231
 
243
232
  url.end_with?("/") ? url : "#{url}/"
244
233
  end
234
+
235
+ def normalize_identity(value)
236
+ return nil if value.nil?
237
+
238
+ text = value.to_s
239
+ text.empty? ? nil : text
240
+ end
245
241
  end
246
242
  end