Skip to content

Commit caef5ff

Browse files
committed
Disable client when project token is missing
1 parent e35bf94 commit caef5ff

3 files changed

Lines changed: 328 additions & 29 deletions

File tree

src/PostHog/Config/PostHogOptions.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Microsoft.Extensions.Options;
2+
using PostHog.Library;
23

34
namespace PostHog;
45

@@ -26,6 +27,15 @@ public string? ProjectToken
2627

2728
internal bool HasLegacyProjectApiKey => _projectApiKey is not null;
2829

30+
internal void Normalize()
31+
{
32+
_projectToken = _projectToken.NullIfEmpty();
33+
_projectApiKey = _projectApiKey.NullIfEmpty();
34+
PersonalApiKey = PersonalApiKey.NullIfEmpty();
35+
HostUrl = HostUrl.NormalizeHostUrl();
36+
Disabled = Disabled || ProjectToken is null;
37+
}
38+
2939
/// <summary>
3040
/// Obsolete alias for <see cref="ProjectToken"/>.
3141
/// </summary>
@@ -49,6 +59,11 @@ public string? ProjectApiKey
4959
/// </remarks>
5060
public string? PersonalApiKey { get; set; }
5161

62+
/// <summary>
63+
/// Whether this client is disabled and should no-op instead of sending data to PostHog. (Default: false)
64+
/// </summary>
65+
public bool Disabled { get; set; }
66+
5267
/// <summary>
5368
/// PostHog API host, usually 'https://us.i.posthog.com' (default) or 'https://eu.i.posthog.com'
5469
/// </summary>

src/PostHog/PostHogClient.cs

Lines changed: 149 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -95,16 +95,42 @@ static void NormalizeOptions(PostHogOptions options, ILogger<PostHogClient> logg
9595
logger.LogWarningProjectApiKeyDeprecated();
9696
}
9797

98-
options.ProjectToken = options.ProjectToken?.Trim();
99-
options.PersonalApiKey = options.PersonalApiKey.NullIfEmpty();
100-
options.HostUrl = options.HostUrl.NormalizeHostUrl();
98+
options.Normalize();
10199

102-
if (string.IsNullOrEmpty(options.ProjectToken))
100+
if (options.ProjectToken is null)
103101
{
104102
logger.LogErrorProjectTokenRequired();
105103
}
106104
}
107105

106+
bool IsDisabled(string methodName)
107+
{
108+
if (!_options.Value.Disabled)
109+
{
110+
return false;
111+
}
112+
113+
_logger.LogWarningClientDisabled(methodName);
114+
return true;
115+
}
116+
117+
// A personal_api_key is only required for feature flag calls when callers explicitly request
118+
// local-only evaluation. Without it we cannot download local flag definitions, and the
119+
// local-only option means we must not fall back to remote /flags evaluation.
120+
bool RequiresMissingPersonalApiKey(AllFeatureFlagsOptions? options, string methodName)
121+
=> options is { OnlyEvaluateLocally: true } && IsPersonalApiKeyMissing(methodName);
122+
123+
bool IsPersonalApiKeyMissing(string methodName)
124+
{
125+
if (_options.Value.PersonalApiKey is not null)
126+
{
127+
return false;
128+
}
129+
130+
_logger.LogWarningPersonalApiKeyMissing(methodName);
131+
return true;
132+
}
133+
108134
/// <summary>
109135
/// To marry up whatever a user does before they sign up or log in with what they do after you need to make an
110136
/// alias call. This will allow you to answer questions like "Which marketing channels leads to users churning
@@ -122,27 +148,48 @@ public async Task<ApiResult> AliasAsync(
122148
string previousId,
123149
string newId,
124150
CancellationToken cancellationToken)
125-
=> await _apiClient.AliasAsync(previousId, newId, cancellationToken);
151+
{
152+
if (IsDisabled(nameof(AliasAsync)))
153+
{
154+
return new ApiResult(0);
155+
}
156+
157+
return await _apiClient.AliasAsync(previousId, newId, cancellationToken);
158+
}
126159

