Skip to content

Commit

Permalink
Add SentryOkHttpInterceptor instrumentation (#288)
Browse files Browse the repository at this point in the history
  • Loading branch information
romtsn committed Mar 10, 2022
1 parent 0fae1e9 commit 6353345
Show file tree
Hide file tree
Showing 20 changed files with 265 additions and 76 deletions.
7 changes: 4 additions & 3 deletions buildSrc/src/main/java/Dependencies.kt
Expand Up @@ -61,9 +61,10 @@ object Samples {
const val rxjava = "androidx.room:room-rxjava2:${version}"
}

object OkHttp {
private const val version = "4.9.3"
const val okhttp = "com.squareup.okhttp3:okhttp:${version}"
object Retrofit {
private const val version = "2.9.0"
const val retrofit = "com.squareup.retrofit2:retrofit:${version}"
const val retrofitGson = "com.squareup.retrofit2:converter-gson:${version}"
}

object Timber {
Expand Down
Expand Up @@ -5,12 +5,6 @@ import android.content.Context
import android.content.SharedPreferences
import androidx.room.Room
import io.sentry.samples.instrumentation.data.TracksDatabase
import io.sentry.samples.instrumentation.util.DEFAULT_LYRICS
import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch

class SampleApp : Application() {

Expand All @@ -30,18 +24,5 @@ class SampleApp : Application() {
.build()

analytics = getSharedPreferences("analytics", Context.MODE_PRIVATE)

GlobalScope.launch(Dispatchers.IO) {
database.tracksDao().all()
.collect { tracks ->
tracks.forEachIndexed { index, track ->
// add lyrics for every 2nd track
if (index % 2 == 0) {
val file = File(filesDir, "${track.id}.txt")
file.writeText(DEFAULT_LYRICS)
}
}
}
}
}
}
@@ -0,0 +1,23 @@
package io.sentry.samples.instrumentation.network

import io.sentry.samples.instrumentation.data.Track
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.create
import retrofit2.http.GET
import retrofit2.http.Path

interface TrackService {

@GET("v3/{uuid}")
suspend fun tracks(@Path("uuid") uuid: String): List<Track>

companion object {
private val retrofit = Retrofit.Builder()
.baseUrl("https://run.mocky.io/")
.addConverterFactory(GsonConverterFactory.create())
.build()

val instance = retrofit.create<TrackService>()
}
}
Expand Up @@ -11,8 +11,12 @@ import io.sentry.Sentry
import io.sentry.SpanStatus
import io.sentry.samples.instrumentation.R
import io.sentry.samples.instrumentation.SampleApp
import io.sentry.samples.instrumentation.network.TrackService
import io.sentry.samples.instrumentation.ui.list.TrackAdapter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
Expand All @@ -24,14 +28,21 @@ class MainActivity : ComponentActivity() {
list.adapter = TrackAdapter()

lifecycleScope.launchWhenStarted {
Sentry.getSpan()?.finish()
val transaction = Sentry.startTransaction(
"Track Interaction",
"ui.action.load",
true
)
SampleApp.database.tracksDao()
.all()
.map {
val remote = withContext(Dispatchers.IO) {
TrackService.instance.tracks("9365c2e9-906c-407c-851c-7204cc2975f7")
}
remote + it
}
.collect {
val transaction = Sentry.startTransaction(
"Track Interaction",
"ui.action.load",
true
)
(list.adapter as TrackAdapter).populate(it)
transaction.finish(SpanStatus.OK)
}
Expand Down

This file was deleted.

3 changes: 2 additions & 1 deletion examples/android-room-lib/build.gradle.kts
Expand Up @@ -24,5 +24,6 @@ dependencies {

// this is here for test purposes, to ensure that transitive dependencies are also recognized
// by our auto-installation
implementation(Samples.OkHttp.okhttp)
api(Samples.Retrofit.retrofit)
api(Samples.Retrofit.retrofitGson)
}
Expand Up @@ -45,7 +45,7 @@ abstract class ASMifyTask : Exec() {
private val tmpDir: String get() = "${project.buildDir}/tmp/asmified"

override fun exec() {
val asmJars = project.configurations.getByName("runtimeClasspath")
val asmJars = project.configurations.getByName("compileClasspath")
.resolvedConfiguration
.resolvedArtifacts
.filter {
Expand Down
Expand Up @@ -41,7 +41,8 @@ open class TracingInstrumentationExtension @Inject constructor(objects: ObjectFa
objects.setProperty(InstrumentationFeature::class.java).convention(
setOf(
InstrumentationFeature.DATABASE,
InstrumentationFeature.FILE_IO
InstrumentationFeature.FILE_IO,
InstrumentationFeature.OKHTTP
)
)
}
Expand All @@ -59,5 +60,13 @@ enum class InstrumentationFeature {
* This feature uses bytecode manipulation and replaces the above
* mentioned classes with Sentry-specific implementations.
*/
FILE_IO
FILE_IO,

/**
* When enabled the SDK will create spans for outgoing network requests and attach
* sentry-trace-header for distributed tracing.
* This feature uses bytecode manipulation and attaches SentryOkHttpInterceptor to all OkHttp
* clients in the project.
*/
OKHTTP
}
Expand Up @@ -9,11 +9,13 @@ import io.sentry.android.gradle.extensions.InstrumentationFeature
import io.sentry.android.gradle.instrumentation.androidx.room.AndroidXRoomDao
import io.sentry.android.gradle.instrumentation.androidx.sqlite.database.AndroidXSQLiteDatabase
import io.sentry.android.gradle.instrumentation.androidx.sqlite.statement.AndroidXSQLiteStatement
import io.sentry.android.gradle.instrumentation.okhttp.OkHttp
import io.sentry.android.gradle.instrumentation.remap.RemappingInstrumentable
import io.sentry.android.gradle.instrumentation.wrap.WrappingInstrumentable
import io.sentry.android.gradle.services.SentrySdkStateHolder
import io.sentry.android.gradle.util.SentryAndroidSdkState
import io.sentry.android.gradle.util.SentryAndroidSdkState.FILE_IO
import io.sentry.android.gradle.util.SentryAndroidSdkState.OKHTTP
import io.sentry.android.gradle.util.SentryAndroidSdkState.PERFORMANCE
import io.sentry.android.gradle.util.debug
import io.sentry.android.gradle.util.info
Expand Down Expand Up @@ -82,6 +84,7 @@ abstract class SpanAddingClassVisitorFactory :
isDatabaseInstrEnabled(sdkState, parameters.get())
},
AndroidXRoomDao().takeIf { isDatabaseInstrEnabled(sdkState, parameters.get()) },
OkHttp().takeIf { isOkHttpInstrEnabled(sdkState, parameters.get()) },
ChainedInstrumentable(
listOf(WrappingInstrumentable(), RemappingInstrumentable())
).takeIf { isFileIOInstrEnabled(sdkState, parameters.get()) }
Expand All @@ -107,6 +110,12 @@ abstract class SpanAddingClassVisitorFactory :
sdkState.isAtLeast(FILE_IO) &&
parameters.features.get().contains(InstrumentationFeature.FILE_IO)

private fun isOkHttpInstrEnabled(
sdkState: SentryAndroidSdkState,
parameters: SpanAddingParameters
): Boolean = sdkState.isAtLeast(OKHTTP) &&
parameters.features.get().contains(InstrumentationFeature.OKHTTP)

override fun createClassVisitor(
classContext: ClassContext,
nextClassVisitor: ClassVisitor
Expand Down
@@ -0,0 +1,49 @@
package io.sentry.android.gradle.instrumentation.okhttp

import com.android.build.api.instrumentation.ClassContext
import io.sentry.android.gradle.instrumentation.ClassInstrumentable
import io.sentry.android.gradle.instrumentation.CommonClassVisitor
import io.sentry.android.gradle.instrumentation.MethodContext
import io.sentry.android.gradle.instrumentation.MethodInstrumentable
import io.sentry.android.gradle.instrumentation.SpanAddingClassVisitorFactory
import io.sentry.android.gradle.instrumentation.okhttp.visitor.ResponseWithInterceptorChainMethodVisitor
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.MethodVisitor

class OkHttp : ClassInstrumentable {
override val fqName: String get() = "okhttp3.internal.connection.RealCall"

override fun getVisitor(
instrumentableContext: ClassContext,
apiVersion: Int,
originalVisitor: ClassVisitor,
parameters: SpanAddingClassVisitorFactory.SpanAddingParameters
): ClassVisitor = CommonClassVisitor(
apiVersion = apiVersion,
classVisitor = originalVisitor,
className = fqName.substringAfterLast('.'),
methodInstrumentables = listOf(ResponseWithInterceptorChain()),
parameters = parameters
)
}

class ResponseWithInterceptorChain : MethodInstrumentable {
override val fqName: String get() = "getResponseWithInterceptorChain"

override fun getVisitor(
instrumentableContext: MethodContext,
apiVersion: Int,
originalVisitor: MethodVisitor,
parameters: SpanAddingClassVisitorFactory.SpanAddingParameters
): MethodVisitor = ResponseWithInterceptorChainMethodVisitor(
api = apiVersion,
originalVisitor = originalVisitor,
access = instrumentableContext.access,
name = instrumentableContext.name,
descriptor = instrumentableContext.descriptor
)

override fun isInstrumentable(data: MethodContext): Boolean {
return data.name?.startsWith(fqName) == true
}
}
@@ -0,0 +1,95 @@
package io.sentry.android.gradle.instrumentation.okhttp.visitor

import io.sentry.android.gradle.instrumentation.util.Types
import org.objectweb.asm.Label
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes
import org.objectweb.asm.commons.GeneratorAdapter
import org.objectweb.asm.commons.Method

class ResponseWithInterceptorChainMethodVisitor(
api: Int,
private val originalVisitor: MethodVisitor,
access: Int,
name: String?,
descriptor: String?
) : GeneratorAdapter(api, originalVisitor, access, name, descriptor) {

private var shouldInstrument = false

override fun visitMethodInsn(
opcode: Int,
owner: String?,
name: String?,
descriptor: String?,
isInterface: Boolean
) {
if (opcode == Opcodes.INVOKEVIRTUAL &&
owner == "okhttp3/OkHttpClient" &&
name == "interceptors"
) {
shouldInstrument = true
}
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
}

override fun visitInsn(opcode: Int) {
super.visitInsn(opcode)
if (opcode == Opcodes.POP && shouldInstrument) {
visitAddSentryInterceptor()
shouldInstrument = false
}
}

/*
Roughly constructing this, but in Java:
if (interceptors.find { it is SentryOkHttpInterceptor } != null) {
interceptors += SentryOkHttpInterceptor()
}
*/
private fun MethodVisitor.visitAddSentryInterceptor() {
originalVisitor.visitVarInsn(Opcodes.ALOAD, 1) // interceptors list

checkCast(Types.ITERABLE)
invokeInterface(Types.ITERABLE, Method.getMethod("java.util.Iterator iterator ()"))
val iteratorIndex = newLocal(Types.ITERATOR)
storeLocal(iteratorIndex)

val whileLabel = Label()
val endWhileLabel = Label()
visitLabel(whileLabel)
loadLocal(iteratorIndex)
invokeInterface(Types.ITERATOR, Method.getMethod("boolean hasNext ()"))
ifZCmp(EQ, endWhileLabel)
loadLocal(iteratorIndex)
invokeInterface(Types.ITERATOR, Method.getMethod("Object next ()"))

val interceptorIndex = newLocal(Types.OBJECT)
storeLocal(interceptorIndex)
loadLocal(interceptorIndex)
checkCast(Types.OKHTTP_INTERCEPTOR)
instanceOf(Types.SENTRY_OKHTTP_INTERCEPTOR)
ifZCmp(EQ, whileLabel)
loadLocal(interceptorIndex)
val ifLabel = Label()
goTo(ifLabel)

visitLabel(endWhileLabel)
originalVisitor.visitInsn(Opcodes.ACONST_NULL)
visitLabel(ifLabel)
val originalMethodLabel = Label()
ifNonNull(originalMethodLabel)

originalVisitor.visitVarInsn(Opcodes.ALOAD, 1)
checkCast(Types.COLLECTION)
newInstance(Types.SENTRY_OKHTTP_INTERCEPTOR)
dup()
val sentryOkHttpCtor = Method.getMethod("void <init> ()")
invokeConstructor(Types.SENTRY_OKHTTP_INTERCEPTOR, sentryOkHttpCtor)
val addInterceptor = Method.getMethod("boolean add (Object)")
invokeInterface(Types.COLLECTION, addInterceptor)
pop()
visitLabel(originalMethodLabel)
}
}
Expand Up @@ -3,11 +3,22 @@ package io.sentry.android.gradle.instrumentation.util
import org.objectweb.asm.Type

object Types {
val SQL_EXCEPTION = Type.getType("Landroid/database/SQLException;")
val CURSOR = Type.getType("Landroid/database/Cursor;")
val SPAN = Type.getType("Lio/sentry/Span;")
// COMMON
val OBJECT = Type.getType("Ljava/lang/Object;")
val STRING = Type.getType("Ljava/lang/String;")
val EXCEPTION = Type.getType("Ljava/lang/Exception;")
val INT = Type.INT_TYPE
val EXCEPTION = Type.getType("Ljava/lang/Exception;")
val ITERABLE = Type.getType("Ljava/lang/Iterable;")
val ITERATOR = Type.getType("Ljava/util/Iterator;")
val COLLECTION = Type.getType("Ljava/util/Collection;")

// DB
val SQL_EXCEPTION = Type.getType("Landroid/database/SQLException;")
val CURSOR = Type.getType("Landroid/database/Cursor;")
val SPAN = Type.getType("Lio/sentry/Span;")

// OKHTTP
val OKHTTP_INTERCEPTOR = Type.getType("Lokhttp3/Interceptor;")
val SENTRY_OKHTTP_INTERCEPTOR =
Type.getType("Lio/sentry/android/okhttp/SentryOkHttpInterceptor;")
}
Expand Up @@ -7,6 +7,8 @@ enum class SentryAndroidSdkState(val minVersion: SemVer) : Serializable {

PERFORMANCE(SemVer(4, 0, 0)),

OKHTTP(SemVer(5, 0, 0)),

FILE_IO(SemVer(5, 5, 0));

fun isAtLeast(state: SentryAndroidSdkState): Boolean = this.ordinal >= state.ordinal
Expand All @@ -20,7 +22,8 @@ enum class SentryAndroidSdkState(val minVersion: SemVer) : Serializable {
val semVer = SemVer.parse(version)
return when {
semVer < PERFORMANCE.minVersion -> MISSING
semVer >= PERFORMANCE.minVersion && semVer < FILE_IO.minVersion -> PERFORMANCE
semVer >= PERFORMANCE.minVersion && semVer < OKHTTP.minVersion -> PERFORMANCE
semVer >= OKHTTP.minVersion && semVer < FILE_IO.minVersion -> OKHTTP
semVer >= FILE_IO.minVersion -> FILE_IO
else -> error("Unknown version $version of sentry-android")
}
Expand Down

0 comments on commit 6353345

Please sign in to comment.