Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: client-side prerequisite events #24

Merged
merged 10 commits into from
Oct 30, 2024
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());
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing the null coalescing operator because otherwise, we'd potentially construct an empty prereq array if the JSON value is null.

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
16 changes: 16 additions & 0 deletions pkgs/sdk/client/src/LdClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,22 @@ 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.
cwaldren-ld marked this conversation as resolved.
Show resolved Hide resolved
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
Loading