launchdarkly-server-sdk 8.14.0 → 8.15.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: 30ec609a22208597d110a93cf4ea65b4b20a723cfccf5b3db743921aeb27a40a
4
- data.tar.gz: 7833879176def90e31a71756cc27b90135985d2a75c305582ef3d09ad504ff72
3
+ metadata.gz: 06b22ffc1ba131d82969abf4c5e6b04ab80d18a5112393a41f59a3b7217e9633
4
+ data.tar.gz: 52e8d3a1615a43d15d9fa548db9568456409c3cbfd716f558635c7377b35987f
5
5
  SHA512:
6
- metadata.gz: c607be8b3926d090a243453c39375df4892c7ea49cb8a5f6c366bd47d4299c6f9ff525f2cb8e94c535b6a1dfa54a9f348ba4a59477e8eac8a420c1ce85fae777
7
- data.tar.gz: 80d5b45d30922ce0c24f3ed56b7ca9f0e0d57e98e972600161b24517e89318da43fc94ba11219558467bf4e9088114b7b3b863b420ab0f9b1a88cc4f2805b018
6
+ metadata.gz: b632a4958bab5ad77398d87cf3b018e0fe35c37e53b5590dc330b6c395a6fe22f0fc50ef820182b197179ac9c933356bf1a718b8f5bdcd4d8b40d3daae8367b5
7
+ data.tar.gz: 22284f8464984ed1de431109260354d8285ae2079a47429a62e2e8fb49abb954e4d75c220cf264a9409b20bcd0ddc2cb6ca167c8ddd481bf8d769e08b5642362
@@ -262,6 +262,10 @@ module LaunchDarkly
262
262
  # You can also specify the same behavior for an individual flag evaluation
263
263
  # by providing the context object with a list of private attributes.
264
264
  #
265
+ # Each entry is an attribute reference. A reference addresses attributes by
266
+ # symbol name, so it cannot make an attribute private if that attribute was
267
+ # given a string name. Refer to {LDContext} for the symbol requirement.
268
+ #
265
269
  # @see https://docs.launchdarkly.com/sdk/features/user-context-config#using-private-attributes
266
270
  #
267
271
  # @return [Array<String>]
@@ -399,6 +403,30 @@ module LaunchDarkly
399
403
  #
400
404
  attr_reader :wrapper_version
401
405
 
406
+ #
407
+ # Returns a copy of this configuration with different wrapper information.
408
+ #
409
+ # This is intended for use by wrapper libraries, such as the LaunchDarkly OpenFeature provider, which need to
410
+ # identify themselves rather than the application which created the configuration.
411
+ #
412
+ # The copy is shallow: it shares the objects the original configuration references, such as the feature store,
413
+ # the logger, and the socket factory. Mutating one of those objects affects both configurations.
414
+ #
415
+ # @param wrapper_name [String, nil] see {#wrapper_name}
416
+ # @param wrapper_version [String, nil] see {#wrapper_version}
417
+ #
418
+ # @return [LaunchDarkly::Config]
419
+ #
420
+ def with_wrapper_information(wrapper_name, wrapper_version = nil)
421
+ updated = dup
422
+ updated.wrapper_name = wrapper_name
423
+ updated.wrapper_version = wrapper_version
424
+
425
+ updated
426
+ end
427
+
428
+ protected attr_writer :wrapper_name, :wrapper_version
429
+
402
430
  #
403
431
  # The factory used to construct sockets for HTTP operations. The factory must
404
432
  # provide the method `open(uri, timeout)`. The `open` method must return a
@@ -6,6 +6,17 @@ module LaunchDarkly
6
6
  # LDContext is a collection of attributes that can be referenced in flag
7
7
  # evaluations and analytics events.
8
8
  #
9
+ # Every attribute name must be a symbol. This applies to the built-in
10
+ # properties, such as :key and :kind, and to custom attributes at every level
11
+ # of nesting. The SDK addresses attributes by symbol, so it cannot find an
12
+ # attribute that has a string name. Such an attribute is invisible to flag
13
+ # evaluation and to private attribute redaction.
14
+ #
15
+ # Take care with data that arrives as JSON. `JSON.parse` returns string keys
16
+ # by default, so pass `symbolize_names: true` before you build a context from
17
+ # it. Note that the `"name": value` form in a hash literal already produces a
18
+ # symbol, so a hand-written literal is safe.
19
+ #
9
20
  # To create an LDContext of a single kind, such as a user, you may use
