Skip to content

Commit

Permalink
Support tracking
Browse files Browse the repository at this point in the history
Signed-off-by: christian.lutnik <[email protected]>
  • Loading branch information
chrfwow committed Dec 4, 2024
1 parent c444e52 commit 76c812c
Show file tree
Hide file tree
Showing 8 changed files with 505 additions and 1 deletion.
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,19 @@ await Api.Instance.SetProviderAsync(myClient.GetMetadata().Name, provider);
myClient.AddHandler(ProviderEventTypes.ProviderReady, callback);
```

### Tracking

The tracking API allows you to use OpenFeature abstractions and objects to associate user actions with feature flag evaluations.
This is essential for robust experimentation powered by feature flags.
For example, a flag enhancing the appearance of a UI component might drive user engagement to a new feature; to test this hypothesis, telemetry collected by a hook(#hooks) or provider(#providers) can be associated with telemetry reported in the client's `track` function.

```csharp
var client = Api.Instance.GetClient();
client.Track("visited-promo-page", trackingEventDetails: new TrackingEventDetailsBuilder().SetValue(99.77).Set("currency", "USD").Build());
```

Note that some providers may not support tracking; check the documentation for your provider for more information.

### Shutdown

The OpenFeature API provides a close function to perform a cleanup of all registered providers. This should only be called when your application is in the process of shutting down.
Expand Down Expand Up @@ -320,7 +333,7 @@ builder.Services.AddOpenFeature(featureBuilder => {
featureBuilder
.AddHostedFeatureLifecycle() // From Hosting package
.AddContext((contextBuilder, serviceProvider) => { /* Custom context configuration */ })
.AddInMemoryProvider();
.AddInMemoryProvider();
});
```
**Domain-Scoped Provider Configuration:**
Expand Down
12 changes: 12 additions & 0 deletions src/OpenFeature/FeatureProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using OpenFeature.Model;

[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // required to allow NSubstitute mocking of internal methods

namespace OpenFeature
{
/// <summary>
Expand Down Expand Up @@ -140,5 +141,16 @@ public virtual Task ShutdownAsync(CancellationToken cancellationToken = default)
/// </summary>
/// <returns>The event channel of the provider</returns>
public virtual Channel<object> GetEventChannel() => this.EventChannel;

/// <summary>
/// Use this method to track user interactions and the application state. The implementation of this method is optional.
/// </summary>
/// <param name="trackingEventName">The name associated with this tracking event</param>
/// <param name="evaluationContext">The evaluation context used in the evaluation of the flag (optional)</param>
/// <param name="trackingEventDetails">Data pertinent to the tracking event (Optional)</param>
public virtual void Track(string trackingEventName, EvaluationContext? evaluationContext = default, TrackingEventDetails? trackingEventDetails = default)
{

Check warning on line 152 in src/OpenFeature/FeatureProvider.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/FeatureProvider.cs#L152

Added line #L152 was not covered by tests
// Intentionally left blank.
}

Check warning on line 154 in src/OpenFeature/FeatureProvider.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/FeatureProvider.cs#L154

Added line #L154 was not covered by tests
}
}
129 changes: 129 additions & 0 deletions src/OpenFeature/Model/TrackingEventDetails.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;

namespace OpenFeature.Model;

