diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml index 88b02783..bce13406 100644 --- a/.github/workflows/lint-pr.yml +++ b/.github/workflows/lint-pr.yml @@ -18,6 +18,6 @@ jobs: name: Validate PR title runs-on: ubuntu-latest steps: - - uses: amannn/action-semantic-pull-request@80c0371c57c5142ed6c844270bba1864bac8a4c6 + - uses: amannn/action-semantic-pull-request@40166f00814508ec3201fc8595b393d451c8cd80 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml index d3d0569e..ad5c5638 100644 --- a/.github/workflows/merge.yml +++ b/.github/workflows/merge.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@cbb722410c2e876e24abbe8de2cc27693e501dcb - name: Set up JDK 8 uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b with: @@ -32,7 +32,7 @@ jobs: server-password: ${{ secrets.OSSRH_PASSWORD }} - name: Cache local Maven repository - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} @@ -49,7 +49,7 @@ jobs: run: mvn --batch-mode --update-snapshots verify - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4.6.0 + uses: codecov/codecov-action@v5.1.1 with: token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos flags: unittests # optional diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index 1a8ee2e8..1eaeaf48 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out the code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + uses: actions/checkout@cbb722410c2e876e24abbe8de2cc27693e501dcb - name: Set up JDK 8 uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b @@ -20,12 +20,12 @@ jobs: cache: maven - name: Initialize CodeQL - uses: github/codeql-action/init@467d7e6d9e138cb28eeebd638e6f0dbab1fd435e + uses: github/codeql-action/init@6f9e628e6f9a18c785dd746325ba455111df1b67 with: languages: java - name: Cache local Maven repository - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} @@ -36,7 +36,7 @@ jobs: run: mvn --batch-mode --update-snapshots --activate-profiles e2e verify - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4.6.0 + uses: codecov/codecov-action@v5.1.1 with: token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos flags: unittests # optional @@ -45,4 +45,4 @@ jobs: verbose: true # optional (default = false) - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@467d7e6d9e138cb28eeebd638e6f0dbab1fd435e + uses: github/codeql-action/analyze@6f9e628e6f9a18c785dd746325ba455111df1b67 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9b1ac340..251ae637 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: # These steps are only run if this was a merged release-please PR - name: checkout if: ${{ steps.release.outputs.release_created }} - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + uses: actions/checkout@cbb722410c2e876e24abbe8de2cc27693e501dcb - name: Set up JDK 8 if: ${{ steps.release.outputs.release_created }} uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b diff --git a/.github/workflows/static-code-scanning.yaml b/.github/workflows/static-code-scanning.yaml index ebd6739f..41ea5f52 100644 --- a/.github/workflows/static-code-scanning.yaml +++ b/.github/workflows/static-code-scanning.yaml @@ -29,16 +29,16 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + uses: actions/checkout@cbb722410c2e876e24abbe8de2cc27693e501dcb # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@467d7e6d9e138cb28eeebd638e6f0dbab1fd435e + uses: github/codeql-action/init@6f9e628e6f9a18c785dd746325ba455111df1b67 with: languages: java - name: Autobuild - uses: github/codeql-action/autobuild@467d7e6d9e138cb28eeebd638e6f0dbab1fd435e + uses: github/codeql-action/autobuild@6f9e628e6f9a18c785dd746325ba455111df1b67 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@467d7e6d9e138cb28eeebd638e6f0dbab1fd435e + uses: github/codeql-action/analyze@6f9e628e6f9a18c785dd746325ba455111df1b67 diff --git a/.gitmodules b/.gitmodules index 5893173a..476d155d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "test-harness"] - path = test-harness - url = https://github.com/open-feature/test-harness +[submodule "spec"] + path = spec + url = https://github.com/open-feature/spec/ diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c6516de7..12295c5d 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1 +1 @@ -{".":"1.12.2"} \ No newline at end of file +{".":"1.13.0"} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bfa9ddb..d91800bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,73 @@ # Changelog +## [1.13.0](https://github.com/open-feature/java-sdk/compare/v1.12.2...v1.13.0) (2024-12-07) + + +### ๐Ÿ› Bug Fixes + +* **deps:** update dependency org.projectlombok:lombok to v1.18.36 ([#1219](https://github.com/open-feature/java-sdk/issues/1219)) ([9cadc71](https://github.com/open-feature/java-sdk/commit/9cadc71d9d8a2a88f9c716c27eb939f423b95fa0)) + + +### โœจ New Features + +* add tracking as per spec ([#1228](https://github.com/open-feature/java-sdk/issues/1228)) ([64ad644](https://github.com/open-feature/java-sdk/commit/64ad644bdbb6a4535da8ec7628e74d5f41f7ebec)) + + +### ๐Ÿงน Chore + +* **deps:** update actions/cache digest to 1bd1e32 ([#1237](https://github.com/open-feature/java-sdk/issues/1237)) ([da725d8](https://github.com/open-feature/java-sdk/commit/da725d89e03d499a37307cca47b2c51af5ac8782)) +* **deps:** update actions/checkout digest to 3b9b8c8 ([#1206](https://github.com/open-feature/java-sdk/issues/1206)) ([446e298](https://github.com/open-feature/java-sdk/commit/446e2987e9b80175dff0ea72de9f58ba8e0dd323)) +* **deps:** update actions/checkout digest to cbb7224 ([#1216](https://github.com/open-feature/java-sdk/issues/1216)) ([273efc6](https://github.com/open-feature/java-sdk/commit/273efc62a7bb2e3fe962036d82818eb1da43b197)) +* **deps:** update amannn/action-semantic-pull-request digest to 40166f0 ([#1212](https://github.com/open-feature/java-sdk/issues/1212)) ([d5228f5](https://github.com/open-feature/java-sdk/commit/d5228f5ccfa55753178425c55a02af1833168513)) +* **deps:** update codecov/codecov-action action to v5 ([#1217](https://github.com/open-feature/java-sdk/issues/1217)) ([7aa77b8](https://github.com/open-feature/java-sdk/commit/7aa77b8614401c56e8387d55382e4be115a7d1ef)) +* **deps:** update codecov/codecov-action action to v5.0.2 ([#1218](https://github.com/open-feature/java-sdk/issues/1218)) ([1b4947f](https://github.com/open-feature/java-sdk/commit/1b4947f108c15a4777bb35bafb631a40c7e20e77)) +* **deps:** update codecov/codecov-action action to v5.0.3 ([#1223](https://github.com/open-feature/java-sdk/issues/1223)) ([e91194a](https://github.com/open-feature/java-sdk/commit/e91194ae16c1d68033a750050af18de68a618f61)) +* **deps:** update codecov/codecov-action action to v5.0.4 ([#1224](https://github.com/open-feature/java-sdk/issues/1224)) ([19ed5c7](https://github.com/open-feature/java-sdk/commit/19ed5c7c97dc286a85faae1c4906508f97191497)) +* **deps:** update codecov/codecov-action action to v5.0.6 ([#1226](https://github.com/open-feature/java-sdk/issues/1226)) ([13811dc](https://github.com/open-feature/java-sdk/commit/13811dcf254b604ec73b4df184d432f1dc404398)) +* **deps:** update codecov/codecov-action action to v5.0.7 ([#1227](https://github.com/open-feature/java-sdk/issues/1227)) ([234062c](https://github.com/open-feature/java-sdk/commit/234062cf338036b3b942b83c00b31191fb626432)) +* **deps:** update codecov/codecov-action action to v5.1.1 ([#1238](https://github.com/open-feature/java-sdk/issues/1238)) ([c5ad1b4](https://github.com/open-feature/java-sdk/commit/c5ad1b4d4f805a6ae070eabc6de38b37dd759c05)) +* **deps:** update dependency com.github.spotbugs:spotbugs-maven-plugin to v4.8.6.6 ([#1213](https://github.com/open-feature/java-sdk/issues/1213)) ([92c8791](https://github.com/open-feature/java-sdk/commit/92c87913ac417b8b3651290a4df828bdf5d501b9)) +* **deps:** update dependency net.bytebuddy:byte-buddy to v1.15.10 ([#1202](https://github.com/open-feature/java-sdk/issues/1202)) ([d959059](https://github.com/open-feature/java-sdk/commit/d95905917730dcb8724fe166682ca773a536eb9b)) +* **deps:** update dependency net.bytebuddy:byte-buddy to v1.15.8 ([#1195](https://github.com/open-feature/java-sdk/issues/1195)) ([309f28b](https://github.com/open-feature/java-sdk/commit/309f28b520a8f629a500c359b1f522ba687bcc6b)) +* **deps:** update dependency net.bytebuddy:byte-buddy to v1.15.9 ([#1197](https://github.com/open-feature/java-sdk/issues/1197)) ([54a2345](https://github.com/open-feature/java-sdk/commit/54a234519f36ea803ec8574f27c94a9f754bf822)) +* **deps:** update dependency net.bytebuddy:byte-buddy-agent to v1.15.10 ([#1203](https://github.com/open-feature/java-sdk/issues/1203)) ([2bb2ed3](https://github.com/open-feature/java-sdk/commit/2bb2ed39928e0e15d369741df8b877c751e8d122)) +* **deps:** update dependency net.bytebuddy:byte-buddy-agent to v1.15.8 ([#1196](https://github.com/open-feature/java-sdk/issues/1196)) ([30eb2ce](https://github.com/open-feature/java-sdk/commit/30eb2ce082ae2854025be084da98fb856dbcd17c)) +* **deps:** update dependency net.bytebuddy:byte-buddy-agent to v1.15.9 ([#1198](https://github.com/open-feature/java-sdk/issues/1198)) ([e32a712](https://github.com/open-feature/java-sdk/commit/e32a712615f3b1be9cff61f1337d5b00c365c8f5)) +* **deps:** update dependency org.apache.maven.plugins:maven-checkstyle-plugin to v3.6.0 ([#1188](https://github.com/open-feature/java-sdk/issues/1188)) ([89c7f85](https://github.com/open-feature/java-sdk/commit/89c7f85da436b9f16193948183a1ca54eea6ceef)) +* **deps:** update dependency org.apache.maven.plugins:maven-dependency-plugin to v3.8.1 ([#1187](https://github.com/open-feature/java-sdk/issues/1187)) ([5c7c287](https://github.com/open-feature/java-sdk/commit/5c7c28706e4614061b042080820b9efd04afc342)) +* **deps:** update dependency org.apache.maven.plugins:maven-failsafe-plugin to v3.5.2 ([#1199](https://github.com/open-feature/java-sdk/issues/1199)) ([08da9a3](https://github.com/open-feature/java-sdk/commit/08da9a34395a3e96dc2172f0f0533a4905cff204)) +* **deps:** update dependency org.apache.maven.plugins:maven-javadoc-plugin to v3.11.1 ([#1201](https://github.com/open-feature/java-sdk/issues/1201)) ([a2a57ab](https://github.com/open-feature/java-sdk/commit/a2a57ab8f1161b5de3a112bbbdc421985baf304b)) +* **deps:** update dependency org.apache.maven.plugins:maven-javadoc-plugin to v3.11.2 ([#1240](https://github.com/open-feature/java-sdk/issues/1240)) ([c87c6e7](https://github.com/open-feature/java-sdk/commit/c87c6e7a760e84a5e8d9a6d935ef35611d1de8ab)) +* **deps:** update dependency org.apache.maven.plugins:maven-pmd-plugin to v3.26.0 ([#1189](https://github.com/open-feature/java-sdk/issues/1189)) ([d5082cd](https://github.com/open-feature/java-sdk/commit/d5082cd5f6907b6e7649813dbbea99cdeab20728)) +* **deps:** update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.5.2 ([#1200](https://github.com/open-feature/java-sdk/issues/1200)) ([d2cb092](https://github.com/open-feature/java-sdk/commit/d2cb092b09966bc2d5a7548e35b71ab2e56e0dee)) +* **deps:** update dependency org.cyclonedx:cyclonedx-maven-plugin to v2.9.1 ([#1230](https://github.com/open-feature/java-sdk/issues/1230)) ([764d665](https://github.com/open-feature/java-sdk/commit/764d6650e659aa93c1da66db348a2eb3641ae92f)) +* **deps:** update dependency org.simplify4u:slf4j2-mock to v2.4.0 ([#1208](https://github.com/open-feature/java-sdk/issues/1208)) ([a3ced47](https://github.com/open-feature/java-sdk/commit/a3ced47e5dc23badae4f008e5cf4e97c588fdfd4)) +* **deps:** update github/codeql-action digest to 024283f ([#1211](https://github.com/open-feature/java-sdk/issues/1211)) ([1df5441](https://github.com/open-feature/java-sdk/commit/1df54411b758c67afaf47f103e357cb551e0efca)) +* **deps:** update github/codeql-action digest to 3096afe ([#1235](https://github.com/open-feature/java-sdk/issues/1235)) ([409fd04](https://github.com/open-feature/java-sdk/commit/409fd042f3921948ef0dabd58d0ef7a4c380b5fb)) +* **deps:** update github/codeql-action digest to 3aa7135 ([#1186](https://github.com/open-feature/java-sdk/issues/1186)) ([4e3a329](https://github.com/open-feature/java-sdk/commit/4e3a329c406cc72a268f05766290633c67a9aae0)) +* **deps:** update github/codeql-action digest to 3d3d628 ([#1229](https://github.com/open-feature/java-sdk/issues/1229)) ([a0723ec](https://github.com/open-feature/java-sdk/commit/a0723ec2f886aa834662f2e54bcce5f052262dac)) +* **deps:** update github/codeql-action digest to 3ef4c08 ([#1205](https://github.com/open-feature/java-sdk/issues/1205)) ([eb4f625](https://github.com/open-feature/java-sdk/commit/eb4f6255615a77c65a79002f1233d1efe5eccd37)) +* **deps:** update github/codeql-action digest to 48c3e26 ([#1193](https://github.com/open-feature/java-sdk/issues/1193)) ([8621944](https://github.com/open-feature/java-sdk/commit/86219446337e9c73a41b8517b1e26fa044d3bbaa)) +* **deps:** update github/codeql-action digest to 4dc1519 ([#1209](https://github.com/open-feature/java-sdk/issues/1209)) ([1c21d24](https://github.com/open-feature/java-sdk/commit/1c21d2444b31f61d6d83dfd8f6982f7ad71f708b)) +* **deps:** update github/codeql-action digest to 5ac2ddd ([#1204](https://github.com/open-feature/java-sdk/issues/1204)) ([3a9fd60](https://github.com/open-feature/java-sdk/commit/3a9fd60fd4a9595a729995a59a0c4ef9625444bc)) +* **deps:** update github/codeql-action digest to 5cb4249 ([#1210](https://github.com/open-feature/java-sdk/issues/1210)) ([a94bd37](https://github.com/open-feature/java-sdk/commit/a94bd37cff0c6d7b9f535335709d69b79db2c91e)) +* **deps:** update github/codeql-action digest to 6a38de6 ([#1190](https://github.com/open-feature/java-sdk/issues/1190)) ([f3163df](https://github.com/open-feature/java-sdk/commit/f3163dfbd4b3997a0335699a2472373a846cf710)) +* **deps:** update github/codeql-action digest to 6e3a010 ([#1214](https://github.com/open-feature/java-sdk/issues/1214)) ([9f37927](https://github.com/open-feature/java-sdk/commit/9f37927eaa60e53d1c7db192ca8e6e117f7f0017)) +* **deps:** update github/codeql-action digest to 6f9e628 ([#1239](https://github.com/open-feature/java-sdk/issues/1239)) ([baaa78b](https://github.com/open-feature/java-sdk/commit/baaa78b7ec34a3e508fda3ed8c3ea5382f1e18ea)) +* **deps:** update github/codeql-action digest to 978ed82 ([#1234](https://github.com/open-feature/java-sdk/issues/1234)) ([bb3272d](https://github.com/open-feature/java-sdk/commit/bb3272d36479bde2594fe0bb64cea21d30299931)) +* **deps:** update github/codeql-action digest to 9f93f47 ([#1191](https://github.com/open-feature/java-sdk/issues/1191)) ([f99de6f](https://github.com/open-feature/java-sdk/commit/f99de6fa55bea093418ecc85ea79e9e30ce03d6b)) +* **deps:** update github/codeql-action digest to a1695c5 ([#1215](https://github.com/open-feature/java-sdk/issues/1215)) ([6d3bb69](https://github.com/open-feature/java-sdk/commit/6d3bb694204107f21552b48c5f6f056fa37e6cc0)) +* **deps:** update github/codeql-action digest to a6c8729 ([#1222](https://github.com/open-feature/java-sdk/issues/1222)) ([bbc934c](https://github.com/open-feature/java-sdk/commit/bbc934c6d91af39b9ff384ebd58756d48b00415a)) +* **deps:** update github/codeql-action digest to acb9cb1 ([#1207](https://github.com/open-feature/java-sdk/issues/1207)) ([21dbd3f](https://github.com/open-feature/java-sdk/commit/21dbd3fc4c29acbb6b74cdb6b82bc5bb4dd5523e)) +* **deps:** update github/codeql-action digest to af49565 ([#1231](https://github.com/open-feature/java-sdk/issues/1231)) ([4bbaf51](https://github.com/open-feature/java-sdk/commit/4bbaf517536386f53bd92ceaf62eb08fe4859e80)) +* **deps:** update github/codeql-action digest to b91f43b ([#1184](https://github.com/open-feature/java-sdk/issues/1184)) ([d0309ea](https://github.com/open-feature/java-sdk/commit/d0309eaa6616ef9e9caf8e605895ac82c8f4d780)) +* **deps:** update github/codeql-action digest to cba5fb5 ([#1221](https://github.com/open-feature/java-sdk/issues/1221)) ([37f0f06](https://github.com/open-feature/java-sdk/commit/37f0f06467b10541755e723ff26144b716a26464)) +* **deps:** update github/codeql-action digest to cbe1897 ([#1194](https://github.com/open-feature/java-sdk/issues/1194)) ([2dba3a7](https://github.com/open-feature/java-sdk/commit/2dba3a737dac6fefcbb1f56b292cacdca62735b5)) +* **deps:** update github/codeql-action digest to e782c3a ([#1220](https://github.com/open-feature/java-sdk/issues/1220)) ([45d0656](https://github.com/open-feature/java-sdk/commit/45d065652004ecc0703af3b9c6fbfd2b45e69056)) +* **deps:** update github/codeql-action digest to ef2fd42 ([#1232](https://github.com/open-feature/java-sdk/issues/1232)) ([b3549a1](https://github.com/open-feature/java-sdk/commit/b3549a1b4aa2bc27c38f66e3a0657b62d8ffc1b4)) +* **deps:** update github/codeql-action digest to f1c289a ([#1233](https://github.com/open-feature/java-sdk/issues/1233)) ([5b460ea](https://github.com/open-feature/java-sdk/commit/5b460ead7e5f21eb7c86e9ae78740a2e26957420)) +* **deps:** update github/codeql-action digest to f8e782a ([#1225](https://github.com/open-feature/java-sdk/issues/1225)) ([3227623](https://github.com/open-feature/java-sdk/commit/32276234257f82de98bcb01094c7219611e2c707)) + ## [1.12.2](https://github.com/open-feature/java-sdk/compare/v1.12.1...v1.12.2) (2024-10-24) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2aafb314..84c9645b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,7 @@ If you're adding tests to cover something in the spec, use the `@Specification` ## End-to-End Tests -The continuous integration runs a set of [gherkin e2e tests](https://github.com/open-feature/test-harness/blob/main/features/evaluation.feature) using `InMemoryProvider`. +The continuous integration runs a set of [gherkin e2e tests](https://github.com/open-feature/spec/blob/main/specification/assets/gherkin/evaluation.feature) using `InMemoryProvider`. to run alone: ``` diff --git a/README.md b/README.md index c710de66..e58ef9d8 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,8 @@ - - Release + + Release @@ -59,7 +59,7 @@ Note that this library is intended to be used in server-side contexts and has no dev.openfeature sdk - 1.12.2 + 1.13.0 ``` @@ -84,7 +84,7 @@ If you would like snapshot builds, this is the relevant repository information: ```groovy dependencies { - implementation 'dev.openfeature:sdk:1.12.2' + implementation 'dev.openfeature:sdk:1.13.0' } ``` @@ -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. | Implemented: โœ… | In-progress: โš ๏ธ | Not implemented yet: โŒ @@ -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. diff --git a/pom.xml b/pom.xml index ab36113f..5c44206f 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ dev.openfeature sdk - 1.12.2 + 1.13.0 UTF-8 @@ -45,7 +45,7 @@ org.projectlombok lombok - 1.18.34 + 1.18.36 provided @@ -128,7 +128,7 @@ org.simplify4u slf4j2-mock - 2.3.0 + 2.4.0 test @@ -164,14 +164,14 @@ net.bytebuddy byte-buddy - 1.15.7 + 1.15.10 test net.bytebuddy byte-buddy-agent - 1.15.7 + 1.15.10 test @@ -200,7 +200,7 @@ org.cyclonedx cyclonedx-maven-plugin - 2.9.0 + 2.9.1 library 1.3 @@ -225,7 +225,7 @@ maven-dependency-plugin - 3.8.0 + 3.8.1 verify @@ -260,7 +260,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.5.1 + 3.5.2 ${surefireArgLine} @@ -275,7 +275,7 @@ org.apache.maven.plugins maven-failsafe-plugin - 3.5.1 + 3.5.2 ${surefireArgLine} @@ -359,7 +359,7 @@ org.apache.maven.plugins maven-pmd-plugin - 3.25.0 + 3.26.0 run-pmd @@ -374,7 +374,7 @@ com.github.spotbugs spotbugs-maven-plugin - 4.8.6.5 + 4.8.6.6 spotbugs-exclusions.xml @@ -407,7 +407,7 @@ org.apache.maven.plugins maven-checkstyle-plugin - 3.5.0 + 3.6.0 checkstyle.xml UTF-8 @@ -477,7 +477,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.10.1 + 3.11.2 true all,-missing @@ -554,22 +554,21 @@ submodule update --init - test-harness + spec - copy-gherkin-tests + copy-evaluation-gherkin-tests validate exec - cp - test-harness/features/evaluation.feature + spec/specification/assets/gherkin/evaluation.feature src/test/resources/features/ diff --git a/spec b/spec new file mode 160000 index 00000000..d4a9a910 --- /dev/null +++ b/spec @@ -0,0 +1 @@ +Subproject commit d4a9a910946eded57cf82d6fd4921785a5e64c2b diff --git a/src/main/java/dev/openfeature/sdk/Client.java b/src/main/java/dev/openfeature/sdk/Client.java index 7b41b9b0..441d31e2 100644 --- a/src/main/java/dev/openfeature/sdk/Client.java +++ b/src/main/java/dev/openfeature/sdk/Client.java @@ -5,17 +5,19 @@ /** * Interface used to resolve flags of varying types. */ -public interface Client extends Features, EventBus { +public interface Client extends Features, Tracking, EventBus { 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); @@ -30,12 +32,14 @@ public interface Client extends Features, EventBus { /** * Fetch the hooks associated to this client. + * * @return A list of {@link Hook}s. */ List getHooks(); /** * Returns the current state of the associated provider. + * * @return the provider state */ ProviderState getProviderState(); diff --git a/src/main/java/dev/openfeature/sdk/FeatureProvider.java b/src/main/java/dev/openfeature/sdk/FeatureProvider.java index f73b6cdf..706818e8 100644 --- a/src/main/java/dev/openfeature/sdk/FeatureProvider.java +++ b/src/main/java/dev/openfeature/sdk/FeatureProvider.java @@ -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) { + + } } diff --git a/src/main/java/dev/openfeature/sdk/ImmutableTrackingEventDetails.java b/src/main/java/dev/openfeature/sdk/ImmutableTrackingEventDetails.java new file mode 100644 index 00000000..b535bb7d --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/ImmutableTrackingEventDetails.java @@ -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 attributes) { + this.value = value; + this.structure = new ImmutableStructure(attributes); + } + + /** + * Returns the optional tracking value. + */ + public Optional getValue() { + return Optional.ofNullable(value); + } + + + @SuppressWarnings("all") + private static class DelegateExclusions { + @ExcludeFromGeneratedCoverageReport + public Map merge(Function, Structure> newStructure, + Map base, + Map overriding) { + return null; + } + } +} diff --git a/src/main/java/dev/openfeature/sdk/MutableTrackingEventDetails.java b/src/main/java/dev/openfeature/sdk/MutableTrackingEventDetails.java new file mode 100644 index 00000000..9f0de8c3 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/MutableTrackingEventDetails.java @@ -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 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) { + 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 Map merge(Function, Structure> newStructure, + Map base, + Map overriding) { + return null; + } + } +} diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java index f56df15a..ea566e65 100644 --- a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java +++ b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java @@ -1,13 +1,5 @@ package dev.openfeature.sdk; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.List; -import java.util.function.Consumer; - import dev.openfeature.sdk.exceptions.ExceptionUtils; import dev.openfeature.sdk.exceptions.FatalError; import dev.openfeature.sdk.exceptions.GeneralError; @@ -19,6 +11,15 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; + /** * OpenFeature Client implementation. * You should not instantiate this or reference this class. @@ -28,8 +29,8 @@ * @deprecated // TODO: eventually we will make this non-public. See issue #872 */ @Slf4j -@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.BeanMembersShouldSerialize", "PMD.UnusedLocalVariable", - "unchecked", "rawtypes" }) +@SuppressWarnings({"PMD.DataflowAnomalyAnalysis", "PMD.BeanMembersShouldSerialize", "PMD.UnusedLocalVariable", + "unchecked", "rawtypes"}) @Deprecated() // TODO: eventually we will make this non-public. See issue #872 public class OpenFeatureClient implements Client { @@ -67,11 +68,56 @@ public OpenFeatureClient( this.hookSupport = new HookSupport(); } + /** + * {@inheritDoc} + */ @Override public ProviderState getProviderState() { return openfeatureApi.getFeatureProviderStateManager(domain).getState(); } + /** + * {@inheritDoc} + */ + @Override + public void track(String trackingEventName) { + validateTrackingEventName(trackingEventName); + invokeTrack(trackingEventName, null, null); + } + + + /** + * {@inheritDoc} + */ + @Override + public void track(String trackingEventName, EvaluationContext context) { + validateTrackingEventName(trackingEventName); + Objects.requireNonNull(context); + invokeTrack(trackingEventName, context, null); + } + + /** + * {@inheritDoc} + */ + @Override + public void track(String trackingEventName, TrackingEventDetails details) { + validateTrackingEventName(trackingEventName); + Objects.requireNonNull(details); + invokeTrack(trackingEventName, null, details); + } + + /** + * {@inheritDoc} + */ + @Override + public void track(String trackingEventName, EvaluationContext context, TrackingEventDetails details) { + validateTrackingEventName(trackingEventName); + Objects.requireNonNull(context); + Objects.requireNonNull(details); + invokeTrack(trackingEventName, mergeEvaluationContext(context), details); + } + + /** * {@inheritDoc} */ @@ -115,7 +161,7 @@ public EvaluationContext getEvaluationContext() { } private FlagEvaluationDetails evaluateFlag(FlagValueType type, String key, T defaultValue, - EvaluationContext ctx, FlagEvaluationOptions options) { + EvaluationContext ctx, FlagEvaluationOptions options) { FlagEvaluationOptions flagOptions = ObjectUtils.defaultIfNull(options, () -> FlagEvaluationOptions.builder().build()); Map hints = Collections.unmodifiableMap(flagOptions.getHookHints()); @@ -183,6 +229,19 @@ private static void enrichDetailsWithErrorDefaults(T defaultValue, FlagEvalu details.setReason(Reason.ERROR.toString()); } + private static void validateTrackingEventName(String str) { + Objects.requireNonNull(str); + if (str.isEmpty()) { + throw new IllegalArgumentException("trackingEventName cannot be empty"); + } + } + + private void invokeTrack(String trackingEventName, EvaluationContext context, TrackingEventDetails details) { + openfeatureApi.getFeatureProviderStateManager(domain) + .getProvider() + .track(trackingEventName, mergeEvaluationContext(context), details); + } + /** * Merge invocation contexts with API, transaction and client contexts. * Does not merge before context. @@ -244,7 +303,7 @@ public Boolean getBooleanValue(String key, Boolean defaultValue, EvaluationConte @Override public Boolean getBooleanValue(String key, Boolean defaultValue, EvaluationContext ctx, - FlagEvaluationOptions options) { + FlagEvaluationOptions options) { return getBooleanDetails(key, defaultValue, ctx, options).getValue(); } @@ -260,7 +319,7 @@ public FlagEvaluationDetails getBooleanDetails(String key, Boolean defa @Override public FlagEvaluationDetails getBooleanDetails(String key, Boolean defaultValue, EvaluationContext ctx, - FlagEvaluationOptions options) { + FlagEvaluationOptions options) { return this.evaluateFlag(FlagValueType.BOOLEAN, key, defaultValue, ctx, options); } @@ -276,7 +335,7 @@ public String getStringValue(String key, String defaultValue, EvaluationContext @Override public String getStringValue(String key, String defaultValue, EvaluationContext ctx, - FlagEvaluationOptions options) { + FlagEvaluationOptions options) { return getStringDetails(key, defaultValue, ctx, options).getValue(); } @@ -292,7 +351,7 @@ public FlagEvaluationDetails getStringDetails(String key, String default @Override public FlagEvaluationDetails getStringDetails(String key, String defaultValue, EvaluationContext ctx, - FlagEvaluationOptions options) { + FlagEvaluationOptions options) { return this.evaluateFlag(FlagValueType.STRING, key, defaultValue, ctx, options); } @@ -308,7 +367,7 @@ public Integer getIntegerValue(String key, Integer defaultValue, EvaluationConte @Override public Integer getIntegerValue(String key, Integer defaultValue, EvaluationContext ctx, - FlagEvaluationOptions options) { + FlagEvaluationOptions options) { return getIntegerDetails(key, defaultValue, ctx, options).getValue(); } @@ -324,7 +383,7 @@ public FlagEvaluationDetails getIntegerDetails(String key, Integer defa @Override public FlagEvaluationDetails getIntegerDetails(String key, Integer defaultValue, EvaluationContext ctx, - FlagEvaluationOptions options) { + FlagEvaluationOptions options) { return this.evaluateFlag(FlagValueType.INTEGER, key, defaultValue, ctx, options); } @@ -340,7 +399,7 @@ public Double getDoubleValue(String key, Double defaultValue, EvaluationContext @Override public Double getDoubleValue(String key, Double defaultValue, EvaluationContext ctx, - FlagEvaluationOptions options) { + FlagEvaluationOptions options) { return this.evaluateFlag(FlagValueType.DOUBLE, key, defaultValue, ctx, options).getValue(); } @@ -356,7 +415,7 @@ public FlagEvaluationDetails getDoubleDetails(String key, Double default @Override public FlagEvaluationDetails getDoubleDetails(String key, Double defaultValue, EvaluationContext ctx, - FlagEvaluationOptions options) { + FlagEvaluationOptions options) { return this.evaluateFlag(FlagValueType.DOUBLE, key, defaultValue, ctx, options); } @@ -372,7 +431,7 @@ public Value getObjectValue(String key, Value defaultValue, EvaluationContext ct @Override public Value getObjectValue(String key, Value defaultValue, EvaluationContext ctx, - FlagEvaluationOptions options) { + FlagEvaluationOptions options) { return getObjectDetails(key, defaultValue, ctx, options).getValue(); } @@ -383,13 +442,13 @@ public FlagEvaluationDetails getObjectDetails(String key, Value defaultVa @Override public FlagEvaluationDetails getObjectDetails(String key, Value defaultValue, - EvaluationContext ctx) { + EvaluationContext ctx) { return getObjectDetails(key, defaultValue, ctx, FlagEvaluationOptions.builder().build()); } @Override public FlagEvaluationDetails getObjectDetails(String key, Value defaultValue, EvaluationContext ctx, - FlagEvaluationOptions options) { + FlagEvaluationOptions options) { return this.evaluateFlag(FlagValueType.OBJECT, key, defaultValue, ctx, options); } diff --git a/src/main/java/dev/openfeature/sdk/Tracking.java b/src/main/java/dev/openfeature/sdk/Tracking.java new file mode 100644 index 00000000..ec9c8a8f --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/Tracking.java @@ -0,0 +1,42 @@ +package dev.openfeature.sdk; + +/** + * Interface for Tracking events. + */ +public interface Tracking { + /** + * Performs tracking of a particular action or application state. + * + * @param trackingEventName Event name to track + * @throws IllegalArgumentException if {@code trackingEventName} is null + */ + void track(String trackingEventName); + + /** + * Performs tracking of a particular action or application state. + * + * @param trackingEventName Event name to track + * @param context Evaluation context used in flag evaluation + * @throws IllegalArgumentException if {@code trackingEventName} is null + */ + void track(String trackingEventName, EvaluationContext context); + + /** + * Performs tracking of a particular action or application state. + * + * @param trackingEventName Event name to track + * @param details Data pertinent to a particular tracking event + * @throws IllegalArgumentException if {@code trackingEventName} is null + */ + void track(String trackingEventName, TrackingEventDetails details); + + /** + * Performs tracking of a particular action or application state. + * + * @param trackingEventName Event name to track + * @param context Evaluation context used in flag evaluation + * @param details Data pertinent to a particular tracking event + * @throws IllegalArgumentException if {@code trackingEventName} is null + */ + void track(String trackingEventName, EvaluationContext context, TrackingEventDetails details); +} diff --git a/src/main/java/dev/openfeature/sdk/TrackingEventDetails.java b/src/main/java/dev/openfeature/sdk/TrackingEventDetails.java new file mode 100644 index 00000000..76b20fbb --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/TrackingEventDetails.java @@ -0,0 +1,7 @@ +package dev.openfeature.sdk; + +/** + * Data pertinent to a particular tracking event. + */ +public interface TrackingEventDetails extends Structure { +} diff --git a/src/test/java/dev/openfeature/sdk/MutableTrackingEventDetailsTest.java b/src/test/java/dev/openfeature/sdk/MutableTrackingEventDetailsTest.java new file mode 100644 index 00000000..a169107f --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/MutableTrackingEventDetailsTest.java @@ -0,0 +1,51 @@ +package dev.openfeature.sdk; + +import com.google.common.collect.Lists; +import org.junit.jupiter.api.Test; + +import java.time.Instant; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class MutableTrackingEventDetailsTest { + + + @Test + void hasDefaultValue() { + MutableTrackingEventDetails track = new MutableTrackingEventDetails(); + assertFalse(track.getValue().isPresent()); + } + + @Test + void shouldUseCorrectValue() { + MutableTrackingEventDetails track = new MutableTrackingEventDetails(3); + assertThat(track.getValue()).hasValue(3); + } + + @Test + void shouldStoreAttributes() { + MutableTrackingEventDetails track = new MutableTrackingEventDetails(); + track.add("key0", true); + track.add("key1", 1); + track.add("key2", "2"); + track.add("key3", 1d); + track.add("key4", 4); + track.add("key5", Instant.parse("2023-12-03T10:15:30Z")); + track.add("key6", new MutableContext()); + track.add("key7", new Value(7)); + track.add("key8", Lists.newArrayList(new Value(8), new Value(9))); + + assertEquals(new Value(true), track.getValue("key0")); + assertEquals(new Value(1), track.getValue("key1")); + assertEquals(new Value("2"), track.getValue("key2")); + assertEquals(new Value(1d), track.getValue("key3")); + assertEquals(new Value(4), track.getValue("key4")); + assertEquals(new Value(Instant.parse("2023-12-03T10:15:30Z")), track.getValue("key5")); + assertEquals(new Value(new MutableContext()), track.getValue("key6")); + assertEquals(new Value(7), track.getValue("key7")); + assertArrayEquals(new Object[]{new Value(8), new Value(9)}, track.getValue("key8").asList().toArray()); + } +} \ No newline at end of file diff --git a/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java b/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java index 23c758e9..026170a7 100644 --- a/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java +++ b/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java @@ -12,6 +12,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; class OpenFeatureAPITest { @@ -101,4 +104,21 @@ void getStateReturnsTheStateOfTheAppropriateProvider() throws Exception { assertThat(OpenFeatureAPI.getInstance().getClient(domain).getProviderState()) .isEqualTo(ProviderState.READY); } + + + @Test + void featureProviderTrackIsCalled() throws Exception { + FeatureProvider featureProvider = mock(FeatureProvider.class); + FeatureProviderTestUtils.setFeatureProvider(featureProvider); + + OpenFeatureAPI.getInstance() + .getClient() + .track("track-event", + new ImmutableContext(), + new MutableTrackingEventDetails(22.2f)); + + verify(featureProvider).initialize(any()); + verify(featureProvider).getMetadata(); + verify(featureProvider).track(any(), any(), any()); + } } diff --git a/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java b/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java index 69d38a48..3c82fd65 100644 --- a/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java +++ b/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java @@ -5,8 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; +import static org.mockito.Mockito.*; import java.util.HashMap; import java.util.concurrent.atomic.AtomicBoolean; @@ -79,6 +78,7 @@ void setEvaluationContextShouldAllowChaining() { assertEquals(client, result); } + @Test @DisplayName("Should not call evaluation methods when the provider has state FATAL") void shouldNotCallEvaluationMethodsWhenProviderIsInFatalErrorState() { diff --git a/src/test/java/dev/openfeature/sdk/TrackingSpecTest.java b/src/test/java/dev/openfeature/sdk/TrackingSpecTest.java new file mode 100644 index 00000000..6d195607 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/TrackingSpecTest.java @@ -0,0 +1,189 @@ +package dev.openfeature.sdk; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import dev.openfeature.sdk.fixtures.ProviderFixture; +import dev.openfeature.sdk.testutils.FeatureProviderTestUtils; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.notNull; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.argThat; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +class TrackingSpecTest { + + private OpenFeatureAPI api; + private Client client; + + @BeforeEach + void getApiInstance() { + api = OpenFeatureAPI.getInstance(); + client = api.getClient(); + } + + + @Specification(number = "6.1.1.1", text = "The `client` MUST define a function for tracking the occurrence of " + + "a particular action or application state, with parameters `tracking event name` (string, required), " + + "`evaluation context` (optional) and `tracking event details` (optional), which returns nothing.") + @Specification(number = "6.1.2.1", text = "The `client` MUST define a function for tracking the occurrence of a " + + "particular action or application state, with parameters `tracking event name` (string, required) and " + + "`tracking event details` (optional), which returns nothing.") + @Test + @SneakyThrows + void trackMethodFulfillsSpec() { + + ImmutableContext ctx = new ImmutableContext(); + MutableTrackingEventDetails details = new MutableTrackingEventDetails(0.0f); + assertThatCode(() -> client.track("event")).doesNotThrowAnyException(); + assertThatCode(() -> client.track("event", ctx)).doesNotThrowAnyException(); + assertThatCode(() -> client.track("event", details)).doesNotThrowAnyException(); + assertThatCode(() -> client.track("event", ctx, details)).doesNotThrowAnyException(); + + assertThrows(NullPointerException.class, () -> client.track(null, ctx, details)); + assertThrows(NullPointerException.class, () -> client.track("event", null, details)); + assertThrows(NullPointerException.class, () -> client.track("event", ctx, null)); + assertThrows(NullPointerException.class, () -> client.track(null, null, null)); + assertThrows(NullPointerException.class, () -> client.track(null, ctx)); + assertThrows(NullPointerException.class, () -> client.track(null, details)); + assertThrows(NullPointerException.class, () -> client.track("event", (EvaluationContext) null)); + assertThrows(NullPointerException.class, () -> client.track("event", (TrackingEventDetails) null)); + + assertThrows(IllegalArgumentException.class, () -> client.track("")); + assertThrows(IllegalArgumentException.class, () -> client.track("", ctx)); + assertThrows(IllegalArgumentException.class, () -> client.track("", ctx, details)); + + + Class clientClass = OpenFeatureClient.class; + assertEquals( + void.class, + clientClass.getMethod("track", String.class).getReturnType(), + "The method should return void."); + assertEquals( + void.class, + clientClass.getMethod("track", String.class, EvaluationContext.class).getReturnType(), + "The method should return void."); + + assertEquals( + void.class, + clientClass.getMethod("track", String.class, EvaluationContext.class, TrackingEventDetails.class).getReturnType(), + "The method should return void."); + + + } + + @Specification(number = "6.1.3", text = "The evaluation context passed to the provider's track function " + + "MUST be merged in the order: API (global; lowest precedence) -> transaction -> client -> " + + "invocation (highest precedence), with duplicate values being overwritten.") + @Test + void contextsGetMerged() { + + api.setTransactionContextPropagator(new ThreadLocalTransactionContextPropagator()); + + Map apiAttr = new HashMap<>(); + apiAttr.put("my-key", new Value("hey")); + apiAttr.put("my-api-key", new Value("333")); + EvaluationContext apiCtx = new ImmutableContext(apiAttr); + api.setEvaluationContext(apiCtx); + + Map txAttr = new HashMap<>(); + txAttr.put("my-key", new Value("overwritten")); + txAttr.put("my-tx-key", new Value("444")); + EvaluationContext txCtx = new ImmutableContext(txAttr); + api.setTransactionContext(txCtx); + + Map clAttr = new HashMap<>(); + clAttr.put("my-key", new Value("overwritten-again")); + clAttr.put("my-cl-key", new Value("555")); + EvaluationContext clCtx = new ImmutableContext(clAttr); + client.setEvaluationContext(clCtx); + + FeatureProvider provider = ProviderFixture.createMockedProvider(); + FeatureProviderTestUtils.setFeatureProvider(provider); + + client.track("event", new MutableContext().add("my-key", "final"), new MutableTrackingEventDetails(0.0f)); + + Map expectedMap = Maps.newHashMap(); + expectedMap.put("my-key", new Value("final")); + expectedMap.put("my-api-key", new Value("333")); + expectedMap.put("my-tx-key", new Value("444")); + expectedMap.put("my-cl-key", new Value("555")); + verify(provider).track(eq("event"), argThat(ctx -> ctx.asMap().equals(expectedMap)), notNull()); + } + + @Specification(number = "6.1.4", text = "If the client's `track` function is called and the associated provider " + + "does not implement tracking, the client's `track` function MUST no-op.") + @Test + void noopProvider() { + FeatureProvider provider = spy(FeatureProvider.class); + api.setProvider(provider); + client.track("event"); + verify(provider).track(any(), any(), any()); + } + + @Specification(number = "6.2.1", text = "The `tracking event details` structure MUST define an optional numeric " + + "`value`, associating a scalar quality with an `tracking event`.") + @Specification(number = "6.2.2", text = "The `tracking event details` MUST support the inclusion of custom " + + "fields, having keys of type `string`, and values of type `boolean | string | number | structure`.") + @Test + void eventDetails() { + assertFalse(new MutableTrackingEventDetails().getValue().isPresent()); + assertFalse(new ImmutableTrackingEventDetails().getValue().isPresent()); + assertThat(new ImmutableTrackingEventDetails(2).getValue()).hasValue(2); + assertThat(new MutableTrackingEventDetails(9.87f).getValue()).hasValue(9.87f); + + + // using mutable tracking event details + Map expectedMap = Maps.newHashMap(); + expectedMap.put("my-str", new Value("str")); + expectedMap.put("my-num", new Value(1)); + expectedMap.put("my-bool", new Value(true)); + expectedMap.put("my-struct", new Value(new MutableTrackingEventDetails())); + + MutableTrackingEventDetails details = new MutableTrackingEventDetails() + .add("my-str", new Value("str")) + .add("my-num", new Value(1)) + .add("my-bool", new Value(true)) + .add("my-struct", new Value(new MutableTrackingEventDetails())); + + assertEquals(expectedMap, details.asMap()); + assertThatCode(() -> OpenFeatureAPI.getInstance().getClient(). + track( + "tracking-event-name", + new ImmutableContext(), + new MutableTrackingEventDetails())) + .doesNotThrowAnyException(); + + + // using immutable tracking event details + ImmutableMap expectedImmutable = ImmutableMap.of("my-str", new Value("str"), + "my-num", new Value(1), + "my-bool", new Value(true), + "my-struct", new Value(new ImmutableStructure()) + ); + + ImmutableTrackingEventDetails immutableDetails = new ImmutableTrackingEventDetails(2, expectedMap); + assertEquals(expectedImmutable, immutableDetails.asMap()); + assertThatCode(() -> OpenFeatureAPI.getInstance().getClient(). + track( + "tracking-event-name", + new ImmutableContext(), + new ImmutableTrackingEventDetails())) + .doesNotThrowAnyException(); + + + } + +} diff --git a/src/test/java/dev/openfeature/sdk/e2e/RunCucumberTest.java b/src/test/java/dev/openfeature/sdk/e2e/EvaluationTest.java similarity index 62% rename from src/test/java/dev/openfeature/sdk/e2e/RunCucumberTest.java rename to src/test/java/dev/openfeature/sdk/e2e/EvaluationTest.java index 2c652338..3e0f2ee8 100644 --- a/src/test/java/dev/openfeature/sdk/e2e/RunCucumberTest.java +++ b/src/test/java/dev/openfeature/sdk/e2e/EvaluationTest.java @@ -5,12 +5,17 @@ import org.junit.platform.suite.api.SelectClasspathResource; import org.junit.platform.suite.api.Suite; +import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME; import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME; @Suite @IncludeEngines("cucumber") -@SelectClasspathResource("features") +@SelectClasspathResource("features/evaluation.feature") @ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty") -public class RunCucumberTest { - +@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "dev.openfeature.sdk.e2e.evaluation") +public class EvaluationTest { + } + + + diff --git a/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java b/src/test/java/dev/openfeature/sdk/e2e/evaluation/StepDefinitions.java similarity index 90% rename from src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java rename to src/test/java/dev/openfeature/sdk/e2e/evaluation/StepDefinitions.java index 459fcefe..cf190592 100644 --- a/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java +++ b/src/test/java/dev/openfeature/sdk/e2e/evaluation/StepDefinitions.java @@ -1,4 +1,4 @@ -package dev.openfeature.sdk.e2e; +package dev.openfeature.sdk.e2e.evaluation; import dev.openfeature.sdk.Value; import dev.openfeature.sdk.EvaluationContext; @@ -52,7 +52,7 @@ public class StepDefinitions { @SneakyThrows @BeforeAll() - @Given("an openfeature client is registered with cache disabled") + @Given("a provider is registered") public static void setup() { Map> flags = buildFlags(); InMemoryProvider provider = new InMemoryProvider(flags); @@ -67,7 +67,7 @@ public static void setup() { // boolean value @When("a boolean flag with key {string} is evaluated with default value {string}") public void a_boolean_flag_with_key_boolean_flag_is_evaluated_with_default_value_false(String flagKey, - String defaultValue) { + String defaultValue) { this.booleanFlagValue = client.getBooleanValue(flagKey, Boolean.valueOf(defaultValue)); } @@ -117,7 +117,7 @@ public void an_object_flag_with_key_is_evaluated_with_a_null_default_value(Strin @Then("the resolved object value should be contain fields {string}, {string}, and {string}, with values {string}, {string} and {int}, respectively") public void the_resolved_object_value_should_be_contain_fields_and_with_values_and_respectively(String boolField, - String stringField, String numberField, String boolValue, String stringValue, int numberValue) { + String stringField, String numberField, String boolValue, String stringValue, int numberValue) { Structure structure = this.objectFlagValue.asStructure(); assertEquals(Boolean.valueOf(boolValue), structure.asMap().get(boolField).asBoolean()); @@ -132,7 +132,7 @@ public void the_resolved_object_value_should_be_contain_fields_and_with_values_a // boolean details @When("a boolean flag with key {string} is evaluated with details and default value {string}") public void a_boolean_flag_with_key_is_evaluated_with_details_and_default_value(String flagKey, - String defaultValue) { + String defaultValue) { this.booleanFlagDetails = client.getBooleanDetails(flagKey, Boolean.valueOf(defaultValue)); } @@ -148,13 +148,13 @@ public void the_resolved_boolean_value_should_be_the_variant_should_be_and_the_r // string details @When("a string flag with key {string} is evaluated with details and default value {string}") public void a_string_flag_with_key_is_evaluated_with_details_and_default_value(String flagKey, - String defaultValue) { + String defaultValue) { this.stringFlagDetails = client.getStringDetails(flagKey, defaultValue); } @Then("the resolved string details value should be {string}, the variant should be {string}, and the reason should be {string}") public void the_resolved_string_value_should_be_the_variant_should_be_and_the_reason_should_be(String expectedValue, - String expectedVariant, String expectedReason) { + String expectedVariant, String expectedReason) { assertEquals(expectedValue, this.stringFlagDetails.getValue()); assertEquals(expectedVariant, this.stringFlagDetails.getVariant()); assertEquals(expectedReason, this.stringFlagDetails.getReason()); @@ -168,7 +168,7 @@ public void an_integer_flag_with_key_is_evaluated_with_details_and_default_value @Then("the resolved integer details value should be {int}, the variant should be {string}, and the reason should be {string}") public void the_resolved_integer_value_should_be_the_variant_should_be_and_the_reason_should_be(int expectedValue, - String expectedVariant, String expectedReason) { + String expectedVariant, String expectedReason) { assertEquals(expectedValue, this.intFlagDetails.getValue()); assertEquals(expectedVariant, this.intFlagDetails.getVariant()); assertEquals(expectedReason, this.intFlagDetails.getReason()); @@ -182,7 +182,7 @@ public void a_float_flag_with_key_is_evaluated_with_details_and_default_value(St @Then("the resolved float details value should be {double}, the variant should be {string}, and the reason should be {string}") public void the_resolved_float_value_should_be_the_variant_should_be_and_the_reason_should_be(double expectedValue, - String expectedVariant, String expectedReason) { + String expectedVariant, String expectedReason) { assertEquals(expectedValue, this.doubleFlagDetails.getValue()); assertEquals(expectedVariant, this.doubleFlagDetails.getVariant()); assertEquals(expectedReason, this.doubleFlagDetails.getReason()); @@ -217,7 +217,7 @@ public void the_variant_should_be_and_the_reason_should_be(String expectedVarian @When("context contains keys {string}, {string}, {string}, {string} with values {string}, {string}, {int}, {string}") public void context_contains_keys_with_values(String field1, String field2, String field3, String field4, - String value1, String value2, Integer value3, String value4) { + String value1, String value2, Integer value3, String value4) { Map attributes = new HashMap<>(); attributes.put(field1, new Value(value1)); attributes.put(field2, new Value(value2)); @@ -253,7 +253,7 @@ public void the_resolved_flag_value_is_when_the_context_is_empty(String expected // not found @When("a non-existent string flag with key {string} is evaluated with details and a default value {string}") public void a_non_existent_string_flag_with_key_is_evaluated_with_details_and_a_default_value(String flagKey, - String defaultValue) { + String defaultValue) { notFoundFlagKey = flagKey; notFoundDefaultValue = defaultValue; notFoundDetails = client.getStringDetails(notFoundFlagKey, notFoundDefaultValue); @@ -273,7 +273,7 @@ public void the_reason_should_indicate_an_error_and_the_error_code_should_be_fla // type mismatch @When("a string flag with key {string} is evaluated as an integer, with details and a default value {int}") public void a_string_flag_with_key_is_evaluated_as_an_integer_with_details_and_a_default_value(String flagKey, - int defaultValue) { + int defaultValue) { typeErrorFlagKey = flagKey; typeErrorDefaultValue = defaultValue; typeErrorDetails = client.getIntegerDetails(typeErrorFlagKey, typeErrorDefaultValue); diff --git a/test-harness b/test-harness deleted file mode 160000 index 2d4c63c8..00000000 --- a/test-harness +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2d4c63c800aa3af172cf09176325d93124153cde diff --git a/version.txt b/version.txt index 6b89d58f..feaae22b 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.12.2 +1.13.0