Skip to content

Add auto start instrumentation #2098

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions embrace-android-sdk/api/embrace-android-sdk.api
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
public final class io/embrace/android/embracesdk/AutoStartInstrumentationHook {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally this would be under an internal package (but have public visibility) and the function would not contain an underscore

public static fun _preOnCreate (Landroid/app/Application;)V
}

public final class io/embrace/android/embracesdk/Embrace : io/embrace/android/embracesdk/internal/api/SdkApi {
public static final field Companion Lio/embrace/android/embracesdk/Embrace$Companion;
public fun activityLoaded (Landroid/app/Activity;)V
Expand Down
1 change: 1 addition & 0 deletions embrace-android-sdk/embrace-proguard.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
## Keep gradle plugin hooks
-keep class io.embrace.android.embracesdk.okhttp3.** { *; }
-keep class io.embrace.android.embracesdk.ViewSwazzledHooks { *; }
-keep class io.embrace.android.embracesdk.AutoStartInstrumentationHook { *; }
-keep class io.embrace.android.embracesdk.WebViewClientSwazzledHooks { *; }
-keep class io.embrace.android.embracesdk.WebViewChromeClientSwazzledHooks { *; }
-keep class io.embrace.android.embracesdk.fcm.swazzle.callback.com.android.fcm.FirebaseSwazzledHooks { *; }
Expand Down
24 changes: 23 additions & 1 deletion embrace-android-sdk/lint-baseline.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 8.7.3" type="baseline" client="gradle" dependencies="false" name="AGP (8.7.3)" variant="all" version="8.7.3">
<issues format="6" by="lint 8.9.1" type="baseline" client="gradle" dependencies="false" name="AGP (8.9.1)" variant="all" version="8.9.1">

<issue
id="EmbracePublicApiPackageRule"
message="Don&apos;t put classes in the io.embrace.android.embracesdk package unless they&apos;re part of the public API. Please move the new class to an appropriate package or (if you&apos;re adding to the public API) suppress this error via the lint baseline file."
errorLine1="public final class AutoStartInstrumentationHook {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/io/embrace/android/embracesdk/AutoStartInstrumentationHook.java"
line="12"
column="20"/>
</issue>

<issue
id="EmbracePublicApiPackageRule"
Expand Down Expand Up @@ -67,6 +78,17 @@
column="88"/>
</issue>

<issue
id="UnknownNullness"
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
errorLine1=" public static void _preOnCreate(android.app.Application application) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/io/embrace/android/embracesdk/AutoStartInstrumentationHook.java"
line="17"
column="37"/>
</issue>

<issue
id="UnknownNullness"
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.embrace.android.embracesdk;

import androidx.annotation.NonNull;

import io.embrace.android.embracesdk.annotation.InternalApi;
import io.embrace.android.embracesdk.internal.EmbraceInternalApi;

/**
* @hide
*/
@InternalApi
public final class AutoStartInstrumentationHook {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why Java?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't really instrumentation, but starting the SDK, so maybe call it AutoStartSdkHook or something? And are we still using the word "hook"

And technically this functionality is just wrapping SDK start, and the "auto" part is the result of doing the bytecode instrumentation. Maybe you can even call it SdkInitialization or something, as I think we'll want to add the applicationInitStart/End hooks in there too.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably worth looking at the conventions I've adopted on this integration branch for some of the bytecode instrumentation. Once 7.3.0 is out I'll merge that down: #2103


private AutoStartInstrumentationHook() {
}

public static void _preOnCreate(android.app.Application application) {
try {
Embrace.getInstance().start(application);
} catch (Exception exception) {
logError(exception);
}
}

private static void logError(@NonNull Throwable throwable) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't do anything if the SDK isn't started so IMO could be deleted, and we just swallow any unexpected exception

EmbraceInternalApi.getInstance().getInternalInterface().logInternalError(throwable);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.embrace.test.fixtures

import android.app.Application

class TestApplication : Application() {
override fun onCreate() {
super.onCreate()
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.embrace.gradle.plugin.instrumentation

import io.embrace.android.gradle.plugin.instrumentation.visitor.ApplicationClassAdapter
import io.embrace.android.gradle.plugin.instrumentation.visitor.OkHttpClassAdapter
import io.embrace.android.gradle.plugin.instrumentation.visitor.OnClickClassAdapter
import io.embrace.android.gradle.plugin.instrumentation.visitor.OnLongClickClassAdapter
Expand Down Expand Up @@ -27,6 +28,7 @@ import io.embrace.test.fixtures.MissingInterfaceOnLongClickListener
import io.embrace.test.fixtures.MissingOverrideOnClickListener
import io.embrace.test.fixtures.MissingOverrideOnLongClickListener
import io.embrace.test.fixtures.NoOverrideWebViewClient
import io.embrace.test.fixtures.TestApplication
import io.embrace.test.fixtures.VirtualMethodRefNamedOnClick
import okhttp3.OkHttpClient
import org.objectweb.asm.ClassVisitor
Expand All @@ -47,6 +49,10 @@ private val okHttpFactory: ClassVisitorFactory = { visitor ->
OkHttpClassAdapter(ASM_API_VERSION, visitor) {}
}

private val applicationFactory: ClassVisitorFactory = { visitor ->
ApplicationClassAdapter(ASM_API_VERSION, visitor)
}

/**
* Declares the test cases for bytecode in [InstrumentedBytecodeTest]. You should define the
* input class, the expected output, and the [ClassVisitor] which will instrument the bytecode.
Expand All @@ -60,15 +66,23 @@ internal fun instrumentedBytecodeTestCases(): List<BytecodeTestParams> {
.plus(onLongClickInnerTestCases)
.plus(webclientTestCases)
.plus(okHttpTestCases)
.plus(applicationTestCases)
.distinct() // filter out any unintentional duplicate test cases
.sortedBy(BytecodeTestParams::simpleClzName)
}

private val applicationTestCases = listOf(
TestApplication::class
).map {
BytecodeTestParams(it.java, factory = applicationFactory)
}

private val okHttpTestCases = listOf(
OkHttpClient.Builder::class
).map {
BytecodeTestParams(it.java, factory = okHttpFactory)
}

private val webclientTestCases = listOf(
CustomWebViewClient::class,
ExtendedCustomWebViewClient::class,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// class version 55.0 (55)
// access flags 0x31
public final class io/embrace/test/fixtures/TestApplication extends android/app/Application {

// compiled from: TestApplication.kt

@Lkotlin/Metadata;(mv={1, 8, 0}, k=1, xi=48, d1={"\u0000\u0012\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\u0008\u0002\n\u0002\u0010\u0002\n\u0000\u0018\u00002\u00020\u0001B\u0005\u00a2\u0006\u0002\u0010\u0002J\u0008\u0010\u0003\u001a\u00020\u0004H\u0016\u00a8\u0006\u0005"}, d2={"Lio/embrace/test/fixtures/TestApplication;", "Landroid/app/Application;", "()V", "onCreate", "", "embrace-bytecode-instrumentation-tests_release"})

// access flags 0x1
public <init>()V
L0
LINENUMBER 5 L0
ALOAD 0
INVOKESPECIAL android/app/Application.<init> ()V
RETURN
L1
LOCALVARIABLE this Lio/embrace/test/fixtures/TestApplication; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1

// access flags 0x1
public onCreate()V
ALOAD 0
INVOKESTATIC io/embrace/android/embracesdk/AutoStartInstrumentationHook._preOnCreate (Landroid/app/Application;)V
L0
LINENUMBER 7 L0
ALOAD 0
INVOKESPECIAL android/app/Application.onCreate ()V
L1
LINENUMBER 8 L1
RETURN
L2
LOCALVARIABLE this Lio/embrace/test/fixtures/TestApplication; L0 L2 0
MAXSTACK = 1
MAXLOCALS = 1
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.app

import android.app.Application

class ApplicationFixture : Application() {
override fun onCreate() {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class BytecodeInstrumentationTest {
"/com/example/app/OnLongClickListenerFixture",
"/okhttp3/OkHttpClient\$Builder",
"/com/example/app/FcmServiceFixture",
"/com/example/app/ApplicationFixture"
)
private val defaultArgs = listOf("-x", "lintVitalRelease")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@
"embraceHook": "invoke-static {p1}, Lio/embrace/android/embracesdk/fcm/swazzle/callback/com/android/fcm/FirebaseSwazzledHooks;->_onMessageReceived(Lcom/google/firebase/messaging/RemoteMessage;)V"
}
]
},
{
"className": "ApplicationFixture",
"methods": [
{
"signature": "onCreate()V",
"embraceHook": "invoke-static {p0}, Lio/embrace/android/embracesdk/AutoStartInstrumentationHook;->_preOnCreate(Landroid/app/Application;)V"
}
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ abstract class EmbraceBytecodeInstrumentation @Inject internal constructor(objec
val firebasePushNotificationsEnabled: Property<Boolean> =
objectFactory.property(Boolean::class.java)

/**
* Whether Embrace should automatically start in the Application class. Defaults to true.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defaulting this to true feels like a breaking change as folks will already have setup their own place where Embrace is initialized, which will probably be in a different location to where bytecode instrumentation assumes

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably default to false initially until we do a major version bump. But whether we ever flip it to true is probably something product should decide. Having the "no code start" opt-in-able via a gradle property already seems like a big win.

*/
val autoStartEnabled: Property<Boolean> = objectFactory.property(Boolean::class.java)

/**
* A list of string patterns that are used to filter classes during bytecode instrumentation. For example, 'com.example.foo.*'
* would avoid instrumenting any classes in the 'com.example.foo' package.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,6 @@ interface InstrumentationBehavior {
* A list of string regexes that are used to filter classes during bytecode instrumentation
*/
val ignoredClasses: List<String>

val autoStartEnabled: Boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,8 @@ class InstrumentationBehaviorImpl(
override val ignoredClasses: List<String> by lazy {
instrumentation.classIgnorePatterns.get().plus(extension.classSkipList.get())
}

override val autoStartEnabled: Boolean by lazy {
enabled && (instrumentation.autoStartEnabled.orNull ?: true)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class AsmTaskRegistration : EmbraceTaskRegistration {
params.shouldInstrumentOkHttp.set(behavior.instrumentation.okHttpEnabled)
params.shouldInstrumentOnLongClick.set(behavior.instrumentation.onLongClickEnabled)
params.shouldInstrumentOnClick.set(behavior.instrumentation.onClickEnabled)
params.shouldAutoStart.set(behavior.instrumentation.autoStartEnabled)

project.afterEvaluate {
// Find the Asm transformation task by name and make it depend on encodeSharedObjectFilesTask
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,7 @@ interface BytecodeInstrumentationParams : InstrumentationParameters {

@get:Input
val shouldInstrumentOnClick: Property<Boolean>

@get:Input
val shouldAutoStart: Property<Boolean>
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.embrace.android.gradle.plugin.instrumentation
import com.android.build.api.instrumentation.ClassContext
import com.android.build.api.instrumentation.InstrumentationContext
import io.embrace.android.gradle.plugin.instrumentation.config.ConfigClassVisitorFactory
import io.embrace.android.gradle.plugin.instrumentation.visitor.ApplicationClassAdapter
import io.embrace.android.gradle.plugin.instrumentation.visitor.FirebaseMessagingServiceClassAdapter
import io.embrace.android.gradle.plugin.instrumentation.visitor.OkHttpClassAdapter
import io.embrace.android.gradle.plugin.instrumentation.visitor.OnClickClassAdapter
Expand Down Expand Up @@ -31,6 +32,11 @@ internal fun createClassVisitorImpl(

// chain our own visitors to avoid unlikely (but possible) cases such as a custom
// WebViewClient implementing an OnClickListener
if (parameters.get().shouldAutoStart.get() && ApplicationClassAdapter.accept(classContext)) {
visitor = ApplicationClassAdapter(api, visitor)
logger { "Added ApplicationClassAdapter for $className." }
}

if (parameters.get().shouldInstrumentFirebaseMessaging.get() &&
FirebaseMessagingServiceClassAdapter.accept(classContext)
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,13 @@ data class SdkLocalConfig(
val appExitInfoConfig: AppExitInfoLocalConfig? = null,

@Json(name = "app_framework")
val appFramework: String? = null
val appFramework: String? = null,

/**
* Whether auto-start instrumentation should be enabled or not
*/
@Json(name = "auto_start")
val autoStart: Boolean? = null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this required as well as a property on the extension?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I would just go with property so there isn't two sources of truth. Going with the property also allows customers to use the SDK without a config file, which will make trying it out easier.

) : Serializable {

private companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.embrace.android.gradle.plugin.instrumentation.visitor

import com.android.build.api.instrumentation.ClassContext
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.MethodVisitor

/**
* Visits the Application class and returns an [ApplicationMethodAdapter] for the onCreate method.
*/
class ApplicationClassAdapter(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This current implementation only works if an Application class exists and if the onCreate function has an override. I think that would be a reasonable approach but we will need to document this on the docs site (along with the new gradle DSL property)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another approach would be to stick this in a content provider. But yeah I'd just go with this first and document it - this seems much less intrusive and likely covers most production cases anyway.

api: Int,
nextClassVisitor: ClassVisitor?,
) : ClassVisitor(api, nextClassVisitor) {

companion object : ClassVisitFilter {
private const val METHOD_NAME = "onCreate"
private const val METHOD_DESC = "()V"

override fun accept(classContext: ClassContext) = true
}

override fun visitMethod(
access: Int,
name: String,
desc: String,
signature: String?,
exceptions: Array<String>?
): MethodVisitor? {
val nextMethodVisitor = super.visitMethod(access, name, desc, signature, exceptions)

return if (METHOD_NAME == name && METHOD_DESC == desc) {
ApplicationMethodAdapter(api, nextMethodVisitor)
} else {
nextMethodVisitor
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.embrace.android.gradle.plugin.instrumentation.visitor

import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes

/**
* Visits the onCreate method and inserts a call to AutoStartInstrumentationHook._preOnCreate at the very start
* of the method. This ensures that Embrace is started before any other initialization code runs.
*/
internal class ApplicationMethodAdapter(
api: Int,
methodVisitor: MethodVisitor?
) : MethodVisitor(api, methodVisitor) {

override fun visitCode() {
// invoke AutoStartInstrumentationHook$Application._preOnCreate()
visitVarInsn(Opcodes.ALOAD, 0) // load 'this' onto the stack
visitMethodInsn(
Opcodes.INVOKESTATIC,
"io/embrace/android/embracesdk/AutoStartInstrumentationHook",
"_preOnCreate",
"(Landroid/app/Application;)V",
false
)

// call super last to reduce chance of interference with other bytecode instrumentation
super.visitCode()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class TestBytecodeInstrumentationParams(
instrumentOkHttp: Boolean = SwazzlerExtension.DEFAULT_INSTRUMENT_OKHTTP,
instrumentOnLongClick: Boolean = SwazzlerExtension.DEFAULT_INSTRUMENT_ON_LONG_CLICK,
instrumentOnClick: Boolean = SwazzlerExtension.DEFAULT_INSTRUMENT_ON_CLICK,
shouldAutoStart: Boolean = false
) : BytecodeInstrumentationParams {

override val config: Property<VariantConfig> =
Expand All @@ -42,4 +43,6 @@ class TestBytecodeInstrumentationParams(
DefaultProperty(PropertyHost.NO_OP, Boolean::class.javaObjectType).convention(instrumentOnLongClick)
override val shouldInstrumentOnClick: Property<Boolean> =
DefaultProperty(PropertyHost.NO_OP, Boolean::class.javaObjectType).convention(instrumentOnClick)
override val shouldAutoStart: Property<Boolean> =
DefaultProperty(PropertyHost.NO_OP, Boolean::class.javaObjectType).convention(shouldAutoStart)
}
Loading
Loading