/// <summary>
/// The `tracking event details` structure defines optional data pertinent to a particular `tracking event`.
/// </summary>
/// <seealso href="https://github.com/open-feature/spec/blob/main/specification/sections/06-tracking.md#62-tracking-event-details"/>
public sealed class TrackingEventDetails
{
/// <summary>
/// The index for the "targeting key" property when the EvaluationContext is serialized or expressed as a dictionary.
/// </summary>
internal const string TargetingKeyIndex = "targetingKey";

/// <summary>
///A predefined value field for the tracking details.
/// </summary>
public readonly double? Value;

private readonly Structure _structure;

/// <summary>
/// Internal constructor used by the builder.
/// </summary>
/// <param name="content"></param>
/// <param name="value"></param>
internal TrackingEventDetails(Structure content, double? value)
{
this.Value = value;
this._structure = content;
}


/// <summary>
/// Private constructor for making an empty <see cref="TrackingEventDetails"/>.
/// </summary>
private TrackingEventDetails()
{
this._structure = Structure.Empty;
this.Value = null;
}

/// <summary>
/// Empty tracking event details.
/// </summary>
public static TrackingEventDetails Empty { get; } = new();


/// <summary>
/// Gets the Value at the specified key
/// </summary>
/// <param name="key">The key of the value to be retrieved</param>
/// <returns>The <see cref="Model.Value"/> associated with the key</returns>
/// <exception cref="KeyNotFoundException">
/// Thrown when the context does not contain the specified key
/// </exception>
/// <exception cref="ArgumentNullException">
/// Thrown when the key is <see langword="null" />
/// </exception>
public Value GetValue(string key) => this._structure.GetValue(key);

/// <summary>
/// Bool indicating if the specified key exists in the evaluation context
/// </summary>
/// <param name="key">The key of the value to be checked</param>
/// <returns><see cref="bool" />indicating the presence of the key</returns>
/// <exception cref="ArgumentNullException">
/// Thrown when the key is <see langword="null" />
/// </exception>
public bool ContainsKey(string key) => this._structure.ContainsKey(key);

Check warning on line 73 in src/OpenFeature/Model/TrackingEventDetails.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/Model/TrackingEventDetails.cs#L73

Added line #L73 was not covered by tests

/// <summary>
/// Gets the value associated with the specified key
/// </summary>
/// <param name="value">The <see cref="Model.Value"/> or <see langword="null" /> if the key was not present</param>
/// <param name="key">The key of the value to be retrieved</param>
/// <returns><see cref="bool" />indicating the presence of the key</returns>
/// <exception cref="ArgumentNullException">
/// Thrown when the key is <see langword="null" />
/// </exception>
public bool TryGetValue(string key, out Value? value) => this._structure.TryGetValue(key, out value);

Check warning on line 84 in src/OpenFeature/Model/TrackingEventDetails.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/Model/TrackingEventDetails.cs#L84

Added line #L84 was not covered by tests

/// <summary>
/// Gets all values as a Dictionary
/// </summary>
/// <returns>New <see cref="IDictionary{TKey,TValue}"/> representation of this Structure</returns>
public IImmutableDictionary<string, Value> AsDictionary()
{
return this._structure.AsDictionary();
}

Check warning on line 93 in src/OpenFeature/Model/TrackingEventDetails.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/Model/TrackingEventDetails.cs#L91-L93

Added lines #L91 - L93 were not covered by tests

/// <summary>
/// Return a count of all values
/// </summary>
public int Count => this._structure.Count;

Check warning on line 98 in src/OpenFeature/Model/TrackingEventDetails.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/Model/TrackingEventDetails.cs#L98

Added line #L98 was not covered by tests

/// <summary>
/// Returns the targeting key for the context.
/// </summary>
public string? TargetingKey
{
get
{
this._structure.TryGetValue(TargetingKeyIndex, out Value? targetingKey);

Check warning on line 107 in src/OpenFeature/Model/TrackingEventDetails.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/Model/TrackingEventDetails.cs#L106-L107

Added lines #L106 - L107 were not covered by tests
return targetingKey?.AsString;
}

Check warning on line 109 in src/OpenFeature/Model/TrackingEventDetails.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/Model/TrackingEventDetails.cs#L109

Added line #L109 was not covered by tests
}

/// <summary>
/// Return an enumerator for all values
/// </summary>
/// <returns>An enumerator for all values</returns>
public IEnumerator<KeyValuePair<string, Value>> GetEnumerator()
{
return this._structure.GetEnumerator();
}

Check warning on line 119 in src/OpenFeature/Model/TrackingEventDetails.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/Model/TrackingEventDetails.cs#L117-L119

Added lines #L117 - L119 were not covered by tests

/// <summary>
/// Get a builder which can build an <see cref="EvaluationContext"/>.
/// </summary>
/// <returns>The builder</returns>
public static TrackingEventDetailsBuilder Builder()
{
return new TrackingEventDetailsBuilder();
}

Check warning on line 128 in src/OpenFeature/Model/TrackingEventDetails.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/Model/TrackingEventDetails.cs#L126-L128

Added lines #L126 - L128 were not covered by tests
}
168 changes: 168 additions & 0 deletions src/OpenFeature/Model/TrackingEventDetailsBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
using System;

