launchdarkly-server-sdk 8.15.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: 047cbd8eb2194813d948fae8867456a2ae904370c22e2d9104d40310e9c6c918
4
- data.tar.gz: 5617fd2d3f49a081274b05bdc00dc64787b4311117ad85739c6a82a4b32d9a36
3
+ metadata.gz: 06b22ffc1ba131d82969abf4c5e6b04ab80d18a5112393a41f59a3b7217e9633
4
+ data.tar.gz: 52e8d3a1615a43d15d9fa548db9568456409c3cbfd716f558635c7377b35987f
5
5
  SHA512:
6
- metadata.gz: 3859a3bce1a0fce43c3c6d13b1e5e134ab2d1c1574ba14f645420e04af098fae64169f7f8f43a54fdb4651fbfc25b1188a4694050f5a7eca1c0db425c7eefe60
7
- data.tar.gz: 2e3aa25992a683b6ecdc7b0bb0822c5eb5a41d3e98db03741824dcff0d1820e249bcb4a3504963577a773c4211ae4acc505c253e29ea2b53f7fec17754d8db24
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>]
@@ -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|
@@ -78,9 +83,14 @@ module LaunchDarkly
78
83
  end
79
84
 
80
85
  context.get_custom_attribute_names.each do |attribute|
86
+ unless attribute.is_a?(Symbol)
87
+ log_non_symbol_name(attribute)
88
+ next
89
+ end
90
+
81
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
 
@@ -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
@@ -67,8 +67,11 @@ module LaunchDarkly
67
67
  error_info
68
68
  )
69
69
  else
70
- @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.
71
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
72
75
  end
73
76
  rescue StandardError => e
74
77
  Impl::Util.log_exception(@config.logger, "Exception while polling", e)
@@ -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.15.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.15.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-08-20 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