diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b725d22..9b09163b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- (Experimental) Add snapshot upload support via `sentryUploadSnapshots` task ([#1091](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1091)) + ### Dependencies - Bump CLI from v3.2.0 to v3.2.3 ([#1084](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1084), [#1090](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1090)) diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentryPlugin.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentryPlugin.kt index 2582c78f..838a48ef 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentryPlugin.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentryPlugin.kt @@ -4,6 +4,7 @@ import com.android.build.api.variant.ApplicationAndroidComponentsExtension import io.sentry.BuildConfig import io.sentry.android.gradle.autoinstall.installDependencies import io.sentry.android.gradle.extensions.SentryPluginExtension +import io.sentry.android.gradle.tasks.SentryUploadSnapshotsTask import io.sentry.android.gradle.util.AgpVersions import java.io.File import javax.inject.Inject @@ -65,6 +66,14 @@ constructor(private val buildEvents: BuildEventListenerRegistryInternal) : Plugi sentryProjectParameter, ) + SentryUploadSnapshotsTask.register( + project, + extension, + cliExecutable, + sentryOrgParameter, + sentryProjectParameter, + ) + project.installDependencies(extension, true) } } diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentryPropertiesFileProvider.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentryPropertiesFileProvider.kt index e7447351..e380900f 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentryPropertiesFileProvider.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentryPropertiesFileProvider.kt @@ -18,9 +18,9 @@ internal object SentryPropertiesFileProvider { * @return A [String] for the path if sentry.properties is found or null otherwise */ @JvmStatic - fun getPropertiesFilePath(project: Project, variant: SentryVariant): String? { - val flavorName = variant.flavorName.orEmpty() - val buildTypeName = variant.buildTypeName.orEmpty() + fun getPropertiesFilePath(project: Project, variant: SentryVariant? = null): String? { + val flavorName = variant?.flavorName.orEmpty() + val buildTypeName = variant?.buildTypeName.orEmpty() val projDir = project.projectDir val rootDir = project.rootDir @@ -28,31 +28,43 @@ internal object SentryPropertiesFileProvider { val sep = File.separator // Local Project dirs - val possibleFiles = mutableListOf("${projDir}${sep}src${sep}${buildTypeName}${sep}$FILENAME") + val possibleFiles = mutableListOf() + if (buildTypeName.isNotBlank()) { + possibleFiles.add("${projDir}${sep}src${sep}${buildTypeName}${sep}$FILENAME") + } if (flavorName.isNotBlank()) { - possibleFiles.add("${projDir}${sep}src${sep}${buildTypeName}${sep}$flavorName${sep}$FILENAME") - possibleFiles.add( - "${projDir}${sep}src${sep}${flavorName}${sep}${buildTypeName}${sep}$FILENAME" - ) + if (buildTypeName.isNotBlank()) { + possibleFiles.add( + "${projDir}${sep}src${sep}${buildTypeName}${sep}$flavorName${sep}$FILENAME" + ) + possibleFiles.add( + "${projDir}${sep}src${sep}${flavorName}${sep}${buildTypeName}${sep}$FILENAME" + ) + } possibleFiles.add("${projDir}${sep}src${sep}${flavorName}${sep}$FILENAME") } possibleFiles.add("${projDir}${sep}$FILENAME") // Other flavors dirs possibleFiles.addAll( - variant.productFlavors.map { "${projDir}${sep}src${sep}${it}${sep}$FILENAME" } + variant?.productFlavors?.map { "${projDir}${sep}src${sep}${it}${sep}$FILENAME" } + ?: emptyList() ) // Root project dirs - possibleFiles.add("${rootDir}${sep}src${sep}${buildTypeName}${sep}$FILENAME") + if (buildTypeName.isNotBlank()) { + possibleFiles.add("${rootDir}${sep}src${sep}${buildTypeName}${sep}$FILENAME") + } if (flavorName.isNotBlank()) { possibleFiles.add("${rootDir}${sep}src${sep}${flavorName}${sep}$FILENAME") - possibleFiles.add( - "${rootDir}${sep}src${sep}${buildTypeName}${sep}${flavorName}${sep}$FILENAME" - ) - possibleFiles.add( - "${rootDir}${sep}src${sep}${flavorName}${sep}${buildTypeName}${sep}$FILENAME" - ) + if (buildTypeName.isNotBlank()) { + possibleFiles.add( + "${rootDir}${sep}src${sep}${buildTypeName}${sep}${flavorName}${sep}$FILENAME" + ) + possibleFiles.add( + "${rootDir}${sep}src${sep}${flavorName}${sep}${buildTypeName}${sep}$FILENAME" + ) + } } possibleFiles.add("${rootDir}${sep}$FILENAME") diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/SentryPluginExtension.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/SentryPluginExtension.kt index 377e1a15..6d9ca9c6 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/SentryPluginExtension.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/SentryPluginExtension.kt @@ -127,6 +127,14 @@ abstract class SentryPluginExtension @Inject constructor(project: Project) { vcsInfoAction.execute(vcsInfo) } + val snapshots: SnapshotsExtension = objects.newInstance(SnapshotsExtension::class.java) + + /** Configure the snapshots upload. */ + @Experimental + fun snapshots(snapshotsAction: Action) { + snapshotsAction.execute(snapshots) + } + /** * Disables or enables the reporting of dependencies metadata for Sentry. If enabled the plugin * will collect external dependencies and will take care of uploading them to Sentry as part of diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/SnapshotsExtension.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/SnapshotsExtension.kt new file mode 100644 index 00000000..01542c7a --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/SnapshotsExtension.kt @@ -0,0 +1,17 @@ +package io.sentry.android.gradle.extensions + +import javax.inject.Inject +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.jetbrains.annotations.ApiStatus.Experimental + +@Experimental +open class SnapshotsExtension @Inject constructor(objects: ObjectFactory) { + + /** The application identifier used to associate snapshots with an app. */ + val appId: Property = objects.property(String::class.java) + + /** The path to the folder containing snapshots to upload. */ + val path: DirectoryProperty = objects.directoryProperty() +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/tasks/SentryUploadSnapshotsTask.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/tasks/SentryUploadSnapshotsTask.kt new file mode 100644 index 00000000..6a062092 --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/tasks/SentryUploadSnapshotsTask.kt @@ -0,0 +1,72 @@ +package io.sentry.android.gradle.tasks + +import io.sentry.android.gradle.SentryPropertiesFileProvider.getPropertiesFilePath +import io.sentry.android.gradle.autoinstall.SENTRY_GROUP +import io.sentry.android.gradle.extensions.SentryPluginExtension +import io.sentry.android.gradle.util.asSentryCliExec +import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskProvider +import org.gradle.work.DisableCachingByDefault + +@DisableCachingByDefault(because = "Uploads should not be cached") +abstract class SentryUploadSnapshotsTask : SentryCliExecTask() { + + init { + group = SENTRY_GROUP + description = "Uploads snapshots to Sentry" + } + + @get:Input abstract val appId: Property + + @get:InputDirectory + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val snapshotsPath: DirectoryProperty + + override fun getArguments(args: MutableList) { + args.add("build") + args.add("snapshots") + args.add("--app-id") + args.add(appId.get()) + args.add(snapshotsPath.get().asFile.absolutePath) + } + + companion object { + fun register( + project: Project, + extension: SentryPluginExtension, + cliExecutable: Provider, + sentryOrgOverride: String?, + sentryProjectOverride: String?, + ): TaskProvider { + return project.tasks.register( + "sentryUploadSnapshots", + SentryUploadSnapshotsTask::class.java, + ) { task -> + task.workingDir(project.rootDir) + task.debug.set(extension.debug) + task.cliExecutable.set(cliExecutable) + task.sentryProperties.set( + getPropertiesFilePath(project)?.let { file -> project.file(file) } + ) + task.sentryOrganization.set( + sentryOrgOverride?.let { project.provider { it } } ?: extension.org + ) + task.sentryProject.set( + sentryProjectOverride?.let { project.provider { it } } ?: extension.projectName + ) + task.sentryAuthToken.set(extension.authToken) + task.sentryUrl.set(extension.url) + task.appId.set(extension.snapshots.appId) + task.snapshotsPath.set(extension.snapshots.path) + task.asSentryCliExec() + } + } + } +} diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPropertiesFileProviderTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPropertiesFileProviderTest.kt index 0d4639d3..39f0d533 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPropertiesFileProviderTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPropertiesFileProviderTest.kt @@ -177,6 +177,28 @@ class SentryPropertiesFileProviderTest(private val agpVersion: SemVer) { assertEquals("42", File(getPropertiesFilePath(project, variant)!!).readText()) } + @Test + fun `getPropertiesFilePath with null variant finds file inside project folder`() { + val (project, _) = createTestAndroidProject(forceEvaluate = !AgpVersions.isAGP74(agpVersion)) + createTestFile(project.projectDir, "sentry.properties") + + assertEquals("42", File(getPropertiesFilePath(project)!!).readText()) + } + + @Test + fun `getPropertiesFilePath with null variant skips buildType paths`() { + val rootProject = ProjectBuilder.builder().build() + val (project, _) = + createTestAndroidProject( + parent = rootProject, + forceEvaluate = !AgpVersions.isAGP74(agpVersion), + ) + // Only place the file under src/debug/ — with null variant this should not be found + createTestFile(project.projectDir, "src${sep}debug${sep}sentry.properties") + + assertEquals(null, getPropertiesFilePath(project)) + } + @Test fun `getPropertiesFilePath finds file inside root buildType flavor folder`() { val rootProject = ProjectBuilder.builder().build() diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/tasks/SentryUploadSnapshotsTaskTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/tasks/SentryUploadSnapshotsTaskTest.kt new file mode 100644 index 00000000..5955b08b --- /dev/null +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/tasks/SentryUploadSnapshotsTaskTest.kt @@ -0,0 +1,140 @@ +package io.sentry.android.gradle.tasks + +import java.io.File +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue +import org.gradle.api.Project +import org.gradle.testfixtures.ProjectBuilder +import org.junit.Test + +class SentryUploadSnapshotsTaskTest { + + @Test + fun `cli-executable is set correctly`() { + val task = createTestTask { + it.cliExecutable.set("sentry-cli") + it.appId.set("com.example") + it.snapshotsPath.set(File("/path/to/snapshots")) + } + + val args = task.computeCommandLineArgs() + + assertTrue("sentry-cli" in args) + assertTrue("build" in args) + assertTrue("snapshots" in args) + assertTrue("--app-id" in args) + assertTrue("com.example" in args) + assertTrue("/path/to/snapshots" in args) + assertFalse("--log-level=debug" in args) + } + + @Test + fun `--log-level=debug is set correctly`() { + val task = createTestTask { + it.cliExecutable.set("sentry-cli") + it.appId.set("com.example") + it.snapshotsPath.set(File("/path/to/snapshots")) + it.debug.set(true) + } + + val args = task.computeCommandLineArgs() + + assertTrue("--log-level=debug" in args) + } + + @Test + fun `with sentryProperties file SENTRY_PROPERTIES is set correctly`() { + val project = createProject() + val propertiesFile = project.file("dummy/folder/sentry.properties") + val task = createTestTask(project) { it.sentryProperties.set(propertiesFile) } + + task.setSentryPropertiesEnv() + + assertEquals(propertiesFile.absolutePath, task.environment["SENTRY_PROPERTIES"].toString()) + } + + @Test + fun `without sentryProperties file SENTRY_PROPERTIES is not set`() { + val task = createTestTask() + + task.setSentryPropertiesEnv() + + assertNull(task.environment["SENTRY_PROPERTIES"]) + } + + @Test + fun `with sentryOrganization adds --org`() { + val task = createTestTask { + it.cliExecutable.set("sentry-cli") + it.sentryOrganization.set("dummy-org") + it.appId.set("com.example") + it.snapshotsPath.set(File("/path/to/snapshots")) + } + + val args = task.computeCommandLineArgs() + + assertTrue("--org" in args) + assertTrue("dummy-org" in args) + } + + @Test + fun `with sentryProject adds --project`() { + val task = createTestTask { + it.cliExecutable.set("sentry-cli") + it.sentryProject.set("dummy-proj") + it.appId.set("com.example") + it.snapshotsPath.set(File("/path/to/snapshots")) + } + + val args = task.computeCommandLineArgs() + + assertTrue("--project" in args) + assertTrue("dummy-proj" in args) + } + + @Test + fun `with sentryUrl adds --url`() { + val task = createTestTask { + it.cliExecutable.set("sentry-cli") + it.sentryUrl.set("https://some-host.sentry.io") + it.appId.set("com.example") + it.snapshotsPath.set(File("/path/to/snapshots")) + } + + val args = task.computeCommandLineArgs() + + assertTrue("--url" in args) + assertTrue("https://some-host.sentry.io" in args) + } + + @Test + fun `the --url parameter is placed as the first argument`() { + val task = createTestTask { + it.cliExecutable.set("sentry-cli") + it.sentryUrl.set("https://some-host.sentry.io") + it.appId.set("com.example") + it.snapshotsPath.set(File("/path/to/snapshots")) + } + + val args = task.computeCommandLineArgs() + + assertEquals(1, args.indexOf("--url")) + } + + private fun createProject(): Project { + with(ProjectBuilder.builder().build()) { + plugins.apply("io.sentry.android.gradle") + return this + } + } + + private fun createTestTask( + project: Project = createProject(), + block: (SentryUploadSnapshotsTask) -> Unit = {}, + ): SentryUploadSnapshotsTask = + project.tasks + .register("testUploadSnapshots", SentryUploadSnapshotsTask::class.java) { block(it) } + .get() +}