Skip to content

Java SDK: All tests passing (294/294 unit, 138/138 cross-SDK)#11

Open
joalves wants to merge 10 commits intomainfrom
fix/all-tests-passing
Open

Java SDK: All tests passing (294/294 unit, 138/138 cross-SDK)#11
joalves wants to merge 10 commits intomainfrom
fix/all-tests-passing

Conversation

@joalves
Copy link

@joalves joalves commented Feb 21, 2026

Summary

  • 294/294 unit tests passing (exit code: 0)
  • 138/138 cross-SDK tests passing (exit code: 0)
  • Fixed 5 critical bugs that were causing test failures and deadlocks

Critical Bug Fixes

1. Deadlock in Context.flush()

Context.flush() held a read lock (contextLock_.readLock()) and then called getUnitHash() which tried to acquire a write lock via computeIfAbsentRW(). Since ReentrantReadWriteLock does not allow upgrading from read to write, this caused a permanent deadlock. Fixed by using write lock directly in flush().

2. AudienceMatcher crash on empty/null audience

evaluate("") tried to deserialize empty bytes, throwing MismatchedInputException. Added null/empty guard that returns null.

3. Deserializer error handling

DefaultAudienceDeserializer, DefaultContextDataDeserializer, DefaultContextEventSerializer, and DefaultVariableParser all threw IllegalStateException on bad input. Changed to return null, matching the SDK specification.

4. Double error logging in closeAsync()

logError() was called in both flush() error handler and closeAsync() error handler. Removed the redundant call.

5. Additional fixes

  • Scheduler shutdown with proper shutdownNow() fallback
  • Client HTTPS endpoint handling for Docker environments
  • MatchOperator pattern improvements
  • JsonMapperUtils for consistent ObjectMapper configuration
  • Cross-SDK test compatibility fixes

Test Results

Test Suite Result
Unit Tests 294/294 passed
Cross-SDK Tests 138/138 passed
Exit Code 0

Test plan

  • All 294 unit tests pass with exit code 0
  • All 138 cross-SDK test scenarios pass with exit code 0
  • Docker container builds and starts successfully
  • Health endpoint responds correctly
  • Async context tests pass (translateEndpoint fixed)

Summary by CodeRabbit

  • New Features

    • Builder-pattern SDK initialization with fluent API and expanded platform examples.
    • Enhanced event-logging options and a customizable event logger example.
  • Bug Fixes

    • Stronger endpoint validation, null-safety and timeout protections.
    • Regex matching guarded against ReDoS with timeouts and pattern-length checks.
    • Improved error logging for serialization/deserialization and runtime edge cases.
  • Documentation

    • Major README restructure with Quickstart, Advanced Configuration and SDK options table.
  • Chores

    • Branding standardised to "ABsmartly" and deprecated wrappers for compatibility.
    • Build tooling and dependency modernisation.
  • Tests

    • Extensive new and updated unit tests to improve coverage.

- Add getCustomFieldValue method for accessing custom field values
- Fix type coercion for custom field values (json, number, boolean types)
The attrsSeq assignment was happening during a read operation but mutating
shared state. This could cause race conditions with concurrent access.
The attrsSeq is already properly set in the write path when assignments
are created.
- Add HTTP client error scenario tests (timeout, rate limiting, SSL, retries)
- Add concurrency and thread safety tests
- Add configuration validation tests
- Add edge case type conversion tests
- Add data provider integration tests

Total: ~29 new tests added, all 265 tests pass
Add 29 context tests covering treatment exposure queuing,
variable values, peek, refresh, publish, close, and track.
Upgrade build infrastructure: Gradle 5.6→8.5, Mockito 3→5,
JaCoCo 0.8.6→0.8.12 for Java 21 compatibility. Fix
ABSmartly.java case-sensitivity issue and JaCoCo exclusions.
- Fix deadlock in Context.flush() (read lock upgrade to write lock)
- Fix AudienceMatcher.evaluate() crash on empty/null audience strings
- Fix deserializers to return null instead of throwing IllegalStateException
- Fix double error logging in closeAsync()
- Add JsonMapperUtils for consistent ObjectMapper configuration
- Fix scheduler shutdown, Client HTTPS handling, MatchOperator patterns
@coderabbitai
Copy link

coderabbitai bot commented Feb 21, 2026

Walkthrough

The PR renames public API classes from ABSmartly/ABSmartlyConfig to ABsmartly/ABsmartlyConfig and adds deprecated wrapper classes for backwards compatibility. It introduces a Builder for ABsmartly, refactors Context for safer concurrency and readiness handling, centralises Jackson mapper creation via JsonMapperUtils, adds regex matching timeouts/limits, tightens Client endpoint and response handling, updates Gradle and tooling versions, and expands README and many unit tests and platform examples.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I hopped through classes, names anew,
Built a fluent path and tightened the queue,
Regex with timers, futures made sound,
Docs and tests blooming all around,
A little thump — change safe and true.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title references test results (294/294 unit, 138/138 cross-SDK) but the changeset encompasses significant architectural refactoring including class renames (ABSmartly → ABsmartly), a builder pattern API, multiple bug fixes, and comprehensive documentation updates beyond just test passing. Consider a title reflecting the primary changes such as 'Refactor Java SDK with builder pattern API, fix critical bugs, and update branding' or similar that better captures the scope of architectural changes.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/all-tests-passing

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

- Fix InOperator: args are [needle, haystack] not [haystack, needle]
- Fix EqualsOperator: handle null==null comparison (returns true)
- Update unit tests to match corrected argument ordering
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
README.md (1)

7-7: ⚠️ Potential issue | 🟡 Minor

Java 1.6 compatibility claim conflicts with code examples using Java 9+ APIs.

Line 7 states the SDK is "compatible with Java versions 1.6 and later", yet multiple documentation examples use Map.of() (lines 235, 284, 299, 309, 540), which is a Java 9+ API. Either update the compatibility statement to reflect the actual minimum version, or replace Map.of() calls in examples with HashMap-based alternatives that work on Java 1.6+.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 7, The README claims "compatible with Java versions 1.6
and later" but several examples use the Java 9+ API Map.of(); either update the
README's compatibility statement to the correct minimum Java version or change
the examples that call Map.of(...) to Java 1.6-compatible constructions (e.g.,
create a new HashMap, put the key/value pairs, and optionally wrap with
Collections.unmodifiableMap where immutability is needed); update every Map.of
occurrence referenced in the doc to use the HashMap-based alternative if you
choose to keep 1.6 compatibility.
core-api/src/main/java/com/absmartly/sdk/ABsmartly.java (1)

73-78: ⚠️ Potential issue | 🔴 Critical

The README documentation contains inaccurate examples that will fail to compile.

The README (lines 60–69) documents ABsmartly.builder() as the recommended initialisation approach with method chaining (.endpoint(), .apiKey(), etc.). However, the ABsmartly class only exposes a static create(ABsmartlyConfig config) method; no builder() method exists. The only correct initialisation approach is shown in the "Advanced Configuration" section using ABsmartlyConfig.create() and Client.create().

Update the README to remove the builder pattern examples or implement the missing builder() method if intended.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/main/java/com/absmartly/sdk/ABsmartly.java` around lines 73 -
78, The README shows a fluent ABsmartly.builder() API but the ABsmartly class
only exposes create(ABsmartlyConfig), so implement a static builder() on
ABsmartly that returns a nested Builder (e.g., ABsmartly.Builder) which exposes
fluent setters like endpoint(String), apiKey(String), and any other documented
chainable methods, accumulates values into an ABsmartlyConfig, and has build()
(or create()) that constructs the config and calls ABsmartly.create(config);
ensure the Builder maps each fluent setter to the corresponding ABsmartlyConfig
field and that ABsmartly.create(ABsmartlyConfig) remains the single creation
path.
🧹 Nitpick comments (25)
core-api/src/main/java/com/absmartly/sdk/java/time/Clock.java (1)

17-17: Consider private visibility and conventional static final naming.

utc_ is package-private, so any class in the same package can access the raw SystemClockUTC instance directly, bypassing systemUTC(). Making it private preserves encapsulation. Additionally, Java conventions name static final constants in UPPER_SNAKE_CASE.

♻️ Proposed refactor
-	static final SystemClockUTC utc_ = new SystemClockUTC();
+	private static final SystemClockUTC UTC = new SystemClockUTC();

And update the reference in systemUTC():

 	static public Clock systemUTC() {
-		return utc_;
+		return UTC;
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/main/java/com/absmartly/sdk/java/time/Clock.java` at line 17,
The field utc_ is package-private and not following constant naming conventions;
change the declaration of utc_ (SystemClockUTC) to be private and rename it to
UCT_SYSTEM_CLOCK or SYSTEM_CLOCK_UTC (consistent with project convention) and
update any references, especially the systemUTC() method, to use the new private
constant name so callers must go through systemUTC() rather than accessing the
raw instance directly.
core-api/src/main/java/com/absmartly/sdk/internal/Algorithm.java (1)

7-7: @SuppressWarnings("unchecked") scope could be narrowed (optional)

The annotation is applied at method level, but the only unchecked operation is the (R[]) cast on line 11. Java allows @SuppressWarnings on local variable declarations, so extracting the cast into a helper variable would restrict the suppression to exactly that one expression:

♻️ Proposed narrowing refactor
 public static <T, R> R[] mapSetToArray(Set<T> set, R[] array, Function<T, R> mapper) {
     final int size = set.size();
     if (array.length < size) {
-        array = (R[]) java.lang.reflect.Array.newInstance(array.getClass().getComponentType(), size);
+        `@SuppressWarnings`("unchecked")
+        final R[] resized = (R[]) java.lang.reflect.Array.newInstance(array.getClass().getComponentType(), size);
+        array = resized;
     }

The cast itself is safe by construction — array.getClass().getComponentType() captures the exact runtime component type — so suppressing the warning is correct either way. This is purely a hygiene suggestion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/main/java/com/absmartly/sdk/internal/Algorithm.java` at line 7,
The class-level/method-level `@SuppressWarnings`("unchecked") should be narrowed:
locate the Algorithm class method that currently has
`@SuppressWarnings`("unchecked") and extract the (R[]) cast into a local helper
variable (e.g., R[] result = (R[])
Array.newInstance(array.getClass().getComponentType(), array.length);) and place
the `@SuppressWarnings`("unchecked") directly on that local variable declaration
(or immediately around that single cast), then remove the method-level
suppression so only the exact cast expression is suppressed.
gradle/jacoco.gradle (1)

5-5: Consider pinning to the latest JaCoCo release (0.8.14).

Version 0.8.12 is a valid published release (April 2024), but a newer version — 0.8.14 — is already available. Staying on 0.8.12 is not a blocker, but upgrading would bring in the latest bug fixes and Java-version support improvements.

♻️ Suggested version bump
 jacoco {
-	toolVersion = "0.8.12"
+	toolVersion = "0.8.14"
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gradle/jacoco.gradle` at line 5, Update the JaCoCo toolVersion value from
"0.8.12" to the newer release "0.8.14" by changing the toolVersion assignment
(symbol: toolVersion) in the jacoco Gradle configuration, then run a
build/coverage task to verify compatibility and that no Gradle warnings or
failures occur after the bump.
core-api/src/test/java/com/absmartly/sdk/internal/ConcurrencyTest.java (2)

183-188: ExecutorService not fully shut down — consider awaitTermination and try-finally.

Each test calls executor.shutdown() but never awaitTermination(). While the doneLatch.await() ensures tasks have logically completed, the executor's internal threads may still be running briefly. Using try-finally with awaitTermination is a cleaner pattern and avoids potential thread leaks across tests.

Example cleanup pattern
 		startLatch.countDown();
 		doneLatch.await();
 		executor.shutdown();
+		assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS), "Executor did not terminate in time");