10
21
  # {LDContext#create} or {LDContext#with_key}.
11
22
  #
@@ -148,6 +159,9 @@ module LaunchDarkly
148
159
  #
149
160
  # Return an array of top level attribute keys (excluding built-in attributes)
150
161
  #
162
+ # The keys come back in the form the caller supplied. A string key is
163
+ # returned as a string, even though the SDK cannot address it.
164
+ #
151
165
  # @return [Array<Symbol>]
152
166
  #
153
167
  def get_custom_attribute_names
@@ -172,6 +186,9 @@ module LaunchDarkly
172
186
  # values out of JSON objects or arrays, such as "/address/street". Use
173
187
  # {#get_value_for_reference} for that purpose.
174
188
  #
189
+ # The lookup treats the name as a symbol. An attribute that was stored under
190
+ # a string name is therefore not found, and the result is nil.
191
+ #
175
192
  # If the value is found, the return value is the attribute value;
176
193
  # otherwise, it is nil.
177
194
  #
@@ -200,6 +217,10 @@ module LaunchDarkly
200
217
  # Use {#individual_context} to inspect a Context for a particular kind and
201
218
  # then get its attributes.
202
219
  #
220
+ # A Reference holds its path components as symbols, so each step of the
221
+ # lookup matches a symbol key. A path that crosses an attribute with a
222
+ # string name resolves to nil.
223
+ #
203
224
  # If the value is found, the return value is the attribute value;
204
225
  # otherwise, it is nil.
205
226
  #
@@ -440,6 +461,14 @@ module LaunchDarkly
440
461
  # {https://docs.launchdarkly.com/sdk/features/user-config SDK
441
462
  # documentation}.
442
463
  #
464
+ # Every key in the hash must be a symbol, at the top level and inside any
465
+ # nested attribute value. Refer to the {LDContext} class documentation for
466
+ # why, and for the JSON.parse caveat.
467
+ #
468
+ # A string key does not make the context invalid, with two exceptions. The
469
+ # context requires a symbol :kind and a symbol :key, so a hash that supplies
470
+ # those as strings is invalid.
471
+ #
443
472
  # @param data [Hash]
444
473
  # @return [LDContext]
445
474
  #
@@ -487,7 +487,7 @@ module LaunchDarkly
487
487
  SUMMARY_KIND = 'summary'
488
488
 
489
489
  def initialize(config)
490
- @context_filter = LaunchDarkly::Impl::ContextFilter.new(config.all_attributes_private, config.private_attributes)
490
+ @context_filter = LaunchDarkly::Impl::ContextFilter.new(config.all_attributes_private, config.private_attributes, config.logger)
491
491
  end
492
492
 
493
493
  # Transforms events into the format used for event sending.
@@ -1,12 +1,17 @@
1
+ require "concurrent/atomics"
2
+
1
3
  module LaunchDarkly
2
4
  module Impl
3
5
  class ContextFilter
4
6
  #
5
7
  # @param all_attributes_private [Boolean]
6
8
  # @param private_attributes [Array<String>]
9
+ # @param logger [Logger, nil]
7
10
  #
8
- def initialize(all_attributes_private, private_attributes)
11
+ def initialize(all_attributes_private, private_attributes, logger = nil)
9
12
  @all_attributes_private = all_attributes_private
13
+ @logger = logger
14
+ @non_symbol_name_logged = Concurrent::AtomicBoolean.new(false)
10
15
 
11
16
  @private_attributes = []
12
17
  private_attributes.each do |attribute|
@@ -70,17 +75,22 @@ module LaunchDarkly
70
75
  filtered[:anonymous] = true if anonymous
71
76
 
72
77
  redacted = []
73
- private_attributes = @private_attributes.concat(context.private_attributes)
78
+ private_attributes = @private_attributes + context.private_attributes
74
79
 
75
80
  name = context.get_value(:name)
76
- if !name.nil? && !check_whole_attribute_private(:name, private_attributes, redacted, anonymous && redact_anonymous)
81
+ if !name.nil? && !check_whole_attribute_private(Reference.create_literal(:name), private_attributes, redacted, anonymous && redact_anonymous)
77
82
  filtered[:name] = name
78
83
  end
79
84
 
80
85
  context.get_custom_attribute_names.each do |attribute|
