Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/few-flags-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"PostHog": minor
Comment thread
dustinbyrne marked this conversation as resolved.
"PostHog.AspNetCore": minor
---

Add feature flag evaluation contexts via `PostHogOptions.EvaluationContexts`. `/flags` requests now send `evaluation_contexts` when configured.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 16 additions & 0 deletions src/PostHog.AspNetCore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/PostHog/Api/PostHogApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ public async Task<ApiResult> SendEventAsync(
payload["flag_keys_to_evaluate"] = flagKeysToEvaluate;
}

if (_options.Value.EvaluationContexts is { Count: > 0 } evaluationContexts)
{
payload["evaluation_contexts"] = evaluationContexts;
}

groupProperties?.AddToPayload(payload);

PrepareAndMutatePayload(payload);
Expand Down
9 changes: 9 additions & 0 deletions src/PostHog/Config/PostHogOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ public string? ProjectApiKey
/// </remarks>
public string? PersonalApiKey { get; set; }

/// <summary>
/// Evaluation contexts for feature flags.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public IReadOnlyList<string>? EvaluationContexts { get; set; }

/// <summary>
/// Whether this client is disabled and should no-op instead of sending data to PostHog. (Default: false)
/// </summary>
Expand Down
3 changes: 3 additions & 0 deletions tests/UnitTests/Config/RegistrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
})
Expand All @@ -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",
Expand Down
40 changes: 40 additions & 0 deletions tests/UnitTests/Features/FeatureFlagEvaluationsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,46 @@ await client.EvaluateFlagsAsync(
Assert.Equal(new[] { "flag-a", "flag-b" }, flagKeys);
}

public static IEnumerable<object?[]> EvaluationContextsRequestBodyCases()
{
yield return [new[] { "production", "backend" }, new[] { "production", "backend" }];
yield return [Array.Empty<string>(), null];
yield return [null, null];
}

[Theory]
[MemberData(nameof(EvaluationContextsRequestBodyCases))]
public async Task SendsEvaluationContextsToFlagsRequestBodyWhenConfigured(
string[]? configuredEvaluationContexts,
string[]? expectedEvaluationContexts)
{
var container = new TestContainer(services => services.Configure<PostHogOptions>(options =>
{
options.EvaluationContexts = configuredEvaluationContexts;
}));
var flagsHandler = container.FakeHttpMessageHandler.AddFlagsResponse("""{"featureFlags": {"flag-a": true}}""");
var client = container.Activate<PostHogClient>();

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);

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]
public async Task OnlyEvaluateLocallyDoesNotHitRemote()
{
Expand Down
Loading