diff --git a/.github/workflows/integration-tests-macrobenchmark.yml b/.github/workflows/integration-tests-macrobenchmark.yml index 6b2aca8936c..b88da4f7ba5 100644 --- a/.github/workflows/integration-tests-macrobenchmark.yml +++ b/.github/workflows/integration-tests-macrobenchmark.yml @@ -1,5 +1,5 @@ name: 'Integration Tests - Macrobenchmark' -# Runs the sentry-uitest-android-macrobenchmark cold-start benchmark on a Sauce Labs real +# Runs the sentry-samples-android macrobenchmark cold-start benchmark on a Sauce Labs real # device and recovers timeToInitialDisplay from the device log. # on: @@ -33,7 +33,7 @@ jobs: - name: Assemble target app and Macrobenchmark apk if: env.SAUCE_USERNAME != null - run: ./gradlew :sentry-samples:sentry-samples-android:assembleRelease :sentry-android-integration-tests:sentry-uitest-android-macrobenchmark:assembleBenchmark + run: ./gradlew -p sentry-samples/sentry-samples-android assembleRelease :macrobenchmark:assembleBenchmark - name: Run Macrobenchmark in SauceLab uses: saucelabs/saucectl-run-action@283660aa934c02723c497efa151d582a3acc5801 # pin@v3 diff --git a/.sauce/sentry-uitest-android-macrobenchmark.yml b/.sauce/sentry-uitest-android-macrobenchmark.yml index 4e3d5cfbb3d..a6ef85e7c4a 100644 --- a/.sauce/sentry-uitest-android-macrobenchmark.yml +++ b/.sauce/sentry-uitest-android-macrobenchmark.yml @@ -15,7 +15,7 @@ defaults: espresso: app: ./sentry-samples/sentry-samples-android/build/outputs/apk/release/sentry-samples-android-release.apk - testApp: ./sentry-android-integration-tests/sentry-uitest-android-macrobenchmark/build/outputs/apk/benchmark/sentry-uitest-android-macrobenchmark-benchmark.apk + testApp: ./sentry-samples/sentry-samples-android/macrobenchmark/build/outputs/apk/benchmark/macrobenchmark-benchmark.apk suites: diff --git a/AGENTS.md b/AGENTS.md index 1027e513c4b..c5ed9be162a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,6 +62,9 @@ resolves the per-module test task and the unit-test vs system-test split for you # Assemble Android test APKs ./gradlew :sentry-android-integration-tests:sentry-uitest-android:assembleRelease :sentry-android-integration-tests:sentry-uitest-android:assembleAndroidTest +# Build the Android sample app, which is a separate Gradle build that includes this one +./gradlew -p sentry-samples/sentry-samples-android assembleDebug + # Run critical UI tests ./scripts/test-ui-critical.sh ``` @@ -111,7 +114,7 @@ The repository is organized into multiple modules: ### Utility Modules - **`sentry-test-support`** - Shared test utilities - **`sentry-system-test-support`** - System testing infrastructure -- **`sentry-samples`** - Example applications +- **`sentry-samples`** - Example applications. `sentry-samples-android` is a separate Gradle build that includes this one, so root tasks other than `spotlessApply` do not reach it - **`sentry-bom`** - Bill of Materials for dependency management ### Key Architectural Patterns diff --git a/Makefile b/Makefile index 3941eb93ec3..72361a8047d 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ -.PHONY: all clean compile javadocs dryRelease update checkFormat api assembleUiTestRelease assembleUiTestCriticalRelease runUiTestCritical setupPython systemTest systemTestInteractive check preMerge publish +.PHONY: all clean compile javadocs dryRelease update checkFormat api assembleUiTestRelease assembleUiTestCriticalRelease runUiTestCritical setupPython systemTest systemTestInteractive check checkSampleAndroid preMerge publish all: stop clean javadocs compile assembleUiTests: assembleUiTestRelease -preMerge: check +preMerge: check checkSampleAndroid publish: clean dryRelease # deep clean @@ -63,3 +63,7 @@ systemTestInteractive: setupPython # Run tests and lint check: ./gradlew check + +# The Android sample is a separate build, so the root `check` does not reach it +checkSampleAndroid: + ./gradlew -p sentry-samples/sentry-samples-android assembleDebug lintDebug diff --git a/build.gradle.kts b/build.gradle.kts index 58472fcb3ee..5bb99190c80 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -27,6 +27,7 @@ plugins { alias(libs.plugins.spring.dependency.management) apply false id("io.sentry.javadoc.aggregate") alias(libs.plugins.sentry) apply false + alias(libs.plugins.android.application) apply false } buildscript { @@ -34,8 +35,6 @@ buildscript { google() } dependencies { - classpath(Config.BuildPlugins.androidGradle) - // add classpath of sentry android gradle plugin // classpath("io.sentry:sentry-android-gradle-plugin:{version}") @@ -51,7 +50,6 @@ apiValidation { ) ignoredProjects.addAll( listOf( - "sentry-samples-android", "sentry-samples-console", "sentry-samples-console-opentelemetry-noagent", "sentry-samples-jul", @@ -107,6 +105,12 @@ allprojects { } } +// `subprojects` below cannot reach the Android sample, which is a separate build, so delegate to it +// and keep a single `./gradlew spotlessApply` formatting the whole repository. +tasks.register("spotlessApply") { + dependsOn(gradle.includedBuild("sentry-samples-android").task(":spotlessApply")) +} + subprojects { apply { plugin("io.sentry.spotless") } diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index 09d2869988b..fa230c69647 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -1,14 +1,9 @@ object Config { - val AGP = System.getenv("VERSION_AGP") ?: "9.2.1" val kotlinStdLib = "stdlib-jdk8" val kotlinStdLibVersionAndroid = "1.9.24" val kotlinTestJunit = "test-junit" - object BuildPlugins { - val androidGradle = "com.android.tools.build:gradle:$AGP" - } - object Android { val abiFilters = listOf("x86", "armeabi-v7a", "x86_64", "arm64-v8a") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1b3c05168e4..8ce0e0af633 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,4 +1,6 @@ [versions] +# Also honoured via the VERSION_AGP env var, set by the AGP compatibility matrix job. +agp = "9.2.1" animalsniffer = "2.0.1" apollo = "2.5.9" androidxLifecycle = "2.2.0" @@ -52,6 +54,7 @@ compileSdk = "37" minSdk = "21" [plugins] +android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } diff --git a/sentry-samples/sentry-samples-android/.gitignore b/sentry-samples/sentry-samples-android/.gitignore index 796b96d1c40..f44b621eddb 100644 --- a/sentry-samples/sentry-samples-android/.gitignore +++ b/sentry-samples/sentry-samples-android/.gitignore @@ -1 +1,2 @@ /build +/local.properties diff --git a/sentry-samples/sentry-samples-android/README.md b/sentry-samples/sentry-samples-android/README.md index 99d0edcd1c3..991d501a52d 100644 --- a/sentry-samples/sentry-samples-android/README.md +++ b/sentry-samples/sentry-samples-android/README.md @@ -3,20 +3,26 @@ Sample application demonstrating how to use the Sentry Android SDK, including core functionality (error reporting, tracing, session replay, profiling) and integrations (Compose, OkHttp, SQLite, etc.). +This directory is its own Gradle build. It includes the SDK build (`includeBuild("../..")`), so it +depends on plain `io.sentry:*` coordinates exactly like a real app would, and Gradle substitutes +them with the SDK's projects — nothing needs to be published first. Every command below therefore +runs against this build, not the repository root's. + ## How to run it? Install the app on your device or emulator: ``` -./gradlew :sentry-samples:sentry-samples-android:installDebug +./gradlew -p sentry-samples/sentry-samples-android installDebug ``` -or simply open the project in Android Studio and run the `sentry-samples-android` configuration. +or open `sentry-samples/sentry-samples-android` in Android Studio (not the repository root) and run +the `sentry-samples-android` configuration. The SDK sources come along through the included build. You can also apply the [Sentry Android Gradle Plugin](https://github.com/getsentry/sentry-android-gradle-plugin) (SAGP) when building (not applied by default): ``` -./gradlew :sentry-samples:sentry-samples-android:installDebug -PuseSagp +./gradlew -p sentry-samples/sentry-samples-android installDebug -PuseSagp ``` In Android Studio, add `useSagp=` (empty value) to `gradle.properties`, or pass `-PuseSagp` as a Gradle project property. @@ -52,7 +58,7 @@ Re-run `publishToMavenLocal` after each SAGP change. Then build from sentry-java: ``` -./gradlew :sentry-samples:sentry-samples-android:installDebug -PuseSagp +./gradlew -p sentry-samples/sentry-samples-android installDebug -PuseSagp ``` ## Viewing SDK output diff --git a/sentry-samples/sentry-samples-android/build.gradle.kts b/sentry-samples/sentry-samples-android/build.gradle.kts index 6cb2302d7c7..17478ad57bb 100644 --- a/sentry-samples/sentry-samples-android/build.gradle.kts +++ b/sentry-samples/sentry-samples-android/build.gradle.kts @@ -7,7 +7,8 @@ import org.apache.tools.ant.taskdefs.condition.Os import org.gradle.internal.extensions.stdlib.capitalized plugins { - id("com.android.application") + alias(libs.plugins.android.application) + id("io.sentry.spotless") alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) alias(libs.plugins.ksp) @@ -15,6 +16,23 @@ plugins { alias(libs.plugins.sqldelight) } +// The SDK version lives in the SDK build's gradle.properties, which this build cannot read as a +// Gradle property. It doubles as the sample's versionName, so an APK says which SDK it was built +// against. +val sentryVersion: String = + providers + .fileContents(layout.projectDirectory.dir("../..").file("gradle.properties")) + .asText + .map { properties -> + val match = Regex("""^versionName=(.+)$""", RegexOption.MULTILINE).find(properties) + checkNotNull(match) { "versionName is missing from the SDK build's gradle.properties" } + .groupValues[1] + .trim() + } + .get() + +version = sentryVersion + if (providers.gradleProperty("useSagp").isPresent) { apply(plugin = "io.sentry.android.gradle") } @@ -63,7 +81,7 @@ android { } } - ndk { abiFilters.addAll(Config.Android.abiFilters) } + ndk { abiFilters.addAll(listOf("x86", "armeabi-v7a", "x86_64", "arm64-v8a")) } } lint { @@ -102,7 +120,7 @@ android { signingConfigs { getByName("debug") { - storeFile = rootProject.file("debug.keystore") + storeFile = rootProject.file("../../debug.keystore") storePassword = "android" keyAlias = "androiddebugkey" keyPassword = "android" @@ -152,7 +170,7 @@ android { val toggleNativeLoggingTask = project.tasks.register(taskName) { mergedManifest.set(variant.artifacts.get(SingleArtifact.MERGED_MANIFEST)) - rootDir.set(project.rootDir.absolutePath) + repoDir.set(project.rootDir.resolve("../..").canonicalPath) } project.afterEvaluate { (variant as? VariantImpl<*>)?.taskContainer?.assembleTask?.configure { @@ -167,6 +185,10 @@ android { @Suppress("UnstableApiUsage") packagingOptions { jniLibs { useLegacyPackaging = true } } } +// The SDK build drives formatting repository-wide and references one task per included build, so +// this build's root spotlessApply has to cover its subprojects too. +tasks.named("spotlessApply") { dependsOn(subprojects.map { "${it.path}:spotlessApply" }) } + sqldelight { databases { create("SampleSQLDelightDatabase") { @@ -179,22 +201,20 @@ sqldelight { } dependencies { - implementation( - kotlin(Config.kotlinStdLib, org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION) - ) - - implementation(projects.sentryAndroid) - implementation(projects.sentryAndroidFragment) - implementation(projects.sentryAndroidNavigation) - implementation(projects.sentryAndroidSqlite) - implementation(projects.sentryAndroidTimber) - implementation(projects.sentryCompose) - implementation(projects.sentryKotlinExtensions) - implementation(projects.sentryOkhttp) - implementation(projects.sentrySpotlight) + implementation(kotlin("stdlib-jdk8")) + + implementation("io.sentry:sentry-android:$sentryVersion") + implementation("io.sentry:sentry-android-fragment:$sentryVersion") + implementation("io.sentry:sentry-android-navigation:$sentryVersion") + implementation("io.sentry:sentry-android-sqlite:$sentryVersion") + implementation("io.sentry:sentry-android-timber:$sentryVersion") + implementation("io.sentry:sentry-compose:$sentryVersion") + implementation("io.sentry:sentry-kotlin-extensions:$sentryVersion") + implementation("io.sentry:sentry-okhttp:$sentryVersion") + implementation("io.sentry:sentry-spotlight:$sentryVersion") // how to exclude androidx if release health feature is disabled - // implementation(projects.sentryAndroid) { + // implementation("io.sentry:sentry-android:$sentryVersion") { // exclude(group = "androidx.lifecycle", module = "lifecycle-process") // exclude(group = "androidx.lifecycle", module = "lifecycle-common-java8") // exclude(group = "androidx.core", module = "core") @@ -232,12 +252,12 @@ dependencies { ksp(libs.androidx.room.compiler) ksp(libs.androidx.room3.compiler) - debugImplementation(projects.sentryAndroidDistribution) + debugImplementation("io.sentry:sentry-android-distribution:$sentryVersion") debugImplementation(libs.leakcanary) } abstract class ToggleNativeLoggingTask : Exec() { - @get:Input abstract val rootDir: Property + @get:Input abstract val repoDir: Property @get:InputFile abstract val mergedManifest: RegularFileProperty @@ -255,7 +275,7 @@ abstract class ToggleNativeLoggingTask : Exec() { args.add(0, "cmd") args.add(1, "/c") } - args.add("${rootDir.get()}/scripts/toggle-codec-logs.sh") + args.add("${repoDir.get()}/scripts/toggle-codec-logs.sh") args.add(if (value) "enable" else "disable") commandLine(args) super.exec() diff --git a/sentry-samples/sentry-samples-android/gradle.properties b/sentry-samples/sentry-samples-android/gradle.properties new file mode 100644 index 00000000000..b5f3cb59a17 --- /dev/null +++ b/sentry-samples/sentry-samples-android/gradle.properties @@ -0,0 +1,38 @@ +# Gradle properties are per-build, so this repeats the flags the sample used to inherit from the +# repository root while it was a subproject of the SDK build. The SDK version is not repeated here; +# it is read from the root build's gradle.properties in build.gradle.kts. + +# Daemons heap size +org.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=2g -XX:+CrashOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC +org.gradle.caching=true +org.gradle.parallel=true +org.gradle.configureondemand=true +org.gradle.configuration-cache=true +org.gradle.configuration-cache.parallel=true + +# AndroidX required by AGP >= 3.6.x +android.useAndroidX=true +# AGP 9+ migration opt-outs until we remove kotlin-android plugin and adopt built-in Kotlin. +android.builtInKotlin=false +android.newDsl=false +android.experimental.lint.version=9.2.1 + +# Override the SDK name on native crashes on Android +sentryAndroidSdkName=sentry.native.android + +# disable renderscript, it's enabled by default +android.defaults.buildfeatures.renderscript=false + +# disable shader compilation, it's enabled by default +android.defaults.buildfeatures.shaders=false + +# disable aidl files, it's enabled by default +android.defaults.buildfeatures.aidl=false + +# disable Resource Values generation +android.defaults.buildfeatures.resvalues=false + +# disable automatically adding Kotlin stdlib to compile dependencies +kotlin.stdlib.default.dependency=false + +android.nonTransitiveRClass=true diff --git a/sentry-android-integration-tests/sentry-uitest-android-macrobenchmark/README.md b/sentry-samples/sentry-samples-android/macrobenchmark/README.md similarity index 97% rename from sentry-android-integration-tests/sentry-uitest-android-macrobenchmark/README.md rename to sentry-samples/sentry-samples-android/macrobenchmark/README.md index 9914825584d..23c5fab6921 100644 --- a/sentry-android-integration-tests/sentry-uitest-android-macrobenchmark/README.md +++ b/sentry-samples/sentry-samples-android/macrobenchmark/README.md @@ -1,4 +1,4 @@ -# sentry-uitest-android-macrobenchmark +# macrobenchmark Jetpack Macrobenchmark for cold-start of `sentry-samples-android`, used to evaluate SDK-init performance changes on a real device in a **stable, reproducible** way. Not run in CI. @@ -28,7 +28,7 @@ unlocked-clock device after ~14 iterations, inflating the tail of longer runs. Connect a device, then: ```bash -./gradlew :sentry-android-integration-tests:sentry-uitest-android-macrobenchmark:connectedBenchmarkAndroidTest +./gradlew -p sentry-samples/sentry-samples-android :macrobenchmark:connectedBenchmarkAndroidTest ``` Results print to the console and are written to diff --git a/sentry-android-integration-tests/sentry-uitest-android-macrobenchmark/build.gradle.kts b/sentry-samples/sentry-samples-android/macrobenchmark/build.gradle.kts similarity index 93% rename from sentry-android-integration-tests/sentry-uitest-android-macrobenchmark/build.gradle.kts rename to sentry-samples/sentry-samples-android/macrobenchmark/build.gradle.kts index a00d76d6029..1a7c514c420 100644 --- a/sentry-android-integration-tests/sentry-uitest-android-macrobenchmark/build.gradle.kts +++ b/sentry-samples/sentry-samples-android/macrobenchmark/build.gradle.kts @@ -2,6 +2,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11 plugins { id("com.android.test") + id("io.sentry.spotless") alias(libs.plugins.kotlin.android) } @@ -34,7 +35,8 @@ android { kotlin { compilerOptions.jvmTarget = JVM_11 } - targetProjectPath = ":sentry-samples:sentry-samples-android" + // The sample app is the root project of this build. + targetProjectPath = ":" // Run the test in its own process so it measures the target app cold, not itself. experimentalProperties["android.experimental.self-instrumenting"] = true } diff --git a/sentry-android-integration-tests/sentry-uitest-android-macrobenchmark/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/macrobenchmark/src/main/AndroidManifest.xml similarity index 100% rename from sentry-android-integration-tests/sentry-uitest-android-macrobenchmark/src/main/AndroidManifest.xml rename to sentry-samples/sentry-samples-android/macrobenchmark/src/main/AndroidManifest.xml diff --git a/sentry-android-integration-tests/sentry-uitest-android-macrobenchmark/src/main/java/io/sentry/uitest/android/macrobenchmark/SentryStartupBenchmark.kt b/sentry-samples/sentry-samples-android/macrobenchmark/src/main/java/io/sentry/uitest/android/macrobenchmark/SentryStartupBenchmark.kt similarity index 100% rename from sentry-android-integration-tests/sentry-uitest-android-macrobenchmark/src/main/java/io/sentry/uitest/android/macrobenchmark/SentryStartupBenchmark.kt rename to sentry-samples/sentry-samples-android/macrobenchmark/src/main/java/io/sentry/uitest/android/macrobenchmark/SentryStartupBenchmark.kt diff --git a/sentry-samples/sentry-samples-android/settings.gradle.kts b/sentry-samples/sentry-samples-android/settings.gradle.kts new file mode 100644 index 00000000000..7fdebf6a8de --- /dev/null +++ b/sentry-samples/sentry-samples-android/settings.gradle.kts @@ -0,0 +1,63 @@ +// This sample is a standalone Gradle build that includes the SDK build, so it consumes the SDK +// through the same `io.sentry:*` coordinates a real app would. Gradle substitutes those with the +// projects of the included build, so there is nothing to publish first. +// Run it with `./gradlew -p sentry-samples/sentry-samples-android ` from the repository root. + +pluginManagement { + repositories { + // Prefer local SAGP artifact if one exists; otherwise fall back to libs.versions.toml. + if (providers.gradleProperty("useSagp").isPresent) { + mavenLocal { + content { + includeGroup("io.sentry") + includeGroup("io.sentry.android.gradle") + } + } + } + mavenCentral() + gradlePluginPortal() + google() + } + + // The AGP compatibility matrix job pins a specific AGP; the catalog version is the default. + System.getenv("VERSION_AGP")?.let { agpVersion -> + resolutionStrategy { + eachPlugin { + if (requested.id.id.startsWith("com.android")) { + useVersion(agpVersion) + } + } + } + } +} + +plugins { + id("com.gradle.develocity") version "4.4.2" + id("com.gradle.common-custom-user-data-gradle-plugin") version "2.6.0" +} + +develocity { + buildScan { + termsOfUseUrl.set("https://gradle.com/help/legal-terms-of-use") + termsOfUseAgree.set("yes") + } +} + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + mavenLocal() + } + versionCatalogs { create("libs") { from(files("../../gradle/libs.versions.toml")) } } +} + +rootProject.name = "sentry-samples-android" + +// The SDK, which supplies every io.sentry:* dependency by substitution. +includeBuild("../..") + +// Convention plugins, for io.sentry.spotless. +includeBuild("../../build-logic") + +include("macrobenchmark") diff --git a/settings.gradle.kts b/settings.gradle.kts index a7b16d03d1b..1415cab5235 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,6 +15,17 @@ pluginManagement { gradlePluginPortal() google() } + + // The AGP compatibility matrix job pins a specific AGP; the catalog version is the default. + System.getenv("VERSION_AGP")?.let { agpVersion -> + resolutionStrategy { + eachPlugin { + if (requested.id.id.startsWith("com.android")) { + useVersion(agpVersion) + } + } + } + } } plugins { @@ -40,6 +51,9 @@ dependencyResolutionManagement { rootProject.name = "sentry-root" rootProject.buildFileName = "build.gradle.kts" includeBuild("build-logic") +// The Android sample is its own build and includes this one. Including it back lets repository-wide +// tasks such as spotlessApply reach it; it is not built by anything else here. +includeBuild("sentry-samples/sentry-samples-android") include( "sentry", "sentry-spotlight", @@ -99,7 +113,6 @@ include( "sentry-reactor", "sentry-async-profiler", "sentry-ktor-client", - "sentry-samples:sentry-samples-android", "sentry-samples:sentry-samples-console", "sentry-samples:sentry-samples-console-otlp", "sentry-samples:sentry-samples-console-opentelemetry-noagent", @@ -126,7 +139,6 @@ include( "sentry-samples:sentry-samples-spring-boot-4-webflux", "sentry-samples:sentry-samples-netflix-dgs", "sentry-android-integration-tests:sentry-uitest-android-critical", - "sentry-android-integration-tests:sentry-uitest-android-macrobenchmark", "sentry-android-integration-tests:sentry-uitest-android", "sentry-android-integration-tests:test-app-size", "sentry-samples:sentry-samples-openfeign"