From c3ed1be2fce6f3ccb22f87c69308803cd709710c Mon Sep 17 00:00:00 2001 From: Rich Herrera Date: Tue, 2 Jul 2024 12:02:59 -0600 Subject: [PATCH] [QL] Measure API Request Latency (#1032) * Add Analytics event for network latency * Add BTHTTPResponse to share start and end time * Return new class instead of String on synchronous call * Rename BTHTTPResponse to BTHttpResponse * Add new Response interface and NoResponse callback to pass start and end time * Change return type with new class name * Change response callback HttpResponseCallback with BTHttpResponseCallback to catch BTResponse object * Add sendAnalitycsEvent call on onResult call back * Update analytics event class and database * Add endpoint, start and end time keys to use on AnalyticsClient * Move event timestamp property, add parameters on sendAnalytics method * Send Network latency Analytics events after perform a request * Linting * Remove prints * Fix lints * Fix lints * Fix lint * Migrate previous class created on java to kt * Add Timming interface * Pass interface to ConfigurationLoader fromm BraintreeClient to send event * Fix AnalyticsClient UTs * Fix ConfigurationLoader UTs * Update Configuration loader path sent on Analytics * Update VisaCheckoutClient with new parameters * Fix VisaCheckoutClientTests * Fix Analytics Event parameter setup * Fix PayPalNativeCheckoutClient sendEvent params * Fix lints * Rename BTAPITiming interface * Update BTHttpResponseCallback interface * Fix UTs * Fix UTs * Fix lint * Add default AnalyticsEventParams values for kotlin instantiation * Fix lints * Fix lint identation * Use lambda on callbacks * Add test * Fix name event * Make nullable long analytics parameters * Rename BTHTTPResponse to HttpTimingResponse * Rename interface BTAPITiming to APITiming * Made long parameter nullable * Rename BTHTTPResponseCallback to HttpTimingResponse * Fix tests * Rename HttpTimingResponse with HttpResponse * Stripping out the merchants * Get mutation name * Update docstrings * Introduce HttpResponseTiming to project. * Delete unnecessary classes * Make optional callback * Fix UTs * Delete unnecessary interface * Fix lints * Rename interface HttpTimingResponseCallback to NetworkResponseCallback * Refactor sendGraphQLPostRequest in BraintreeClient to take a JSONObject instead of an optional. * Fix tests * Remove unnecessary method * Address PR comment --------- Co-authored-by: sshropshire --- .../6.json | 92 ++++++++++++++++++ .../braintreepayments/api/AnalyticsClient.kt | 33 ++++++- .../api/AnalyticsDatabase.kt | 5 +- .../braintreepayments/api/AnalyticsEvent.kt | 15 ++- .../api/AnalyticsEventParams.kt | 11 ++- .../com/braintreepayments/api/ApiClient.kt | 2 +- .../braintreepayments/api/BraintreeClient.kt | 93 +++++++++++++++---- .../api/BraintreeGraphQLClient.kt | 4 +- .../api/BraintreeHttpClient.kt | 18 ++-- .../api/ConfigurationLoader.kt | 44 ++++----- .../api/ConfigurationLoaderCallback.kt | 2 +- .../braintreepayments/api/CoreAnalytics.kt | 5 + .../api/AnalyticsClientUnitTest.kt | 26 +++--- .../api/ApiClientUnitTest.kt | 4 +- .../api/BraintreeClientUnitTest.kt | 23 +++-- .../api/BraintreeGraphQLClientUnitTest.kt | 2 +- .../api/BraintreeHttpClientUnitTest.kt | 30 +++--- .../api/ConfigurationLoaderUnitTest.kt | 30 +++--- .../api/MockkConfigurationLoaderBuilder.kt | 4 +- .../braintreepayments/api/HttpClientTest.java | 6 +- .../com/braintreepayments/api/HttpClient.java | 20 ++-- .../braintreepayments/api/HttpNoResponse.java | 8 -- .../com/braintreepayments/api/HttpResponse.kt | 6 ++ .../api/HttpResponseTiming.kt | 6 ++ .../api/NetworkResponseCallback.kt | 13 +++ .../api/SynchronousHttpClient.java | 15 ++- .../api/HttpClientUnitTest.java | 34 ++++--- .../api/SynchronousHttpClientUnitTest.java | 2 +- .../api/MockBraintreeClientBuilder.java | 2 +- .../com/braintreepayments/api/VenmoApi.java | 4 +- .../api/VenmoApiUnitTest.java | 25 ++--- 31 files changed, 408 insertions(+), 176 deletions(-) create mode 100644 BraintreeCore/schemas/com.braintreepayments.api.AnalyticsDatabase/6.json create mode 100644 BraintreeCore/src/main/java/com/braintreepayments/api/CoreAnalytics.kt delete mode 100644 SharedUtils/src/main/java/com/braintreepayments/api/HttpNoResponse.java create mode 100644 SharedUtils/src/main/java/com/braintreepayments/api/HttpResponse.kt create mode 100644 SharedUtils/src/main/java/com/braintreepayments/api/HttpResponseTiming.kt create mode 100644 SharedUtils/src/main/java/com/braintreepayments/api/NetworkResponseCallback.kt diff --git a/BraintreeCore/schemas/com.braintreepayments.api.AnalyticsDatabase/6.json b/BraintreeCore/schemas/com.braintreepayments.api.AnalyticsDatabase/6.json new file mode 100644 index 0000000000..64f955750c --- /dev/null +++ b/BraintreeCore/schemas/com.braintreepayments.api.AnalyticsDatabase/6.json @@ -0,0 +1,92 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "a1fb75547e5dd4f48e64a0534e726dcf", + "entities": [ + { + "tableName": "analytics_event", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `paypal_context_id` TEXT, `link_type` TEXT, `venmo_installed` INTEGER NOT NULL DEFAULT 0, `is_vault` INTEGER NOT NULL DEFAULT 0, `start_time` INTEGER DEFAULT -1, `end_time` INTEGER DEFAULT -1, `endpoint` TEXT, `timestamp` INTEGER NOT NULL, `_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "payPalContextId", + "columnName": "paypal_context_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "linkType", + "columnName": "link_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "venmoInstalled", + "columnName": "venmo_installed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isVaultRequest", + "columnName": "is_vault", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "startTime", + "columnName": "start_time", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "-1" + }, + { + "fieldPath": "endTime", + "columnName": "end_time", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "-1" + }, + { + "fieldPath": "endpoint", + "columnName": "endpoint", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a1fb75547e5dd4f48e64a0534e726dcf')" + ] + } +} \ No newline at end of file diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/AnalyticsClient.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/AnalyticsClient.kt index 137aff9c34..9aa10c8d07 100644 --- a/BraintreeCore/src/main/java/com/braintreepayments/api/AnalyticsClient.kt +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/AnalyticsClient.kt @@ -50,6 +50,9 @@ internal class AnalyticsClient @VisibleForTesting constructor( .putLong(WORK_INPUT_KEY_TIMESTAMP, event.timestamp) .putBoolean(WORK_INPUT_KEY_VENMO_INSTALLED, event.venmoInstalled) .putBoolean(WORK_INPUT_KEY_IS_VAULT_REQUEST, event.isVaultRequest) + .putLong(WORK_INPUT_KEY_START_TIME, event.startTime ?: INVALID_TIMESTAMP) + .putLong(WORK_INPUT_KEY_END_TIME, event.endTime ?: INVALID_TIMESTAMP) + .putString(WORK_INPUT_KEY_ENDPOINT, event.endpoint) .build() val analyticsWorkRequest = @@ -68,6 +71,9 @@ internal class AnalyticsClient @VisibleForTesting constructor( val timestamp = inputData.getLong(WORK_INPUT_KEY_TIMESTAMP, INVALID_TIMESTAMP) val venmoInstalled = inputData.getBoolean(WORK_INPUT_KEY_VENMO_INSTALLED, false) val isVaultRequest = inputData.getBoolean(WORK_INPUT_KEY_IS_VAULT_REQUEST, false) + val startTime = inputData.getLong(WORK_INPUT_KEY_START_TIME, INVALID_TIMESTAMP) + val endTime = inputData.getLong(WORK_INPUT_KEY_END_TIME, INVALID_TIMESTAMP) + val endpoint = inputData.getString(WORK_INPUT_KEY_ENDPOINT) return if (eventName == null || timestamp == INVALID_TIMESTAMP) { ListenableWorker.Result.failure() @@ -76,9 +82,12 @@ internal class AnalyticsClient @VisibleForTesting constructor( eventName, payPalContextId, linkType, - timestamp, venmoInstalled, - isVaultRequest + isVaultRequest, + startTime, + endTime, + endpoint, + timestamp ) val analyticsEventDao = analyticsDatabase.analyticsEventDao() analyticsEventDao.insertEvent(event) @@ -164,7 +173,14 @@ internal class AnalyticsClient @VisibleForTesting constructor( return } val metadata = deviceInspector.getDeviceMetadata(context, configuration, sessionId, integration) - val event = AnalyticsEvent("android.crash", null, null, timestamp) + val event = AnalyticsEvent( + "android.crash", + null, + null, + false, + false, + timestamp = timestamp + ) val events = listOf(event) try { val analyticsRequest = serializeEvents(authorization, events, metadata) @@ -173,7 +189,7 @@ internal class AnalyticsClient @VisibleForTesting constructor( data = analyticsRequest.toString(), configuration = null, authorization = authorization, - callback = HttpNoResponse() + callback = null ) } catch (e: JSONException) { /* ignored */ } @@ -206,6 +222,9 @@ internal class AnalyticsClient @VisibleForTesting constructor( .put(TIMESTAMP_KEY, analyticsEvent.timestamp) .put(VENMO_INSTALLED_KEY, analyticsEvent.venmoInstalled) .put(IS_VAULT_REQUEST_KEY, analyticsEvent.isVaultRequest) + .put(START_TIME_KEY, analyticsEvent.startTime) + .put(END_TIME_KEY, analyticsEvent.endTime) + .putOpt(ENDPOINT_KEY, analyticsEvent.endpoint) .put(TENANT_NAME_KEY, "Braintree") eventParamsJSON.put(singleEventJSON) } @@ -231,6 +250,9 @@ internal class AnalyticsClient @VisibleForTesting constructor( private const val EVENT_NAME_KEY = "event_name" private const val TIMESTAMP_KEY = "t" private const val TENANT_NAME_KEY = "tenant_name" + private const val START_TIME_KEY = "start_time" + private const val END_TIME_KEY = "end_time" + private const val ENDPOINT_KEY = "endpoint" const val WORK_NAME_ANALYTICS_UPLOAD = "uploadAnalytics" const val WORK_NAME_ANALYTICS_WRITE = "writeAnalyticsToDb" const val WORK_INPUT_KEY_AUTHORIZATION = "authorization" @@ -243,6 +265,9 @@ internal class AnalyticsClient @VisibleForTesting constructor( const val WORK_INPUT_KEY_VENMO_INSTALLED = "venmoInstalled" const val WORK_INPUT_KEY_IS_VAULT_REQUEST = "isVaultRequest" const val WORK_INPUT_KEY_LINK_TYPE = "linkType" + const val WORK_INPUT_KEY_START_TIME = "startTime" + const val WORK_INPUT_KEY_END_TIME = "endTime" + const val WORK_INPUT_KEY_ENDPOINT = "endpoint" private const val DELAY_TIME_SECONDS = 30L private fun getAuthorizationFromData(inputData: Data?): Authorization? = diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/AnalyticsDatabase.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/AnalyticsDatabase.kt index 8a6ed03267..e9220d9094 100644 --- a/BraintreeCore/src/main/java/com/braintreepayments/api/AnalyticsDatabase.kt +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/AnalyticsDatabase.kt @@ -8,13 +8,14 @@ import androidx.room.RoomDatabase // Ref: https://developer.android.com/training/data-storage/room/migrating-db-versions @Database( - version = 5, + version = 6, entities = [AnalyticsEvent::class], autoMigrations = [ AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3), AutoMigration(from = 3, to = 4), - AutoMigration(from = 4, to = 5) + AutoMigration(from = 4, to = 5), + AutoMigration(from = 5, to = 6), ] ) internal abstract class AnalyticsDatabase : RoomDatabase() { diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/AnalyticsEvent.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/AnalyticsEvent.kt index 0598f001e4..27fb541fc4 100644 --- a/BraintreeCore/src/main/java/com/braintreepayments/api/AnalyticsEvent.kt +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/AnalyticsEvent.kt @@ -20,13 +20,22 @@ open class AnalyticsEvent internal constructor( @ColumnInfo(name = "link_type") open val linkType: String? = null, - open val timestamp: Long = System.currentTimeMillis(), - @ColumnInfo(name = "venmo_installed", defaultValue = "0") open val venmoInstalled: Boolean = false, @ColumnInfo(name = "is_vault", defaultValue = "0") - open val isVaultRequest: Boolean = false + open val isVaultRequest: Boolean = false, + + @ColumnInfo(name = "start_time", defaultValue = "-1") + open val startTime: Long? = -1, + + @ColumnInfo(name = "end_time", defaultValue = "-1") + open val endTime: Long? = -1, + + @ColumnInfo(name = "endpoint") + open val endpoint: String? = null, + + open val timestamp: Long = System.currentTimeMillis() ) { @JvmField @PrimaryKey(autoGenerate = true) diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/AnalyticsEventParams.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/AnalyticsEventParams.kt index 27074b6bc2..c796c64dc6 100644 --- a/BraintreeCore/src/main/java/com/braintreepayments/api/AnalyticsEventParams.kt +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/AnalyticsEventParams.kt @@ -4,10 +4,13 @@ import androidx.annotation.RestrictTo @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) data class AnalyticsEventParams constructor( - var payPalContextId: String?, - var linkType: String?, - var isVaultRequest: Boolean, + var payPalContextId: String? = null, + var linkType: String? = null, + var isVaultRequest: Boolean = false, + var startTime: Long? = null, + var endTime: Long? = null, + var endpoint: String? = null ) { // TODO: this is a convenience constructor for Java; remove after Kotlin migration is complete - constructor() : this(null, null, false) + constructor() : this(null, null, false, null, null, null) } diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/ApiClient.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/ApiClient.kt index 0ee3cfddc0..b5d9a2ab55 100644 --- a/BraintreeCore/src/main/java/com/braintreepayments/api/ApiClient.kt +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/ApiClient.kt @@ -13,7 +13,7 @@ class ApiClient(private val braintreeClient: BraintreeClient) { fun tokenizeGraphQL(tokenizePayload: JSONObject, callback: TokenizeCallback) = braintreeClient.run { sendAnalyticsEvent("card.graphql.tokenization.started") - sendGraphQLPOST(tokenizePayload.toString(), object : HttpResponseCallback { + sendGraphQLPOST(tokenizePayload, object : HttpResponseCallback { override fun onResult(responseBody: String?, httpError: Exception?) { parseResponseToJSON(responseBody)?.let { json -> sendAnalyticsEvent("card.graphql.tokenization.success") diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/BraintreeClient.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/BraintreeClient.kt index 6db7a0efd5..73353ee8f7 100644 --- a/BraintreeCore/src/main/java/com/braintreepayments/api/BraintreeClient.kt +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/BraintreeClient.kt @@ -7,6 +7,8 @@ import android.net.Uri import androidx.annotation.RestrictTo import androidx.annotation.VisibleForTesting import androidx.fragment.app.FragmentActivity +import org.json.JSONException +import org.json.JSONObject /** * Core Braintree class that handles network requests. @@ -160,12 +162,13 @@ open class BraintreeClient @VisibleForTesting internal constructor( open fun getConfiguration(callback: ConfigurationCallback) { getAuthorization { authorization, authError -> if (authorization != null) { - configurationLoader.loadConfiguration(authorization) { configuration, configError -> + configurationLoader.loadConfiguration(authorization) { configuration, configError, timing -> if (configuration != null) { callback.onResult(configuration, null) } else { callback.onResult(null, configError) } + timing?.let { sendAnalyticsTimingEvent("v1/configuration", it) } } } else { callback.onResult(null, authError) @@ -187,19 +190,22 @@ open class BraintreeClient @VisibleForTesting internal constructor( @JvmOverloads @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) fun sendAnalyticsEvent( - eventName: String, - params: AnalyticsEventParams = AnalyticsEventParams() + eventName: String, + params: AnalyticsEventParams = AnalyticsEventParams() ) { getAuthorization { authorization, _ -> if (authorization != null) { getConfiguration { configuration, _ -> val isVenmoInstalled = deviceInspector.isVenmoInstalled(applicationContext) val event = AnalyticsEvent( - eventName, - params.payPalContextId, - params.linkType, - venmoInstalled = isVenmoInstalled, - isVaultRequest = params.isVaultRequest + eventName, + params.payPalContextId, + params.linkType, + isVenmoInstalled, + params.isVaultRequest, + params.startTime, + params.endTime, + params.endpoint ) sendAnalyticsEvent(event, configuration, authorization) } @@ -232,7 +238,18 @@ open class BraintreeClient @VisibleForTesting internal constructor( if (authorization != null) { getConfiguration { configuration, configError -> if (configuration != null) { - httpClient.get(url, configuration, authorization, responseCallback) + httpClient.get(url, configuration, authorization) { response, httpError -> + response?.let { + try { + sendAnalyticsTimingEvent(url, response.timing) + responseCallback.onResult(it.body, null) + } catch (jsonException: JSONException) { + responseCallback.onResult(null, jsonException) + } + } ?: httpError?.let { error -> + responseCallback.onResult(null, error) + } + } } else { responseCallback.onResult(null, configError) } @@ -263,9 +280,19 @@ open class BraintreeClient @VisibleForTesting internal constructor( data = data, configuration = configuration, authorization = authorization, - additionalHeaders = additionalHeaders, - callback = responseCallback - ) + additionalHeaders = additionalHeaders + ) { response, httpError -> + response?.let { + try { + sendAnalyticsTimingEvent(url, it.timing) + responseCallback.onResult(it.body, null) + } catch (jsonException: JSONException) { + responseCallback.onResult(null, jsonException) + } + } ?: httpError?.let { error -> + responseCallback.onResult(null, error) + } + } } else { responseCallback.onResult(null, configError) } @@ -280,17 +307,37 @@ open class BraintreeClient @VisibleForTesting internal constructor( * @suppress */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - fun sendGraphQLPOST(payload: String?, responseCallback: HttpResponseCallback) { + fun sendGraphQLPOST(json: JSONObject?, responseCallback: HttpResponseCallback) { getAuthorization { authorization, authError -> if (authorization != null) { getConfiguration { configuration, configError -> if (configuration != null) { graphQLClient.post( - payload, + json?.toString(), configuration, - authorization, - responseCallback - ) + authorization + ) { response, httpError -> + response?.let { + try { + json?.optString(GraphQLConstants.Keys.OPERATION_NAME)?.let { query -> + val params = AnalyticsEventParams( + startTime = it.timing.startTime, + endTime = it.timing.endTime, + endpoint = query + ) + sendAnalyticsEvent( + CoreAnalytics.apiRequestLatency, + params + ) + } + responseCallback.onResult(it.body, null) + } catch (jsonException: JSONException) { + responseCallback.onResult(null, jsonException) + } + } ?: httpError?.let { error -> + responseCallback.onResult(null, error) + } + } } else { responseCallback.onResult(null, configError) } @@ -466,4 +513,16 @@ open class BraintreeClient @VisibleForTesting internal constructor( open fun launchesBrowserSwitchAsNewTask(launchesBrowserSwitchAsNewTask: Boolean) { this.launchesBrowserSwitchAsNewTask = launchesBrowserSwitchAsNewTask } + + private fun sendAnalyticsTimingEvent(endpoint: String, timing: HttpResponseTiming) { + val cleanedPath = endpoint.replace(Regex("/merchants/([A-Za-z0-9]+)/client_api"), "") + sendAnalyticsEvent( + CoreAnalytics.apiRequestLatency, + AnalyticsEventParams( + startTime = timing.startTime, + endTime = timing.endTime, + endpoint = cleanedPath + ) + ) + } } diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/BraintreeGraphQLClient.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/BraintreeGraphQLClient.kt index f93bac258c..8c5929b95c 100644 --- a/BraintreeCore/src/main/java/com/braintreepayments/api/BraintreeGraphQLClient.kt +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/BraintreeGraphQLClient.kt @@ -12,7 +12,7 @@ internal class BraintreeGraphQLClient( data: String?, configuration: Configuration, authorization: Authorization, - callback: HttpResponseCallback + callback: NetworkResponseCallback ) { if (authorization is InvalidAuthorization) { val message = authorization.errorMessage @@ -35,7 +35,7 @@ internal class BraintreeGraphQLClient( data: String?, configuration: Configuration, authorization: Authorization, - callback: HttpResponseCallback + callback: NetworkResponseCallback ) { if (authorization is InvalidAuthorization) { val message = authorization.errorMessage diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/BraintreeHttpClient.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/BraintreeHttpClient.kt index 00e5fc5dc7..e362930ddb 100644 --- a/BraintreeCore/src/main/java/com/braintreepayments/api/BraintreeHttpClient.kt +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/BraintreeHttpClient.kt @@ -19,13 +19,13 @@ internal class BraintreeHttpClient( * @param path The path or url to request from the server via GET * @param configuration configuration for the Braintree Android SDK. * @param authorization - * @param callback [HttpResponseCallback] + * @param callback [NetworkResponseCallback] */ operator fun get( path: String, configuration: Configuration?, authorization: Authorization?, - callback: HttpResponseCallback + callback: NetworkResponseCallback ) = get(path, configuration, authorization, HttpClient.NO_RETRY, callback) /** @@ -35,14 +35,14 @@ internal class BraintreeHttpClient( * @param configuration configuration for the Braintree Android SDK. * @param authorization * @param retryStrategy retry strategy - * @param callback [HttpResponseCallback] + * @param callback [NetworkResponseCallback] */ operator fun get( path: String, configuration: Configuration?, authorization: Authorization?, @RetryStrategy retryStrategy: Int, - callback: HttpResponseCallback + callback: NetworkResponseCallback ) { if (authorization is InvalidAuthorization) { val message = authorization.errorMessage @@ -82,7 +82,7 @@ internal class BraintreeHttpClient( * @param data The body of the POST request * @param configuration configuration for the Braintree Android SDK. * @param authorization - * @param callback [HttpResponseCallback] + * @param callback [NetworkResponseCallback] */ @Suppress("CyclomaticComplexMethod") fun post( @@ -91,11 +91,11 @@ internal class BraintreeHttpClient( configuration: Configuration?, authorization: Authorization?, additionalHeaders: Map = emptyMap(), - callback: HttpResponseCallback + callback: NetworkResponseCallback? ) { if (authorization is InvalidAuthorization) { val message = authorization.errorMessage - callback.onResult(null, BraintreeException(message)) + callback?.onResult(null, BraintreeException(message)) return } val isRelativeURL = !path.startsWith("http") @@ -103,7 +103,7 @@ internal class BraintreeHttpClient( val message = "Braintree HTTP GET request without configuration cannot have a relative path." val relativeURLNotAllowedError = BraintreeException(message) - callback.onResult(null, relativeURLNotAllowedError) + callback?.onResult(null, relativeURLNotAllowedError) return } val requestData = if (authorization is ClientToken) { @@ -113,7 +113,7 @@ internal class BraintreeHttpClient( authorization.authorizationFingerprint ).toString() } catch (e: JSONException) { - callback.onResult(null, e) + callback?.onResult(null, e) return } } else { diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/ConfigurationLoader.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/ConfigurationLoader.kt index d49d37e7e6..20f0bbc1e2 100644 --- a/BraintreeCore/src/main/java/com/braintreepayments/api/ConfigurationLoader.kt +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/ConfigurationLoader.kt @@ -16,7 +16,7 @@ internal class ConfigurationLoader internal constructor( fun loadConfiguration(authorization: Authorization, callback: ConfigurationLoaderCallback) { if (authorization is InvalidAuthorization) { val message = authorization.errorMessage - callback.onResult(null, BraintreeException(message)) + callback.onResult(null, BraintreeException(message), null) return } val configUrl = Uri.parse(authorization.configUrl) @@ -27,27 +27,26 @@ internal class ConfigurationLoader internal constructor( val cachedConfig = getCachedConfiguration(authorization, configUrl) cachedConfig?.let { - callback.onResult(cachedConfig, null) + callback.onResult(cachedConfig, null, null) } ?: run { - httpClient.get(configUrl, null, authorization, HttpClient.RETRY_MAX_3_TIMES, - object : HttpResponseCallback { - override fun onResult(responseBody: String?, httpError: Exception?) { - responseBody?.let { - try { - val configuration = Configuration.fromJson(it) - saveConfigurationToCache(configuration, authorization, configUrl) - callback.onResult(configuration, null) - } catch (jsonException: JSONException) { - callback.onResult(null, jsonException) - } - } ?: httpError?.let { error -> - val errorMessageFormat = "Request for configuration has failed: %s" - val errorMessage = String.format(errorMessageFormat, error.message) - val configurationException = ConfigurationException(errorMessage, error) - callback.onResult(null, configurationException) - } + httpClient.get( + configUrl, null, authorization, HttpClient.RETRY_MAX_3_TIMES + ) { response, httpError -> + response?.let { + try { + val configuration = Configuration.fromJson(it.body) + saveConfigurationToCache(configuration, authorization, configUrl) + callback.onResult(configuration, null, it.timing) + } catch (jsonException: JSONException) { + callback.onResult(null, jsonException, null) } - }) + } ?: httpError?.let { error -> + val errorMessageFormat = "Request for configuration has failed: %s" + val errorMessage = String.format(errorMessageFormat, error.message) + val configurationException = ConfigurationException(errorMessage, error) + callback.onResult(null, configurationException, null) + } + } } } @@ -60,7 +59,10 @@ internal class ConfigurationLoader internal constructor( configurationCache.saveConfiguration(configuration, cacheKey) } - private fun getCachedConfiguration(authorization: Authorization, configUrl: String): Configuration? { + private fun getCachedConfiguration( + authorization: Authorization, + configUrl: String + ): Configuration? { val cacheKey = createCacheKey(authorization, configUrl) val cachedConfigResponse = configurationCache.getConfiguration(cacheKey) return try { diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/ConfigurationLoaderCallback.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/ConfigurationLoaderCallback.kt index 9f0d22c1e4..a2b7801d05 100644 --- a/BraintreeCore/src/main/java/com/braintreepayments/api/ConfigurationLoaderCallback.kt +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/ConfigurationLoaderCallback.kt @@ -1,5 +1,5 @@ package com.braintreepayments.api internal fun interface ConfigurationLoaderCallback { - fun onResult(result: Configuration?, error: Exception?) + fun onResult(result: Configuration?, error: Exception?, timing: HttpResponseTiming?) } diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/CoreAnalytics.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/CoreAnalytics.kt new file mode 100644 index 0000000000..18993243ab --- /dev/null +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/CoreAnalytics.kt @@ -0,0 +1,5 @@ +package com.braintreepayments.api + +object CoreAnalytics { + const val apiRequestLatency = "core:api-request-latency" +} diff --git a/BraintreeCore/src/test/java/com/braintreepayments/api/AnalyticsClientUnitTest.kt b/BraintreeCore/src/test/java/com/braintreepayments/api/AnalyticsClientUnitTest.kt index f2d0e7d240..1d4f734a7d 100644 --- a/BraintreeCore/src/test/java/com/braintreepayments/api/AnalyticsClientUnitTest.kt +++ b/BraintreeCore/src/test/java/com/braintreepayments/api/AnalyticsClientUnitTest.kt @@ -70,7 +70,7 @@ class AnalyticsClientUnitTest { ) } returns mockk() - var event = AnalyticsEvent(eventName, null, null, 123) + var event = AnalyticsEvent(eventName, null, null, timestamp = 123) val sut = AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector) sut.sendEvent(configuration, event, sessionId, integration, authorization) @@ -190,10 +190,10 @@ class AnalyticsClientUnitTest { } returns metadata val events: MutableList = ArrayList() - events.add(AnalyticsEvent("event0", null, null, 123)) - events.add(AnalyticsEvent("event1", payPalContextId, null, 456)) - events.add(AnalyticsEvent("event2", null, linkType, 789)) - events.add(AnalyticsEvent("event3", payPalContextId, linkType, 987)) + events.add(AnalyticsEvent("event0", null, null, timestamp = 123)) + events.add(AnalyticsEvent("event1", payPalContextId, null, timestamp = 456)) + events.add(AnalyticsEvent("event2", null, linkType, timestamp = 789)) + events.add(AnalyticsEvent("event3", payPalContextId, linkType, timestamp = 987)) every { analyticsEventDao.getAllEvents() } returns events val analyticsJSONSlot = slot() @@ -351,10 +351,10 @@ class AnalyticsClientUnitTest { } returns metadata val events: MutableList = ArrayList() - events.add(AnalyticsEvent("event0", null, null, 123)) - events.add(AnalyticsEvent("event1", payPalContextId, null, 456)) - events.add(AnalyticsEvent("event2", null, linkType, 789)) - events.add(AnalyticsEvent("event3", payPalContextId, linkType, 987)) + events.add(AnalyticsEvent("event0", null, null, timestamp = 123)) + events.add(AnalyticsEvent("event1", payPalContextId, null, timestamp = 456)) + events.add(AnalyticsEvent("event2", null, linkType, timestamp = 789)) + events.add(AnalyticsEvent("event3", payPalContextId, linkType, timestamp = 987)) every { analyticsEventDao.getAllEvents() } returns events val sut = AnalyticsClient(httpClient, analyticsDatabase, workManager, deviceInspector) @@ -378,10 +378,10 @@ class AnalyticsClientUnitTest { } returns createSampleDeviceMetadata() val events: MutableList = ArrayList() - events.add(AnalyticsEvent("event0", null, null, 123)) - events.add(AnalyticsEvent("event1", payPalContextId, null, 456)) - events.add(AnalyticsEvent("event0", null, linkType, 789)) - events.add(AnalyticsEvent("event1", payPalContextId, linkType, 987)) + events.add(AnalyticsEvent("event0", null, null, timestamp = 123)) + events.add(AnalyticsEvent("event1", payPalContextId, null, timestamp = 456)) + events.add(AnalyticsEvent("event0", null, linkType, timestamp = 789)) + events.add(AnalyticsEvent("event1", payPalContextId, linkType, timestamp = 987)) every { analyticsEventDao.getAllEvents() } returns events val httpError = Exception("error") diff --git a/BraintreeCore/src/test/java/com/braintreepayments/api/ApiClientUnitTest.kt b/BraintreeCore/src/test/java/com/braintreepayments/api/ApiClientUnitTest.kt index 634003a69e..1480bcc5f6 100644 --- a/BraintreeCore/src/test/java/com/braintreepayments/api/ApiClientUnitTest.kt +++ b/BraintreeCore/src/test/java/com/braintreepayments/api/ApiClientUnitTest.kt @@ -58,7 +58,7 @@ class ApiClientUnitTest { .authorizationSuccess(Authorization.fromString(Fixtures.BASE64_CLIENT_TOKEN)) .build() - val graphQLBodySlot = slot() + val graphQLBodySlot = slot() every { braintreeClient.sendGraphQLPOST(capture(graphQLBodySlot), any()) } returns Unit val sut = ApiClient(braintreeClient) @@ -66,7 +66,7 @@ class ApiClientUnitTest { sut.tokenizeGraphQL(card.buildJSONForGraphQL(), tokenizeCallback) verify(inverse = true) { braintreeClient.sendPOST(any(), any(), any(), any()) } - assertEquals(card.buildJSONForGraphQL().toString(), graphQLBodySlot.captured) + assertEquals(card.buildJSONForGraphQL().toString(), graphQLBodySlot.captured.toString()) } @Test diff --git a/BraintreeCore/src/test/java/com/braintreepayments/api/BraintreeClientUnitTest.kt b/BraintreeCore/src/test/java/com/braintreepayments/api/BraintreeClientUnitTest.kt index c55ecc2cf9..0c5ce98f75 100644 --- a/BraintreeCore/src/test/java/com/braintreepayments/api/BraintreeClientUnitTest.kt +++ b/BraintreeCore/src/test/java/com/braintreepayments/api/BraintreeClientUnitTest.kt @@ -156,6 +156,7 @@ class BraintreeClientUnitTest { val params = createDefaultParams(configurationLoader, authorizationLoader) val sut = BraintreeClient(params) val httpResponseCallback = mockk(relaxed = true) + val networkResponseCallbackSlot = slot() sut.sendGET("sample-url", httpResponseCallback) verify { @@ -163,9 +164,11 @@ class BraintreeClientUnitTest { "sample-url", configuration, authorization, - httpResponseCallback + capture(networkResponseCallbackSlot) ) } + + assertTrue(networkResponseCallbackSlot.isCaptured) } @Test @@ -219,6 +222,7 @@ class BraintreeClientUnitTest { val params = createDefaultParams(configurationLoader, authorizationLoader) val sut = BraintreeClient(params) + val networkResponseCallbackSlot = slot() val httpResponseCallback = mockk(relaxed = true) sut.sendPOST("sample-url", "{}", emptyMap(), httpResponseCallback) @@ -228,9 +232,11 @@ class BraintreeClientUnitTest { data = "{}", configuration = configuration, authorization = authorization, - callback = httpResponseCallback + callback = capture(networkResponseCallbackSlot) ) } + + assertTrue(networkResponseCallbackSlot.isCaptured) } @Test @@ -343,16 +349,19 @@ class BraintreeClientUnitTest { val params = createDefaultParams(configurationLoader, authorizationLoader) val sut = BraintreeClient(params) val httpResponseCallback = mockk(relaxed = true) + val networkResponseCallbackSlot = slot() - sut.sendGraphQLPOST("{}", httpResponseCallback) + sut.sendGraphQLPOST(JSONObject(), httpResponseCallback) verify { braintreeGraphQLClient.post( "{}", configuration, authorization, - httpResponseCallback + capture(networkResponseCallbackSlot) ) } + + assertTrue(networkResponseCallbackSlot.isCaptured) } @Test @@ -367,7 +376,7 @@ class BraintreeClientUnitTest { val sut = BraintreeClient(params) val httpResponseCallback = mockk(relaxed = true) - sut.sendGraphQLPOST("{}", httpResponseCallback) + sut.sendGraphQLPOST(JSONObject(), httpResponseCallback) verify { httpResponseCallback.onResult(null, authError) } } @@ -387,7 +396,7 @@ class BraintreeClientUnitTest { val sut = BraintreeClient(params) val httpResponseCallback = mockk(relaxed = true) - sut.sendGraphQLPOST("{}", httpResponseCallback) + sut.sendGraphQLPOST(JSONObject(), httpResponseCallback) verify { httpResponseCallback.onResult(null, exception) } } @@ -666,7 +675,7 @@ class BraintreeClientUnitTest { configurationLoader.loadConfiguration(authorization, capture(callbackSlot)) } - callbackSlot.captured.onResult(configuration, null) + callbackSlot.captured.onResult(configuration, null, null) verify { analyticsClient.reportCrash( diff --git a/BraintreeCore/src/test/java/com/braintreepayments/api/BraintreeGraphQLClientUnitTest.kt b/BraintreeCore/src/test/java/com/braintreepayments/api/BraintreeGraphQLClientUnitTest.kt index b7b55003e8..9747e53533 100644 --- a/BraintreeCore/src/test/java/com/braintreepayments/api/BraintreeGraphQLClientUnitTest.kt +++ b/BraintreeCore/src/test/java/com/braintreepayments/api/BraintreeGraphQLClientUnitTest.kt @@ -18,7 +18,7 @@ import java.nio.charset.StandardCharsets class BraintreeGraphQLClientUnitTest { private lateinit var httpClient: HttpClient - private lateinit var httpResponseCallback: HttpResponseCallback + private lateinit var httpResponseCallback: NetworkResponseCallback private lateinit var configuration: Configuration private lateinit var authorization: Authorization diff --git a/BraintreeCore/src/test/java/com/braintreepayments/api/BraintreeHttpClientUnitTest.kt b/BraintreeCore/src/test/java/com/braintreepayments/api/BraintreeHttpClientUnitTest.kt index fa9a1ca6ac..0833761ddd 100644 --- a/BraintreeCore/src/test/java/com/braintreepayments/api/BraintreeHttpClientUnitTest.kt +++ b/BraintreeCore/src/test/java/com/braintreepayments/api/BraintreeHttpClientUnitTest.kt @@ -23,7 +23,7 @@ import java.util.UUID class BraintreeHttpClientUnitTest { private lateinit var httpClient: HttpClient - private lateinit var httpResponseCallback: HttpResponseCallback + private lateinit var httpResponseCallback: NetworkResponseCallback @Before fun beforeEach() { @@ -34,7 +34,7 @@ class BraintreeHttpClientUnitTest { @Test fun get_withNullConfiguration_requiresRequiresRequestToHaveAnAbsolutePath() { val tokenizationKey = mockk() - val callback = mockk() + val callback = mockk() val exceptionSlot = slot() every { callback.onResult(null, capture(exceptionSlot)) } returns Unit @@ -53,7 +53,7 @@ class BraintreeHttpClientUnitTest { @Throws(Exception::class) fun get_withNullConfigurationAndAbsoluteURL_doesNotSetABaseURLOnTheRequest() { val tokenizationKey: Authorization = TokenizationKey(Fixtures.TOKENIZATION_KEY) - val callback = mockk() + val callback = mockk() val httpRequestSlot = slot() every { @@ -75,7 +75,7 @@ class BraintreeHttpClientUnitTest { every { configuration.clientApiUrl } returns "https://example.com" val httpRequestSlot = slot() - val callback = mockk() + val callback = mockk() every { httpClient.sendRequest(capture(httpRequestSlot), HttpClient.NO_RETRY, callback) } returns Unit @@ -100,7 +100,7 @@ class BraintreeHttpClientUnitTest { every { configuration.clientApiUrl } returns "https://example.com" val httpRequestSlot = slot() - val callback = mockk() + val callback = mockk() every { httpClient.sendRequest(capture(httpRequestSlot), HttpClient.NO_RETRY, callback) } returns Unit @@ -128,7 +128,7 @@ class BraintreeHttpClientUnitTest { val configuration = mockk() val exceptionSlot = slot() - val callback = mockk() + val callback = mockk() every { callback.onResult(null, capture(exceptionSlot)) } returns Unit val sut = BraintreeHttpClient(httpClient) @@ -251,7 +251,7 @@ class BraintreeHttpClientUnitTest { val configuration = mockk() every { configuration.clientApiUrl } returns "https://example.com" - val callback = mockk() + val callback = mockk() val httpRequestSlot = slot() every { httpClient.sendRequest(capture(httpRequestSlot), callback) } returns Unit @@ -283,7 +283,7 @@ class BraintreeHttpClientUnitTest { val configuration = mockk() every { configuration.clientApiUrl } returns "https://example.com" - val callback = mockk() + val callback = mockk() val httpRequestSlot = slot() every { httpClient.sendRequest(capture(httpRequestSlot), callback) } returns Unit @@ -314,7 +314,7 @@ class BraintreeHttpClientUnitTest { ) as ClientToken val exceptionSlot = slot() - val callback = mockk() + val callback = mockk() every { callback.onResult(null, capture(exceptionSlot)) } returns Unit val sut = BraintreeHttpClient(httpClient) @@ -341,7 +341,7 @@ class BraintreeHttpClientUnitTest { ) as ClientToken val httpRequestSlot = slot() - val callback = mockk() + val callback = mockk() every { httpClient.sendRequest(capture(httpRequestSlot), callback) } returns Unit val sut = BraintreeHttpClient(httpClient) @@ -366,7 +366,7 @@ class BraintreeHttpClientUnitTest { Authorization.fromString(FixturesHelper.base64Encode(Fixtures.CLIENT_TOKEN)) val exceptionSlot = slot() - val callback = mockk() + val callback = mockk() every { callback.onResult(null, capture(exceptionSlot)) } returns Unit val sut = BraintreeHttpClient(httpClient) @@ -392,7 +392,7 @@ class BraintreeHttpClientUnitTest { InvalidAuthorization("invalid", "token invalid") val exceptionSlot = slot() - val callback = mockk() + val callback = mockk() every { callback.onResult(null, capture(exceptionSlot)) } returns Unit val sut = BraintreeHttpClient(httpClient) @@ -423,7 +423,7 @@ class BraintreeHttpClientUnitTest { data = "{}", configuration = mockk(relaxed = true), authorization = tokenizationKey, - callback = mockk() + callback = mockk() ) val headers = httpRequestSlot.captured.headers @@ -444,7 +444,7 @@ class BraintreeHttpClientUnitTest { data = "{}", configuration = mockk(relaxed = true), authorization = tokenizationKey, - callback = mockk() + callback = mockk() ) val headers = httpRequestSlot.captured.headers @@ -454,7 +454,7 @@ class BraintreeHttpClientUnitTest { @Test fun `when post is called with additional headers, headers are added to the request`() { val headers = mapOf("name1" to "value1", "name2" to "value2") - val callback = mockk() + val callback = mockk() val sut = BraintreeHttpClient(httpClient) sut.post( diff --git a/BraintreeCore/src/test/java/com/braintreepayments/api/ConfigurationLoaderUnitTest.kt b/BraintreeCore/src/test/java/com/braintreepayments/api/ConfigurationLoaderUnitTest.kt index 53c7992db5..8deddbc6c2 100644 --- a/BraintreeCore/src/test/java/com/braintreepayments/api/ConfigurationLoaderUnitTest.kt +++ b/BraintreeCore/src/test/java/com/braintreepayments/api/ConfigurationLoaderUnitTest.kt @@ -25,7 +25,7 @@ class ConfigurationLoaderUnitTest { sut.loadConfiguration(authorization, callback) val expectedConfigUrl = "https://example.com/config?configVersion=3" - val callbackSlot = slot() + val callbackSlot = slot() verify { braintreeHttpClient.get( expectedConfigUrl, @@ -37,9 +37,11 @@ class ConfigurationLoaderUnitTest { } val httpResponseCallback = callbackSlot.captured - httpResponseCallback.onResult(Fixtures.CONFIGURATION_WITH_ACCESS_TOKEN, null) + httpResponseCallback.onResult( + HttpResponse(Fixtures.CONFIGURATION_WITH_ACCESS_TOKEN, HttpResponseTiming(0, 0)), null + ) - verify { callback.onResult(ofType(Configuration::class), null) } + verify { callback.onResult(ofType(Configuration::class), null, HttpResponseTiming(0, 0)) } } @Test @@ -51,7 +53,7 @@ class ConfigurationLoaderUnitTest { sut.loadConfiguration(authorization, callback) val expectedConfigUrl = "https://example.com/config?configVersion=3" - val callbackSlot = slot() + val callbackSlot = slot() verify { braintreeHttpClient.get( expectedConfigUrl, @@ -63,7 +65,9 @@ class ConfigurationLoaderUnitTest { } val httpResponseCallback = callbackSlot.captured - httpResponseCallback.onResult(Fixtures.CONFIGURATION_WITH_ACCESS_TOKEN, null) + httpResponseCallback.onResult( + HttpResponse(Fixtures.CONFIGURATION_WITH_ACCESS_TOKEN, HttpResponseTiming(0, 0)), null + ) val cacheKey = Base64.encodeToString( "https://example.com/config?configVersion=3bearer".toByteArray(), 0 @@ -80,7 +84,7 @@ class ConfigurationLoaderUnitTest { val sut = ConfigurationLoader(braintreeHttpClient, configurationCache) sut.loadConfiguration(authorization, callback) - val callbackSlot = slot() + val callbackSlot = slot() verify { braintreeHttpClient.get( ofType(String::class), @@ -91,9 +95,9 @@ class ConfigurationLoaderUnitTest { ) } val httpResponseCallback = callbackSlot.captured - httpResponseCallback.onResult("not json", null) + httpResponseCallback.onResult(HttpResponse("not json", HttpResponseTiming(0, 0)), null) verify { - callback.onResult(null, ofType(JSONException::class)) + callback.onResult(null, ofType(JSONException::class), null) } } @@ -103,7 +107,7 @@ class ConfigurationLoaderUnitTest { val sut = ConfigurationLoader(braintreeHttpClient, configurationCache) sut.loadConfiguration(authorization, callback) - val callbackSlot = slot() + val callbackSlot = slot() verify { braintreeHttpClient.get( @@ -120,7 +124,7 @@ class ConfigurationLoaderUnitTest { httpResponseCallback.onResult(null, httpError) val errorSlot = slot() verify { - callback.onResult(null, capture(errorSlot)) + callback.onResult(null, capture(errorSlot), null) } val error = errorSlot.captured as ConfigurationException @@ -137,7 +141,7 @@ class ConfigurationLoaderUnitTest { sut.loadConfiguration(authorization, callback) val errorSlot = slot() verify { - callback.onResult(null, capture(errorSlot)) + callback.onResult(null, capture(errorSlot), null) } val exception = errorSlot.captured @@ -163,9 +167,9 @@ class ConfigurationLoaderUnitTest { null, authorization, ofType(Int::class), - ofType(HttpResponseCallback::class) + ofType(NetworkResponseCallback::class) ) } - verify { callback.onResult(ofType(Configuration::class), null) } + verify { callback.onResult(ofType(Configuration::class), null, null) } } } diff --git a/BraintreeCore/src/test/java/com/braintreepayments/api/MockkConfigurationLoaderBuilder.kt b/BraintreeCore/src/test/java/com/braintreepayments/api/MockkConfigurationLoaderBuilder.kt index 6e5ab46420..c1752d5f9c 100644 --- a/BraintreeCore/src/test/java/com/braintreepayments/api/MockkConfigurationLoaderBuilder.kt +++ b/BraintreeCore/src/test/java/com/braintreepayments/api/MockkConfigurationLoaderBuilder.kt @@ -23,9 +23,9 @@ internal class MockkConfigurationLoaderBuilder { every { configurationLoader.loadConfiguration(any(), any()) } answers { val callback = secondArg() if (configuration != null) { - callback.onResult(configuration, null) + callback.onResult(configuration, null, null) } else if (configurationError != null) { - callback.onResult(null, configurationError) + callback.onResult(null, configurationError, null) } } return configurationLoader diff --git a/SharedUtils/src/androidTest/java/com/braintreepayments/api/HttpClientTest.java b/SharedUtils/src/androidTest/java/com/braintreepayments/api/HttpClientTest.java index 1275549354..0416a27194 100644 --- a/SharedUtils/src/androidTest/java/com/braintreepayments/api/HttpClientTest.java +++ b/SharedUtils/src/androidTest/java/com/braintreepayments/api/HttpClientTest.java @@ -30,10 +30,10 @@ public void sendRequest_whenErrorOccurs_callsFailure() throws InterruptedExcepti .baseUrl("https://bad.endpoint") .path("bad/path"); - sut.sendRequest(httpRequest, new HttpResponseCallback() { + sut.sendRequest(httpRequest, new NetworkResponseCallback() { @Override - public void onResult(String responseBody, Exception httpError) { - assertNull(responseBody); + public void onResult(HttpResponse response, Exception httpError) { + assertNull(response); assertNotNull(httpError); countDownLatch.countDown(); } diff --git a/SharedUtils/src/main/java/com/braintreepayments/api/HttpClient.java b/SharedUtils/src/main/java/com/braintreepayments/api/HttpClient.java index 58414ab85d..0e34c80e3f 100644 --- a/SharedUtils/src/main/java/com/braintreepayments/api/HttpClient.java +++ b/SharedUtils/src/main/java/com/braintreepayments/api/HttpClient.java @@ -42,26 +42,26 @@ protected HttpClient(SSLSocketFactory socketFactory, HttpResponseParser httpResp } String sendRequest(HttpRequest request) throws Exception { - return syncHttpClient.request(request); + return syncHttpClient.request(request).getBody(); } - void sendRequest(HttpRequest request, HttpResponseCallback callback) { + void sendRequest(HttpRequest request, NetworkResponseCallback callback) { sendRequest(request, HttpClient.NO_RETRY, callback); } - void sendRequest(HttpRequest request, @RetryStrategy int retryStrategy, HttpResponseCallback callback) { + void sendRequest(HttpRequest request, @RetryStrategy int retryStrategy, NetworkResponseCallback callback) { scheduleRequest(request, retryStrategy, callback); } - private void scheduleRequest(final HttpRequest request, @RetryStrategy final int retryStrategy, final HttpResponseCallback callback) { + private void scheduleRequest(final HttpRequest request, @RetryStrategy final int retryStrategy, final NetworkResponseCallback callback) { resetRetryCount(request); scheduler.runOnBackground(new Runnable() { @Override public void run() { try { - String responseBody = syncHttpClient.request(request); - notifySuccessOnMainThread(callback, responseBody); + HttpResponse httpResponse = syncHttpClient.request(request); + notifySuccessOnMainThread(callback, httpResponse); } catch (Exception e) { switch (retryStrategy) { case HttpClient.NO_RETRY: @@ -76,7 +76,7 @@ public void run() { }); } - private void retryGet(final HttpRequest request, @RetryStrategy final int retryStrategy, final HttpResponseCallback callback) { + private void retryGet(final HttpRequest request, @RetryStrategy final int retryStrategy, final NetworkResponseCallback callback) { URL url = null; try { url = request.getURL(); @@ -115,18 +115,18 @@ private void resetRetryCount(HttpRequest request) { } } - private void notifySuccessOnMainThread(final HttpResponseCallback callback, final String responseBody) { + private void notifySuccessOnMainThread(final NetworkResponseCallback callback, final HttpResponse response) { if (callback != null) { scheduler.runOnMain(new Runnable() { @Override public void run() { - callback.onResult(responseBody, null); + callback.onResult(response, null); } }); } } - private void notifyErrorOnMainThread(final HttpResponseCallback callback, final Exception e) { + private void notifyErrorOnMainThread(final NetworkResponseCallback callback, final Exception e) { if (callback != null) { scheduler.runOnMain(new Runnable() { @Override diff --git a/SharedUtils/src/main/java/com/braintreepayments/api/HttpNoResponse.java b/SharedUtils/src/main/java/com/braintreepayments/api/HttpNoResponse.java deleted file mode 100644 index 3d98f2a384..0000000000 --- a/SharedUtils/src/main/java/com/braintreepayments/api/HttpNoResponse.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.braintreepayments.api; - -class HttpNoResponse implements HttpResponseCallback { - - @Override - public void onResult(String responseBody, Exception httpError) { - } -} diff --git a/SharedUtils/src/main/java/com/braintreepayments/api/HttpResponse.kt b/SharedUtils/src/main/java/com/braintreepayments/api/HttpResponse.kt new file mode 100644 index 0000000000..a6adb8dd0f --- /dev/null +++ b/SharedUtils/src/main/java/com/braintreepayments/api/HttpResponse.kt @@ -0,0 +1,6 @@ +package com.braintreepayments.api + +data class HttpResponse( + var body: String? = null, + var timing: HttpResponseTiming +) diff --git a/SharedUtils/src/main/java/com/braintreepayments/api/HttpResponseTiming.kt b/SharedUtils/src/main/java/com/braintreepayments/api/HttpResponseTiming.kt new file mode 100644 index 0000000000..8b8c479368 --- /dev/null +++ b/SharedUtils/src/main/java/com/braintreepayments/api/HttpResponseTiming.kt @@ -0,0 +1,6 @@ +package com.braintreepayments.api + +data class HttpResponseTiming( + var startTime: Long, + var endTime: Long, +) diff --git a/SharedUtils/src/main/java/com/braintreepayments/api/NetworkResponseCallback.kt b/SharedUtils/src/main/java/com/braintreepayments/api/NetworkResponseCallback.kt new file mode 100644 index 0000000000..e373697901 --- /dev/null +++ b/SharedUtils/src/main/java/com/braintreepayments/api/NetworkResponseCallback.kt @@ -0,0 +1,13 @@ +package com.braintreepayments.api + +import androidx.annotation.MainThread +import androidx.annotation.RestrictTo + +/** + * @suppress + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +fun interface NetworkResponseCallback { + @MainThread + fun onResult(response: HttpResponse?, httpError: Exception?) +} diff --git a/SharedUtils/src/main/java/com/braintreepayments/api/SynchronousHttpClient.java b/SharedUtils/src/main/java/com/braintreepayments/api/SynchronousHttpClient.java index 8f91b634b9..4bdb11eaea 100644 --- a/SharedUtils/src/main/java/com/braintreepayments/api/SynchronousHttpClient.java +++ b/SharedUtils/src/main/java/com/braintreepayments/api/SynchronousHttpClient.java @@ -35,12 +35,14 @@ void setSSLSocketFactory(SSLSocketFactory socketFactory) { this.socketFactory = socketFactory; } - String request(HttpRequest httpRequest) throws Exception { + HttpResponse request(HttpRequest httpRequest) throws Exception { if (httpRequest.getPath() == null) { throw new IllegalArgumentException("Path cannot be null"); } URL url = httpRequest.getURL(); + long startTime = System.currentTimeMillis(); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); if (connection instanceof HttpsURLConnection) { if (socketFactory == null) { @@ -57,7 +59,7 @@ String request(HttpRequest httpRequest) throws Exception { // apply request headers Map headers = httpRequest.getHeaders(); - for (Map.Entry entry : headers.entrySet()) { + for (Map.Entry entry : headers.entrySet()) { connection.setRequestProperty(entry.getKey(), entry.getValue()); } @@ -75,9 +77,14 @@ String request(HttpRequest httpRequest) throws Exception { try { int responseCode = connection.getResponseCode(); - return parser.parse(responseCode, connection); + long endTime = System.currentTimeMillis(); + + String responseBody = parser.parse(responseCode, connection); + + HttpResponseTiming timing = new HttpResponseTiming(startTime, endTime); + return new HttpResponse(responseBody, timing); } finally { connection.disconnect(); } } -} +} \ No newline at end of file diff --git a/SharedUtils/src/test/java/com/braintreepayments/api/HttpClientUnitTest.java b/SharedUtils/src/test/java/com/braintreepayments/api/HttpClientUnitTest.java index 090e0ae6e8..6775de8fd5 100644 --- a/SharedUtils/src/test/java/com/braintreepayments/api/HttpClientUnitTest.java +++ b/SharedUtils/src/test/java/com/braintreepayments/api/HttpClientUnitTest.java @@ -35,7 +35,7 @@ public void beforeEach() { public void sendRequest_sendsRequestOnBackgroundThread() throws Exception { HttpClient sut = new HttpClient(syncHttpClient, threadScheduler); - HttpResponseCallback callback = mock(HttpResponseCallback.class); + NetworkResponseCallback callback = mock(NetworkResponseCallback.class); sut.sendRequest(httpRequest, callback); verifyNoInteractions(syncHttpClient); @@ -51,7 +51,7 @@ public void sendRequest_whenBaseHttpClientThrowsException_notifiesErrorViaCallba Exception exception = new Exception("error"); when(syncHttpClient.request(httpRequest)).thenThrow(exception); - HttpResponseCallback callback = mock(HttpResponseCallback.class); + NetworkResponseCallback callback = mock(NetworkResponseCallback.class); sut.sendRequest(httpRequest, callback); threadScheduler.flushBackgroundThread(); @@ -64,24 +64,26 @@ public void sendRequest_whenBaseHttpClientThrowsException_notifiesErrorViaCallba @Test public void sendRequest_onBaseHttpClientRequestSuccess_notifiesSuccessViaCallbackOnMainThread() throws Exception { HttpClient sut = new HttpClient(syncHttpClient, threadScheduler); + HttpResponse response = new HttpResponse("response body", new HttpResponseTiming(123, 456)); - when(syncHttpClient.request(httpRequest)).thenReturn("response body"); + when(syncHttpClient.request(httpRequest)).thenReturn(response); - HttpResponseCallback callback = mock(HttpResponseCallback.class); + NetworkResponseCallback callback = mock(NetworkResponseCallback.class); sut.sendRequest(httpRequest, callback); threadScheduler.flushBackgroundThread(); - verify(callback, never()).onResult("response body", null); + verify(callback, never()).onResult(response, null); threadScheduler.flushMainThread(); - verify(callback).onResult("response body", null); + verify(callback).onResult(response, null); } @Test public void sendRequest_whenCallbackIsNull_doesNotNotifySuccess() throws Exception { HttpClient sut = new HttpClient(syncHttpClient, threadScheduler); + HttpResponse response = new HttpResponse("response body", new HttpResponseTiming(123, 456)); - when(syncHttpClient.request(httpRequest)).thenReturn("response body"); + when(syncHttpClient.request(httpRequest)).thenReturn(response); sut.sendRequest(httpRequest, null); threadScheduler.flushBackgroundThread(); @@ -108,7 +110,7 @@ public void sendRequest_whenRetryMax3TimesEnabled_retriesRequest3Times() throws Exception exception = new Exception("error"); when(syncHttpClient.request(httpRequest)).thenThrow(exception); - HttpResponseCallback callback = mock(HttpResponseCallback.class); + NetworkResponseCallback callback = mock(NetworkResponseCallback.class); sut.sendRequest(httpRequest, HttpClient.RETRY_MAX_3_TIMES, callback); threadScheduler.flushBackgroundThread(); @@ -122,16 +124,16 @@ public void sendRequest_whenRetryMax3TimesEnabled_notifiesMaxRetriesLimitExceede Exception exception = new Exception("error"); when(syncHttpClient.request(httpRequest)).thenThrow(exception); - HttpResponseCallback callback = mock(HttpResponseCallback.class); + NetworkResponseCallback callback = mock(NetworkResponseCallback.class); sut.sendRequest(httpRequest, HttpClient.RETRY_MAX_3_TIMES, callback); threadScheduler.flushBackgroundThread(); - verify(callback, never()).onResult((String) isNull(), any(Exception.class)); + verify(callback, never()).onResult((HttpResponse) isNull(), any(Exception.class)); threadScheduler.flushMainThread(); ArgumentCaptor captor = ArgumentCaptor.forClass(Exception.class); - verify(callback).onResult((String) isNull(), captor.capture()); + verify(callback).onResult((HttpResponse) isNull(), captor.capture()); HttpClientException httpClientException = (HttpClientException) captor.getValue(); String expectedMessage = "Retry limit has been exceeded. Try again later."; @@ -141,11 +143,12 @@ public void sendRequest_whenRetryMax3TimesEnabled_notifiesMaxRetriesLimitExceede @Test public void sendRequest_whenRetryMax3TimesEnabled_futureRequestsAreAllowed() throws Exception { HttpClient sut = new HttpClient(syncHttpClient, threadScheduler); + HttpResponse response = new HttpResponse("response body", new HttpResponseTiming(123, 456)); Exception exception = new Exception("error"); when(syncHttpClient.request(httpRequest)).thenThrow(exception); - HttpResponseCallback callback = mock(HttpResponseCallback.class); + NetworkResponseCallback callback = mock(NetworkResponseCallback.class); sut.sendRequest(httpRequest, HttpClient.RETRY_MAX_3_TIMES, callback); threadScheduler.flushBackgroundThread(); @@ -153,20 +156,21 @@ public void sendRequest_whenRetryMax3TimesEnabled_futureRequestsAreAllowed() thr reset(syncHttpClient); when(syncHttpClient.request(httpRequest)) .thenThrow(exception) - .thenReturn("response body"); + .thenReturn(response); sut.sendRequest(httpRequest, HttpClient.RETRY_MAX_3_TIMES, callback); threadScheduler.flushBackgroundThread(); threadScheduler.flushMainThread(); - verify(callback).onResult("response body", null); + verify(callback).onResult(response, null); } @Test public void sendRequestSynchronous_sendsHttpRequest() throws Exception { HttpClient sut = new HttpClient(syncHttpClient, threadScheduler); + HttpResponse response = new HttpResponse("response body", new HttpResponseTiming(123, 456)); - when(syncHttpClient.request(httpRequest)).thenReturn("response body"); + when(syncHttpClient.request(httpRequest)).thenReturn(response); String result = sut.sendRequest(httpRequest); assertEquals("response body", result); diff --git a/SharedUtils/src/test/java/com/braintreepayments/api/SynchronousHttpClientUnitTest.java b/SharedUtils/src/test/java/com/braintreepayments/api/SynchronousHttpClientUnitTest.java index 54b53d1ee8..e5fba68b23 100644 --- a/SharedUtils/src/test/java/com/braintreepayments/api/SynchronousHttpClientUnitTest.java +++ b/SharedUtils/src/test/java/com/braintreepayments/api/SynchronousHttpClientUnitTest.java @@ -241,7 +241,7 @@ public void request_parsesResponseAndReturnsHttpBody() throws Exception { when(httpResponseParser.parse(200, connection)).thenReturn("http_ok"); SynchronousHttpClient sut = new SynchronousHttpClient(sslSocketFactory, httpResponseParser); - String result = sut.request(httpRequest); + String result = sut.request(httpRequest).getBody(); assertEquals("http_ok", result); } diff --git a/TestUtils/src/main/java/com/braintreepayments/api/MockBraintreeClientBuilder.java b/TestUtils/src/main/java/com/braintreepayments/api/MockBraintreeClientBuilder.java index 2f334e0b73..9365f643b5 100644 --- a/TestUtils/src/main/java/com/braintreepayments/api/MockBraintreeClientBuilder.java +++ b/TestUtils/src/main/java/com/braintreepayments/api/MockBraintreeClientBuilder.java @@ -216,7 +216,7 @@ public Void answer(InvocationOnMock invocation) { } return null; } - }).when(braintreeClient).sendGraphQLPOST(anyString(), any(HttpResponseCallback.class)); + }).when(braintreeClient).sendGraphQLPOST(any(), any(HttpResponseCallback.class)); return braintreeClient; } diff --git a/Venmo/src/main/java/com/braintreepayments/api/VenmoApi.java b/Venmo/src/main/java/com/braintreepayments/api/VenmoApi.java index 584fe5f283..458a6349e0 100644 --- a/Venmo/src/main/java/com/braintreepayments/api/VenmoApi.java +++ b/Venmo/src/main/java/com/braintreepayments/api/VenmoApi.java @@ -72,7 +72,7 @@ void createPaymentContext(@NonNull final VenmoRequest request, String venmoProfi callback.onResult(null, new BraintreeException("unexpected error")); } - braintreeClient.sendGraphQLPOST(params.toString(), new HttpResponseCallback() { + braintreeClient.sendGraphQLPOST(params, new HttpResponseCallback() { @Override public void onResult(String responseBody, Exception httpError) { @@ -99,7 +99,7 @@ void createNonceFromPaymentContext(String paymentContextId, final VenmoOnActivit variables.put("id", paymentContextId); params.put("variables", variables); - braintreeClient.sendGraphQLPOST(params.toString(), new HttpResponseCallback() { + braintreeClient.sendGraphQLPOST(params, new HttpResponseCallback() { @Override public void onResult(String responseBody, Exception httpError) { diff --git a/Venmo/src/test/java/com/braintreepayments/api/VenmoApiUnitTest.java b/Venmo/src/test/java/com/braintreepayments/api/VenmoApiUnitTest.java index 6dd9b1f721..9d4ec43926 100644 --- a/Venmo/src/test/java/com/braintreepayments/api/VenmoApiUnitTest.java +++ b/Venmo/src/test/java/com/braintreepayments/api/VenmoApiUnitTest.java @@ -52,11 +52,10 @@ public void createPaymentContext_createsPaymentContextViaGraphQL() throws JSONEx venmoAPI.createPaymentContext(request, request.getProfileId(), mock(VenmoApiCallback.class)); - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor captor = ArgumentCaptor.forClass(JSONObject.class); verify(braintreeClient).sendGraphQLPOST(captor.capture(), any(HttpResponseCallback.class)); - String graphQLBody = captor.getValue(); - JSONObject graphQLJSON = new JSONObject(graphQLBody); + JSONObject graphQLJSON = captor.getValue(); String expectedQuery = "mutation CreateVenmoPaymentContext($input: CreateVenmoPaymentContextInput!) { createVenmoPaymentContext(input: $input) { venmoPaymentContext { id } } }"; assertEquals(expectedQuery, graphQLJSON.getString("query")); @@ -98,11 +97,10 @@ public void createPaymentContext_whenTransactionAmountOptionsMissing() throws JS venmoAPI.createPaymentContext(request, request.getProfileId(), mock(VenmoApiCallback.class)); - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor captor = ArgumentCaptor.forClass(JSONObject.class); verify(braintreeClient).sendGraphQLPOST(captor.capture(), any(HttpResponseCallback.class)); - String graphQLBody = captor.getValue(); - JSONObject graphQLJSON = new JSONObject(graphQLBody); + JSONObject graphQLJSON = captor.getValue(); String expectedQuery = "mutation CreateVenmoPaymentContext($input: CreateVenmoPaymentContextInput!) { createVenmoPaymentContext(input: $input) { venmoPaymentContext { id } } }"; assertEquals(expectedQuery, graphQLJSON.getString("query")); @@ -183,11 +181,10 @@ public void createPaymentContext_withTotalAmountAndSetsFinalAmountToTrue() throw venmoAPI.createPaymentContext(request, request.getProfileId(), mock(VenmoApiCallback.class)); - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor captor = ArgumentCaptor.forClass(JSONObject.class); verify(braintreeClient).sendGraphQLPOST(captor.capture(), any(HttpResponseCallback.class)); - String graphQLBody = captor.getValue(); - JSONObject graphQLJSON = new JSONObject(graphQLBody); + JSONObject graphQLJSON = captor.getValue(); String expectedQuery = "mutation CreateVenmoPaymentContext($input: CreateVenmoPaymentContextInput!) { createVenmoPaymentContext(input: $input) { venmoPaymentContext { id } } }"; assertEquals(expectedQuery, graphQLJSON.getString("query")); @@ -212,11 +209,10 @@ public void createPaymentContext_withTotalAmountAndSetsFinalAmountToFalse() thro venmoAPI.createPaymentContext(request, request.getProfileId(), mock(VenmoApiCallback.class)); - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor captor = ArgumentCaptor.forClass(JSONObject.class); verify(braintreeClient).sendGraphQLPOST(captor.capture(), any(HttpResponseCallback.class)); - String graphQLBody = captor.getValue(); - JSONObject graphQLJSON = new JSONObject(graphQLBody); + JSONObject graphQLJSON = captor.getValue(); String expectedQuery = "mutation CreateVenmoPaymentContext($input: CreateVenmoPaymentContextInput!) { createVenmoPaymentContext(input: $input) { venmoPaymentContext { id } } }"; assertEquals(expectedQuery, graphQLJSON.getString("query")); @@ -235,11 +231,10 @@ public void createNonceFromPaymentContext_queriesGraphQLPaymentContext() throws VenmoApi sut = new VenmoApi(braintreeClient, apiClient); sut.createNonceFromPaymentContext("payment-context-id", mock(VenmoOnActivityResultCallback.class)); - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor captor = ArgumentCaptor.forClass(JSONObject.class); verify(braintreeClient).sendGraphQLPOST(captor.capture(), any(HttpResponseCallback.class)); - String payload = captor.getValue(); - JSONObject jsonPayload = new JSONObject(payload); + JSONObject jsonPayload = captor.getValue(); String expectedQuery = "query PaymentContext($id: ID!) { node(id: $id) { ... on VenmoPaymentContext { paymentMethodId userName payerInfo { firstName lastName phoneNumber email externalId userName " + "shippingAddress { fullName addressLine1 addressLine2 adminArea1 adminArea2 postalCode countryCode } billingAddress { fullName addressLine1 addressLine2 adminArea1 adminArea2 postalCode countryCode } } } } }"; assertEquals(expectedQuery, jsonPayload.get("query"));