Skip to content

Commit ca5bcff

Browse files
authored
refactor: share feature flag event enrichment (#302)
1 parent a62e287 commit ca5bcff

2 files changed

Lines changed: 63 additions & 15 deletions

File tree

src/PostHog/PostHogClient.cs

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ public sealed class PostHogClient : IPostHogClient
2424
readonly MemoryCache _featureFlagCalledEventCache;
2525
static readonly ApiResult NoOpApiResult = new(0);
2626
static readonly IReadOnlyDictionary<string, FeatureFlag> EmptyFeatureFlags = new Dictionary<string, FeatureFlag>(0);
27+
static readonly Func<FeatureFlag, object> FeatureFlagResponseSelector = flag => flag.ToResponseObject();
28+
static readonly Func<FeatureFlag, bool> FeatureFlagEnabledSelector = flag => (bool)flag;
29+
static readonly Func<EvaluatedFlagRecord, object> EvaluatedFlagResponseSelector = record => record.Flag.ToResponseObject();
30+
static readonly Func<EvaluatedFlagRecord, bool> EvaluatedFlagEnabledSelector = record => record.Enabled;
2731

2832
// Strict allowlist for minimal $feature_flag_called events, shared across PostHog SDKs.
2933
// Everything else — including super properties, context properties, the $feature/<key>
@@ -581,28 +585,34 @@ async Task<CapturedEvent> AddLocalFeatureFlagDataAsync(
581585
static CapturedEvent AddFeatureFlagsToCapturedEvent(
582586
CapturedEvent capturedEvent,
583587
IReadOnlyDictionary<string, FeatureFlag> flags)
584-
{
585-
capturedEvent.Properties.Merge(flags.ToDictionary(
586-
f => $"$feature/{f.Key}",
587-
f => f.Value.ToResponseObject()));
588-
capturedEvent.Properties["$active_feature_flags"] = flags
589-
.Where(f => (bool)f.Value)
590-
.Select(kvp => kvp.Key)
591-
.ToArray();
592-
return capturedEvent;
593-
}
588+
=> AddFeatureFlagsToCapturedEvent(
589+
capturedEvent,
590+
flags,
591+
FeatureFlagResponseSelector,
592+
FeatureFlagEnabledSelector);
594593

595594
static CapturedEvent AddFeatureFlagsToCapturedEvent(
596595
CapturedEvent capturedEvent,
597596
FeatureFlagEvaluations flags)
597+
=> AddFeatureFlagsToCapturedEvent(
598+
capturedEvent,
599+
flags.Records,
600+
EvaluatedFlagResponseSelector,
601+
EvaluatedFlagEnabledSelector);
602+
603+
static CapturedEvent AddFeatureFlagsToCapturedEvent<TFlag>(
604+
CapturedEvent capturedEvent,
605+
IReadOnlyDictionary<string, TFlag> flags,
606+
Func<TFlag, object> responseSelector,
607+
Func<TFlag, bool> enabledSelector)
598608
{
599609
// Single-pass: per-flag $feature/<key> property + $active_feature_flags collection in one
600-
// enumeration of the records dictionary. Runs per captured event, so worth keeping tight.
601-
var active = new List<string>(flags.Records.Count);
602-
foreach (var (key, record) in flags.Records)
610+
// enumeration. The selectors are cached statically so this hot path does not allocate delegates.
611+
var active = new List<string>(flags.Count);
612+
foreach (var (key, flag) in flags)
603613
{
604-
capturedEvent.Properties[$"$feature/{key}"] = record.Flag.ToResponseObject();
605-
if (record.Enabled)
614+
capturedEvent.Properties[$"$feature/{key}"] = responseSelector(flag);
615+
if (enabledSelector(flag))
606616
{
607617
active.Add(key);
608618
}

tests/UnitTests/Features/FeatureFlagEvaluationsTests.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,44 @@ static async Task<FeatureFlagEvaluations> EvaluateAsync(string flagsResponseBody
716716

717717
public class TheCaptureWithFlagsSnapshotMethod
718718
{
719+
[Fact]
720+
public async Task LegacyAndSnapshotInputsProduceIdenticalFeatureFlagProperties()
721+
{
722+
const string response =
723+
"""{"featureFlags": {"flag-a": true, "flag-b": false, "flag-c": "variant-x"}}""";
724+
var container = new TestContainer();
725+
container.FakeHttpMessageHandler.AddRepeatedFlagsResponse(2, response);
726+
var batchHandler = container.FakeHttpMessageHandler.AddBatchResponse();
727+
var client = container.Activate<PostHogClient>();
728+
729+
var snapshot = await client.EvaluateFlagsAsync("user-1", options: null, CancellationToken.None);
730+
client.Capture("user-1", "snapshot", properties: null, groups: null, flags: snapshot);
731+
#pragma warning disable CS0618
732+
client.Capture("user-1", "legacy", properties: null, groups: null, sendFeatureFlags: true);
733+
#pragma warning restore CS0618
734+
await client.FlushAsync();
735+
736+
using var doc = JsonDocument.Parse(batchHandler.GetReceivedRequestBody(indented: false));
737+
var events = doc.RootElement.GetProperty("batch").EnumerateArray().ToArray();
738+
var snapshotProperties = events.Single(e => e.GetProperty("event").GetString() == "snapshot")
739+
.GetProperty("properties");
740+
var legacyProperties = events.Single(e => e.GetProperty("event").GetString() == "legacy")
741+
.GetProperty("properties");
742+
743+
foreach (var propertyName in new[]
744+
{
745+
"$feature/flag-a",
746+
"$feature/flag-b",
747+
"$feature/flag-c",
748+
"$active_feature_flags"
749+
})
750+
{
751+
Assert.Equal(
752+
legacyProperties.GetProperty(propertyName).GetRawText(),
753+
snapshotProperties.GetProperty(propertyName).GetRawText());
754+
}
755+
}
756+
719757
[Fact]
720758
public async Task AttachesFeatureFlagPropertiesAndActiveFeatureFlagsFromSnapshot()
721759
{

0 commit comments

Comments
 (0)