Skip to content

Commit a6278a4

Browse files
tjosepokinyoklion
andauthored
feat: Add support for Tracking API (#40)
**Requirements** - [x] I have added test coverage for new or changed functionality - [x] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests) - [x] I have validated my changes against all supported platform versions **Related issues** The Tracking API has been part of the OpenFeature since version 2.2.0. LaunchDarkly has an API for receiving [custom events](https://launchdarkly.com/docs/sdk/features/events), which is used by their Experiments and Guarded Rollout modules. **Describe the solution you've provided** Add a `Track` method the provider, which translates the OpenFeature values into LaunchDarkly values. **Describe alternatives you've considered** Using the LaunchDarkly client directly, which defeats the whole purpose of using OpenFeature. **Additional context** Should probably update the README.md to state it supports tracking. BEGIN_COMMIT_OVERRIDE feat: Add support for Tracking API feat: Bump minimum OpenFeature SDK version to 2.2. END_COMMIT_OVERRIDE Co-authored-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
1 parent 970f75d commit a6278a4

File tree

6 files changed

+185
-3
lines changed

6 files changed

+185
-3
lines changed

src/LaunchDarkly.OpenFeature.ServerProvider/LaunchDarkly.OpenFeature.ServerProvider.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141

4242
<ItemGroup>
4343
<PackageReference Include="LaunchDarkly.ServerSdk" Version="[8.1.0,9.0)" />
44-
<PackageReference Include="OpenFeature" Version="[2.0.0, 3.0.0)" />
44+
<PackageReference Include="OpenFeature" Version="[2.2.0, 3.0.0)" />
4545
</ItemGroup>
4646

4747
<PropertyGroup>

src/LaunchDarkly.OpenFeature.ServerProvider/Provider.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ await EventChannel.Writer.WriteAsync(new ProviderEventPayload
183183
{
184184
ProviderName = _metadata.Name,
185185
Type = ProviderEventTypes.ProviderConfigurationChanged,
186-
FlagsChanged = new List<string> {changeEvent.Key},
186+
FlagsChanged = new List<string> { changeEvent.Key },
187187
}).ConfigureAwait(false);
188188
}
189189
catch (Exception e)
@@ -217,5 +217,24 @@ private void StatusChangeHandler(object sender, DataSourceStatus status)
217217
break;
218218
}
219219
}
220+
221+
/// <inheritdoc />
222+
public override void Track(string trackingEventName, EvaluationContext evaluationContext = null, TrackingEventDetails trackingEventDetails = default)
223+
{
224+
var (value, details) = trackingEventDetails.ToLdValue();
225+
226+
if (value.HasValue)
227+
{
228+
_client.Track(trackingEventName, _contextConverter.ToLdContext(evaluationContext), details, value.Value);
229+
}
230+
else if (details.Type != LdValueType.Null)
231+
{
232+
_client.Track(trackingEventName, _contextConverter.ToLdContext(evaluationContext), details);
233+
}
234+
else
235+
{
236+
_client.Track(trackingEventName, _contextConverter.ToLdContext(evaluationContext));
237+
}
238+
}
220239
}
221240
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using LaunchDarkly.Sdk;
2+
using OpenFeature.Model;
3+
4+
namespace LaunchDarkly.OpenFeature.ServerProvider
5+
{
6+
internal static class TrackingEventDetailsExtensions
7+
{
8+
/// <summary>
9+
/// Extract an OpenFeature <see cref="TrackingEventDetails"/> into an <see cref="LdValue"/>.
10+
/// </summary>
11+
/// <param name="trackingEventDetails">The value to extract</param>
12+
public static (double?, LdValue) ToLdValue(this TrackingEventDetails trackingEventDetails)
13+
{
14+
if (trackingEventDetails == null)
15+
{
16+
return (null, LdValue.Null);
17+
}
18+
19+
var value = trackingEventDetails.Value;
20+
21+
LdValue details;
22+
if (trackingEventDetails.Count == 0)
23+
{
24+
details = LdValue.Null;
25+
}
26+
else
27+
{
28+
var builder = LdValue.BuildObject();
29+
foreach (var keyvalue in trackingEventDetails)
30+
{
31+
builder.Add(keyvalue.Key, keyvalue.Value.ToLdValue());
32+
}
33+
details = builder.Build();
34+
}
35+
36+
return (value, details);
37+
}
38+
}
39+
}

test/LaunchDarkly.OpenFeature.ServerProvider.Tests/LaunchDarkly.OpenFeature.ServerProvider.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.5.1" />
3333
<PackageReference Include="LaunchDarkly.TestHelpers" Version="2.0.0" />
3434
<PackageReference Include="Moq" Version="4.8.1" />
35-
<PackageReference Include="OpenFeature" Version="2.0.0" />
35+
<PackageReference Include="OpenFeature" Version="2.2.0" />
3636
</ItemGroup>
3737

3838
<ItemGroup>

