From a9079d07dddab509e612eee2c845b7a100607afe Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Thu, 27 Aug 2026 11:02:41 -0400 Subject: [PATCH] docs: State that context attribute names must be symbols The contexts documentation requires a symbol for every attribute name, for the built-in properties and for custom attributes alike. Nothing in the code said so, and the SDK never enforced it, so an application could pass string names and get a context that looks valid but that flag evaluation and private attribute redaction cannot address. Say it where a reader meets the requirement: - LDContext, which now carries the rule and the reason - LDContext.create, which names the two string keys that make a context invalid, :kind and :key - get_value and get_value_for_reference, which return nil for a string name because the lookup uses a symbol - get_custom_attribute_names, which returns names in the form the caller supplied - Reference, whose components are always symbols - Config#private_attributes, whose references cannot reach a string name The JSON.parse caveat is included, because that is how string names arrive in practice. A hand-written hash literal is safe: the "name": value form already produces a symbol. Comments only. No behavior change. --- lib/ldclient-rb/config.rb | 4 ++++ lib/ldclient-rb/context.rb | 29 +++++++++++++++++++++++++++++ lib/ldclient-rb/reference.rb | 4 ++++ 3 files changed, 37 insertions(+) diff --git a/lib/ldclient-rb/config.rb b/lib/ldclient-rb/config.rb index 20ab4008..29bd2d66 100644 --- a/lib/ldclient-rb/config.rb +++ b/lib/ldclient-rb/config.rb @@ -262,6 +262,10 @@ def offline? # You can also specify the same behavior for an individual flag evaluation # by providing the context object with a list of private attributes. # + # Each entry is an attribute reference. A reference addresses attributes by + # symbol name, so it cannot make an attribute private if that attribute was + # given a string name. Refer to {LDContext} for the symbol requirement. + # # @see https://docs.launchdarkly.com/sdk/features/user-context-config#using-private-attributes # # @return [Array] diff --git a/lib/ldclient-rb/context.rb b/lib/ldclient-rb/context.rb index 081d61a2..58e8c40a 100644 --- a/lib/ldclient-rb/context.rb +++ b/lib/ldclient-rb/context.rb @@ -6,6 +6,17 @@ module LaunchDarkly # LDContext is a collection of attributes that can be referenced in flag # evaluations and analytics events. # + # Every attribute name must be a symbol. This applies to the built-in + # properties, such as :key and :kind, and to custom attributes at every level + # of nesting. The SDK addresses attributes by symbol, so it cannot find an + # attribute that has a string name. Such an attribute is invisible to flag + # evaluation and to private attribute redaction. + # + # Take care with data that arrives as JSON. `JSON.parse` returns string keys + # by default, so pass `symbolize_names: true` before you build a context from + # it. Note that the `"name": value` form in a hash literal already produces a + # symbol, so a hand-written literal is safe. + # # To create an LDContext of a single kind, such as a user, you may use # {LDContext#create} or {LDContext#with_key}. # @@ -148,6 +159,9 @@ def kinds # # Return an array of top level attribute keys (excluding built-in attributes) # + # The keys come back in the form the caller supplied. A string key is + # returned as a string, even though the SDK cannot address it. + # # @return [Array] # def get_custom_attribute_names @@ -172,6 +186,9 @@ def get_custom_attribute_names # values out of JSON objects or arrays, such as "/address/street". Use # {#get_value_for_reference} for that purpose. # + # The lookup treats the name as a symbol. An attribute that was stored under + # a string name is therefore not found, and the result is nil. + # # If the value is found, the return value is the attribute value; # otherwise, it is nil. # @@ -200,6 +217,10 @@ def get_value(attribute) # Use {#individual_context} to inspect a Context for a particular kind and # then get its attributes. # + # A Reference holds its path components as symbols, so each step of the + # lookup matches a symbol key. A path that crosses an attribute with a + # string name resolves to nil. + # # If the value is found, the return value is the attribute value; # otherwise, it is nil. # @@ -440,6 +461,14 @@ def self.with_key(key, kind = KIND_DEFAULT) # {https://docs.launchdarkly.com/sdk/features/user-config SDK # documentation}. # + # Every key in the hash must be a symbol, at the top level and inside any + # nested attribute value. Refer to the {LDContext} class documentation for + # why, and for the JSON.parse caveat. + # + # A string key does not make the context invalid, with two exceptions. The + # context requires a symbol :kind and a symbol :key, so a hash that supplies + # those as strings is invalid. + # # @param data [Hash] # @return [LDContext] # diff --git a/lib/ldclient-rb/reference.rb b/lib/ldclient-rb/reference.rb index 8c248fe3..d50b1e6d 100644 --- a/lib/ldclient-rb/reference.rb +++ b/lib/ldclient-rb/reference.rb @@ -12,6 +12,10 @@ module LaunchDarkly # or to identify an attribute or nested value that should be considered # private. # + # A Reference holds its path components as symbols, whichever form the input + # string used. It therefore addresses only context attributes that have symbol + # names, which is the form a context requires. + # # Parsing and validation are done at the time that the Reference is # constructed. If a Reference instance was created from an invalid string, it # is considered invalid and its {Reference#error} attribute will return a