81
- unless check_whole_attribute_private(attribute, private_attributes, redacted, anonymous && redact_anonymous)
86
+ unless attribute.is_a?(Symbol)
87
+ log_non_symbol_name(attribute)
88
+ next
89
+ end
90
+
91
+ unless check_whole_attribute_private(Reference.create_literal(attribute), private_attributes, redacted, anonymous && redact_anonymous)
82
92
  value = context.get_value(attribute)
83
- filtered[attribute] = redact_json_value(nil, attribute, value, private_attributes, redacted)
93
+ filtered[attribute] = redact_json_value(nil, attribute, value, private_attributes, redacted, [])
84
94
  end
85
95
  end
86
96
 
@@ -92,7 +102,7 @@ module LaunchDarkly
92
102
  #
93
103
  # Check if an entire attribute should be redacted.
94
104
  #
95
- # @param attribute [Symbol]
105
+ # @param attribute [Reference]
96
106
  # @param private_attributes [Array<Reference>]
97
107
  # @param redacted [Array<Symbol>]
98
108
  # @param redact_all [Boolean]
@@ -100,13 +110,13 @@ module LaunchDarkly
100
110
  #
101
111
  private def check_whole_attribute_private(attribute, private_attributes, redacted, redact_all)
102
112
  if @all_attributes_private || redact_all
103
- redacted << attribute
113
+ redacted << attribute.raw_path.to_sym
104
114
  return true
105
115
  end
106
116
 
107
117
  private_attributes.each do |private_attribute|
108
- if private_attribute.component(0) == attribute && private_attribute.depth == 1
109
- redacted << attribute
118
+ if private_attribute.component(0) == attribute.component(0) && private_attribute.depth == 1
119
+ redacted << attribute.raw_path.to_sym
110
120
  return true
111
121
  end
112
122
  end
@@ -122,16 +132,34 @@ module LaunchDarkly
122
132
  # @param value [any]
123
133
  # @param private_attributes [Array<Reference>]
124
134
  # @param redacted [Array<Symbol>]
135
+ # @param visited [Array<Hash>]
125
136
  # @return [any]
126
137
  #
127
- private def redact_json_value(parent_path, name, value, private_attributes, redacted)
138
+ private def redact_json_value(parent_path, name, value, private_attributes, redacted, visited)
128
139
  return value unless value.is_a?(Hash)
129
140
 
130
141
  ret = {}
131
142
  current_path = parent_path.clone || []
132
143
  current_path << name
133
144
 
145
+ # The chain of hashes from the attribute root down to this value. A
146
+ # nested value that points back into this chain is a cycle and is
147
+ # omitted.
148
+ #
149
+ # The comparison is by object identity, which keeps the check cheap.
150
+ # Context creation copies each top-level attribute value, so a cycle
151
+ # through that value appears once in the output before it is cut.
152
+ # Private attribute references match the paths that appear in the
153
+ # output. Cyclic values are not valid context data; this check only
154
+ # prevents unbounded recursion.
155
+ branch = visited + [value]
156
+
134
157
  value.each do |k, v|
158
+ unless k.is_a?(Symbol)
159
+ log_non_symbol_name(k)
160
+ next
161
+ end
162
+
135
163
  was_redacted = false
136
164
  private_attributes.each do |private_attribute|
137
165
  next unless private_attribute.depth == (current_path.count + 1)
@@ -155,12 +183,34 @@ module LaunchDarkly
155
183
  end
156
184
 
157
185
  unless was_redacted
158
- ret[k] = redact_json_value(current_path, k, v, private_attributes, redacted)
186
+ next if v.is_a?(Hash) && branch.any? { |seen| seen.equal?(v) }
187
+
188
+ ret[k] = redact_json_value(current_path, k, v, private_attributes, redacted, branch)
159
189
  end
160
190
  end
161
191
 
162
192
  ret
163
193
  end
194
+
195
+ #
196
+ # Log the first attribute found with a non-symbol name. Later
197
+ # occurrences are not logged to prevent log spam.
198
+ #
199
+ # Attribute references cannot address non-symbol names, so these
200
+ # attributes cannot be evaluated or redacted. They are omitted from
201
+ # analytics events.
202
+ #
203
+ # @param name [any]
204
+ #
205
+ private def log_non_symbol_name(name)
206
+ return if @logger.nil?
207
+ return unless @non_symbol_name_logged.make_true
208
+
209
+ @logger.error do
210
+ "[LDClient] Context attributes with non-symbol names cannot be evaluated or redacted, " \
211
+ "so they are omitted from analytics events (first occurrence: #{name.inspect}). This message is logged once."
212
+ end
213
+ end
164
214
  end
