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
2 changes: 1 addition & 1 deletion src/PostHog/ErrorTracking/ExceptionPropertiesBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ private static List<Dictionary<string, object>> BuildStackFrameList(Exception ex
continue;
}

var sourceContext = BuildSourceCodeContext(fileName, lineNumber, columnNumber,
var sourceContext = BuildSourceCodeContext(fileName!, lineNumber, columnNumber,
DEFAULT_MAX_LINES, DEFAULT_MAX_LENGTH);

frameDetails["pre_context"] = sourceContext.PreContext ?? [];
Expand Down
2 changes: 1 addition & 1 deletion src/PostHog/Features/Group.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public Group(string groupType, string groupKey, IReadOnlyDictionary<string, obje
/// <param name="properties"></param>
public Group(IReadOnlyDictionary<string, object?> properties)
{
Properties = new Dictionary<string, object?>(properties);
Properties = properties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}

/// <summary>
Expand Down
22 changes: 20 additions & 2 deletions src/PostHog/Features/IFeatureFlagCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,18 @@ Task<IReadOnlyDictionary<string, FeatureFlag>> GetAndCacheFeatureFlagsAsync(
/// <param name="cancellationToken">The cancellation token that can be used to cancel the operation.</param>
/// <returns>The set of feature flags.</returns>
[Obsolete("Use GetAndCacheFlagsAsync overload that accepts personProperties and groups to ensure correct cache keys. This method will be removed in a future version.")]
async Task<FlagsResult> GetAndCacheFlagsAsync(
// netstandard2.0 does not support default interface methods (DIM), so the default
// implementations below are conditionally compiled. All concrete implementations
// (FeatureFlagCacheBase, NullFeatureFlagCache, HttpContextFeatureFlagCache) provide
// explicit implementations of every method, so the DIM bodies are never invoked at runtime.
Comment thread
haacked marked this conversation as resolved.
#if !NETSTANDARD2_0
async
#endif
Task<FlagsResult> GetAndCacheFlagsAsync(
string distinctId,
Func<string, CancellationToken, Task<FlagsResult>> fetcher,
CancellationToken cancellationToken)
#if !NETSTANDARD2_0
{
return new FlagsResult
{
Expand All @@ -56,6 +64,9 @@ async Task<FlagsResult> GetAndCacheFlagsAsync(
)
};
}
#else
;
#endif

/// <summary>
/// Attempts to retrieve the flags API result. If the feature flags are not in the cache, then
Expand All @@ -71,17 +82,24 @@ async Task<FlagsResult> GetAndCacheFlagsAsync(
/// <param name="fetcher">The feature flag fetcher.</param>
/// <param name="cancellationToken">The cancellation token that can be used to cancel the operation.</param>
/// <returns>The set of feature flags.</returns>
async Task<FlagsResult> GetAndCacheFlagsAsync(
#if !NETSTANDARD2_0
async
#endif
Task<FlagsResult> GetAndCacheFlagsAsync(
string distinctId,
IReadOnlyDictionary<string, object?>? personProperties,
GroupCollection? groups,
Func<string, CancellationToken, Task<FlagsResult>> fetcher,
CancellationToken cancellationToken)
#if !NETSTANDARD2_0
{
// Default implementation: no caching to avoid incorrect cache keys
// Implementations should override this to provide proper caching
return await NotNull(fetcher)(distinctId, cancellationToken);
}
#else
;
#endif
}

/// <summary>
Expand Down
40 changes: 40 additions & 0 deletions src/PostHog/Library/Polyfills/DictionaryPolyfills.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#if NETSTANDARD2_0
// ReSharper disable once CheckNamespace
namespace System.Collections.Generic;

internal static class DictionaryPolyfills
{
public static bool TryAdd<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey key, TValue value)
where TKey : notnull
{
if (dictionary.ContainsKey(key))
{
return false;
}

dictionary.Add(key, value);
return true;
}

public static TValue? GetValueOrDefault<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, TKey key)
where TKey : notnull
{
return dictionary.TryGetValue(key, out var value) ? value : default;
}

public static TValue GetValueOrDefault<TKey, TValue>(
this IReadOnlyDictionary<TKey, TValue> dictionary,
TKey key,
TValue defaultValue)
where TKey : notnull
{
return dictionary.TryGetValue(key, out var value) ? value : defaultValue;
}

public static void Deconstruct<TKey, TValue>(this KeyValuePair<TKey, TValue> pair, out TKey key, out TValue value)
{
key = pair.Key;
value = pair.Value;
}
}
#endif
120 changes: 120 additions & 0 deletions src/PostHog/Library/Polyfills/IndexRangePolyfill.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#if NETSTANDARD2_0
using System.Runtime.CompilerServices;

// ReSharper disable once CheckNamespace
namespace System;

/// <summary>
/// Polyfill for System.Index to enable C# 8 index syntax (e.g., ^1) on netstandard2.0.
/// </summary>
internal readonly struct Index : IEquatable<Index>
{
readonly int _value;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Index(int value, bool fromEnd = false)
{
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative");
}

_value = fromEnd ? ~value : value;
}

Index(int value)
{
_value = value;
}

public static Index Start => new(0);
public static Index End => new(~0);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Index FromStart(int value)
{
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative");
}

return new Index(value);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Index FromEnd(int value)
{
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative");
}

return new Index(~value);
}

public int Value => _value < 0 ? ~_value : _value;
public bool IsFromEnd => _value < 0;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetOffset(int length)
{
var offset = _value;
if (IsFromEnd)
{
offset += length + 1;
}

return offset;
}

public override bool Equals(object? value) => value is Index index && _value == index._value;
public bool Equals(Index other) => _value == other._value;
public override int GetHashCode() => _value;

public static implicit operator Index(int value) => FromStart(value);
}

/// <summary>
/// Polyfill for System.Range to enable C# 8 range syntax (e.g., 1..^1) on netstandard2.0.
/// </summary>
internal readonly struct Range : IEquatable<Range>
{
public Index Start { get; }
public Index End { get; }

public Range(Index start, Index end)
{
Start = start;
End = end;
}

public static Range StartAt(Index start) => new(start, Index.End);
public static Range EndAt(Index end) => new(Index.Start, end);
public static Range All => new(Index.Start, Index.End);

public override bool Equals(object? value) => value is Range r && r.Start.Equals(Start) && r.End.Equals(End);
public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End);
public override int GetHashCode() => Start.GetHashCode() * 31 + End.GetHashCode();

public (int Offset, int Length) GetOffsetAndLength(int length)
{
var start = Start.GetOffset(length);
var end = End.GetOffset(length);
if ((uint)end > (uint)length || (uint)start > (uint)end)
{
throw new ArgumentOutOfRangeException(nameof(length));
}

return (start, end - start);
}
}

internal static class RuntimeCompatibilityExtensions
{
public static string Substring(this string s, Range range)
{
var (offset, length) = range.GetOffsetAndLength(s.Length);
return s.Substring(offset, length);
}
}
#endif
63 changes: 63 additions & 0 deletions src/PostHog/Library/Polyfills/StringPolyfills.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#if NETSTANDARD2_0
// ReSharper disable once CheckNamespace
namespace System;

internal static class StringPolyfills
{
public static bool Contains(this string source, string value, StringComparison comparisonType)
{
return source.IndexOf(value, comparisonType) >= 0;
}

public static string Replace(this string source, string oldValue, string? newValue, StringComparison comparisonType)
{
if (string.IsNullOrEmpty(oldValue))
{
throw new ArgumentException("String cannot be of zero length.", nameof(oldValue));
}

if (comparisonType == StringComparison.Ordinal)
{
return source.Replace(oldValue, newValue ?? string.Empty);
}

var result = new Text.StringBuilder();
var searchIndex = 0;

while (searchIndex < source.Length)
{
var matchIndex = source.IndexOf(oldValue, searchIndex, comparisonType);
if (matchIndex < 0)
{
result.Append(source, searchIndex, source.Length - searchIndex);
break;
}

result.Append(source, searchIndex, matchIndex - searchIndex);
result.Append(newValue);
searchIndex = matchIndex + oldValue.Length;
}
Comment thread
haacked marked this conversation as resolved.

return result.ToString();
}

public static int GetHashCode(this string source, StringComparison comparisonType)
{
return GetStringComparer(comparisonType).GetHashCode(source);
}

static StringComparer GetStringComparer(StringComparison comparisonType)
{
return comparisonType switch
{
StringComparison.CurrentCulture => StringComparer.CurrentCulture,
StringComparison.CurrentCultureIgnoreCase => StringComparer.CurrentCultureIgnoreCase,
StringComparison.InvariantCulture => StringComparer.InvariantCulture,
StringComparison.InvariantCultureIgnoreCase => StringComparer.InvariantCultureIgnoreCase,
StringComparison.Ordinal => StringComparer.Ordinal,
StringComparison.OrdinalIgnoreCase => StringComparer.OrdinalIgnoreCase,
_ => throw new ArgumentException($"Unknown StringComparison value: {comparisonType}", nameof(comparisonType))
};
}
}
#endif
6 changes: 5 additions & 1 deletion src/PostHog/PostHog.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.1;net8.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0</TargetFrameworks>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageId>PostHog</PackageId>
<Description>A library for interacting with the PostHog Analytics Platform.</Description>
Expand All @@ -20,6 +20,10 @@
<PackageReference Include="Microsoft.Bcl.TimeProvider" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
</ItemGroup>

