Skip to content

Commit

Permalink
feat: client-side prerequisite events (#24)
Browse files Browse the repository at this point in the history
Allows the client SDK to deserialize `prerequisites` from the flag model
and then emit prerequisite evaluation events.
  • Loading branch information
cwaldren-ld authored Oct 30, 2024
1 parent c561e31 commit f5828cb
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 18 deletions.
3 changes: 2 additions & 1 deletion pkgs/sdk/client/contract-tests/TestService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ public class Webapp
"tags",
"auto-env-attributes",
"inline-context",
"anonymous-redaction"
"anonymous-redaction",
"client-prereq-events"
};

public readonly Handler Handler;
Expand Down
75 changes: 60 additions & 15 deletions pkgs/sdk/client/src/DataModel.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using LaunchDarkly.Sdk.Internal;
Expand Down Expand Up @@ -36,6 +38,8 @@ public sealed class FeatureFlag : IEquatable<FeatureFlag>, IJsonSerializable
internal bool TrackReason { get; }
internal UnixMillisecondTime? DebugEventsUntilDate { get; }

internal IReadOnlyList<string> Prerequisites { get; }

internal FeatureFlag(
LdValue value,
int? variation,
Expand All @@ -44,8 +48,8 @@ internal FeatureFlag(
int? flagVersion,
bool trackEvents,
bool trackReason,
UnixMillisecondTime? debugEventsUntilDate
)
UnixMillisecondTime? debugEventsUntilDate,
IReadOnlyList<string> prerequisites = null)
{
Value = value;
Variation = variation;
Expand All @@ -55,30 +59,51 @@ internal FeatureFlag(
TrackEvents = trackEvents;
TrackReason = trackReason;
DebugEventsUntilDate = debugEventsUntilDate;
Prerequisites = prerequisites != null ? new List<string>(prerequisites) : null;
}

/// <inheritdoc/>
public override bool Equals(object obj) =>
Equals(obj as FeatureFlag);
obj is FeatureFlag other && Equals(other);

/// <inheritdoc/>
public bool Equals(FeatureFlag otherFlag) =>
Value.Equals(otherFlag.Value)
&& Variation == otherFlag.Variation
&& Reason.Equals(otherFlag.Reason)
&& Version == otherFlag.Version
&& FlagVersion == otherFlag.FlagVersion
&& TrackEvents == otherFlag.TrackEvents
&& DebugEventsUntilDate == otherFlag.DebugEventsUntilDate;
public bool Equals(FeatureFlag otherFlag)
{

if (otherFlag is null)
{
return false;
}

if (ReferenceEquals(this, otherFlag))
{
return true;
}

if (GetType() != otherFlag.GetType())
{
return false;
}

return Variation == otherFlag.Variation
&& Reason.Equals(otherFlag.Reason)
&& Version == otherFlag.Version
&& FlagVersion == otherFlag.FlagVersion
&& TrackEvents == otherFlag.TrackEvents
&& DebugEventsUntilDate == otherFlag.DebugEventsUntilDate
&& (Prerequisites == null && otherFlag.Prerequisites == null ||
Prerequisites != null && otherFlag.Prerequisites != null &&
Prerequisites.SequenceEqual(otherFlag.Prerequisites));
}

/// <inheritdoc/>
public override int GetHashCode() =>
Value.GetHashCode();

/// <inheritdoc/>
public override string ToString() =>
string.Format("({0},{1},{2},{3},{4},{5},{6},{7})",
Value, Variation, Reason, Version, FlagVersion, TrackEvents, TrackReason, DebugEventsUntilDate);
string.Format("({0},{1},{2},{3},{4},{5},{6},{7},{8})",
Value, Variation, Reason, Version, FlagVersion, TrackEvents, TrackReason, DebugEventsUntilDate, Prerequisites);

internal ItemDescriptor ToItemDescriptor() =>
new ItemDescriptor(Version, this);
Expand All @@ -99,6 +124,7 @@ public static FeatureFlag ReadJsonValue(ref Utf8JsonReader reader)
bool trackEvents = false;
bool trackReason = false;
UnixMillisecondTime? debugEventsUntilDate = null;
List<string> prerequisites = null;

for (var obj = RequireObject(ref reader); obj.Next(ref reader);)
{
Expand Down Expand Up @@ -128,6 +154,14 @@ public static FeatureFlag ReadJsonValue(ref Utf8JsonReader reader)
case "debugEventsUntilDate":
debugEventsUntilDate = JsonSerializer.Deserialize<UnixMillisecondTime?>(ref reader);
break;
case "prerequisites":
for (var array = RequireArrayOrNull(ref reader); array.Next(ref reader);)
{
prerequisites ??= new List<string>();
prerequisites.Add(reader.GetString());
}
break;

}
}

