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

Add SentryOkHttpInterceptor instrumentation #288

Merged
merged 8 commits into from Mar 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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) {
romtsn marked this conversation as resolved.
Show resolved Hide resolved
romtsn marked this conversation as resolved.
Show resolved Hide resolved
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
)
romtsn marked this conversation as resolved.
Show resolved Hide resolved
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")
romtsn marked this conversation as resolved.
Show resolved Hide resolved
.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