Skip to content

Commit

Permalink
Merge pull request #211 from DataDog/nogorodnikov/rum-1822/inject-bui…
Browse files Browse the repository at this point in the history
…ld-id-into-source-map-upload

RUM-1822: Inject build ID into mapping file upload and SDK host application as well
  • Loading branch information
0xnm authored Dec 8, 2023
2 parents bfff839 + c4ace3b commit 0fd0071
Show file tree
Hide file tree
Showing 11 changed files with 662 additions and 204 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,21 @@ import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.logging.Logging
import org.gradle.api.provider.Provider
import org.gradle.api.provider.ProviderFactory
import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.TaskProvider
import org.gradle.process.ExecOperations
import java.io.File
import javax.inject.Inject
import kotlin.io.path.Path

/**
* Plugin adding tasks for Android projects using Datadog's SDK for Android.
*/
@Suppress("TooManyFunctions")
class DdAndroidGradlePlugin @Inject constructor(
private val execOps: ExecOperations
private val execOps: ExecOperations,
private val providerFactory: ProviderFactory
) : Plugin<Project> {

// region Plugin
Expand All @@ -37,17 +41,29 @@ class DdAndroidGradlePlugin @Inject constructor(
val extension = target.extensions.create(EXT_NAME, DdExtension::class.java)
val apiKey = resolveApiKey(target)

// need to use withPlugin instead of afterEvaluate, because otherwise generated assets
// folder with buildId is not picked by AGP by some reason
target.pluginManager.withPlugin("com.android.application") {
val androidExtension = target.androidApplicationExtension ?: return@withPlugin
androidExtension.applicationVariants.all { variant ->
if (extension.enabled) {
configureTasksForVariant(
target,
androidExtension,
extension,
variant,
apiKey
)
}
}
}

target.afterEvaluate {
val androidExtension = target.extensions.findByType(AppExtension::class.java)
val androidExtension = target.androidApplicationExtension
if (androidExtension == null) {
LOGGER.error(ERROR_NOT_ANDROID)
} else if (!extension.enabled) {
LOGGER.info("Datadog extension disabled, no upload task created")
} else {
androidExtension.applicationVariants.all { variant ->
configureVariantForUploadTask(target, variant, apiKey, extension)
configureVariantForSdkCheck(target, variant, extension)
}
}
}
}
Expand All @@ -56,6 +72,30 @@ class DdAndroidGradlePlugin @Inject constructor(

// region Internal

internal fun configureTasksForVariant(
target: Project,
androidExtension: AppExtension,
datadogExtension: DdExtension,
variant: ApplicationVariant,
apiKey: ApiKey
) {
if (isObfuscationEnabled(variant, datadogExtension)) {
val buildIdGenerationTask =
configureBuildIdGenerationTask(target, androidExtension, variant)

configureVariantForUploadTask(
target,
variant,
buildIdGenerationTask,
apiKey,
datadogExtension
)
} else {
LOGGER.info("Minifying disabled for variant ${variant.name}, no upload task created")
}
configureVariantForSdkCheck(target, variant, datadogExtension)
}

@Suppress("ReturnCount")
// TODO RUMM-2382 use ProviderFactory/Provider APIs to watch changes in external environment
internal fun resolveApiKey(target: Project): ApiKey {
Expand All @@ -69,23 +109,52 @@ class DdAndroidGradlePlugin @Inject constructor(
return apiKey ?: ApiKey.NONE
}

@Suppress("StringLiteralDuplication")
internal fun configureBuildIdGenerationTask(
target: Project,
appExtension: AppExtension,
variant: ApplicationVariant
): TaskProvider<GenerateBuildIdTask> {
val buildIdDirectory = target.layout.buildDirectory
.dir(Path("generated", "datadog", "buildId", variant.name).toString())
val buildIdGenerationTask = GenerateBuildIdTask.register(target, variant, buildIdDirectory)

// we could generate buildIdDirectory inside GenerateBuildIdTask and read it here as
// property using flatMap, but when Gradle sync is done inside Android Studio there is an error
// Querying the mapped value of provider (java.util.Set) before task ... has completed is
// not supported, which doesn't happen when Android Studio is not used (pure Gradle build)
// so applying such workaround
// TODO RUM-0000 use new AndroidComponents API to inject generated stuff, it is more friendly
appExtension.sourceSets.getByName(variant.name).assets.srcDir(buildIdDirectory)

val variantName = variant.name.capitalize()
listOf(
"package${variantName}Bundle",
"build${variantName}PreBundle",
"lintVitalAnalyze$variantName"
).forEach {
target.tasks.findByName(it)?.dependsOn(buildIdGenerationTask)
}

listOf(
variant.packageApplicationProvider,
variant.mergeAssetsProvider
).forEach {
it.configure { it.dependsOn(buildIdGenerationTask) }
}

return buildIdGenerationTask
}

@Suppress("DefaultLocale", "ReturnCount")
internal fun configureVariantForUploadTask(
target: Project,
variant: ApplicationVariant,
buildIdGenerationTask: TaskProvider<GenerateBuildIdTask>,
apiKey: ApiKey,
extension: DdExtension
): Task? {
): Task {
val extensionConfiguration = resolveExtensionConfiguration(extension, variant)
val isDefaultObfuscationEnabled = variant.buildType.isMinifyEnabled
val isNonDefaultObfuscationEnabled = extensionConfiguration.nonDefaultObfuscation
val isObfuscationEnabled = isDefaultObfuscationEnabled || isNonDefaultObfuscationEnabled

if (!isObfuscationEnabled) {
LOGGER.info("Minifying disabled for variant ${variant.name}, no upload task created")
return null
}

val flavorName = variant.flavorName
val uploadTaskName = UPLOAD_TASK_NAME + variant.name.capitalize()
// TODO RUMM-2382 use tasks.register
Expand All @@ -97,6 +166,15 @@ class DdAndroidGradlePlugin @Inject constructor(

configureVariantTask(uploadTask, apiKey, flavorName, extensionConfiguration, variant)

// upload task shouldn't depend on the build ID generation task, but only read its property,
// because upload task may be triggered after assemble task and we don't want to re-generate
// build ID, because it will be different then from the one which is already embedded in
// the application package
uploadTask.buildId = buildIdGenerationTask.flatMap {
it.buildIdFile.flatMap {
providerFactory.provider { it.asFile.readText().trim() }
}
}
uploadTask.mappingFilePath = resolveMappingFilePath(extensionConfiguration, target, variant)
uploadTask.mappingFilePackagesAliases =
filterMappingFileReplacements(
Expand Down Expand Up @@ -199,6 +277,7 @@ class DdAndroidGradlePlugin @Inject constructor(
}
}

@Suppress("StringLiteralDuplication")
private fun resolveDatadogRepositoryFile(target: Project): File {
val outputsDir = File(target.buildDir, "outputs")
val reportsDir = File(outputsDir, "reports")
Expand Down Expand Up @@ -239,6 +318,7 @@ class DdAndroidGradlePlugin @Inject constructor(

uploadTask.site = extensionConfiguration.site ?: ""
uploadTask.versionName = extensionConfiguration.versionName ?: variant.versionName
uploadTask.versionCode = variant.versionCode
uploadTask.serviceName = extensionConfiguration.serviceName ?: variant.applicationId
uploadTask.remoteRepositoryUrl = extensionConfiguration.remoteRepositoryUrl ?: ""
}
Expand Down Expand Up @@ -280,6 +360,19 @@ class DdAndroidGradlePlugin @Inject constructor(
return findProperty(propertyName)?.toString()
}

private fun isObfuscationEnabled(
variant: ApplicationVariant,
extension: DdExtension
): Boolean {
val extensionConfiguration = resolveExtensionConfiguration(extension, variant)
val isDefaultObfuscationEnabled = variant.buildType.isMinifyEnabled
val isNonDefaultObfuscationEnabled = extensionConfiguration.nonDefaultObfuscation
return isDefaultObfuscationEnabled || isNonDefaultObfuscationEnabled
}

private val Project.androidApplicationExtension: AppExtension?
get() = extensions.findByType(AppExtension::class.java)

// endregion

companion object {
Expand All @@ -288,11 +381,13 @@ class DdAndroidGradlePlugin @Inject constructor(

internal const val DATADOG_API_KEY = "DATADOG_API_KEY"

internal const val DATADOG_TASK_GROUP = "datadog"

internal val LOGGER = Logging.getLogger("DdAndroidGradlePlugin")

private const val EXT_NAME = "datadog"

private const val UPLOAD_TASK_NAME = "uploadMapping"
internal const val UPLOAD_TASK_NAME = "uploadMapping"

private const val ERROR_NOT_ANDROID = "The dd-android-gradle-plugin has been applied on " +
"a non android application project"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ abstract class DdCheckSdkDepsTask : DefaultTask() {
internal var isLastRunSuccessful: Boolean = true

init {
group = "datadog"
group = DdAndroidGradlePlugin.DATADOG_TASK_GROUP
description = "Checks for the Datadog SDK into your variant dependencies."
outputs.upToDateWhen { it is DdCheckSdkDepsTask && it.isLastRunSuccessful }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ open class DdMappingFileUploadTask
@get:Input
var versionName: String = ""

/**
* The version code of the application.
*/
@get:Input
var versionCode: Int = 0

/**
* The service name of the application (by default, it is your app's package name).
*/
Expand All @@ -85,6 +91,12 @@ open class DdMappingFileUploadTask
@get:Input
var remoteRepositoryUrl: String = ""

/**
* Build ID which will be used for mapping file matching.
*/
@get:Input
var buildId: Provider<String> = providerFactory.provider { "" }

/**
* The path to the mapping file to upload.
*/
Expand Down Expand Up @@ -123,7 +135,7 @@ open class DdMappingFileUploadTask
var repositoryFile: File = File("")

init {
group = "datadog"
group = DdAndroidGradlePlugin.DATADOG_TASK_GROUP
description = "Uploads the Proguard/R8 mapping file to Datadog"
// it is never up-to-date, because request may fail
outputs.upToDateWhen { false }
Expand All @@ -143,7 +155,11 @@ open class DdMappingFileUploadTask
validateConfiguration()

check(!(apiKey.contains("\"") || apiKey.contains("'"))) {
"DD_API_KEY provided shouldn't contain quotes or apostrophes."
INVALID_API_KEY_FORMAT_ERROR
}

check(buildId.isPresent && buildId.get().isNotEmpty()) {
MISSING_BUILD_ID_ERROR
}

var mappingFile = File(mappingFilePath)
Expand All @@ -168,7 +184,9 @@ open class DdMappingFileUploadTask
DdAppIdentifier(
serviceName = serviceName,
version = versionName,
variant = variantName
versionCode = versionCode,
variant = variantName,
buildId = buildId.get()
),
repositories.firstOrNull(),
!disableGzipOption.isPresent
Expand Down Expand Up @@ -247,11 +265,7 @@ open class DdMappingFileUploadTask

@Suppress("CheckInternal")
private fun validateConfiguration() {
check(apiKey.isNotBlank()) {
"Make sure you define an API KEY to upload your mapping files to Datadog. " +
"Create a DD_API_KEY or DATADOG_API_KEY environment variable, gradle" +
" property or define it in datadog-ci.json file."
}
check(apiKey.isNotBlank()) { API_KEY_MISSING_ERROR }

if (site.isBlank()) {
site = DatadogSite.US1.name
Expand Down Expand Up @@ -375,5 +389,13 @@ open class DdMappingFileUploadTask
private const val DATADOG_CI_SITE_PROPERTY = "datadogSite"
const val DATADOG_SITE = "DATADOG_SITE"
const val DISABLE_GZIP_GRADLE_PROPERTY = "dd-disable-gzip"

const val API_KEY_MISSING_ERROR = "Make sure you define an API KEY to upload your mapping files to Datadog. " +
"Create a DD_API_KEY or DATADOG_API_KEY environment variable, gradle" +
" property or define it in datadog-ci.json file."
const val INVALID_API_KEY_FORMAT_ERROR =
"DD_API_KEY provided shouldn't contain quotes or apostrophes."
const val MISSING_BUILD_ID_ERROR =
"Build ID is missing, you need to run upload task only after APK/AAB file is generated."
}
}
Loading

0 comments on commit 0fd0071

Please sign in to comment.