Expand All @@ -139,8 +173,9 @@ public static FeatureFlag ReadJsonValue(ref Utf8JsonReader reader)
flagVersion,
trackEvents,
trackReason,
debugEventsUntilDate
);
debugEventsUntilDate,
prerequisites
);
}

public override void Write(Utf8JsonWriter writer, FeatureFlag value, JsonSerializerOptions options) =>
Expand All @@ -166,6 +201,16 @@ public static void WriteJsonValue(FeatureFlag value, Utf8JsonWriter writer)
writer.WriteNumber("debugEventsUntilDate", value.DebugEventsUntilDate.Value.Value);
}

if (value.Prerequisites != null && value.Prerequisites.Count > 0)
{
writer.WriteStartArray("prerequisites");
foreach (var p in value.Prerequisites)
{
writer.WriteStringValue(p);
}
writer.WriteEndArray();
}

writer.WriteEndObject();
}
}
Expand Down
3 changes: 2 additions & 1 deletion pkgs/sdk/client/src/Integrations/TestData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,8 @@ internal ItemDescriptor CreateFlag(int version, Context context)
_preconfiguredFlag.FlagVersion,
_preconfiguredFlag.TrackEvents,
_preconfiguredFlag.TrackReason,
_preconfiguredFlag.DebugEventsUntilDate));
_preconfiguredFlag.DebugEventsUntilDate,
_preconfiguredFlag.Prerequisites));
}
int variation;
if (!_variationByContextKey.TryGetValue(context.Kind, out var keys) ||
Expand Down
19 changes: 19 additions & 0 deletions pkgs/sdk/client/src/LdClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,25 @@ EvaluationDetail<T> errorResult(EvaluationErrorKind kind) =>
}
}

// The flag.Prerequisites array represents the evaluated prerequisites of this flag. We need to generate
// events for both this flag and its prerequisites (recursively), which is necessary to ensure LaunchDarkly
// analytics functions properly.
//
// We're using JsonVariationDetail because the type of the prerequisite is both unknown and irrelevant
// to emitting the events.
//
// We're passing LdValue.Null to match a server-side SDK's behavior when evaluating prerequisites.
//
// NOTE: if "hooks" functionality is implemented into this SDK, take care that evaluating prerequisites
// does not trigger hooks. This may require refactoring the code below to not use JsonVariationDetail.
if (flag.Prerequisites != null)
{
foreach (var prerequisiteKey in flag.Prerequisites)
{
JsonVariationDetail(prerequisiteKey, LdValue.Null);
}
}

EvaluationDetail<T> result;
LdValue valueJson;
if (flag.Value.IsNull)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ public class LdClientEventTests : BaseTest
private readonly TestData _testData = TestData.DataSource();
private MockEventProcessor eventProcessor = new MockEventProcessor();
private IComponentConfigurer<IEventProcessor> _factory;
private ITestOutputHelper _testOutput;

public LdClientEventTests(ITestOutputHelper testOutput) : base(testOutput)
{
_factory = eventProcessor.AsSingletonFactory<IEventProcessor>();
_testOutput = testOutput;
}

private LdClient MakeClient(Context c) =>
Expand Down Expand Up @@ -333,11 +335,55 @@ public void VariationSendsFeatureEventWithReasonForUnknownFlagWhenClientIsNotIni
}
}

[Fact]
public void VariationSendsFeatureEventForPrerequisites()
{
var flagA = new FeatureFlagBuilder().Value(LdValue.Of(true)).Variation(1).Version(1000)
.TrackEvents(false).TrackReason(false).Build();
var flagAB = new FeatureFlagBuilder().Value(LdValue.Of(true)).Variation(1).Version(1000)
.TrackEvents(false).TrackReason(false).Prerequisites("flagA").Build();
var flagAC = new FeatureFlagBuilder().Value(LdValue.Of(true)).Variation(1).Version(1000)
.TrackEvents(false).TrackReason(false).Prerequisites("flagA").Build();
var flagABD = new FeatureFlagBuilder().Value(LdValue.Of(true)).Variation(1).Version(1000)
.TrackEvents(false).TrackReason(false).Prerequisites("flagAB").Build();

_testData.Update(_testData.Flag("flagA").PreconfiguredFlag(flagA));
_testData.Update(_testData.Flag("flagAB").PreconfiguredFlag(flagAB));
_testData.Update(_testData.Flag("flagAC").PreconfiguredFlag(flagAC));
_testData.Update(_testData.Flag("flagABD").PreconfiguredFlag(flagABD));

using (LdClient client = MakeClient(user))
{
client.BoolVariation("flagA");
client.BoolVariation("flagAB");
client.BoolVariation("flagAC");
client.BoolVariation("flagABD");

Assert.Collection(eventProcessor.Events,
e => CheckIdentifyEvent(e, user),
e => CheckEvaluationEvent(e, "flagA"),
e => CheckEvaluationEvent(e, "flagA"),
e => CheckEvaluationEvent(e, "flagAB"),
e => CheckEvaluationEvent(e, "flagA"),
e => CheckEvaluationEvent(e, "flagAC"),
e => CheckEvaluationEvent(e, "flagA"),
e => CheckEvaluationEvent(e, "flagAB"),
e => CheckEvaluationEvent(e, "flagABD")
);
}
}

private void CheckIdentifyEvent(object e, Context c)
{
IdentifyEvent ie = Assert.IsType<IdentifyEvent>(e);
Assert.Equal(c.FullyQualifiedKey, ie.Context.FullyQualifiedKey);
Assert.NotEqual(0, ie.Timestamp.Value);
}

private void CheckEvaluationEvent(object e, string flagKey)
{
EvaluationEvent fe = Assert.IsType<EvaluationEvent>(e);
Assert.Equal(flagKey, fe.FlagKey);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ internal class FeatureFlagBuilder
private bool _trackReason;
private UnixMillisecondTime? _debugEventsUntilDate;
private EvaluationReason? _reason;
private List<string> _prerequisites;

public FeatureFlagBuilder()
{
Expand All @@ -30,11 +31,12 @@ public FeatureFlagBuilder(FeatureFlag from)
_trackReason = from.TrackReason;
_debugEventsUntilDate = from.DebugEventsUntilDate;
_reason = from.Reason;
_prerequisites = from.Prerequisites != null ? new List<string>(from.Prerequisites) : null;
}

public FeatureFlag Build()
{
return new FeatureFlag(_value, _variation, _reason, _version, _flagVersion, _trackEvents, _trackReason, _debugEventsUntilDate);
return new FeatureFlag(_value, _variation, _reason, _version, _flagVersion, _trackEvents, _trackReason, _debugEventsUntilDate, _prerequisites);
}

public FeatureFlagBuilder Value(LdValue value)
Expand Down Expand Up @@ -88,6 +90,18 @@ public FeatureFlagBuilder DebugEventsUntilDate(UnixMillisecondTime? debugEventsU
_debugEventsUntilDate = debugEventsUntilDate;
return this;
}

public FeatureFlagBuilder Prerequisites(params string[] prerequisites)
{
if (prerequisites == null || prerequisites.Length == 0)
{
_prerequisites = null;
return this;
}

_prerequisites = new List<string>(prerequisites);
return this;
}
}

internal class DataSetBuilder
Expand Down

0 comments on commit f5828cb

Please sign in to comment.