namespace OpenFeature.Model
{
/// <summary>
/// A builder which allows the specification of attributes for an <see cref="TrackingEventDetails"/>.
/// <para>
/// A <see cref="TrackingEventDetailsBuilder"/> object is intended for use by a single thread and should not be used
/// from multiple threads. Once an <see cref="TrackingEventDetails"/> has been created it is immutable and safe for use
/// from multiple threads.
/// </para>
/// </summary>
public sealed class TrackingEventDetailsBuilder
{
private readonly StructureBuilder _attributes = Structure.Builder();
private double? _value;

/// <summary>
/// Internal to only allow direct creation by <see cref="TrackingEventDetails.Builder()"/>.
/// </summary>
internal TrackingEventDetailsBuilder() { }

/// <summary>
/// Set the predefined value field for the tracking details.
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public TrackingEventDetailsBuilder SetValue(double? value)
{
this._value = value;
return this;
}

/// <summary>
/// Set the targeting key for the tracking details.
/// </summary>
/// <param name="targetingKey">The targeting key</param>
/// <returns>This builder</returns>
public TrackingEventDetailsBuilder SetTargetingKey(string targetingKey)
{
this._attributes.Set(TrackingEventDetails.TargetingKeyIndex, targetingKey);
return this;
}

Check warning on line 43 in src/OpenFeature/Model/TrackingEventDetailsBuilder.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/Model/TrackingEventDetailsBuilder.cs#L40-L43

Added lines #L40 - L43 were not covered by tests

/// <summary>
/// Set the key to the given <see cref="Value"/>.
/// </summary>
/// <param name="key">The key for the value</param>
/// <param name="value">The value to set</param>
/// <returns>This builder</returns>
public TrackingEventDetailsBuilder Set(string key, Value value)
{
this._attributes.Set(key, value);
return this;
}

Check warning on line 55 in src/OpenFeature/Model/TrackingEventDetailsBuilder.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/Model/TrackingEventDetailsBuilder.cs#L52-L55

Added lines #L52 - L55 were not covered by tests

/// <summary>
/// Set the key to the given string.
/// </summary>
/// <param name="key">The key for the value</param>
/// <param name="value">The value to set</param>
/// <returns>This builder</returns>
public TrackingEventDetailsBuilder Set(string key, string value)
{
this._attributes.Set(key, value);
return this;
}

/// <summary>
/// Set the key to the given int.
/// </summary>
/// <param name="key">The key for the value</param>
/// <param name="value">The value to set</param>
/// <returns>This builder</returns>
public TrackingEventDetailsBuilder Set(string key, int value)
{
this._attributes.Set(key, value);
return this;
}

Check warning on line 79 in src/OpenFeature/Model/TrackingEventDetailsBuilder.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/Model/TrackingEventDetailsBuilder.cs#L76-L79

Added lines #L76 - L79 were not covered by tests

/// <summary>
/// Set the key to the given double.
/// </summary>
/// <param name="key">The key for the value</param>
/// <param name="value">The value to set</param>
/// <returns>This builder</returns>
public TrackingEventDetailsBuilder Set(string key, double value)
{
this._attributes.Set(key, value);
return this;
}

/// <summary>
/// Set the key to the given long.
/// </summary>
/// <param name="key">The key for the value</param>
/// <param name="value">The value to set</param>
/// <returns>This builder</returns>
public TrackingEventDetailsBuilder Set(string key, long value)
{
this._attributes.Set(key, value);
return this;
}

Check warning on line 103 in src/OpenFeature/Model/TrackingEventDetailsBuilder.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/Model/TrackingEventDetailsBuilder.cs#L100-L103

Added lines #L100 - L103 were not covered by tests

/// <summary>
/// Set the key to the given bool.
/// </summary>
/// <param name="key">The key for the value</param>
/// <param name="value">The value to set</param>
/// <returns>This builder</returns>
public TrackingEventDetailsBuilder Set(string key, bool value)
{
this._attributes.Set(key, value);
return this;
}

/// <summary>
/// Set the key to the given <see cref="Structure"/>.
/// </summary>
/// <param name="key">The key for the value</param>
/// <param name="value">The value to set</param>
/// <returns>This builder</returns>
public TrackingEventDetailsBuilder Set(string key, Structure value)
{
this._attributes.Set(key, value);
return this;
}

/// <summary>
/// Set the key to the given DateTime.
/// </summary>
/// <param name="key">The key for the value</param>
/// <param name="value">The value to set</param>
/// <returns>This builder</returns>
public TrackingEventDetailsBuilder Set(string key, DateTime value)
{
this._attributes.Set(key, value);
return this;
}

Check warning on line 139 in src/OpenFeature/Model/TrackingEventDetailsBuilder.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/Model/TrackingEventDetailsBuilder.cs#L136-L139

Added lines #L136 - L139 were not covered by tests

/// <summary>
/// Incorporate existing tracking details into the builder.
/// <para>
/// Any existing keys in the builder will be replaced by keys in the tracking details.
/// </para>
/// </summary>
/// <param name="trackingDetails">The tracking details to add merge</param>
/// <returns>This builder</returns>
public TrackingEventDetailsBuilder Merge(TrackingEventDetails trackingDetails)
{

Check warning on line 150 in src/OpenFeature/Model/TrackingEventDetailsBuilder.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/Model/TrackingEventDetailsBuilder.cs#L150

Added line #L150 was not covered by tests
foreach (var kvp in trackingDetails)
{
this.Set(kvp.Key, kvp.Value);
}

Check warning on line 154 in src/OpenFeature/Model/TrackingEventDetailsBuilder.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/Model/TrackingEventDetailsBuilder.cs#L152-L154

Added lines #L152 - L154 were not covered by tests

return this;
}

Check warning on line 157 in src/OpenFeature/Model/TrackingEventDetailsBuilder.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/Model/TrackingEventDetailsBuilder.cs#L156-L157

Added lines #L156 - L157 were not covered by tests

/// <summary>
/// Build an immutable <see cref="TrackingEventDetails"/>.
/// </summary>
/// <returns>An immutable <see cref="TrackingEventDetails"/></returns>
public TrackingEventDetails Build()
{
return new TrackingEventDetails(this._attributes.Build(), this._value);
}
}
}
25 changes: 25 additions & 0 deletions src/OpenFeature/OpenFeatureClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,31 @@ private async Task TriggerFinallyHooksAsync<T>(IReadOnlyList<Hook> hooks, HookCo
}
}

