From ae9806e573a2817cd2fa0490b7bb9b504f699583 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 15:08:48 +0000 Subject: [PATCH 1/2] fix: Validate privateAttributes in evaluation context conversion Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> --- .../EvalContextConverter.cs | 35 ++++++++++++- .../EvalContextConverterTests.cs | 51 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider/EvalContextConverter.cs b/src/LaunchDarkly.OpenFeature.ServerProvider/EvalContextConverter.cs index 210dc7a..a96936a 100644 --- a/src/LaunchDarkly.OpenFeature.ServerProvider/EvalContextConverter.cs +++ b/src/LaunchDarkly.OpenFeature.ServerProvider/EvalContextConverter.cs @@ -85,6 +85,39 @@ private void Extract(string key, LdValue value, Func sette _log.Error(InvalidTypeMessage(key, "bool")); } + /// + /// Extract private attributes and log an error if they are not a list of strings. + /// + /// The value to extract + /// The context builder to add the private attributes to + private void ExtractPrivateAttributes(LdValue value, ContextBuilder builder) + { + if (value.IsNull) + { + // Ignore null values. + return; + } + + if (value.Type != LdValueType.Array) + { + _log.Error(InvalidTypeMessage("privateAttributes", "array")); + return; + } + + var items = value.AsList(LdValue.Convert.Json); + var privateAttributes = items.Where(item => item.IsString).Select(item => item.AsString).ToArray(); + + if (privateAttributes.Length != items.Count) + { + _log.Error("'privateAttributes' must be an array of only string values"); + } + + if (privateAttributes.Length != 0) + { + builder.Private(privateAttributes); + } + } + /// /// Extract a value and add it to a context builder. /// @@ -108,7 +141,7 @@ private void ProcessValue(string key, Value value, ContextBuilder builder) Extract(key, ldValue, builder.Anonymous); break; case "privateAttributes": - builder.Private(ldValue.AsList(LdValue.Convert.String).ToArray()); + ExtractPrivateAttributes(ldValue, builder); break; default: // Was not a built-in attribute. diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/EvalContextConverterTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/EvalContextConverterTests.cs index 01b8478..5f35985 100644 --- a/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/EvalContextConverterTests.cs +++ b/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/EvalContextConverterTests.cs @@ -179,6 +179,57 @@ public void ItUsesTheTargetingKeyInFavorOfKey() Assert.Equal("targeting-key", _converter.ToLdContext(evaluationContext).Key); } + [Fact] + public void ItLogsAnErrorWhenPrivateAttributesIsNotAnArray() + { + var evaluationContext = EvaluationContext.Builder() + .Set("targetingKey", "my-key") + .Set("myCustomAttribute", "myCustomValue") + .Set("privateAttributes", "myCustomAttribute") + .Build(); + + var expectedContext = Context.Builder("my-key") + .Set("myCustomAttribute", "myCustomValue") + .Build(); + + Assert.Equal(expectedContext, _converter.ToLdContext(evaluationContext)); + Assert.True(_logCapture.HasMessageWithText(LogLevel.Error, + "The attribute 'privateAttributes' must be of type array")); + } + + [Fact] + public void ItOmitsNonStringPrivateAttributes() + { + var evaluationContext = EvaluationContext.Builder() + .Set("targetingKey", "my-key") + .Set("privateAttributes", new Value(new List + { + new Value("myCustomAttribute"), + new Value(17) + })) + .Build(); + + var expectedContext = Context.Builder("my-key") + .Private("myCustomAttribute") + .Build(); + + Assert.Equal(expectedContext, _converter.ToLdContext(evaluationContext)); + Assert.True(_logCapture.HasMessageWithText(LogLevel.Error, + "'privateAttributes' must be an array of only string values")); + } + + [Fact] + public void ItAllowsEmptyPrivateAttributes() + { + var evaluationContext = EvaluationContext.Builder() + .Set("targetingKey", "my-key") + .Set("privateAttributes", new Value(new List())) + .Build(); + + Assert.Equal(Context.Builder("my-key").Build(), _converter.ToLdContext(evaluationContext)); + Assert.Empty(_logCapture.GetMessages()); + } + [Fact] public void ItCanBuildASingleContext() { From 4998d10662c0c2498e015641d1459cfe8cba0792 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:57:27 +0000 Subject: [PATCH 2/2] chore: Clarify privateAttributes error message Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> --- .../EvalContextConverter.cs | 3 ++- .../EvalContextConverterTests.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider/EvalContextConverter.cs b/src/LaunchDarkly.OpenFeature.ServerProvider/EvalContextConverter.cs index a96936a..fd57e12 100644 --- a/src/LaunchDarkly.OpenFeature.ServerProvider/EvalContextConverter.cs +++ b/src/LaunchDarkly.OpenFeature.ServerProvider/EvalContextConverter.cs @@ -109,7 +109,8 @@ private void ExtractPrivateAttributes(LdValue value, ContextBuilder builder) if (privateAttributes.Length != items.Count) { - _log.Error("'privateAttributes' must be an array of only string values"); + _log.Error("'privateAttributes' must be an array of only string values. The non-string" + + " values have been dropped and the remaining values have been applied."); } if (privateAttributes.Length != 0) diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/EvalContextConverterTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/EvalContextConverterTests.cs index 5f35985..ac71c31 100644 --- a/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/EvalContextConverterTests.cs +++ b/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/EvalContextConverterTests.cs @@ -215,7 +215,8 @@ public void ItOmitsNonStringPrivateAttributes() Assert.Equal(expectedContext, _converter.ToLdContext(evaluationContext)); Assert.True(_logCapture.HasMessageWithText(LogLevel.Error, - "'privateAttributes' must be an array of only string values")); + "'privateAttributes' must be an array of only string values. The non-string" + + " values have been dropped and the remaining values have been applied.")); } [Fact]