test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ProviderTests.cs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,5 +272,73 @@ public async Task ItEmitsConfigurationChangedEvents()
272272
Assert.Single(eventPayloadB?.FlagsChanged ?? new List<string>());
273273
Assert.NotEqual(eventPayloadA?.FlagsChanged[0], eventPayloadB?.FlagsChanged[0]);
274274
}
275+
276+
[Fact(Timeout = 5000)]
277+
public void ItTracksCustomEvents()
278+
{
279+
var evaluationContext = EvaluationContext.Builder()
280+
.Set("targetingKey", "the-key")
281+
.Build();
282+
var mock = new Mock<ILdClient>();
283+
mock.Setup(l => l.GetLogger())
284+
.Returns(Components.NoLogging.Build(null).LogAdapter.Logger(null));
285+
mock.Setup(l => l.Track("event-key-123abc", _converter.ToLdContext(evaluationContext))).Verifiable();
286+
var provider = new Provider(mock.Object);
287+
288+
provider.Track("event-key-123abc", evaluationContext);
289+
290+
mock.Verify();
291+
}
292+
293+
[Fact(Timeout = 5000)]
294+
public void ItTracksCustomEventsWithValue()
295+
{
296+
var evaluationContext = EvaluationContext.Builder()
297+
.Set("targetingKey", "the-key")
298+
.Build();
299+
var mock = new Mock<ILdClient>();
300+
mock.Setup(l => l.GetLogger())
301+
.Returns(Components.NoLogging.Build(null).LogAdapter.Logger(null));
302+
mock.Setup(l => l.Track("event-key-123abc", _converter.ToLdContext(evaluationContext), LdValue.Null, 99.77)).Verifiable();
303+
var provider = new Provider(mock.Object);
304+
305+
provider.Track("event-key-123abc", evaluationContext, TrackingEventDetails.Builder().SetValue(99.77).Build());
306+
307+
mock.Verify();
308+
}
309+
310+
[Fact(Timeout = 5000)]
311+
public void ItTracksCustomEventsWithDetails()
312+
{
313+
var evaluationContext = EvaluationContext.Builder()
314+
.Set("targetingKey", "the-key")
315+
.Build();
316+
var mock = new Mock<ILdClient>();
317+
mock.Setup(l => l.GetLogger())
318+
.Returns(Components.NoLogging.Build(null).LogAdapter.Logger(null));
319+
mock.Setup(l => l.Track("event-key-123abc", _converter.ToLdContext(evaluationContext), LdValue.BuildObject().Set("color", "red").Build())).Verifiable();
320+
var provider = new Provider(mock.Object);
321+
322+
provider.Track("event-key-123abc", evaluationContext, TrackingEventDetails.Builder().Set("color", "red").Build());
323+
324+
mock.Verify();
325+
}
326+
327+
[Fact(Timeout = 5000)]
328+
public void ItTracksCustomEventsWithDetailsAndValue()
329+
{
330+
var evaluationContext = EvaluationContext.Builder()
331+
.Set("targetingKey", "the-key")
332+
.Build();
333+
var mock = new Mock<ILdClient>();
334+
mock.Setup(l => l.GetLogger())
335+
.Returns(Components.NoLogging.Build(null).LogAdapter.Logger(null));
336+
mock.Setup(l => l.Track("event-key-123abc", _converter.ToLdContext(evaluationContext), LdValue.BuildObject().Set("currency", "USD").Build(), 99.77)).Verifiable();
337+
var provider = new Provider(mock.Object);
338+
339+
provider.Track("event-key-123abc", evaluationContext, TrackingEventDetails.Builder().SetValue(99.77).Set("currency", "USD").Build());
340+
341+
mock.Verify();
342+
}
275343
}
276344
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using LaunchDarkly.Sdk;
2+
using OpenFeature.Model;
3+
using Xunit;
4+
5+
namespace LaunchDarkly.OpenFeature.ServerProvider.Tests
6+
{
7+
public class TrackingEventDetailsExtensionsTest
8+
{
9+
[Fact]
10+
public void ItCanHandleValuesAndDetails()
11+
{
12+
var trackingEventDetails = TrackingEventDetails.Builder().SetValue(99.77).Set("currency", "USD").Build();
13+
var (value, details) = trackingEventDetails.ToLdValue();
14+
Assert.Equal(LdValueType.Object, details.Type);
15+
Assert.Equal(LdValue.Of("USD"), details.Get("currency"));
16+
Assert.Equal(99.77, value);
17+
}
18+
19+
[Fact]
20+
public void ItCanHandleDetailsOnly()
21+
{
22+
var trackingEventDetails = TrackingEventDetails.Builder().Set("color", "red").Build();
23+
var (value, details) = trackingEventDetails.ToLdValue();
24+
Assert.Equal(LdValueType.Object, details.Type);
25+
Assert.Equal(LdValue.Of("red"), details.Get("color"));
26+
Assert.Null(value);
27+
}
28+
29+
[Fact]
30+
public void ItCanHandleValuesOnly()
31+
{
32+
var trackingEventDetails = TrackingEventDetails.Builder().SetValue(99.77).Build();
33+
var (value, details) = trackingEventDetails.ToLdValue();
34+
Assert.Equal(LdValueType.Null, details.Type);
35+
Assert.Equal(99.77, value);
36+
}
37+
38+
[Fact]
39+
public void ItCanHandleEmptyStructures()
40+
{
41+
var trackingEventDetails = TrackingEventDetails.Empty;
42+
var (value, details) = trackingEventDetails.ToLdValue();
43+
Assert.Equal(LdValueType.Null, details.Type);
44+
Assert.Null(value);
45+
}
46+
47+
[Fact]
48+
public void ItCanHandleNull()
49+
{
50+
TrackingEventDetails trackingEventDetails = null;
51+
var (value, details) = trackingEventDetails.ToLdValue();
52+
Assert.Equal(LdValueType.Null, details.Type);
53+
Assert.Null(value);
54+
}
55+
}
56+
}

0 commit comments

Comments
 (0)