From 5d6a90a94de650c860738334f93c10c133dff450 Mon Sep 17 00:00:00 2001 From: Dustin Byrne Date: Wed, 13 May 2026 08:33:40 -0400 Subject: [PATCH 1/3] feat: add feature flag evaluation contexts --- .changeset/few-flags-dance.md | 5 +++ README.md | 16 ++++++++ src/PostHog.AspNetCore/README.md | 16 ++++++++ src/PostHog/Api/PostHogApiClient.cs | 5 +++ src/PostHog/Config/PostHogOptions.cs | 9 +++++ tests/UnitTests/Config/RegistrationTests.cs | 3 ++ .../Features/FeatureFlagEvaluationsTests.cs | 40 +++++++++++++++++++ 7 files changed, 94 insertions(+) create mode 100644 .changeset/few-flags-dance.md diff --git a/.changeset/few-flags-dance.md b/.changeset/few-flags-dance.md new file mode 100644 index 00000000..f39252d5 --- /dev/null +++ b/.changeset/few-flags-dance.md @@ -0,0 +1,5 @@ +--- +"PostHog": minor +--- + +Add feature flag evaluation contexts via `PostHogOptions.EvaluationContexts`. `/flags` requests now send `evaluation_contexts` when configured. diff --git a/README.md b/README.md index 837f005e..bbb852b0 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,22 @@ if (await posthog.IsFeatureEnabledAsync( > [!NOTE] > Specifying `PersonProperties` and `GroupProperties` is necessary when using local evaluation of feature flags. +#### Evaluation contexts + +You can configure SDK-level feature flag evaluation contexts to only evaluate flags intended for this application, +platform, or product area. + +```csharp +services.AddPostHog(options => +{ + options.ProjectToken = "YOUR_PROJECT_TOKEN"; + options.EvaluationContexts = ["main-app", "api", "backend"]; +}); +``` + +Flags without evaluation contexts continue to evaluate for all SDKs. Flags with contexts evaluate only when at least +one configured context matches. + ### Get a single Feature Flag Some feature flags may have associated payloads. diff --git a/src/PostHog.AspNetCore/README.md b/src/PostHog.AspNetCore/README.md index acd09bad..dedaff1a 100644 --- a/src/PostHog.AspNetCore/README.md +++ b/src/PostHog.AspNetCore/README.md @@ -253,6 +253,22 @@ options: new AllFeatureFlagsOptions }); ``` +### Evaluation contexts + +Configure SDK-level feature flag evaluation contexts to only evaluate flags intended for this application, +platform, or product area. + +```csharp +builder.Services.AddPostHog(options => +{ + options.ProjectToken = "YOUR_PROJECT_TOKEN"; + options.EvaluationContexts = ["main-app", "api", "backend"]; +}); +``` + +Flags without evaluation contexts continue to evaluate for all SDKs. Flags with contexts evaluate only when at least +one configured context matches. + ## Feature Management `PostHog.AspNetCore` supports .NET Feature Management. This allows you to use the <feature /> tag helper and diff --git a/src/PostHog/Api/PostHogApiClient.cs b/src/PostHog/Api/PostHogApiClient.cs index e7b0c1f0..1f1a5e22 100644 --- a/src/PostHog/Api/PostHogApiClient.cs +++ b/src/PostHog/Api/PostHogApiClient.cs @@ -133,6 +133,11 @@ public async Task SendEventAsync( payload["flag_keys_to_evaluate"] = flagKeysToEvaluate; } + if (_options.Value.EvaluationContexts is { Count: > 0 } evaluationContexts) + { + payload["evaluation_contexts"] = evaluationContexts; + } + groupProperties?.AddToPayload(payload); PrepareAndMutatePayload(payload); diff --git a/src/PostHog/Config/PostHogOptions.cs b/src/PostHog/Config/PostHogOptions.cs index feca6a7e..192504b0 100644 --- a/src/PostHog/Config/PostHogOptions.cs +++ b/src/PostHog/Config/PostHogOptions.cs @@ -58,6 +58,15 @@ public string? ProjectApiKey /// public string? PersonalApiKey { get; set; } + /// + /// Evaluation contexts for feature flags. + /// + /// + /// When set, only feature flags with no evaluation contexts or at least one matching context will be evaluated + /// for this SDK instance. Use this to isolate flags by application, platform, or product area. + /// + public IReadOnlyList? EvaluationContexts { get; set; } + /// /// Whether this client is disabled and should no-op instead of sending data to PostHog. (Default: false) /// diff --git a/tests/UnitTests/Config/RegistrationTests.cs b/tests/UnitTests/Config/RegistrationTests.cs index 8fa96ce7..6dbc41c2 100644 --- a/tests/UnitTests/Config/RegistrationTests.cs +++ b/tests/UnitTests/Config/RegistrationTests.cs @@ -48,6 +48,8 @@ public void CanReadConfiguration() ["PostHogTest:FeatureFlagPollInterval"] = "00:00:10", ["PostHogTest:FlushAt"] = "10", ["PostHogTest:MaxBatchSize"] = "99", + ["PostHogTest:EvaluationContexts:0"] = "main-app", + ["PostHogTest:EvaluationContexts:1"] = "api", ["PostHogTest:SuperProperties:Castle"] = "Winterfell", ["PostHogTest:SuperProperties:Family"] = "Starks" }) @@ -73,6 +75,7 @@ public void CanReadConfiguration() Assert.Equal(TimeSpan.FromSeconds(10), options.FeatureFlagPollInterval); Assert.Equal(10, options.FlushAt); Assert.Equal(99, options.MaxBatchSize); + Assert.Equal(new[] { "main-app", "api" }, options.EvaluationContexts); Assert.Equal(new() { ["Castle"] = "Winterfell", diff --git a/tests/UnitTests/Features/FeatureFlagEvaluationsTests.cs b/tests/UnitTests/Features/FeatureFlagEvaluationsTests.cs index b82ce0a8..271a9d0b 100644 --- a/tests/UnitTests/Features/FeatureFlagEvaluationsTests.cs +++ b/tests/UnitTests/Features/FeatureFlagEvaluationsTests.cs @@ -79,6 +79,46 @@ await client.EvaluateFlagsAsync( Assert.Equal(new[] { "flag-a", "flag-b" }, flagKeys); } + [Fact] + public async Task SendsEvaluationContextsToFlagsRequestBody() + { + var container = new TestContainer(services => services.Configure(options => + { + options.EvaluationContexts = ["production", "backend"]; + })); + var flagsHandler = container.FakeHttpMessageHandler.AddFlagsResponse("""{"featureFlags": {"flag-a": true}}"""); + var client = container.Activate(); + + await client.EvaluateFlagsAsync("user-1", options: null, CancellationToken.None); + + var request = flagsHandler.ReceivedRequests.Single(); + var body = await request.Content!.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(body); + var evaluationContexts = doc.RootElement.GetProperty("evaluation_contexts") + .EnumerateArray() + .Select(e => e.GetString() ?? string.Empty) + .ToArray(); + Assert.Equal(new[] { "production", "backend" }, evaluationContexts); + } + + [Fact] + public async Task OmitsEvaluationContextsWhenEmpty() + { + var container = new TestContainer(services => services.Configure(options => + { + options.EvaluationContexts = []; + })); + var flagsHandler = container.FakeHttpMessageHandler.AddFlagsResponse("""{"featureFlags": {"flag-a": true}}"""); + var client = container.Activate(); + + await client.EvaluateFlagsAsync("user-1", options: null, CancellationToken.None); + + var request = flagsHandler.ReceivedRequests.Single(); + var body = await request.Content!.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(body); + Assert.False(doc.RootElement.TryGetProperty("evaluation_contexts", out _)); + } + [Fact] public async Task OnlyEvaluateLocallyDoesNotHitRemote() { From 66fec0de2c9a0c3a3ce41387be6db0c9c75c9f68 Mon Sep 17 00:00:00 2001 From: Dustin Byrne Date: Tue, 26 May 2026 16:16:37 -0400 Subject: [PATCH 2/3] test: parameterize evaluation contexts request tests --- .../Features/FeatureFlagEvaluationsTests.cs | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/tests/UnitTests/Features/FeatureFlagEvaluationsTests.cs b/tests/UnitTests/Features/FeatureFlagEvaluationsTests.cs index 271a9d0b..ae7fbdee 100644 --- a/tests/UnitTests/Features/FeatureFlagEvaluationsTests.cs +++ b/tests/UnitTests/Features/FeatureFlagEvaluationsTests.cs @@ -79,34 +79,22 @@ await client.EvaluateFlagsAsync( Assert.Equal(new[] { "flag-a", "flag-b" }, flagKeys); } - [Fact] - public async Task SendsEvaluationContextsToFlagsRequestBody() + public static IEnumerable EvaluationContextsRequestBodyCases() { - var container = new TestContainer(services => services.Configure(options => - { - options.EvaluationContexts = ["production", "backend"]; - })); - var flagsHandler = container.FakeHttpMessageHandler.AddFlagsResponse("""{"featureFlags": {"flag-a": true}}"""); - var client = container.Activate(); - - await client.EvaluateFlagsAsync("user-1", options: null, CancellationToken.None); - - var request = flagsHandler.ReceivedRequests.Single(); - var body = await request.Content!.ReadAsStringAsync(); - using var doc = JsonDocument.Parse(body); - var evaluationContexts = doc.RootElement.GetProperty("evaluation_contexts") - .EnumerateArray() - .Select(e => e.GetString() ?? string.Empty) - .ToArray(); - Assert.Equal(new[] { "production", "backend" }, evaluationContexts); + yield return [new[] { "production", "backend" }, new[] { "production", "backend" }]; + yield return [Array.Empty(), null]; + yield return [null, null]; } - [Fact] - public async Task OmitsEvaluationContextsWhenEmpty() + [Theory] + [MemberData(nameof(EvaluationContextsRequestBodyCases))] + public async Task SendsEvaluationContextsToFlagsRequestBodyWhenConfigured( + string[]? configuredEvaluationContexts, + string[]? expectedEvaluationContexts) { var container = new TestContainer(services => services.Configure(options => { - options.EvaluationContexts = []; + options.EvaluationContexts = configuredEvaluationContexts; })); var flagsHandler = container.FakeHttpMessageHandler.AddFlagsResponse("""{"featureFlags": {"flag-a": true}}"""); var client = container.Activate(); @@ -116,7 +104,19 @@ public async Task OmitsEvaluationContextsWhenEmpty() var request = flagsHandler.ReceivedRequests.Single(); var body = await request.Content!.ReadAsStringAsync(); using var doc = JsonDocument.Parse(body); - Assert.False(doc.RootElement.TryGetProperty("evaluation_contexts", out _)); + + if (expectedEvaluationContexts is null) + { + Assert.False(doc.RootElement.TryGetProperty("evaluation_contexts", out _)); + } + else + { + var evaluationContexts = doc.RootElement.GetProperty("evaluation_contexts") + .EnumerateArray() + .Select(e => e.GetString() ?? string.Empty) + .ToArray(); + Assert.Equal(expectedEvaluationContexts, evaluationContexts); + } } [Fact] From 8e335f42aabd9f5a024f0cb8f22dd85b75b95a71 Mon Sep 17 00:00:00 2001 From: Dustin Byrne Date: Wed, 27 May 2026 11:53:12 -0400 Subject: [PATCH 3/3] chore: include aspnetcore in evaluation contexts changeset --- .changeset/few-flags-dance.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.changeset/few-flags-dance.md b/.changeset/few-flags-dance.md index f39252d5..e12e9b64 100644 --- a/.changeset/few-flags-dance.md +++ b/.changeset/few-flags-dance.md @@ -1,5 +1,6 @@ --- "PostHog": minor +"PostHog.AspNetCore": minor --- Add feature flag evaluation contexts via `PostHogOptions.EvaluationContexts`. `/flags` requests now send `evaluation_contexts` when configured.