/// <summary>
/// Use this method to track user interactions and the application state.
/// </summary>
/// <param name="trackingEventName">The name associated with this tracking event</param>
/// <param name="evaluationContext">The evaluation context used in the evaluation of the flag (optional)</param>
/// <param name="trackingEventDetails">Data pertinent to the tracking event (Optional)</param>
/// <exception cref="ArgumentException">When trackingEventName is null or empty</exception>
public void Track(string trackingEventName, EvaluationContext? evaluationContext = default, TrackingEventDetails? trackingEventDetails = default)
{
if (string.IsNullOrEmpty(trackingEventName))
{
throw new ArgumentException(nameof(trackingEventName) + " cannot be null or empty.");

Check warning on line 381 in src/OpenFeature/OpenFeatureClient.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/OpenFeatureClient.cs#L380-L381

Added lines #L380 - L381 were not covered by tests
}

var globalContext = Api.Instance.GetContext();
var clientContext = this.GetContext();

var evaluationContextBuilder = EvaluationContext.Builder()
.Merge(globalContext)
.Merge(clientContext);
if (evaluationContext != null) evaluationContextBuilder.Merge(evaluationContext);

this._providerAccessor.Invoke().Track(trackingEventName, evaluationContextBuilder.Build(), trackingEventDetails);
}

[LoggerMessage(100, LogLevel.Debug, "Hook {HookName} returned null, nothing to merge back into context")]
partial void HookReturnedNull(string hookName);

Expand Down
Loading

0 comments on commit 76c812c

Please sign in to comment.