"
+labels: ["enhancement"]
+body:
+ - type: textarea
+ id: description
+ attributes:
+ label: "Description"
+ description: Briefly describe the enhancement in a few sentences.
+ placeholder: Short description...
+ validations:
+ required: true
+ - type: textarea
+ id: benefits
+ attributes:
+ label: "Benefits"
+ description: How would the enhancement benefit to your product or usage?
+ placeholder: Benefits...
+ validations:
+ required: true
+ - type: textarea
+ id: detail
+ attributes:
+ label: "Detail"
+ description: How would you like the enhancement to work? Please provide as much detail as possible
+ placeholder: Detailed description...
+ validations:
+ required: false
+ - type: textarea
+ id: examples
+ attributes:
+ label: "Examples"
+ description: Are there any examples of this enhancement in other products/services? If so, please provide links or references.
+ placeholder: Links/References...
+ validations:
+ required: false
+ - type: textarea
+ id: risks
+ attributes:
+ label: "Risks/Downsides"
+ description: Do you think this enhancement could have any potential downsides or risks?
+ placeholder: Risks/Downsides...
+ validations:
+ required: false
diff --git a/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md
new file mode 100644
index 000000000..5aa42ce83
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md
@@ -0,0 +1,4 @@
+
+## Feedback requesting a new feature can be shared [here.](https://feedback.optimizely.com/)
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 000000000..dc7735bc9
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+ - name: 💡Feature Requests
+ url: https://feedback.optimizely.com/
+ about: Feedback requesting a new feature can be shared here.
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 0cd965aad..1cb2193c8 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -6,7 +6,7 @@ on:
action:
required: true
type: string
- travis_tag:
+ github_tag:
required: true
type: string
secrets:
@@ -20,15 +20,14 @@ on:
required: true
jobs:
run_build:
- runs-on: ubuntu-18.04
+ runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- name: set up JDK 8
uses: actions/setup-java@v2
with:
java-version: '8'
distribution: 'temurin'
- cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: ${{ inputs.action }}
@@ -37,4 +36,4 @@ jobs:
MAVEN_SIGNING_PASSPHRASE: ${{ secrets.MAVEN_SIGNING_PASSPHRASE }}
MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
- run: TRAVIS_TAG=${{ inputs.travis_tag }} ./gradlew ${{ inputs.action }}
+ run: GITHUB_TAG=${{ inputs.github_tag }} ./gradlew ${{ inputs.action }}
diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml
index 9471458fc..76fef5ad3 100644
--- a/.github/workflows/integration_test.yml
+++ b/.github/workflows/integration_test.yml
@@ -9,29 +9,29 @@ on:
secrets:
CI_USER_TOKEN:
required: true
- TRAVIS_COM_TOKEN:
- required: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
with:
# You should create a personal access token and store it in your repository
token: ${{ secrets.CI_USER_TOKEN }}
- repository: 'optimizely/travisci-tools'
- path: 'home/runner/travisci-tools'
+ repository: 'optimizely/ci-helper-tools'
+ path: 'home/runner/ci-helper-tools'
ref: 'master'
- name: set SDK Branch if PR
+ env:
+ HEAD_REF: ${{ github.head_ref }}
if: ${{ github.event_name == 'pull_request' }}
run: |
- echo "SDK_BRANCH=${{ github.head_ref }}" >> $GITHUB_ENV
- echo "TRAVIS_BRANCH=${{ github.head_ref }}" >> $GITHUB_ENV
+ echo "SDK_BRANCH=$HEAD_REF" >> $GITHUB_ENV
- name: set SDK Branch if not pull request
+ env:
+ REF_NAME: ${{ github.ref_name }}
if: ${{ github.event_name != 'pull_request' }}
run: |
- echo "SDK_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV
- echo "TRAVIS_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV
+ echo "SDK_BRANCH=$REF_NAME" >> $GITHUB_ENV
- name: Trigger build
env:
SDK: java
@@ -41,16 +41,13 @@ jobs:
GITHUB_TOKEN: ${{ secrets.CI_USER_TOKEN }}
EVENT_TYPE: ${{ github.event_name }}
GITHUB_CONTEXT: ${{ toJson(github) }}
- #REPO_SLUG: ${{ github.repository }}
PULL_REQUEST_SLUG: ${{ github.repository }}
UPSTREAM_REPO: ${{ github.repository }}
PULL_REQUEST_SHA: ${{ github.event.pull_request.head.sha }}
PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
UPSTREAM_SHA: ${{ github.sha }}
- TOKEN: ${{ secrets.TRAVIS_COM_TOKEN }}
- TRAVIS_COM_TOKEN: ${{ secrets.TRAVIS_COM_TOKEN }}
EVENT_MESSAGE: ${{ github.event.message }}
HOME: 'home/runner'
run: |
echo "$GITHUB_CONTEXT"
- home/runner/travisci-tools/trigger-script-with-status-update.sh
+ home/runner/ci-helper-tools/trigger-script-with-status-update.sh
diff --git a/.github/workflows/java.yml b/.github/workflows/java.yml
index 1c6c57a02..95e8ccf8d 100644
--- a/.github/workflows/java.yml
+++ b/.github/workflows/java.yml
@@ -17,7 +17,7 @@ jobs:
lint_markdown_files:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
@@ -31,10 +31,9 @@ jobs:
integration_tests:
if: ${{ startsWith(github.ref, 'refs/tags/') != true && github.event.inputs.SNAPSHOT != 'true' }}
- uses: optimizely/java-sdk/.github/workflows/integration_test.yml@mnoman/fsc-gitaction-test
+ uses: optimizely/java-sdk/.github/workflows/integration_test.yml@master
secrets:
CI_USER_TOKEN: ${{ secrets.CI_USER_TOKEN }}
- TRAVIS_COM_TOKEN: ${{ secrets.TRAVIS_COM_TOKEN }}
fullstack_production_suite:
if: ${{ startsWith(github.ref, 'refs/tags/') != true && github.event.inputs.SNAPSHOT != 'true' }}
@@ -43,7 +42,6 @@ jobs:
FULLSTACK_TEST_REPO: ProdTesting
secrets:
CI_USER_TOKEN: ${{ secrets.CI_USER_TOKEN }}
- TRAVIS_COM_TOKEN: ${{ secrets.TRAVIS_COM_TOKEN }}
test:
if: ${{ startsWith(github.ref, 'refs/tags/') != true && github.event.inputs.SNAPSHOT != 'true' }}
@@ -55,7 +53,7 @@ jobs:
optimizely_default_parser: [GSON_CONFIG_PARSER, JACKSON_CONFIG_PARSER, JSON_CONFIG_PARSER, JSON_SIMPLE_CONFIG_PARSER]
steps:
- name: checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: set up JDK ${{ matrix.jdk }}
uses: AdoptOpenJDK/install-jdk@v1
@@ -67,7 +65,7 @@ jobs:
run: chmod +x gradlew
- name: Gradle cache
- uses: actions/cache@v2
+ uses: actions/cache@v4
with:
path: |
~/.gradle/caches
@@ -85,19 +83,19 @@ jobs:
- name: Check on failures
if: always() && steps.unit_tests.outcome != 'success'
run: |
- cat /home/runner/java-sdk/core-api/build/reports/findbugs/main.html
- cat /home/runner/java-sdk/core-api/build/reports/findbugs/test.html
+ cat /Users/runner/work/java-sdk/core-api/build/reports/spotbugs/main.html
+ cat /Users/runner/work/java-sdk/core-api/build/reports/spotbugs/test.html
- name: Check on success
if: always() && steps.unit_tests.outcome == 'success'
run: |
- ./gradlew coveralls uploadArchives --console plain
+ ./gradlew coveralls --console plain
publish:
if: startsWith(github.ref, 'refs/tags/')
uses: optimizely/java-sdk/.github/workflows/build.yml@master
with:
action: ship
- travis_tag: ${GITHUB_REF#refs/*/}
+ github_tag: ${GITHUB_REF#refs/*/}
secrets:
MAVEN_SIGNING_KEY_BASE64: ${{ secrets.MAVEN_SIGNING_KEY_BASE64 }}
MAVEN_SIGNING_PASSPHRASE: ${{ secrets.MAVEN_SIGNING_PASSPHRASE }}
@@ -109,7 +107,7 @@ jobs:
uses: optimizely/java-sdk/.github/workflows/build.yml@master
with:
action: ship
- travis_tag: BB-SNAPSHOT
+ github_tag: BB-SNAPSHOT
secrets:
MAVEN_SIGNING_KEY_BASE64: ${{ secrets.MAVEN_SIGNING_KEY_BASE64 }}
MAVEN_SIGNING_PASSPHRASE: ${{ secrets.MAVEN_SIGNING_PASSPHRASE }}
diff --git a/.gitignore b/.gitignore
index aefc53cb6..dcf3ee891 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,5 @@ classes
.vagrant
.DS_Store
.venv
+
+.vscode/mcp.json
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9512cb740..565bfcd5d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,114 @@
# Optimizely Java X SDK Changelog
+## [4.2.2]
+May 28th, 2025
+
+### Fixes
+- Added experimentId and variationId to decision notification ([#569](https://github.com/optimizely/java-sdk/pull/569)).
+
+## [4.2.1]
+Feb 19th, 2025
+
+### Fixes
+- Fix big integer conversion ([#556](https://github.com/optimizely/java-sdk/pull/556)).
+
+## [4.2.0]
+November 6th, 2024
+
+### New Features
+* Batch UPS lookup and save calls in decideAll and decideForKeys methods ([#549](https://github.com/optimizely/java-sdk/pull/549)).
+
+
+## [4.1.1]
+May 8th, 2024
+
+### Fixes
+- Fix logx events discarded for staled connections with httpclient connection pooling ([#545](https://github.com/optimizely/java-sdk/pull/545)).
+
+
+## [4.1.0]
+April 12th, 2024
+
+### New Features
+* OptimizelyFactory method for injecting customHttpClient is fixed to share the customHttpClient for all modules using httpClient (HttpProjectConfigManager, AsyncEventHander, ODPManager) ([#542](https://github.com/optimizely/java-sdk/pull/542)).
+* A custom ThreadFactory can be injected to support virtual threads (Loom) ([#540](https://github.com/optimizely/java-sdk/pull/540)).
+
+
+## [4.0.0]
+January 16th, 2024
+
+### New Features
+The 4.0.0 release introduces a new primary feature, [Advanced Audience Targeting]( https://docs.developers.optimizely.com/feature-experimentation/docs/optimizely-data-platform-advanced-audience-targeting)
+enabled through integration with [Optimizely Data Platform (ODP)](https://docs.developers.optimizely.com/optimizely-data-platform/docs) (
+[#474](https://github.com/optimizely/java-sdk/pull/474),
+[#481](https://github.com/optimizely/java-sdk/pull/481),
+[#482](https://github.com/optimizely/java-sdk/pull/482),
+[#483](https://github.com/optimizely/java-sdk/pull/483),
+[#484](https://github.com/optimizely/java-sdk/pull/484),
+[#485](https://github.com/optimizely/java-sdk/pull/485),
+[#487](https://github.com/optimizely/java-sdk/pull/487),
+[#489](https://github.com/optimizely/java-sdk/pull/489),
+[#490](https://github.com/optimizely/java-sdk/pull/490),
+[#494](https://github.com/optimizely/java-sdk/pull/494)
+).
+
+You can use ODP, a high-performance [Customer Data Platform (CDP)]( https://www.optimizely.com/optimization-glossary/customer-data-platform/), to easily create complex
+real-time segments (RTS) using first-party and 50+ third-party data sources out of the box. You can create custom schemas that support the user attributes important
+for your business, and stitch together user behavior done on different devices to better understand and target your customers for personalized user experiences. ODP can
+be used as a single source of truth for these segments in any Optimizely or 3rd party tool.
+
+With ODP accounts integrated into Optimizely projects, you can build audiences using segments pre-defined in ODP. The SDK will fetch the segments for given users and
+make decisions using the segments. For access to ODP audience targeting in your Feature Experimentation account, please contact your Optimizely Customer Success Manager.
+
+This version includes the following changes:
+- New API added to `OptimizelyUserContext`:
+ - `fetchQualifiedSegments()`: this API will retrieve user segments from the ODP server. The fetched segments will be used for audience evaluation. The fetched data will be stored in the local cache to avoid repeated network delays.
+ - When an `OptimizelyUserContext` is created, the SDK will automatically send an identify request to the ODP server to facilitate observing user activities.
+- New APIs added to `OptimizelyClient`:
+ - `sendOdpEvent()`: customers can build/send arbitrary ODP events that will bind user identifiers and data to user profiles in ODP.
+
+For details, refer to our documentation pages:
+- [Advanced Audience Targeting](https://docs.developers.optimizely.com/feature-experimentation/docs/optimizely-data-platform-advanced-audience-targeting)
+- [Server SDK Support](https://docs.developers.optimizely.com/feature-experimentation/v1.0/docs/advanced-audience-targeting-for-server-side-sdks)
+- [Initialize Java SDK](https://docs.developers.optimizely.com/feature-experimentation/docs/initialize-sdk-java)
+- [OptimizelyUserContext Java SDK](https://docs.developers.optimizely.com/feature-experimentation/docs/optimizelyusercontext-java)
+- [Advanced Audience Targeting segment qualification methods](https://docs.developers.optimizely.com/feature-experimentation/docs/advanced-audience-targeting-segment-qualification-methods-java)
+- [Send Optimizely Data Platform data using Advanced Audience Targeting](https://docs.developers.optimizely.com/feature-experimentation/docs/send-odp-data-using-advanced-audience-targeting-java)
+
+### Breaking Changes
+- `OdpManager` in the SDK is enabled by default, if initialized using OptimizelyFactory. Unless an ODP account is integrated into the Optimizely projects, most `OdpManager` functions will be ignored. If needed, ODP features can be disabled by initializing `OptimizelyClient` without passing `OdpManager`.
+- `ProjectConfigManager` interface has been changed to add 2 more methods `getCachedConfig()` and `getSDKKey()`. Custom ProjectConfigManager should implement these new methods. See `PollingProjectConfigManager` for reference. This change is required to support ODPManager updated on datafile download ([#501](https://github.com/optimizely/java-sdk/pull/501)).
+
+### Fixes
+- Fix thread leak from httpClient in HttpProjectConfigManager ([#530](https://github.com/optimizely/java-sdk/pull/530)).
+- Fix issue when vuid is passed as userid for `AsyncGetQualifiedSegments` ([#527](https://github.com/optimizely/java-sdk/pull/527)).
+- Fix to support arbitrary client names to be included in logx and odp events ([#524](https://github.com/optimizely/java-sdk/pull/524)).
+- Add evict timeout to logx connections ([#518](https://github.com/optimizely/java-sdk/pull/518)).
+
+### Functionality Enhancements
+- Update Github Issue Templates ([#531](https://github.com/optimizely/java-sdk/pull/531))
+
+
+
+## [4.0.0-beta2]
+August 28th, 2023
+
+### Fixes
+- Fix thread leak from httpClient in HttpProjectConfigManager ([#530](https://github.com/optimizely/java-sdk/pull/530)).
+- Fix issue when vuid is passed as userid for `AsyncGetQualifiedSegments` ([#527](https://github.com/optimizely/java-sdk/pull/527)).
+- Fix to support arbitrary client names to be included in logx and odp events ([#524](https://github.com/optimizely/java-sdk/pull/524)).
+
+### Functionality Enhancements
+- Update Github Issue Templates ([#531](https://github.com/optimizely/java-sdk/pull/531))
+
+
+## [3.10.4]
+June 8th, 2023
+
+### Fixes
+- Fix intermittent logx event dispatch failures possibly caused by reusing stale connections. Add `evictIdleConnections` (1min) to `OptimizelyHttpClient` in `AsyncEventHandler` to force close persistent connections after 1min idle time ([#518](https://github.com/optimizely/java-sdk/pull/518)).
+
+
## [4.0.0-beta]
May 5th, 2023
diff --git a/LICENSE b/LICENSE
index afc550977..c9f7279d1 100644
--- a/LICENSE
+++ b/LICENSE
@@ -187,7 +187,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright 2016, Optimizely
+ Copyright 2016-2024, Optimizely, Inc. and contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/README.md b/README.md
index 33e55928d..1a7370c43 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,5 @@
# Optimizely Java SDK
-[](https://travis-ci.org/optimizely/java-sdk)
[](http://www.apache.org/licenses/LICENSE-2.0)
This repository houses the Java SDK for use with Optimizely Feature Experimentation and Optimizely Full Stack (legacy).
@@ -71,7 +70,7 @@ You can run all unit tests with:
### Checking for bugs
-We utilize [FindBugs](http://findbugs.sourceforge.net/) to identify possible bugs in the SDK. To run the check:
+We utilize [SpotBugs](https://spotbugs.github.io/) to identify possible bugs in the SDK. To run the check:
```
@@ -176,3 +175,4 @@ License (Apache 2.0): [https://github.com/apache/httpcomponents-client/blob/mast
- Ruby - https://github.com/optimizely/ruby-sdk
- Swift - https://github.com/optimizely/swift-sdk
+
diff --git a/build.gradle b/build.gradle
index b8405e39b..5b449a47e 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,10 +1,13 @@
plugins {
- id 'com.github.kt3k.coveralls' version '2.8.2'
+ id 'com.github.kt3k.coveralls' version '2.12.2'
id 'jacoco'
- id 'me.champeau.gradle.jmh' version '0.4.5'
- id 'nebula.optional-base' version '3.2.0'
- id 'com.github.hierynomus.license' version '0.15.0'
- id 'com.github.spotbugs' version "4.5.0"
+ id 'me.champeau.gradle.jmh' version '0.5.3'
+ id 'nebula.optional-base' version '3.1.0'
+ id 'com.github.hierynomus.license' version '0.16.1'
+ id 'com.github.spotbugs' version "6.0.14"
+ id 'maven-publish'
+ id 'signing'
+ id 'io.github.gradle-nexus.publish-plugin' version '2.0.0'
}
allprojects {
@@ -12,7 +15,10 @@ allprojects {
apply plugin: 'jacoco'
repositories {
- jcenter()
+ mavenCentral()
+ maven {
+ url 'https://plugins.gradle.org/m2/'
+ }
}
jacoco {
@@ -23,9 +29,9 @@ allprojects {
allprojects {
group = 'com.optimizely.ab'
- def travis_defined_version = System.getenv('TRAVIS_TAG')
- if (travis_defined_version != null) {
- version = travis_defined_version
+ def github_tagged_version = System.getenv('GITHUB_TAG')
+ if (github_tagged_version != null) {
+ version = github_tagged_version
}
ext.isReleaseVersion = !version.endsWith("SNAPSHOT")
@@ -46,13 +52,6 @@ configure(publishedProjects) {
sourceCompatibility = 1.8
targetCompatibility = 1.8
- repositories {
- jcenter()
- maven {
- url 'https://plugins.gradle.org/m2/'
- }
- }
-
task sourcesJar(type: Jar, dependsOn: classes) {
archiveClassifier.set('sources')
from sourceSets.main.allSource
@@ -72,6 +71,7 @@ configure(publishedProjects) {
spotbugs {
spotbugsJmh.enabled = false
+ reportLevel = com.github.spotbugs.snom.Confidence.valueOf('HIGH')
}
test {
@@ -94,21 +94,28 @@ configure(publishedProjects) {
}
dependencies {
- compile group: 'commons-codec', name: 'commons-codec', version: commonCodecVersion
+ implementation group: 'commons-codec', name: 'commons-codec', version: commonCodecVersion
- testCompile group: 'junit', name: 'junit', version: junitVersion
- testCompile group: 'org.mockito', name: 'mockito-core', version: mockitoVersion
- testCompile group: 'org.hamcrest', name: 'hamcrest-all', version: hamcrestVersion
- testCompile group: 'com.google.guava', name: 'guava', version: guavaVersion
+ testImplementation group: 'junit', name: 'junit', version: junitVersion
+ testImplementation group: 'org.mockito', name: 'mockito-core', version: mockitoVersion
+ testImplementation group: 'org.hamcrest', name: 'hamcrest-all', version: hamcrestVersion
+ testImplementation group: 'com.google.guava', name: 'guava', version: guavaVersion
// logging dependencies (logback)
- testCompile group: 'ch.qos.logback', name: 'logback-classic', version: logbackVersion
- testCompile group: 'ch.qos.logback', name: 'logback-core', version: logbackVersion
+ testImplementation group: 'ch.qos.logback', name: 'logback-classic', version: logbackVersion
+ testImplementation group: 'ch.qos.logback', name: 'logback-core', version: logbackVersion
+
+ testImplementation group: 'com.google.code.gson', name: 'gson', version: gsonVersion
+ testImplementation group: 'org.json', name: 'json', version: jsonVersion
+ testImplementation group: 'com.googlecode.json-simple', name: 'json-simple', version: jsonSimpleVersion
+ testImplementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: jacksonVersion
+ }
- testCompile group: 'com.google.code.gson', name: 'gson', version: gsonVersion
- testCompile group: 'org.json', name: 'json', version: jsonVersion
- testCompile group: 'com.googlecode.json-simple', name: 'json-simple', version: jsonSimpleVersion
- testCompile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: jacksonVersion
+ configurations.all {
+ resolutionStrategy {
+ force "junit:junit:${junitVersion}"
+ force 'com.netflix.nebula:nebula-gradle-interop:2.2.2'
+ }
}
def docTitle = "Optimizely Java SDK"
@@ -127,17 +134,6 @@ configure(publishedProjects) {
artifact javadocJar
}
}
- repositories {
- maven {
- def releaseUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2"
- def snapshotUrl = "https://oss.sonatype.org/content/repositories/snapshots"
- url = isReleaseVersion ? releaseUrl : snapshotUrl
- credentials {
- username System.getenv('MAVEN_CENTRAL_USERNAME')
- password System.getenv('MAVEN_CENTRAL_PASSWORD')
- }
- }
- }
}
signing {
@@ -173,7 +169,18 @@ configure(publishedProjects) {
}
task ship() {
- dependsOn(':core-api:ship', ':core-httpclient-impl:ship')
+ dependsOn(':core-httpclient-impl:ship', ':core-api:ship', 'publishToSonatype', 'closeSonatypeStagingRepository')
+}
+
+nexusPublishing {
+ repositories {
+ sonatype {
+ nexusUrl.set(uri('https://ossrh-staging-api.central.sonatype.com/service/local/'))
+ snapshotRepositoryUrl.set(uri('https://central.sonatype.com/repository/maven-snapshots/'))
+ username = System.getenv('MAVEN_CENTRAL_USERNAME')
+ password = System.getenv('MAVEN_CENTRAL_PASSWORD')
+ }
+ }
}
task jacocoMerge(type: JacocoMerge) {
@@ -214,7 +221,6 @@ tasks.coveralls {
}
// standard POM format required by MavenCentral
-
def customizePom(pom, title) {
pom.withXml {
asNode().children().last() + {
diff --git a/core-api/build.gradle b/core-api/build.gradle
index d2609a97d..602131cd3 100644
--- a/core-api/build.gradle
+++ b/core-api/build.gradle
@@ -1,9 +1,10 @@
dependencies {
- compile group: 'org.slf4j', name: 'slf4j-api', version: slf4jVersion
- compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: jacksonVersion
-
- compile group: 'com.google.code.findbugs', name: 'annotations', version: findbugsAnnotationVersion
- compile group: 'com.google.code.findbugs', name: 'jsr305', version: findbugsJsrVersion
+ implementation group: 'org.slf4j', name: 'slf4j-api', version: slf4jVersion
+ implementation group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: jacksonVersion
+ implementation group: 'com.google.code.findbugs', name: 'annotations', version: findbugsAnnotationVersion
+ implementation group: 'com.google.code.findbugs', name: 'jsr305', version: findbugsJsrVersion
+ testImplementation group: 'junit', name: 'junit', version: junitVersion
+ testImplementation group: 'ch.qos.logback', name: 'logback-classic', version: logbackVersion
// an assortment of json parsers
compileOnly group: 'com.google.code.gson', name: 'gson', version: gsonVersion, optional
@@ -12,6 +13,11 @@ dependencies {
compileOnly group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: jacksonVersion, optional
}
+tasks.named('processJmhResources') {
+ duplicatesStrategy = DuplicatesStrategy.WARN
+}
+
+
test {
useJUnit {
excludeCategories 'com.optimizely.ab.categories.ExhaustiveTest'
@@ -24,6 +30,7 @@ task exhaustiveTest(type: Test) {
}
}
+
task generateVersionFile {
// add the build version information into a file that'll go into the distribution
ext.buildVersion = new File(projectDir, "src/main/resources/optimizely-build-version")
diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java
index acd9d05fd..d041bfad3 100644
--- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java
+++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java
@@ -1,5 +1,5 @@
/****************************************************************************
- * Copyright 2016-2023, Optimizely, Inc. and contributors *
+ * Copyright 2016-2024, Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
@@ -20,20 +20,54 @@
import com.optimizely.ab.bucketing.DecisionService;
import com.optimizely.ab.bucketing.FeatureDecision;
import com.optimizely.ab.bucketing.UserProfileService;
-import com.optimizely.ab.config.*;
+import com.optimizely.ab.config.AtomicProjectConfigManager;
+import com.optimizely.ab.config.DatafileProjectConfig;
+import com.optimizely.ab.config.EventType;
+import com.optimizely.ab.config.Experiment;
+import com.optimizely.ab.config.ExperimentCore;
+import com.optimizely.ab.config.FeatureFlag;
+import com.optimizely.ab.config.FeatureVariable;
+import com.optimizely.ab.config.FeatureVariableUsageInstance;
+import com.optimizely.ab.config.ProjectConfig;
+import com.optimizely.ab.config.ProjectConfigManager;
+import com.optimizely.ab.config.Variation;
import com.optimizely.ab.config.parser.ConfigParseException;
import com.optimizely.ab.error.ErrorHandler;
import com.optimizely.ab.error.NoOpErrorHandler;
-import com.optimizely.ab.event.*;
-import com.optimizely.ab.event.internal.*;
+import com.optimizely.ab.event.EventHandler;
+import com.optimizely.ab.event.EventProcessor;
+import com.optimizely.ab.event.ForwardingEventProcessor;
+import com.optimizely.ab.event.LogEvent;
+import com.optimizely.ab.event.NoopEventHandler;
+import com.optimizely.ab.event.internal.BuildVersionInfo;
+import com.optimizely.ab.event.internal.ClientEngineInfo;
+import com.optimizely.ab.event.internal.EventFactory;
+import com.optimizely.ab.event.internal.UserEvent;
+import com.optimizely.ab.event.internal.UserEventFactory;
import com.optimizely.ab.event.internal.payload.EventBatch;
import com.optimizely.ab.internal.NotificationRegistry;
-import com.optimizely.ab.notification.*;
-import com.optimizely.ab.odp.*;
+import com.optimizely.ab.notification.ActivateNotification;
+import com.optimizely.ab.notification.DecisionNotification;
+import com.optimizely.ab.notification.FeatureTestSourceInfo;
+import com.optimizely.ab.notification.NotificationCenter;
+import com.optimizely.ab.notification.NotificationHandler;
+import com.optimizely.ab.notification.RolloutSourceInfo;
+import com.optimizely.ab.notification.SourceInfo;
+import com.optimizely.ab.notification.TrackNotification;
+import com.optimizely.ab.notification.UpdateConfigNotification;
+import com.optimizely.ab.odp.ODPEvent;
+import com.optimizely.ab.odp.ODPManager;
+import com.optimizely.ab.odp.ODPSegmentManager;
+import com.optimizely.ab.odp.ODPSegmentOption;
import com.optimizely.ab.optimizelyconfig.OptimizelyConfig;
import com.optimizely.ab.optimizelyconfig.OptimizelyConfigManager;
import com.optimizely.ab.optimizelyconfig.OptimizelyConfigService;
-import com.optimizely.ab.optimizelydecision.*;
+import com.optimizely.ab.optimizelydecision.DecisionMessage;
+import com.optimizely.ab.optimizelydecision.DecisionReasons;
+import com.optimizely.ab.optimizelydecision.DecisionResponse;
+import com.optimizely.ab.optimizelydecision.DefaultDecisionReasons;
+import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption;
+import com.optimizely.ab.optimizelydecision.OptimizelyDecision;
import com.optimizely.ab.optimizelyjson.OptimizelyJSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -42,19 +76,25 @@
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import java.io.Closeable;
-import java.util.*;
+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.concurrent.locks.ReentrantLock;
import static com.optimizely.ab.internal.SafetyUtils.tryClose;
/**
* Top-level container class for Optimizely functionality.
* Thread-safe, so can be created as a singleton and safely passed around.
- *
+ *
* Example instantiation:
*
* Optimizely optimizely = Optimizely.builder(projectWatcher, eventHandler).build();
*
- *
+ *
* To activate an experiment and perform variation specific processing:
*
* Variation variation = optimizely.activate(experimentKey, userId, attributes);
@@ -77,7 +117,6 @@ public class Optimizely implements AutoCloseable {
private static final Logger logger = LoggerFactory.getLogger(Optimizely.class);
final DecisionService decisionService;
- @VisibleForTesting
@Deprecated
final EventHandler eventHandler;
@VisibleForTesting
@@ -87,7 +126,8 @@ public class Optimizely implements AutoCloseable {
public final List defaultDecideOptions;
- private final ProjectConfigManager projectConfigManager;
+ @VisibleForTesting
+ final ProjectConfigManager projectConfigManager;
@Nullable
private final OptimizelyConfigManager optimizelyConfigManager;
@@ -101,6 +141,8 @@ public class Optimizely implements AutoCloseable {
@Nullable
private final ODPManager odpManager;
+ private final ReentrantLock lock = new ReentrantLock();
+
private Optimizely(@Nonnull EventHandler eventHandler,
@Nonnull EventProcessor eventProcessor,
@Nonnull ErrorHandler errorHandler,
@@ -131,7 +173,9 @@ private Optimizely(@Nonnull EventHandler eventHandler,
if (projectConfigManager.getSDKKey() != null) {
NotificationRegistry.getInternalNotificationCenter(projectConfigManager.getSDKKey()).
addNotificationHandler(UpdateConfigNotification.class,
- configNotification -> { updateODPSettings(); });
+ configNotification -> {
+ updateODPSettings();
+ });
}
}
@@ -276,7 +320,7 @@ private void sendImpression(@Nonnull ProjectConfig projectConfig,
* @param ruleType It can either be experiment in case impression event is sent from activate or it's feature-test or rollout
*/
private boolean sendImpression(@Nonnull ProjectConfig projectConfig,
- @Nullable Experiment experiment,
+ @Nullable ExperimentCore experiment,
@Nonnull String userId,
@Nonnull Map filteredAttributes,
@Nullable Variation variation,
@@ -301,13 +345,17 @@ private boolean sendImpression(@Nonnull ProjectConfig projectConfig,
if (experiment != null) {
logger.info("Activating user \"{}\" in experiment \"{}\".", userId, experiment.getKey());
}
+
+ // Legacy API methods only apply to the Experiment type and not to Holdout.
+ boolean isExperimentType = experiment instanceof Experiment;
+
// Kept For backwards compatibility.
// This notification is deprecated and the new DecisionNotifications
// are sent via their respective method calls.
- if (notificationCenter.getNotificationManager(ActivateNotification.class).size() > 0) {
+ if (notificationCenter.getNotificationManager(ActivateNotification.class).size() > 0 && isExperimentType) {
LogEvent impressionEvent = EventFactory.createLogEvent(userEvent);
ActivateNotification activateNotification = new ActivateNotification(
- experiment, userId, filteredAttributes, variation, impressionEvent);
+ (Experiment)experiment, userId, filteredAttributes, variation, impressionEvent);
notificationCenter.send(activateNotification);
}
return true;
@@ -629,6 +677,53 @@ public Integer getFeatureVariableInteger(@Nonnull String featureKey,
return variableValue;
}
+ /**
+ * Get the Long value of the specified variable in the feature.
+ *
+ * @param featureKey The unique key of the feature.
+ * @param variableKey The unique key of the variable.
+ * @param userId The ID of the user.
+ * @return The Integer value of the integer single variable feature.
+ * Null if the feature or variable could not be found.
+ */
+ @Nullable
+ public Long getFeatureVariableLong(@Nonnull String featureKey,
+ @Nonnull String variableKey,
+ @Nonnull String userId) {
+ return getFeatureVariableLong(featureKey, variableKey, userId, Collections.emptyMap());
+ }
+
+ /**
+ * Get the Integer value of the specified variable in the feature.
+ *
+ * @param featureKey The unique key of the feature.
+ * @param variableKey The unique key of the variable.
+ * @param userId The ID of the user.
+ * @param attributes The user's attributes.
+ * @return The Integer value of the integer single variable feature.
+ * Null if the feature or variable could not be found.
+ */
+ @Nullable
+ public Long getFeatureVariableLong(@Nonnull String featureKey,
+ @Nonnull String variableKey,
+ @Nonnull String userId,
+ @Nonnull Map attributes) {
+ try {
+ return getFeatureVariableValueForType(
+ featureKey,
+ variableKey,
+ userId,
+ attributes,
+ FeatureVariable.INTEGER_TYPE
+ );
+
+ } catch (Exception exception) {
+ logger.error("NumberFormatException while trying to parse value as Long. {}", String.valueOf(exception));
+ }
+
+ return null;
+ }
+
/**
* Get the String value of the specified variable in the feature.
*
@@ -823,8 +918,13 @@ Object convertStringToType(String variableValue, String type) {
try {
return Integer.parseInt(variableValue);
} catch (NumberFormatException exception) {
- logger.error("NumberFormatException while trying to parse \"" + variableValue +
- "\" as Integer. " + exception.toString());
+ try {
+ return Long.parseLong(variableValue);
+ } catch (NumberFormatException longException) {
+ logger.error("NumberFormatException while trying to parse \"{}\" as Integer. {}",
+ variableValue,
+ exception.toString());
+ }
}
break;
case FeatureVariable.JSON_TYPE:
@@ -840,11 +940,10 @@ Object convertStringToType(String variableValue, String type) {
/**
* Get the values of all variables in the feature.
*
- * @param featureKey The unique key of the feature.
- * @param userId The ID of the user.
+ * @param featureKey The unique key of the feature.
+ * @param userId The ID of the user.
* @return An OptimizelyJSON instance for all variable values.
* Null if the feature could not be found.
- *
*/
@Nullable
public OptimizelyJSON getAllFeatureVariables(@Nonnull String featureKey,
@@ -855,12 +954,11 @@ public OptimizelyJSON getAllFeatureVariables(@Nonnull String featureKey,
/**
* Get the values of all variables in the feature.
*
- * @param featureKey The unique key of the feature.
- * @param userId The ID of the user.
- * @param attributes The user's attributes.
+ * @param featureKey The unique key of the feature.
+ * @param userId The ID of the user.
+ * @param attributes The user's attributes.
* @return An OptimizelyJSON instance for all variable values.
* Null if the feature could not be found.
- *
*/
@Nullable
public OptimizelyJSON getAllFeatureVariables(@Nonnull String featureKey,
@@ -944,7 +1042,6 @@ public OptimizelyJSON getAllFeatureVariables(@Nonnull String featureKey,
* @param attributes The user's attributes.
* @return List of the feature keys that are enabled for the user if the userId is empty it will
* return Empty List.
- *
*/
public List getEnabledFeatures(@Nonnull String userId, @Nonnull Map attributes) {
List enabledFeaturesList = new ArrayList();
@@ -1159,10 +1256,10 @@ public OptimizelyConfig getOptimizelyConfig() {
/**
* Create a context of the user for which decision APIs will be called.
- *
+ *
* A user context will be created successfully even when the SDK is not fully configured yet.
*
- * @param userId The user ID to be used for bucketing.
+ * @param userId The user ID to be used for bucketing.
* @param attributes: A map of attribute names to current user attribute values.
* @return An OptimizelyUserContext associated with this OptimizelyClient.
*/
@@ -1191,42 +1288,28 @@ private OptimizelyUserContext createUserContextCopy(@Nonnull String userId, @Non
OptimizelyDecision decide(@Nonnull OptimizelyUserContext user,
@Nonnull String key,
@Nonnull List options) {
-
ProjectConfig projectConfig = getProjectConfig();
if (projectConfig == null) {
return OptimizelyDecision.newErrorDecision(key, user, DecisionMessage.SDK_NOT_READY.reason());
}
- FeatureFlag flag = projectConfig.getFeatureKeyMapping().get(key);
- if (flag == null) {
- return OptimizelyDecision.newErrorDecision(key, user, DecisionMessage.FLAG_KEY_INVALID.reason(key));
- }
-
- String userId = user.getUserId();
- Map attributes = user.getAttributes();
- Boolean decisionEventDispatched = false;
List allOptions = getAllOptions(options);
- DecisionReasons decisionReasons = DefaultDecisionReasons.newInstance(allOptions);
+ allOptions.remove(OptimizelyDecideOption.ENABLED_FLAGS_ONLY);
- Map copiedAttributes = new HashMap<>(attributes);
- FeatureDecision flagDecision;
-
- // Check Forced Decision
- OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flag.getKey(), null);
- DecisionResponse forcedDecisionVariation = decisionService.validatedForcedDecision(optimizelyDecisionContext, projectConfig, user);
- decisionReasons.merge(forcedDecisionVariation.getReasons());
- if (forcedDecisionVariation.getResult() != null) {
- flagDecision = new FeatureDecision(null, forcedDecisionVariation.getResult(), FeatureDecision.DecisionSource.FEATURE_TEST);
- } else {
- // Regular decision
- DecisionResponse decisionVariation = decisionService.getVariationForFeature(
- flag,
- user,
- projectConfig,
- allOptions);
- flagDecision = decisionVariation.getResult();
- decisionReasons.merge(decisionVariation.getReasons());
- }
+ return decideForKeys(user, Arrays.asList(key), allOptions, true).get(key);
+ }
+
+ private OptimizelyDecision createOptimizelyDecision(
+ OptimizelyUserContext user,
+ String flagKey,
+ FeatureDecision flagDecision,
+ DecisionReasons decisionReasons,
+ List allOptions,
+ ProjectConfig projectConfig
+ ) {
+ String userId = user.getUserId();
+ String experimentId = null;
+ String variationId = null;
Boolean flagEnabled = false;
if (flagDecision.variation != null) {
@@ -1234,12 +1317,12 @@ OptimizelyDecision decide(@Nonnull OptimizelyUserContext user,
flagEnabled = true;
}
}
- logger.info("Feature \"{}\" is enabled for user \"{}\"? {}", key, userId, flagEnabled);
+ logger.info("Feature \"{}\" is enabled for user \"{}\"? {}", flagKey, userId, flagEnabled);
Map variableMap = new HashMap<>();
if (!allOptions.contains(OptimizelyDecideOption.EXCLUDE_VARIABLES)) {
DecisionResponse