Skip to content

Commit f5828cb

Browse files
authored
feat: client-side prerequisite events (#24)
Allows the client SDK to deserialize `prerequisites` from the flag model and then emit prerequisite evaluation events.
1 parent c561e31 commit f5828cb

File tree

6 files changed

+144
-18
lines changed

6 files changed

+144
-18
lines changed

pkgs/sdk/client/contract-tests/TestService.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ public class Webapp
4040
"tags",
4141
"auto-env-attributes",
4242
"inline-context",
43-
"anonymous-redaction"
43+
"anonymous-redaction",
44+
"client-prereq-events"
4445
};
4546

4647
public readonly Handler Handler;

pkgs/sdk/client/src/DataModel.cs

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
24
using System.Text.Json;
35
using System.Text.Json.Serialization;
46
using LaunchDarkly.Sdk.Internal;
@@ -36,6 +38,8 @@ public sealed class FeatureFlag : IEquatable<FeatureFlag>, IJsonSerializable
3638
internal bool TrackReason { get; }
3739
internal UnixMillisecondTime? DebugEventsUntilDate { get; }
3840

41+
internal IReadOnlyList<string> Prerequisites { get; }
42+
3943
internal FeatureFlag(
4044
LdValue value,
4145
int? variation,
@@ -44,8 +48,8 @@ internal FeatureFlag(
4448
int? flagVersion,
4549
bool trackEvents,
4650
bool trackReason,
47-
UnixMillisecondTime? debugEventsUntilDate
48-
)
51+
UnixMillisecondTime? debugEventsUntilDate,
52+
IReadOnlyList<string> prerequisites = null)
4953
{
5054
Value = value;
5155
Variation = variation;
@@ -55,30 +59,51 @@ internal FeatureFlag(
5559
TrackEvents = trackEvents;
5660
TrackReason = trackReason;
5761
DebugEventsUntilDate = debugEventsUntilDate;
62+
Prerequisites = prerequisites != null ? new List<string>(prerequisites) : null;
5863
}
5964

6065
/// <inheritdoc/>
6166
public override bool Equals(object obj) =>
62-
Equals(obj as FeatureFlag);
67+
obj is FeatureFlag other && Equals(other);
6368

6469
/// <inheritdoc/>
65-
public bool Equals(FeatureFlag otherFlag) =>
66-
Value.Equals(otherFlag.Value)
67-
&& Variation == otherFlag.Variation
68-
&& Reason.Equals(otherFlag.Reason)
69-
&& Version == otherFlag.Version
70-
&& FlagVersion == otherFlag.FlagVersion
71-
&& TrackEvents == otherFlag.TrackEvents
72-
&& DebugEventsUntilDate == otherFlag.DebugEventsUntilDate;
70+
public bool Equals(FeatureFlag otherFlag)
71+
{
72+
73+
if (otherFlag is null)
74+
{
75+
return false;
76+
}
77+
78+
if (ReferenceEquals(this, otherFlag))
79+
{
80+
return true;
81+
}
82+
83+
if (GetType() != otherFlag.GetType())
84+
{
85+
return false;
86+
}
87+
88+
return Variation == otherFlag.Variation
89+
&& Reason.Equals(otherFlag.Reason)
90+
&& Version == otherFlag.Version
91+
&& FlagVersion == otherFlag.FlagVersion
92+
&& TrackEvents == otherFlag.TrackEvents
93+
&& DebugEventsUntilDate == otherFlag.DebugEventsUntilDate
94+
&& (Prerequisites == null && otherFlag.Prerequisites == null ||
95+
Prerequisites != null && otherFlag.Prerequisites != null &&
96+
Prerequisites.SequenceEqual(otherFlag.Prerequisites));
97+
}
7398

7499
/// <inheritdoc/>
75100
public override int GetHashCode() =>
76101
Value.GetHashCode();
77102

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

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

103129
for (var obj = RequireObject(ref reader); obj.Next(ref reader);)
104130
{
@@ -128,6 +154,14 @@ public static FeatureFlag ReadJsonValue(ref Utf8JsonReader reader)
128154
case "debugEventsUntilDate":
129155
debugEventsUntilDate = JsonSerializer.Deserialize<UnixMillisecondTime?>(ref reader);
130156
break;
157+
case "prerequisites":
158+
for (var array = RequireArrayOrNull(ref reader); array.Next(ref reader);)
159+
{
160+
prerequisites ??= new List<string>();
161+
prerequisites.Add(reader.GetString());
162+
}
163+
break;
164+
131165
}
132166
}
133167

