From c1dcdd33ab2303ce3280406f6448349367857bac Mon Sep 17 00:00:00 2001 From: Alan Gomes <1418294+alangalvino@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:58:22 -0300 Subject: [PATCH 1/2] Add X-Incognia-Latency header to propagate last request latency Tracks the latency of each outgoing API call in TokenAwareNetworkingClient and sends it as the X-Incognia-Latency header on the next request, enabling server-side latency-based monitoring (LBMT). The header is absent on the first call and populated from the second call onward. Covers POST requests with and without query parameters. --- .../clients/TokenAwareNetworkingClient.java | 50 +++++----- .../com/incognia/api/IncogniaAPITest.java | 95 +++++++++++++++++++ 2 files changed, 121 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/incognia/api/clients/TokenAwareNetworkingClient.java b/src/main/java/com/incognia/api/clients/TokenAwareNetworkingClient.java index 28f68b38..d8f915e1 100644 --- a/src/main/java/com/incognia/api/clients/TokenAwareNetworkingClient.java +++ b/src/main/java/com/incognia/api/clients/TokenAwareNetworkingClient.java @@ -3,11 +3,14 @@ import com.incognia.common.exceptions.IncogniaException; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import okhttp3.OkHttpClient; public class TokenAwareNetworkingClient { private static final String USER_AGENT_HEADER = "User-Agent"; private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String LATENCY_HEADER = "X-Incognia-Latency"; private static final String USER_AGENT_HEADER_CONTENT = String.format( "incognia-api-java/%s (%s %s %s) Java/%s", @@ -19,6 +22,7 @@ public class TokenAwareNetworkingClient { private final NetworkingClient networkingClient; private final TokenProvider tokenProvider; + private final AtomicReference lastLatency = new AtomicReference<>(); public TokenAwareNetworkingClient( OkHttpClient httpClient, String baseUrl, String clientId, String clientSecret) { @@ -26,42 +30,40 @@ public TokenAwareNetworkingClient( this.tokenProvider = new TokenProvider(clientId, clientSecret, networkingClient); } + private Map buildHeaders() throws IncogniaException { + Map headers = new HashMap<>(); + headers.put(USER_AGENT_HEADER, USER_AGENT_HEADER_CONTENT); + headers.put(AUTHORIZATION_HEADER, tokenProvider.buildAuthorizationHeader()); + Long latency = lastLatency.get(); + if (latency != null) { + headers.put(LATENCY_HEADER, Long.toString(latency)); + } + return headers; + } + public U doPost( String path, T body, Class responseType, Map queryParameters) throws IncogniaException { tokenProvider.getToken(); - Map headers = - new HashMap() { - { - put(USER_AGENT_HEADER, USER_AGENT_HEADER_CONTENT); - put(AUTHORIZATION_HEADER, tokenProvider.buildAuthorizationHeader()); - } - }; - return networkingClient.doPost(path, body, responseType, headers, queryParameters); + long start = System.nanoTime(); + U result = networkingClient.doPost(path, body, responseType, buildHeaders(), queryParameters); + lastLatency.set(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); + return result; } public U doPost(String path, T body, Class responseType) throws IncogniaException { tokenProvider.getToken(); - Map headers = - new HashMap() { - { - put(USER_AGENT_HEADER, USER_AGENT_HEADER_CONTENT); - put(AUTHORIZATION_HEADER, tokenProvider.buildAuthorizationHeader()); - } - }; - return networkingClient.doPost(path, body, responseType, headers); + long start = System.nanoTime(); + U result = networkingClient.doPost(path, body, responseType, buildHeaders()); + lastLatency.set(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); + return result; } public void doPost(String path, T body, Map queryParameters) throws IncogniaException { tokenProvider.getToken(); - Map headers = - new HashMap() { - { - put(USER_AGENT_HEADER, USER_AGENT_HEADER_CONTENT); - put(AUTHORIZATION_HEADER, tokenProvider.buildAuthorizationHeader()); - } - }; - networkingClient.doPost(path, body, headers, queryParameters); + long start = System.nanoTime(); + networkingClient.doPost(path, body, buildHeaders(), queryParameters); + lastLatency.set(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); } } diff --git a/src/test/java/com/incognia/api/IncogniaAPITest.java b/src/test/java/com/incognia/api/IncogniaAPITest.java index 2163bb16..76e62da3 100644 --- a/src/test/java/com/incognia/api/IncogniaAPITest.java +++ b/src/test/java/com/incognia/api/IncogniaAPITest.java @@ -61,7 +61,9 @@ import lombok.SneakyThrows; import okhttp3.ConnectionPool; import okhttp3.OkHttpClient; +import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -1106,6 +1108,99 @@ void testRegisterLogin_whenAccountIdIsNotValid() { .hasMessage("'account id' cannot be empty"); } + private static final String TOKEN_RESPONSE = + "{\"access_token\": \"test-token\", \"expires_in\": 300, \"token_type\": \"Bearer\"}"; + private static final String SIGNUP_RESPONSE = + "{\"id\": \"5e76a7ca-577c-4f47-a752-9e1e0cee9e49\"," + + "\"request_id\": \"8afc84a7-f1d4-488d-bd69-36d9a37168b7\"," + + "\"risk_assessment\": \"low_risk\"}"; + private static final String TRANSACTION_RESPONSE = + "{\"id\": \"dfe1f2ff-8f0d-4ce8-aed1-af8435143044\"," + "\"risk_assessment\": \"low_risk\"}"; + + @Test + @SneakyThrows + void testLbmt_absentOnFirstCall() { + mockServer.enqueue(new MockResponse().setResponseCode(200).setBody(TOKEN_RESPONSE)); + mockServer.enqueue(new MockResponse().setResponseCode(200).setBody(SIGNUP_RESPONSE)); + + client.registerSignup(RegisterSignupRequest.builder().installationId("test").build()); + + mockServer.takeRequest(); // token request + RecordedRequest signupRequest = mockServer.takeRequest(); + assertThat(signupRequest.getHeader("X-Incognia-Latency")).isNull(); + } + + @Test + @SneakyThrows + void testLbmt_sentOnSecondSignupCall() { + mockServer.enqueue(new MockResponse().setResponseCode(200).setBody(TOKEN_RESPONSE)); + mockServer.enqueue(new MockResponse().setResponseCode(200).setBody(SIGNUP_RESPONSE)); + mockServer.enqueue(new MockResponse().setResponseCode(200).setBody(SIGNUP_RESPONSE)); + + RegisterSignupRequest request = RegisterSignupRequest.builder().installationId("test").build(); + client.registerSignup(request); + client.registerSignup(request); + + mockServer.takeRequest(); // token request + RecordedRequest firstRequest = mockServer.takeRequest(); + RecordedRequest secondRequest = mockServer.takeRequest(); + + assertThat(firstRequest.getHeader("X-Incognia-Latency")).isNull(); + String latencyHeader = secondRequest.getHeader("X-Incognia-Latency"); + assertThat(latencyHeader).isNotNull(); + assertThat(Long.parseLong(latencyHeader)).isGreaterThanOrEqualTo(0L); + } + + @Test + @SneakyThrows + void testLbmt_sentOnFeedbackAfterTransaction() { + mockServer.enqueue(new MockResponse().setResponseCode(200).setBody(TOKEN_RESPONSE)); + mockServer.enqueue(new MockResponse().setResponseCode(200).setBody(TRANSACTION_RESPONSE)); + mockServer.enqueue(new MockResponse().setResponseCode(200)); + + client.registerPayment( + RegisterPaymentRequest.builder() + .accountId("account-id") + .addresses(Collections.emptyMap()) + .build()); + client.registerFeedback( + FeedbackEvent.ACCOUNT_TAKEOVER, + Instant.now(), + FeedbackIdentifiers.builder().accountId("account-id").build()); + + mockServer.takeRequest(); // token request + RecordedRequest transactionRequest = mockServer.takeRequest(); + RecordedRequest feedbackRequest = mockServer.takeRequest(); + + assertThat(transactionRequest.getHeader("X-Incognia-Latency")).isNull(); + String latencyHeader = feedbackRequest.getHeader("X-Incognia-Latency"); + assertThat(latencyHeader).isNotNull(); + assertThat(Long.parseLong(latencyHeader)).isGreaterThanOrEqualTo(0L); + } + + @Test + @SneakyThrows + void testLbmt_sentOnSignupAfterFeedback() { + mockServer.enqueue(new MockResponse().setResponseCode(200).setBody(TOKEN_RESPONSE)); + mockServer.enqueue(new MockResponse().setResponseCode(200)); + mockServer.enqueue(new MockResponse().setResponseCode(200).setBody(SIGNUP_RESPONSE)); + + client.registerFeedback( + FeedbackEvent.ACCOUNT_TAKEOVER, + Instant.now(), + FeedbackIdentifiers.builder().accountId("account-id").build()); + client.registerSignup(RegisterSignupRequest.builder().installationId("test").build()); + + mockServer.takeRequest(); // token request + RecordedRequest feedbackRequest = mockServer.takeRequest(); + RecordedRequest signupRequest = mockServer.takeRequest(); + + assertThat(feedbackRequest.getHeader("X-Incognia-Latency")).isNull(); + String latencyHeader = signupRequest.getHeader("X-Incognia-Latency"); + assertThat(latencyHeader).isNotNull(); + assertThat(Long.parseLong(latencyHeader)).isGreaterThanOrEqualTo(0L); + } + private void assertTransactionAssessment(TransactionAssessment transactionAssessment) { assertThat(transactionAssessment) .extracting("id", "riskAssessment", "deviceId") From 7e1b4ed225fc81fe83fbbc0ec98a795e5bc9b709 Mon Sep 17 00:00:00 2001 From: Thiago Cardoso Date: Thu, 16 Apr 2026 10:22:24 -0300 Subject: [PATCH 2/2] Bump version to 3.16.0 --- README.md | 8 ++++---- build.gradle | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f8e1922d..b87f68cb 100644 --- a/README.md +++ b/README.md @@ -23,14 +23,14 @@ And then add the artifact `incognia-api-client` **or** `incognia-api-client-shad com.incognia incognia-api-client - 3.15.1 + 3.16.0 ``` ```xml com.incognia incognia-api-client-shaded - 3.15.1 + 3.16.0 ``` @@ -47,13 +47,13 @@ repositories { And then add the dependency ```gradle dependencies { - implementation 'com.incognia:incognia-api-client:3.15.1' + implementation 'com.incognia:incognia-api-client:3.16.0' } ``` OR ```gradle dependencies { - implementation 'com.incognia:incognia-api-client-shaded:3.15.1' + implementation 'com.incognia:incognia-api-client-shaded:3.16.0' } ``` diff --git a/build.gradle b/build.gradle index 2108fd2f..bced639a 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ plugins { } group = "com.incognia" -version = "3.15.1" +version = "3.16.0" task createProjectVersionFile { def projectVersionDir = "$projectDir/src/main/java/com/incognia/api"