Skip to content

Commit

Permalink
[QL] Measure API Request Latency (#1032)
Browse files Browse the repository at this point in the history
* 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 <sshropshire@paypal.com>
  • Loading branch information
richherrera and sshropshire authored Jul 2, 2024
1 parent 12fbf83 commit c3ed1be
Show file tree
Hide file tree
Showing 31 changed files with 408 additions and 176 deletions.
Original file line number Diff line number Diff line change
@@ -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')"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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()
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -173,7 +189,7 @@ internal class AnalyticsClient @VisibleForTesting constructor(
data = analyticsRequest.toString(),
configuration = null,
authorization = authorization,
callback = HttpNoResponse()
callback = null
)
} catch (e: JSONException) { /* ignored */
}
Expand Down Expand Up @@ -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)
}
Expand All @@ -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"
Expand All @@ -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? =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading

0 comments on commit c3ed1be

Please sign in to comment.