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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add auto-installation for sentry-android SDK and integrations #282

Merged
merged 18 commits into from Mar 9, 2022
Merged
Show file tree
Hide file tree
Changes from 15 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
15 changes: 15 additions & 0 deletions buildSrc/src/main/java/Dependencies.kt
Expand Up @@ -60,4 +60,19 @@ object Samples {
const val compiler = "androidx.room:room-compiler:${version}"
const val rxjava = "androidx.room:room-rxjava2:${version}"
}

object OkHttp {
private const val version = "4.9.3"
romtsn marked this conversation as resolved.
Show resolved Hide resolved
const val okhttp = "com.squareup.okhttp3:okhttp:${version}"
}

object Timber {
private const val version = "5.0.1"
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
const val timber = "com.jakewharton.timber:timber:${version}"
}

object Fragment {
private const val version = "1.3.5"
const val fragmentKtx = "androidx.fragment:fragment-ktx:${version}"
}
}
4 changes: 2 additions & 2 deletions examples/android-instrumentation-sample/build.gradle.kts
Expand Up @@ -50,8 +50,6 @@ android {
// }

dependencies {
implementation(Libs.SENTRY_ANDROID)

implementation(Samples.AndroidX.recyclerView)
implementation(Samples.AndroidX.lifecycle)
implementation(Samples.AndroidX.appcompat)
Expand All @@ -63,6 +61,8 @@ dependencies {
implementation(Samples.Room.ktx)
implementation(Samples.Room.rxjava)

implementation(Samples.Timber.timber)
implementation(Samples.Fragment.fragmentKtx)
implementation(project(":examples:android-room-lib"))

kapt(Samples.Room.compiler)
Expand Down
4 changes: 4 additions & 0 deletions examples/android-room-lib/build.gradle.kts
Expand Up @@ -21,4 +21,8 @@ dependencies {

implementation(Samples.Room.runtime)
implementation(Samples.Room.ktx)

// this is here for test purposes, to ensure that transitive dependencies are also recognized
// by our auto-installation
implementation(Samples.OkHttp.okhttp)
}
Expand Up @@ -16,6 +16,8 @@ import io.sentry.android.gradle.SentryTasksProvider.getPackageBundleTask
import io.sentry.android.gradle.SentryTasksProvider.getPackageProvider
import io.sentry.android.gradle.SentryTasksProvider.getPreBundleTask
import io.sentry.android.gradle.SentryTasksProvider.getTransformerTask
import io.sentry.android.gradle.autoinstall.installDependencies
import io.sentry.android.gradle.extensions.SentryPluginExtension
import io.sentry.android.gradle.instrumentation.SpanAddingClassVisitorFactory
import io.sentry.android.gradle.services.SentrySdkStateHolder
import io.sentry.android.gradle.tasks.SentryGenerateProguardUuidTask
Expand Down Expand Up @@ -290,6 +292,8 @@ class SentryPlugin : Plugin<Project> {
project.logger.info { "uploadSentryNativeSymbols won't be executed" }
}
}

project.installDependencies(extension)
}
}

