File tree Expand file tree Collapse file tree
src/PostHog.AspNetCore/FeatureManagement
tests/UnitTests.AspNetCore/FeatureManagement Expand file tree Collapse file tree Original file line number Diff line number Diff line change 1+ namespace PostHog . FeatureManagement ;
2+
3+ /// <summary>
4+ /// Shared fallback used by <see cref="PostHogFeatureDefinitionProvider"/> and
5+ /// <see cref="PostHogVariantFeatureManager"/> to enumerate flag keys when no
6+ /// <see cref="Config.PostHogOptions.PersonalApiKey"/> is configured (no local evaluation source).
7+ /// </summary>
8+ /// <remarks>
9+ /// <para>
10+ /// Polls <c>/flags</c> with a stable sentinel <c>distinct_id</c> and returns the keys from the
11+ /// response. Per-flag values for the sentinel are discarded — only the set of keys is consumed.
12+ /// </para>
13+ /// <para>
14+ /// The sentinel id is intentionally stable: a random id would spawn a phantom person on every poll
15+ /// and defeat the in-memory <c>/flags</c> cache. See PostHog/posthog-dotnet#64 for the rationale.
16+ /// </para>
17+ /// </remarks>
18+ internal static class FeatureEnumerationFallback
19+ {
20+ /// <summary>
21+ /// Stable sentinel <c>distinct_id</c> used by the enumeration fallback. Stays constant so the
22+ /// SDK's in-memory <c>/flags</c> cache (keyed by distinct_id) reuses the response and PostHog
23+ /// doesn't create a new phantom person per poll.
24+ /// </summary>
25+ internal const string SentinelDistinctId = "$feature_enumeration_sentinel" ;
26+
27+ public static async Task < IReadOnlyCollection < string > > GetFeatureKeysAsync (
28+ IPostHogClient posthog ,
29+ CancellationToken cancellationToken )
30+ {
31+ var flags = await posthog . GetAllFeatureFlagsAsync (
32+ distinctId : SentinelDistinctId ,
33+ options : null ,
34+ cancellationToken ) ;
35+ // Order is not guaranteed by /flags; callers (Microsoft Feature Management) don't rely on it.
36+ return flags . Keys . ToList ( ) ;
37+ }
38+ }
Original file line number Diff line number Diff line change @@ -31,16 +31,25 @@ public async IAsyncEnumerable<FeatureDefinition> GetAllFeatureDefinitionsAsync()
3131 {
3232 foreach ( var flag in localEvaluator . LocalEvaluationApiResult . Flags )
3333 {
34- yield return CreateFeatureDefinition ( flag ) ;
34+ yield return CreateFeatureDefinition ( flag . Key ) ;
3535 }
36+ yield break ;
37+ }
38+
39+ // Fallback: no PersonalApiKey means no local-evaluation flag list. Poll /flags with a stable
40+ // sentinel distinct_id and use the returned keys as the enumeration source. We only care about
41+ // keys; the values for the sentinel are discarded. See PostHog/posthog-dotnet#64.
42+ foreach ( var key in await FeatureEnumerationFallback . GetFeatureKeysAsync ( posthog , CancellationToken . None ) )
43+ {
44+ yield return CreateFeatureDefinition ( key ) ;
3645 }
3746 }
3847
39- static FeatureDefinition CreateFeatureDefinition ( LocalFeatureFlag flag )
48+ static FeatureDefinition CreateFeatureDefinition ( string key )
4049 {
4150 return new FeatureDefinition
4251 {
43- Name = flag . Key ,
52+ Name = key ,
4453 EnabledFor = [ new FeatureFilterConfiguration { Name = "PostHog" } ] ,
4554 RequirementType = RequirementType . Any
4655 } ;
Original file line number Diff line number Diff line change @@ -23,13 +23,20 @@ public async IAsyncEnumerable<string> GetFeatureNamesAsync(
2323 [ EnumeratorCancellation ] CancellationToken cancellationToken = new ( ) )
2424 {
2525 var localEvaluator = await posthog . GetLocalEvaluatorAsync ( cancellationToken ) ;
26- if ( localEvaluator is null )
26+ if ( localEvaluator is not null )
2727 {
28+ foreach ( var flag in localEvaluator . LocalEvaluationApiResult . Flags )
29+ {
30+ yield return flag . Key ;
31+ }
2832 yield break ;
2933 }
30- foreach ( var flag in localEvaluator . LocalEvaluationApiResult . Flags )
34+
35+ // Fallback: no PersonalApiKey means no local-evaluation flag list. Poll /flags with a stable
36+ // sentinel distinct_id and yield the returned keys. See PostHog/posthog-dotnet#64.
37+ foreach ( var key in await FeatureEnumerationFallback . GetFeatureKeysAsync ( posthog , cancellationToken ) )
3138 {
32- yield return flag . Key ;
39+ yield return key ;
3340 }
3441 }
3542
Original file line number Diff line number Diff line change @@ -67,6 +67,41 @@ public async Task ReturnsAllActiveFeatures()
6767 }
6868}
6969
70+ public class TheGetAllFeatureDefinitionsAsyncMethodFallback
71+ {
72+ [ Fact ]
73+ public async Task ReturnsFlagsFromFlagsEndpointWhenNoPersonalApiKey ( )
74+ {
75+ var container = new TestContainer ( sp =>
76+ {
77+ var builder = new PostHogConfigurationBuilder ( sp ) ;
78+ builder . UseFeatureManagement < FakePostHogFeatureFlagContextProvider > ( ) ;
79+ // No PersonalApiKey configured.
80+ } ) ;
81+ container . FakeHttpMessageHandler . AddFlagsResponse (
82+ requestPredicate : body =>
83+ body . TryGetValue ( "distinct_id" , out var id )
84+ && id is System . Text . Json . JsonElement el
85+ && el . GetString ( ) == FeatureEnumerationFallback . SentinelDistinctId ,
86+ responseBody : """
87+ {
88+ "featureFlags": {
89+ "beta-feature": true,
90+ "alpha-feature": false
91+ }
92+ }
93+ """
94+ ) ;
95+ var provider = container . Activate < PostHogFeatureDefinitionProvider > ( ) ;
96+
97+ var features = await provider . GetAllFeatureDefinitionsAsync ( ) . ToListAsync ( ) ;
98+
99+ Assert . Equal (
100+ new [ ] { "beta-feature" , "alpha-feature" } ,
101+ features . Select ( f => f . Name ) . OrderByDescending ( n => n ) ) ;
102+ }
103+ }
104+
70105public class TheGetFeatureDefinitionAsyncMethod
71106{
72107 [ Fact ]
Original file line number Diff line number Diff line change @@ -63,6 +63,41 @@ public async Task ReturnsAllActiveFeatures()
6363 }
6464}
6565
66+ public class TheGetFeatureNamesAsyncMethodFallback
67+ {
68+ [ Fact ]
69+ public async Task ReturnsFlagsFromFlagsEndpointWhenNoPersonalApiKey ( )
70+ {
71+ var container = new TestContainer ( sp =>
72+ {
73+ var builder = new PostHogConfigurationBuilder ( sp ) ;
74+ builder . UseFeatureManagement < FakePostHogFeatureFlagContextProvider > ( ) ;
75+ // No PersonalApiKey configured.
76+ } ) ;
77+ container . FakeHttpMessageHandler . AddFlagsResponse (
78+ requestPredicate : body =>
79+ body . TryGetValue ( "distinct_id" , out var id )
80+ && id is System . Text . Json . JsonElement el
81+ && el . GetString ( ) == FeatureEnumerationFallback . SentinelDistinctId ,
82+ responseBody : """
83+ {
84+ "featureFlags": {
85+ "beta-feature": true,
86+ "alpha-feature": false
87+ }
88+ }
89+ """
90+ ) ;
91+ var featureManager = container . Activate < PostHogVariantFeatureManager > ( ) ;
92+
93+ var featureNames = await featureManager . GetFeatureNamesAsync ( ) . ToListAsync ( ) ;
94+
95+ Assert . Equal (
96+ new [ ] { "beta-feature" , "alpha-feature" } ,
97+ featureNames . OrderByDescending ( n => n ) ) ;
98+ }
99+ }
100+
66101public class TheIsEnabledAsyncMethod
67102{
68103 [ Fact ]
You can’t perform that action at this time.
0 commit comments