Also applies to: 219-224, 257-263, 302-308, 338-343

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/test/java/com/absmartly/sdk/internal/ConcurrencyTest.java`
around lines 183 - 188, The tests currently call executor.shutdown() (e.g., in
ConcurrencyTest after startLatch.countDown() / doneLatch.await()) but do not
await termination or guarantee shutdown on failure; wrap executor lifecycle in a
try-finally, call executor.shutdown() in finally, then call
executor.awaitTermination with a reasonable timeout and fall back to
executor.shutdownNow() if it times out to avoid thread leaks; apply this pattern
to the executor usage around the blocks referencing executor, doneLatch,
startLatch and the assertions that use threadCount, operationsPerThread, and
successCount (also update the other similar test blocks at the locations
mentioned).

149-188: Using ConcurrentHashMap (and synchronizedList) undermines what these concurrency tests validate.

All the new concurrent tests use ConcurrentHashMap or Collections.synchronizedList as the underlying collection. These are inherently thread-safe, so the tests pass even if the Concurrency.getRW/putRW/addRW locking is completely removed. This means the tests don't actually verify that the ReentrantReadWriteLock in Concurrency provides the necessary synchronisation.

Switching to a plain HashMap (or ArrayList) would let the test genuinely prove that Concurrency.*RW methods are required for correctness under contention. This applies to all five new test methods (testConcurrentGetOperations, testConcurrentPutOperations, testConcurrentComputeIfAbsent, testConcurrentReadWriteMixed, testThreadSafeListOperations).

Example for this test
-		final Map<String, Integer> map = new ConcurrentHashMap<>();
+		final Map<String, Integer> map = new HashMap<>();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/test/java/com/absmartly/sdk/internal/ConcurrencyTest.java`
around lines 149 - 188, The tests currently use thread-safe collections
(ConcurrentHashMap / Collections.synchronizedList) so they succeed even if
Concurrency's locking is removed; replace those with non-thread-safe collections
so the Concurrency.*RW helpers are actually required: in
testConcurrentGetOperations (and the other four tests
testConcurrentPutOperations, testConcurrentComputeIfAbsent,
testConcurrentReadWriteMixed, testThreadSafeListOperations) change the backing
map/list instantiations to plain HashMap / ArrayList (e.g., Map<String,Integer>
map = new HashMap<>(); List<T> list = new ArrayList<>()) so that calls to
Concurrency.getRW, putRW, computeIfAbsent, addRW actually provide the
synchronization under contention and the assertions validate the locking
behavior.
core-api/src/main/java/com/absmartly/sdk/jsonexpr/ExprEvaluator.java (1)

101-105: Appropriate exception refinement in extractVar.

Catching NumberFormatException (from Integer.parseInt) and IndexOutOfBoundsException (from List.get) is precise and correct. However, note that the two separate catch clauses on a single line reduce readability.