@@ -139,8 +173,9 @@ public static FeatureFlag ReadJsonValue(ref Utf8JsonReader reader)
139173
flagVersion,
140174
trackEvents,
141175
trackReason,
142-
debugEventsUntilDate
143-
);
176+
debugEventsUntilDate,
177+
prerequisites
178+
);
144179
}
145180

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

204+
if (value.Prerequisites != null && value.Prerequisites.Count > 0)
205+
{
206+
writer.WriteStartArray("prerequisites");
207+
foreach (var p in value.Prerequisites)
208+
{
209+
writer.WriteStringValue(p);
210+
}
211+
writer.WriteEndArray();
212+
}
213+
169214
writer.WriteEndObject();
170215
}
171216
}

pkgs/sdk/client/src/Integrations/TestData.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,8 @@ internal ItemDescriptor CreateFlag(int version, Context context)
592592
_preconfiguredFlag.FlagVersion,
593593
_preconfiguredFlag.TrackEvents,
594594
_preconfiguredFlag.TrackReason,
595-
_preconfiguredFlag.DebugEventsUntilDate));
595+
_preconfiguredFlag.DebugEventsUntilDate,
596+
_preconfiguredFlag.Prerequisites));
596597
}
597598
int variation;
598599
if (!_variationByContextKey.TryGetValue(context.Kind, out var keys) ||

pkgs/sdk/client/src/LdClient.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,25 @@ EvaluationDetail<T> errorResult(EvaluationErrorKind kind) =>
761761
}
762762
}
763763

764+
// The flag.Prerequisites array represents the evaluated prerequisites of this flag. We need to generate
765+
// events for both this flag and its prerequisites (recursively), which is necessary to ensure LaunchDarkly
766+
// analytics functions properly.
767+
//
768+
// We're using JsonVariationDetail because the type of the prerequisite is both unknown and irrelevant
769+
// to emitting the events.
770+
//
771+
// We're passing LdValue.Null to match a server-side SDK's behavior when evaluating prerequisites.
772+
//
773+
// NOTE: if "hooks" functionality is implemented into this SDK, take care that evaluating prerequisites
774+
// does not trigger hooks. This may require refactoring the code below to not use JsonVariationDetail.
775+
if (flag.Prerequisites != null)
776+
{
777+
foreach (var prerequisiteKey in flag.Prerequisites)
778+
{
779+
JsonVariationDetail(prerequisiteKey, LdValue.Null);
780+
}
781+
}
782+
764783
EvaluationDetail<T> result;
765784
LdValue valueJson;
766785
if (flag.Value.IsNull)

pkgs/sdk/client/test/LaunchDarkly.ClientSdk.Tests/LdClientEventTests.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ public class LdClientEventTests : BaseTest
1414
private readonly TestData _testData = TestData.DataSource();
1515
private MockEventProcessor eventProcessor = new MockEventProcessor();
1616
private IComponentConfigurer<IEventProcessor> _factory;
17+
private ITestOutputHelper _testOutput;
1718

1819
public LdClientEventTests(ITestOutputHelper testOutput) : base(testOutput)
1920
{
2021
_factory = eventProcessor.AsSingletonFactory<IEventProcessor>();
22+
_testOutput = testOutput;
2123
}
2224

2325
private LdClient MakeClient(Context c) =>
@@ -333,11 +335,55 @@ public void VariationSendsFeatureEventWithReasonForUnknownFlagWhenClientIsNotIni
333335
}
334336
}
335337

