Skip to content

Commit

Permalink
[full ci] feat: capture breadcrumbs for OkHttp requests
Browse files Browse the repository at this point in the history
  • Loading branch information
fractalwrench committed Sep 10, 2021
1 parent a535872 commit 265cc5a
Show file tree
Hide file tree
Showing 13 changed files with 576 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

* Capture breadcrumbs for OkHttp network requests
[#1358](https://github.com/bugsnag/bugsnag-android/pull/1358)
[#1361](https://github.com/bugsnag/bugsnag-android/pull/1361)

* Update project to build using Gradle/AGP 7
[#1354](https://github.com/bugsnag/bugsnag-android/pull/1354)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ data class ImmutableConfig(

// results cached here to avoid unnecessary lookups in Client.
val packageInfo: PackageInfo?,
val appInfo: ApplicationInfo?
val appInfo: ApplicationInfo?,
val redactedKeys: Collection<String>
) {

@JvmName("getErrorApiDeliveryParams")
Expand Down Expand Up @@ -162,7 +163,8 @@ internal fun convertToImmutableConfig(
persistenceDirectory = persistenceDir,
sendLaunchCrashesSynchronously = config.sendLaunchCrashesSynchronously,
packageInfo = packageInfo,
appInfo = appInfo
appInfo = appInfo,
redactedKeys = config.redactedKeys
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.bugsnag.android

private const val REDACTED_PLACEHOLDER = "[REDACTED]"

/**
* Replaces any values in the map that match [Configuration.getRedactedKeys] with a
* placeholder.
*/
internal fun Client.redactMap(data: Map<String, String?>): Map<String, String?> {
return data.mapValues { entry ->
val redactedKeys = config.redactedKeys

when {
redactedKeys.contains(entry.key) -> REDACTED_PLACEHOLDER
else -> entry.value
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,148 @@
package com.bugsnag.android.okhttp

import androidx.annotation.VisibleForTesting
import com.bugsnag.android.BreadcrumbType
import com.bugsnag.android.Client
import com.bugsnag.android.Plugin
import com.bugsnag.android.redactMap
import okhttp3.Call
import okhttp3.EventListener
import okhttp3.Request
import okhttp3.Response
import java.io.IOException
import java.util.concurrent.ConcurrentHashMap

class BugsnagOkHttpPlugin : Plugin, EventListener() {
/**
* This plugin captures network requests made by OkHttp as breadcrumbs in Bugsnag.
* It tracks OkHttp calls by extending [EventListener] and leaves a breadcrumb when a
* call succeeds or failed.
*
* To enable this functionality in Bugsnag call [com.bugsnag.android.Configuration.addPlugin]
* with an instance of this object before calling [com.bugsnag.android.Bugsnag.start].
*/
class BugsnagOkHttpPlugin @JvmOverloads constructor(
internal val timeProvider: () -> Long = { System.currentTimeMillis() }
) : Plugin, EventListener() {

internal val requestMap = ConcurrentHashMap<Call, NetworkRequestMetadata>()
private var client: Client? = null

override fun load(client: Client) {
this.client = client
}

override fun unload() {
this.client = null
}

override fun callStart(call: Call) {
requestMap[call] = NetworkRequestMetadata(timeProvider())
}

override fun canceled(call: Call) {
requestMap.remove(call)
}

override fun requestBodyEnd(call: Call, byteCount: Long) {
requestMap[call]?.requestBodyCount = byteCount
}

override fun responseBodyEnd(call: Call, byteCount: Long) {
requestMap[call]?.responseBodyCount = byteCount
}

override fun responseHeadersEnd(call: Call, response: Response) {
requestMap[call]?.status = response.code
}

override fun callEnd(call: Call) {
requestMap.remove(call)?.let { requestInfo ->
client?.apply {
leaveBreadcrumb(
"OkHttp call succeeded",
collateMetadata(call, requestInfo, timeProvider()),
BreadcrumbType.REQUEST
)
}
}
}

override fun callFailed(call: Call, ioe: IOException) {
requestMap.remove(call)?.let { requestInfo ->
client?.apply {
leaveBreadcrumb(
"OkHttp call failed",
collateMetadata(call, requestInfo, timeProvider()),
BreadcrumbType.REQUEST
)
}
}
}

@VisibleForTesting
internal fun Client.collateMetadata(
call: Call,
info: NetworkRequestMetadata,
nowMs: Long
): Map<String, Any> {
val request = call.request()

return mapOf(
"method" to request.method,
"url" to sanitizeUrl(request),
"duration" to nowMs - info.startTime,
"urlParams" to buildQueryParams(request),
"requestContentLength" to info.requestBodyCount,
"responseContentLength" to info.responseBodyCount,
"status" to info.status
)
}

/**
* Constructs a map of query parameters, redacting any sensitive values.
*/
private fun Client.buildQueryParams(request: Request): Map<String, String?> {
val url = request.url
val params = mutableMapOf<String, String?>()

url.queryParameterNames.forEach { name ->
params[name] = url.queryParameter(name)
}
return redactMap(params)
}

/**
* Sanitizes the URL by removing query params.
*/
private fun sanitizeUrl(request: Request): String {
val url = request.url
val builder = url.newBuilder()

url.queryParameterNames.forEach { name ->
builder.removeAllQueryParameters(name)
}
return builder.build().toString()
}
}

/**
* Stores stateful information about the in-flight network request, and contains functions
* that construct breadcrumb metadata from this information.
*/
internal class NetworkRequestMetadata(
@JvmField
val startTime: Long
) {

@JvmField
@Volatile
var status: Int = -1

@JvmField
@Volatile
var requestBodyCount: Long = -1

@JvmField
@Volatile
var responseBodyCount: Long = -1
}
Loading

0 comments on commit 265cc5a

Please sign in to comment.