🔧 Consider multi-catch syntax for clarity (Java 7+)
-			} catch (NumberFormatException ignored) {} catch (IndexOutOfBoundsException ignored) {}
+			} catch (NumberFormatException | IndexOutOfBoundsException ignored) {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/main/java/com/absmartly/sdk/jsonexpr/ExprEvaluator.java` around
lines 101 - 105, In extractVar (class ExprEvaluator) the two exception catches
for Integer.parseInt and List.get are on a single line; refactor to use Java 7+
multi-catch (catch (NumberFormatException | IndexOutOfBoundsException e)) or
separate multi-line catch for readability, and ensure the caught exception
variable is either used or named e/ignored consistently to avoid compiler
warnings and improve clarity.
core-api/src/test/java/com/absmartly/sdk/jsonexpr/ExprEvaluatorTest.java (1)

414-429: stringConvert special-number tests are DecimalFormat-dependent.

The expected strings "NaN" and "\u221E" / "-\u221E" are produced by DecimalFormat.format(), which uses the JVM's default locale symbols. On a JVM with a non-standard DecimalFormatSymbols locale (e.g., set via -Duser.language), the infinity symbol or NaN representation could theoretically differ. This is unlikely to be an issue in practice but worth being aware of if tests run in varied locale environments.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/test/java/com/absmartly/sdk/jsonexpr/ExprEvaluatorTest.java`
around lines 414 - 429, The test testStringConvertSpecialNumbers relies on
locale-dependent DecimalFormat output from ExprEvaluator.stringConvert, so make
it locale-independent by setting a known locale for the test (e.g., Locale.ROOT
or Locale.US) before calling stringConvert and restoring the original default
afterwards; alternatively, change the assertions to compare against a
locale-neutral representation (e.g., Double.toString equivalents) or normalize
the produced string to a predictable form prior to asserting. Ensure this change
is applied inside testStringConvertSpecialNumbers and that original locale is
restored after the assertions.
gradle/wrapper/gradle-wrapper.properties (1)

1-1: Add distributionSha256Sum for distribution integrity verification.

Always set the distributionSha256Sum property in your gradle-wrapper.properties file to verify the integrity of the downloaded Gradle distribution. The Gradle Wrapper allows for verification of the downloaded Gradle distribution via SHA-256 hash sum comparison, which increases security against targeted attacks by preventing a man-in-the-middle attacker from tampering with the downloaded Gradle distribution.

🛡️ Proposed fix
 distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip
+distributionSha256Sum=17f277867f6914d61b1aa02efab1ba7bb439ad652ca485cd8ca6842fccec6e43
 distributionBase=GRADLE_USER_HOME

The SHA-256 value above is sourced from the official Gradle release checksums page for the gradle-8.5-all.zip distribution.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gradle/wrapper/gradle-wrapper.properties` at line 1, Add the
distributionSha256Sum property next to the existing distributionUrl entry to
enable Gradle Wrapper integrity verification: locate the distributionUrl line
(which references gradle-8.5-all.zip) and add a matching
distributionSha256Sum=<SHA-256-for-gradle-8.5-all.zip> value sourced from the
official Gradle release checksums so the wrapper verifies the downloaded
distribution.
build.gradle (1)

52-60: Consider migrating eager task creation to tasks.register (Gradle 8 deprecations).

The eager task sourcesJar(...) and task javadocJar(...) patterns, plus javadoc.destinationDir, are deprecated since Gradle 7 and generate deprecation warnings in Gradle 8.5. They still work, but migrating them avoids warning noise and future compatibility issues.

♻️ Proposed refactor
-	task sourcesJar(type: Jar, dependsOn: classes) {
-		archiveClassifier.set('sources')
-		from sourceSets.main.allSource
-	}
-
-	task javadocJar(type: Jar, dependsOn: javadoc) {
-		archiveClassifier.set('javadoc')
-		from javadoc.destinationDir
-	}
+	tasks.register('sourcesJar', Jar) {
+		dependsOn classes
+		archiveClassifier.set('sources')
+		from sourceSets.main.allSource
+	}
+
+	tasks.register('javadocJar', Jar) {
+		dependsOn javadoc
+		archiveClassifier.set('javadoc')
+		from javadoc.destinationDirectory.get().asFile
+	}

The artifact sourcesJar / artifact javadocJar references in the publishing block also need to become artifact tasks.named('sourcesJar') / artifact tasks.named('javadocJar') respectively.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@build.gradle` around lines 52 - 60, Replace the eager task declarations for
sourcesJar and javadocJar with lazy registrations using tasks.register
(referencing sourcesJar and javadocJar) and make javadocJar depend on the
lazily-resolved javadoc task (use tasks.named('javadoc') or the javadoc task's
destinationDirectory property instead of the deprecated javadoc.destinationDir).
Also update the publishing block to reference the artifacts via
tasks.named('sourcesJar') and tasks.named('javadocJar') (or
tasks.named(...).get()) rather than the old artifact variables so everything is
lazily configured and avoids the Gradle 8 deprecation warnings.
core-api/src/test/java/com/absmartly/sdk/DefaultHTTPClientTest.java (2)

301-302: setConnectTimeout(100) is a no-op in this test context.

The mocked transport never consults the config's timeout value; the ConnectTimeoutException is injected directly via callback.failed(...). The setConnectTimeout(100) call implies the test is asserting something about the 100 ms limit, but it has no bearing on the test outcome. Using DefaultHTTPClientConfig.create() (no custom timeout) would be less misleading.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/test/java/com/absmartly/sdk/DefaultHTTPClientTest.java` around
lines 301 - 302, The test is misleadingly configuring a timeout that the mocked
transport ignores; remove the no-op setConnectTimeout(100) and instantiate the
client with DefaultHTTPClient.create(DefaultHTTPClientConfig.create()) instead
so the test uses the default config. Update the instantiation in
DefaultHTTPClientTest where DefaultHTTPClient.create(...) is called (the call
that currently uses DefaultHTTPClientConfig.create().setConnectTimeout(100)) to
use the plain DefaultHTTPClientConfig.create() to avoid implying the test
depends on the timeout.

314-316: Redundant instanceof assertions after assertSame.

assertSame(x, thrown.getCause()) already guarantees the objects are identical, so assertTrue(thrown.getCause() instanceof T) is always trivially true and adds no signal. This pattern appears in testConnectionTimeout (lines 315–316), testReadTimeout (lines 340–341), and testSSLCertificateValidation (lines 415–416).

♻️ Remove redundant assertions
-		assertSame(timeoutException, thrown.getCause());
-		assertTrue(thrown.getCause() instanceof ConnectTimeoutException);
+		assertSame(timeoutException, thrown.getCause());
-		assertSame(readTimeoutException, thrown.getCause());
-		assertTrue(thrown.getCause() instanceof SocketTimeoutException);
+		assertSame(readTimeoutException, thrown.getCause());
-		assertSame(sslException, thrown.getCause());
-		assertTrue(thrown.getCause() instanceof javax.net.ssl.SSLHandshakeException);
+		assertSame(sslException, thrown.getCause());

Also applies to: 339-341, 414-416

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/test/java/com/absmartly/sdk/DefaultHTTPClientTest.java` around
lines 314 - 316, Remove the redundant instanceof assertions that follow
assertSame in the unit tests: in testConnectionTimeout, testReadTimeout, and
testSSLCertificateValidation delete the lines asserting instanceof (e.g., the
assertTrue(thrown.getCause() instanceof ConnectTimeoutException) and similar for
read/SSL exceptions) since assertSame(timeoutException, thrown.getCause())
already guarantees identity; keep the assertThrows and assertSame checks
unchanged.
core-api/src/test/java/com/absmartly/sdk/DefaultHTTPClientConfigTest.java (1)

105-114: testDefaultValues does not assert the securityProvider default.

DefaultHTTPClientConfig initialises securityProvider_ to null. The test covers all other six fields but omits this one.

♻️ Proposed addition
 	void testDefaultValues() {
 		final DefaultHTTPClientConfig config = DefaultHTTPClientConfig.create();
 		assertEquals(3000, config.getConnectTimeout());
 		assertEquals(30000, config.getConnectionKeepAlive());
 		assertEquals(1000, config.getConnectionRequestTimeout());
 		assertEquals(5, config.getMaxRetries());
 		assertEquals(333, config.getRetryInterval());
 		assertEquals(HTTPVersionPolicy.NEGOTIATE, config.getHTTPVersionPolicy());
+		assertNull(config.getSecurityProvider());
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/test/java/com/absmartly/sdk/DefaultHTTPClientConfigTest.java`
around lines 105 - 114, The test testDefaultValues in
DefaultHTTPClientConfigTest misses asserting the default for securityProvider_;
update the test to assert the default value returned by
DefaultHTTPClientConfig.create() via getSecurityProvider() (expecting null) so
the securityProvider default is covered alongside the other fields (modify
testDefaultValues to include this assertion).
core-api/src/main/java/com/absmartly/sdk/internal/JsonMapperUtils.java (2)

7-13: Add a private constructor to prevent instantiation of this utility class.

♻️ Suggested fix
 public class JsonMapperUtils {
+    private JsonMapperUtils() {}
+
     public static ObjectMapper createStandardObjectMapper() {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/main/java/com/absmartly/sdk/internal/JsonMapperUtils.java`
around lines 7 - 13, The utility class JsonMapperUtils should not be
instantiable; add a private no-argument constructor to the JsonMapperUtils class
to prevent creating instances (so only its static method
createStandardObjectMapper can be used), and ensure the constructor is declared
private and empty to satisfy static-utility-class conventions.

10-10: MapperFeature.USE_STATIC_TYPING is a serialisation-only feature — it is a no-op for all current consumers.

Jackson's own documentation states: "Note that deserialization always uses declared static types since no runtime types are available."

Every current consumer of createStandardObjectMapper() (DefaultAudienceDeserializer, DefaultContextDataDeserializer, DefaultVariableParser) uses the mapper exclusively for reading/deserialisation. The feature therefore has no effect. Only DefaultContextEventSerializer (a writer) would benefit, but it does not use this utility. Consider either removing the flag from this utility or documenting the intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/main/java/com/absmartly/sdk/internal/JsonMapperUtils.java` at
line 10, The MapperFeature.USE_STATIC_TYPING flag set in
createStandardObjectMapper() is a serialization-only/no-op for current
deserializing consumers; remove the
objectMapper.enable(MapperFeature.USE_STATIC_TYPING) call from
createStandardObjectMapper() (or alternatively add a comment explaining why it's
present) and update callers if needed—notably DefaultAudienceDeserializer,
DefaultContextDataDeserializer, and DefaultVariableParser use the mapper only
for reading so they don't need this flag, while DefaultContextEventSerializer is
the only writer that would benefit and does not use this utility.
core-api/src/main/java/com/absmartly/sdk/DefaultContextEventSerializer.java (1)

18-19: DefaultContextEventSerializer skips JsonMapperUtils.createStandardObjectMapper() — only consumer where it would matter.

USE_STATIC_TYPING is a serialisation-only feature (Jackson docs state deserialization always uses declared static types). All three deserialiser/parser callers of JsonMapperUtils are read-only, so the feature has no effect there. This serialiser is the sole class where enabling USE_STATIC_TYPING would actually change behaviour, yet it still constructs a plain new ObjectMapper().

♻️ Suggested alignment
+import com.absmartly.sdk.internal.JsonMapperUtils;
 ...
 public DefaultContextEventSerializer() {
-    final ObjectMapper objectMapper = new ObjectMapper();
+    final ObjectMapper objectMapper = JsonMapperUtils.createStandardObjectMapper();
     this.writer_ = objectMapper.writerFor(PublishEvent.class);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/main/java/com/absmartly/sdk/DefaultContextEventSerializer.java`
around lines 18 - 19, DefaultContextEventSerializer constructs a plain new
ObjectMapper() so its writer_ (writerFor(PublishEvent.class)) ignores any
standard configuration (e.g. USE_STATIC_TYPING) provided by JsonMapperUtils;
replace the ObjectMapper creation with
JsonMapperUtils.createStandardObjectMapper() when building the ObjectMapper used
to initialize writer_ in DefaultContextEventSerializer so the serializer honors
the shared standard mapper settings for PublishEvent serialization.
example/src/main/java/com/absmartly/sdk/Example.java (1)

24-36: Consider using try-with-resources to guarantee ctx and sdk are closed even on exception.

As written, any exception between createContext(...) and ctx.close() will leak the context and SDK resources. Even in an example, demonstrating the idiomatic pattern is useful.

♻️ Suggested pattern
-final Context ctx = sdk.createContext(contextConfig).waitUntilReady();
-// ... use ctx ...
-ctx.close();
-sdk.close();
+try (final ABsmartly sdk = ABsmartly.create(sdkConfig);
+     final Context ctx = sdk.createContext(contextConfig).waitUntilReady()) {
+    // ... use ctx ...
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example/src/main/java/com/absmartly/sdk/Example.java` around lines 24 - 36,
Wrap the SDK and Context in a try-with-resources so they are always closed on
exception: move or create the SDK instance in a resource declaration (or nest
try-with-resources) and obtain the Context via
sdk.createContext(...).waitUntilReady() as a resource, then perform
ctx.getTreatment(...), ctx.track(...), etc.; remove explicit ctx.close() and
sdk.close() calls and rely on try-with-resources to auto-close the Context and
SDK (referencing createContext, waitUntilReady, ctx, ctx.getTreatment,
ctx.track, ctx.close, sdk.close).
core-api/src/test/java/com/absmartly/sdk/ContextTest.java (2)

2511-2523: Near-duplicate of the existing publishExceptionally test at line 1641

publishPropagatesClientErrorOnFailure is essentially the same test as publishExceptionally, with minor differences in goal properties and exception message. Consider removing this test and, if the intent is to give a more focused name, rename publishExceptionally instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/test/java/com/absmartly/sdk/ContextTest.java` around lines 2511
- 2523, The test publishPropagatesClientErrorOnFailure duplicates
publishExceptionally; remove the redundant publishPropagatesClientErrorOnFailure
test or consolidate by renaming/adjusting publishExceptionally to cover both
cases (e.g., change publishExceptionally’s name to publishPropagatesClientError
and/or add the slightly different goal/property inputs and exception message
there) so only one test verifies that Context.publish propagates the client
error (references: test methods publishPropagatesClientErrorOnFailure and
publishExceptionally, Context.publish).

2565-2583: Near-duplicate of the existing closeAsync test at line 1661

closeAsyncReturnsSameFutureWhenCalledTwice covers the same core assertion as closeAsync (lines 1661–1685), which also verifies that two closeAsync() calls return the same future, plus additionally checks closing/closed state and event handler invocation count. Consider removing this test.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/test/java/com/absmartly/sdk/ContextTest.java` around lines 2565
- 2583, The test closeAsyncReturnsSameFutureWhenCalledTwice duplicates
assertions already covered in the existing closeAsync test; remove the redundant
test method closeAsyncReturnsSameFutureWhenCalledTwice from ContextTest (or
consolidate its unique assertions into the existing closeAsync test) so we only
keep one test that verifies multiple closeAsync() calls return the same
CompletableFuture and the context transitions to closed; update imports/mocks if
needed after removal.
core-api/src/test/java/com/absmartly/sdk/DefaultContextDataDeserializerTest.java (1)

127-146: testMalformedJsonResponse bundles four distinct scenarios — one failing assertion silences the rest

In JUnit 5, a failing assert* stops the method, so the {} and [] cases will never be reached if the earlier assertions fail. Also, the third case ({}assertNotNull) is valid JSON and arguably belongs in a separate test (testEmptyObjectJson), distinct from truly malformed inputs. Consider splitting into individual @Test methods or using assertAll.

♻️ Suggested split (partial example)
-    `@Test`
-    void testMalformedJsonResponse() {
-        final ContextDataDeserializer deser = new DefaultContextDataDeserializer();
-
-        final byte[] malformedJson = "{\"experiments\": [".getBytes();
-        final ContextData result = deser.deserialize(malformedJson, 0, malformedJson.length);
-        assertNull(result);
-
-        final byte[] invalidJson = "not a json at all".getBytes();
-        final ContextData result2 = deser.deserialize(invalidJson, 0, invalidJson.length);
-        assertNull(result2);
-
-        final byte[] emptyBraces = "{}".getBytes();
-        final ContextData result3 = deser.deserialize(emptyBraces, 0, emptyBraces.length);
-        assertNotNull(result3);
-
-        final byte[] emptyArray = "[]".getBytes();
-        final ContextData result4 = deser.deserialize(emptyArray, 0, emptyArray.length);
-        assertNull(result4);
-    }
+    `@Test`
+    void testIncompleteJsonReturnsNull() {
+        final ContextDataDeserializer deser = new DefaultContextDataDeserializer();
+        assertNull(deser.deserialize("{\"experiments\": [".getBytes(), 0, 18));
+    }
+
+    `@Test`
+    void testNonJsonInputReturnsNull() {
+        final ContextDataDeserializer deser = new DefaultContextDataDeserializer();
+        assertNull(deser.deserialize("not a json at all".getBytes(), 0, 17));
+    }
+
+    `@Test`
+    void testEmptyObjectDeserializesToContextData() {
+        final ContextDataDeserializer deser = new DefaultContextDataDeserializer();
+        assertNotNull(deser.deserialize("{}".getBytes(), 0, 2));
+    }
+
+    `@Test`
+    void testJsonArrayRootReturnsNull() {
+        final ContextDataDeserializer deser = new DefaultContextDataDeserializer();
+        assertNull(deser.deserialize("[]".getBytes(), 0, 2));
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core-api/src/test/java/com/absmartly/sdk/DefaultContextDataDeserializerTest.java`
around lines 127 - 146, The test method testMalformedJsonResponse in
DefaultContextDataDeserializerTest mixes four scenarios so a failing assertion
aborts the rest; split it into separate `@Test` methods (e.g.,
testMalformedJson_returnsNull, testNonJson_returnsNull,
testEmptyObject_returnsNotNull, testEmptyArray_returnsNull) that each call
ContextDataDeserializer.deserialize (or keep reuse of deser/deserialize) and
assert the expected outcome, or wrap the assertions in an assertAll if you
prefer a single method; ensure each new test references the same
ContextDataDeserializer deser and the deserialize(byte[], int, int) invocation
so behavior is validated independently.
core-api/src/test/java/com/absmartly/sdk/DefaultContextDataProviderTest.java (1)

91-93: Minor: prefer a top-level import over the fully-qualified TimeoutException class name

java.util.concurrent.TimeoutException is referenced twice in this method. Adding an import keeps the code consistent with other types in this file.

♻️ Suggested change

Add at the top of the file alongside other imports:

+import java.util.concurrent.TimeoutException;

Then simplify the declaration:

-        final java.util.concurrent.TimeoutException timeoutException = new java.util.concurrent.TimeoutException(
-                "Request timed out");
+        final TimeoutException timeoutException = new TimeoutException("Request timed out");

And update the instanceof check:

-        assertTrue(actual.getCause() instanceof java.util.concurrent.TimeoutException);
+        assertTrue(actual.getCause() instanceof TimeoutException);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/test/java/com/absmartly/sdk/DefaultContextDataProviderTest.java`
around lines 91 - 93, In DefaultContextDataProviderTest, replace the
fully-qualified java.util.concurrent.TimeoutException usages with a top-level
import: add "import java.util.concurrent.TimeoutException;" to the imports, then
change the declaration of timeoutException to use TimeoutException and adjust
any instanceof checks (e.g., in assertions against failedFuture's exception) to
use TimeoutException instead of the fully-qualified name; this keeps the style
consistent with other types in the file.
core-api/src/main/java/com/absmartly/sdk/deprecated/ABSmartly.java (1)

7-15: Missing @deprecated Javadoc tag — callers won't know what to migrate to

The class-level @Deprecated annotation suppresses usage but provides no guidance. Adding a @deprecated Javadoc tag with a @see (or @link) reference to the replacement class is the standard Java convention and improves IDE tooling.

♻️ Suggested Javadoc addition
+/**
+ * `@deprecated` Use {`@link` com.absmartly.sdk.ABsmartly} instead.
+ */
 `@Deprecated`
 public class ABSmartly extends ABsmartly {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/main/java/com/absmartly/sdk/deprecated/ABSmartly.java` around
lines 7 - 15, Add a Javadoc block to the deprecated ABSmartly class that
includes a `@deprecated` tag and a `@see` (or {`@link`}) reference to the replacement
ABsmartly class so callers/IDEs know the migration target; update the
class-level comment above public class ABSmartly (which contains the create(...)
factory and protected constructor ABSmartly(...)) to include the deprecation
guidance and reference.
core-api/src/main/java/com/absmartly/sdk/DefaultContextDataDeserializer.java (1)

19-19: ObjectMapper.enable(MapperFeature) is deprecated since Jackson 2.13, and USE_STATIC_TYPING has no effect on deserialisation

The JsonMapperUtils.createStandardObjectMapper() utility (referenced from core-api/src/main/java/com/absmartly/sdk/internal/JsonMapperUtils.java, lines 10–11) uses deprecated Jackson API. Two concerns:

  1. ObjectMapper.enable(MapperFeature...) has been deprecated since Jackson 2.13 — the idiomatic replacement is JsonMapper.builder().
  2. USE_STATIC_TYPING determines whether serialisation type detection uses runtime or declared static type. Deserialistion always uses declared static types; enabling this flag has no effect in a deserialiser context.

The project uses Jackson 2.13.4.2, so migrate JsonMapperUtils to the builder API and remove the no-op USE_STATIC_TYPING flag for clarity and to eliminate deprecation warnings.

♻️ Suggested update for `JsonMapperUtils.createStandardObjectMapper()`
-import com.fasterxml.jackson.databind.MapperFeature;
 import com.fasterxml.jackson.databind.DeserializationFeature;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;

 public class JsonMapperUtils {
     public static ObjectMapper createStandardObjectMapper() {
-        final ObjectMapper objectMapper = new ObjectMapper();
-        objectMapper.enable(MapperFeature.USE_STATIC_TYPING);
-        objectMapper.enable(DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY);
-        return objectMapper;
+        return JsonMapper.builder()
+                .enable(DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY)
+                .build();
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/main/java/com/absmartly/sdk/DefaultContextDataDeserializer.java`
at line 19, Update JsonMapperUtils.createStandardObjectMapper() to stop using
the deprecated ObjectMapper.enable(MapperFeature...) call and instead build the
mapper via JsonMapper.builder(), configuring the equivalent features on the
builder; remove any enabling of MapperFeature.USE_STATIC_TYPING (it's a no-op
for deserialization) and ensure the resulting mapper is functionally equivalent
for the project (e.g., register modules, set property naming/visibility, and any
other previously-enabled MapperFeatures) so that DefaultContextDataDeserializer
(which calls JsonMapperUtils.createStandardObjectMapper()) continues to work
without deprecation warnings.
core-api/src/main/java/com/absmartly/sdk/Context.java (2)

1131-1137: buildAttributesMap iterates attributes_ without acquiring a lock — relies on caller to hold contextLock_.

All current call sites hold contextLock_ (read or write), so this is safe. Consider adding a brief Javadoc or assertion noting the precondition for future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/main/java/com/absmartly/sdk/Context.java` around lines 1131 -
1137, buildAttributesMap reads the mutable attributes_ collection without
synchronizing, relying on callers to hold contextLock_; add an explicit contract
to prevent future misuse by either inserting a brief Javadoc on
buildAttributesMap stating it must be called while holding contextLock_ and/or
add a runtime assertion inside buildAttributesMap that checks
Thread.holdsLock(contextLock_) (or equivalent) to fail fast; reference the
buildAttributesMap method and the contextLock_ field so maintainers see the
precondition.

726-740: audienceMatches re-evaluates on every cache hit after any setAttribute call.

The attrsSeq stored on the cached Assignment is only set when the assignment is first created (line 870). It is never updated when audienceMatches confirms the cached value is still correct. This means after any setAttribute call, every subsequent getTreatment will rebuild the attributes map and re-evaluate audiences, even when the result is unchanged.

If this is intentional (simplicity over performance — avoids needing a write lock to update the assignment), it's fine for most workloads. But for contexts with frequent attribute changes and many experiments, consider updating assignment.attrsSeq inside the write lock path when the cache is rebuilt, to avoid redundant evaluations on subsequent read-lock hits.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/main/java/com/absmartly/sdk/Context.java` around lines 726 -
740, The audienceMatches method currently re-evaluates audiences after any
setAttribute because Assignment.attrsSeq is only initialized when the assignment
is created (see where attrsSeq is set in Assignment) and never updated; change
the cache-rebuild/write-lock path that calls buildAttributesMap and
audienceMatcher_.evaluate (the code that updates the cached Assignment) so that
when a re-evaluation confirms the cached assignment is still valid you also
update assignment.attrsSeq to the current attrsSeq_ value; specifically locate
the code that rebuilds the assignment in the write-lock path and set
assignment.attrsSeq = attrsSeq_ (or the current sequence token) whenever
audienceMatches returns that the audience result did not change, so subsequent
read-lock hits skip redundant buildAttributesMap and audienceMatcher_.evaluate
calls.
core-api/src/main/java/com/absmartly/sdk/ABsmartly.java (1)

72-91: close() is not thread-safe — concurrent callers may double-close resources.

Both client_ and scheduler_ are non-volatile fields with no synchronisation around the null-check-then-act pattern. If two threads call close() concurrently, both could enter the if (client_ != null) block and invoke client_.close() twice, potentially causing issues depending on the Client implementation. Similarly for the scheduler block.

This may be acceptable if close() is only ever called from a single thread (typical for application shutdown), but it's worth noting given the concurrency improvements elsewhere in this PR.

🛡️ Suggested fix: synchronise close() or use a closed guard

A simple approach is to use an AtomicBoolean guard (similar to Context.closed_):

+    private final AtomicBoolean closed_ = new AtomicBoolean(false);
+
     `@Override`
     public void close() throws IOException {
+        if (!closed_.compareAndSet(false, true)) {
+            return;
+        }
         if (client_ != null) {
             client_.close();
             client_ = null;
         }
         // ... scheduler shutdown ...
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/main/java/com/absmartly/sdk/ABsmartly.java` around lines 72 -
91, The close() method is not thread-safe: concurrent callers can double-close
client_ and scheduler_. Add a final AtomicBoolean closed_ (or reuse
Context.closed_) and check if closed_.compareAndSet(false,true) at the start of
close(); only proceed to close client_ and shutdown scheduler_ when the CAS
succeeds, and make client_ and scheduler_ volatile or only accessed after the
guard to avoid races; this ensures close() runs exactly once and prevents
double-close races on client_.close() and
scheduler_.shutdown()/shutdownNow()/awaitTermination().

Comment on lines +38 to +39
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Document the Java 6/7 → 8 bytecode break in the release notes.

Bumping sourceCompatibility and targetCompatibility from 1.6 to 1.8 changes the published JAR's minimum required JVM from Java 6 to Java 8. Any downstream consumer still running Java 6 or 7 will fail to load the SDK at class-loading time. This is a binary-breaking change and should be called out explicitly in the changelog or release notes alongside the version bump.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@build.gradle` around lines 38 - 39, The build now sets sourceCompatibility
and targetCompatibility to "1.8", which raises the published JAR's minimum JVM
requirement from Java 6/7 to Java 8; add a clear changelog/release-notes entry
calling out this binary-breaking change so downstream users are warned. Update
your release notes (or CHANGELOG) to state that
sourceCompatibility/targetCompatibility were bumped to 1.8, that older JVMs
(Java 6 and 7) will fail at class-load time, and include migration guidance
(e.g., upgrade JVM to Java 8 or use an earlier SDK release). Ensure the note
references the change to sourceCompatibility and targetCompatibility so it’s
discoverable.

Comment on lines +34 to +42
if (!endpoint.startsWith("https://")) {
if (endpoint.startsWith("http://")) {
System.err.println(
"WARNING: ABSmartly SDK endpoint is not using HTTPS. API keys will be transmitted in plaintext: "
+ endpoint);
} else {
throw new IllegalArgumentException("Endpoint must use http:// or https:// protocol: " + endpoint);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Replace System.err.println with the SLF4J logger — library code must not write directly to stderr.

System.err.println bypasses any logging framework configured by the SDK consumer, cannot be suppressed or redirected, and is inconsistent with every other class in this codebase. Client has no Logger field; one should be added.

🔧 Proposed fix

Add a logger field alongside the other private final fields at the bottom of the class:

+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 ...
 public class Client implements Closeable {
+    private static final Logger log = LoggerFactory.getLogger(Client.class);

Then replace the System.err.println call:

-    System.err.println(
-            "WARNING: ABSmartly SDK endpoint is not using HTTPS. API keys will be transmitted in plaintext: "
-                    + endpoint);
+    log.warn("ABsmartly SDK endpoint is not using HTTPS. API keys will be transmitted in plaintext: {}", endpoint);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/main/java/com/absmartly/sdk/Client.java` around lines 34 - 42,
Replace the direct stderr write in Client with an SLF4J logger: add a private
static final Logger (e.g., LoggerFactory.getLogger(Client.class)) as a field in
the Client class and change the System.err.println call to use logger.warn(...)
with the same warning message (including the endpoint). Ensure imports for
org.slf4j.Logger and org.slf4j.LoggerFactory are added and no other behavior is
changed.

Comment on lines +37 to +38
"WARNING: ABSmartly SDK endpoint is not using HTTPS. API keys will be transmitted in plaintext: "
+ endpoint);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Branding inconsistency: warning text says "ABSmartly SDK" but the project renames to "ABsmartly".

🔧 Proposed fix
-"WARNING: ABSmartly SDK endpoint is not using HTTPS. API keys will be transmitted in plaintext: "
+"WARNING: ABsmartly SDK endpoint is not using HTTPS. API keys will be transmitted in plaintext: "
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"WARNING: ABSmartly SDK endpoint is not using HTTPS. API keys will be transmitted in plaintext: "
+ endpoint);
"WARNING: ABsmartly SDK endpoint is not using HTTPS. API keys will be transmitted in plaintext: "
endpoint);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/main/java/com/absmartly/sdk/Client.java` around lines 37 - 38,
Update the branding in the plaintext HTTPS warning emitted by the Client class:
replace the "ABSmartly SDK" token in the string literal currently built in
Client.java with the correct project name "ABsmartly" so the message reads e.g.
"WARNING: ABsmartly SDK endpoint is not using HTTPS. API keys will be
transmitted in plaintext: " + endpoint; locate the string inside the Client
class where the warning is constructed and change the casing to match the
project's branding.

Comment on lines +103 to +104
dataFuture.complete(
deserializer_.deserialize(content, 0, content.length));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

deserializer_.deserialize() can return null — completing the future successfully with null propagates a silent failure.

The deserializers were changed in this PR to return null on bad input rather than throw. If deserialization fails here, dataFuture.complete(null) is called, and any downstream code that dereferences the ContextData result without a null check will throw an NPE. The same guard pattern used for the empty-body case (line 100) should apply here.

🔧 Proposed fix
-    dataFuture.complete(
-            deserializer_.deserialize(content, 0, content.length));
+    final ContextData data = deserializer_.deserialize(content, 0, content.length);
+    if (data == null) {
+        dataFuture.completeExceptionally(
+                new IllegalStateException("Failed to deserialize context data response"));
+    } else {
+        dataFuture.complete(data);
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
dataFuture.complete(
deserializer_.deserialize(content, 0, content.length));
final ContextData data = deserializer_.deserialize(content, 0, content.length);
if (data == null) {
dataFuture.completeExceptionally(
new IllegalStateException("Failed to deserialize context data response"));
} else {
dataFuture.complete(data);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/main/java/com/absmartly/sdk/Client.java` around lines 103 - 104,
deserializer_.deserialize(...) can return null which currently causes
dataFuture.complete(null) and later NPEs; change the handler so that after
calling deserializer_.deserialize(content, 0, content.length) you check for null
and treat it like the empty-body case: do not complete the future with null but
instead completeExceptionally (or complete with a failed state) and
log/propagate an error so downstream code expecting a non-null ContextData does
not receive null; update the code path around deserializer_.deserialize,
dataFuture.complete and any code that assumes a non-null ContextData to follow
the same guard pattern used for the empty-body branch (the earlier empty-body
check) and ensure ContextData is validated before completing the future.

Comment on lines +9 to +11
final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(MapperFeature.USE_STATIC_TYPING);
objectMapper.enable(DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find build configuration files
find . -maxdepth 3 -type f \( -name "pom.xml" -o -name "build.gradle*" -o -name "gradle.properties" \) | head -20

Repository: absmartly/java-sdk

Length of output: 123


🌐 Web query:

ObjectMapper.enable MapperFeature deprecated Jackson 2.13 builder pattern JsonMapper

💡 Result:

In Jackson 2.13, only the ObjectMapper mutators that take MapperFeature were deprecated:

  • ObjectMapper.enable(MapperFeature... f)
  • ObjectMapper.disable(MapperFeature... f)
  • ObjectMapper.configure(MapperFeature f, boolean state)

They’re deprecated “Since 2.13” in favor of configuring via the JsonMapper builder: JsonMapper.builder().enable(...) / .disable(...) / .configure(...). [1]

Recommended (new) pattern

import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;

JsonMapper mapper = JsonMapper.builder()
    .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)
    .disable(MapperFeature.DEFAULT_VIEW_INCLUSION)
    .build();

(You can also mix in other feature types on the builder, e.g. SerializationFeature, DeserializationFeature, etc.)

If you already have a JsonMapper instance

JsonMapper provides rebuild() to get a builder from the current mapper, then build() a new configured instance. [2]

JsonMapper mapper2 = existingJsonMapper.rebuild()
    .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)
    .build();

Note

ObjectMapper.enable(...) for non-MapperFeature enums (like SerializationFeature, DeserializationFeature, JsonParser.Feature, JsonGenerator.Feature) is not what this 2.13 deprecation targets—this deprecation is specifically for the MapperFeature overloads. [1]

Sources:
[1] Jackson databind 2.13 ObjectMapper Javadoc (deprecations + replacement with JsonMapper.builder()) https://fasterxml.github.io/jackson-databind/javadoc/2.13/com/fasterxml/jackson/databind/ObjectMapper.html
[2] Jackson databind 2.13 JsonMapper Javadoc (builder() / rebuild()) https://fasterxml.github.io/jackson-databind/javadoc/2.13/com/fasterxml/jackson/databind/json/JsonMapper.html


🏁 Script executed:

# Check the Jackson version in core-api/build.gradle
cat core-api/build.gradle | grep -A 10 -B 10 "jackson"

Repository: absmartly/java-sdk

Length of output: 1399


🏁 Script executed:

# Check the root build.gradle for jacksonVersion definition
cat build.gradle | grep -i "jackson"

Repository: absmartly/java-sdk

Length of output: 125


🏁 Script executed:

# Also check for gradle.properties or similar version files
cat gradle.properties 2>/dev/null || echo "gradle.properties not found"

Repository: absmartly/java-sdk

Length of output: 89


Replace ObjectMapper with JsonMapper builder to avoid deprecated mutation API.

The ObjectMapper.enable(MapperFeature) mutation API was deprecated in Jackson 2.13 (your project uses 2.13.4.2) in favour of the builder pattern. The DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY call is unaffected—only MapperFeature overloads are deprecated.

Use JsonMapper.builder() instead:

Suggested fix
final ObjectMapper objectMapper = JsonMapper.builder()
    .enable(MapperFeature.USE_STATIC_TYPING)
    .build()
    .enable(DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY);

Or if you prefer to keep ObjectMapper, configure all features on the builder:

final ObjectMapper objectMapper = JsonMapper.builder()
    .enable(MapperFeature.USE_STATIC_TYPING)
    .enable(DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY)
    .build();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/main/java/com/absmartly/sdk/internal/JsonMapperUtils.java`
around lines 9 - 11, Replace the deprecated ObjectMapper mutation API by
creating the mapper with JsonMapper.builder(): stop calling
objectMapper.enable(MapperFeature.USE_STATIC_TYPING) on an ObjectMapper instance
and instead use JsonMapper.builder().enable(MapperFeature.USE_STATIC_TYPING)
before build(); for DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY either
enable it on the same builder or call .enable(...) on the returned
ObjectMapper—adjust the code around the existing ObjectMapper variable in
JsonMapperUtils to use JsonMapper.builder() and enable the listed features
there.

Comment on lines +63 to +103
@Test
void testNegativeConnectTimeout() {
final DefaultHTTPClientConfig config = DefaultHTTPClientConfig.create()
.setConnectTimeout(-1);
assertEquals(-1, config.getConnectTimeout());
}

@Test
void testNegativeConnectionKeepAlive() {
final DefaultHTTPClientConfig config = DefaultHTTPClientConfig.create()
.setConnectionKeepAlive(-1);
assertEquals(-1, config.getConnectionKeepAlive());
}

@Test
void testNegativeConnectionRequestTimeout() {
final DefaultHTTPClientConfig config = DefaultHTTPClientConfig.create()
.setConnectionRequestTimeout(-1);
assertEquals(-1, config.getConnectionRequestTimeout());
}

@Test
void testNegativeRetryInterval() {
final DefaultHTTPClientConfig config = DefaultHTTPClientConfig.create()
.setRetryInterval(-1);
assertEquals(-1, config.getRetryInterval());
}

@Test
void testZeroMaxRetries() {
final DefaultHTTPClientConfig config = DefaultHTTPClientConfig.create()
.setMaxRetries(0);
assertEquals(0, config.getMaxRetries());
}

@Test
void testNegativeMaxRetries() {
final DefaultHTTPClientConfig config = DefaultHTTPClientConfig.create()
.setMaxRetries(-1);
assertEquals(-1, config.getMaxRetries());
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "DefaultHTTPClient.java" | head -5

Repository: absmartly/java-sdk

Length of output: 127


🏁 Script executed:

rg -n "Timeout\.ofMilliseconds|TimeValue\.ofMilliseconds|getConnectTimeout|getConnectionKeepAlive|getConnectionRequestTimeout|getRetryInterval" \
   --type java -A 2 -B 2 | head -100

Repository: absmartly/java-sdk

Length of output: 9361


🏁 Script executed:

find . -type f -name "DefaultHTTPClientConfig.java" -o -name "DefaultHTTPClient.java" | xargs wc -l

Repository: absmartly/java-sdk

Length of output: 220


🏁 Script executed:

cat -n core-api/src/main/java/com/absmartly/sdk/DefaultHTTPClientConfig.java

Repository: absmartly/java-sdk

Length of output: 2703


🏁 Script executed:

cat -n core-api/src/main/java/com/absmartly/sdk/DefaultHTTPClient.java | head -70

Repository: absmartly/java-sdk

Length of output: 3523


🌐 Web query:

Apache HttpClient 5 HC5 Timeout.ofMilliseconds non-negative contract

💡 Result:

In Apache HttpComponents Core / HttpClient 5 (HC5), org.apache.hc.core5.util.Timeout is defined to be non-negative (“a timeout value as a non-negative long time …”).[1]

What Timeout.ofMilliseconds(...) accepts

  • Timeout.ofMilliseconds(long milliseconds) ultimately calls new Timeout(duration, unit).[2]
  • The Timeout constructor enforces the contract with Args.notNegative(duration, "duration"), so any negative value (e.g., -1) violates the contract and results in an IllegalArgumentException.[2]

How to represent “no timeout”

  • HC5 represents disabled / infinite timeout as 0 MILLISECONDS via the constants Timeout.DISABLED and Timeout.INFINITE (both effectively 0 ms).[1][2]
  • Separately, some APIs that take a more general TimeValue (not Timeout) may treat non-positive values as “indefinite” (for example, Deadline.calculate(TimeValue)).[3] But that does not change Timeout’s non-negative requirement.

Sources:
[1] Timeout Javadoc (Core 5.3.x)
[2] Timeout.java source (constructor uses Args.notNegative, ofMilliseconds delegates to it)
[3] Deadline.calculate(TimeValue) Javadoc (non-positive means indefinite for TimeValue)


🌐 Web query:

Apache HttpClient 5 TimeValue.ofMilliseconds negative values behavior

💡 Result:

TimeValue.ofMilliseconds(long millis) in HttpClient/HttpCore 5 accepts negative values (it does not clamp or throw). The API even exposes predefined constants like TimeValue.NEG_ONE_MILLISECOND / NEG_ONE_SECOND alongside ZERO_MILLISECONDS, which indicates negatives are an expected/valid representation of a duration value. [1]

What that means depends on where the TimeValue is used:

  • In deadline-based calculations, non-positive time values (0 or negative) are treated as “indefinite timeout without a deadline” (i.e., no deadline is computed). [2]
  • In the closely related Timeout type (a subclass/related concept in the same util package), DISABLED and INFINITE are both represented as 0 milliseconds, and the project has an issue discussing/clarifying the semantics difference between “disabled” vs “infinite.” [3][4]

So: TimeValue.ofMilliseconds(-N) produces a negative TimeValue, and some parts of the stack interpret <= 0 as “indefinite / no deadline”, while other timeout APIs may treat 0 as a special value with semantics that vary by component. [1][2][3][4]

Sources: [1] [2] [3] [4]


Negative timeout/retry tests document unsafe values that will cause runtime failures in DefaultHTTPClient.

These tests verify that DefaultHTTPClientConfig stores negative values without throwing—correct for a simple POJO. However, DefaultHTTPClient passes these directly to Apache HC5's Timeout and TimeValue methods, which behave differently:

  • Timeout.ofMilliseconds(config.getConnectTimeout()) (line 59) and Timeout.ofMilliseconds(config.getConnectionRequestTimeout()) (line 60) will throw IllegalArgumentException if passed -1, as Timeout enforces a non-negative contract via Args.notNegative().
  • TimeValue.ofMilliseconds(config.getConnectionKeepAlive()) (lines 54, 58) accepts -1 but interprets it as "indefinite timeout", which is likely unintended.

The tests implicitly document that these negative values are acceptable inputs, when in fact they violate HC5's contract. Consider adding validation to the config setters to reject negative values with IllegalArgumentException, or at minimum add a comment clarifying why negative values are intentionally unvalidated. If validation is added, update these tests to assert the thrown exception rather than the stored value.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/test/java/com/absmartly/sdk/DefaultHTTPClientConfigTest.java`
around lines 63 - 103, The tests allow negative timeout/retry values but Apache
HC5 (used in DefaultHTTPClient via Timeout.ofMilliseconds and
TimeValue.ofMilliseconds) rejects or misinterprets negatives; update
DefaultHTTPClientConfig setter methods (setConnectTimeout,
setConnectionRequestTimeout, setConnectionKeepAlive, setRetryInterval,
setMaxRetries) to validate inputs and throw IllegalArgumentException for
negative values (ensure any special-case semantics are deliberate), and then
update the corresponding tests (testNegativeConnectTimeout,
testNegativeConnectionKeepAlive, testNegativeConnectionRequestTimeout,
testNegativeRetryInterval, testNegativeMaxRetries) to assertThrows
IllegalArgumentException instead of checking the stored negative value so
behavior matches HC5 expectations.

Comment on lines +369 to +393
@Test
void testRetryOnTransientError503Response() throws ExecutionException, InterruptedException {
try (final MockedStatic<HttpAsyncClientBuilder> builderStatic = Mockito
.mockStatic(HttpAsyncClientBuilder.class)) {
builderStatic.when(HttpAsyncClientBuilder::create).thenReturn(asyncHTTPClientBuilder);

final DefaultHTTPClient httpClient = DefaultHTTPClient.create(
DefaultHTTPClientConfig.create().setMaxRetries(3).setRetryInterval(100));

when(asyncHTTPClient.execute(any(), any())).thenAnswer(invocation -> {
final FutureCallback<SimpleHttpResponse> callback = invocation.getArgument(1);
callback.completed(SimpleHttpResponse.create(503, "Service Unavailable".getBytes(),
ContentType.TEXT_PLAIN));
return null;
});

final CompletableFuture<HTTPClient.Response> responseFuture = httpClient
.get("https://api.absmartly.com/v1/context", null, null);
final HTTPClient.Response response = responseFuture.get();

assertEquals(503, response.getStatusCode());

verify(asyncHTTPClient, Mockito.timeout(5000).times(1)).execute(any(), any());
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

testRetryOnTransientError503Response does not test retry behaviour at all.

Because the mock is hooked at the CloseableHttpAsyncClient.execute() level, the DefaultHTTPClientRetryStrategy configured via setRetryStrategy(...) on the builder is never consulted — it lives inside Apache's real async client implementation, which is entirely replaced by the mock. The verify(…).times(1) assertion actually proves the opposite of what the test name implies: execute is called exactly once, confirming zero retries occur.

As written, the test only checks that a 503 SimpleHttpResponse completes the CompletableFuture successfully — identical in effect to the existing get() test with a different status code.

To genuinely test retry logic, DefaultHTTPClientRetryStrategy itself should be exercised in a dedicated unit test, or an integration-style test should drive a real (or closer-to-real) HTTP client against a local stub server.

💡 Minimal rename + assertion adjustment to reflect actual coverage

If the intent is only to assert that 503 passes through the future without throwing (not that retries fire), rename accordingly and drop the misleading config options:

-	`@Test`
-	void testRetryOnTransientError503Response() throws ExecutionException, InterruptedException {
+	`@Test`
+	void get503ResponsePassesThroughFuture() throws ExecutionException, InterruptedException {
 		try (final MockedStatic<HttpAsyncClientBuilder> builderStatic = Mockito
 				.mockStatic(HttpAsyncClientBuilder.class)) {
 			builderStatic.when(HttpAsyncClientBuilder::create).thenReturn(asyncHTTPClientBuilder);
 
-			final DefaultHTTPClient httpClient = DefaultHTTPClient.create(
-					DefaultHTTPClientConfig.create().setMaxRetries(3).setRetryInterval(100));
+			final DefaultHTTPClient httpClient = DefaultHTTPClient.create(DefaultHTTPClientConfig.create());
 
 			when(asyncHTTPClient.execute(any(), any())).thenAnswer(invocation -> {
 				final FutureCallback<SimpleHttpResponse> callback = invocation.getArgument(1);
 				callback.completed(SimpleHttpResponse.create(503, "Service Unavailable".getBytes(),
 						ContentType.TEXT_PLAIN));
 				return null;
 			});
 
 			final CompletableFuture<HTTPClient.Response> responseFuture = httpClient
 					.get("https://api.absmartly.com/v1/context", null, null);
 			final HTTPClient.Response response = responseFuture.get();
 
 			assertEquals(503, response.getStatusCode());
 
 			verify(asyncHTTPClient, Mockito.timeout(5000).times(1)).execute(any(), any());
 		}
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Test
void testRetryOnTransientError503Response() throws ExecutionException, InterruptedException {
try (final MockedStatic<HttpAsyncClientBuilder> builderStatic = Mockito
.mockStatic(HttpAsyncClientBuilder.class)) {
builderStatic.when(HttpAsyncClientBuilder::create).thenReturn(asyncHTTPClientBuilder);
final DefaultHTTPClient httpClient = DefaultHTTPClient.create(
DefaultHTTPClientConfig.create().setMaxRetries(3).setRetryInterval(100));
when(asyncHTTPClient.execute(any(), any())).thenAnswer(invocation -> {
final FutureCallback<SimpleHttpResponse> callback = invocation.getArgument(1);
callback.completed(SimpleHttpResponse.create(503, "Service Unavailable".getBytes(),
ContentType.TEXT_PLAIN));
return null;
});
final CompletableFuture<HTTPClient.Response> responseFuture = httpClient
.get("https://api.absmartly.com/v1/context", null, null);
final HTTPClient.Response response = responseFuture.get();
assertEquals(503, response.getStatusCode());
verify(asyncHTTPClient, Mockito.timeout(5000).times(1)).execute(any(), any());
}
}
`@Test`
void get503ResponsePassesThroughFuture() throws ExecutionException, InterruptedException {
try (final MockedStatic<HttpAsyncClientBuilder> builderStatic = Mockito
.mockStatic(HttpAsyncClientBuilder.class)) {
builderStatic.when(HttpAsyncClientBuilder::create).thenReturn(asyncHTTPClientBuilder);
final DefaultHTTPClient httpClient = DefaultHTTPClient.create(DefaultHTTPClientConfig.create());
when(asyncHTTPClient.execute(any(), any())).thenAnswer(invocation -> {
final FutureCallback<SimpleHttpResponse> callback = invocation.getArgument(1);
callback.completed(SimpleHttpResponse.create(503, "Service Unavailable".getBytes(),
ContentType.TEXT_PLAIN));
return null;
});
final CompletableFuture<HTTPClient.Response> responseFuture = httpClient
.get("https://api.absmartly.com/v1/context", null, null);
final HTTPClient.Response response = responseFuture.get();
assertEquals(503, response.getStatusCode());
verify(asyncHTTPClient, Mockito.timeout(5000).times(1)).execute(any(), any());
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/test/java/com/absmartly/sdk/DefaultHTTPClientTest.java` around
lines 369 - 393, The test method testRetryOnTransientError503Response mistakenly
mocks CloseableHttpAsyncClient.execute so the builder-installed
DefaultHTTPClientRetryStrategy is never exercised; either rename the test to
reflect that it only verifies a 503 response is forwarded (e.g.,
testGetReturns503WithoutThrowing) and remove the misleading retry
config/verify(times(1)), or replace it with a true retry test that does not mock
execute at the client boundary but instead exercises
DefaultHTTPClientRetryStrategy directly (unit test the
DefaultHTTPClientRetryStrategy behavior) or runs an integration-style test
against a local stub server to observe multiple execute invocations; update
assertions and test name accordingly and reference
testRetryOnTransientError503Response, DefaultHTTPClient.create /
DefaultHTTPClientConfig.setMaxRetries / setRetryInterval,
DefaultHTTPClientRetryStrategy, and CloseableHttpAsyncClient.execute to locate
the code to change.

Comment on lines +243 to +253
for (int i = 0; i < threadCount; i++) {
executor.submit(() -> {
try {
startLatch.await();
final Integer result = Concurrency.computeIfAbsentRW(rwlock, map, "shared_key", computer);
assertEquals(42, result);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
doneLatch.countDown();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Assertion inside worker thread will be silently swallowed on failure.

The assertEquals(42, result) at line 248 executes inside a Runnable submitted to the executor. If this assertion fails, the AssertionError is caught by the executor and never propagated to the test thread. The test still passes because doneLatch.countDown() runs in the finally block regardless.

To surface failures, collect exceptions or use Future.get():

Proposed fix using Future.get()
+		final List<Future<?>> futures = new ArrayList<>();
 		for (int i = 0; i < threadCount; i++) {
-			executor.submit(() -> {
+			futures.add(executor.submit(() -> {
 				try {
 					startLatch.await();
 					final Integer result = Concurrency.computeIfAbsentRW(rwlock, map, "shared_key", computer);
 					assertEquals(42, result);
 				} catch (InterruptedException e) {
 					Thread.currentThread().interrupt();
 				} finally {
 					doneLatch.countDown();
 				}
-			});
+			}));
 		}
 
 		startLatch.countDown();
 		doneLatch.await();
 		executor.shutdown();
 
+		for (Future<?> f : futures) {
+			f.get(); // propagates assertion errors from worker threads
+		}
+
 		assertEquals(1, map.size());
 		assertEquals(1, computeCount.get());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/test/java/com/absmartly/sdk/internal/ConcurrencyTest.java`
around lines 243 - 253, The assertion inside the Runnable is swallowed by the
executor; change the worker submission in the test to submit Callables (or
capture Futures) so that you can call Future.get() after doneLatch.await() to
rethrow any AssertionError/Exceptions back to the test thread, or alternatively
collect thrown Throwables from each worker into a concurrent collection and
assert emptiness on the main thread; update the submission around
Concurrency.computeIfAbsentRW(rwlock, map, "shared_key", computer) and ensure
doneLatch.countDown() still runs in finally but failures are surfaced by
checking Futures or the exception collection.

Comment on lines +456 to +476
public ModelAndView showProduct(HttpSession session) {
final ContextConfig contextConfig = ContextConfig.create()
.setUnit("session_id", session.getId());

final Context context = absmartly.createContext(contextConfig)
.waitUntilReady();

final int treatment = context.getTreatment("exp_product_layout");

context.close();

ModelAndView mav = new ModelAndView();
if (treatment == 0) {
mav.setViewName("product_control");
} else {
mav.setViewName("product_treatment");
}

return mav;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Context is not closed if getTreatment throws an exception.

In the Spring Boot example, context.close() (line 465) is called after getTreatment() (line 463), but if getTreatment() throws, the context will leak. Use try-with-resources since Context implements Closeable:

📝 Suggested improvement
-        final Context context = absmartly.createContext(contextConfig)
-            .waitUntilReady();
-
-        final int treatment = context.getTreatment("exp_product_layout");
-
-        context.close();
-
-        ModelAndView mav = new ModelAndView();
+        ModelAndView mav = new ModelAndView();
+        try (Context context = absmartly.createContext(contextConfig)
+                .waitUntilReady()) {
+            final int treatment = context.getTreatment("exp_product_layout");
+            if (treatment == 0) {
+                mav.setViewName("product_control");
+            } else {
+                mav.setViewName("product_treatment");
+            }
+        }

The same pattern applies to the Jakarta EE example (lines 532–537).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 456 - 476, The Context is not closed if
Context.getTreatment throws in showProduct; change the code that creates Context
(via ContextConfig.create/setUnit and
absmartly.createContext(...).waitUntilReady()) to use a try-with-resources block
around the Context object (since Context implements Closeable) so that
context.close() is guaranteed even on exceptions; update the showProduct method
accordingly and apply the same try-with-resources pattern to the Jakarta EE
example where Context is created and getTreatment is called.

…ctory

Allows creating a fully configured SDK instance in a single call
instead of manually creating ClientConfig + ABsmartlyConfig + Client.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
core-api/src/main/java/com/absmartly/sdk/ABsmartly.java (1)

88-104: ⚠️ Potential issue | 🟠 Major

Ensure scheduler cleanup still happens when client close fails.

If client_.close() throws IOException on line 89, the scheduler shutdown block is skipped entirely, leaving executor threads running. Wrap client close in a try/catch with scheduler shutdown in a finally block, and rethrow the IOException afterwards.

Proposed fix
 `@Override`
 public void close() throws IOException {
-	if (client_ != null) {
-		client_.close();
-		client_ = null;
-	}
-
-	if (scheduler_ != null) {
-		scheduler_.shutdown();
-		try {
-			if (!scheduler_.awaitTermination(5000, TimeUnit.MILLISECONDS)) {
-				scheduler_.shutdownNow();
-			}
-		} catch (InterruptedException e) {
-			Thread.currentThread().interrupt();
-			scheduler_.shutdownNow();
-		}
-		scheduler_ = null;
-	}
+	IOException closeError = null;
+	try {
+		if (client_ != null) {
+			client_.close();
+		}
+	} catch (IOException e) {
+		closeError = e;
+	} finally {
+		client_ = null;
+		if (scheduler_ != null) {
+			scheduler_.shutdown();
+			try {
+				if (!scheduler_.awaitTermination(5000, TimeUnit.MILLISECONDS)) {
+					scheduler_.shutdownNow();
+				}
+			} catch (InterruptedException e) {
+				Thread.currentThread().interrupt();
+				scheduler_.shutdownNow();
+			}
+			scheduler_ = null;
+		}
+	}
+	if (closeError != null) {
+		throw closeError;
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/main/java/com/absmartly/sdk/ABsmartly.java` around lines 88 -
104, The client shutdown can throw IOException and currently prevents scheduler
cleanup; modify the block around client_.close() so client_.close() is invoked
in a try block, catch the IOException to capture it, run the scheduler
shutdown/awaitTermination/shutdownNow logic in a finally block (using
scheduler_.shutdown(), awaitTermination(...), scheduler_.shutdownNow(),
Thread.currentThread().interrupt() on InterruptedException as currently
implemented), and after the finally rethrow the original IOException (or rethrow
if uncaught) so scheduler_ cleanup always runs; reference client_.close(),
scheduler_.shutdown(), scheduler_.awaitTermination(...), and
scheduler_.shutdownNow().
♻️ Duplicate comments (2)
README.md (2)

437-457: ⚠️ Potential issue | 🟠 Major

Context resource leak if getTreatment throws an exception.

In the Spring Boot example, context.close() is called after getTreatment(), but if getTreatment() throws an exception, the context will not be closed, causing a resource leak. Since Context implements Closeable, use try-with-resources to guarantee cleanup.

📝 Suggested fix
     `@GetMapping`("/product")
     public ModelAndView showProduct(HttpSession session) {
-        final ContextConfig contextConfig = ContextConfig.create()
-            .setUnit("session_id", session.getId());
-
-        final Context context = absmartly.createContext(contextConfig)
-            .waitUntilReady();
-
-        final int treatment = context.getTreatment("exp_product_layout");
-
-        context.close();
-
         ModelAndView mav = new ModelAndView();
-        if (treatment == 0) {
-            mav.setViewName("product_control");
-        } else {
-            mav.setViewName("product_treatment");
+        final ContextConfig contextConfig = ContextConfig.create()
+            .setUnit("session_id", session.getId());
+
+        try (Context context = absmartly.createContext(contextConfig)
+                .waitUntilReady()) {
+            final int treatment = context.getTreatment("exp_product_layout");
+            if (treatment == 0) {
+                mav.setViewName("product_control");
+            } else {
+                mav.setViewName("product_treatment");
+            }
         }
 
         return mav;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 437 - 457, The Context is not guaranteed to be closed
if context.getTreatment(...) throws; update showProduct to use
try-with-resources: create the Context via
absmartly.createContext(ContextConfig.create().setUnit("session_id",
session.getId())).waitUntilReady() and wrap that Context in a try(...) block so
getTreatment("exp_product_layout") executes inside the try and the Context is
automatically closed; remove the explicit context.close() call and keep the rest
of the logic that sets the view based on the treatment.

507-523: ⚠️ Potential issue | 🟠 Major

Context resource leak if getTreatment throws an exception.

The Jakarta EE example has the same resource management issue as the Spring Boot example. If getTreatment() or any operation before context.close() throws an exception, the context will not be closed. Use try-with-resources to ensure proper cleanup.

📝 Suggested fix
     `@GET`
     public Response getProduct() {
         final String sessionId = request.getSession().getId();
 
         final ContextConfig contextConfig = ContextConfig.create()
             .setUnit("session_id", sessionId);
 
-        final Context context = absmartly.createContext(contextConfig)
-            .waitUntilReady();
-
-        final int treatment = context.getTreatment("exp_product_layout");
-
-        context.close();
-
-        return Response.ok()
-            .entity(Map.of("treatment", treatment))
-            .build();
+        try (Context context = absmartly.createContext(contextConfig)
+                .waitUntilReady()) {
+            final int treatment = context.getTreatment("exp_product_layout");
+            return Response.ok()
+                .entity(Map.of("treatment", treatment))
+                .build();
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 507 - 523, The getProduct method can leak the Context
if absmartly.createContext(...).waitUntilReady() or context.getTreatment(...)
throws before context.close() is called; wrap the context lifecycle in a
try-with-resources (or a try/finally that always calls context.close()) so the
Context is closed even on exceptions. Locate the Context creation and usage in
getProduct (the Context context = absmartly.createContext(...).waitUntilReady();
and subsequent context.getTreatment("exp_product_layout") and context.close())
and move them inside a try-with-resources block (or add a finally that calls
context.close()) to guarantee cleanup.
🧹 Nitpick comments (2)
README.md (1)

189-189: Remove redundant "first" from "first started".

The phrase "first started" is redundant. Consider simplifying to "started" for conciseness.

📝 Suggested improvement
-For long-running contexts, the context is usually created once when the application is first started.
+For long-running contexts, the context is usually created once when the application is started.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 189, Edit the sentence "For long-running contexts, the
context is usually created once when the application is first started." and
remove the redundant word "first" so it reads "For long-running contexts, the
context is usually created once when the application is started." This is the
only change required—replace "first started" with "started" in the README
sentence.
core-api/src/test/java/com/absmartly/sdk/ABsmartlyTest.java (1)

109-117: Remove duplicate static verifications for the same Context.create() invocation.

You verify Context.create(...) twice at each location: first with any() matchers, then again with captors. The captor verification already validates the invocation count and argument values; the first call is redundant. This pattern appears at lines 109–111, 171–173, and 264–266.

Proposed simplification (apply to all three test methods)
-				contextStatic.verify(
-						() -> Context.create(any(), any(), any(), any(), any(), any(), any(), any(), any()),
-						Mockito.times(1));
 				contextStatic.verify(
 						() -> Context.create(clockCaptor.capture(), configCaptor.capture(), schedulerCaptor.capture(),
 								dataFutureCaptor.capture(), dataProviderCaptor.capture(), eventHandlerCaptor.capture(),
 								eventLoggerCaptor.capture(), variableParserCaptor.capture(),
 								audienceMatcherCaptor.capture()),
 						Mockito.times(1));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/test/java/com/absmartly/sdk/ABsmartlyTest.java` around lines 109
- 117, Remove the redundant static verifications of Context.create() that use
any() matchers: you should keep only the contextStatic.verify(...) call that
uses the captors (clockCaptor, configCaptor, schedulerCaptor, dataFutureCaptor,
dataProviderCaptor, eventHandlerCaptor, eventLoggerCaptor, variableParserCaptor,
audienceMatcherCaptor) since it already asserts invocation count and captures
arguments; delete the duplicate contextStatic.verify(...) calls that pass any()
for the same Context.create(...) invocation (they appear alongside the
captor-based verifies in the same test methods) so each Context.create(...) is
verified only once with the captors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Around line 656-695: The example uses a non-existent ABsmartly.builder() API;
replace the builder invocation that constructs the SDK (the lines that call
ABsmartly.builder().endpoint(...).apiKey(...).application(...).environment(...).build())
with a direct call to the provided factory method ABsmartly.create(endpoint,
apiKey, application, environment) passing the same endpoint, API key,
application and environment strings so the example uses the correct
ABsmartly.create(...) factory.

---

Outside diff comments:
In `@core-api/src/main/java/com/absmartly/sdk/ABsmartly.java`:
- Around line 88-104: The client shutdown can throw IOException and currently
prevents scheduler cleanup; modify the block around client_.close() so
client_.close() is invoked in a try block, catch the IOException to capture it,
run the scheduler shutdown/awaitTermination/shutdownNow logic in a finally block
(using scheduler_.shutdown(), awaitTermination(...), scheduler_.shutdownNow(),
Thread.currentThread().interrupt() on InterruptedException as currently
implemented), and after the finally rethrow the original IOException (or rethrow
if uncaught) so scheduler_ cleanup always runs; reference client_.close(),
scheduler_.shutdown(), scheduler_.awaitTermination(...), and
scheduler_.shutdownNow().

---

Duplicate comments:
In `@README.md`:
- Around line 437-457: The Context is not guaranteed to be closed if
context.getTreatment(...) throws; update showProduct to use try-with-resources:
create the Context via
absmartly.createContext(ContextConfig.create().setUnit("session_id",
session.getId())).waitUntilReady() and wrap that Context in a try(...) block so
getTreatment("exp_product_layout") executes inside the try and the Context is
automatically closed; remove the explicit context.close() call and keep the rest
of the logic that sets the view based on the treatment.
- Around line 507-523: The getProduct method can leak the Context if
absmartly.createContext(...).waitUntilReady() or context.getTreatment(...)
throws before context.close() is called; wrap the context lifecycle in a
try-with-resources (or a try/finally that always calls context.close()) so the
Context is closed even on exceptions. Locate the Context creation and usage in
getProduct (the Context context = absmartly.createContext(...).waitUntilReady();
and subsequent context.getTreatment("exp_product_layout") and context.close())
and move them inside a try-with-resources block (or add a finally that calls
context.close()) to guarantee cleanup.

---

Nitpick comments:
In `@core-api/src/test/java/com/absmartly/sdk/ABsmartlyTest.java`:
- Around line 109-117: Remove the redundant static verifications of
Context.create() that use any() matchers: you should keep only the
contextStatic.verify(...) call that uses the captors (clockCaptor, configCaptor,
schedulerCaptor, dataFutureCaptor, dataProviderCaptor, eventHandlerCaptor,
eventLoggerCaptor, variableParserCaptor, audienceMatcherCaptor) since it already
asserts invocation count and captures arguments; delete the duplicate
contextStatic.verify(...) calls that pass any() for the same Context.create(...)
invocation (they appear alongside the captor-based verifies in the same test
methods) so each Context.create(...) is verified only once with the captors.

In `@README.md`:
- Line 189: Edit the sentence "For long-running contexts, the context is usually
created once when the application is first started." and remove the redundant
word "first" so it reads "For long-running contexts, the context is usually
created once when the application is started." This is the only change
required—replace "first started" with "started" in the README sentence.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a7a3604 and 76e0293.

📒 Files selected for processing (3)
  • README.md
  • core-api/src/main/java/com/absmartly/sdk/ABsmartly.java
  • core-api/src/test/java/com/absmartly/sdk/ABsmartlyTest.java

Comment on lines +656 to +695
public class CancellableContextExample {

public static void main(String[] args) throws Exception {
final ABsmartly sdk = ABsmartly.builder()
.endpoint("https://your-company.absmartly.io/v1")
.apiKey("YOUR-API-KEY")
.application("website")
.environment("development")
.build();

final ContextConfig contextConfig = ContextConfig.create()
.setUnit("session_id", "abc123");

final Context context = sdk.createContext(contextConfig);

// Create future for context initialization
final CompletableFuture<Context> future = context.waitUntilReadyAsync();

// Cancel after 1.5 seconds if not ready
final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.schedule(() -> {
if (!future.isDone()) {
future.cancel(true);
System.out.println("Context creation cancelled");
}
}, 1500, TimeUnit.MILLISECONDS);

try {
final Context readyContext = future.get();
System.out.println("Context ready!");
readyContext.close();
} catch (CancellationException e) {
System.out.println("Context creation was cancelled");
} catch (ExecutionException e) {
System.out.println("Context creation failed: " + e.getCause());
} finally {
scheduler.shutdown();
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find ABsmartly.java file
fd -t f 'ABsmartly\.java' -x echo {}

Repository: absmartly/java-sdk

Length of output: 119


🏁 Script executed:

# Search for builder method in Java files
rg -n 'builder\s*\(' --type=java -C 3

Repository: absmartly/java-sdk

Length of output: 44


🏁 Script executed:

# Read the ABsmartly.java file to check actual API
cat ./core-api/src/main/java/com/absmartly/sdk/ABsmartly.java

Repository: absmartly/java-sdk

Length of output: 3652


Update the example to use the correct ABsmartly.create() factory method instead of the non-existent builder() API.

The ABsmartly class does not have a builder() method. The actual public factory methods are:

Replace the builder pattern (lines 659–664) with a direct call to the convenience factory: ABsmartly.create(endpoint, apiKey, application, environment).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 656 - 695, The example uses a non-existent
ABsmartly.builder() API; replace the builder invocation that constructs the SDK
(the lines that call
ABsmartly.builder().endpoint(...).apiKey(...).application(...).environment(...).build())
with a direct call to the provided factory method ABsmartly.create(endpoint,
apiKey, application, environment) passing the same endpoint, API key,
application and environment strings so the example uses the correct
ABsmartly.create(...) factory.

The positional create(endpoint, apiKey, app, env) required users to
know parameter order. Builder pattern provides named parameters:
ABsmartly.builder().endpoint(...).apiKey(...).application(...).environment(...).build()
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
README.md (2)

442-447: ⚠️ Potential issue | 🟡 Minor

Context is not closed if getTreatment throws an exception.

In the Spring Boot example, context.close() (line 447) is called after getTreatment() (line 445), but if getTreatment() throws, the context will leak. Use try-with-resources since Context implements Closeable.

🛡️ Proposed fix using try-with-resources
-        final Context context = absmartly.createContext(contextConfig)
-            .waitUntilReady();
-
-        final int treatment = context.getTreatment("exp_product_layout");
-
-        context.close();
-
-        ModelAndView mav = new ModelAndView();
+        ModelAndView mav = new ModelAndView();
+        try (Context context = absmartly.createContext(contextConfig)
+                .waitUntilReady()) {
+            final int treatment = context.getTreatment("exp_product_layout");
+            if (treatment == 0) {
+                mav.setViewName("product_control");
+            } else {
+                mav.setViewName("product_treatment");
+            }
+        }
+        return mav;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 442 - 447, The Context may leak if
context.getTreatment("exp_product_layout") throws; change the creation and
closing to use try-with-resources around the Context returned by
absmartly.createContext(contextConfig).waitUntilReady() so it is always closed;
locate the block that calls absmartly.createContext(...).waitUntilReady(), the
subsequent call to context.getTreatment(...) and the trailing context.close()
and replace with a try-with-resources that declares Context context =
absmartly.createContext(contextConfig).waitUntilReady() so close happens
automatically.

514-519: ⚠️ Potential issue | 🟡 Minor

Same context leak issue applies to the Jakarta EE example.

The context.close() call on line 519 won't execute if getTreatment() on line 517 throws. Apply the same try-with-resources pattern here.

🛡️ Proposed fix using try-with-resources
-        final Context context = absmartly.createContext(contextConfig)
-            .waitUntilReady();
-
-        final int treatment = context.getTreatment("exp_product_layout");
-
-        context.close();
-
-        return Response.ok()
-            .entity(Map.of("treatment", treatment))
-            .build();
+        try (Context context = absmartly.createContext(contextConfig)
+                .waitUntilReady()) {
+            final int treatment = context.getTreatment("exp_product_layout");
+            return Response.ok()
+                .entity(Map.of("treatment", treatment))
+                .build();
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 514 - 519, The Jakarta EE example leaks Context
because context.close() is never reached if
context.getTreatment("exp_product_layout") throws; change the pattern around
absmartly.createContext(contextConfig).waitUntilReady() so Context is acquired
in a try-with-resources (or try (Context context = ...) { ... }) block and call
getTreatment inside that block, ensuring the Context is always closed; update
references to Context, absmartly.createContext, getTreatment, and context.close
accordingly.
🧹 Nitpick comments (4)
README.md (1)

190-190: Minor style improvement: "first started" is redundant.

Consider simplifying to just "started" for cleaner prose.

📝 Suggested change
-For long-running contexts, the context is usually created once when the application is first started.
+For long-running contexts, the context is usually created once when the application is started.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 190, The sentence "For long-running contexts, the context
is usually created once when the application is first started." is wordy —
replace "first started" with "started" so it reads: "For long-running contexts,
the context is usually created once when the application is started." Update
that exact line in README.md to simplify the phrasing.
core-api/src/main/java/com/absmartly/sdk/ABsmartly.java (2)

58-62: Consider validating for empty strings in addition to null.

The Builder validates that required fields are not null, but the Client constructor (as seen in Client.java lines 28-56) validates that these same fields are neither null nor empty. This inconsistency means users could pass empty strings through the Builder, only to get a different IllegalArgumentException from the Client with a less helpful message.

♻️ Proposed fix to add empty string validation
 public ABsmartly build() {
-    if (endpoint == null) throw new IllegalArgumentException("endpoint is required");
-    if (apiKey == null) throw new IllegalArgumentException("apiKey is required");
-    if (application == null) throw new IllegalArgumentException("application is required");
-    if (environment == null) throw new IllegalArgumentException("environment is required");
+    if (endpoint == null || endpoint.isEmpty()) throw new IllegalArgumentException("endpoint is required");
+    if (apiKey == null || apiKey.isEmpty()) throw new IllegalArgumentException("apiKey is required");
+    if (application == null || application.isEmpty()) throw new IllegalArgumentException("application is required");
+    if (environment == null || environment.isEmpty()) throw new IllegalArgumentException("environment is required");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/main/java/com/absmartly/sdk/ABsmartly.java` around lines 58 -
62, In ABsmartly.build(), extend the null checks to also reject empty or
all-whitespace strings for endpoint, apiKey, application, and environment (e.g.,
using isBlank()/trim().isEmpty()) and throw IllegalArgumentException with the
same messages currently used ("endpoint is required", "apiKey is required",
"application is required", "environment is required"); update the build() method
in class ABsmartly (the build() function) to validate each of those four fields
accordingly before constructing the Client.

135-138: Minor: Consider nullifying client_ only after successful close.

If client_.close() throws an IOException, the reference is still nullified, which could mask issues on retry attempts. However, since close() is typically called once during teardown, this is a minor concern.

♻️ Proposed safer nullification
 if (client_ != null) {
-    client_.close();
-    client_ = null;
+    try {
+        client_.close();
+    } finally {
+        client_ = null;
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/main/java/com/absmartly/sdk/ABsmartly.java` around lines 135 -
138, In ABsmartly.java, avoid nullifying client_ before confirming close()
succeeded: wrap the client_.close() call in a try-catch and only set client_ =
null after close() completes successfully (e.g., try { client_.close(); client_
= null; } catch (IOException e) { /* log/handle and keep client_ for potential
retry */ }), referencing the client_ field and its close() method so exceptions
don’t cause the reference to be lost.
core-api/src/test/java/com/absmartly/sdk/ABsmartlyTest.java (1)

333-356: Consider adding tests for scheduler shutdown edge cases.

The close() test verifies that awaitTermination is called, but doesn't cover the fallback paths in the implementation:

  1. When awaitTermination returns false (timeout)
  2. When awaitTermination throws InterruptedException

These paths invoke shutdownNow() and should ideally be tested.

🧪 Example test for shutdown timeout fallback
`@Test`
void closeCallsShutdownNowOnTimeout() throws IOException, InterruptedException {
    final ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
    when(scheduler.awaitTermination(anyLong(), any())).thenReturn(false);

    final ABsmartlyConfig config = ABsmartlyConfig.create()
            .setClient(client)
            .setScheduler(scheduler);

    final ABsmartly absmartly = ABsmartly.create(config);
    absmartly.close();

    verify(scheduler, times(1)).shutdown();
    verify(scheduler, times(1)).shutdownNow();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-api/src/test/java/com/absmartly/sdk/ABsmartlyTest.java` around lines 333
- 356, Add unit tests in ABsmartlyTest to cover the scheduler shutdown fallback
paths: create a mock ScheduledExecutorService, inject it via ABsmartlyConfig
into ABsmartly.create(), then call absmartly.close() while stubbing
scheduler.awaitTermination(...) to (a) return false and (b) throw
InterruptedException; for the timeout case verify scheduler.shutdown() and
scheduler.shutdownNow() are each called, and for the InterruptedException case
verify shutdown() and shutdownNow() are called and that the thread interruption
is handled/restored as appropriate. Use the existing test pattern (mocking
Context.create with MockedStatic<Context>) and reference the same symbols:
ABsmartly.create(config), absmartly.close(), and the mock scheduler in your new
tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@README.md`:
- Around line 442-447: The Context may leak if
context.getTreatment("exp_product_layout") throws; change the creation and
closing to use try-with-resources around the Context returned by
absmartly.createContext(contextConfig).waitUntilReady() so it is always closed;
locate the block that calls absmartly.createContext(...).waitUntilReady(), the
subsequent call to context.getTreatment(...) and the trailing context.close()
and replace with a try-with-resources that declares Context context =
absmartly.createContext(contextConfig).waitUntilReady() so close happens
automatically.
- Around line 514-519: The Jakarta EE example leaks Context because
context.close() is never reached if context.getTreatment("exp_product_layout")
throws; change the pattern around
absmartly.createContext(contextConfig).waitUntilReady() so Context is acquired
in a try-with-resources (or try (Context context = ...) { ... }) block and call
getTreatment inside that block, ensuring the Context is always closed; update
references to Context, absmartly.createContext, getTreatment, and context.close
accordingly.

---

Nitpick comments:
In `@core-api/src/main/java/com/absmartly/sdk/ABsmartly.java`:
- Around line 58-62: In ABsmartly.build(), extend the null checks to also reject
empty or all-whitespace strings for endpoint, apiKey, application, and
environment (e.g., using isBlank()/trim().isEmpty()) and throw
IllegalArgumentException with the same messages currently used ("endpoint is
required", "apiKey is required", "application is required", "environment is
required"); update the build() method in class ABsmartly (the build() function)
to validate each of those four fields accordingly before constructing the
Client.
- Around line 135-138: In ABsmartly.java, avoid nullifying client_ before
confirming close() succeeded: wrap the client_.close() call in a try-catch and
only set client_ = null after close() completes successfully (e.g., try {
client_.close(); client_ = null; } catch (IOException e) { /* log/handle and
keep client_ for potential retry */ }), referencing the client_ field and its
close() method so exceptions don’t cause the reference to be lost.

In `@core-api/src/test/java/com/absmartly/sdk/ABsmartlyTest.java`:
- Around line 333-356: Add unit tests in ABsmartlyTest to cover the scheduler
shutdown fallback paths: create a mock ScheduledExecutorService, inject it via
ABsmartlyConfig into ABsmartly.create(), then call absmartly.close() while
stubbing scheduler.awaitTermination(...) to (a) return false and (b) throw
InterruptedException; for the timeout case verify scheduler.shutdown() and
scheduler.shutdownNow() are each called, and for the InterruptedException case
verify shutdown() and shutdownNow() are called and that the thread interruption
is handled/restored as appropriate. Use the existing test pattern (mocking
Context.create with MockedStatic<Context>) and reference the same symbols:
ABsmartly.create(config), absmartly.close(), and the mock scheduler in your new
tests.

In `@README.md`:
- Line 190: The sentence "For long-running contexts, the context is usually
created once when the application is first started." is wordy — replace "first
started" with "started" so it reads: "For long-running contexts, the context is
usually created once when the application is started." Update that exact line in
README.md to simplify the phrasing.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 76e0293 and e4b79c0.

📒 Files selected for processing (3)
  • README.md
  • core-api/src/main/java/com/absmartly/sdk/ABsmartly.java
  • core-api/src/test/java/com/absmartly/sdk/ABsmartlyTest.java

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant