From 7060ae3ea21868c01ee280a73fec0b34237d8ef7 Mon Sep 17 00:00:00 2001 From: Daniel <95646168+daniel-statsig@users.noreply.github.com> Date: Wed, 8 May 2024 11:03:23 -0700 Subject: [PATCH 01/13] #39 Cleanup dependencies for newer TFM's (#142) Co-authored-by: James Thompson --- dotnet-statsig/dotnet-statsig.csproj | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/dotnet-statsig/dotnet-statsig.csproj b/dotnet-statsig/dotnet-statsig.csproj index 8ed9ab9..b291e70 100644 --- a/dotnet-statsig/dotnet-statsig.csproj +++ b/dotnet-statsig/dotnet-statsig.csproj @@ -1,4 +1,4 @@ - + netstandard2.0;net6.0;net471 @@ -26,8 +26,11 @@ - + + + + From 551d883aa55bbaa453fc4609d55a7947e11a4acf Mon Sep 17 00:00:00 2001 From: Daniel <95646168+daniel-statsig@users.noreply.github.com> Date: Wed, 8 May 2024 15:15:28 -0700 Subject: [PATCH 02/13] Chore/#38 package metadata (#141) Co-authored-by: James Thompson --- StatsigRedis/StatsigRedis.csproj | 7 ++----- dotnet-statsig/dotnet-statsig.csproj | 7 ++----- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/StatsigRedis/StatsigRedis.csproj b/StatsigRedis/StatsigRedis.csproj index 2fba169..8e31daf 100644 --- a/StatsigRedis/StatsigRedis.csproj +++ b/StatsigRedis/StatsigRedis.csproj @@ -9,8 +9,9 @@ 1.0.0 Statsig Inc. true + https://github.com/statsig-io/dotnet-sdk.git Statsig Inc., - LICENSE + ISC https://github.com/statsig-io/dotnet-sdk feature flags; feature gates; a/b testing; experimentation Statsig Redis Data Store @@ -24,8 +25,4 @@ - - - - diff --git a/dotnet-statsig/dotnet-statsig.csproj b/dotnet-statsig/dotnet-statsig.csproj index b291e70..0edd599 100644 --- a/dotnet-statsig/dotnet-statsig.csproj +++ b/dotnet-statsig/dotnet-statsig.csproj @@ -10,8 +10,9 @@ 1.25.0 Statsig Inc. true + https://github.com/statsig-io/dotnet-sdk.git Statsig Inc., - LICENSE + ISC https://github.com/statsig-io/dotnet-sdk feature flags; feature gates; a/b testing; experimentation Statsig .NET SDK @@ -22,10 +23,6 @@ - - - - From 878ad86590b963d31cc9e0cd529ab802a4407ec6 Mon Sep 17 00:00:00 2001 From: sroyal-statsig <76536058+sroyal-statsig@users.noreply.github.com> Date: Thu, 16 May 2024 12:52:02 -0700 Subject: [PATCH 03/13] Log Event Failure Logging (#143) --- .../Common/EventLoggerTest.cs | 4 +- .../Server/ErrorBoundaryUsageTest.cs | 2 +- .../src/Statsig/Client/ClientDriver.cs | 1 + .../src/Statsig/Lib/ErrorBoundary.cs | 8 ++-- .../src/Statsig/Network/EventLogger.cs | 26 +++++++++++- .../src/Statsig/Network/RequestDispatcher.cs | 42 +++++++++++++++---- .../src/Statsig/Server/ServerDriver.cs | 5 ++- .../src/Statsig/Server/StatsigServer.cs | 2 +- 8 files changed, 70 insertions(+), 20 deletions(-) diff --git a/dotnet-statsig-tests/Common/EventLoggerTest.cs b/dotnet-statsig-tests/Common/EventLoggerTest.cs index 1dde510..eeba256 100644 --- a/dotnet-statsig-tests/Common/EventLoggerTest.cs +++ b/dotnet-statsig-tests/Common/EventLoggerTest.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Newtonsoft.Json.Linq; using Statsig; +using Statsig.Lib; using Statsig.Network; using WireMock; using WireMock.RequestBuilders; @@ -33,7 +34,8 @@ public Task InitializeAsync() var sdkDetails = SDKDetails.GetClientSDKDetails(); var dispatcher = new RequestDispatcher("a-key", new StatsigOptions(apiUrlBase: _server.Urls[0]), sdkDetails, "my-session"); - _logger = new EventLogger(dispatcher, sdkDetails, maxQueueLength: 3, maxThresholdSecs: ThresholdSeconds); + var errorBoundary = new ErrorBoundary("a-key", SDKDetails.GetServerSDKDetails()); + _logger = new EventLogger(dispatcher, sdkDetails, maxQueueLength: 3, maxThresholdSecs: ThresholdSeconds, errorBoundary); _onLogCountdown = new CountdownEvent(1); return Task.CompletedTask; } diff --git a/dotnet-statsig-tests/Server/ErrorBoundaryUsageTest.cs b/dotnet-statsig-tests/Server/ErrorBoundaryUsageTest.cs index 4060752..73a9970 100644 --- a/dotnet-statsig-tests/Server/ErrorBoundaryUsageTest.cs +++ b/dotnet-statsig-tests/Server/ErrorBoundaryUsageTest.cs @@ -34,9 +34,9 @@ Task IAsyncLifetime.InitializeAsync() _requests = new List(); _server = WireMockServer.Start(); _server.Given(Request.Create().WithPath("*").UsingAnyMethod()).RespondWith(this); - ErrorBoundary.ExceptionEndpoint = $"{_server.Urls[0]}/v1/sdk_exception"; _statsig = new ServerDriver("secret-key"); + _statsig._errorBoundary.ExceptionEndpoint = $"{_server.Urls[0]}/v1/sdk_exception"; return Task.CompletedTask; } diff --git a/dotnet-statsig/src/Statsig/Client/ClientDriver.cs b/dotnet-statsig/src/Statsig/Client/ClientDriver.cs index 3b7a89a..eaa3b71 100644 --- a/dotnet-statsig/src/Statsig/Client/ClientDriver.cs +++ b/dotnet-statsig/src/Statsig/Client/ClientDriver.cs @@ -64,6 +64,7 @@ public ClientDriver(string clientKey, StatsigOptions? options = null) sdkDetails, clientOpts?.LoggingBufferMaxSize ?? Constants.CLIENT_MAX_LOGGER_QUEUE_LENGTH, clientOpts?.LoggingIntervalSeconds ?? Constants.CLIENT_MAX_LOGGER_WAIT_TIME_IN_SEC, + null, Constants.CLIENT_DEDUPE_INTERVAL ); _user = new StatsigUser(); diff --git a/dotnet-statsig/src/Statsig/Lib/ErrorBoundary.cs b/dotnet-statsig/src/Statsig/Lib/ErrorBoundary.cs index 27fad7d..f9a6b73 100644 --- a/dotnet-statsig/src/Statsig/Lib/ErrorBoundary.cs +++ b/dotnet-statsig/src/Statsig/Lib/ErrorBoundary.cs @@ -9,7 +9,7 @@ namespace Statsig.Lib { public class ErrorBoundary { - internal static string ExceptionEndpoint = "https://statsigapi.net/v1/sdk_exception"; + public string ExceptionEndpoint = "https://statsigapi.net/v1/sdk_exception"; private string _sdkKey; private SDKDetails _sdkDetails; @@ -81,7 +81,7 @@ private T OnCaught(string tag, Exception ex, Func recover) return recover(); } - public async void LogException(string tag, Exception ex, Dictionary? extra = null) + public async void LogException(string tag, Exception ex, Dictionary? extra = null, bool force = false) { try { @@ -91,7 +91,7 @@ public async void LogException(string tag, Exception ex, Dictionary()} + { "extra", extra ?? new Dictionary()} }); request.Content = new StringContent(body, Encoding.UTF8, "application/json"); diff --git a/dotnet-statsig/src/Statsig/Network/EventLogger.cs b/dotnet-statsig/src/Statsig/Network/EventLogger.cs index a93e3a3..c828258 100644 --- a/dotnet-statsig/src/Statsig/Network/EventLogger.cs +++ b/dotnet-statsig/src/Statsig/Network/EventLogger.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Statsig.Lib; namespace Statsig.Network { @@ -14,6 +15,7 @@ public class EventLogger private readonly SDKDetails _sdkDetails; private readonly RequestDispatcher _dispatcher; private readonly Dictionary _statsigMetadata; + private readonly ErrorBoundary? _errorBoundary; private readonly Task _backgroundPeriodicFlushTask; private readonly CancellationTokenSource _shutdownCTS; @@ -28,7 +30,7 @@ public class EventLogger private DateTime _dedupeStartTime; public EventLogger(RequestDispatcher dispatcher, SDKDetails sdkDetails, int maxQueueLength, - int maxThresholdSecs, int dedupeInterval = 60 * 1000) + int maxThresholdSecs, ErrorBoundary? errorBoundary = null, int dedupeInterval = 60 * 1000) { _sdkDetails = sdkDetails; _maxQueueLength = maxQueueLength; @@ -38,6 +40,7 @@ public EventLogger(RequestDispatcher dispatcher, SDKDetails sdkDetails, int maxQ ["sdkType"] = _sdkDetails.SDKType, ["sdkVersion"] = _sdkDetails.SDKVersion, }; + _errorBoundary = errorBoundary; _eventLogQueue = new List(); _errorsLogged = new HashSet(); @@ -157,7 +160,26 @@ internal async Task FlushEvents() ["events"] = snapshot }; - await _dispatcher.Fetch("log_event", body, 5, 1); + var additionalHeaders = new Dictionary + { + ["STATSIG-EVENT-COUNT"] = String.Format("{0}", snapshot.Count) + }; + + var status = await _dispatcher.FetchStatus("log_event", body, 5, 1, 0, additionalHeaders); + if (status != InitializeResult.Success) + { + var message = String.Format("Failed to post {0} logs after {1} retries, dropping the request", snapshot.Count, 5); + System.Diagnostics.Debug.WriteLine(message); + if (this._errorBoundary != null) + { + var extra = new Dictionary + { + ["eventCount"] = snapshot.Count, + ["error"] = message + }; + this._errorBoundary.LogException("statsig::log_event_failed", new Exception(message), extra, true); + } + } } public async Task Shutdown() diff --git a/dotnet-statsig/src/Statsig/Network/RequestDispatcher.cs b/dotnet-statsig/src/Statsig/Network/RequestDispatcher.cs index f78111c..74f66a7 100644 --- a/dotnet-statsig/src/Statsig/Network/RequestDispatcher.cs +++ b/dotnet-statsig/src/Statsig/Network/RequestDispatcher.cs @@ -51,18 +51,32 @@ string sessionID IReadOnlyDictionary body, int retries = 0, int backoff = 1, - int timeoutInMs = 0) + int timeoutInMs = 0, + IReadOnlyDictionary? additionalHeaders = null) { - var (result, status) = await FetchAsString(endpoint, body, retries, backoff, timeoutInMs).ConfigureAwait(false); + var (result, status) = await FetchAsString(endpoint, body, retries, backoff, timeoutInMs, additionalHeaders).ConfigureAwait(false); return JsonConvert.DeserializeObject>(result ?? ""); } + public async Task FetchStatus( + string endpoint, + IReadOnlyDictionary body, + int retries = 0, + int backoff = 1, + int timeoutInMs = 0, + IReadOnlyDictionary? additionalHeaders = null) + { + var (result, status) = await FetchAsString(endpoint, body, retries, backoff, timeoutInMs, additionalHeaders).ConfigureAwait(false); + return status; + } + public async Task<(string?, InitializeResult)> FetchAsString( string endpoint, IReadOnlyDictionary body, int retries = 0, int backoff = 1, - int timeoutInMs = 0) + int timeoutInMs = 0, + IReadOnlyDictionary? additionalHeaders = null) { if (_options is StatsigServerOptions { LocalMode: true }) { @@ -95,6 +109,14 @@ string sessionID request.Headers.Add(kv.Key, kv.Value); } + if (additionalHeaders != null) + { + foreach (var kv in additionalHeaders) + { + request.Headers.Add(kv.Key, kv.Value); + } + } + if (timeoutInMs > 0) { client.Timeout = TimeSpan.FromMilliseconds(timeoutInMs); @@ -114,14 +136,14 @@ string sessionID if (retries > 0 && RetryCodes.Contains((int)response.StatusCode)) { - return await Retry(endpoint, body, retries, backoff).ConfigureAwait(false); + return await Retry(endpoint, body, retries, backoff, timeoutInMs, additionalHeaders).ConfigureAwait(false); } } catch (TaskCanceledException) { if (retries > 0) { - return await Retry(endpoint, body, retries, backoff).ConfigureAwait(false); + return await Retry(endpoint, body, retries, backoff, timeoutInMs, additionalHeaders).ConfigureAwait(false); } else { @@ -133,7 +155,7 @@ string sessionID { if (retries > 0) { - return await Retry(endpoint, body, retries, backoff).ConfigureAwait(false); + return await Retry(endpoint, body, retries, backoff, timeoutInMs, additionalHeaders).ConfigureAwait(false); } else { @@ -145,7 +167,7 @@ string sessionID { if (retries > 0) { - return await Retry(endpoint, body, retries, backoff).ConfigureAwait(false); + return await Retry(endpoint, body, retries, backoff, timeoutInMs, additionalHeaders).ConfigureAwait(false); } } @@ -156,10 +178,12 @@ string sessionID string endpoint, IReadOnlyDictionary body, int retries = 0, - int backoff = 1) + int backoff = 1, + int timeoutInMs = 0, + IReadOnlyDictionary? additionalHeaders = null) { await Task.Delay(backoff * 1000).ConfigureAwait(false); - return await FetchAsString(endpoint, body, retries - 1, backoff * BackoffMultiplier).ConfigureAwait(false); + return await FetchAsString(endpoint, body, retries - 1, backoff * BackoffMultiplier, timeoutInMs, additionalHeaders).ConfigureAwait(false); } } } \ No newline at end of file diff --git a/dotnet-statsig/src/Statsig/Server/ServerDriver.cs b/dotnet-statsig/src/Statsig/Server/ServerDriver.cs index 4f6a826..c85c9c8 100644 --- a/dotnet-statsig/src/Statsig/Server/ServerDriver.cs +++ b/dotnet-statsig/src/Statsig/Server/ServerDriver.cs @@ -28,7 +28,7 @@ public class ServerDriver : IDisposable internal EventLogger _eventLogger; internal Evaluator evaluator; - private ErrorBoundary _errorBoundary; + public ErrorBoundary _errorBoundary; private readonly string _sessionID = Guid.NewGuid().ToString(); public ServerDriver(string serverSecret, StatsigOptions? options = null) @@ -49,6 +49,7 @@ public ServerDriver(string serverSecret, StatsigOptions? options = null) sdkDetails, serverOpts?.LoggingBufferMaxSize ?? Constants.SERVER_MAX_LOGGER_QUEUE_LENGTH, serverOpts?.LoggingIntervalSeconds ?? Constants.SERVER_MAX_LOGGER_WAIT_TIME_IN_SEC, + _errorBoundary, Constants.SERVER_DEDUPE_INTERVAL ); evaluator = new Evaluator(options, _requestDispatcher, serverSecret, _errorBoundary); @@ -279,7 +280,7 @@ public Dictionary GenerateInitializeResponse(StatsigUser user, s var allEvals = evaluator.GetAllEvaluations(user, clientSDKKey, hash, includeLocalOverrides) ?? new Dictionary(); if (!allEvals.ContainsKey("feature_gates")) { - var extra = new Dictionary(); + var extra = new Dictionary(); if (clientSDKKey != null) { extra["clientSDKKey"] = clientSDKKey; diff --git a/dotnet-statsig/src/Statsig/Server/StatsigServer.cs b/dotnet-statsig/src/Statsig/Server/StatsigServer.cs index 23eeff2..80c0d71 100644 --- a/dotnet-statsig/src/Statsig/Server/StatsigServer.cs +++ b/dotnet-statsig/src/Statsig/Server/StatsigServer.cs @@ -8,7 +8,7 @@ namespace Statsig.Server { public static class StatsigServer { - static ServerDriver? _singleDriver; + public static ServerDriver? _singleDriver; public static async Task Initialize(string serverSecret, StatsigOptions? options = null) From 74b0d368610f94f6574ab612c8139521746b32a0 Mon Sep 17 00:00:00 2001 From: sroyal-statsig <76536058+sroyal-statsig@users.noreply.github.com> Date: Tue, 21 May 2024 10:47:57 -0700 Subject: [PATCH 04/13] Gzip Log Event Body (#144) linear: https://linear.app/statsig/issue/SDK-395/net-sdkflags-log-event-compression kong: https://github.com/statsig-io/kong/pull/2130 --------- Co-authored-by: Daniel <95646168+daniel-statsig@users.noreply.github.com> --- .../src/Statsig/Client/ClientDriver.cs | 5 +- .../src/Statsig/Network/EventLogger.cs | 3 +- .../src/Statsig/Network/RequestDispatcher.cs | 73 +++++++++++++++---- .../Statsig/Server/Evaluation/SpecStore.cs | 8 +- 4 files changed, 67 insertions(+), 22 deletions(-) diff --git a/dotnet-statsig/src/Statsig/Client/ClientDriver.cs b/dotnet-statsig/src/Statsig/Client/ClientDriver.cs index eaa3b71..8eb7c1a 100644 --- a/dotnet-statsig/src/Statsig/Client/ClientDriver.cs +++ b/dotnet-statsig/src/Statsig/Client/ClientDriver.cs @@ -11,6 +11,7 @@ using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Statsig.Client.Storage; using Statsig.Network; @@ -91,11 +92,11 @@ public async Task Initialize(StatsigUser? user) _user.statsigEnvironment = _options.StatsigEnvironment.Values; var response = await _requestDispatcher.Fetch( "initialize", - new Dictionary + JsonConvert.SerializeObject(new Dictionary { ["user"] = _user, ["statsigMetadata"] = GetStatsigMetadata(), - }, + }), timeoutInMs: _options.ClientRequestTimeoutMs ).ConfigureAwait(false); if (response == null) diff --git a/dotnet-statsig/src/Statsig/Network/EventLogger.cs b/dotnet-statsig/src/Statsig/Network/EventLogger.cs index c828258..34de1b4 100644 --- a/dotnet-statsig/src/Statsig/Network/EventLogger.cs +++ b/dotnet-statsig/src/Statsig/Network/EventLogger.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Newtonsoft.Json; using Statsig.Lib; namespace Statsig.Network @@ -165,7 +166,7 @@ internal async Task FlushEvents() ["STATSIG-EVENT-COUNT"] = String.Format("{0}", snapshot.Count) }; - var status = await _dispatcher.FetchStatus("log_event", body, 5, 1, 0, additionalHeaders); + var status = await _dispatcher.FetchStatus("log_event", JsonConvert.SerializeObject(body), 5, 1, 0, additionalHeaders, true); if (status != InitializeResult.Success) { var message = String.Format("Failed to post {0} logs after {1} retries, dropping the request", snapshot.Count, 5); diff --git a/dotnet-statsig/src/Statsig/Network/RequestDispatcher.cs b/dotnet-statsig/src/Statsig/Network/RequestDispatcher.cs index 74f66a7..49af229 100644 --- a/dotnet-statsig/src/Statsig/Network/RequestDispatcher.cs +++ b/dotnet-statsig/src/Statsig/Network/RequestDispatcher.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; +using System.IO; +using System.IO.Compression; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; @@ -48,7 +51,7 @@ string sessionID public async Task?> Fetch( string endpoint, - IReadOnlyDictionary body, + string body, int retries = 0, int backoff = 1, int timeoutInMs = 0, @@ -60,23 +63,53 @@ string sessionID public async Task FetchStatus( string endpoint, - IReadOnlyDictionary body, + string body, int retries = 0, int backoff = 1, int timeoutInMs = 0, - IReadOnlyDictionary? additionalHeaders = null) + IReadOnlyDictionary? additionalHeaders = null, + bool zipped = false) { - var (result, status) = await FetchAsString(endpoint, body, retries, backoff, timeoutInMs, additionalHeaders).ConfigureAwait(false); + var (result, status) = await FetchAsString(endpoint, body, retries, backoff, timeoutInMs, additionalHeaders, zipped).ConfigureAwait(false); return status; } + public static byte[] Zip(string str) + { + var bytes = Encoding.UTF8.GetBytes(str); + + using (var msi = new MemoryStream(bytes)) + using (var mso = new MemoryStream()) + { + using (var gs = new GZipStream(mso, CompressionMode.Compress)) + { + CopyTo(msi, gs); + } + + return mso.ToArray(); + } + } + + public static void CopyTo(Stream src, Stream dest) + { + byte[] bytes = new byte[4096]; + + int cnt; + + while ((cnt = src.Read(bytes, 0, bytes.Length)) != 0) + { + dest.Write(bytes, 0, cnt); + } + } + public async Task<(string?, InitializeResult)> FetchAsString( string endpoint, - IReadOnlyDictionary body, + string body, int retries = 0, int backoff = 1, int timeoutInMs = 0, - IReadOnlyDictionary? additionalHeaders = null) + IReadOnlyDictionary? additionalHeaders = null, + bool zipped = false) { if (_options is StatsigServerOptions { LocalMode: true }) { @@ -91,8 +124,17 @@ public async Task FetchStatus( AutomaticDecompression = DecompressionMethods.GZip }); using var request = new HttpRequestMessage(HttpMethod.Post, url); - var bodyJson = JsonConvert.SerializeObject(body); - request.Content = new StringContent(bodyJson, Encoding.UTF8, "application/json"); + if (zipped) + { + var zippedBody = Zip(body); + request.Content = new ByteArrayContent(zippedBody); + request.Content.Headers.Add("Content-Encoding", "gzip"); + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + } + else + { + request.Content = new StringContent(body, Encoding.UTF8, "application/json"); + } request.Headers.Add("STATSIG-API-KEY", Key); request.Headers.Add("STATSIG-CLIENT-TIME", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString()); @@ -136,14 +178,14 @@ public async Task FetchStatus( if (retries > 0 && RetryCodes.Contains((int)response.StatusCode)) { - return await Retry(endpoint, body, retries, backoff, timeoutInMs, additionalHeaders).ConfigureAwait(false); + return await Retry(endpoint, body, retries, backoff, timeoutInMs, additionalHeaders, zipped).ConfigureAwait(false); } } catch (TaskCanceledException) { if (retries > 0) { - return await Retry(endpoint, body, retries, backoff, timeoutInMs, additionalHeaders).ConfigureAwait(false); + return await Retry(endpoint, body, retries, backoff, timeoutInMs, additionalHeaders, zipped).ConfigureAwait(false); } else { @@ -155,7 +197,7 @@ public async Task FetchStatus( { if (retries > 0) { - return await Retry(endpoint, body, retries, backoff, timeoutInMs, additionalHeaders).ConfigureAwait(false); + return await Retry(endpoint, body, retries, backoff, timeoutInMs, additionalHeaders, zipped).ConfigureAwait(false); } else { @@ -167,7 +209,7 @@ public async Task FetchStatus( { if (retries > 0) { - return await Retry(endpoint, body, retries, backoff, timeoutInMs, additionalHeaders).ConfigureAwait(false); + return await Retry(endpoint, body, retries, backoff, timeoutInMs, additionalHeaders, zipped).ConfigureAwait(false); } } @@ -176,14 +218,15 @@ public async Task FetchStatus( private async Task<(string?, InitializeResult)> Retry( string endpoint, - IReadOnlyDictionary body, + string body, int retries = 0, int backoff = 1, int timeoutInMs = 0, - IReadOnlyDictionary? additionalHeaders = null) + IReadOnlyDictionary? additionalHeaders = null, + bool zipped = false) { await Task.Delay(backoff * 1000).ConfigureAwait(false); - return await FetchAsString(endpoint, body, retries - 1, backoff * BackoffMultiplier, timeoutInMs, additionalHeaders).ConfigureAwait(false); + return await FetchAsString(endpoint, body, retries - 1, backoff * BackoffMultiplier, timeoutInMs, additionalHeaders, zipped).ConfigureAwait(false); } } } \ No newline at end of file diff --git a/dotnet-statsig/src/Statsig/Server/Evaluation/SpecStore.cs b/dotnet-statsig/src/Statsig/Server/Evaluation/SpecStore.cs index b89fe64..6f3123b 100644 --- a/dotnet-statsig/src/Statsig/Server/Evaluation/SpecStore.cs +++ b/dotnet-statsig/src/Statsig/Server/Evaluation/SpecStore.cs @@ -339,10 +339,10 @@ private async Task SyncIDLists(IReadOnlyDictionary idListMap) private async Task SyncIDLists() { - var response = await _requestDispatcher.Fetch("get_id_lists", new Dictionary + var response = await _requestDispatcher.Fetch("get_id_lists", JsonConvert.SerializeObject(new Dictionary { ["statsigMetadata"] = SDKDetails.GetServerSDKDetails().StatsigMetadata - }).ConfigureAwait(false); + })).ConfigureAwait(false); if (response == null || response.Count == 0) { return; @@ -355,11 +355,11 @@ private async Task SyncValuesFromNetwork() { var (response, status) = await _requestDispatcher.FetchAsString( "download_config_specs", - new Dictionary + JsonConvert.SerializeObject(new Dictionary { ["sinceTime"] = LastSyncTime, ["statsigMetadata"] = SDKDetails.GetServerSDKDetails().StatsigMetadata - } + }) ).ConfigureAwait(false); var hasUpdates = ParseResponse(response); From aa736908b206749e8a8f03d98c57e244abc9eb52 Mon Sep 17 00:00:00 2001 From: sroyal-statsig <76536058+sroyal-statsig@users.noreply.github.com> Date: Thu, 6 Jun 2024 10:01:40 -0700 Subject: [PATCH 05/13] Filter Segment Exposures (#146) --- .../Statsig/Server/Evaluation/Evaluator.cs | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/dotnet-statsig/src/Statsig/Server/Evaluation/Evaluator.cs b/dotnet-statsig/src/Statsig/Server/Evaluation/Evaluator.cs index 8bb0e21..55b0ccd 100644 --- a/dotnet-statsig/src/Statsig/Server/Evaluation/Evaluator.cs +++ b/dotnet-statsig/src/Statsig/Server/Evaluation/Evaluator.cs @@ -232,7 +232,7 @@ internal List GetSpecNames(string type) ["name"] = hashedName, ["value"] = gate.Value, ["rule_id"] = gate.RuleID, - ["secondary_exposures"] = CleanExposures(gate.SecondaryExposures), + ["secondary_exposures"] = CleanExposures(gate.SecondaryExposures).ToArray(), }; gates.Add(hashedName, entry); } @@ -291,7 +291,7 @@ internal List GetSpecNames(string type) } entry["undelegated_secondary_exposures"] = - CleanExposures(evaluation.UndelegatedSecondaryExposures); + CleanExposures(evaluation.UndelegatedSecondaryExposures).ToArray(); layerConfigs.Add(hashedName, entry); } @@ -343,8 +343,8 @@ private ConfigEvaluation EvaluateSpec(StatsigUser user, string specName, SpecTyp return Evaluate(user, lookup[name], 0); } - private IEnumerable> CleanExposures( - IEnumerable> exposures + private List> CleanExposures( + List> exposures ) { if (exposures == null) @@ -356,6 +356,10 @@ IEnumerable> exposures return exposures.Select((exp) => { var gate = exp["gate"]; + if (gate.StartsWith("segment:")) + { + return null; + } var gateValue = exp["gateValue"]; var ruleID = exp["ruleID"]; var key = $"{gate}|{gateValue}|{ruleID}"; @@ -366,7 +370,7 @@ IEnumerable> exposures seen.Add(key); return exp; - }).Where(exp => exp != null).Select(exp => exp!).ToArray(); + }).Where(exp => exp != null).Select(exp => exp!).ToList(); } private bool IsUserAllocatedToExperiment( @@ -398,7 +402,7 @@ DynamicConfig config ["group"] = config.RuleID, ["is_device_based"] = (spec.IDType != null && spec.IDType.ToLowerInvariant() == "stableid"), - ["secondary_exposures"] = CleanExposures(config.SecondaryExposures), + ["secondary_exposures"] = CleanExposures(config.SecondaryExposures).ToArray(), }; return entry; @@ -438,7 +442,7 @@ private string HashName(string? name = "", string? hashAlgo = "sha256") UndelegatedSecondaryExposures = exposures }; result.ConfigValue.SecondaryExposures = - exposures.Concat(delegatedResult.ConfigValue.SecondaryExposures).ToList(); + CleanExposures(exposures.Concat(delegatedResult.ConfigValue.SecondaryExposures).ToList()); result.ConfigDelegate = rule.ConfigDelegate; return result; } @@ -485,7 +489,7 @@ private ConfigEvaluation Evaluate(StatsigUser user, ConfigSpec spec, int depth) spec.Name, passPercentage ? rule.FeatureGateValue.Value : spec.FeatureGateDefault.Value, rule.ID, - secondaryExposures, + CleanExposures(secondaryExposures), _store.EvalReason ); var configV = new DynamicConfig @@ -494,7 +498,7 @@ private ConfigEvaluation Evaluate(StatsigUser user, ConfigSpec spec, int depth) passPercentage ? rule.DynamicConfigValue.Value : spec.DynamicConfigDefault.Value, rule.ID, rule.GroupName, - secondaryExposures, + CleanExposures(secondaryExposures), spec.ExplicitParameters, spec.HasSharedParams, IsUserAllocatedToExperiment(user, spec, rule.ID) @@ -511,8 +515,8 @@ private ConfigEvaluation Evaluate(StatsigUser user, ConfigSpec spec, int depth) ( EvaluationResult.Fail, _store.EvalReason, - new FeatureGate(spec.Name, spec.FeatureGateDefault.Value, "default", secondaryExposures), - new DynamicConfig(spec.Name, spec.DynamicConfigDefault.Value, "default", null, secondaryExposures, + new FeatureGate(spec.Name, spec.FeatureGateDefault.Value, "default", CleanExposures(secondaryExposures)), + new DynamicConfig(spec.Name, spec.DynamicConfigDefault.Value, "default", null, CleanExposures(secondaryExposures), spec.ExplicitParameters) ); } @@ -619,15 +623,17 @@ private EvaluationResult EvaluateCondition(StatsigUser user, ConfigCondition con } var pass = otherGateResult.Result == EvaluationResult.Pass; - var newExposure = new Dictionary + secondaryExposures = new List>(otherGateResult.GateValue.SecondaryExposures); + if (!targetStr.StartsWith("segment:")) { - ["gate"] = targetStr, - ["gateValue"] = pass ? "true" : "false", - ["ruleID"] = otherGateResult.GateValue.RuleID - }; - secondaryExposures = - new List>(otherGateResult.GateValue.SecondaryExposures); - secondaryExposures.Add(newExposure); + var newExposure = new Dictionary + { + ["gate"] = targetStr, + ["gateValue"] = pass ? "true" : "false", + ["ruleID"] = otherGateResult.GateValue.RuleID + }; + secondaryExposures.Add(newExposure); + } if ((type == "pass_gate" && pass) || (type == "fail_gate" && !pass)) { return EvaluationResult.Pass; From 5244a4e6ef69f828927094591a5444dafa5f86f9 Mon Sep 17 00:00:00 2001 From: sroyal-statsig <76536058+sroyal-statsig@users.noreply.github.com> Date: Tue, 11 Jun 2024 10:18:53 -0700 Subject: [PATCH 06/13] [release] 1.26.0 - Compress Log Event Body (#148) Adds compression of the log event body Improves logging for cases where log event fails Updates package metadata >Included In This Release >- aa736908b206749e8a8f03d98c57e244abc9eb52 sroyal-statsig > - Filter Segment Exposures (#146) >- 74b0d368610f94f6574ab612c8139521746b32a0 sroyal-statsig > - Gzip Log Event Body (#144) >- 878ad86590b963d31cc9e0cd529ab802a4407ec6 sroyal-statsig > - Log Event Failure Logging (#143) >- 551d883aa55bbaa453fc4609d55a7947e11a4acf Daniel > - Chore/#38 package metadata (#141) >- 7060ae3ea21868c01ee280a73fec0b34237d8ef7 Daniel > - #39 Cleanup dependencies for newer TFM's (#142) --------- Co-authored-by: statsig-kong[bot] --- dotnet-statsig-tests/Common/StatsigTest.cs | 2 +- dotnet-statsig/dotnet-statsig.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet-statsig-tests/Common/StatsigTest.cs b/dotnet-statsig-tests/Common/StatsigTest.cs index 7a030b4..427066e 100644 --- a/dotnet-statsig-tests/Common/StatsigTest.cs +++ b/dotnet-statsig-tests/Common/StatsigTest.cs @@ -21,7 +21,7 @@ public class StatsigTest : IAsyncLifetime { WireMockServer _server; - private const String ExpectedSdkVersion = "1.25.0.0"; + private const String ExpectedSdkVersion = "1.26.0.0"; Task IAsyncLifetime.InitializeAsync() { diff --git a/dotnet-statsig/dotnet-statsig.csproj b/dotnet-statsig/dotnet-statsig.csproj index 0edd599..fbfd9a7 100644 --- a/dotnet-statsig/dotnet-statsig.csproj +++ b/dotnet-statsig/dotnet-statsig.csproj @@ -7,7 +7,7 @@ statsig_dotnet true Statsig - 1.25.0 + 1.26.0 Statsig Inc. true https://github.com/statsig-io/dotnet-sdk.git From 053c3e88977b62b6135bdfa70c2314c1de7f3f47 Mon Sep 17 00:00:00 2001 From: sroyal-statsig <76536058+sroyal-statsig@users.noreply.github.com> Date: Mon, 17 Jun 2024 14:44:54 -0700 Subject: [PATCH 07/13] Eval Details (#147) --- .../Server/GetFeatureGateTest.cs | 6 +++ dotnet-statsig/src/Statsig/DynamicConfig.cs | 7 +++- dotnet-statsig/src/Statsig/FeatureGate.cs | 4 +- dotnet-statsig/src/Statsig/Layer.cs | 7 +++- .../Server/Evaluation/EvaluationDetails.cs | 22 +++++++++++ .../Statsig/Server/Evaluation/Evaluator.cs | 27 ++++++++----- .../Statsig/Server/Evaluation/SpecStore.cs | 10 +++++ .../src/Statsig/Server/ServerDriver.cs | 38 ++++++++++++------- 8 files changed, 94 insertions(+), 27 deletions(-) create mode 100644 dotnet-statsig/src/Statsig/Server/Evaluation/EvaluationDetails.cs diff --git a/dotnet-statsig-tests/Server/GetFeatureGateTest.cs b/dotnet-statsig-tests/Server/GetFeatureGateTest.cs index 1eb9424..63e5deb 100644 --- a/dotnet-statsig-tests/Server/GetFeatureGateTest.cs +++ b/dotnet-statsig-tests/Server/GetFeatureGateTest.cs @@ -86,10 +86,16 @@ public async void GetFeatureGate() Assert.True(gate.Value); Assert.Equal("7w9rbTSffLT89pxqpyhuqK", gate.RuleID); Assert.Equal(EvaluationReason.Network, gate.Reason); + Assert.Equal(EvaluationReason.Network, gate.EvaluationDetails?.Reason); + Assert.Equal(1631638014811, gate.EvaluationDetails?.ConfigSyncTime); + Assert.Equal(1631638014811, gate.EvaluationDetails?.InitTime); var gate2 = StatsigServer.GetFeatureGateWithExposureLoggingDisabled(user, "fake_gate"); Assert.False(gate2.Value); Assert.Equal(EvaluationReason.Unrecognized, gate2.Reason); + Assert.Equal(EvaluationReason.Unrecognized, gate2.EvaluationDetails?.Reason); + Assert.Equal(1631638014811, gate.EvaluationDetails?.ConfigSyncTime); + Assert.Equal(1631638014811, gate.EvaluationDetails?.InitTime); await StatsigServer.Shutdown(); diff --git a/dotnet-statsig/src/Statsig/DynamicConfig.cs b/dotnet-statsig/src/Statsig/DynamicConfig.cs index a19d198..26c386c 100644 --- a/dotnet-statsig/src/Statsig/DynamicConfig.cs +++ b/dotnet-statsig/src/Statsig/DynamicConfig.cs @@ -4,6 +4,7 @@ using Newtonsoft.Json; using Statsig.Lib; using Statsig.Server; +using Statsig.Server.Evaluation; namespace Statsig { @@ -26,6 +27,8 @@ public class DynamicConfig [JsonProperty("is_user_in_experiment")] public bool IsUserInExperiment { get; private set; } + public EvaluationDetails? EvaluationDetails { get; } + static DynamicConfig? _defaultConfig; @@ -50,7 +53,8 @@ public DynamicConfig( List>? secondaryExposures = null, List? explicitParameters = null, bool isInLayer = false, - bool isUserInExperiment = false + bool isUserInExperiment = false, + EvaluationDetails? details = null ) { ConfigName = configName ?? ""; @@ -61,6 +65,7 @@ public DynamicConfig( ExplicitParameters = explicitParameters ?? new List(); IsInLayer = isInLayer; IsUserInExperiment = isUserInExperiment; + EvaluationDetails = details; } public T? Get(string key, T? defaultValue = default(T)) diff --git a/dotnet-statsig/src/Statsig/FeatureGate.cs b/dotnet-statsig/src/Statsig/FeatureGate.cs index 3cd1ae5..9eb2ecb 100644 --- a/dotnet-statsig/src/Statsig/FeatureGate.cs +++ b/dotnet-statsig/src/Statsig/FeatureGate.cs @@ -16,6 +16,7 @@ public class FeatureGate [JsonProperty("secondary_exposures")] public List> SecondaryExposures { get; } public EvaluationReason? Reason { get; } + public EvaluationDetails? EvaluationDetails { get; } static FeatureGate? _defaultConfig; @@ -31,13 +32,14 @@ public static FeatureGate Default } } - public FeatureGate(string? name = null, bool value = false, string? ruleID = null, List>? secondaryExposures = null, EvaluationReason? reason = null) + public FeatureGate(string? name = null, bool value = false, string? ruleID = null, List>? secondaryExposures = null, EvaluationReason? reason = null, EvaluationDetails? details = null) { Name = name ?? ""; Value = value; RuleID = ruleID ?? ""; SecondaryExposures = secondaryExposures ?? new List>(); Reason = reason ?? EvaluationReason.Uninitialized; + EvaluationDetails = details; } internal static FeatureGate? FromJObject(string name, JObject? jobj) diff --git a/dotnet-statsig/src/Statsig/Layer.cs b/dotnet-statsig/src/Statsig/Layer.cs index 21d4da9..44353ab 100644 --- a/dotnet-statsig/src/Statsig/Layer.cs +++ b/dotnet-statsig/src/Statsig/Layer.cs @@ -4,6 +4,7 @@ using Newtonsoft.Json; using Statsig.Lib; using Statsig.Server; +using Statsig.Server.Evaluation; namespace Statsig { @@ -31,6 +32,8 @@ public class Layer static Layer? _default; + public EvaluationDetails? EvaluationDetails { get; } + public static Layer Default { get @@ -50,7 +53,8 @@ public Layer(string? name = null, string? allocatedExperimentName = null, List? explicitParameters = null, Action? onExposure = null, - string? groupName = null) + string? groupName = null, + EvaluationDetails? details = null) { Name = name ?? ""; Value = value ?? new Dictionary(); @@ -61,6 +65,7 @@ public Layer(string? name = null, ExplicitParameters = explicitParameters ?? new List(); AllocatedExperimentName = allocatedExperimentName ?? ""; GroupName = groupName; + EvaluationDetails = details; } public T? Get(string key, T? defaultValue = default(T)) diff --git a/dotnet-statsig/src/Statsig/Server/Evaluation/EvaluationDetails.cs b/dotnet-statsig/src/Statsig/Server/Evaluation/EvaluationDetails.cs new file mode 100644 index 0000000..6868f4d --- /dev/null +++ b/dotnet-statsig/src/Statsig/Server/Evaluation/EvaluationDetails.cs @@ -0,0 +1,22 @@ + +using System; +using System.Collections.Generic; + +namespace Statsig.Server.Evaluation +{ + public class EvaluationDetails + { + public EvaluationReason Reason { get; } + public long InitTime { get; } + public long ServerTime { get; } + public long ConfigSyncTime { get; } + + public EvaluationDetails(EvaluationReason reason, long initTime, long configSyncTime) + { + Reason = reason; + this.InitTime = initTime; + this.ServerTime = DateTime.Now.Millisecond; + this.ConfigSyncTime = configSyncTime; + } + } +} \ No newline at end of file diff --git a/dotnet-statsig/src/Statsig/Server/Evaluation/Evaluator.cs b/dotnet-statsig/src/Statsig/Server/Evaluation/Evaluator.cs index 55b0ccd..e4dc72d 100644 --- a/dotnet-statsig/src/Statsig/Server/Evaluation/Evaluator.cs +++ b/dotnet-statsig/src/Statsig/Server/Evaluation/Evaluator.cs @@ -97,13 +97,13 @@ internal void OverrideLayer(string layerName, Dictionary value, if (user.UserID != null && overrides.ContainsKey(user.UserID)) { return new ConfigEvaluation(EvaluationResult.Pass, EvaluationReason.LocalOverride, - new FeatureGate(gateName, overrides[user.UserID]!, "override", null, EvaluationReason.LocalOverride)); + new FeatureGate(gateName, overrides[user.UserID]!, "override", null, EvaluationReason.LocalOverride, _store.GetEvaluationDetails(EvaluationReason.LocalOverride))); } if (overrides.ContainsKey("")) { return new ConfigEvaluation(EvaluationResult.Pass, EvaluationReason.LocalOverride, - new FeatureGate(gateName, overrides[""]!, "override", null, EvaluationReason.LocalOverride)); + new FeatureGate(gateName, overrides[""]!, "override", null, EvaluationReason.LocalOverride, _store.GetEvaluationDetails(EvaluationReason.LocalOverride))); } return null; } @@ -142,13 +142,13 @@ internal void OverrideLayer(string layerName, Dictionary value, if (user.UserID != null && overrides.ContainsKey(user.UserID)) { return new ConfigEvaluation(EvaluationResult.Pass, EvaluationReason.LocalOverride, null, - new DynamicConfig(configName, overrides[user.UserID]!, "override")); + new DynamicConfig(configName, overrides[user.UserID]!, "override", null, null, null, false, false, _store.GetEvaluationDetails(EvaluationReason.LocalOverride))); } if (overrides.ContainsKey("")) { return new ConfigEvaluation(EvaluationResult.Pass, EvaluationReason.LocalOverride, null, - new DynamicConfig(configName, overrides[""]!, "override")); + new DynamicConfig(configName, overrides[""]!, "override", null, null, null, false, false, _store.GetEvaluationDetails(EvaluationReason.LocalOverride))); } return null; } @@ -192,6 +192,11 @@ internal List GetSpecNames(string type) return _store.GetSpecNames(type); } + internal EvaluationDetails GetEvaluationDetails(EvaluationReason? reason = null) + { + return _store.GetEvaluationDetails(reason); + } + internal Dictionary? GetAllEvaluations(StatsigUser user, string? clientSDKKey, string? hash, bool includeLocalOverrides = false) { if (_store.EvalReason == EvaluationReason.Uninitialized || _store.LastSyncTime == 0) @@ -460,9 +465,9 @@ private ConfigEvaluation Evaluate(StatsigUser user, ConfigSpec spec, int depth) ( EvaluationResult.Fail, _store.EvalReason, - new FeatureGate(spec.Name, spec.FeatureGateDefault.Value, "disabled", null, _store.EvalReason), + new FeatureGate(spec.Name, spec.FeatureGateDefault.Value, "disabled", null, _store.EvalReason, _store.GetEvaluationDetails()), new DynamicConfig(spec.Name, spec.DynamicConfigDefault.Value, "disabled", null, null, - spec.ExplicitParameters) + spec.ExplicitParameters, false, false, _store.GetEvaluationDetails()) ); } @@ -490,7 +495,8 @@ private ConfigEvaluation Evaluate(StatsigUser user, ConfigSpec spec, int depth) passPercentage ? rule.FeatureGateValue.Value : spec.FeatureGateDefault.Value, rule.ID, CleanExposures(secondaryExposures), - _store.EvalReason + _store.EvalReason, + _store.GetEvaluationDetails() ); var configV = new DynamicConfig ( @@ -501,7 +507,8 @@ private ConfigEvaluation Evaluate(StatsigUser user, ConfigSpec spec, int depth) CleanExposures(secondaryExposures), spec.ExplicitParameters, spec.HasSharedParams, - IsUserAllocatedToExperiment(user, spec, rule.ID) + IsUserAllocatedToExperiment(user, spec, rule.ID), + _store.GetEvaluationDetails() ); return new ConfigEvaluation(passPercentage ? EvaluationResult.Pass : EvaluationResult.Fail, _store.EvalReason, gateV, configV); @@ -515,9 +522,9 @@ private ConfigEvaluation Evaluate(StatsigUser user, ConfigSpec spec, int depth) ( EvaluationResult.Fail, _store.EvalReason, - new FeatureGate(spec.Name, spec.FeatureGateDefault.Value, "default", CleanExposures(secondaryExposures)), + new FeatureGate(spec.Name, spec.FeatureGateDefault.Value, "default", CleanExposures(secondaryExposures), _store.EvalReason, _store.GetEvaluationDetails()), new DynamicConfig(spec.Name, spec.DynamicConfigDefault.Value, "default", null, CleanExposures(secondaryExposures), - spec.ExplicitParameters) + spec.ExplicitParameters, false, false, _store.GetEvaluationDetails()) ); } diff --git a/dotnet-statsig/src/Statsig/Server/Evaluation/SpecStore.cs b/dotnet-statsig/src/Statsig/Server/Evaluation/SpecStore.cs index 6f3123b..b0b37d4 100644 --- a/dotnet-statsig/src/Statsig/Server/Evaluation/SpecStore.cs +++ b/dotnet-statsig/src/Statsig/Server/Evaluation/SpecStore.cs @@ -31,6 +31,8 @@ class SpecStore internal Dictionary SDKKeysToAppIDs { get; private set; } internal Dictionary HashedSDKKeysToAppIDs { get; private set; } internal readonly ConcurrentDictionary _idLists; + + internal long InitialUpdateTime { get; private set; } private double _idListsSyncInterval; private double _rulesetsSyncInterval; private Func? _idStoreFactory; @@ -47,6 +49,7 @@ internal SpecStore(StatsigOptions options, RequestDispatcher dispatcher, string _idListsSyncInterval = options.IDListsSyncInterval; _rulesetsSyncInterval = options.RulesetsSyncInterval; LastSyncTime = 0; + InitialUpdateTime = 0; FeatureGates = new Dictionary(); DynamicConfigs = new Dictionary(); LayerConfigs = new Dictionary(); @@ -84,6 +87,8 @@ internal async Task Initialize() EvalReason = EvaluationReason.DataAdapter; } + InitialUpdateTime = LastSyncTime; + await SyncIDLists().ConfigureAwait(false); // Start background tasks to periodically refresh the store @@ -136,6 +141,11 @@ internal List GetSpecNames(string type) }; } + internal EvaluationDetails GetEvaluationDetails(EvaluationReason? reason = null) + { + return new EvaluationDetails(reason ?? EvalReason, InitialUpdateTime, LastSyncTime); + } + private async Task BackgroundPeriodicSyncIDListsTask(CancellationToken cancellationToken) { var delayInterval = TimeSpan.FromSeconds(_idListsSyncInterval); diff --git a/dotnet-statsig/src/Statsig/Server/ServerDriver.cs b/dotnet-statsig/src/Statsig/Server/ServerDriver.cs index c85c9c8..d5a561e 100644 --- a/dotnet-statsig/src/Statsig/Server/ServerDriver.cs +++ b/dotnet-statsig/src/Statsig/Server/ServerDriver.cs @@ -135,7 +135,7 @@ public FeatureGate GetFeatureGate(StatsigUser user, string gateName) return _errorBoundary.Capture( "GetFeatureGate", () => CheckGateImpl(user, gateName, shouldLogExposure: true), - () => new FeatureGate(gateName, false, null, null, EvaluationReason.Error) + () => new FeatureGate(gateName) ); } @@ -144,7 +144,7 @@ public FeatureGate GetFeatureGateWithExposureLoggingDisabled(StatsigUser user, s return _errorBoundary.Capture( "GetFeatureGateWithExposureLoggingDisabled", () => CheckGateImpl(user, gateName, shouldLogExposure: false), - () => new FeatureGate(gateName, false, null, null, EvaluationReason.Error) + () => new FeatureGate(gateName) ); } @@ -453,30 +453,30 @@ private FeatureGate CheckGateImpl(StatsigUser user, string gateName, bool should var isInitialized = EnsureInitialized(); if (!isInitialized) { - return new FeatureGate(gateName, false, null, null, EvaluationReason.Uninitialized); + return new FeatureGate(gateName, false, null, null, EvaluationReason.Uninitialized, evaluator.GetEvaluationDetails(EvaluationReason.Uninitialized)); } var userIsValid = ValidateUser(user); if (!userIsValid) { - return new FeatureGate(gateName, false, null, null, EvaluationReason.Error); + return new FeatureGate(gateName, false, null, null, EvaluationReason.Error, evaluator.GetEvaluationDetails(EvaluationReason.Error)); } NormalizeUser(user); var nameValid = ValidateNonEmptyArgument(gateName, "gateName"); if (!nameValid) { - return new FeatureGate(gateName, false, null, null, EvaluationReason.Error); + return new FeatureGate(gateName, false, null, null, EvaluationReason.Error, evaluator.GetEvaluationDetails(EvaluationReason.Error)); } var evaluation = evaluator.CheckGate(user, gateName); if (evaluation.Result == EvaluationResult.Unsupported) { - return new FeatureGate(gateName, false, null, null, EvaluationReason.Unsupported); + return new FeatureGate(gateName, false, null, null, EvaluationReason.Unsupported, evaluator.GetEvaluationDetails(EvaluationReason.Unsupported)); } if (evaluation.Reason == EvaluationReason.Unrecognized) { - var gateValue = new FeatureGate(gateName, false, null, null, EvaluationReason.Unrecognized); + var gateValue = new FeatureGate(gateName, false, null, null, EvaluationReason.Unrecognized, evaluator.GetEvaluationDetails(EvaluationReason.Unrecognized)); if (shouldLogExposure) { LogGateExposureImpl(user, gateName, gateValue, ExposureCause.Automatic, evaluation.Reason); @@ -509,13 +509,13 @@ private DynamicConfig GetConfigImpl(StatsigUser user, string configName, bool sh var userIsValid = ValidateUser(user); if (!userIsValid) { - return new DynamicConfig(configName); + return new DynamicConfig(configName, null, null, null, null, null, false, false, evaluator.GetEvaluationDetails(EvaluationReason.Error)); } NormalizeUser(user); var nameValid = ValidateNonEmptyArgument(configName, "configName"); if (!nameValid) { - return new DynamicConfig(configName); + return new DynamicConfig(configName, null, null, null, null, null, false, false, evaluator.GetEvaluationDetails(EvaluationReason.Error)); } @@ -523,7 +523,17 @@ private DynamicConfig GetConfigImpl(StatsigUser user, string configName, bool sh if (evaluation.Result == EvaluationResult.Unsupported) { - return new DynamicConfig(configName); + return new DynamicConfig(configName, null, null, null, null, null, false, false, evaluator.GetEvaluationDetails(EvaluationReason.Unsupported)); + } + + if (evaluation.Reason == EvaluationReason.Unrecognized) + { + var configValue = new DynamicConfig(configName, null, null, null, null, null, false, false, evaluator.GetEvaluationDetails(EvaluationReason.Unrecognized)); + if (shouldLogExposure) + { + LogConfigExposureImpl(user, configName, configValue, ExposureCause.Automatic, evaluation.Reason); + } + return configValue; } if (shouldLogExposure) @@ -551,20 +561,20 @@ private Layer GetLayerImpl(StatsigUser user, string layerName, bool shouldLogExp var userIsValid = ValidateUser(user); if (!userIsValid) { - return new Layer(layerName); + return new Layer(layerName, null, null, null, null, null, null, evaluator.GetEvaluationDetails(EvaluationReason.Error)); } NormalizeUser(user); var nameIsValid = ValidateNonEmptyArgument(layerName, "layerName"); if (!nameIsValid) { - return new Layer(layerName); + return new Layer(layerName, null, null, null, null, null, null, evaluator.GetEvaluationDetails(EvaluationReason.Error)); } var evaluation = evaluator.GetLayer(user, layerName); if (evaluation.Result == EvaluationResult.Unsupported) { - return new Layer(layerName); + return new Layer(layerName, null, null, null, null, null, null, evaluator.GetEvaluationDetails(EvaluationReason.Unsupported)); } void OnExposure(Layer layer, string parameterName) @@ -578,7 +588,7 @@ void OnExposure(Layer layer, string parameterName) } var dc = evaluation.ConfigValue; - return new Layer(layerName, dc.Value, dc.RuleID, evaluation.ConfigDelegate, evaluation.ExplicitParameters, OnExposure, dc.GroupName); + return new Layer(layerName, dc.Value, dc.RuleID, evaluation.ConfigDelegate, evaluation.ExplicitParameters, OnExposure, dc.GroupName, evaluator.GetEvaluationDetails(evaluation.Reason)); } private void LogLayerParameterExposureImpl( From 497d3186fd98ba1960c16f1f2cb79fae4fc961de Mon Sep 17 00:00:00 2001 From: Daniel <95646168+daniel-statsig@users.noreply.github.com> Date: Wed, 26 Jun 2024 09:09:33 -0700 Subject: [PATCH 08/13] =?UTF-8?q?fix:=20#43=20appVersion=20is=20unknown=20?= =?UTF-8?q?in=20case=20Assembly.GetEntryAssembly()=20is=E2=80=A6=20(#149)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … not set Merging in public PR: https://github.com/statsig-io/dotnet-sdk/pull/44 --------- Co-authored-by: ondrej.pajgrt --- .github/workflows/build-and-test.yml | 7 +++++-- dotnet-statsig/src/Statsig/Client/ClientDriver.cs | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 3236faf..7d70ecd 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -8,21 +8,24 @@ on: jobs: build: + timeout-minutes: 10 runs-on: windows-latest steps: - uses: actions/checkout@v2 + - name: Setup .NET Core SDK uses: actions/setup-dotnet@v2 with: dotnet-version: "6.x.x" + - name: Install dependencies run: dotnet restore + - name: Build run: dotnet build --configuration Release --no-restore - timeout-minutes: 3 + - name: Test run: dotnet test --no-restore --verbosity normal - timeout-minutes: 5 env: test_api_key: ${{ secrets.SDK_CONSISTENCY_TEST_COMPANY_API_KEY }} test_client_key: ${{ secrets.SDK_CLIENT_KEY }} diff --git a/dotnet-statsig/src/Statsig/Client/ClientDriver.cs b/dotnet-statsig/src/Statsig/Client/ClientDriver.cs index 8eb7c1a..e2ca1d9 100644 --- a/dotnet-statsig/src/Statsig/Client/ClientDriver.cs +++ b/dotnet-statsig/src/Statsig/Client/ClientDriver.cs @@ -406,7 +406,7 @@ IReadOnlyDictionary GetStatsigMetadata() ["sessionID"] = _sessionID, ["stableID"] = PersistentStore.StableID, ["locale"] = CultureInfo.CurrentUICulture.Name, - ["appVersion"] = Assembly.GetEntryAssembly()!.GetName()!.Version!.ToString()!, + ["appVersion"] = Assembly.GetEntryAssembly()?.GetName().Version!.ToString() ?? "unknown", ["systemVersion"] = Environment.OSVersion.Version.ToString(), ["systemName"] = systemName, ["sdkType"] = sdkDetails.SDKType, From 9728fe2185359728aab4c28383cef8f66b5aac8d Mon Sep 17 00:00:00 2001 From: Daniel <95646168+daniel-statsig@users.noreply.github.com> Date: Wed, 26 Jun 2024 12:05:12 -0700 Subject: [PATCH 09/13] [release] 1.27.0 - Expose EvaluationDetails on Statsig Types (#150) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### New Features You can now access EvaluationDetails on all evaluated classes - `DynamicConfig.EvaluationDetails` - `FeatureGate.EvaluationDetails` - `Layer.EvaluationDetails` >Included In This Release >- 497d3186fd98ba1960c16f1f2cb79fae4fc961de Daniel > - fix: #43 appVersion is unknown in case Assembly.GetEntryAssembly() is… (#149) >- 053c3e88977b62b6135bdfa70c2314c1de7f3f47 sroyal-statsig > - Eval Details (#147) --- dotnet-statsig-tests/Common/StatsigTest.cs | 2 +- dotnet-statsig/dotnet-statsig.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet-statsig-tests/Common/StatsigTest.cs b/dotnet-statsig-tests/Common/StatsigTest.cs index 427066e..e532b5f 100644 --- a/dotnet-statsig-tests/Common/StatsigTest.cs +++ b/dotnet-statsig-tests/Common/StatsigTest.cs @@ -21,7 +21,7 @@ public class StatsigTest : IAsyncLifetime { WireMockServer _server; - private const String ExpectedSdkVersion = "1.26.0.0"; + private const String ExpectedSdkVersion = "1.27.0.0"; Task IAsyncLifetime.InitializeAsync() { diff --git a/dotnet-statsig/dotnet-statsig.csproj b/dotnet-statsig/dotnet-statsig.csproj index fbfd9a7..72a39a7 100644 --- a/dotnet-statsig/dotnet-statsig.csproj +++ b/dotnet-statsig/dotnet-statsig.csproj @@ -7,7 +7,7 @@ statsig_dotnet true Statsig - 1.26.0 + 1.27.0 Statsig Inc. true https://github.com/statsig-io/dotnet-sdk.git From 8d39580fb965d0ca5e806229716dffda6f0b77c5 Mon Sep 17 00:00:00 2001 From: sroyal-statsig <76536058+sroyal-statsig@users.noreply.github.com> Date: Fri, 5 Jul 2024 11:03:42 -0700 Subject: [PATCH 10/13] Throw away outdated values (#151) --- .../src/Statsig/Server/Evaluation/SpecStore.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/dotnet-statsig/src/Statsig/Server/Evaluation/SpecStore.cs b/dotnet-statsig/src/Statsig/Server/Evaluation/SpecStore.cs index b0b37d4..168d65b 100644 --- a/dotnet-statsig/src/Statsig/Server/Evaluation/SpecStore.cs +++ b/dotnet-statsig/src/Statsig/Server/Evaluation/SpecStore.cs @@ -410,15 +410,20 @@ private bool ParseResponse(string? response) return false; } - JToken? time; - LastSyncTime = json.TryGetValue("time", out time) ? time.Value() : LastSyncTime; - if (!json.TryGetValue("has_updates", out var hasUpdates) || !hasUpdates.Value()) { return false; } + JToken? time; + var newTime = json.TryGetValue("time", out time) ? time.Value() : LastSyncTime; + if (newTime < LastSyncTime) + { + return false; + } + LastSyncTime = newTime; + var newGates = new Dictionary(); var newConfigs = new Dictionary(); var newLayerConfigs = new Dictionary(); From cc9d00fa0d7120c8c0020bc3fe742370d000f5b1 Mon Sep 17 00:00:00 2001 From: sroyal-statsig <76536058+sroyal-statsig@users.noreply.github.com> Date: Fri, 5 Jul 2024 13:51:56 -0700 Subject: [PATCH 11/13] [release] 1.27.1 - Don't Update Internal if Definition is Outdated (#152) ### Fixes - Fixes a bug where we could update the internal store with stale values (older than the current values in memory). While this was unlikely, now it wont happen >Included In This Release >- 8d39580fb965d0ca5e806229716dffda6f0b77c5 sroyal-statsig > - Throw away outdated values (#151) --------- Co-authored-by: statsig-kong[bot] --- dotnet-statsig-tests/Common/StatsigTest.cs | 2 +- dotnet-statsig/dotnet-statsig.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet-statsig-tests/Common/StatsigTest.cs b/dotnet-statsig-tests/Common/StatsigTest.cs index e532b5f..c8795eb 100644 --- a/dotnet-statsig-tests/Common/StatsigTest.cs +++ b/dotnet-statsig-tests/Common/StatsigTest.cs @@ -21,7 +21,7 @@ public class StatsigTest : IAsyncLifetime { WireMockServer _server; - private const String ExpectedSdkVersion = "1.27.0.0"; + private const String ExpectedSdkVersion = "1.27.1.0"; Task IAsyncLifetime.InitializeAsync() { diff --git a/dotnet-statsig/dotnet-statsig.csproj b/dotnet-statsig/dotnet-statsig.csproj index 72a39a7..d157770 100644 --- a/dotnet-statsig/dotnet-statsig.csproj +++ b/dotnet-statsig/dotnet-statsig.csproj @@ -7,7 +7,7 @@ statsig_dotnet true Statsig - 1.27.0 + 1.27.1 Statsig Inc. true https://github.com/statsig-io/dotnet-sdk.git From b231b09b0f223dfed741c5736a72d8e73dba88f0 Mon Sep 17 00:00:00 2001 From: sroyal-statsig <76536058+sroyal-statsig@users.noreply.github.com> Date: Mon, 8 Jul 2024 15:07:32 -0700 Subject: [PATCH 12/13] Make Eval Reason a String (#153) kong test https://github.com/statsig-io/kong/pull/2328 --- .../Client/ClientOptionsTest.cs | 3 +-- dotnet-statsig/src/Statsig/FeatureGate.cs | 4 ++-- .../Server/Evaluation/ConfigEvaluation.cs | 22 +++++++++---------- .../Server/Evaluation/EvaluationDetails.cs | 4 ++-- .../Statsig/Server/Evaluation/Evaluator.cs | 2 +- .../Statsig/Server/Evaluation/SpecStore.cs | 4 ++-- .../src/Statsig/Server/ServerDriver.cs | 18 +++++++++------ 7 files changed, 30 insertions(+), 27 deletions(-) diff --git a/dotnet-statsig-tests/Client/ClientOptionsTest.cs b/dotnet-statsig-tests/Client/ClientOptionsTest.cs index 79d6320..b7a27fe 100644 --- a/dotnet-statsig-tests/Client/ClientOptionsTest.cs +++ b/dotnet-statsig-tests/Client/ClientOptionsTest.cs @@ -65,8 +65,7 @@ await StatsigClient.Initialize Assert.False(StatsigClient.CheckGate("test_gate")); var endTime = DateTime.Now; - - Assert.True(endTime.Subtract(TimeSpan.FromMilliseconds(300)) < startTime); // make sure it took less than 200 ms to complete + Assert.True(endTime.Subtract(TimeSpan.FromMilliseconds(600)) < startTime); // make sure it took less than 600 ms to complete await StatsigClient.Shutdown(); startTime = DateTime.Now; diff --git a/dotnet-statsig/src/Statsig/FeatureGate.cs b/dotnet-statsig/src/Statsig/FeatureGate.cs index 9eb2ecb..4011535 100644 --- a/dotnet-statsig/src/Statsig/FeatureGate.cs +++ b/dotnet-statsig/src/Statsig/FeatureGate.cs @@ -15,7 +15,7 @@ public class FeatureGate public string RuleID { get; } [JsonProperty("secondary_exposures")] public List> SecondaryExposures { get; } - public EvaluationReason? Reason { get; } + public string? Reason { get; } public EvaluationDetails? EvaluationDetails { get; } static FeatureGate? _defaultConfig; @@ -32,7 +32,7 @@ public static FeatureGate Default } } - public FeatureGate(string? name = null, bool value = false, string? ruleID = null, List>? secondaryExposures = null, EvaluationReason? reason = null, EvaluationDetails? details = null) + public FeatureGate(string? name = null, bool value = false, string? ruleID = null, List>? secondaryExposures = null, string? reason = null, EvaluationDetails? details = null) { Name = name ?? ""; Value = value; diff --git a/dotnet-statsig/src/Statsig/Server/Evaluation/ConfigEvaluation.cs b/dotnet-statsig/src/Statsig/Server/Evaluation/ConfigEvaluation.cs index b5f90a5..d47c410 100644 --- a/dotnet-statsig/src/Statsig/Server/Evaluation/ConfigEvaluation.cs +++ b/dotnet-statsig/src/Statsig/Server/Evaluation/ConfigEvaluation.cs @@ -10,16 +10,16 @@ enum EvaluationResult Unsupported } - public enum EvaluationReason + public class EvaluationReason { - Network, - LocalOverride, - Unrecognized, - Uninitialized, - Bootstrap, - DataAdapter, - Unsupported, - Error, + public static string Network = "Network"; + public static string LocalOverride = "LocalOverride"; + public static string Unrecognized = "Unrecognized"; + public static string Uninitialized = "Uninitialized"; + public static string Bootstrap = "Bootstrap"; + public static String DataAdapter = "DataAdapter"; + public static string Unsupported = "Unsupported"; + public static string Error = "Error"; } @@ -31,11 +31,11 @@ class ConfigEvaluation internal List> UndelegatedSecondaryExposures { get; set; } internal List ExplicitParameters { get; set; } internal string? ConfigDelegate { get; set; } - internal EvaluationReason Reason { get; set; } + internal string Reason { get; set; } internal ConfigEvaluation( EvaluationResult result, - EvaluationReason reason, + string reason, FeatureGate? gate = null, DynamicConfig? config = null) { diff --git a/dotnet-statsig/src/Statsig/Server/Evaluation/EvaluationDetails.cs b/dotnet-statsig/src/Statsig/Server/Evaluation/EvaluationDetails.cs index 6868f4d..c68d438 100644 --- a/dotnet-statsig/src/Statsig/Server/Evaluation/EvaluationDetails.cs +++ b/dotnet-statsig/src/Statsig/Server/Evaluation/EvaluationDetails.cs @@ -6,12 +6,12 @@ namespace Statsig.Server.Evaluation { public class EvaluationDetails { - public EvaluationReason Reason { get; } + public string Reason { get; } public long InitTime { get; } public long ServerTime { get; } public long ConfigSyncTime { get; } - public EvaluationDetails(EvaluationReason reason, long initTime, long configSyncTime) + public EvaluationDetails(string reason, long initTime, long configSyncTime) { Reason = reason; this.InitTime = initTime; diff --git a/dotnet-statsig/src/Statsig/Server/Evaluation/Evaluator.cs b/dotnet-statsig/src/Statsig/Server/Evaluation/Evaluator.cs index e4dc72d..8955b56 100644 --- a/dotnet-statsig/src/Statsig/Server/Evaluation/Evaluator.cs +++ b/dotnet-statsig/src/Statsig/Server/Evaluation/Evaluator.cs @@ -192,7 +192,7 @@ internal List GetSpecNames(string type) return _store.GetSpecNames(type); } - internal EvaluationDetails GetEvaluationDetails(EvaluationReason? reason = null) + internal EvaluationDetails GetEvaluationDetails(string? reason = null) { return _store.GetEvaluationDetails(reason); } diff --git a/dotnet-statsig/src/Statsig/Server/Evaluation/SpecStore.cs b/dotnet-statsig/src/Statsig/Server/Evaluation/SpecStore.cs index 168d65b..e849751 100644 --- a/dotnet-statsig/src/Statsig/Server/Evaluation/SpecStore.cs +++ b/dotnet-statsig/src/Statsig/Server/Evaluation/SpecStore.cs @@ -40,7 +40,7 @@ class SpecStore private string _serverSecret; private ErrorBoundary _errorBoundary; - internal EvaluationReason EvalReason { get; set; } + internal string EvalReason { get; set; } internal SpecStore(StatsigOptions options, RequestDispatcher dispatcher, string serverSecret, ErrorBoundary errorBoundary) { @@ -141,7 +141,7 @@ internal List GetSpecNames(string type) }; } - internal EvaluationDetails GetEvaluationDetails(EvaluationReason? reason = null) + internal EvaluationDetails GetEvaluationDetails(string? reason = null) { return new EvaluationDetails(reason ?? EvalReason, InitialUpdateTime, LastSyncTime); } diff --git a/dotnet-statsig/src/Statsig/Server/ServerDriver.cs b/dotnet-statsig/src/Statsig/Server/ServerDriver.cs index d5a561e..ee93c73 100644 --- a/dotnet-statsig/src/Statsig/Server/ServerDriver.cs +++ b/dotnet-statsig/src/Statsig/Server/ServerDriver.cs @@ -474,9 +474,9 @@ private FeatureGate CheckGateImpl(StatsigUser user, string gateName, bool should return new FeatureGate(gateName, false, null, null, EvaluationReason.Unsupported, evaluator.GetEvaluationDetails(EvaluationReason.Unsupported)); } - if (evaluation.Reason == EvaluationReason.Unrecognized) + if (evaluation.Reason == EvaluationReason.Unrecognized || evaluation.Reason == EvaluationReason.Uninitialized) { - var gateValue = new FeatureGate(gateName, false, null, null, EvaluationReason.Unrecognized, evaluator.GetEvaluationDetails(EvaluationReason.Unrecognized)); + var gateValue = new FeatureGate(gateName, false, null, null, EvaluationReason.Unrecognized, evaluator.GetEvaluationDetails(evaluation.Reason)); if (shouldLogExposure) { LogGateExposureImpl(user, gateName, gateValue, ExposureCause.Automatic, evaluation.Reason); @@ -492,7 +492,7 @@ private FeatureGate CheckGateImpl(StatsigUser user, string gateName, bool should return evaluation.GateValue; } - private void LogGateExposureImpl(StatsigUser user, string gateName, FeatureGate gate, ExposureCause cause, EvaluationReason reason) + private void LogGateExposureImpl(StatsigUser user, string gateName, FeatureGate gate, ExposureCause cause, string reason) { _eventLogger.Enqueue(EventLog.CreateGateExposureLog(user, gateName, gate?.Value ?? false, @@ -505,7 +505,11 @@ private void LogGateExposureImpl(StatsigUser user, string gateName, FeatureGate private DynamicConfig GetConfigImpl(StatsigUser user, string configName, bool shouldLogExposure) { - EnsureInitialized(); + var isInitialized = EnsureInitialized(); + if (!isInitialized) + { + return new DynamicConfig(configName, null, null, null, null, null, false, false, evaluator.GetEvaluationDetails(EvaluationReason.Uninitialized)); + } var userIsValid = ValidateUser(user); if (!userIsValid) { @@ -526,9 +530,9 @@ private DynamicConfig GetConfigImpl(StatsigUser user, string configName, bool sh return new DynamicConfig(configName, null, null, null, null, null, false, false, evaluator.GetEvaluationDetails(EvaluationReason.Unsupported)); } - if (evaluation.Reason == EvaluationReason.Unrecognized) + if (evaluation.Reason == EvaluationReason.Unrecognized || evaluation.Reason == EvaluationReason.Uninitialized) { - var configValue = new DynamicConfig(configName, null, null, null, null, null, false, false, evaluator.GetEvaluationDetails(EvaluationReason.Unrecognized)); + var configValue = new DynamicConfig(configName, null, null, null, null, null, false, false, evaluator.GetEvaluationDetails(evaluation.Reason)); if (shouldLogExposure) { LogConfigExposureImpl(user, configName, configValue, ExposureCause.Automatic, evaluation.Reason); @@ -545,7 +549,7 @@ private DynamicConfig GetConfigImpl(StatsigUser user, string configName, bool sh } private void LogConfigExposureImpl(StatsigUser user, string configName, DynamicConfig config, - ExposureCause cause, EvaluationReason reason) + ExposureCause cause, string reason) { _eventLogger.Enqueue(EventLog.CreateConfigExposureLog(user, configName, config?.RuleID ?? "", From dd4a4ecac339efc819732bf3c90c4a4e4bf18028 Mon Sep 17 00:00:00 2001 From: sroyal-statsig <76536058+sroyal-statsig@users.noreply.github.com> Date: Mon, 8 Jul 2024 15:34:52 -0700 Subject: [PATCH 13/13] [release] 1.27.2 - Make Evaluation Reason a String (#154) ### Fixes - Switches evaluation reason to be a string instead of an enum to be inline with other sdks >Included In This Release >- b231b09b0f223dfed741c5736a72d8e73dba88f0 sroyal-statsig > - Make Eval Reason a String (#153) --------- Co-authored-by: statsig-kong[bot] --- dotnet-statsig-tests/Common/StatsigTest.cs | 2 +- dotnet-statsig/dotnet-statsig.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet-statsig-tests/Common/StatsigTest.cs b/dotnet-statsig-tests/Common/StatsigTest.cs index c8795eb..3ffd970 100644 --- a/dotnet-statsig-tests/Common/StatsigTest.cs +++ b/dotnet-statsig-tests/Common/StatsigTest.cs @@ -21,7 +21,7 @@ public class StatsigTest : IAsyncLifetime { WireMockServer _server; - private const String ExpectedSdkVersion = "1.27.1.0"; + private const String ExpectedSdkVersion = "1.27.2.0"; Task IAsyncLifetime.InitializeAsync() { diff --git a/dotnet-statsig/dotnet-statsig.csproj b/dotnet-statsig/dotnet-statsig.csproj index d157770..56bfea2 100644 --- a/dotnet-statsig/dotnet-statsig.csproj +++ b/dotnet-statsig/dotnet-statsig.csproj @@ -7,7 +7,7 @@ statsig_dotnet true Statsig - 1.27.1 + 1.27.2 Statsig Inc. true https://github.com/statsig-io/dotnet-sdk.git