Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions .changeset/few-flags-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"PostHog": minor
Comment thread
dustinbyrne marked this conversation as resolved.
---

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

[Fact]
public async Task SendsEvaluationContextsToFlagsRequestBody()
{
var container = new TestContainer(services => services.Configure<PostHogOptions>(options =>
{
options.EvaluationContexts = ["production", "backend"];
}));
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);
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<PostHogOptions>(options =>
{
options.EvaluationContexts = [];
}));
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);
Assert.False(doc.RootElement.TryGetProperty("evaluation_contexts", out _));
}

[Fact]
public async Task OnlyEvaluateLocallyDoesNotHitRemote()
{
Comment thread
dustinbyrne marked this conversation as resolved.
Outdated
Expand Down
Loading