127160
/// <inheritdoc/>
128161
public async Task<ApiResult> IdentifyAsync(
129162
string distinctId,
130163
Dictionary<string, object>? personPropertiesToSet,
131164
Dictionary<string, object>? personPropertiesToSetOnce,
132165
CancellationToken cancellationToken)
133-
=> await _apiClient.IdentifyAsync(
166+
{
167+
if (IsDisabled(nameof(IdentifyAsync)))
168+
{
169+
return new ApiResult(0);
170+
}
171+
172+
return await _apiClient.IdentifyAsync(
134173
distinctId,
135174
personPropertiesToSet,
136175
personPropertiesToSetOnce,
137176
cancellationToken);
177+
}
138178

139179
/// <inheritdoc/>
140180
public Task<ApiResult> GroupIdentifyAsync(
141181
string type,
142182
StringOrValue<int> key,
143183
Dictionary<string, object>? properties,
144184
CancellationToken cancellationToken)
145-
=> _apiClient.GroupIdentifyAsync(type, key, properties, cancellationToken);
185+
{
186+
if (IsDisabled(nameof(GroupIdentifyAsync)))
187+
{
188+
return Task.FromResult(new ApiResult(0));
189+
}
190+
191+
return _apiClient.GroupIdentifyAsync(type, key, properties, cancellationToken);
192+
}
146193

147194
/// <inheritdoc/>
148195
public Task<ApiResult> GroupIdentifyAsync(
@@ -151,7 +198,14 @@ public Task<ApiResult> GroupIdentifyAsync(
151198
StringOrValue<int> key,
152199
Dictionary<string, object>? properties,
153200
CancellationToken cancellationToken)
154-
=> _apiClient.GroupIdentifyAsync(type, key, properties, cancellationToken, distinctId);
201+
{
202+
if (IsDisabled(nameof(GroupIdentifyAsync)))
203+
{
204+
return Task.FromResult(new ApiResult(0));
205+
}
206+
207+
return _apiClient.GroupIdentifyAsync(type, key, properties, cancellationToken, distinctId);
208+
}
155209

156210
/// <inheritdoc/>
157211
public bool Capture(
@@ -162,6 +216,11 @@ public bool Capture(
162216
bool sendFeatureFlags,
163217
DateTimeOffset? timestamp = null)
164218
{
219+
if (IsDisabled(nameof(Capture)))
220+
{
221+
return false;
222+
}
223+
165224
// If custom timestamp provided, add it to properties
166225
if (timestamp.HasValue)
167226
{
@@ -218,6 +277,11 @@ public bool CaptureException(
218277
bool sendFeatureFlags,
219278
DateTimeOffset? timestamp = null)
220279
{
280+
if (IsDisabled(nameof(CaptureException)))
281+
{
282+
return false;
283+
}
284+
221285
if (exception == null)
222286
{
223287
_logger.LogErrorCaptureExceptionNull();
@@ -305,6 +369,16 @@ public async Task<bool> IsFeatureEnabledAsync(
305369
FeatureFlagOptions? options,
306370
CancellationToken cancellationToken)
307371
{
372+
if (IsDisabled(nameof(IsFeatureEnabledAsync)))
373+
{
374+
return false;
375+
}
376+
377+
if (RequiresMissingPersonalApiKey(options, nameof(IsFeatureEnabledAsync)))
378+
{
379+
return false;
380+
}
381+
308382
var result = await GetFeatureFlagAsync(
309383
featureKey,
310384
distinctId,
@@ -321,6 +395,16 @@ public async Task<bool> IsFeatureEnabledAsync(
321395
FeatureFlagOptions? options,
322396
CancellationToken cancellationToken)
323397
{
398+
if (IsDisabled(nameof(GetFeatureFlagAsync)))
399+
{
400+
return null;
401+
}
402+
403+
if (RequiresMissingPersonalApiKey(options, nameof(GetFeatureFlagAsync)))
404+
{
405+
return null;
406+
}
407+
324408
LocalEvaluator? localEvaluator;
325409
try
326410
{
@@ -461,9 +545,13 @@ void HandleRemoteError(Exception ex, string errorType)
461545
/// <inheritdoc/>
462546
public async Task<JsonDocument?> GetRemoteConfigPayloadAsync(string key, CancellationToken cancellationToken)
463547
{
464-
if (_options.Value.PersonalApiKey is null)
548+
if (IsDisabled(nameof(GetRemoteConfigPayloadAsync)))
549+
{
550+
return null;
551+
}
552+
553+
if (IsPersonalApiKeyMissing(nameof(GetRemoteConfigPayloadAsync)))
465554
{
466-
_logger.LogWarningPersonalApiKeyRequiredForRemoteConfigPayload();
467555
return null;
468556
}
469557

@@ -565,6 +653,16 @@ public async Task<IReadOnlyDictionary<string, FeatureFlag>> GetAllFeatureFlagsAs
565653
AllFeatureFlagsOptions? options,
566654
CancellationToken cancellationToken)
567655
{
656+
if (IsDisabled(nameof(GetAllFeatureFlagsAsync)))
657+
{
658+
return new Dictionary<string, FeatureFlag>();
659+
}
660+
661+
if (RequiresMissingPersonalApiKey(options, nameof(GetAllFeatureFlagsAsync)))
662+
{
663+
return new Dictionary<string, FeatureFlag>();
664+
}
665+
568666
if (_options.Value.PersonalApiKey is not null)
569667
{
570668
// Attempt to load local feature flags.
@@ -650,9 +748,13 @@ public async Task LoadFeatureFlagsAsync(CancellationToken cancellationToken)
650748
{
651749
_logger.LogInfoLoadFeatureFlags();
652750

653-
if (_options.Value.PersonalApiKey is null)
751+
if (IsDisabled(nameof(LoadFeatureFlagsAsync)))
752+
{
753+
return;
754+
}
755+
756+
if (IsPersonalApiKeyMissing(nameof(LoadFeatureFlagsAsync)))
654757
{
655-
_logger.LogWarningPersonalApiKeyRequired();
656758
return;
657759
}
658760

@@ -678,7 +780,15 @@ public async Task LoadFeatureFlagsAsync(CancellationToken cancellationToken)
678780
}
679781

680782
/// <inheritdoc/>
681-
public async Task FlushAsync() => await _asyncBatchHandler.FlushAsync();
783+
public async Task FlushAsync()
784+
{
785+
if (IsDisabled(nameof(FlushAsync)))
786+
{
787+
return;
788+
}
789+
790+
await _asyncBatchHandler.FlushAsync();
791+
}
682792

683793
/// <inheritdoc/>
684794
public string Version => VersionConstants.Version;
@@ -688,6 +798,11 @@ public async Task LoadFeatureFlagsAsync(CancellationToken cancellationToken)
688798
[Obsolete("This method is for internal use only and may go away soon.")]
689799
internal async Task<LocalEvaluator?> GetLocalEvaluatorAsync(CancellationToken cancellationToken)
690800
{
801+
if (IsDisabled(nameof(GetLocalEvaluatorAsync)))
802+
{
803+
return null;
804+
}
805+
691806
try
692807
{
693808
return await _featureFlagsLoader.GetFeatureFlagsForLocalEvaluationAsync(cancellationToken);
@@ -705,7 +820,15 @@ public async Task LoadFeatureFlagsAsync(CancellationToken cancellationToken)
705820
/// <summary>
706821
/// Clears the local flags cache.
707822
/// </summary>
708-
public void ClearLocalFlagsCache() => _featureFlagsLoader.Clear();
823+
public void ClearLocalFlagsCache()
824+
{
825+
if (IsDisabled(nameof(ClearLocalFlagsCache)))
826+
{
827+
return;
828+
}
829+
830+
_featureFlagsLoader.Clear();
831+
}
709832

710833
/// <inheritdoc/>
711834
public async ValueTask DisposeAsync()
@@ -796,12 +919,6 @@ public static partial void LogWarnCaptureFailed(
796919
int propertiesCount,
797920
int count);
798921

799-
[LoggerMessage(
800-
EventId = 9,
801-
Level = LogLevel.Warning,
802-
Message = "[FEATURE FLAGS] You have to specify a personal_api_key to fetch remote config payloads.")]
803-
public static partial void LogWarningPersonalApiKeyRequiredForRemoteConfigPayload(this ILogger<PostHogClient> logger);
804-
805922
[LoggerMessage(
806923
EventId = 10,
807924
Level = LogLevel.Error,
@@ -846,12 +963,6 @@ public static partial void LogErrorUnableToGetRemoteConfigPayload(
846963
Message = "[FEATURE FLAGS] Loading feature flags for local evaluation")]
847964
public static partial void LogInfoLoadFeatureFlags(this ILogger<PostHogClient> logger);
848965

849-
[LoggerMessage(
850-
EventId = 17,
851-
Level = LogLevel.Warning,
852-
Message = "[FEATURE FLAGS] You have to specify a personal_api_key to use feature flags.")]
853-
public static partial void LogWarningPersonalApiKeyRequired(this ILogger<PostHogClient> logger);
854-
855966
[LoggerMessage(
856967
EventId = 18,
857968
Level = LogLevel.Debug,
@@ -875,4 +986,16 @@ public static partial void LogErrorUnableToGetRemoteConfigPayload(
875986
Level = LogLevel.Error,
876987
Message = "CaptureException failed with an exception")]
877988
public static partial void LogErrorCaptureExceptionFailed(this ILogger<PostHogClient> logger, Exception exception);
989+
990+
[LoggerMessage(
991+
EventId = 21,
992+
Level = LogLevel.Warning,
993+
Message = "PostHog SDK is disabled; {MethodName} is a no-op.")]
994+
public static partial void LogWarningClientDisabled(this ILogger<PostHogClient> logger, string methodName);
995+
996+
[LoggerMessage(
997+
EventId = 22,
998+
Level = LogLevel.Warning,
999+
Message = "PostHog personal_api_key is not configured; {MethodName} is a no-op.")]
1000+
public static partial void LogWarningPersonalApiKeyMissing(this ILogger<PostHogClient> logger, string methodName);
8781001
}

0 commit comments

Comments
 (0)