diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 0234b8d7..f7f40b61 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -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 { diff --git a/examples/android-instrumentation-sample/src/main/java/io/sentry/samples/instrumentation/SampleApp.kt b/examples/android-instrumentation-sample/src/main/java/io/sentry/samples/instrumentation/SampleApp.kt index 26531d22..31eeab1f 100644 --- a/examples/android-instrumentation-sample/src/main/java/io/sentry/samples/instrumentation/SampleApp.kt +++ b/examples/android-instrumentation-sample/src/main/java/io/sentry/samples/instrumentation/SampleApp.kt @@ -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() { @@ -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) - } - } - } - } } } diff --git a/examples/android-instrumentation-sample/src/main/java/io/sentry/samples/instrumentation/network/TrackService.kt b/examples/android-instrumentation-sample/src/main/java/io/sentry/samples/instrumentation/network/TrackService.kt new file mode 100644 index 00000000..94fa424f --- /dev/null +++ b/examples/android-instrumentation-sample/src/main/java/io/sentry/samples/instrumentation/network/TrackService.kt @@ -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 + + companion object { + private val retrofit = Retrofit.Builder() + .baseUrl("https://run.mocky.io/") + .addConverterFactory(GsonConverterFactory.create()) + .build() + + val instance = retrofit.create() + } +} diff --git a/examples/android-instrumentation-sample/src/main/java/io/sentry/samples/instrumentation/ui/MainActivity.kt b/examples/android-instrumentation-sample/src/main/java/io/sentry/samples/instrumentation/ui/MainActivity.kt index 6ebb669f..3819b171 100644 --- a/examples/android-instrumentation-sample/src/main/java/io/sentry/samples/instrumentation/ui/MainActivity.kt +++ b/examples/android-instrumentation-sample/src/main/java/io/sentry/samples/instrumentation/ui/MainActivity.kt @@ -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?) { @@ -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) } diff --git a/examples/android-instrumentation-sample/src/main/java/io/sentry/samples/instrumentation/util/Lyrics.kt b/examples/android-instrumentation-sample/src/main/java/io/sentry/samples/instrumentation/util/Lyrics.kt deleted file mode 100644 index f4ec53b8..00000000 --- a/examples/android-instrumentation-sample/src/main/java/io/sentry/samples/instrumentation/util/Lyrics.kt +++ /dev/null @@ -1,14 +0,0 @@ -package io.sentry.samples.instrumentation.util - -val DEFAULT_LYRICS = - """ -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi dapibus elit vel commodo varius. Suspendisse eget tempus est. Pellentesque egestas mi vitae massa ultrices vulputate. Aenean tempor nec sem eu congue. Phasellus mollis tellus odio, ut tincidunt arcu ullamcorper at. Nunc quis lorem vel risus auctor ultricies. Quisque ornare congue sagittis. Donec sit amet arcu vitae mi sodales porta vitae et mi. Nullam eu viverra urna, quis elementum risus. In at accumsan justo. Nam hendrerit, lorem ac lacinia semper, diam dolor pulvinar turpis, non tincidunt dolor lacus non quam. Etiam tempus, dui ut rutrum ornare, quam eros feugiat nisi, et blandit nisi lacus nec neque. Mauris aliquam id tellus vel molestie. Quisque ultricies ante nec lacus condimentum, at aliquet diam volutpat. - -Vivamus fermentum eu ante non aliquam. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum interdum semper orci, et auctor enim pharetra vel. Ut porttitor neque in blandit scelerisque. Cras fermentum urna sit amet metus tincidunt, eu porttitor nisi mattis. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Mauris pharetra leo vitae turpis commodo, sit amet cursus nunc varius. Etiam bibendum interdum dolor, a scelerisque massa mattis eu. Suspendisse consequat tortor ac tincidunt vulputate. - -Maecenas non lectus sit amet dui porta dapibus. Etiam vitae velit nibh. Morbi purus urna, cursus convallis feugiat vel, vulputate eget turpis. Vivamus et tincidunt lectus. Nullam id risus est. Sed ullamcorper pellentesque massa, dignissim ultricies urna maximus ac. Vivamus maximus eu ex quis ultricies. Donec facilisis diam arcu, id cursus sapien condimentum ac. Aenean suscipit, nibh ac tempus pharetra, ligula augue bibendum arcu, sed fringilla magna nisl vel felis. - -Cras lacinia efficitur elit, ac sagittis nulla commodo eu. Aliquam a est at augue imperdiet eleifend. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Maecenas luctus est non vulputate porta. Phasellus eros libero, auctor id justo ac, mollis eleifend enim. Suspendisse pulvinar mi lorem, a maximus massa tincidunt ac. Duis mattis nibh a mauris laoreet laoreet. Donec malesuada quis dui feugiat ultricies. Ut at sem consectetur, tincidunt massa in, bibendum metus. - -Sed fermentum eros ac odio venenatis, id varius est mollis. Fusce sollicitudin tellus risus, vel accumsan ante auctor in. Phasellus vel blandit massa. Suspendisse potenti. Donec vitae elementum enim, quis dictum mauris. Nullam consectetur enim at turpis bibendum, consequat facilisis sem ultrices. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam eget nisl id libero tempus fringilla vitae eget urna. Etiam dui neque, vestibulum a mi vulputate, placerat interdum sem. Mauris sit amet aliquam felis. Suspendisse potenti. Nullam odio purus, ultricies in tincidunt sit amet, interdum ac orci. Nulla volutpat auctor velit, sed malesuada urna auctor sed. Suspendisse non sollicitudin tortor. Vivamus vulputate lacinia nisi, pellentesque sagittis sapien. - """.trimIndent() diff --git a/examples/android-room-lib/build.gradle.kts b/examples/android-room-lib/build.gradle.kts index a7168ada..125e5e21 100644 --- a/examples/android-room-lib/build.gradle.kts +++ b/examples/android-room-lib/build.gradle.kts @@ -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) } diff --git a/plugin-build/buildSrc/src/main/kotlin/io/sentry/android/gradle/internal/ASMifyTask.kt b/plugin-build/buildSrc/src/main/kotlin/io/sentry/android/gradle/internal/ASMifyTask.kt index 875bcc72..9bf0576d 100644 --- a/plugin-build/buildSrc/src/main/kotlin/io/sentry/android/gradle/internal/ASMifyTask.kt +++ b/plugin-build/buildSrc/src/main/kotlin/io/sentry/android/gradle/internal/ASMifyTask.kt @@ -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 { diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/TracingInstrumentationExtension.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/TracingInstrumentationExtension.kt index ecc4a78d..7a0986db 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/TracingInstrumentationExtension.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/TracingInstrumentationExtension.kt @@ -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 ) ) } @@ -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 } diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/SpanAddingClassVisitorFactory.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/SpanAddingClassVisitorFactory.kt index 12c8ba20..38aa275e 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/SpanAddingClassVisitorFactory.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/SpanAddingClassVisitorFactory.kt @@ -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 @@ -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()) } @@ -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 diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/okhttp/OkHttp.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/okhttp/OkHttp.kt new file mode 100644 index 00000000..e45e583b --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/okhttp/OkHttp.kt @@ -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 + } +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/okhttp/visitor/ResponseWithInterceptorChainMethodVisitor.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/okhttp/visitor/ResponseWithInterceptorChainMethodVisitor.kt new file mode 100644 index 00000000..dfff1a8f --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/okhttp/visitor/ResponseWithInterceptorChainMethodVisitor.kt @@ -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 ()") + invokeConstructor(Types.SENTRY_OKHTTP_INTERCEPTOR, sentryOkHttpCtor) + val addInterceptor = Method.getMethod("boolean add (Object)") + invokeInterface(Types.COLLECTION, addInterceptor) + pop() + visitLabel(originalMethodLabel) + } +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/util/Types.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/util/Types.kt index b20a19fd..bc3a3eab 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/util/Types.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/util/Types.kt @@ -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;") } diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/util/SentryAndroidSdkState.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/util/SentryAndroidSdkState.kt index 6196cce2..02d834a8 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/util/SentryAndroidSdkState.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/util/SentryAndroidSdkState.kt @@ -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 @@ -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") } diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/VisitorTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/VisitorTest.kt index 1538ace1..e477a15f 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/VisitorTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/VisitorTest.kt @@ -8,6 +8,7 @@ import io.sentry.android.gradle.instrumentation.classloader.GeneratingMissingCla import io.sentry.android.gradle.instrumentation.fakes.TestClassContext import io.sentry.android.gradle.instrumentation.fakes.TestClassData import io.sentry.android.gradle.instrumentation.fakes.TestSpanAddingParameters +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 java.io.FileInputStream @@ -134,7 +135,9 @@ class VisitorTest( "zzhm", ChainedInstrumentable(listOf(WrappingInstrumentable(), RemappingInstrumentable())), null - ) + ), + arrayOf("okhttp/v3", "RealCall", OkHttp(), null), + arrayOf("okhttp/v4", "RealCall", OkHttp(), null) ) private fun roomDaoTestParameters(suffix: String = "") = arrayOf( diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/classloader/GeneratingMissingClassesClassLoader.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/classloader/GeneratingMissingClassesClassLoader.kt index 6781366c..a9d2323c 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/classloader/GeneratingMissingClassesClassLoader.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/classloader/GeneratingMissingClassesClassLoader.kt @@ -1,8 +1,8 @@ package io.sentry.android.gradle.instrumentation.classloader import io.sentry.android.gradle.instrumentation.classloader.mapping.deletionDaoMissingClasses -import io.sentry.android.gradle.instrumentation.classloader.mapping.gmsMapping import io.sentry.android.gradle.instrumentation.classloader.mapping.insertionDaoMissingClasses +import io.sentry.android.gradle.instrumentation.classloader.mapping.okHttpMissingClasses import io.sentry.android.gradle.instrumentation.classloader.mapping.selectDaoMissingClasses import io.sentry.android.gradle.instrumentation.classloader.mapping.sqliteCopyOpenHelperMissingClasses import io.sentry.android.gradle.instrumentation.classloader.mapping.updateDaoMissingClasses @@ -16,17 +16,28 @@ class GeneratingMissingClassesClassLoader : ClassLoader(getSystemClassLoader()) *updateDaoMissingClasses, *selectDaoMissingClasses, *sqliteCopyOpenHelperMissingClasses, - *gmsMapping + *okHttpMissingClasses ) } override fun findClass(name: String): Class<*> { if (name in missingClasses) { - val fqName = name.replace('.', '/') - val source = missingClasses[name]!!.invoke(name) - val bytes = compileClass(fqName, source) - return defineClass(name, bytes.toByteArray(), 0, bytes.size()) + return generateClass(name, missingClasses[name]!!.invoke(name)) } - return super.findClass(name) + + return try { + super.findClass(name) + } catch (e: ClassNotFoundException) { + generateClass(name) + } + } + + private fun generateClass( + name: String, + source: String = standardClassSource(name) + ): Class<*> { + val fqName = name.replace('.', '/') + val bytes = compileClass(fqName, source) + return defineClass(name, bytes.toByteArray(), 0, bytes.size()) } } diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/classloader/mapping/GmsMapping.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/classloader/mapping/GmsMapping.kt deleted file mode 100644 index 87db19fd..00000000 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/classloader/mapping/GmsMapping.kt +++ /dev/null @@ -1,18 +0,0 @@ -package io.sentry.android.gradle.instrumentation.classloader.mapping - -import io.sentry.android.gradle.instrumentation.classloader.standardClassSource - -val gmsMapping = arrayOf String>>( - "com.google.android.gms.internal.measurement.zzhz" to { name -> - standardClassSource( - name, - interfaces = arrayOf("android.app.Application.ActivityLifecycleCallbacks") - ) - }, - "com.google.android.gms.internal.measurement.zzhi" to { name -> - standardClassSource( - name, - interfaces = arrayOf("java.lang.Runnable") - ) - } -) diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/classloader/mapping/OkHttpMapping.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/classloader/mapping/OkHttpMapping.kt new file mode 100644 index 00000000..ce874db7 --- /dev/null +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/classloader/mapping/OkHttpMapping.kt @@ -0,0 +1,9 @@ +package io.sentry.android.gradle.instrumentation.classloader.mapping + +import io.sentry.android.gradle.instrumentation.classloader.standardClassSource + +val okHttpMissingClasses = arrayOf String>>( + "okhttp3.internal.http.RealInterceptorChain" to { name -> + standardClassSource(name, interfaces = arrayOf("okhttp3.Interceptor.Chain")) + } +) diff --git a/plugin-build/src/test/kotlin/okhttp3/Interceptor.kt b/plugin-build/src/test/kotlin/okhttp3/Interceptor.kt new file mode 100644 index 00000000..bd703cd7 --- /dev/null +++ b/plugin-build/src/test/kotlin/okhttp3/Interceptor.kt @@ -0,0 +1,5 @@ +package okhttp3 + +interface Interceptor { + interface Chain +} diff --git a/plugin-build/src/test/resources/testFixtures/instrumentation/okhttp/v3/RealCall.class b/plugin-build/src/test/resources/testFixtures/instrumentation/okhttp/v3/RealCall.class new file mode 100644 index 00000000..f6672d78 Binary files /dev/null and b/plugin-build/src/test/resources/testFixtures/instrumentation/okhttp/v3/RealCall.class differ diff --git a/plugin-build/src/test/resources/testFixtures/instrumentation/okhttp/v4/RealCall.class b/plugin-build/src/test/resources/testFixtures/instrumentation/okhttp/v4/RealCall.class new file mode 100644 index 00000000..c84967a1 Binary files /dev/null and b/plugin-build/src/test/resources/testFixtures/instrumentation/okhttp/v4/RealCall.class differ