165
215
  end
166
216
  end
@@ -1,3 +1,4 @@
1
+ require "ldclient-rb/impl/data_source"
1
2
  require "ldclient-rb/impl/repeating_task"
2
3
  require "ldclient-rb/impl/util"
3
4
 
@@ -35,7 +36,8 @@ module LaunchDarkly
35
36
 
36
37
  def poll
37
38
  begin
38
- all_data = @requestor.request_all_data
39
+ all_data, headers = request_all_data
40
+ DataSource.record_environment_id(@config.data_source_update_sink, headers)
39
41
  if all_data
40
42
  update_sink_or_data_store.init(all_data)
41
43
  if @initialized.make_true
@@ -65,8 +67,11 @@ module LaunchDarkly
65
67
  error_info
66
68
  )
67
69
  else
68
- @ready.set # if client was waiting on us, make it stop waiting - has no effect if already set
70
+ # Publish the OFF status before releasing anyone waiting on the
71
+ # ready event, so a client that returns from start can rely on the
72
+ # data source status already reflecting the failure.
69
73
  stop_with_error_info error_info
74
+ @ready.set # if client was waiting on us, make it stop waiting - has no effect if already set
70
75
  end
71
76
  rescue StandardError => e
72
77
  Impl::Util.log_exception(@config.logger, "Exception while polling", e)
@@ -92,6 +97,17 @@ module LaunchDarkly
92
97
  @config.data_source_update_sink || @config.feature_store
93
98
  end
94
99
 
100
+ #
101
+ # Requestors provided by application code may not be able to report the response headers.
102
+ #
103
+ # @return [Array(Hash, HTTP::Headers, nil)]
104
+ #
105
+ private def request_all_data
106
+ return @requestor.request_all_data_with_headers if @requestor.respond_to?(:request_all_data_with_headers)
107
+
108
+ [@requestor.request_all_data, nil]
109
+ end
110
+
95
111
  #
96
112
  # @param [LaunchDarkly::Interfaces::DataSource::ErrorInfo, nil] error_info
97
113
  #
@@ -40,8 +40,18 @@ module LaunchDarkly
40
40
  end
41
41
 
42
42
  def request_all_data()
43
- all_data = JSON.parse(make_request("/sdk/latest-all"), symbolize_names: true)
44
- Impl::Model.make_all_store_data(all_data, @config.logger)
43
+ request_all_data_with_headers.first
44
+ end
45
+
46
+ #
47
+ # Requests the full data set, also returning the headers of the response it was retrieved from.
48
+ #
49
+ # @return [Array(Hash, HTTP::Headers)]
50
+ #
51
+ def request_all_data_with_headers
52
+ body, headers = make_request("/sdk/latest-all")
53
+ all_data = JSON.parse(body, symbolize_names: true)
54
+ [Impl::Model.make_all_store_data(all_data, @config.logger), headers]
45
55
  end
46
56
 
47
57
  def stop
@@ -80,7 +90,7 @@ module LaunchDarkly
80
90
  etag = response.headers["etag"]
81
91
  @cache.write(uri, CacheEntry.new(etag, body)) unless etag.nil?
82
92
  end
83
- body
93
+ [body, response.headers]
84
94
  end
85
95
 
86
96
  def fix_encoding(body, content_type)
@@ -1,3 +1,4 @@
1
+ require "ldclient-rb/impl/data_source"
1
2
  require "ldclient-rb/impl/model/serialization"
2
3
  require "ldclient-rb/impl/util"
3
4
  require "ldclient-rb/in_memory_store"
@@ -54,6 +55,7 @@ module LaunchDarkly
54
55
 
55
56
  uri = Impl::Util.add_payload_filter_key(@config.stream_uri + "/all", @config)
56
57
  @es = SSE::Client.new(uri, **opts) do |conn|
58
+ conn.on_connect { |response_headers| DataSource.record_environment_id(@data_source_update_sink, response_headers) }
57
59
  conn.on_event { |event| process_message(event) }
