Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extended Graphql support: Identify GraphQL requests fired as HTTP GET Requests. #884

Merged
merged 25 commits into from Sep 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b1202c8
Updates HttpTransaction.kt to identify GQL requests on the basis of i…
ArjanSM Sep 14, 2022
d259994
bare bones implementation
ArjanSM Sep 14, 2022
48be8a5
bugs
ArjanSM Sep 14, 2022
4ff761b
UI Cleanup
ArjanSM Sep 14, 2022
589c7f2
Fix tests
ArjanSM Sep 14, 2022
436d420
Adds Get request to sample
ArjanSM Sep 14, 2022
bf99a3b
Update library api dump.
ArjanSM Sep 15, 2022
6606b0b
id graphql request on the basis of /graphql path in the url.
ArjanSM Sep 15, 2022
ef395d2
updates api dump
ArjanSM Sep 15, 2022
61bad35
restores ChuckerInterceptor.kt
ArjanSM Sep 15, 2022
af80a8c
restores changes that identify GQL requests based on operation name.
ArjanSM Sep 15, 2022
8516dc0
restores changes that identify GQL requests based on operation name.
ArjanSM Sep 15, 2022
89bf041
renames property `isGraphQLRequest` to `graphqlDetected` in HttpTrans…
ArjanSM Sep 19, 2022
61ba9ab
defines a function `isGraphQLRequest` in RequestProcessor.kt to set t…
ArjanSM Sep 19, 2022
92e9c7f
reverts operation name in chucker_list_item_transaction.xml
ArjanSM Sep 19, 2022
ab62817
cleanup
ArjanSM Sep 19, 2022
8cf9044
fix test
ArjanSM Sep 19, 2022
56f459d
fix preview text in chucker_list_item_transaction.xml
ArjanSM Sep 19, 2022
afca77b
fix wildcard imports
ArjanSM Sep 19, 2022
e2c0445
fixes indentation in RequestProcessorTest.kt
ArjanSM Sep 19, 2022
038dcc6
PR Comments
ArjanSM Sep 20, 2022
920d3ad
Fix test
ArjanSM Sep 20, 2022
6147f71
Removes GsonConverterFactory and changes call types to ResponseBody i…
ArjanSM Sep 20, 2022
be40880
Fix property order
ArjanSM Sep 20, 2022
adfec92
PR Comments
ArjanSM Sep 20, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -54,6 +54,7 @@ internal class HttpTransaction(
@ColumnInfo(name = "responseBody") var responseBody: String?,
@ColumnInfo(name = "isResponseBodyEncoded") var isResponseBodyEncoded: Boolean = false,
@ColumnInfo(name = "responseImageData") var responseImageData: ByteArray?,
@ColumnInfo(name = "graphQlDetected") var graphQlDetected: Boolean = false,
@ColumnInfo(name = "graphQlOperationName") var graphQlOperationName: String?,
) {

Expand Down Expand Up @@ -293,6 +294,7 @@ internal class HttpTransaction(
(responseBody == other.responseBody) &&
(isResponseBodyEncoded == other.isResponseBodyEncoded) &&
(responseImageData?.contentEquals(other.responseImageData ?: byteArrayOf()) != false) &&
(graphQlOperationName == other.graphQlOperationName)
(graphQlOperationName == other.graphQlOperationName) &&
(graphQlDetected == other.graphQlDetected)
}
}
Expand Up @@ -23,6 +23,7 @@ internal data class HttpTransactionTuple(
@ColumnInfo(name = "requestPayloadSize") var requestPayloadSize: Long?,
@ColumnInfo(name = "responsePayloadSize") var responsePayloadSize: Long?,
@ColumnInfo(name = "error") var error: String?,
@ColumnInfo(name = "graphQlDetected") var graphQlDetected: Boolean = false,
@ColumnInfo(name = "graphQlOperationName") var graphQlOperationName: String?,
) {
val isSsl: Boolean get() = scheme.equals("https", ignoreCase = true)
Expand Down
Expand Up @@ -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 = 8, exportSchema = false)
@Database(entities = [HttpTransaction::class], version = 9, exportSchema = false)
internal abstract class ChuckerDatabase : RoomDatabase() {

abstract fun transactionDao(): HttpTransactionDao
Expand Down
Expand Up @@ -14,14 +14,14 @@ internal interface HttpTransactionDao {

@Query(
"SELECT id, requestDate, tookMs, protocol, method, host, path, scheme, responseCode, " +
"requestPayloadSize, responsePayloadSize, error, graphQlOperationName FROM " +
"requestPayloadSize, responsePayloadSize, error, graphQLDetected, graphQlOperationName FROM " +
"transactions ORDER BY requestDate DESC"
)
fun getSortedTuples(): LiveData<List<HttpTransactionTuple>>

@Query(
"SELECT id, requestDate, tookMs, protocol, method, host, path, scheme, responseCode, " +
"requestPayloadSize, responsePayloadSize, error, graphQlOperationName FROM " +
"requestPayloadSize, responsePayloadSize, error, graphQLDetected, graphQlOperationName FROM " +
"transactions WHERE responseCode LIKE :codeQuery AND path LIKE :pathQuery " +
"ORDER BY requestDate DESC"
)
Expand Down
Expand Up @@ -31,6 +31,7 @@ internal class RequestProcessor(
setGraphQlOperationName(it)
}
populateUrl(request.url)
graphQlDetected = isGraphQLRequest(this.graphQlOperationName, request)

requestDate = System.currentTimeMillis()
method = request.method
Expand Down Expand Up @@ -78,4 +79,9 @@ internal class RequestProcessor(
null
}
}.firstOrNull()

private fun isGraphQLRequest(graphQLOperationName: String?, request: Request) =
graphQLOperationName != null ||
request.url.pathSegments.contains("graphql") ||
request.url.host.contains("graphql")
}
Expand Up @@ -67,7 +67,7 @@ internal class TransactionAdapter internal constructor(
transactionId = transaction.id

itemBinding.apply {
displayGraphQlFields(transaction.graphQlOperationName)
displayGraphQlFields(transaction.graphQlOperationName, transaction.graphQlDetected)
path.text = "${transaction.method} ${transaction.getFormattedPath(encode = false)}"
host.text = transaction.host
timeStart.text = DateFormat.getTimeInstance().format(transaction.requestDate)
Expand Down Expand Up @@ -120,12 +120,15 @@ internal class TransactionAdapter internal constructor(
}
}

private fun ChuckerListItemTransactionBinding.displayGraphQlFields(graphQlOperationName: String?) {
if (graphQlOperationName != null) {
private fun ChuckerListItemTransactionBinding.displayGraphQlFields(
graphQlOperationName: String?,
graphQLDetected: Boolean
) {
if (graphQLDetected) {
graphqlIcon.visibility = View.VISIBLE
graphqlPath.visibility = View.VISIBLE

graphqlPath.text = graphQlOperationName
cortinico marked this conversation as resolved.
Show resolved Hide resolved
graphqlPath.text = graphQlOperationName ?: root.resources.getString(R.string.chucker_graphql_operation_is_empty)
} else {
graphqlIcon.visibility = View.GONE
graphqlPath.visibility = View.GONE
Expand Down
1 change: 1 addition & 0 deletions library/src/main/res/values/strings.xml
Expand Up @@ -58,4 +58,5 @@
<string name="chucker_request_is_empty">This request is empty</string>
<string name="chucker_response_is_empty">This response is empty</string>
<string name="chucker_shortcut_label">Open Chucker</string>
<string name="chucker_graphql_operation_is_empty"> &lt;Unable to discover GraphQL operation name&gt;</string>
</resources>
Expand Up @@ -120,6 +120,7 @@ internal class HttpTransactionTupleTest {
responsePayloadSize: Long? = null,
error: String? = null,
graphQlOperationName: String? = null,
graphQLDetected: Boolean = false,
) = HttpTransactionTuple(
id = id,
requestDate = requestDate,
Expand All @@ -133,6 +134,7 @@ internal class HttpTransactionTupleTest {
requestPayloadSize = requestPayloadSize,
responsePayloadSize = responsePayloadSize,
error = error,
graphQlOperationName = graphQlOperationName
graphQlOperationName = graphQlOperationName,
graphQlDetected = graphQLDetected
)
}
Expand Up @@ -68,6 +68,8 @@ internal class HttpTransactionDaoTest {
assertThat(stringValue("responseMessage")).isEqualTo(data.responseMessage)
assertThat(stringValue("responseBody")).isEqualTo(data.responseBody)
assertThat(stringValue("error")).isEqualTo(data.error)
assertThat(stringValue("graphQlOperationName")).isNull()
assertThat(booleanValue("graphQlDetected")).isFalse()
}
}

Expand Down Expand Up @@ -244,4 +246,7 @@ internal class HttpTransactionDaoTest {

private fun Cursor.longValue(fieldName: String) =
getLong(getColumnIndex(fieldName))

private fun Cursor.booleanValue(fieldName: String) =
getInt(getColumnIndex(fieldName)) == 1
}
Expand Up @@ -7,8 +7,11 @@ import com.chuckerteam.chucker.internal.data.entity.HttpTransaction
import io.mockk.every
import io.mockk.mockk
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Assert.assertFalse
import org.junit.Test

internal class RequestProcessorTest {
Expand Down Expand Up @@ -39,5 +42,39 @@ internal class RequestProcessorTest {
requestProcessor.process(request, transaction)

assertEquals(operationName, transaction.graphQlOperationName)
assertTrue(transaction.graphQlDetected)
}

@Test
fun `GIVEN an Url containing graphql path WHEN process request THEN transaction isGraphQLRequest`() {
val transaction = HttpTransaction()
val request: Request = mockk(relaxed = true ) {
every { url } returns "http://some/api/graphql".toHttpUrl()
}
requestProcessor.process(request, transaction)

assertTrue(transaction.graphQlDetected)
}

@Test
fun `GIVEN a url with no graphql path WHEN process request THEN transaction !isGraphQLRequest`() {
val transaction = HttpTransaction()
val request: Request = mockk(relaxed = true ) {
every { url } returns "http://some/random/api".toHttpUrl()
}
requestProcessor.process(request, transaction)

assertFalse(transaction.graphQlDetected)
}

@Test
fun `GIVEN a url with graphql host WHEN process request THEN transaction isGraphQLRequest`() {
val transaction = HttpTransaction()
val request: Request = mockk(relaxed = true ) {
every { url } returns "http://some.graphql.api".toHttpUrl()
}
requestProcessor.process(request, transaction)

assertTrue(transaction.graphQlDetected)
}
}
Expand Up @@ -51,6 +51,7 @@ internal object HarTestUtils {
isResponseBodyEncoded = false,
responseImageData = null,
graphQlOperationName = null,
graphQlDetected = false,
)
}

Expand Down Expand Up @@ -82,19 +83,19 @@ internal object HarTestUtils {
return Content(createTransaction(method))
}

internal fun createEntry(method: String): Entry? {
internal fun createEntry(method: String): Entry {
return Entry(createTransaction(method))
}

internal fun createPostData(method: String): PostData? {
internal fun createPostData(method: String): PostData {
return PostData(createTransaction(method))
}

internal fun createRequest(method: String): Request? {
internal fun createRequest(method: String): Request {
return Request(createTransaction(method))
}

internal fun createResponse(method: String): Response? {
internal fun createResponse(method: String): Response {
return Response(createTransaction(method))
}
}
Expand Up @@ -35,6 +35,7 @@ internal object TestTransactionFactory {
responseBody = """{"field": "value"}""",
isResponseBodyEncoded = false,
responseImageData = null,
graphQlDetected = false,
graphQlOperationName = null,
)
}
Expand Down
Expand Up @@ -6,23 +6,62 @@ import com.apollographql.apollo3.network.okHttpClient
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import okhttp3.ResponseBody
import retrofit2.Callback
import retrofit2.Call
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.create
import retrofit2.http.GET
import retrofit2.http.Query

private const val GRAPHQL_BASE_URL = "https://rickandmortyapi.com/graphql/"
private const val BASE_URL = "https://rickandmortyapi.com/"
class GraphQlTask (
client: OkHttpClient,
) : HttpTask {

private val apolloClient = ApolloClient.Builder()
.serverUrl("https://rickandmortyapi.com/graphql")
.serverUrl(GRAPHQL_BASE_URL)
.okHttpClient(client)
.build()

private val api = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.build()
.create<Api>()

private val scope = MainScope()

override fun run() {
scope.launch {
api.getCharacterById(GRAPHQL_QUERY, GRAPHQL_QUERY_VARIABLE).enqueue(object: Callback<ResponseBody> {
override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) = Unit

override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
t.printStackTrace()
}
})
apolloClient
.query(SearchCharactersQuery(Optional.presentIfNotNull("Morty")))
.execute()
}
}

private interface Api {
@GET("graphql")
fun getCharacterById(@Query("query") query: String,
@Query("variables") variables: String? = null )
: Call<ResponseBody>
}
}

const val GRAPHQL_QUERY = """query GetCharacter( ${'$'}id: ID! ){
character(id:${'$'}id) {
id:id,
name,
status
}
}"""
const val GRAPHQL_QUERY_VARIABLE = """{"id":1}"""