diff --git a/android-example/app/build.gradle b/android-example/app/build.gradle index 0c63c96..bd117e3 100644 --- a/android-example/app/build.gradle +++ b/android-example/app/build.gradle @@ -38,7 +38,7 @@ dependencies { implementation 'androidx.navigation:navigation-fragment:2.5.0' implementation 'androidx.navigation:navigation-ui:2.5.0' - implementation 'com.github.growthbook:growthbook-sdk-java:0.7.1' + implementation 'com.github.growthbook:growthbook-sdk-java:0.9.0' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' diff --git a/jvm-kotlin-ktor-example/build.gradle.kts b/jvm-kotlin-ktor-example/build.gradle.kts index 6530246..5ff63e0 100644 --- a/jvm-kotlin-ktor-example/build.gradle.kts +++ b/jvm-kotlin-ktor-example/build.gradle.kts @@ -47,7 +47,7 @@ dependencies { // GrowthBook SDK // 2. Add the GrowthBook SDK - implementation("com.github.growthbook:growthbook-sdk-java:0.7.1") + implementation("com.github.growthbook:growthbook-sdk-java:0.9.0") // Test dependencies testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version") diff --git a/jvm-kotlin-ktor-example/src/main/kotlin/com/growthbook/example/plugins/di/ServiceModule.kt b/jvm-kotlin-ktor-example/src/main/kotlin/com/growthbook/example/plugins/di/ServiceModule.kt index 319d1d4..3a9c908 100644 --- a/jvm-kotlin-ktor-example/src/main/kotlin/com/growthbook/example/plugins/di/ServiceModule.kt +++ b/jvm-kotlin-ktor-example/src/main/kotlin/com/growthbook/example/plugins/di/ServiceModule.kt @@ -11,7 +11,8 @@ import org.koin.dsl.module val serviceModule = module { single { AcmeDonutFeaturesRepository( - endpoint = "https://cdn.growthbook.io/api/features/java_NsrWldWd5bxQJZftGsWKl7R2yD2LtAK8C8EUYh9L8", + apiHost = "https://cdn.growthbook.io", + clientKey = "sdk-pGmC6LrsiUoEUcpZ", ttlSeconds = 10, ).apply { onFeaturesRefresh { @@ -22,7 +23,8 @@ val serviceModule = module { single { BasicEncryptedFeaturesRepository( - endpoint = "https://cdn.growthbook.io/api/features/sdk-862b5mHcP9XPugqD", + apiHost = "https://cdn.growthbook.io", + clientKey = "sdk-862b5mHcP9XPugqD", encryptionKey = "BhB1wORFmZLTDjbvstvS8w==", ttlSeconds = 15 ).apply { diff --git a/jvm-kotlin-ktor-example/src/main/kotlin/com/growthbook/example/plugins/growthbook/AcmeDonutFeaturesRepository.kt b/jvm-kotlin-ktor-example/src/main/kotlin/com/growthbook/example/plugins/growthbook/AcmeDonutFeaturesRepository.kt index a37564f..66c70e7 100644 --- a/jvm-kotlin-ktor-example/src/main/kotlin/com/growthbook/example/plugins/growthbook/AcmeDonutFeaturesRepository.kt +++ b/jvm-kotlin-ktor-example/src/main/kotlin/com/growthbook/example/plugins/growthbook/AcmeDonutFeaturesRepository.kt @@ -1,9 +1,11 @@ package com.growthbook.example.plugins.growthbook +import growthbook.sdk.java.FeatureRefreshStrategy import growthbook.sdk.java.GBFeaturesRepository class AcmeDonutFeaturesRepository( - endpoint: String, + apiHost: String, + clientKey: String, encryptionKey: String? = null, ttlSeconds: Int -) : GBFeaturesRepository(endpoint, encryptionKey, ttlSeconds) +) : GBFeaturesRepository(apiHost, clientKey, encryptionKey, FeatureRefreshStrategy.STALE_WHILE_REVALIDATE, ttlSeconds) diff --git a/jvm-kotlin-ktor-example/src/main/kotlin/com/growthbook/example/plugins/growthbook/BasicEncryptedFeaturesRepository.kt b/jvm-kotlin-ktor-example/src/main/kotlin/com/growthbook/example/plugins/growthbook/BasicEncryptedFeaturesRepository.kt index 325e3b5..9d88d37 100644 --- a/jvm-kotlin-ktor-example/src/main/kotlin/com/growthbook/example/plugins/growthbook/BasicEncryptedFeaturesRepository.kt +++ b/jvm-kotlin-ktor-example/src/main/kotlin/com/growthbook/example/plugins/growthbook/BasicEncryptedFeaturesRepository.kt @@ -1,9 +1,11 @@ package com.growthbook.example.plugins.growthbook +import growthbook.sdk.java.FeatureRefreshStrategy import growthbook.sdk.java.GBFeaturesRepository class BasicEncryptedFeaturesRepository( - endpoint: String, - encryptionKey: String, + apiHost: String, + clientKey: String, + encryptionKey: String? = null, ttlSeconds: Int -) : GBFeaturesRepository(endpoint, encryptionKey, ttlSeconds) +) : GBFeaturesRepository(apiHost, clientKey, encryptionKey, FeatureRefreshStrategy.STALE_WHILE_REVALIDATE, ttlSeconds) diff --git a/jvm-kotlin-ktor-example/src/main/kotlin/com/growthbook/example/plugins/routing/AcmeDonutRoutes.kt b/jvm-kotlin-ktor-example/src/main/kotlin/com/growthbook/example/plugins/routing/AcmeDonutRoutes.kt index 0bfd66d..350329e 100644 --- a/jvm-kotlin-ktor-example/src/main/kotlin/com/growthbook/example/plugins/routing/AcmeDonutRoutes.kt +++ b/jvm-kotlin-ktor-example/src/main/kotlin/com/growthbook/example/plugins/routing/AcmeDonutRoutes.kt @@ -44,6 +44,11 @@ fun Routing.acmeRoutes() { .builder() .featuresJson(acmeDonutFeaturesRepository.featuresJson) .attributesJson(user.toJson()) + .featureUsageCallback(object : FeatureUsageCallback { + override fun onFeatureUsage(featureKey: String?, result: FeatureResult?) { + application.log.info("🔵feature usage callback called. key = $featureKey , result = $result") + } + }) .trackingCallback(object : TrackingCallback { override fun onTrack( experiment: Experiment?, diff --git a/jvm-spring-web/build.gradle b/jvm-spring-web/build.gradle index cc22a08..ff8f487 100644 --- a/jvm-spring-web/build.gradle +++ b/jvm-spring-web/build.gradle @@ -23,7 +23,7 @@ dependencies { // implementation 'com.github.growthbook:growthbook-sdk-java:main-SNAPSHOT' // Option: specific release version - implementation 'com.github.growthbook:growthbook-sdk-java:0.7.1' + implementation 'com.github.growthbook:growthbook-sdk-java:0.9.0' // This examples project includes examples for Gson and JSON deserialization implementation 'com.google.code.gson:gson:2.10' diff --git a/jvm-spring-web/src/main/java/com/example/demo/DemoApplication.java b/jvm-spring-web/src/main/java/com/example/demo/DemoApplication.java index 6c5c754..277c851 100644 --- a/jvm-spring-web/src/main/java/com/example/demo/DemoApplication.java +++ b/jvm-spring-web/src/main/java/com/example/demo/DemoApplication.java @@ -26,6 +26,7 @@ public CommandLineRunner commandLineRunner(ApplicationContext ctx) { System.out.println("Features forced by URL: http://localhost:8080/url-feature-force"); System.out.println("Features forced by URL (with URL overrides): http://localhost:8080/url-feature-force?gb~meal_overrides_gluten_free=%7B%22meal_type%22%3A%20%22gf%22%2C%20%22dessert%22%3A%20%22French%20Vanilla%20Ice%20Cream%22%7D&gb~dark_mode=true&gb~donut_price=3.33&gb~banner_text=Hello%2C%20everyone!%20I%20hope%20you%20are%20all%20doing%20well!"); System.out.println("Using an interceptor: http://localhost:8080/interceptors"); + System.out.println("Using SSE: http://localhost:8080/sse"); }; } } diff --git a/jvm-spring-web/src/main/java/com/example/demo/MainController.java b/jvm-spring-web/src/main/java/com/example/demo/MainController.java index 2b5d4b1..5d0c3ba 100644 --- a/jvm-spring-web/src/main/java/com/example/demo/MainController.java +++ b/jvm-spring-web/src/main/java/com/example/demo/MainController.java @@ -3,6 +3,7 @@ import com.example.demo.models.*; import com.example.demo.services.AcmeDonutsFeatureService; import com.example.demo.services.BasicEncryptedFeaturesService; +import com.example.demo.services.RealTimeSSEFeaturesService; import growthbook.sdk.java.FeatureResult; import growthbook.sdk.java.GBContext; import growthbook.sdk.java.GrowthBook; @@ -39,6 +40,9 @@ public class MainController { @Autowired AcmeDonutsFeatureService acmeDonutsFeatureService; + @Autowired + RealTimeSSEFeaturesService realTimeSSEFeaturesService; + // If you are managing the fetching of your own features, this class is doing that. FeaturesRepository featuresRepository = new FeaturesRepository(); @@ -388,4 +392,27 @@ public ResponseEntity> usingInterceptors(HttpServletRequ return new ResponseEntity<>(res, HttpStatus.OK); } + + @GetMapping("/sse") + public ResponseEntity> evalFeaturesFromSSE() { + // prepare response object + HashMap res = new HashMap<>(); + + // create GrowthBook instance with features from real-time SSE features service + GBContext context = GBContext.builder() + .featuresJson(realTimeSSEFeaturesService.getFeaturesJson()) + .attributesJson("{\"version\": \"2.1.0\"}") + .build(); + GrowthBook growthBook = new GrowthBook(context); + + // get values + String appVersion = growthBook.getFeatureValue("app_name", "unknown app version"); + String greeting = growthBook.getFeatureValue("greeting", "???"); + + // add values to response + res.put("app_version", appVersion); + res.put("greeting", greeting); + + return new ResponseEntity<>(res, HttpStatus.OK); + } } diff --git a/jvm-spring-web/src/main/java/com/example/demo/services/AcmeDonutsFeatureService.java b/jvm-spring-web/src/main/java/com/example/demo/services/AcmeDonutsFeatureService.java index 41e35b2..e13b0e7 100644 --- a/jvm-spring-web/src/main/java/com/example/demo/services/AcmeDonutsFeatureService.java +++ b/jvm-spring-web/src/main/java/com/example/demo/services/AcmeDonutsFeatureService.java @@ -2,6 +2,7 @@ import growthbook.sdk.java.FeatureFetchException; import growthbook.sdk.java.FeatureRefreshCallback; +import growthbook.sdk.java.FeatureRefreshStrategy; import growthbook.sdk.java.GBFeaturesRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -11,8 +12,10 @@ public class AcmeDonutsFeatureService extends GBFeaturesRepository { @Autowired public AcmeDonutsFeatureService() { super( - "https://cdn.growthbook.io/api/features/java_NsrWldWd5bxQJZftGsWKl7R2yD2LtAK8C8EUYh9L8", + "https://cdn.growthbook.io", + "java_NsrWldWd5bxQJZftGsWKl7R2yD2LtAK8C8EUYh9L8", null, + FeatureRefreshStrategy.STALE_WHILE_REVALIDATE, 10 ); @@ -39,13 +42,13 @@ void handleError(FeatureFetchException e) { // Handle NO_RESPONSE_ERROR } + case SSE_CONNECTION_ERROR -> { + // SSE is not applicable for this service but this was added here for completion. + } + case CONFIGURATION_ERROR, UNKNOWN -> { throw new RuntimeException(e); } } } - - private AcmeDonutsFeatureService(String endpoint, String encryptionKey, Integer ttlSeconds) { - super(endpoint, encryptionKey, ttlSeconds); - } } diff --git a/jvm-spring-web/src/main/java/com/example/demo/services/BasicEncryptedFeaturesService.java b/jvm-spring-web/src/main/java/com/example/demo/services/BasicEncryptedFeaturesService.java index f6029bd..8caff27 100644 --- a/jvm-spring-web/src/main/java/com/example/demo/services/BasicEncryptedFeaturesService.java +++ b/jvm-spring-web/src/main/java/com/example/demo/services/BasicEncryptedFeaturesService.java @@ -2,6 +2,7 @@ import growthbook.sdk.java.FeatureFetchException; import growthbook.sdk.java.FeatureRefreshCallback; +import growthbook.sdk.java.FeatureRefreshStrategy; import growthbook.sdk.java.GBFeaturesRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -10,11 +11,7 @@ public class BasicEncryptedFeaturesService extends GBFeaturesRepository { @Autowired public BasicEncryptedFeaturesService() { - super( - "https://cdn.growthbook.io/api/features/sdk-862b5mHcP9XPugqD", - "BhB1wORFmZLTDjbvstvS8w==", - 15 - ); + super("https://cdn.growthbook.io", "sdk-862b5mHcP9XPugqD", "BhB1wORFmZLTDjbvstvS8w==", FeatureRefreshStrategy.STALE_WHILE_REVALIDATE, 15); this.onFeaturesRefresh(new FeatureRefreshCallback() { @Override @@ -39,13 +36,13 @@ void handleError(FeatureFetchException e) { // Handle NO_RESPONSE_ERROR } + case SSE_CONNECTION_ERROR -> { + // SSE is not applicable for this service but this was added here for completion. + } + case CONFIGURATION_ERROR, UNKNOWN -> { throw new RuntimeException(e); } } } - - private BasicEncryptedFeaturesService(String endpoint, String encryptionKey, Integer ttlSeconds) { - super(endpoint, encryptionKey, ttlSeconds); - } } diff --git a/jvm-spring-web/src/main/java/com/example/demo/services/RealTimeSSEFeaturesService.java b/jvm-spring-web/src/main/java/com/example/demo/services/RealTimeSSEFeaturesService.java new file mode 100644 index 0000000..3d96c69 --- /dev/null +++ b/jvm-spring-web/src/main/java/com/example/demo/services/RealTimeSSEFeaturesService.java @@ -0,0 +1,54 @@ +package com.example.demo.services; + +import growthbook.sdk.java.FeatureFetchException; +import growthbook.sdk.java.FeatureRefreshCallback; +import growthbook.sdk.java.FeatureRefreshStrategy; +import growthbook.sdk.java.GBFeaturesRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class RealTimeSSEFeaturesService extends GBFeaturesRepository { + @Autowired + public RealTimeSSEFeaturesService() { + super( + "https://cdn.growthbook.io", + "sdk-pGmC6LrsiUoEUcpZ", + null, + FeatureRefreshStrategy.SERVER_SENT_EVENTS, + null + ); + + this.onFeaturesRefresh(new FeatureRefreshCallback() { + @Override + public void onRefresh(String featuresJson) { + System.out.println("🔵 RealTimeSSEFeaturesService -> Features have been refreshed"); + System.out.println(featuresJson); + } + }); + + try { + this.initialize(); + } catch (FeatureFetchException e) { + this.handleError(e); + } + } + + void handleError(FeatureFetchException e) { + e.printStackTrace(); + + switch (e.getErrorCode()) { + case NO_RESPONSE_ERROR -> { + // Handle NO_RESPONSE_ERROR + } + + case SSE_CONNECTION_ERROR -> { + // Handle SSE_CONNECTION_ERROR + } + + case CONFIGURATION_ERROR, UNKNOWN -> { + throw new RuntimeException(e); + } + } + } +}