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
4 changes: 2 additions & 2 deletions src/PostHog/Features/LocalFeatureFlagsLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ void StartPollingIfNotStarted()
/// <exception cref="ApiException">Thrown when the API returns a <c>quota_limited</c> error.</exception>
public async ValueTask<LocalEvaluator?> GetFeatureFlagsForLocalEvaluationAsync(CancellationToken cancellationToken)
{
if (options.Value.PersonalApiKey is null)
if (string.IsNullOrWhiteSpace(options.Value.PersonalApiKey))
{
// Local evaluation is not enabled since it requires a personal api key.
return null;
Expand All @@ -70,7 +70,7 @@ void StartPollingIfNotStarted()
/// <returns>The local evaluator with the feature flags.</returns>
public async ValueTask<LocalEvaluator?> RefreshAsync(CancellationToken cancellationToken)
{
if (options.Value.PersonalApiKey is null)
if (string.IsNullOrWhiteSpace(options.Value.PersonalApiKey))
{
return null;
}
Expand Down
11 changes: 11 additions & 0 deletions src/PostHog/Library/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ namespace PostHog.Library;

internal static class StringExtensions
{
/// <summary>
/// Trims <paramref name="value"/> and returns <c>null</c> when the result is empty.
/// </summary>
/// <param name="value">The string to normalize.</param>
/// <returns>The trimmed string, or <c>null</c> if the input is null, empty, or whitespace-only.</returns>
public static string? NullIfEmpty(this string? value)
{
var trimmed = value?.Trim();
return string.IsNullOrEmpty(trimmed) ? null : trimmed;
}

/// <summary>
/// Truncates <paramref name="value"/> so its UTF-8 byte length is at most <paramref name="maxLength"/>.
/// Appends <paramref name="truncationSymbol"/> if truncation occurs. Respects UTF-8 code point boundaries, but it can split graphemes.
Expand Down
23 changes: 23 additions & 0 deletions src/PostHog/Library/UriExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace PostHog.Library;

internal static class UriExtensions
{
static readonly Uri DefaultHostUrl = new("https://us.i.posthog.com");

public static Uri NormalizeHostUrl(this Uri? value)
{
if (value is null)
{
return DefaultHostUrl;
}

var rawValue = value.OriginalString.Trim();
if (!Uri.TryCreate(rawValue, UriKind.Absolute, out var normalized)
|| (normalized.Scheme != Uri.UriSchemeHttp && normalized.Scheme != Uri.UriSchemeHttps))
{
return DefaultHostUrl;
}

return normalized;
}
}
28 changes: 24 additions & 4 deletions src/PostHog/PostHogClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ public PostHogClient(
_taskScheduler = taskScheduler ?? new TaskRunTaskScheduler();
_timeProvider = timeProvider ?? TimeProvider.System;
loggerFactory ??= NullLoggerFactory.Instance;
_logger = loggerFactory.CreateLogger<PostHogClient>();

NormalizeOptions(_options.Value, _logger);

_apiClient = new PostHogApiClient(
httpClientFactory.CreateClient(nameof(PostHogClient)),
Expand Down Expand Up @@ -82,10 +85,21 @@ public PostHogClient(
CompactionPercentage = options.Value.FeatureFlagSentCacheCompactionPercentage
});

_logger = loggerFactory.CreateLogger<PostHogClient>();
_logger.LogInfoClientCreated(options.Value.MaxBatchSize, options.Value.FlushInterval, options.Value.FlushAt);
}

static void NormalizeOptions(PostHogOptions options, ILogger<PostHogClient> logger)
{
options.ProjectApiKey = options.ProjectApiKey?.Trim();
options.PersonalApiKey = options.PersonalApiKey.NullIfEmpty();
options.HostUrl = options.HostUrl.NormalizeHostUrl();

if (string.IsNullOrEmpty(options.ProjectApiKey))
{
logger.LogErrorProjectApiKeyBlankAfterTrim();
}
}

/// <summary>
/// To marry up whatever a user does before they sign up or log in with what they do after you need to make an
/// alias call. This will allow you to answer questions like "Which marketing channels leads to users churning
Expand Down Expand Up @@ -811,24 +825,30 @@ public static partial void LogErrorUnableToGetRemoteConfigPayload(

[LoggerMessage(
EventId = 14,
Level = LogLevel.Error,
Message = "ProjectApiKey is empty after trimming whitespace; check your project API key")]
public static partial void LogErrorProjectApiKeyBlankAfterTrim(this ILogger<PostHogClient> logger);

[LoggerMessage(
EventId = 15,
Level = LogLevel.Information,
Message = "[FEATURE FLAGS] Loading feature flags for local evaluation")]
public static partial void LogInfoLoadFeatureFlags(this ILogger<PostHogClient> logger);

[LoggerMessage(
EventId = 15,
EventId = 16,
Level = LogLevel.Warning,
Message = "[FEATURE FLAGS] You have to specify a personal_api_key to use feature flags.")]
public static partial void LogWarningPersonalApiKeyRequired(this ILogger<PostHogClient> logger);

[LoggerMessage(
EventId = 16,
EventId = 17,
Level = LogLevel.Debug,
Message = "[FEATURE FLAGS] Feature flags loaded successfully, polling {PollingStatus}")]
public static partial void LogDebugFeatureFlagsLoaded(this ILogger<PostHogClient> logger, string pollingStatus);

[LoggerMessage(
EventId = 17,
EventId = 18,
Level = LogLevel.Error,
Message = "[FEATURE FLAGS] Failed to load feature flags")]
public static partial void LogErrorFailedToLoadFeatureFlags(this ILogger<PostHogClient> logger, Exception exception);
Expand Down
53 changes: 53 additions & 0 deletions tests/UnitTests/PostHogClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1438,6 +1438,59 @@ public async Task LogsWarningWhenPersonalApiKeyIsNull()
log.Message?.Contains("You have to specify a personal_api_key to use feature flags", StringComparison.Ordinal) == true);
}

[Fact]
public async Task LogsWarningWhenPersonalApiKeyIsBlankAfterTrimmingWhitespace()
{
var container = new TestContainer(personalApiKey: " \n\t ");
var client = container.Activate<PostHogClient>();

await client.LoadFeatureFlagsAsync();

var warningLogs = container.FakeLoggerProvider.GetAllEvents(minimumLevel: LogLevel.Warning);
Assert.Contains(warningLogs, log =>
log.Message?.Contains("You have to specify a personal_api_key to use feature flags", StringComparison.Ordinal) == true);
}

[Fact]
public void LogsErrorWhenProjectApiKeyIsBlankAfterTrimmingWhitespace()
{
var container = new TestContainer(services =>
{
services.Configure<PostHogOptions>(options => options.ProjectApiKey = " \n\t ");
});

_ = container.Activate<PostHogClient>();

var errorLogs = container.FakeLoggerProvider.GetAllEvents(minimumLevel: LogLevel.Error);
Assert.Contains(errorLogs, log =>
log.Message?.Contains("ProjectApiKey is empty after trimming whitespace", StringComparison.Ordinal) == true);
}

[Fact]
public void DefaultsHostUrlWhenConfiguredHostIsNullOrInvalid()
{
var nullHostContainer = new TestContainer(services =>
{
services.Configure<PostHogOptions>(options => options.HostUrl = null!);
});

_ = nullHostContainer.Activate<PostHogClient>();

var nullHostOptions = ((IOptions<PostHogOptions>)((IServiceProvider)nullHostContainer).GetService(typeof(IOptions<PostHogOptions>))!).Value;
Assert.Equal(new Uri("https://us.i.posthog.com"), nullHostOptions.HostUrl);

var invalidHostContainer = new TestContainer(services =>
{
services.Configure<PostHogOptions>(options =>
options.HostUrl = new Uri(" \n\t ", UriKind.RelativeOrAbsolute));
});

_ = invalidHostContainer.Activate<PostHogClient>();

var invalidHostOptions = ((IOptions<PostHogOptions>)((IServiceProvider)invalidHostContainer).GetService(typeof(IOptions<PostHogOptions>))!).Value;
Assert.Equal(new Uri("https://us.i.posthog.com"), invalidHostOptions.HostUrl);
}

[Fact]
public async Task LogsDebugWhenFlagsLoadedSuccessfully()
{
Expand Down
Loading