public void DebugModeExpiresBasedOnServerTimeIfServerTimeIsLaterThanClientTime() { // Pick a server time that is somewhat ahead of the client time var serverTime = DateTime.Now.Add(TimeSpan.FromSeconds(20)); var mockSender = MakeMockSender(); var captured = EventCapture.From(mockSender, new EventSenderResult(DeliveryStatus.Succeeded, serverTime)); using (var ep = MakeProcessor(_config, mockSender)) { // Send and flush an event we don't care about, just to set the last server time RecordIdentify(ep, _fixedTimestamp, User.WithKey("otherUser")); FlushAndWait(ep); captured.Events.Clear(); // Now send an event with debug mode on, with a "debug until" time that is further in // the future than the client time, but in the past compared to the server. var flag = BasicFlag; flag.DebugEventsUntilDate = UnixMillisecondTime.FromDateTime(serverTime).PlusMillis(-1000); RecordEval(ep, flag, BasicEval); FlushAndWait(ep); // Should get a summary event only, not a full feature event Assert.Collection(captured.Events, item => CheckIndexEvent(item, BasicEval.Timestamp, _userJson), item => CheckSummaryEvent(item)); } }
public void VariationDetailSendsFeatureEventWithReasonForValidFlag() { var flag = new FeatureFlagBuilder().Value(LdValue.Of("a")).Variation(1).Version(1000) .TrackEvents(true).DebugEventsUntilDate(UnixMillisecondTime.OfMillis(2000)) .Reason(EvaluationReason.OffReason).Build(); _testData.Update(_testData.Flag("flag").PreconfiguredFlag(flag)); using (LdClient client = MakeClient(user)) { EvaluationDetail <string> result = client.StringVariationDetail("flag", "b"); Assert.Equal("a", result.Value); Assert.Equal(EvaluationReason.OffReason, result.Reason); Assert.Collection(eventProcessor.Events, e => CheckIdentifyEvent(e, user), e => { EvaluationEvent fe = Assert.IsType <EvaluationEvent>(e); Assert.Equal("flag", fe.FlagKey); Assert.Equal("a", fe.Value.AsString); Assert.Equal(1, fe.Variation); Assert.Equal(1000, fe.FlagVersion); Assert.Equal("b", fe.Default.AsString); Assert.True(fe.TrackEvents); Assert.Equal(UnixMillisecondTime.OfMillis(2000), fe.DebugEventsUntilDate); Assert.Equal(EvaluationReason.OffReason, fe.Reason); Assert.NotEqual(0, fe.Timestamp.Value); }); } }
public void SerializeFlagWithAllProperties() { var flag1 = new FeatureFlagBuilder() .Version(1) .Value(LdValue.Of(false)) .Variation(2) .FlagVersion(3) .Reason(EvaluationReason.OffReason) .TrackEvents(true) .DebugEventsUntilDate(UnixMillisecondTime.OfMillis(1234)) .Build(); AssertJsonEqual(@"{""version"":1,""value"":false,""variation"":2,""flagVersion"":3," + @"""reason"":{""kind"":""OFF""},""trackEvents"":true,""debugEventsUntilDate"":1234}", DataModelSerialization.SerializeFlag(flag1)); // make sure we're treating trackReason separately from trackEvents var flag2 = new FeatureFlagBuilder() .Version(1) .Value(LdValue.Of(false)) .Reason(EvaluationReason.OffReason) .Variation(2) .FlagVersion(3) .TrackReason(true) .Build(); AssertJsonEqual(@"{""version"":1,""value"":false,""variation"":2,""flagVersion"":3," + @"""reason"":{""kind"":""OFF""},""trackReason"":true}", DataModelSerialization.SerializeFlag(flag2)); }
public void AllFlagsStateReturnsStateWithReasons() { var flag1 = new FeatureFlagBuilder("key1").Version(100) .OffVariation(0).Variations(LdValue.Of("value1")) .Build(); var flag2 = new FeatureFlagBuilder("key2").Version(200) .OffVariation(1).Variations(LdValue.Of("x"), LdValue.Of("value2")) .TrackEvents(true).DebugEventsUntilDate(UnixMillisecondTime.OfMillis(1000)) .Build(); testData.UsePreconfiguredFlag(flag1); testData.UsePreconfiguredFlag(flag2); var state = client.AllFlagsState(user, FlagsStateOption.WithReasons); Assert.True(state.Valid); var expectedString = @"{""key1"":""value1"",""key2"":""value2"", ""$flagsState"":{ ""key1"":{ ""variation"":0,""version"":100,""reason"":{""kind"":""OFF""} },""key2"":{ ""variation"":1,""version"":200,""reason"":{""kind"":""OFF""},""trackEvents"":true,""debugEventsUntilDate"":1000 } }, ""$valid"":true }"; var actualString = LdJsonSerialization.SerializeObject(state); AssertHelpers.JsonEqual(expectedString, actualString); }
private async Task FlushEventsAsync(FlushPayload payload) { EventOutputFormatter formatter = new EventOutputFormatter(_config); string jsonEvents; int eventCount; try { jsonEvents = formatter.SerializeOutputEvents(payload.Events, payload.Summary, out eventCount); } catch (Exception e) { LogHelpers.LogException(_logger, "Error preparing events, will not send", e); return; } var result = await _eventSender.SendEventDataAsync(EventDataKind.AnalyticsEvents, jsonEvents, eventCount); if (result.Status == DeliveryStatus.FailedAndMustShutDown) { _disabled = true; } if (result.TimeFromServer.HasValue) { Interlocked.Exchange(ref _lastKnownPastTime, UnixMillisecondTime.FromDateTime(result.TimeFromServer.Value).Value); } }
/// <summary> /// Parses the user index from a JSON representation. If the JSON string is null or /// empty, it returns an empty user index. /// </summary> /// <param name="json">the JSON representation</param> /// <returns>the parsed data</returns> /// <exception cref="FormatException">if the JSON is malformed</exception> public static UserIndex Deserialize(string json) { if (string.IsNullOrEmpty(json)) { return(new UserIndex()); } var builder = ImmutableList.CreateBuilder <IndexEntry>(); try { var r = JReader.FromString(json); for (var ar0 = r.Array(); ar0.Next(ref r);) { var ar1 = r.Array(); if (ar1.Next(ref r)) { var userId = r.String(); if (ar1.Next(ref r)) { var timeMillis = r.Long(); builder.Add(new IndexEntry { UserId = userId, Timestamp = UnixMillisecondTime.OfMillis(timeMillis) }); ar1.Next(ref r); } } } } catch (Exception e) { throw new FormatException("invalid stored user index", e); } return(new UserIndex(builder.ToImmutable())); }
public void SummarizeEventIncrementsCounters() { var time = UnixMillisecondTime.OfMillis(1000); string flag1Key = "flag1", flag2Key = "flag2", unknownFlagKey = "badkey"; int flag1Version = 100, flag2Version = 200; int variation1 = 1, variation2 = 2; LdValue value1 = LdValue.Of("value1"), value2 = LdValue.Of("value2"), value99 = LdValue.Of("value99"), default1 = LdValue.Of("default1"), default2 = LdValue.Of("default2"), default3 = LdValue.Of("default3"); EventSummarizer es = new EventSummarizer(); es.SummarizeEvent(time, flag1Key, flag1Version, variation1, value1, default1); es.SummarizeEvent(time, flag1Key, flag1Version, variation2, value2, default1); es.SummarizeEvent(time, flag2Key, flag2Version, variation1, value99, default2); es.SummarizeEvent(time, flag1Key, flag1Version, variation1, value1, default1); es.SummarizeEvent(time, unknownFlagKey, null, null, default3, default3); EventSummary data = es.Snapshot(); Dictionary <EventsCounterKey, EventsCounterValue> expected = new Dictionary <EventsCounterKey, EventsCounterValue>(); Assert.Equal(new EventsCounterValue(2, value1, default1), data.Counters[new EventsCounterKey(flag1Key, flag1Version, variation1)]); Assert.Equal(new EventsCounterValue(1, value2, default1), data.Counters[new EventsCounterKey(flag1Key, flag1Version, variation2)]); Assert.Equal(new EventsCounterValue(1, value99, default2), data.Counters[new EventsCounterKey(flag2Key, flag2Version, variation1)]); Assert.Equal(new EventsCounterValue(1, default3, default3), data.Counters[new EventsCounterKey(unknownFlagKey, null, null)]); }
public object ReadJson(ref JReader reader) { var valid = true; var flags = new Dictionary <string, FlagState>(); for (var topLevelObj = reader.Object(); topLevelObj.Next(ref reader);) { var key = topLevelObj.Name.ToString(); switch (key) { case "$valid": valid = reader.Bool(); break; case "$flagsState": for (var flagsObj = reader.Object(); flagsObj.Next(ref reader);) { var subKey = flagsObj.Name.ToString(); var flag = flags.ContainsKey(subKey) ? flags[subKey] : new FlagState(); for (var metaObj = reader.Object(); metaObj.Next(ref reader);) { switch (metaObj.Name.ToString()) { case "variation": flag.Variation = reader.IntOrNull(); break; case "version": flag.Version = reader.IntOrNull(); break; case "trackEvents": flag.TrackEvents = reader.Bool(); break; case "debugEventsUntilDate": var n = reader.LongOrNull(); flag.DebugEventsUntilDate = n.HasValue ? UnixMillisecondTime.OfMillis(n.Value) : (UnixMillisecondTime?)null; break; case "reason": flag.Reason = EvaluationReasonConverter.ReadJsonNullableValue(ref reader); break; } } flags[subKey] = flag; } break; default: var flagForValue = flags.ContainsKey(key) ? flags[key] : new FlagState(); flagForValue.Value = LdValueConverter.ReadJsonValue(ref reader); flags[key] = flagForValue; break; } } return(new FeatureFlagsState(valid, flags)); }
public void DataSinceFromLastDiagnostic() { IDiagnosticStore _serverDiagnosticStore = CreateDiagnosticStore(null); DiagnosticEvent periodicEvent = _serverDiagnosticStore.CreateEventAndReset(); Assert.Equal(periodicEvent.JsonValue.Get("creationDate").AsLong, UnixMillisecondTime.FromDateTime(_serverDiagnosticStore.DataSince).Value); }
public void DataSinceFromLastDiagnostic() { var store = new DiagnosticStoreImpl(); DiagnosticEvent periodicEvent = store.CreateEventAndReset(); Assert.Equal(periodicEvent.JsonValue.Get("creationDate").AsLong, UnixMillisecondTime.FromDateTime(store.DataSince).Value); }
public void SetIndex() { var index = new UserIndex().UpdateTimestamp("user1", UnixMillisecondTime.OfMillis(1000)); _wrapper.SetIndex(index); var serializedData = _persistentStore.GetValue(ExpectedEnvironmentNamespace, ExpectedIndexKey); AssertJsonEqual(index.Serialize(), serializedData); }
public UserIndex UpdateTimestamp(string userId, UnixMillisecondTime timestamp) { var builder = ImmutableList.CreateBuilder <IndexEntry>(); builder.AddRange(Data.Where(e => e.UserId != userId)); builder.Add(new IndexEntry { UserId = userId, Timestamp = timestamp }); return(new UserIndex(builder.ToImmutable())); }
public void NoteTimestamp(UnixMillisecondTime timestamp) { if (StartDate.Value == 0 || timestamp.Value < StartDate.Value) { StartDate = timestamp; } if (timestamp.Value > EndDate.Value) { EndDate = timestamp; } }
public void Serialize() { var ui = new UserIndex() .UpdateTimestamp("user1", UnixMillisecondTime.OfMillis(1000)) .UpdateTimestamp("user2", UnixMillisecondTime.OfMillis(2000)); var json = ui.Serialize(); var expected = @"[[""user1"",1000],[""user2"",2000]]"; AssertJsonEqual(expected, json); }
public void PruneWhenLimitIsNotExceeded() { var ui = new UserIndex() .UpdateTimestamp("user1", UnixMillisecondTime.OfMillis(1000)) .UpdateTimestamp("user2", UnixMillisecondTime.OfMillis(2000)); Assert.Same(ui, ui.Prune(3, out var removed1)); Assert.Empty(removed1); Assert.Same(ui, ui.Prune(2, out var removed2)); Assert.Empty(removed2); }
public void UpdateTimestampForExistingUser() { var ui = new UserIndex() .UpdateTimestamp("user1", UnixMillisecondTime.OfMillis(1000)) .UpdateTimestamp("user2", UnixMillisecondTime.OfMillis(2000)); ui = ui.UpdateTimestamp("user1", UnixMillisecondTime.OfMillis(2001)); Assert.Collection(ui.Data, AssertEntry("user2", 2000), AssertEntry("user1", 2001)); }
// Adds this event to our counters, if it is a type of event we need to count. public void SummarizeEvent( UnixMillisecondTime timestamp, string flagKey, int?flagVersion, int?variation, LdValue value, LdValue defaultValue ) { _eventsState.IncrementCounter(flagKey, variation, flagVersion, value, defaultValue); _eventsState.NoteTimestamp(timestamp); }
public void AddStreamInit(DateTime timestamp, TimeSpan duration, bool failed) { var streamInitObject = LdValue.BuildObject(); streamInitObject.Add("timestamp", UnixMillisecondTime.FromDateTime(timestamp).Value); streamInitObject.Add("durationMillis", duration.TotalMilliseconds); streamInitObject.Add("failed", failed); lock (StreamInitsLock) { StreamInits.Add(streamInitObject.Build()); } }
private void CheckSummaryEventDetails(LdValue o, UnixMillisecondTime startDate, UnixMillisecondTime endDate, params Action <LdValue>[] flagChecks) { CheckSummaryEvent(o); Assert.Equal(LdValue.Of(startDate.Value), o.Get("startDate")); Assert.Equal(LdValue.Of(endDate.Value), o.Get("endDate")); var features = o.Get("features"); Assert.Equal(flagChecks.Length, features.Count); foreach (var flagCheck in flagChecks) { flagCheck(features); } }
public void SummaryEventIsSerialized() { var summary = new EventSummary(); summary.NoteTimestamp(UnixMillisecondTime.OfMillis(1001)); summary.IncrementCounter("first", 1, 11, LdValue.Of("value1a"), LdValue.Of("default1")); summary.IncrementCounter("second", 1, 21, LdValue.Of("value2a"), LdValue.Of("default2")); summary.IncrementCounter("first", 1, 11, LdValue.Of("value1a"), LdValue.Of("default1")); summary.IncrementCounter("first", 1, 12, LdValue.Of("value1a"), LdValue.Of("default1")); summary.IncrementCounter("second", 2, 21, LdValue.Of("value2b"), LdValue.Of("default2")); summary.IncrementCounter("second", null, 21, LdValue.Of("default2"), LdValue.Of("default2")); // flag exists (has version), but eval failed (no variation) summary.IncrementCounter("third", null, null, LdValue.Of("default3"), LdValue.Of("default3")); // flag doesn't exist (no version) summary.NoteTimestamp(UnixMillisecondTime.OfMillis(1000)); summary.NoteTimestamp(UnixMillisecondTime.OfMillis(1002)); var f = new EventOutputFormatter(new EventsConfiguration()); var outputEvent = LdValue.Parse(f.SerializeOutputEvents(new object[0], summary, out var count)).Get(0); Assert.Equal(1, count); Assert.Equal("summary", outputEvent.Get("kind").AsString); Assert.Equal(1000, outputEvent.Get("startDate").AsInt); Assert.Equal(1002, outputEvent.Get("endDate").AsInt); var featuresJson = outputEvent.Get("features"); Assert.Equal(3, featuresJson.Count); var firstJson = featuresJson.Get("first"); Assert.Equal("default1", firstJson.Get("default").AsString); TestUtil.AssertContainsInAnyOrder(firstJson.Get("counters").AsList(LdValue.Convert.Json), LdValue.Parse(@"{""value"":""value1a"",""variation"":1,""version"":11,""count"":2}"), LdValue.Parse(@"{""value"":""value1a"",""variation"":1,""version"":12,""count"":1}")); var secondJson = featuresJson.Get("second"); Assert.Equal("default2", secondJson.Get("default").AsString); TestUtil.AssertContainsInAnyOrder(secondJson.Get("counters").AsList(LdValue.Convert.Json), LdValue.Parse(@"{""value"":""value2a"",""variation"":1,""version"":21,""count"":1}"), LdValue.Parse(@"{""value"":""value2b"",""variation"":2,""version"":21,""count"":1}"), LdValue.Parse(@"{""value"":""default2"",""version"":21,""count"":1}")); var thirdJson = featuresJson.Get("third"); Assert.Equal("default3", thirdJson.Get("default").AsString); TestUtil.AssertContainsInAnyOrder(thirdJson.Get("counters").AsList(LdValue.Convert.Json), LdValue.Parse(@"{""unknown"":true,""value"":""default3"",""count"":1}")); }
public void SummarizeEventSetsStartAndEndDates() { var time1 = UnixMillisecondTime.OfMillis(1000); var time2 = UnixMillisecondTime.OfMillis(2000); var time3 = UnixMillisecondTime.OfMillis(3000); EventSummarizer es = new EventSummarizer(); es.SummarizeEvent(time2, "flag", null, null, LdValue.Null, LdValue.Null); es.SummarizeEvent(time1, "flag", null, null, LdValue.Null, LdValue.Null); es.SummarizeEvent(time3, "flag", null, null, LdValue.Null, LdValue.Null); EventSummary data = es.Snapshot(); Assert.Equal(time1, data.StartDate); Assert.Equal(time3, data.EndDate); }
public void PeriodicEventDefaultValuesAreCorrect() { IDiagnosticStore _serverDiagnosticStore = CreateDiagnosticStore(null); DateTime dataSince = _serverDiagnosticStore.DataSince; LdValue periodicEvent = _serverDiagnosticStore.CreateEventAndReset().JsonValue; Assert.Equal("diagnostic", periodicEvent.Get("kind").AsString); Assert.Equal(UnixMillisecondTime.FromDateTime(dataSince).Value, periodicEvent.Get("dataSinceDate").AsLong); Assert.Equal(0, periodicEvent.Get("eventsInLastBatch").AsInt); Assert.Equal(0, periodicEvent.Get("droppedEvents").AsInt); Assert.Equal(0, periodicEvent.Get("deduplicatedUsers").AsInt); LdValue streamInits = periodicEvent.Get("streamInits"); Assert.Equal(0, streamInits.Count); }
public void PruneRemovesLeastRecentUsers() { var ui = new UserIndex() .UpdateTimestamp("user1", UnixMillisecondTime.OfMillis(1000)) .UpdateTimestamp("user2", UnixMillisecondTime.OfMillis(2000)) .UpdateTimestamp("user3", UnixMillisecondTime.OfMillis(1111)) // deliberately out of order .UpdateTimestamp("user4", UnixMillisecondTime.OfMillis(3000)) .UpdateTimestamp("user5", UnixMillisecondTime.OfMillis(4000)); var ui1 = ui.Prune(3, out var removed); Assert.Equal(ImmutableList.Create("user1", "user3"), removed); Assert.Collection(ui1.Data, AssertEntry("user2", 2000), AssertEntry("user4", 3000), AssertEntry("user5", 4000)); }
public void CanAddStreamInit() { IDiagnosticStore _serverDiagnosticStore = CreateDiagnosticStore(null); DateTime timestamp = DateTime.Now; _serverDiagnosticStore.AddStreamInit(timestamp, TimeSpan.FromMilliseconds(200.0), true); DiagnosticEvent periodicEvent = _serverDiagnosticStore.CreateEventAndReset(); LdValue streamInits = periodicEvent.JsonValue.Get("streamInits"); Assert.Equal(1, streamInits.Count); LdValue streamInit = streamInits.Get(0); Assert.Equal(UnixMillisecondTime.FromDateTime(timestamp).Value, streamInit.Get("timestamp").AsLong); Assert.Equal(200, streamInit.Get("durationMillis").AsInt); Assert.True(streamInit.Get("failed").AsBool); }
internal static DateTime?ValueToDate(LdValue value) { if (value.IsString) { try { return(DateTime.Parse(value.AsString).ToUniversalTime()); } catch (FormatException) { return(null); } } if (value.IsNumber) { return(UnixMillisecondTime.OfMillis(value.AsLong).AsDateTime); } return(null); }
public void UsePreconfiguredFlag() { CreateAndStart(); _updates.ExpectInit(_initialUser); var flag = new FeatureFlagBuilder().Version(1).Value(true).Variation(0).Reason(EvaluationReason.OffReason) .TrackEvents(true).TrackReason(true).DebugEventsUntilDate(UnixMillisecondTime.OfMillis(123)).Build(); _td.Update(_td.Flag("flag1").PreconfiguredFlag(flag)); var item1 = _updates.ExpectUpsert(_initialUser, "flag1"); Assert.Equal(flag, item1.Item); _td.Update(_td.Flag("flag1").PreconfiguredFlag(flag)); var item2 = _updates.ExpectUpsert(_initialUser, "flag1"); var updatedFlag = new FeatureFlagBuilder(flag).Version(2).Build(); Assert.Equal(updatedFlag, item2.Item); }
public async Task <StoreMetadata?> GetMetadataAsync() { var db = _redis.GetDatabase(); var value = await db.StringGetAsync(_syncTimeKey); if (value.IsNull) { return(null); } if (value == "") { return(new StoreMetadata { LastUpToDate = null }); } var millis = long.Parse(value); return(new StoreMetadata { LastUpToDate = UnixMillisecondTime.OfMillis(millis) }); }
public DiagnosticEvent CreateEventAndReset() { DateTime currentTime = DateTime.Now; long droppedEvents = Interlocked.Exchange(ref DroppedEvents, 0); long deduplicatedUsers = Interlocked.Exchange(ref DeduplicatedUsers, 0); long eventsInLastBatch = Interlocked.Exchange(ref EventsInLastBatch, 0); long dataSince = Interlocked.Exchange(ref DataSince, currentTime.ToBinary()); var statEvent = LdValue.BuildObject(); AddDiagnosticCommonFields(statEvent, "diagnostic", currentTime); statEvent.Add("eventsInLastBatch", eventsInLastBatch); statEvent.Add("dataSinceDate", UnixMillisecondTime.FromDateTime(DateTime.FromBinary(dataSince)).Value); statEvent.Add("droppedEvents", droppedEvents); statEvent.Add("deduplicatedUsers", deduplicatedUsers); lock (StreamInitsLock) { statEvent.Add("streamInits", StreamInits.Build()); StreamInits = LdValue.BuildArray(); } return(new DiagnosticEvent(statEvent.Build())); }
public async Task <StoreMetadata?> GetMetadataAsync() { var key = PrefixedNamespace(MetadataKey); var request = new GetItemRequest(_tableName, MakeKeysMap(key, key), true); var result = await _client.GetItemAsync(request); if (result.Item is null || result.Item.Count == 0) { return(null); } if (!result.Item.TryGetValue(SyncTimeAttr, out var syncTimeValue) || string.IsNullOrEmpty(syncTimeValue.N)) { return(new StoreMetadata { LastUpToDate = null }); } if (!long.TryParse(syncTimeValue.N, out var milliseconds)) { throw new InvalidOperationException("Invalid data in DynamoDB: non-numeric timestamp"); } return(new StoreMetadata { LastUpToDate = UnixMillisecondTime.OfMillis(milliseconds) }); }
public object ReadJson(ref JReader reader) { string key = null; int version = 0; bool deleted = false; bool on = false; ImmutableList <Prerequisite> prerequisites = null; ImmutableList <Target> targets = null; ImmutableList <FlagRule> rules = null; string salt = null; VariationOrRollout fallthrough = new VariationOrRollout(); int?offVariation = null; ImmutableList <LdValue> variations = null; bool trackEvents = false, trackEventsFallthrough = false; UnixMillisecondTime?debugEventsUntilDate = null; bool clientSide = false; for (var obj = reader.Object().WithRequiredProperties(_requiredProperties); obj.Next(ref reader);) { switch (obj.Name) { case var n when n == "key": key = reader.String(); break; case var n when n == "version": version = reader.Int(); break; case var n when n == "deleted": deleted = reader.Bool(); break; case var n when n == "on": on = reader.Bool(); break; case var n when n == "prerequisites": var prereqsBuilder = ImmutableList.CreateBuilder <Prerequisite>(); for (var arr = reader.ArrayOrNull(); arr.Next(ref reader);) { prereqsBuilder.Add(ReadPrerequisite(ref reader)); } prerequisites = prereqsBuilder.ToImmutable(); break; case var n when n == "targets": var targetsBuilder = ImmutableList.CreateBuilder <Target>(); for (var arr = reader.ArrayOrNull(); arr.Next(ref reader);) { targetsBuilder.Add(ReadTarget(ref reader)); } targets = targetsBuilder.ToImmutable(); break; case var n when n == "rules": var rulesBuilder = ImmutableList.CreateBuilder <FlagRule>(); for (var arr = reader.ArrayOrNull(); arr.Next(ref reader);) { rulesBuilder.Add(ReadFlagRule(ref reader)); } rules = rulesBuilder.ToImmutable(); break; case var n when n == "fallthrough": fallthrough = ReadVariationOrRollout(ref reader); break; case var n when n == "offVariation": offVariation = reader.IntOrNull(); break; case var n when n == "variations": variations = SerializationHelpers.ReadValues(ref reader); break; case var n when n == "salt": salt = reader.StringOrNull(); break; case var n when n == "trackEvents": trackEvents = reader.Bool(); break; case var n when n == "trackEventsFallthrough": trackEventsFallthrough = reader.Bool(); break; case var n when n == "debugEventsUntilDate": var dt = reader.LongOrNull(); debugEventsUntilDate = dt.HasValue ? UnixMillisecondTime.OfMillis(dt.Value) : (UnixMillisecondTime?)null; break; case var n when n == "clientSide": clientSide = reader.Bool(); break; } } if (key is null && !deleted) { throw new RequiredPropertyException("key", 0); } return(new FeatureFlag(key, version, deleted, on, prerequisites, targets, rules, fallthrough, offVariation, variations, salt, trackEvents, trackEventsFallthrough, debugEventsUntilDate, clientSide)); }