Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dd-smoke-tests/openfeature/application/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@ if (hasProperty('featureFlaggingApiJar')) {
dependencies {
// OpenFeature SDK is an API dependency of feature-flagging-api but is not
// transitively resolved when the jar is passed as a files() dependency.
implementation 'dev.openfeature:sdk:1.20.1'
implementation 'dev.openfeature:sdk:1.23.0'
Comment thread
danyal002 marked this conversation as resolved.
implementation 'org.springframework.boot:spring-boot-starter-web'
}
8 changes: 8 additions & 0 deletions metadata/supported-configurations.json
Original file line number Diff line number Diff line change
Expand Up @@ -1604,6 +1604,14 @@
"aliases": []
}
],
"DD_FEATURE_FLAGS_EXPOSURES_DATADOG_LOGGING_ENABLED": [
{
"version": "A",
"type": "boolean",
"default": "true",
"aliases": []
}
],
"DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED": [
{
"version": "A",
Expand Down
101 changes: 101 additions & 0 deletions products/feature-flagging/feature-flagging-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ Published as `com.datadoghq:dd-openfeature` on Maven Central.

The OpenFeature SDK (`dev.openfeature:sdk`) is included as a transitive dependency.

The provider sends exposures from an OpenFeature provider hook. If you wrap it in OpenFeature's
`MultiProvider`, use `dev.openfeature:sdk` 1.23.0 or later: earlier versions do not run the hooks of
the providers inside a `MultiProvider`, so the provider sends no exposures there.

### Evaluation metrics (optional)

To enable evaluation metrics (`feature_flag.evaluations` counter), enable the Datadog Java agent's
Expand Down Expand Up @@ -82,6 +86,103 @@ OTEL_EXPORTER_OTLP_PROTOCOL=grpc
| `error.type` | Error code (lowercased, only on error) |
| `feature_flag.result.allocation_key` | Allocation key (when present) |

## Exposures in your own data store

The provider sends exposures to Datadog. It also puts its exposure decision and the selected split's
features into the flag metadata of every evaluation, so any OpenFeature hook can record exposures in
your own data store (see [Flag metadata](#flag-metadata)).

For convenience, `dd-openfeature` ships `ExposureHook`, which reads that metadata for you. It
receives every completed evaluation, with the same exposure decision that the provider uses for
Datadog.

```java
import datadog.trace.api.openfeature.ExposureHook;

BlockingQueue<ExposureRecord> queue = new LinkedBlockingQueue<>(10_000);

OpenFeatureAPI.getInstance().addHooks(new ExposureHook(evaluation -> {
if (!evaluation.shouldSend()) {
return;
}
Map<String, Object> features = evaluation.getFeatures();
queue.offer(new ExposureRecord(
evaluation.getDetails().getFlagKey(),
evaluation.getDetails().getVariant(),
evaluation.getContext().getTargetingKey(),
(String) features.get("holdout.key"),
(String) features.get("holdout.assignment_group")));
}));
// A background thread drains the queue and writes to the data store.
```

- The callback runs on the evaluation thread. Do not write to the data store from the callback.
Copy what you need and hand it to a background queue.
- `shouldSend()` is true for a successful evaluation of an allocation that logs exposures, for a
subject not yet exposed to the same allocation, variant and serial id. The serial id counts only
with `dd-java-agent` 1.67.0 or later. These are exactly the exposures that the provider sends to
Datadog, as long as Datadog logging is on and the provider's own hooks run: evaluate through an
OpenFeature client, and inside a `MultiProvider` use OpenFeature SDK 1.23.0 or later. With an
older SDK, a provider inside a `MultiProvider` sends no exposures, and `shouldSend()` is true for
every exposure.
- `isExposure()` is true for every successful evaluation of an allocation that logs exposures,
including repeats. `isCacheHit()` tells repeats apart.
- An evaluation that fails, including one that a later `after` hook fails, is not an exposure.
- An exception from the callback does not affect the evaluation. The first one is logged at warn
level, later ones at debug level.

### Features

`getFeatures()` returns the features of the selected split whose destinations include `HOOK`.
Values are a `String`, `Double` or `Boolean`. Holdout splits carry:

| Feature | Value |
|---|---|
| `holdout.key` | The holdout key |
| `holdout.assignment_group` | `status_quo` or `all_shipped` |

The map is empty when the split has no such features. Datadog can add destinations later; the
provider ignores a destination it does not know and logs a warning once for each one.

### Flag metadata

The same data is in the evaluation's flag metadata, for hooks that use only the OpenFeature API.
Read it in the hook's `finallyAfter` stage, where the details match what the application receives,
and skip details that have an error code. `ExposureHook` does both.

| Key | Type | Meaning |
|---|---|---|
| `allocationKey` | String | The selected allocation |
| `__dd_do_log` | Boolean | Whether the allocation logs exposures. Present when the allocation logs exposures or span enrichment is on |
| `__dd_exposure_cache_hit` | Boolean | The subject was already exposed. Present only when `__dd_do_log` is true |
| `__dd_split_serial_id` | Integer | The split's serial id. Present when `__dd_do_log` is present, the split has a serial id, and the installed `dd-java-agent` supports serial ids |
| `__dd_feature.<key>` | String, Double or Boolean | One entry for each feature of the selected split that lists the `HOOK` destination |

### Agent versions

`dd-openfeature` and `dd-java-agent` ship separately. The provider turns a capability off when the
installed agent lacks it:

| Capability | Needs `dd-java-agent` | Without it |
|---|---|---|
| Serial ids on splits (`__dd_split_serial_id`) | 1.65.0 | The key is absent |
| Serial ids on exposures sent to Datadog | 1.67.0 | Exposures are sent without a serial id, so holdout attribution is lost |
| Flag evaluation events sent to Datadog | 1.66.0 | No flag evaluation events are sent |
| Split features (`getFeatures()`, `__dd_feature.<key>`) | 1.68.0 | `getFeatures()` is empty |

### Stop sending exposures to Datadog

```shell
DD_FEATURE_FLAGS_EXPOSURES_DATADOG_LOGGING_ENABLED=false
```

The provider then sends no exposures to Datadog. `ExposureHook` callbacks still receive every
evaluation, and `shouldSend()` gives the same result as before. The default is `true`.

This setting stops exposures only. Flag evaluation events have their own setting,
`DD_FLAGGING_EVALUATION_COUNTS_ENABLED`. The Java provider does not read dd-trace-py's
`DD_FFE_INTAKE_ENABLED`.

## Requirements

- Java 11+
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ configure<TestJvmConstraintsExtension> {

description = "Implementation of the OpenFeature Provider interface."

extra["excludedClassesCoverage"] = listOf(
// Value classes with only equals/hashCode; the cache tests cover their use.
"datadog.trace.api.openfeature.ExposureDeduplicationCache.Key",
"datadog.trace.api.openfeature.ExposureDeduplicationCache.Sent"
)

// Set both JAR and Maven artifact name
val openFeatureArtifactId = "dd-openfeature"
base {
Expand All @@ -39,7 +45,7 @@ java {
}

dependencies {
api("dev.openfeature:sdk:1.20.1")
api("dev.openfeature:sdk:1.23.0")

compileOnly(project(":products:feature-flagging:feature-flagging-bootstrap"))
compileOnly(project(":products:feature-flagging:feature-flagging-config"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ com.squareup.okio:okio:1.17.5=jmhRuntimeClasspath,testCompileClasspath,testRunti
com.thoughtworks.qdox:qdox:1.12.1=codenarc
commons-io:commons-io:2.21.0=spotbugs
de.thetaphi:forbiddenapis:3.11=compileClasspath,jmhCompileClasspath
dev.openfeature:sdk:1.20.1=compileClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
dev.openfeature:sdk:1.23.0=compileClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.leangen.geantyref:geantyref:1.3.16=jmhRuntimeClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-api:1.47.0=compileClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-context:1.47.0=compileClasspath,jmhCompileClasspath,jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
Expand Down Expand Up @@ -76,8 +76,9 @@ org.ow2.asm:asm:9.10.1=jacocoAnt,spotbugs
org.slf4j:jcl-over-slf4j:1.7.30=jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.slf4j:jul-to-slf4j:1.7.30=jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.slf4j:log4j-over-slf4j:1.7.30=jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.slf4j:slf4j-api:2.0.17=compileClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,spotbugsSlf4j,testCompileClasspath,testRuntimeClasspath
org.slf4j:slf4j-api:2.0.17=spotbugsSlf4j
org.slf4j:slf4j-api:2.0.18=spotbugs
org.slf4j:slf4j-api:2.0.19=compileClasspath,jmhCompileClasspath,jmhRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.slf4j:slf4j-simple:2.0.17=spotbugsSlf4j
org.snakeyaml:snakeyaml-engine:2.9=jmhRuntimeClasspath,testRuntimeClasspath
org.spockframework:spock-bom:2.4-groovy-3.0=jmhRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
Expand Down
Loading
Loading