diff --git a/library/src/main/kotlin/com/chuckerteam/chucker/api/ChuckerEventListener.kt b/library/src/main/kotlin/com/chuckerteam/chucker/api/ChuckerEventListener.kt new file mode 100644 index 000000000..41c977642 --- /dev/null +++ b/library/src/main/kotlin/com/chuckerteam/chucker/api/ChuckerEventListener.kt @@ -0,0 +1,75 @@ +package com.chuckerteam.chucker.api + +import com.chuckerteam.chucker.internal.data.entity.HttpTransaction +import okhttp3.Call +import okhttp3.EventListener +import okhttp3.Handshake +import okhttp3.Protocol +import java.io.IOException +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.Proxy + +internal class ChuckerEventListener constructor( + private val onCallEnded: ((Call) -> Unit) +) : EventListener() { + + val httpTransaction = HttpTransaction() + + override fun callStart(call: Call) { + super.callStart(call) + httpTransaction.callStartDate = System.currentTimeMillis() + } + + override fun callEnd(call: Call) { + super.callEnd(call) + onCallEnded(call) + httpTransaction.callEndDate = System.currentTimeMillis() + } + + override fun callFailed(call: Call, ioe: IOException) { + super.callFailed(call, ioe) + onCallEnded(call) + } + + override fun canceled(call: Call) { + super.canceled(call) + onCallEnded(call) + } + + override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy) { + super.connectStart(call, inetSocketAddress, proxy) + httpTransaction.connectStartDate = System.currentTimeMillis() + } + + override fun connectEnd( + call: Call, + inetSocketAddress: InetSocketAddress, + proxy: Proxy, + protocol: Protocol? + ) { + super.connectEnd(call, inetSocketAddress, proxy, protocol) + httpTransaction.connectEndDate = System.currentTimeMillis() + } + + override fun dnsStart(call: Call, domainName: String) { + super.dnsStart(call, domainName) + httpTransaction.dnsStartDate = System.currentTimeMillis() + } + + override fun dnsEnd(call: Call, domainName: String, inetAddressList: List) { + super.dnsEnd(call, domainName, inetAddressList) + httpTransaction.dnsEndDate = System.currentTimeMillis() + } + + override fun secureConnectStart(call: Call) { + super.secureConnectStart(call) + httpTransaction.secureConnectStartDate = System.currentTimeMillis() + } + + override fun secureConnectEnd(call: Call, handshake: Handshake?) { + super.secureConnectEnd(call, handshake) + httpTransaction.secureConnectEndDate = System.currentTimeMillis() + } + +} diff --git a/library/src/main/kotlin/com/chuckerteam/chucker/api/ChuckerInterceptor.kt b/library/src/main/kotlin/com/chuckerteam/chucker/api/ChuckerInterceptor.kt index f860c87b4..382e85295 100644 --- a/library/src/main/kotlin/com/chuckerteam/chucker/api/ChuckerInterceptor.kt +++ b/library/src/main/kotlin/com/chuckerteam/chucker/api/ChuckerInterceptor.kt @@ -2,11 +2,14 @@ package com.chuckerteam.chucker.api import android.content.Context import androidx.annotation.VisibleForTesting -import com.chuckerteam.chucker.internal.data.entity.HttpTransaction import com.chuckerteam.chucker.internal.support.CacheDirectoryProvider +import com.chuckerteam.chucker.internal.support.HttpTransactionFactory import com.chuckerteam.chucker.internal.support.PlainTextDecoder +import com.chuckerteam.chucker.internal.support.RealChuckerEventListenerFactory import com.chuckerteam.chucker.internal.support.RequestProcessor import com.chuckerteam.chucker.internal.support.ResponseProcessor +import com.chuckerteam.chucker.internal.support.SimpleHttpTransactionFactory +import okhttp3.EventListener import okhttp3.Interceptor import okhttp3.Response import java.io.IOException @@ -53,6 +56,8 @@ public class ChuckerInterceptor private constructor( decoders, ) + private var httpTransactionFactory: HttpTransactionFactory = SimpleHttpTransactionFactory() + init { if (builder.createShortcut) { Chucker.createShortcut(builder.context) @@ -64,9 +69,16 @@ public class ChuckerInterceptor private constructor( headersToRedact.addAll(headerName) } + public fun useEventListener(): EventListener.Factory { + val realEventListenerFactory = RealChuckerEventListenerFactory() + httpTransactionFactory = realEventListenerFactory + + return realEventListenerFactory + } + @Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response { - val transaction = HttpTransaction() + val transaction = httpTransactionFactory.getHttpTransaction(chain.call()) val request = chain.request() requestProcessor.process(request, transaction) @@ -87,7 +99,7 @@ public class ChuckerInterceptor private constructor( * * @param context An Android [Context]. */ - public class Builder(internal var context: Context) { + public class Builder(providedContext: Context) { internal var collector: ChuckerCollector? = null internal var maxContentLength = MAX_CONTENT_LENGTH internal var cacheDirectoryProvider: CacheDirectoryProvider? = null @@ -95,6 +107,7 @@ public class ChuckerInterceptor private constructor( internal var headersToRedact = emptySet() internal var decoders = emptyList() internal var createShortcut = true + internal val context = providedContext.applicationContext /** * Sets the [ChuckerCollector] to customize data retention. diff --git a/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/entity/HttpTransaction.kt b/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/entity/HttpTransaction.kt index e37035d58..ebecfbe0e 100644 --- a/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/entity/HttpTransaction.kt +++ b/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/entity/HttpTransaction.kt @@ -56,6 +56,14 @@ internal class HttpTransaction( @ColumnInfo(name = "responseImageData") var responseImageData: ByteArray?, @ColumnInfo(name = "graphQlDetected") var graphQlDetected: Boolean = false, @ColumnInfo(name = "graphQlOperationName") var graphQlOperationName: String?, + @ColumnInfo(name = "callStartDate") var callStartDate: Long?, + @ColumnInfo(name = "callEndDate") var callEndDate: Long?, + @ColumnInfo(name = "connectStartDate") var connectStartDate: Long?, + @ColumnInfo(name = "connectEndDate") var connectEndDate: Long?, + @ColumnInfo(name = "secureConnectStartDate") var secureConnectStartDate: Long?, + @ColumnInfo(name = "secureConnectEndDate") var secureConnectEndDate: Long?, + @ColumnInfo(name = "dnsStartDate") var dnsStartDate: Long?, + @ColumnInfo(name = "dnsEndDate") var dnsEndDate: Long?, ) { @Ignore @@ -85,7 +93,15 @@ internal class HttpTransaction( responseHeadersSize = null, responseBody = null, responseImageData = null, - graphQlOperationName = null + graphQlOperationName = null, + callStartDate = null, + callEndDate = null, + connectStartDate = null, + connectEndDate = null, + secureConnectStartDate = null, + secureConnectEndDate = null, + dnsStartDate = null, + dnsEndDate = null, ) enum class Status { @@ -109,6 +125,42 @@ internal class HttpTransaction( val durationString: String? get() = tookMs?.let { "$it ms" } + val callDurationString: String? + get() { + val start = callStartDate ?: return null + val end = callEndDate ?: return null + return (end - start).let { "$it ms" } + } + + val connectDuration: Long? + get() { + val start = connectStartDate ?: return null + val end = connectEndDate ?: return null + return (end - start) + } + val connectDurationString: String? + get() = connectDuration?.let { "$it ms" } + + + val secureConnectDuration: Long? + get() { + val start = secureConnectStartDate ?: return null + val end = secureConnectEndDate ?: return null + return (end - start) + } + + val secureConnectDurationString: String? + get() = secureConnectDuration?.let { "$it ms" } + + val dnsDuration: Long? + get() { + val start = dnsStartDate ?: return null + val end = dnsEndDate ?: return null + return (end - start) + } + + val dnsDurationString: String? + get() = dnsDuration?.let { "$it ms" } val requestSizeString: String get() = formatBytes(requestPayloadSize ?: 0) @@ -211,6 +263,7 @@ internal class HttpTransaction( contentType.contains("xml", ignoreCase = true) -> FormatUtils.formatXml(body) contentType.contains("x-www-form-urlencoded", ignoreCase = true) -> FormatUtils.formatUrlEncodedForm(body) + else -> body } } @@ -295,6 +348,14 @@ internal class HttpTransaction( (isResponseBodyEncoded == other.isResponseBodyEncoded) && (responseImageData?.contentEquals(other.responseImageData ?: byteArrayOf()) != false) && (graphQlOperationName == other.graphQlOperationName) && - (graphQlDetected == other.graphQlDetected) + (graphQlDetected == other.graphQlDetected) && + (callStartDate == other.callStartDate) && + (callEndDate == other.callEndDate) && + (connectStartDate == other.connectStartDate) && + (connectEndDate == other.connectEndDate) && + (secureConnectStartDate == other.secureConnectStartDate) && + (secureConnectEndDate == other.secureConnectEndDate) && + (dnsStartDate == other.dnsStartDate) && + (dnsEndDate == other.dnsEndDate) } } diff --git a/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/har/log/entry/Timings.kt b/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/har/log/entry/Timings.kt index 27b895d80..7a723e4d8 100644 --- a/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/har/log/entry/Timings.kt +++ b/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/har/log/entry/Timings.kt @@ -17,6 +17,9 @@ internal data class Timings( ) { constructor(transaction: HttpTransaction) : this( wait = transaction.tookMs ?: 0, + dns = transaction.dnsDuration, + connect = transaction.connectDuration ?: 0, + ssl = transaction.secureConnectDuration ) fun getTime(): Long { diff --git a/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/repository/HttpTransactionDatabaseRepository.kt b/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/repository/HttpTransactionDatabaseRepository.kt index b15f37fc1..7648685c4 100644 --- a/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/repository/HttpTransactionDatabaseRepository.kt +++ b/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/repository/HttpTransactionDatabaseRepository.kt @@ -30,7 +30,7 @@ internal class HttpTransactionDatabaseRepository(private val database: ChuckerDa override suspend fun insertTransaction(transaction: HttpTransaction) { val id = transactionDao.insert(transaction) - transaction.id = id ?: 0 + transaction.id = id } override suspend fun updateTransaction(transaction: HttpTransaction): Int { diff --git a/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/room/ChuckerDatabase.kt b/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/room/ChuckerDatabase.kt index aa81af687..9e8bf4473 100644 --- a/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/room/ChuckerDatabase.kt +++ b/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/room/ChuckerDatabase.kt @@ -6,7 +6,7 @@ import androidx.room.Room import androidx.room.RoomDatabase import com.chuckerteam.chucker.internal.data.entity.HttpTransaction -@Database(entities = [HttpTransaction::class], version = 9, exportSchema = false) +@Database(entities = [HttpTransaction::class], version = 10, exportSchema = false) internal abstract class ChuckerDatabase : RoomDatabase() { abstract fun transactionDao(): HttpTransactionDao diff --git a/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/room/HttpTransactionDao.kt b/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/room/HttpTransactionDao.kt index fa4429c1f..a94927ad7 100644 --- a/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/room/HttpTransactionDao.kt +++ b/library/src/main/kotlin/com/chuckerteam/chucker/internal/data/room/HttpTransactionDao.kt @@ -28,7 +28,7 @@ internal interface HttpTransactionDao { fun getFilteredTuples(codeQuery: String, pathQuery: String): LiveData> @Insert - suspend fun insert(transaction: HttpTransaction): Long? + suspend fun insert(transaction: HttpTransaction): Long @Update(onConflict = OnConflictStrategy.REPLACE) suspend fun update(transaction: HttpTransaction): Int diff --git a/library/src/main/kotlin/com/chuckerteam/chucker/internal/support/HttpTransactionFactory.kt b/library/src/main/kotlin/com/chuckerteam/chucker/internal/support/HttpTransactionFactory.kt new file mode 100644 index 000000000..b4b51f58e --- /dev/null +++ b/library/src/main/kotlin/com/chuckerteam/chucker/internal/support/HttpTransactionFactory.kt @@ -0,0 +1,37 @@ +package com.chuckerteam.chucker.internal.support + +import com.chuckerteam.chucker.api.ChuckerEventListener +import com.chuckerteam.chucker.internal.data.entity.HttpTransaction +import okhttp3.Call +import okhttp3.EventListener +import java.util.concurrent.ConcurrentHashMap + +internal class RealChuckerEventListenerFactory : EventListener.Factory, HttpTransactionFactory { + + private val httpTransactionHolder = ConcurrentHashMap() + + override fun create(call: Call): EventListener { + val eventListener = ChuckerEventListener { httpTransactionHolder.remove(it) } + httpTransactionHolder[call] = eventListener + + return eventListener + } + + override fun getHttpTransaction(call: Call): HttpTransaction { + return httpTransactionHolder[call]?.httpTransaction + ?: error("HttpTransaction required before the Call was created") + } + +} + +internal interface HttpTransactionFactory { + fun getHttpTransaction(call: Call): HttpTransaction +} + +internal class SimpleHttpTransactionFactory : HttpTransactionFactory { + override fun getHttpTransaction(call: Call): HttpTransaction { + return HttpTransaction() + } + +} + diff --git a/library/src/main/kotlin/com/chuckerteam/chucker/internal/ui/transaction/TransactionOverviewFragment.kt b/library/src/main/kotlin/com/chuckerteam/chucker/internal/ui/transaction/TransactionOverviewFragment.kt index b1a2e47f1..6851f7e02 100644 --- a/library/src/main/kotlin/com/chuckerteam/chucker/internal/ui/transaction/TransactionOverviewFragment.kt +++ b/library/src/main/kotlin/com/chuckerteam/chucker/internal/ui/transaction/TransactionOverviewFragment.kt @@ -8,7 +8,6 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Observer import com.chuckerteam.chucker.R import com.chuckerteam.chucker.databinding.ChuckerFragmentTransactionOverviewBinding import com.chuckerteam.chucker.internal.data.entity.HttpTransaction @@ -37,9 +36,8 @@ internal class TransactionOverviewFragment : Fragment() { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { menu.findItem(R.id.save_body).isVisible = false viewModel.doesUrlRequireEncoding.observe( - viewLifecycleOwner, - Observer { menu.findItem(R.id.encode_url).isVisible = it } - ) + viewLifecycleOwner + ) { menu.findItem(R.id.encode_url).isVisible = it } super.onCreateOptionsMenu(menu, inflater) } @@ -48,9 +46,8 @@ internal class TransactionOverviewFragment : Fragment() { super.onViewCreated(view, savedInstanceState) viewModel.transaction.combineLatest(viewModel.encodeUrl).observe( - viewLifecycleOwner, - Observer { (transaction, encodeUrl) -> populateUI(transaction, encodeUrl) } - ) + viewLifecycleOwner + ) { (transaction, encodeUrl) -> populateUI(transaction, encodeUrl) } } private fun populateUI(transaction: HttpTransaction?, encodeUrl: Boolean) { @@ -84,6 +81,10 @@ internal class TransactionOverviewFragment : Fragment() { requestTime.text = transaction?.requestDateString responseTime.text = transaction?.responseDateString duration.text = transaction?.durationString + callDuration.text = transaction?.callDurationString + connectDuration.text = transaction?.connectDurationString + secureConnectDuration.text = transaction?.secureConnectDurationString + dnsDuration.text = transaction?.dnsDurationString requestSize.text = transaction?.requestSizeString responseSize.text = transaction?.responseSizeString totalSize.text = transaction?.totalSizeString diff --git a/library/src/main/res/layout/chucker_fragment_transaction_overview.xml b/library/src/main/res/layout/chucker_fragment_transaction_overview.xml index b56440617..709a71171 100644 --- a/library/src/main/res/layout/chucker_fragment_transaction_overview.xml +++ b/library/src/main/res/layout/chucker_fragment_transaction_overview.xml @@ -283,7 +283,87 @@ app:layout_constraintTop_toBottomOf="@id/barrierResponseTime" tools:text="405 ms" /> - + + + + + + + + + + + + + + + + - - - - + + + +