<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="\"/>
Expand Down
5 changes: 3 additions & 2 deletions tests/TestLibrary/Fakes/FakeHttpMessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,9 @@ async Task<bool> Predicate(HttpRequestMessage request) =>
public async Task<HttpResponseMessage> Respond(HttpRequestMessage requestMessage)
{
Debug.Assert(requestMessage != null, nameof(requestMessage) + " != null");
_receivedRequests.Add(requestMessage);
if (requestMessage.Content is not null)
// Null-forgiving: Debug.Assert ensures non-null, but ns2.0 lacks [DoesNotReturnIf] annotation
_receivedRequests.Add(requestMessage!);
if (requestMessage!.Content is not null)
Comment thread
haacked marked this conversation as resolved.
{
_receivedBodiesJson.Add(await requestMessage.Content.ReadAsStringAsync());
}
Expand Down
2 changes: 1 addition & 1 deletion tests/TestLibrary/Fakes/FakeLoggerProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ bool StateMatches(object? argState, IReadOnlyDictionary<string, object> expected
{
return false;
}
var argDict = new Dictionary<string, object>(pairs);
var argDict = pairs.ToDictionary(kv => kv.Key, kv => kv.Value);
foreach (var (expectedKey, expectedValue) in expectedParameters)
{
if (!argDict.TryGetValue(expectedKey, out var actualValue)
Expand Down
12 changes: 12 additions & 0 deletions tests/TestLibrary/Fakes/Polyfills/StringPolyfills.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#if NETSTANDARD2_0
// ReSharper disable once CheckNamespace
namespace System;

internal static class StringPolyfills
{
public static int IndexOf(this string source, char value, StringComparison comparisonType)
{
return source.IndexOf(value.ToString(), comparisonType);
}
}
#endif
4 changes: 2 additions & 2 deletions tests/TestLibrary/TestLibrary.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.1;net8.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
Expand All @@ -18,7 +18,7 @@
<PackageReference Include="xunit" Version="2.9.2"/>
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' != 'netstandard2.1'">
<ItemGroup Condition="!$(TargetFramework.StartsWith('netstandard'))">
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="9.0.0"/>
</ItemGroup>

Expand Down
Loading
Loading