Skip to content

Commit a8c9329

Browse files
authored
feat: global variables in Java (#6)
1 parent 560b5bf commit a8c9329

36 files changed

Lines changed: 1690 additions & 265 deletions

‎.github/workflows/publish.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ jobs:
3636
id: version
3737
shell: bash
3838
run: |
39-
if [[ ! "$GITHUB_REF_NAME" =~ ^v3\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
40-
echo "Expected a Featurevisor Java v3 semantic version tag" >&2
39+
if [[ ! "$GITHUB_REF_NAME" =~ ^v4\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
40+
echo "Expected a Featurevisor Java v4 semantic version tag" >&2
4141
exit 1
4242
fi
4343
version="${GITHUB_REF_NAME#v}"

‎README.md‎

Lines changed: 72 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,9 @@ This SDK supports Featurevisor v3 behavior and v2 datafiles. Generated datafiles
2222
- [Getting variation](#getting-variation)
2323
- [Getting variables](#getting-variables)
2424
- [Type specific methods](#type-specific-methods)
25-
- [Getting all evaluations](#getting-all-evaluations)
26-
- [Sticky](#sticky)
27-
- [Initialize with sticky](#initialize-with-sticky)
28-
- [Set sticky afterwards](#set-sticky-afterwards)
25+
- [Getting global variables](#getting-global-variables)
26+
- [Getting aggregate evaluations](#getting-aggregate-evaluations)
27+
- [Sticky features and variables](#sticky-features-and-variables)
2928
- [Setting datafile](#setting-datafile)
3029
- [Merging by default](#merging-by-default)
3130
- [Replacing](#replacing)
@@ -38,7 +37,8 @@ This SDK supports Featurevisor v3 behavior and v2 datafiles. Generated datafiles
3837
- [Events](#events)
3938
- [`datafile_set`](#datafile_set)
4039
- [`context_set`](#context_set)
41-
- [`sticky_set`](#sticky_set)
40+
- [`sticky_features_set`](#sticky_features_set)
41+
- [`sticky_variables_set`](#sticky_variables_set)
4242
- [`error`](#error)
4343
- [Evaluation details](#evaluation-details)
4444
- [Modules](#modules)
@@ -86,7 +86,7 @@ Add the Featurevisor Java SDK as a dependency with your desired version:
8686
<dependency>
8787
<groupId>com.featurevisor</groupId>
8888
<artifactId>featurevisor-java</artifactId>
89-
<version>3.0.0</version>
89+
<version>4.0.0</version>
9090
</dependency>
9191
</dependencies>
9292
```
@@ -136,7 +136,7 @@ Featurevisor f = Featurevisor.createFeaturevisor(
136136

137137
Most applications only need `Featurevisor.createFeaturevisor`, the `Featurevisor` instance type, and `Featurevisor.FeaturevisorOptions`. Public extension and observability types include `FeaturevisorModule`, `FeaturevisorDiagnostic`, and the datafile model types.
138138

139-
Concurrent evaluations are safe after an instance is configured. Do not call state-changing methods such as `setDatafile`, `setContext`, `setSticky`, `addModule`, `removeModule`, or `close` concurrently with evaluations or with each other. Apply those changes from a serialized update path. Module, event, and diagnostic callbacks must synchronize mutable state that they capture.
139+
Concurrent evaluations are safe after an instance is configured. Do not call state changing methods such as `setDatafile`, `setContext`, `setStickyFeatures`, `setStickyVariables`, `addModule`, `removeModule`, or `close` concurrently with evaluations or with each other. Apply those changes from a serialized update path. Module, event, and diagnostic callbacks must synchronize mutable state that they capture.
140140

141141
## Initialization
142142

@@ -167,11 +167,12 @@ We will learn about several different options in the next sections.
167167

168168
## Evaluation types
169169

170-
We can evaluate 3 types of values against a particular [feature](https://featurevisor.com/docs/features/):
170+
We can evaluate flags, variations, variables inside features, and [global variables](https://featurevisor.com/docs/global-variables/):
171171

172172
- [**Flag**](#check-if-enabled) (`boolean`): whether the feature is enabled or not
173173
- [**Variation**](#getting-variation) (`Object`): the variation of the feature (if any)
174174
- [**Variables**](#getting-variables): variable values of the feature (if any)
175+
- [**Global variables**](#getting-global-variables): reusable values that are not owned by a feature
175176

176177
These evaluations are run against the provided context.
177178

@@ -384,15 +385,29 @@ If a variable schema type is `json` and the resolved value is a malformed string
384385
- `getVariableJSONNode(...)`
385386
- `getVariableJSON(...)`
386387

387-
## Getting all evaluations
388+
## Getting global variables
389+
390+
Global variables use the same overloaded methods as variables inside features. A call with one key evaluates a global variable, while a call with a feature key and variable key evaluates a variable owned by that feature:
391+
392+
```java
393+
String message = f.getVariableString("welcomeMessage", context, null);
394+
Object value = f.getVariable("checkoutSettings", context);
395+
Evaluation evaluation = f.evaluateVariable("checkoutSettings", context);
396+
```
397+
398+
The type specific methods are `getVariableBoolean`, `getVariableString`, `getVariableInteger`, `getVariableDouble`, `getVariableArray`, `getVariableObject`, and `getVariableJSON`.
399+
400+
Global variables resolve sticky values first, then required features, then the first matching override, and finally their default value. If required features are unmet, `disabledValue` is used unless `useDefaultWhenDisabled` is enabled. Caller defaults are only used when the evaluation itself has no value.
401+
402+
## Getting aggregate evaluations
388403

389404
You can get evaluations of all features available in the SDK instance:
390405

391406
```java
392407
import com.featurevisor.sdk.EvaluatedFeatures;
393408
import com.featurevisor.sdk.EvaluatedFeature;
394409

395-
EvaluatedFeatures allEvaluations = f.getAllEvaluations(context);
410+
EvaluatedFeatures allEvaluations = f.getFeatureEvaluations(context);
396411

397412
// Access the evaluations map
398413
Map<String, EvaluatedFeature> evaluations = allEvaluations.getValue();
@@ -416,13 +431,17 @@ System.out.println(evaluations);
416431

417432
This is handy especially when you want to pass all evaluations from a backend application to the frontend.
418433

419-
## Sticky
434+
Global variables can be evaluated together as well:
420435

421-
For the lifecycle of the SDK instance in your application, you can set some features with sticky values, meaning that they will not be evaluated against the fetched [datafile](https://featurevisor.com/docs/building-datafiles/):
436+
```java
437+
Map<String, Object> variables = f.getVariableEvaluations(context, null, null);
438+
```
422439

423-
Sticky values belong to an SDK or child instance. Evaluation options do not accept sticky overrides; use `new Featurevisor.SpawnOptions().sticky(...)` when a child needs its own sticky state.
440+
## Sticky features and variables
424441

425-
### Initialize with sticky
442+
For the lifecycle of the SDK instance in your application, you can set some features with sticky values, meaning that they will not be evaluated against the fetched [datafile](https://featurevisor.com/docs/building-datafiles/):
443+
444+
Sticky values belong to an SDK or child instance. Feature sticky values and global variable sticky values are independent.
426445

427446
```java
428447
Map<String, Object> stickyFeatures = new HashMap<>();
@@ -443,20 +462,20 @@ stickyFeatures.put("anotherFeatureKey", anotherFeatureSticky);
443462

444463
Featurevisor f = Featurevisor.createFeaturevisor(new Featurevisor.FeaturevisorOptions()
445464
.datafile(datafile)
446-
.sticky(stickyFeatures));
465+
.stickyFeatures(stickyFeatures)
466+
.stickyVariables(Map.of("welcomeMessage", "Hello")));
447467
```
448468

449469
Once initialized with sticky features, the SDK will look for values there first before evaluating the targeting conditions and going through the bucketing process.
450470

451-
### Set sticky afterwards
452-
453471
You can also set sticky features after the SDK is initialized:
454472

455473
```java
456474
Map<String, Object> stickyFeatures = new HashMap<>();
457475
// ... build sticky features map
458476

459-
f.setSticky(stickyFeatures, true); // replace existing sticky features
477+
f.setStickyFeatures(stickyFeatures, true); // replace existing sticky features
478+
f.setStickyVariables(Map.of("welcomeMessage", "Welcome back"), true);
460479
```
461480

462481
## Setting datafile
@@ -469,7 +488,7 @@ f.setDatafile(datafileContent);
469488

470489
### Merging by default
471490

472-
By default, `setDatafile(datafile)` merges the incoming datafile with the SDK's stored datafile. Incoming top-level metadata is used, and incoming segments/features override existing segments/features with the same keys.
491+
By default, `setDatafile(datafile)` merges the incoming datafile with the SDK's stored datafile. Incoming top level metadata is used, and incoming segments, features, and global variables override existing entities with the same keys.
473492

474493
This means you can call `setDatafile` more than once with different datafiles, and the SDK instance accumulates their features and segments together. This is what makes [loading datafiles on demand](#loading-datafiles-on-demand) possible.
475494

@@ -587,14 +606,17 @@ FeaturevisorUnsubscribe unsubscribe = f.on(FeaturevisorEventName.DATAFILE_SET, (
587606
@SuppressWarnings("unchecked")
588607
List<String> features = (List<String>) event.get("features");
589608

609+
@SuppressWarnings("unchecked")
610+
List<String> variables = (List<String>) event.get("variables");
611+
590612
// handle here
591613
});
592614

593615
// stop listening to the event
594616
unsubscribe.unsubscribe();
595617
```
596618

597-
The `features` array will contain keys of features that have either been:
619+
The `features` and `variables` arrays contain directly changed entities and entities affected through segment or required feature dependencies.
598620

599621
- added, or
600622
- updated, or
@@ -614,10 +636,10 @@ FeaturevisorUnsubscribe unsubscribe = f.on(FeaturevisorEventName.CONTEXT_SET, (e
614636
});
615637
```
616638

617-
### `sticky_set`
639+
### `sticky_features_set`
618640

619641
```java
620-
FeaturevisorUnsubscribe unsubscribe = f.on(FeaturevisorEventName.STICKY_SET, (event) -> {
642+
FeaturevisorUnsubscribe unsubscribe = f.on(FeaturevisorEventName.STICKY_FEATURES_SET, (event) -> {
621643
Boolean replaced = (Boolean) event.get("replaced"); // true if sticky features got replaced
622644
@SuppressWarnings("unchecked")
623645
List<String> features = (List<String>) event.get("features"); // list of all affected feature keys
@@ -626,6 +648,15 @@ FeaturevisorUnsubscribe unsubscribe = f.on(FeaturevisorEventName.STICKY_SET, (ev
626648
});
627649
```
628650

651+
### `sticky_variables_set`
652+
653+
```java
654+
FeaturevisorUnsubscribe unsubscribe = f.on(FeaturevisorEventName.STICKY_VARIABLES_SET, (event) -> {
655+
@SuppressWarnings("unchecked")
656+
List<String> variables = (List<String>) event.get("variables");
657+
});
658+
```
659+
629660
### `error`
630661

631662
```java
@@ -650,6 +681,9 @@ Evaluation evaluation = f.evaluateVariation(featureKey, context);
650681

651682
// variable
652683
Evaluation evaluation = f.evaluateVariable(featureKey, variableKey, context);
684+
685+
// global variable
686+
Evaluation evaluation = f.evaluateVariable(variableKey, context);
653687
```
654688

655689
The returned `Evaluation` exposes the following properties:
@@ -673,6 +707,8 @@ And optionally these properties depending on whether you are evaluating a featur
673707

674708
Modules allow you to intercept the evaluation process and customize it further as per your needs.
675709

710+
For feature evaluations, all `before` callbacks run in registration order, followed by all `beforeEvaluation` callbacks. After evaluation and caller defaults, all `afterEvaluation` callbacks run, followed by all `after` callbacks. Global variable evaluations use only `beforeEvaluation` and `afterEvaluation`. Required feature checks run through the complete module pipeline, and transformed defaults are preserved.
711+
676712
### Defining a module
677713

678714
A module is a `FeaturevisorModule` with a unique `name` and optional lifecycle functions:
@@ -692,6 +728,9 @@ FeaturevisorModule myCustomModule = new FeaturevisorModule("my-custom-module")
692728
return options.copy().context(context);
693729
})
694730

731+
// before any feature or global variable evaluation
732+
.beforeEvaluation(options -> options)
733+
695734
// configure bucket key
696735
.bucketKey(options -> {
697736
String bucketKey = options.getBucketKey();
@@ -707,6 +746,9 @@ FeaturevisorModule myCustomModule = new FeaturevisorModule("my-custom-module")
707746
// after evaluation
708747
.after((evaluation, options) -> evaluation)
709748

749+
// after any feature or global variable evaluation
750+
.afterEvaluation((evaluation, options) -> evaluation)
751+
710752
// called by f.close()
711753
.close(() -> {
712754
// clean up resources
@@ -780,7 +822,8 @@ String variableValue = childF.getVariableString("my_feature", "my_variable");
780822
Similar to parent SDK, child instances also support several additional methods:
781823

782824
- `setContext`
783-
- `setSticky`
825+
- `setStickyFeatures`
826+
- `setStickyVariables`
784827
- `evaluateFlag`
785828
- `isEnabled`
786829
- `evaluateVariation`
@@ -795,7 +838,8 @@ Similar to parent SDK, child instances also support several additional methods:
795838
- `getVariableObject`
796839
- `getVariableJSON`
797840
- `getVariableJSONNode`
798-
- `getAllEvaluations`
841+
- `getFeatureEvaluations`
842+
- `getVariableEvaluations`
799843
- `on`
800844
- `close`
801845

@@ -855,7 +899,7 @@ Add the provider with the same version as the Featurevisor Java SDK:
855899
<dependency>
856900
<groupId>com.featurevisor</groupId>
857901
<artifactId>featurevisor-openfeature</artifactId>
858-
<version>FEATUREVISOR_VERSION</version>
902+
<version>4.0.0</version>
859903
</dependency>
860904
```
861905

@@ -880,9 +924,9 @@ var client = api.getClient();
880924
boolean enabled = client.getBooleanValue("checkout", false, new ImmutableContext("user-123"));
881925
```
882926

883-
Use `checkout` for a flag, `checkout:variation` for its variation, and `checkout:title` for its `title` variable. Boolean variables use the boolean resolver. Lists, structures, and JSON variables use the object resolver.
927+
Use `checkout` for a flag, `checkout:variation` for its variation, `checkout:title` for its `title` variable, and `variable:welcomeMessage` for a global variable. Boolean variables use the boolean resolver. Lists, structures, and JSON variables use the object resolver.
884928

885-
OpenFeature's targeting key maps to `userId` by default. `targetingKeyField`, `keySeparator`, and `variationKey` on `FeaturevisorOpenFeatureProvider.Options` can customize the mapping.
929+
OpenFeature's targeting key maps to `userId` by default. `targetingKeyField`, `keySeparator`, `variationKey`, and `globalVariablePrefix` on `FeaturevisorOpenFeatureProvider.Options` can customize the mapping. The global variable prefix defaults to `variable` and cannot contain the configured separator.
886930

887931
You can also reuse an existing Featurevisor instance:
888932

@@ -927,7 +971,7 @@ $ make verify-artifacts
927971
### Releasing
928972

929973
1. Merge the release changes into `main`.
930-
2. Tag the release with a `v` prefix, such as `v3.0.0`, and push the tag.
974+
2. Tag the release with a `v` prefix, such as `v4.0.0`, and push the tag.
931975
3. GitHub Actions verifies and publishes the parent POM, Java SDK, and OpenFeature provider to [GitHub Packages](https://github.com/orgs/featurevisor/packages?repo_name=featurevisor-java).
932976
4. Create the corresponding [GitHub release](https://github.com/featurevisor/featurevisor-java/releases).
933977

0 commit comments

Comments
 (0)