Expand All @@ -307,6 +311,7 @@ class SentryPlugin : Plugin<Project> {
companion object {
const val SENTRY_ORG_PARAMETER = "sentryOrg"
const val SENTRY_PROJECT_PARAMETER = "sentryProject"
internal const val SENTRY_SDK_VERSION = "5.6.1"
romtsn marked this conversation as resolved.
Show resolved Hide resolved

internal val sep = File.separator

Expand Down
@@ -0,0 +1,75 @@
package io.sentry.android.gradle.autoinstall

import io.sentry.android.gradle.autoinstall.fragment.FragmentInstallStrategy
import io.sentry.android.gradle.autoinstall.fragment.FragmentInstallStrategy.Registrar.SENTRY_FRAGMENT_ID
import io.sentry.android.gradle.autoinstall.okhttp.OkHttpInstallStrategy
import io.sentry.android.gradle.autoinstall.okhttp.OkHttpInstallStrategy.Registrar.SENTRY_OKHTTP_ID
import io.sentry.android.gradle.autoinstall.timber.TimberInstallStrategy
import io.sentry.android.gradle.autoinstall.timber.TimberInstallStrategy.Registrar.SENTRY_TIMBER_ID
import io.sentry.android.gradle.extensions.SentryPluginExtension
import io.sentry.android.gradle.util.info
import org.gradle.api.Project
import org.gradle.api.artifacts.DependencySet

internal const val SENTRY_GROUP = "io.sentry"
private const val SENTRY_ANDROID_ID = "sentry-android"
private const val SENTRY_ANDROID_CORE_ID = "sentry-android-core"

private val strategies = listOf(
OkHttpInstallStrategy.Registrar,
TimberInstallStrategy.Registrar,
FragmentInstallStrategy.Registrar
)

fun Project.installDependencies(extension: SentryPluginExtension) {
configurations.named("implementation").configure { configuration ->
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
configuration.withDependencies { dependencies ->
// if autoInstallation is disabled, the autoInstallState will contain initial values
// which all default to false, hence, the integrations won't be installed as well
if (extension.autoInstallation.enabled.get()) {
val sentryVersion = dependencies.findSentryAndroidVersion()
with(AutoInstallState.getInstance(gradle)) {
this.sentryVersion = installSentrySdk(sentryVersion, dependencies, extension)

installOkHttp = !dependencies.isModuleAvailable(SENTRY_OKHTTP_ID)
installTimber = !dependencies.isModuleAvailable(SENTRY_TIMBER_ID)
installFragment = !dependencies.isModuleAvailable(SENTRY_FRAGMENT_ID)
}
}
}
}
project.dependencies.components { component ->
strategies.forEach { it.register(component) }
}
}

private fun Project.installSentrySdk(
sentryVersion: String?,
dependencies: DependencySet,
extension: SentryPluginExtension
): String {
return if (sentryVersion == null) {
val userDefinedVersion = extension.autoInstallation.sentryVersion.get()
val sentryAndroidDep =
this.dependencies.create("$SENTRY_GROUP:$SENTRY_ANDROID_ID:$userDefinedVersion")
dependencies.add(sentryAndroidDep)
logger.info {
"sentry-android was successfully installed with version: $userDefinedVersion"
}
userDefinedVersion
} else {
logger.info {
"sentry-android won't be installed because it was already installed directly"
}
sentryVersion
}
}

private fun DependencySet.findSentryAndroidVersion(): String? =
find {
it.group == SENTRY_GROUP &&
(it.name == SENTRY_ANDROID_ID || it.name == SENTRY_ANDROID_CORE_ID)
}?.version

private fun DependencySet.isModuleAvailable(id: String): Boolean =
any { it.group == SENTRY_GROUP && it.name == id }
@@ -0,0 +1,79 @@
@file:Suppress("UnstableApiUsage")
romtsn marked this conversation as resolved.
Show resolved Hide resolved

package io.sentry.android.gradle.autoinstall

import io.sentry.android.gradle.SentryPlugin.Companion.SENTRY_SDK_VERSION
import java.io.Serializable
import org.gradle.api.invocation.Gradle

class AutoInstallState private constructor() : Serializable {

@get:Synchronized
@set:Synchronized
var sentryVersion: String = SENTRY_SDK_VERSION

@get:Synchronized
@set:Synchronized
var installOkHttp: Boolean = false

@get:Synchronized
@set:Synchronized
var installFragment: Boolean = false

@get:Synchronized
@set:Synchronized
var installTimber: Boolean = false

override fun toString(): String {
return "AutoInstallState(sentryVersion='$sentryVersion', " +
"installOkHttp=$installOkHttp, " +
"installFragment=$installFragment, " +
"installTimber=$installTimber)"
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as AutoInstallState

if (sentryVersion != other.sentryVersion) return false
if (installOkHttp != other.installOkHttp) return false
if (installFragment != other.installFragment) return false
if (installTimber != other.installTimber) return false

return true
}

override fun hashCode(): Int {
var result = sentryVersion.hashCode()
result = 31 * result + installOkHttp.hashCode()
result = 31 * result + installFragment.hashCode()
result = 31 * result + installTimber.hashCode()
return result
}

// We can't use Kotlin object because we need new instance on each Gradle rebuild
// But if we're inside Gradle daemon, Kotlin object will be shared between builds
companion object {
@field:Volatile
private var instance: AutoInstallState? = null

@JvmStatic
@Synchronized
fun getInstance(gradle: Gradle? = null): AutoInstallState {
if (instance != null) {
return instance!!
}

val state = AutoInstallState()
instance = state

if (gradle != null) {
BuildFinishedListenerService.getInstance(gradle).onClose { instance = null }
}

return state
}
}
}
@@ -0,0 +1,35 @@
@file:Suppress("UnstableApiUsage")

package io.sentry.android.gradle.autoinstall

import io.sentry.android.gradle.util.getBuildServiceName
import org.gradle.api.invocation.Gradle
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters

abstract class BuildFinishedListenerService :
BuildService<BuildServiceParameters.None>,
AutoCloseable {

private val actionsOnClose = mutableListOf<() -> Unit>()

fun onClose(action: () -> Unit) {
actionsOnClose.add(action)
}

override fun close() {
for (action in actionsOnClose) {
action()
}
actionsOnClose.clear()
}

companion object {
fun getInstance(gradle: Gradle): BuildFinishedListenerService {
return gradle.sharedServices.registerIfAbsent(
getBuildServiceName(BuildFinishedListenerService::class.java),
BuildFinishedListenerService::class.java
) {}.get()
}
}
}
@@ -0,0 +1,7 @@
package io.sentry.android.gradle.autoinstall

import org.gradle.api.artifacts.dsl.ComponentMetadataHandler

interface InstallStrategyRegistrar {
fun register(component: ComponentMetadataHandler)
}
@@ -0,0 +1,57 @@
package io.sentry.android.gradle.autoinstall.fragment

import io.sentry.android.gradle.SentryPlugin
import io.sentry.android.gradle.autoinstall.AutoInstallState
import io.sentry.android.gradle.autoinstall.InstallStrategyRegistrar
import io.sentry.android.gradle.autoinstall.SENTRY_GROUP
import io.sentry.android.gradle.util.info
import javax.inject.Inject
import org.gradle.api.artifacts.ComponentMetadataContext
import org.gradle.api.artifacts.ComponentMetadataRule
import org.gradle.api.artifacts.dsl.ComponentMetadataHandler
import org.slf4j.Logger

// @Inject is needed to avoid Gradle error
// @CacheableRule // TODO: make it cacheable somehow (probably depends on parameters)
abstract class FragmentInstallStrategy @Inject constructor() : ComponentMetadataRule {

private var logger: Logger = SentryPlugin.logger

constructor(logger: Logger) : this() {
this.logger = logger
}
romtsn marked this conversation as resolved.
Show resolved Hide resolved

override fun execute(context: ComponentMetadataContext) {
val autoInstallState = AutoInstallState.getInstance()
if (!autoInstallState.installFragment) {
logger.info {
"$SENTRY_FRAGMENT_ID won't be installed because it was already installed directly"
}
return
}

context.details.allVariants { metadata ->
metadata.withDependencies { dependencies ->
val sentryVersion = autoInstallState.sentryVersion
dependencies.add("$SENTRY_GROUP:$SENTRY_FRAGMENT_ID:$sentryVersion")

logger.info {
"$SENTRY_FRAGMENT_ID was successfully installed with version: $sentryVersion"
}
}
}
}

companion object Registrar : InstallStrategyRegistrar {
private const val FRAGMENT_GROUP = "androidx.fragment"
private const val FRAGMENT_ID = "fragment"
internal const val SENTRY_FRAGMENT_ID = "sentry-android-fragment"

override fun register(component: ComponentMetadataHandler) {
component.withModule(
"$FRAGMENT_GROUP:$FRAGMENT_ID",
FragmentInstallStrategy::class.java
) {}
}
}
}