338+
[Fact]
339+
public void VariationSendsFeatureEventForPrerequisites()
340+
{
341+
var flagA = new FeatureFlagBuilder().Value(LdValue.Of(true)).Variation(1).Version(1000)
342+
.TrackEvents(false).TrackReason(false).Build();
343+
var flagAB = new FeatureFlagBuilder().Value(LdValue.Of(true)).Variation(1).Version(1000)
344+
.TrackEvents(false).TrackReason(false).Prerequisites("flagA").Build();
345+
var flagAC = new FeatureFlagBuilder().Value(LdValue.Of(true)).Variation(1).Version(1000)
346+
.TrackEvents(false).TrackReason(false).Prerequisites("flagA").Build();
347+
var flagABD = new FeatureFlagBuilder().Value(LdValue.Of(true)).Variation(1).Version(1000)
348+
.TrackEvents(false).TrackReason(false).Prerequisites("flagAB").Build();
349+
350+
_testData.Update(_testData.Flag("flagA").PreconfiguredFlag(flagA));
351+
_testData.Update(_testData.Flag("flagAB").PreconfiguredFlag(flagAB));
352+
_testData.Update(_testData.Flag("flagAC").PreconfiguredFlag(flagAC));
353+
_testData.Update(_testData.Flag("flagABD").PreconfiguredFlag(flagABD));
354+
355+
using (LdClient client = MakeClient(user))
356+
{
357+
client.BoolVariation("flagA");
358+
client.BoolVariation("flagAB");
359+
client.BoolVariation("flagAC");
360+
client.BoolVariation("flagABD");
361+
362+
Assert.Collection(eventProcessor.Events,
363+
e => CheckIdentifyEvent(e, user),
364+
e => CheckEvaluationEvent(e, "flagA"),
365+
e => CheckEvaluationEvent(e, "flagA"),
366+
e => CheckEvaluationEvent(e, "flagAB"),
367+
e => CheckEvaluationEvent(e, "flagA"),
368+
e => CheckEvaluationEvent(e, "flagAC"),
369+
e => CheckEvaluationEvent(e, "flagA"),
370+
e => CheckEvaluationEvent(e, "flagAB"),
371+
e => CheckEvaluationEvent(e, "flagABD")
372+
);
373+
}
374+
}
375+
336376
private void CheckIdentifyEvent(object e, Context c)
337377
{
338378
IdentifyEvent ie = Assert.IsType<IdentifyEvent>(e);
339379
Assert.Equal(c.FullyQualifiedKey, ie.Context.FullyQualifiedKey);
340380
Assert.NotEqual(0, ie.Timestamp.Value);
341381
}
382+
383+
private void CheckEvaluationEvent(object e, string flagKey)
384+
{
385+
EvaluationEvent fe = Assert.IsType<EvaluationEvent>(e);
386+
Assert.Equal(flagKey, fe.FlagKey);
387+
}
342388
}
343389
}

pkgs/sdk/client/test/LaunchDarkly.ClientSdk.Tests/ModelBuilders.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ internal class FeatureFlagBuilder
1616
private bool _trackReason;
1717
private UnixMillisecondTime? _debugEventsUntilDate;
1818
private EvaluationReason? _reason;
19+
private List<string> _prerequisites;
1920

2021
public FeatureFlagBuilder()
2122
{
@@ -30,11 +31,12 @@ public FeatureFlagBuilder(FeatureFlag from)
3031
_trackReason = from.TrackReason;
3132
_debugEventsUntilDate = from.DebugEventsUntilDate;
3233
_reason = from.Reason;
34+
_prerequisites = from.Prerequisites != null ? new List<string>(from.Prerequisites) : null;
3335
}
3436

3537
public FeatureFlag Build()
3638
{
37-
return new FeatureFlag(_value, _variation, _reason, _version, _flagVersion, _trackEvents, _trackReason, _debugEventsUntilDate);
39+
return new FeatureFlag(_value, _variation, _reason, _version, _flagVersion, _trackEvents, _trackReason, _debugEventsUntilDate, _prerequisites);
3840
}
3941

4042
public FeatureFlagBuilder Value(LdValue value)
@@ -88,6 +90,18 @@ public FeatureFlagBuilder DebugEventsUntilDate(UnixMillisecondTime? debugEventsU
8890
_debugEventsUntilDate = debugEventsUntilDate;
8991
return this;
9092
}
93+
94+
public FeatureFlagBuilder Prerequisites(params string[] prerequisites)
95+
{
96+
if (prerequisites == null || prerequisites.Length == 0)
97+
{
98+
_prerequisites = null;
99+
return this;
100+
}
101+
102+
_prerequisites = new List<string>(prerequisites);
103+
return this;
104+
}
91105
}
92106

93107
internal class DataSetBuilder

0 commit comments

Comments
 (0)