Skip to content

Commit

Permalink
feat: add tracking as per spec (#1228)
Browse files Browse the repository at this point in the history
feat: add tracking as per spec

---------

Signed-off-by: Bernd Warmuth <[email protected]>
  • Loading branch information
warber authored Dec 6, 2024
1 parent c5ad1b4 commit 64ad644
Show file tree
Hide file tree
Showing 12 changed files with 576 additions and 36 deletions.
33 changes: 22 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,17 +120,18 @@ See [here](https://javadoc.io/doc/dev.openfeature/sdk/latest/) for the Javadocs.

## 🌟 Features

| Status | Features | Description |
| ------ |-----------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
|| [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. |
|| [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). |
|| [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
|| [Logging](#logging) | Integrate with popular logging packages. |
|| [Domains](#domains) | Logically bind clients with providers. |
|| [Eventing](#eventing) | React to state changes in the provider or flag management system. |
|| [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
|| [Transaction Context Propagation](#transaction-context-propagation) | Set a specific [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context) for a transaction (e.g. an HTTP request or a thread). |
|| [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
| Status | Features | Description |
| ------ |---------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
|| [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. |
|| [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). |
|| [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
|| [Tracking](#tracking) | Associate user actions with feature flag evaluations. |
|| [Logging](#logging) | Integrate with popular logging packages. |
|| [Domains](#domains) | Logically bind clients with providers. |
|| [Eventing](#eventing) | React to state changes in the provider or flag management system. |
|| [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
|| [Transaction Context Propagation](#transaction-context-propagation) | Set a specific [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context) for a transaction (e.g. an HTTP request or a thread). |
|| [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |

<sub>Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌</sub>

Expand Down Expand Up @@ -215,6 +216,16 @@ Once you've added a hook as a dependency, it can be registered at the global, cl
FlagEvaluationOptions.builder().hook(new ExampleHook()).build());
```

### Tracking

The [tracking API](https://openfeature.dev/specification/sections/tracking/) allows you to use OpenFeature abstractions to associate user actions with feature flag evaluations.
This is essential for robust experimentation powered by feature flags. Note that, unlike methods that handle feature flag evaluations, calling `track(...)` may throw an `IllegalArgumentException` if an empty string is passed as the `trackingEventName`.

```java
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
api.getClient().track("visited-promo-page", new MutableTrackingEventDetails(99.77).add("currency", "USD"));
```

### Logging

The Java SDK uses SLF4J. See the [SLF4J manual](https://slf4j.org/manual.html) for complete documentation.
Expand Down
6 changes: 5 additions & 1 deletion src/main/java/dev/openfeature/sdk/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@
/**
* Interface used to resolve flags of varying types.
*/
public interface Client extends Features, EventBus<Client> {
public interface Client extends Features, Tracking, EventBus<Client> {
ClientMetadata getMetadata();

/**
* Return an optional client-level evaluation context.
*
* @return {@link EvaluationContext}
*/
EvaluationContext getEvaluationContext();

/**
* Set the client-level evaluation context.
*
* @param ctx Client level context.
*/
Client setEvaluationContext(EvaluationContext ctx);
Expand All @@ -30,12 +32,14 @@ public interface Client extends Features, EventBus<Client> {

/**
* Fetch the hooks associated to this client.
*
* @return A list of {@link Hook}s.
*/
List<Hook> getHooks();

/**
* Returns the current state of the associated provider.
*
* @return the provider state
*/
ProviderState getProviderState();
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/dev/openfeature/sdk/FeatureProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,14 @@ default ProviderState getState() {
return ProviderState.READY;
}

/**
* Feature provider implementations can opt in for to support Tracking by implementing this method.
*
* @param eventName The name of the tracking event
* @param context Evaluation context used in flag evaluation (Optional)
* @param details Data pertinent to a particular tracking event (Optional)
*/
default void track(String eventName, EvaluationContext context, TrackingEventDetails details) {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package dev.openfeature.sdk;

import dev.openfeature.sdk.internal.ExcludeFromGeneratedCoverageReport;
import lombok.experimental.Delegate;

import java.util.Map;
import java.util.Optional;
import java.util.function.Function;


/**
* ImmutableTrackingEventDetails represents data pertinent to a particular tracking event.
*/
public class ImmutableTrackingEventDetails implements TrackingEventDetails {

@Delegate(excludes = DelegateExclusions.class)
private final ImmutableStructure structure;

private final Number value;

public ImmutableTrackingEventDetails() {
this.value = null;
this.structure = new ImmutableStructure();
}

public ImmutableTrackingEventDetails(final Number value) {
this.value = value;
this.structure = new ImmutableStructure();
}

public ImmutableTrackingEventDetails(final Number value, final Map<String, Value> attributes) {
this.value = value;
this.structure = new ImmutableStructure(attributes);
}

/**
* Returns the optional tracking value.
*/
public Optional<Number> getValue() {
return Optional.ofNullable(value);
}


@SuppressWarnings("all")
private static class DelegateExclusions {
@ExcludeFromGeneratedCoverageReport
public <T extends Structure> Map<String, Value> merge(Function<Map<String, Value>, Structure> newStructure,
Map<String, Value> base,
Map<String, Value> overriding) {
return null;
}
}
}
94 changes: 94 additions & 0 deletions src/main/java/dev/openfeature/sdk/MutableTrackingEventDetails.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package dev.openfeature.sdk;

import dev.openfeature.sdk.internal.ExcludeFromGeneratedCoverageReport;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.experimental.Delegate;

import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;

/**
* MutableTrackingEventDetails represents data pertinent to a particular tracking event.
*/
@EqualsAndHashCode
@ToString
public class MutableTrackingEventDetails implements TrackingEventDetails {

private final Number value;
@Delegate(excludes = MutableTrackingEventDetails.DelegateExclusions.class)
private final MutableStructure structure;

public MutableTrackingEventDetails() {
this.value = null;
this.structure = new MutableStructure();
}

public MutableTrackingEventDetails(final Number value) {
this.value = value;
this.structure = new MutableStructure();
}

/**
* Returns the optional tracking value.
*/
public Optional<Number> getValue() {
return Optional.ofNullable(value);
}

// override @Delegate methods so that we can use "add" methods and still return MutableTrackingEventDetails,
// not Structure
public MutableTrackingEventDetails add(String key, Boolean value) {
this.structure.add(key, value);
return this;
}

public MutableTrackingEventDetails add(String key, String value) {
this.structure.add(key, value);
return this;
}

public MutableTrackingEventDetails add(String key, Integer value) {
this.structure.add(key, value);
return this;
}

public MutableTrackingEventDetails add(String key, Double value) {
this.structure.add(key, value);
return this;
}

public MutableTrackingEventDetails add(String key, Instant value) {
this.structure.add(key, value);
return this;
}

public MutableTrackingEventDetails add(String key, Structure value) {
this.structure.add(key, value);
return this;
}

public MutableTrackingEventDetails add(String key, List<Value> value) {
this.structure.add(key, value);
return this;
}

public MutableTrackingEventDetails add(String key, Value value) {
this.structure.add(key, value);
return this;
}


@SuppressWarnings("all")
private static class DelegateExclusions {
@ExcludeFromGeneratedCoverageReport
public <T extends Structure> Map<String, Value> merge(Function<Map<String, Value>, Structure> newStructure,
Map<String, Value> base,
Map<String, Value> overriding) {
return null;
}
}
}
Loading

0 comments on commit 64ad644

Please sign in to comment.