diff --git a/.github/workflows/integration-tests-ui-critical.yml b/.github/workflows/integration-tests-ui-critical.yml index 05cd811f5a0..c7c756444e0 100644 --- a/.github/workflows/integration-tests-ui-critical.yml +++ b/.github/workflows/integration-tests-ui-critical.yml @@ -11,11 +11,14 @@ concurrency: cancel-in-progress: true env: + INTEGRATION_TESTS_PATH: "sentry-android-integration-tests" BASE_PATH: "sentry-android-integration-tests/sentry-uitest-android-critical" BUILD_PATH: "build/outputs/apk/release" APK_NAME: "sentry-uitest-android-critical-release.apk" APK_ARTIFACT_NAME: "sentry-uitest-android-critical-release" MAESTRO_VERSION: "1.39.0" + MOCK_RELAY_ARTIFACT_NAME: "sentry-mock-relay" + MOCK_RELAY_PATH: "sentry-mock-relay-0.0.1.zip" jobs: build: @@ -41,6 +44,16 @@ jobs: gradle-home-cache-cleanup: true cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + - name: Build mock relay + run: make buildMockRelay + + - name: Upload Mock Relay + uses: actions/upload-artifact@v4 + with: + name: ${{env.MOCK_RELAY_ARTIFACT_NAME}} + path: "${{env.INTEGRATION_TESTS_PATH}}/sentry-mock-relay/build/distributions/${{env.MOCK_RELAY_PATH}}" + retention-days: 1 + - name: Build debug APK run: make assembleUiTestCriticalRelease @@ -91,11 +104,25 @@ jobs: sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm + - name: Set up Java 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + - name: Download APK artifact uses: actions/download-artifact@v4 with: name: ${{env.APK_ARTIFACT_NAME}} + - name: Download Mock Relay + uses: actions/download-artifact@v4 + with: + name: ${{env.MOCK_RELAY_ARTIFACT_NAME}} + + - name: Unzip Mock Relay + run: unzip -o ${{env.MOCK_RELAY_PATH}} + - name: Install Maestro uses: dniHze/maestro-test-action@bda8a93211c86d0a05b7a4597c5ad134566fbde4 # pin@v1.0.0 with: @@ -122,8 +149,13 @@ jobs: -timezone US/Pacific script: | adb install -r -d "${{env.APK_NAME}}" + ./sentry-mock-relay-0.0.1/bin/sentry-mock-relay > /dev/null & + maestro test "${{env.BASE_PATH}}/maestro" --debug-output "${{env.BASE_PATH}}/maestro-logs" + curl --fail http://localhost:8961/assertReceivedAtLeastOneCrashReport + curl --fail http://localhost:8961/stop + - name: Upload Maestro test results if: failure() uses: actions/upload-artifact@v4 diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml index aed5fd2864a..4c4c0e87ce6 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -74,6 +74,7 @@ jobs: -e '/.*"sentry-android-integration-tests:sentry-uitest-android-benchmark",/d' \ -e '/.*"sentry-android-integration-tests:sentry-uitest-android",/d' \ -e '/.*"sentry-android-integration-tests:sentry-uitest-android-critical",/d' \ + -e '/.*"sentry-android-integration-tests:sentry-mock-relay",/d' \ -e '/.*"sentry-android-integration-tests:test-app-sentry",/d' \ -e '/.*"sentry-samples:sentry-samples-android",/d' \ -e '/.*"sentry-android-replay",/d' \ @@ -85,6 +86,7 @@ jobs: -e '/.*"sentry-uitest-android",/d' \ -e '/.*"sentry-uitest-android-benchmark",/d' \ -e '/.*"sentry-uitest-android-critical",/d' \ + -e '/.*"sentry-mock-relay",/d' \ -e '/.*"test-app-sentry",/d' \ -e '/.*"sentry-samples-android",/d' \ build.gradle.kts diff --git a/Makefile b/Makefile index 3fff2c01ff6..2fc9b0757ee 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all clean compile javadocs dryRelease update stop checkFormat format api assembleBenchmarkTestRelease assembleUiTestRelease assembleUiTestCriticalRelease createCoverageReports runUiTestCritical check preMerge publish +.PHONY: all clean compile javadocs dryRelease update stop checkFormat format api assembleBenchmarkTestRelease assembleUiTestRelease assembleUiTestCriticalRelease createCoverageReports runUiTestCritical buildMockRelay check preMerge publish all: stop clean javadocs compile createCoverageReports assembleBenchmarks: assembleBenchmarkTestRelease @@ -61,6 +61,10 @@ assembleUiTestCriticalRelease: runUiTestCritical: ./scripts/test-ui-critical.sh +# Build the mock relay for critical tests +buildMockRelay: + ./gradlew :sentry-android-integration-tests:sentry-mock-relay:build + # Create coverage reports # - Jacoco for Java & Android modules # - Kover for KMP modules e.g sentry-compose diff --git a/build.gradle.kts b/build.gradle.kts index bb60529161a..9d5b0962bbd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -70,6 +70,7 @@ apiValidation { "sentry-uitest-android", "sentry-uitest-android-benchmark", "sentry-uitest-android-critical", + "sentry-mock-relay", "test-app-plain", "test-app-sentry", "sentry-samples-netflix-dgs" diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index a8c3f760ea4..63dd05d8c72 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -208,6 +208,10 @@ object Config { val javaFaker = "com.github.javafaker:javafaker:1.0.2" val msgpack = "org.msgpack:msgpack-core:0.9.8" val leakCanaryInstrumentation = "com.squareup.leakcanary:leakcanary-android-instrumentation:2.14" + val kotlinxSerializationJson = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1" + val pluginSerializationVersion = "1.5.0" + val logbackClassic = "ch.qos.logback:logback-classic:1.4.14" + val ktorVersion = "2.3.12" val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4:1.6.8" } diff --git a/scripts/test-ui-critical.sh b/scripts/test-ui-critical.sh index 7bb36eebec7..4a4cd0052da 100755 --- a/scripts/test-ui-critical.sh +++ b/scripts/test-ui-critical.sh @@ -19,6 +19,9 @@ if ! command -v maestro &> /dev/null; then exit 1 fi +echo "Building the mock relay..." +make buildMockRelay + echo "Building the UI Test Critical app..." make assembleUiTestCriticalRelease @@ -29,7 +32,24 @@ apkName="sentry-uitest-android-critical-release.apk" appPath="${baseDir}/${buildDir}/${apkName}" adb install -r -d "$appPath" +echo "Starting the mock relay..." +cd sentry-android-integration-tests/sentry-mock-relay/build/distributions +unzip -o sentry-mock-relay-0.0.1.zip +./sentry-mock-relay-0.0.1/bin/sentry-mock-relay > /dev/null & MOCK_RELAY_PID=$! +echo "Mock relay PID: $MOCK_RELAY_PID" + +set +e echo "Running the Maestro tests..." maestro test \ "${baseDir}/maestro" \ --debug-output "${baseDir}/maestro-logs" +MAESTRO_EXIT_CODE=$? + +echo "Checking mock relay results..." +curl --fail http://localhost:8961/assertReceivedAtLeastOneCrashReport +MOCK_RELAY_EXIT_CODE=$? + +echo "Stopping the mock relay..." +kill $MOCK_RELAY_PID + +exit $(($MAESTRO_EXIT_CODE || $MOCK_RELAY_EXIT_CODE)) diff --git a/sentry-android-integration-tests/sentry-mock-relay/.gitignore b/sentry-android-integration-tests/sentry-mock-relay/.gitignore new file mode 100644 index 00000000000..c426c32f86f --- /dev/null +++ b/sentry-android-integration-tests/sentry-mock-relay/.gitignore @@ -0,0 +1,36 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ \ No newline at end of file diff --git a/sentry-android-integration-tests/sentry-mock-relay/build.gradle.kts b/sentry-android-integration-tests/sentry-mock-relay/build.gradle.kts new file mode 100644 index 00000000000..cc4df2b62ab --- /dev/null +++ b/sentry-android-integration-tests/sentry-mock-relay/build.gradle.kts @@ -0,0 +1,28 @@ + +plugins { + kotlin("jvm") + id("io.ktor.plugin") version Config.TestLibs.ktorVersion + kotlin("plugin.serialization") version Config.TestLibs.pluginSerializationVersion +} + +group = "io.sentry.mock-relay" +version = "0.0.1" + +application { + mainClass.set("io.ktor.server.netty.EngineMain") + + val isDevelopment: Boolean = project.ext.has("development") + applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") +} + +repositories { + mavenCentral() +} + +dependencies { + implementation(Config.TestLibs.kotlinxSerializationJson) + implementation("io.ktor:ktor-server-core-jvm") + implementation("io.ktor:ktor-server-netty-jvm") + implementation(Config.TestLibs.logbackClassic) + implementation("io.ktor:ktor-server-config-yaml") +} diff --git a/sentry-android-integration-tests/sentry-mock-relay/scripts/kill.sh b/sentry-android-integration-tests/sentry-mock-relay/scripts/kill.sh new file mode 100644 index 00000000000..c653be4eec1 --- /dev/null +++ b/sentry-android-integration-tests/sentry-mock-relay/scripts/kill.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +pkill -9 -f "sentry-mock-relay" || true diff --git a/sentry-android-integration-tests/sentry-mock-relay/src/main/kotlin/io/sentry/Application.kt b/sentry-android-integration-tests/sentry-mock-relay/src/main/kotlin/io/sentry/Application.kt new file mode 100644 index 00000000000..f2d5484fa4c --- /dev/null +++ b/sentry-android-integration-tests/sentry-mock-relay/src/main/kotlin/io/sentry/Application.kt @@ -0,0 +1,99 @@ +package io.sentry + +import io.ktor.server.application.Application +import io.ktor.server.application.call +import io.ktor.server.request.receive +import io.ktor.server.request.receiveText +import io.ktor.server.request.uri +import io.ktor.server.response.respondText +import io.ktor.server.routing.get +import io.ktor.server.routing.post +import io.ktor.server.routing.routing +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import java.util.zip.GZIPInputStream + +fun main(args: Array) { + io.ktor.server.netty.EngineMain.main(args) +} + +fun Application.module() { + configureRouting() +} + +fun Application.configureRouting() { + val receivedEnvelopes = mutableListOf>() + + routing { + post("/{...}") { + println("Received request: ${call.request.uri}") + val textBody: String = if (call.request.headers["Content-Encoding"] == "gzip") { + call.receive().let { + GZIPInputStream(it.inputStream()).bufferedReader().use { reader -> + reader.readText() + } + } + } else { + call.receiveText() + } + + val jsonItems = textBody.split('\n').mapNotNull { line -> + try { + Json.parseToJsonElement(line) + } catch (e: Exception) { + null + } + } + + receivedEnvelopes.add(jsonItems) + + call.respondText("{}") + } + get("/healthCheck") { + call.respondText("OK") + } + get("/assertReceivedAtLeastOneCrashReport") { + if (receivedEnvelopes.isEmpty()) { + call.respondText("Mocked Replay have not received any envelopes\n", status = io.ktor.http.HttpStatusCode.BadRequest) + } + + val hasCrashReport = receivedEnvelopes.any { envelope -> + envelope.any { item -> + try { + if (item.jsonObject.containsKey("exception")) { + val exception = item.jsonObject["exception"]?.jsonObject + val values = exception?.get("values")?.jsonArray + values?.any { value -> + val message = value.jsonObject["value"]?.jsonPrimitive?.content + message == "Crash the test app." + } ?: false + } else { + false + } + } catch (e: Exception) { + false + } + } + } + + if (hasCrashReport) { + call.respondText("Received at least one crash report\n") + } else { + call.respondText("No crash report received\n", status = io.ktor.http.HttpStatusCode.BadRequest) + } + } + get("/stop") { + call.respondText("OK") + + launch { + delay(1000) + Runtime.getRuntime().halt(0) + } + } + } +} diff --git a/sentry-android-integration-tests/sentry-mock-relay/src/main/resources/application.yaml b/sentry-android-integration-tests/sentry-mock-relay/src/main/resources/application.yaml new file mode 100644 index 00000000000..2c74879be83 --- /dev/null +++ b/sentry-android-integration-tests/sentry-mock-relay/src/main/resources/application.yaml @@ -0,0 +1,6 @@ +ktor: + application: + modules: + - io.sentry.ApplicationKt.module + deployment: + port: 8961 diff --git a/sentry-android-integration-tests/sentry-mock-relay/src/main/resources/logback.xml b/sentry-android-integration-tests/sentry-mock-relay/src/main/resources/logback.xml new file mode 100644 index 00000000000..3e11d7811a6 --- /dev/null +++ b/sentry-android-integration-tests/sentry-mock-relay/src/main/resources/logback.xml @@ -0,0 +1,12 @@ + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + \ No newline at end of file diff --git a/sentry-android-integration-tests/sentry-uitest-android-critical/build.gradle.kts b/sentry-android-integration-tests/sentry-uitest-android-critical/build.gradle.kts index dcd52443ca6..6d1a45ad5f4 100644 --- a/sentry-android-integration-tests/sentry-uitest-android-critical/build.gradle.kts +++ b/sentry-android-integration-tests/sentry-uitest-android-critical/build.gradle.kts @@ -49,6 +49,8 @@ android { dependencies { implementation(kotlin(Config.kotlinStdLib, org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION)) + implementation(Config.Libs.okhttp) + implementation(Config.TestLibs.mockWebserver) implementation(Config.Libs.androidxCore) implementation(Config.Libs.composeActivity) implementation(Config.Libs.composeFoundation) diff --git a/sentry-android-integration-tests/sentry-uitest-android-critical/src/main/AndroidManifest.xml b/sentry-android-integration-tests/sentry-uitest-android-critical/src/main/AndroidManifest.xml index 0ab5e6052df..65a0c51f30a 100644 --- a/sentry-android-integration-tests/sentry-uitest-android-critical/src/main/AndroidManifest.xml +++ b/sentry-android-integration-tests/sentry-uitest-android-critical/src/main/AndroidManifest.xml @@ -5,8 +5,10 @@ - + tools:targetApi="31" + android:networkSecurityConfig="@xml/network" + > + + + + + + 10.0.2.2 + + diff --git a/settings.gradle.kts b/settings.gradle.kts index d99f0f0e0a9..97bbe44e5f0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -71,6 +71,7 @@ include( "sentry-samples:sentry-samples-spring-boot-webflux-jakarta", "sentry-samples:sentry-samples-netflix-dgs", "sentry-android-integration-tests:sentry-uitest-android-critical", + "sentry-android-integration-tests:sentry-mock-relay", "sentry-android-integration-tests:sentry-uitest-android-benchmark", "sentry-android-integration-tests:sentry-uitest-android", "sentry-android-integration-tests:test-app-plain",