58
60
  conn.on_error { |err|
59
61
  log_connection_result(false)
@@ -7,6 +7,27 @@ require "set"
7
7
  module LaunchDarkly
8
8
  module Impl
9
9
  module DataSource
10
+ LD_ENVID_HEADER = "X-LD-EnvID"
11
+
12
+ #
13
+ # Records the environment ID reported by LaunchDarkly, when the response headers provide one and the sink is
14
+ # able to hold it. Externally implemented sinks are not required to support this.
15
+ #
16
+ # @param sink [LaunchDarkly::Interfaces::DataSource::UpdateSink, nil]
17
+ # @param headers [#[], nil]
18
+ # @return [void]
19
+ #
20
+ def self.record_environment_id(sink, headers)
21
+ return if headers.nil? || !sink.respond_to?(:set_environment_id)
22
+
23
+ environment_id = headers[LD_ENVID_HEADER]
24
+ # The http gem returns arrays for repeated headers; normalize to a string.
25
+ environment_id = environment_id.first if environment_id.is_a?(Array)
26
+ return unless environment_id.is_a?(String) && !environment_id.empty?
27
+
28
+ sink.set_environment_id(environment_id)
29
+ end
30
+
10
31
  class StatusProvider
11
32
  include LaunchDarkly::Interfaces::DataSource::StatusProvider
12
33
 
@@ -41,12 +62,28 @@ module LaunchDarkly
41
62
  @dependency_tracker = LaunchDarkly::Impl::DependencyTracker.new
42
63
 
43
64
  @mutex = Mutex.new
65
+ @environment_id = nil
44
66
  @current_status = LaunchDarkly::Interfaces::DataSource::Status.new(
45
67
  LaunchDarkly::Interfaces::DataSource::Status::INITIALIZING,
46
68
  Time.now,
47
69
  nil)
48
70
  end
49
71
 
72
+ #
73
+ # @return [String, nil] The environment ID reported by LaunchDarkly, if known
74
+ #
75
+ def environment_id
76
+ @mutex.synchronize { @environment_id }
77
+ end
78
+
79
+ #
80
+ # @param environment_id [String]
81
+ # @return [void]
82
+ #
83
+ def set_environment_id(environment_id)
84
+ @mutex.synchronize { @environment_id = environment_id }
85
+ end
86
+
50
87
  def init(all_data)
51
88
  old_data = nil
52
89
  monitor_store_update do
@@ -121,6 +121,11 @@ module LaunchDarkly
121
121
  @flag_change_broadcaster
122
122
  end
123
123
 
124
+ # (see DataSystem#environment_id)
125
+ def environment_id
126
+ @data_source_update_sink.environment_id
127
+ end
128
+
124
129
  #
125
130
  # (see DataSystem#data_availability)
126
131
  #
@@ -100,6 +100,9 @@ module LaunchDarkly
100
100
  @store.with_persistence(wrapper, writable, @data_store_status_provider)
101
101
  end
102
102
 
103
+ @environment_id = nil
104
+ @environment_id_lock = Mutex.new
105
+
103
106
  # Threading
104
107
  @stop_event = Concurrent::Event.new
105
108
  @ready_event = Concurrent::Event.new
@@ -185,6 +188,11 @@ module LaunchDarkly
185
188
  @flag_change_broadcaster
186
189
  end
187
190
 
191
+ # (see DataSystem#environment_id)
192
+ def environment_id
193
+ @environment_id_lock.synchronize { @environment_id }
194
+ end
195
+
188
196
  # (see DataSystem#data_availability)
189
197
  def data_availability
190
198
  return DataAvailability::REFRESHED if @store.selector.defined?
@@ -270,6 +278,7 @@ module LaunchDarkly
270
278
  if basis_result.success?
271
279
  basis = basis_result.value
272
280
  @logger.info { "[LDClient] Initialized via #{initializer.name}" }
281
+ record_environment_id(basis.environment_id)
273
282
 
274
283
  # Apply the basis to the store regardless of whether fallback was signalled.
275
284
  # If the server returned a valid payload alongside the directive we still want
@@ -460,7 +469,10 @@ module LaunchDarkly
460
469
  @store.apply(update.change_set, true) if update.change_set
461
470
 
462
471
  # Set ready event on valid update
463
- @ready_event.set if update.state == LaunchDarkly::Interfaces::DataSource::Status::VALID
472
+ if update.state == LaunchDarkly::Interfaces::DataSource::Status::VALID
473
+ @ready_event.set
474
+ record_environment_id(update.environment_id)
475
+ end
464
476
 
465
477
  # Update status
466
478
  @data_source_status_provider.update_status(update.state, update.error)
@@ -481,6 +493,18 @@ module LaunchDarkly
481
493
  SyncResult::REMOVE
482
494
  end
483
495
 
496
+ #
497
+ # Retains the environment ID reported by a successful connection to LaunchDarkly.
498
+ #
499
+ # @param environment_id [String, nil]
500
+ # @return [void]
501
+ #
502
+ def record_environment_id(environment_id)
503
+ return unless environment_id.is_a?(String) && !environment_id.empty?
504
+
505
+ @environment_id_lock.synchronize { @environment_id = environment_id }
506
+ end
507
+
484
508
  #
485
509
  # Determine if we should fallback to the next synchronizer.
486
510
  #
@@ -313,7 +313,7 @@ module LaunchDarkly
313
313
  query_params << ["filter", @config.payload_filter_key] unless @config.payload_filter_key.nil?
314
314
 
315
315
  if selector && selector.defined?
316
- query_params << ["selector", selector.state]
316
+ query_params << ["basis", selector.state]
317
317
  end
318
318
 
319
319
  uri = @poll_uri
@@ -119,6 +119,15 @@ module LaunchDarkly
119
119
  raise NotImplementedError, "#{self.class} must implement #set_diagnostic_accumulator"
120
120
  end
121
121
 
122
+ #
123
+ # Returns the ID of the environment the SDK is connected to, if LaunchDarkly has reported one.
124
+ #
125
+ # @return [String, nil]
126
+ #
127
+ def environment_id
128
+ raise NotImplementedError, "#{self.class} must implement #environment_id"
129
+ end
130
+
122
131
  #
123
132
  # Represents the availability of data in the SDK.
124
133
  #
@@ -50,7 +50,7 @@ module LaunchDarkly
50
50
  # "segments": {
51
51
  # "segment-key-1": {
52
52
  # "key": "segment-key-1",
53
- # "includes": [ "user-key-1" ]
53
+ # "included": [ "user-key-1" ]
54
54
  # }
55
55
  # }
56
56
  # }
@@ -70,17 +70,27 @@ module LaunchDarkly
70
70
  attr_reader :default_value
71
71
  attr_reader :method
72
72
 
73
+ #
74
+ # The ID of the LaunchDarkly environment the SDK is connected to, if known. This is only available once the
75
+ # SDK has received data from LaunchDarkly.
76
+ #
77
+ # @return [String, nil]
78
+ #
79
+ attr_reader :environment_id
80
+
73
81
  #
74
82
  # @param key [String]
75
83
  # @param context [LaunchDarkly::LDContext]
76
84
  # @param default_value [any]
77
85
  # @param method [Symbol]
86
+ # @param environment_id [String, nil]
78
87
  #
79
- def initialize(key, context, default_value, method)
88
+ def initialize(key, context, default_value, method, environment_id = nil)
80
89
  @key = key
81
90
  @context = context
82
91
  @default_value = default_value
83
92
  @method = method
93
+ @environment_id = environment_id
84
94
  end
85
95
  end
86
96
  end
@@ -421,7 +421,7 @@ module LaunchDarkly
421
421
  # Hooks can be added and we want to ensure all correct stages for a given hook execute. For example, we do not
422
422
  # want to trigger the after_evaluation method without also triggering the before_evaluation method.
423
423
  hooks = @hooks.dup
424
- evaluation_series_context = Interfaces::Hooks::EvaluationSeriesContext.new(key, context, default, method)
424
+ evaluation_series_context = Interfaces::Hooks::EvaluationSeriesContext.new(key, context, default, method, @data_system.environment_id)
425
425
 
426
426
  [hooks, evaluation_series_context]
427
427
  end
@@ -12,6 +12,10 @@ module LaunchDarkly
12
12
  # or to identify an attribute or nested value that should be considered
13
13
  # private.
14
14
  #
15
+ # A Reference holds its path components as symbols, whichever form the input
16
+ # string used. It therefore addresses only context attributes that have symbol
17
+ # names, which is the form a context requires.
18
+ #
15
19
  # Parsing and validation are done at the time that the Reference is
16
20
  # constructed. If a Reference instance was created from an invalid string, it
17
21
  # is considered invalid and its {Reference#error} attribute will return a
@@ -1,3 +1,3 @@
1
1
  module LaunchDarkly
2
- VERSION = "8.14.0" # x-release-please-version
2
+ VERSION = "8.15.1" # x-release-please-version
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: launchdarkly-server-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.14.0
4
+ version: 8.15.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - LaunchDarkly
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-01 00:00:00.000000000 Z
11
+ date: 2026-08-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-dynamodb