diff --git a/README.md b/README.md index a7e24161..3fdacf8c 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ flashcat { versionName = "1.3.0" // Optional, by default it is read from your Android plugin configuration's version name serviceName = "my-service" // Optional, by default it is read from your Android plugin configuration's package name site = "CN" // Optional, can be "CN" or "STAGING" (check `FlashcatSite` documentation for the full list). Default is "CN" + sourcemapEndpoint = "https://rum.example.com" // Optional, custom sourcemap intake endpoint for private deployments. `/sourcemap/upload` is appended if omitted. checkProjectDependencies = "warn" // Optional, can be "warn", "fail" or "none". Default is "fail". Will check if Flashcat SDK is in the project dependencies. mappingFilePath = "path/to/mapping.txt" // Optional, provides a custom mapping file path. Default is "build/outputs/mapping/{variant}/mapping.txt". nonDefaultObfuscation = false // Optional, to be used if a 3rd-party obfuscation tool is used. Default is false. @@ -74,9 +75,11 @@ If you're using variants, you can set a custom configuration per variant using t ```groovy flashcat { site = "CN" // Variants with no configurations will use this as default + sourcemapEndpoint = "https://rum.example.com" // Variants with no configurations will use this as default variants { fr { site = "STAGING" + sourcemapEndpoint = "https://rum-fr.example.com/sourcemap/upload" mappingFilePath = "path/to/fr/mapping.txt" } } @@ -88,9 +91,11 @@ flashcat { ```kotlin flashcat { site = "CN" // Variants with no configurations will use this as default + sourcemapEndpoint = "https://rum.example.com" // Variants with no configurations will use this as default variants { register("fr") { site = "STAGING" + sourcemapEndpoint = "https://rum-fr.example.com/sourcemap/upload" mappingFilePath = "path/to/fr/mapping.txt" } } @@ -121,6 +126,9 @@ export FLASHCAT_API_KEY="your-flashcat-api-key" # Site (optional) export FLASHCAT_SITE="ci.flashcat.cloud" + +# Sourcemap intake endpoint (optional, useful for private deployments) +export FLASHCAT_SOURCEMAP_INTAKE_URL="https://rum.example.com" ``` ### Configuration File (flashcat-ci.json) @@ -130,7 +138,8 @@ You can also use a `flashcat-ci.json` file in your project root for configuratio ```json { "apiKey": "your-flashcat-api-key", - "flashcatSite": "ci.flashcat.cloud" + "flashcatSite": "ci.flashcat.cloud", + "sourcemapEndpoint": "https://rum.example.com" } ``` diff --git a/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/DdAndroidGradlePlugin.kt b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/DdAndroidGradlePlugin.kt index a3d9abe1..d844ee94 100644 --- a/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/DdAndroidGradlePlugin.kt +++ b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/DdAndroidGradlePlugin.kt @@ -339,6 +339,7 @@ class DdAndroidGradlePlugin @Inject constructor( uploadTask.sourceSetRoots.set(variant.collectJavaAndKotlinSourceDirectories()) uploadTask.site = extensionConfiguration.site ?: "" + uploadTask.sourcemapEndpoint = extensionConfiguration.sourcemapEndpoint ?: "" if (extensionConfiguration.versionName != null) { uploadTask.versionName.set(extensionConfiguration.versionName) } else { diff --git a/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/DdExtensionConfiguration.kt b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/DdExtensionConfiguration.kt index 4cf6e568..7a818959 100644 --- a/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/DdExtensionConfiguration.kt +++ b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/DdExtensionConfiguration.kt @@ -33,6 +33,12 @@ open class DdExtensionConfiguration( */ var site: String? = null + /** + * Custom sourcemap intake endpoint. If the value doesn't end with `/sourcemap/upload`, + * the upload path will be appended automatically. + */ + var sourcemapEndpoint: String? = null + /** * The url of the remote repository where the source code was deployed. If not provided this * value will be resolved from your current GIT configuration during the task execution time. @@ -122,6 +128,7 @@ open class DdExtensionConfiguration( config.versionName?.let { versionName = it } config.serviceName?.let { serviceName = it } config.site?.let { site = it } + config.sourcemapEndpoint?.let { sourcemapEndpoint = it } config.remoteRepositoryUrl?.let { remoteRepositoryUrl = it } config.checkProjectDependencies?.let { checkProjectDependencies = it } config.mappingFilePath?.let { mappingFilePath = it } diff --git a/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/FileUploadTask.kt b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/FileUploadTask.kt index 37ca97fc..b85585fa 100644 --- a/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/FileUploadTask.kt +++ b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/FileUploadTask.kt @@ -93,6 +93,12 @@ abstract class FileUploadTask @Inject constructor( @get:Input var site: String = "" + /** + * Custom sourcemap intake endpoint. If empty, the endpoint is resolved from [site]. + */ + @get:Input + var sourcemapEndpoint: String = "" + /** * The url of the remote repository where the source code was deployed. */ @@ -140,6 +146,7 @@ abstract class FileUploadTask @Inject constructor( applyFlashcatCiConfig(it) } applySiteFromEnvironment() + applySourcemapEndpointFromEnvironment() validateConfiguration() check(!(apiKey.contains("\"") || apiKey.contains("'"))) { @@ -192,7 +199,8 @@ abstract class FileUploadTask @Inject constructor( ), repositories.firstOrNull(), !disableGzipOption.isPresent, - emulateNetworkCall.isPresent + emulateNetworkCall.isPresent, + sourcemapEndpoint.ifBlank { null } ) } catch (e: Exception) { caughtErrors.add(e) @@ -225,6 +233,7 @@ abstract class FileUploadTask @Inject constructor( this.apiKey = apiKey.value apiKeySource = apiKey.source site = extensionConfiguration.site ?: "" + sourcemapEndpoint = extensionConfiguration.sourcemapEndpoint ?: "" versionName.set(variant.versionName) versionCode.set(variant.versionCode) @@ -263,16 +272,49 @@ abstract class FileUploadTask @Inject constructor( } } + private fun applySourcemapEndpointFromEnvironment() { + val environmentEndpoint = System.getenv(FLASHCAT_SOURCEMAP_INTAKE_URL) + if (!environmentEndpoint.isNullOrEmpty()) { + if (sourcemapEndpoint.isNotEmpty()) { + DdAndroidGradlePlugin.LOGGER.info( + "Sourcemap endpoint found as FLASHCAT_SOURCEMAP_INTAKE_URL env variable, but it will be " + + "ignored because one was already provided in extension or Flashcat CI config file." + ) + return + } + DdAndroidGradlePlugin.LOGGER.info( + "Sourcemap endpoint found as FLASHCAT_SOURCEMAP_INTAKE_URL env variable, using it." + ) + sourcemapEndpoint = environmentEndpoint + } + } + private fun applyFlashcatCiConfig(flashcatCiFile: File) { try { val config = JSONObject(flashcatCiFile.readText()) applyApiKeyFromFlashcatCiConfig(config) + applySourcemapEndpointFromFlashcatCiConfig(config) applySiteFromFlashcatCiConfig(config) } catch (e: JSONException) { DdAndroidGradlePlugin.LOGGER.error("Failed to parse Flashcat CI config file.", e) } } + private fun applySourcemapEndpointFromFlashcatCiConfig(config: JSONObject) { + val endpoint = config.optString(FLASHCAT_CI_SOURCEMAP_ENDPOINT_PROPERTY, null) + if (!endpoint.isNullOrEmpty()) { + if (sourcemapEndpoint.isNotEmpty()) { + DdAndroidGradlePlugin.LOGGER.info( + "Sourcemap endpoint found in Flashcat CI config file, but it will be ignored," + + " because one was already provided in extension." + ) + } else { + DdAndroidGradlePlugin.LOGGER.info("Sourcemap endpoint found in Flashcat CI config file, using it.") + sourcemapEndpoint = endpoint + } + } + } + private fun applyApiKeyFromFlashcatCiConfig(config: JSONObject) { val apiKey = config.optString(FLASHCAT_CI_API_KEY_PROPERTY, null) if (!apiKey.isNullOrEmpty()) { @@ -350,7 +392,9 @@ abstract class FileUploadTask @Inject constructor( private const val FLASHCAT_CI_API_KEY_PROPERTY = "apiKey" private const val FLASHCAT_CI_SITE_PROPERTY = "flashcatSite" + private const val FLASHCAT_CI_SOURCEMAP_ENDPOINT_PROPERTY = "sourcemapEndpoint" const val FLASHCAT_SITE = "FLASHCAT_SITE" + const val FLASHCAT_SOURCEMAP_INTAKE_URL = "FLASHCAT_SOURCEMAP_INTAKE_URL" internal val LOGGER = Logging.getLogger("DdFileUploadTask") diff --git a/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/internal/OkHttpUploader.kt b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/internal/OkHttpUploader.kt index be0dcf83..993673c3 100644 --- a/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/internal/OkHttpUploader.kt +++ b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/internal/OkHttpUploader.kt @@ -54,9 +54,11 @@ internal class OkHttpUploader : Uploader { identifier: DdAppIdentifier, repositoryInfo: RepositoryInfo?, useGzip: Boolean, - emulateNetworkCall: Boolean + emulateNetworkCall: Boolean, + customSourcemapEndpoint: String? ) { - LOGGER.info("Uploading file ${fileInfo.fileName} with tags $identifier (site=${site.intakeHostName}):") + val uploadEndpoint = customSourcemapEndpoint?.let(::resolveSourcemapUploadEndpoint) ?: site.uploadEndpoint() + LOGGER.info("Uploading file ${fileInfo.fileName} with tags $identifier (endpoint=$uploadEndpoint):") if (fileInfo.extraAttributes.isNotEmpty()) { LOGGER.info(" extra attributes: ${fileInfo.extraAttributes}") } @@ -64,7 +66,7 @@ internal class OkHttpUploader : Uploader { val body = createBody(identifier, fileInfo, repositoryFile, repositoryInfo) val requestBuilder = Request.Builder() - .url(site.uploadEndpoint()) + .url(uploadEndpoint) .header(HEADER_EVP_ORIGIN, "dd-sdk-android-gradle-plugin") .header(HEADER_EVP_ORIGIN_VERSION, VERSION) .header(HEADER_API_KEY, apiKey) @@ -97,7 +99,7 @@ internal class OkHttpUploader : Uploader { null } - handleResponse(response, site, apiKey, identifier) + handleResponse(response, site, apiKey, identifier, uploadEndpoint, customSourcemapEndpoint != null) } // endregion @@ -158,7 +160,9 @@ internal class OkHttpUploader : Uploader { response: Response?, site: FlashcatSite, apiKey: String, - identifier: DdAppIdentifier + identifier: DdAppIdentifier, + uploadEndpoint: String, + isCustomEndpoint: Boolean ) { val statusCode = response?.code when { @@ -173,7 +177,7 @@ internal class OkHttpUploader : Uploader { ) statusCode == HttpURLConnection.HTTP_FORBIDDEN -> throw InvalidApiKeyException( identifier, - site + uploadEndpoint ) statusCode == HttpURLConnection.HTTP_CLIENT_TIMEOUT -> throw RuntimeException( "Unable to upload mapping file with tags $identifier because of a request timeout; " + @@ -184,10 +188,13 @@ internal class OkHttpUploader : Uploader { MAX_MAP_SIZE_EXCEEDED_ERROR.format(Locale.US, identifier) ) statusCode >= HttpURLConnection.HTTP_BAD_REQUEST -> { + // The API key validation endpoint is only known for predefined sites, so skip the + // check when uploading to a custom endpoint (e.g. a private deployment). if (statusCode == HttpURLConnection.HTTP_BAD_REQUEST && + !isCustomEndpoint && validateApiKey(site, apiKey) == false ) { - throw InvalidApiKeyException(identifier, site) + throw InvalidApiKeyException(identifier, uploadEndpoint) } response.body.use { throw IllegalStateException( @@ -236,9 +243,9 @@ internal class OkHttpUploader : Uploader { internal inner class InvalidApiKeyException( uploadIdentifier: DdAppIdentifier, - site: FlashcatSite + endpoint: String ) : RuntimeException( - "Unable to upload mapping file for $uploadIdentifier (site=${site.intakeHostName}); " + + "Unable to upload mapping file for $uploadIdentifier (endpoint=$endpoint); " + "verify that you're using a valid API Key" ) @@ -295,5 +302,14 @@ internal class OkHttpUploader : Uploader { HttpURLConnection.HTTP_CREATED, HttpURLConnection.HTTP_ACCEPTED ) + + internal fun resolveSourcemapUploadEndpoint(endpoint: String): String { + val normalized = endpoint.trim().trimEnd('/') + return if (normalized.endsWith("/sourcemap/upload")) { + normalized + } else { + "$normalized/sourcemap/upload" + } + } } } diff --git a/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/internal/Uploader.kt b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/internal/Uploader.kt index bcec8da5..ad7adebc 100644 --- a/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/internal/Uploader.kt +++ b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/internal/Uploader.kt @@ -57,6 +57,7 @@ internal interface Uploader { identifier: DdAppIdentifier, repositoryInfo: RepositoryInfo?, useGzip: Boolean = true, - emulateNetworkCall: Boolean = false + emulateNetworkCall: Boolean = false, + customSourcemapEndpoint: String? = null ) } diff --git a/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/DdAndroidGradlePluginTest.kt b/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/DdAndroidGradlePluginTest.kt index 09e783a2..92fc736e 100644 --- a/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/DdAndroidGradlePluginTest.kt +++ b/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/DdAndroidGradlePluginTest.kt @@ -140,6 +140,7 @@ internal class DdAndroidGradlePluginTest { assertThat(task.versionName.get()).isEqualTo(versionName) assertThat(task.serviceName.get()).isEqualTo(packageName) assertThat(task.site).isEqualTo(fakeExtension.site) + assertThat(task.sourcemapEndpoint).isEqualTo(fakeExtension.sourcemapEndpoint) assertThat(task.remoteRepositoryUrl).isEqualTo(fakeExtension.remoteRepositoryUrl) assertThat(task.mappingFile.get().asFile) .isEqualTo(File(fakeProject.projectDir, fakeExtension.mappingFilePath)) @@ -706,6 +707,7 @@ internal class DdAndroidGradlePluginTest { assertThat(config.versionName).isEqualTo(fakeExtension.versionName) assertThat(config.serviceName).isEqualTo(fakeExtension.serviceName) assertThat(config.site).isEqualTo(fakeExtension.site) + assertThat(config.sourcemapEndpoint).isEqualTo(fakeExtension.sourcemapEndpoint) assertThat(config.remoteRepositoryUrl).isEqualTo(fakeExtension.remoteRepositoryUrl) assertThat(config.checkProjectDependencies) .isEqualTo(fakeExtension.checkProjectDependencies) @@ -1137,6 +1139,7 @@ internal class DdAndroidGradlePluginTest { assertThat(config.versionName).isEqualTo(configuration.versionName) assertThat(config.serviceName).isEqualTo(configuration.serviceName) assertThat(config.site).isEqualTo(configuration.site) + assertThat(config.sourcemapEndpoint).isEqualTo(configuration.sourcemapEndpoint) assertThat(config.checkProjectDependencies) .isEqualTo(configuration.checkProjectDependencies) assertThat(config.remoteRepositoryUrl).isEqualTo(configuration.remoteRepositoryUrl) diff --git a/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/MappingFileUploadTaskTest.kt b/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/MappingFileUploadTaskTest.kt index ecefad73..321e62b6 100644 --- a/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/MappingFileUploadTaskTest.kt +++ b/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/MappingFileUploadTaskTest.kt @@ -127,11 +127,13 @@ internal class MappingFileUploadTaskTest { testedTask.buildId.set(fakeBuildId) testedTask.mappingFile.set(fakeProject.objects.fileProperty().fileValue(File(tempDir, fakeMappingFileName))) setEnv(FileUploadTask.FLASHCAT_SITE, "") + setEnv(FileUploadTask.FLASHCAT_SOURCEMAP_INTAKE_URL, "") } @AfterEach fun `tear down`() { removeEnv(FileUploadTask.FLASHCAT_SITE) + removeEnv(FileUploadTask.FLASHCAT_SOURCEMAP_INTAKE_URL) } @Test @@ -177,6 +179,45 @@ internal class MappingFileUploadTaskTest { ) } + @Test + fun `M upload file to custom sourcemap endpoint W applyTask()`( + @StringForgery(regex = "https://[a-z]{8}\\.example\\.com") fakeSourcemapEndpoint: String + ) { + // Given + val fakeMappingFile = File(tempDir, fakeMappingFileName) + fakeMappingFile.writeText(fakeMappingFileContent) + testedTask.mappingFile.set(fakeProject.objects.fileProperty().fileValue(File(fakeMappingFile.path))) + testedTask.sourcemapEndpoint = fakeSourcemapEndpoint + + // When + testedTask.applyTask() + + // Then + verify(mockUploader).upload( + fakeSite, + Uploader.UploadFileInfo( + fileKey = MappingFileUploadTask.KEY_JVM_MAPPING_FILE, + file = fakeMappingFile, + encoding = MappingFileUploadTask.MEDIA_TYPE_TXT, + fileType = MappingFileUploadTask.TYPE_JVM_MAPPING_FILE, + fileName = MappingFileUploadTask.KEY_JVM_MAPPING_FILE_NAME + ), + null, + fakeApiKey.value, + DdAppIdentifier( + serviceName = fakeService, + version = fakeVersion, + versionCode = fakeVersionCode, + variant = fakeVariant, + buildId = fakeBuildId + ), + null, + useGzip = true, + emulateNetworkCall = false, + customSourcemapEndpoint = fakeSourcemapEndpoint + ) + } + @Test fun `M upload file W applyTask() { short aliases requested }`( @StringForgery fakeApplicationId: String @@ -222,7 +263,8 @@ internal class MappingFileUploadTaskTest { ), eq(fakeRepoInfo), useGzip = eq(true), - emulateNetworkCall = eq(false) + emulateNetworkCall = eq(false), + customSourcemapEndpoint = eq(null) ) assertThat(lastValue.file).hasSameTextualContentAs( fileFromResourcesPath("mapping-with-aliases.txt") @@ -274,7 +316,8 @@ internal class MappingFileUploadTaskTest { ), eq(fakeRepoInfo), useGzip = eq(true), - emulateNetworkCall = eq(false) + emulateNetworkCall = eq(false), + customSourcemapEndpoint = eq(null) ) assertThat(lastValue.file.readLines()).isEqualTo(expectedLines) } @@ -330,7 +373,8 @@ internal class MappingFileUploadTaskTest { ), eq(fakeRepoInfo), useGzip = eq(true), - emulateNetworkCall = eq(false) + emulateNetworkCall = eq(false), + customSourcemapEndpoint = eq(null) ) assertThat(lastValue.file.readLines()).isEqualTo(expectedLines) } @@ -786,6 +830,45 @@ internal class MappingFileUploadTaskTest { assertThat(testedTask.site).isEqualTo(fakeSite.name) } + @Test + fun `M read sourcemap endpoint from environment variable W applyTask() { endpoint is not set }`( + @StringForgery(regex = "https://[a-z]{8}\\.example\\.com") fakeSourcemapEndpoint: String + ) { + // Given + setEnv(FileUploadTask.FLASHCAT_SOURCEMAP_INTAKE_URL, fakeSourcemapEndpoint) + testedTask.sourcemapEndpoint = "" + + // When + testedTask.applyTask() + + // Then + assertThat(testedTask.sourcemapEndpoint).isEqualTo(fakeSourcemapEndpoint) + } + + @Test + fun `M prefer sourcemap endpoint from CI config W applyTask() { endpoint exists in env variable }`( + @StringForgery(regex = "https://[a-z]{8}\\.example\\.com") fakeEnvironmentEndpoint: String, + @StringForgery(regex = "https://[a-z]{8}\\.example\\.org") fakeCiEndpoint: String + ) { + // Given + val fakeFlashcatCiFile = File(tempDir, "flashcat-ci.json") + fakeFlashcatCiFile.writeText( + JSONObject().apply { + put("sourcemapEndpoint", fakeCiEndpoint) + }.toString() + ) + + setEnv(FileUploadTask.FLASHCAT_SOURCEMAP_INTAKE_URL, fakeEnvironmentEndpoint) + testedTask.flashcatCiFile = fakeFlashcatCiFile + testedTask.sourcemapEndpoint = "" + + // When + testedTask.applyTask() + + // Then + assertThat(testedTask.sourcemapEndpoint).isEqualTo(fakeCiEndpoint) + } + @Test fun `M not apply datadog CI config if exists W applyTask() { malformed json }`(forge: Forge) { // Given diff --git a/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/internal/OkHttpUploaderTest.kt b/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/internal/OkHttpUploaderTest.kt index dd60678d..eabea440 100644 --- a/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/internal/OkHttpUploaderTest.kt +++ b/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/internal/OkHttpUploaderTest.kt @@ -39,6 +39,7 @@ import org.mockito.kotlin.whenever import org.mockito.quality.Strictness import java.io.File import java.net.HttpURLConnection +import java.net.InetAddress import java.util.Locale import kotlin.IllegalStateException @@ -114,6 +115,7 @@ internal class OkHttpUploaderTest { mockWebServer = MockWebServer() mockDispatcher = MockDispatcher() mockWebServer.dispatcher = mockDispatcher + mockWebServer.start(InetAddress.getByName("127.0.0.1"), 0) testedUploader = OkHttpUploader() fakeUploadUrl = mockWebServer.url("/upload").toString() @@ -241,6 +243,101 @@ internal class OkHttpUploaderTest { ) } + @Test + fun `M upload to custom sourcemap endpoint W upload`() { + // Given + mockUploadResponse = MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK) + .setBody("{}") + val customUploadUrl = mockWebServer.url("/sourcemap/upload").toString() + + // When + testedUploader.upload( + mockSite, + fakeMappingFileInfo, + fakeRepositoryFile, + fakeApiKey, + fakeIdentifier, + fakeRepositoryInfo, + useGzip = false, + emulateNetworkCall = false, + customSourcemapEndpoint = customUploadUrl + ) + + // Then + assertThat(mockWebServer.requestCount).isEqualTo(1) + assertThat(dispatchedUploadRequest) + .hasMethod("POST") + .doesNotHaveHeader("Content-Encoding") + assertThat(dispatchedUploadRequest?.path).isEqualTo("/sourcemap/upload") + } + + @Test + fun `M throw InvalidApiKeyException W upload() { custom endpoint, response 403 }`() { + // Given + mockUploadResponse = MockResponse() + .setResponseCode(HttpURLConnection.HTTP_FORBIDDEN) + .setBody("{}") + val customUploadUrl = mockWebServer.url("/sourcemap/upload").toString() + + // When + assertThrows { + testedUploader.upload( + mockSite, + fakeMappingFileInfo, + fakeRepositoryFile, + fakeApiKey, + fakeIdentifier, + fakeRepositoryInfo, + useGzip = true, + emulateNetworkCall = false, + customSourcemapEndpoint = customUploadUrl + ) + } + + // Then no API key validation request is made against the predefined site + assertThat(mockWebServer.requestCount).isEqualTo(1) + assertThat(dispatchedApiKeyValidationRequest).isNull() + } + + @Test + fun `M skip API key validation W upload() { custom endpoint, response 400 }`() { + // Given + mockUploadResponse = MockResponse() + .setResponseCode(HttpURLConnection.HTTP_BAD_REQUEST) + .setBody("{}") + val customUploadUrl = mockWebServer.url("/sourcemap/upload").toString() + + // When + assertThrows { + testedUploader.upload( + mockSite, + fakeMappingFileInfo, + fakeRepositoryFile, + fakeApiKey, + fakeIdentifier, + fakeRepositoryInfo, + useGzip = true, + emulateNetworkCall = false, + customSourcemapEndpoint = customUploadUrl + ) + } + + // Then the API key validation endpoint of the predefined site is not contacted + assertThat(mockWebServer.requestCount).isEqualTo(1) + assertThat(dispatchedApiKeyValidationRequest).isNull() + } + + @Test + fun `M resolve upload endpoint W resolveSourcemapUploadEndpoint()`() { + assertThat(OkHttpUploader.resolveSourcemapUploadEndpoint("https://rum.example.com")) + .isEqualTo("https://rum.example.com/sourcemap/upload") + assertThat(OkHttpUploader.resolveSourcemapUploadEndpoint("https://rum.example.com/sourcemap/upload")) + .isEqualTo("https://rum.example.com/sourcemap/upload") + assertThat(OkHttpUploader.resolveSourcemapUploadEndpoint(" https://rum.example.com/ ")) + .isEqualTo("https://rum.example.com/sourcemap/upload") + } + @Test fun `M upload proper request W upload() {repository=null}`() { // Given @@ -654,7 +751,7 @@ internal class OkHttpUploaderTest { inner class MockDispatcher : Dispatcher() { override fun dispatch(request: RecordedRequest): MockResponse { return when (request.requestUrl?.encodedPath) { - "/upload" -> { + "/upload", "/sourcemap/upload" -> { dispatchedUploadRequest = request mockUploadResponse } diff --git a/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/utils/forge/DdExtensionConfigurationForgeryFactory.kt b/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/utils/forge/DdExtensionConfigurationForgeryFactory.kt index d7b89d90..9eafcea3 100644 --- a/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/utils/forge/DdExtensionConfigurationForgeryFactory.kt +++ b/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/utils/forge/DdExtensionConfigurationForgeryFactory.kt @@ -18,6 +18,7 @@ internal class DdExtensionConfigurationForgeryFactory : ForgeryFactory { serviceName = forge.aStringMatching("[a-z]{3}(\\.[a-z]{5,10}){2,4}") versionName = forge.aStringMatching("\\d\\.\\d{1,2}\\.\\d{1,3}") site = forge.aValueFrom(FlashcatSite::class.java).name + sourcemapEndpoint = forge.aStringMatching("https://[a-z]{4,10}\\.example\\.com") checkProjectDependencies = forge.aValueFrom(SdkCheckLevel::class.java) remoteRepositoryUrl = forge.aStringMatching( "https://[a-z]{4,10}\\.[com|org]/[a-z]{4,10}/[a-z]{4,10}\\.git" diff --git "a/\345\277\253\351\200\237\345\274\200\345\247\213.md" "b/\345\277\253\351\200\237\345\274\200\345\247\213.md" index e47c3909..f9a465b9 100644 --- "a/\345\277\253\351\200\237\345\274\200\345\247\213.md" +++ "b/\345\277\253\351\200\237\345\274\200\345\247\213.md" @@ -32,6 +32,7 @@ plugins { ```groovy flashcat { site = "CN" // Flashcat 站点:CN 或 STAGING + sourcemapEndpoint = "https://rum.example.com" // 可选,私有化 sourcemap 上传地址;未带 /sourcemap/upload 时会自动补全 versionName = "1.3.0" // 可选,默认读取 Android 配置 serviceName = "my-app" // 可选,默认使用包名 } @@ -43,6 +44,7 @@ flashcat { ```bash export FC_API_KEY="your-flashcat-api-key" +export FLASHCAT_SOURCEMAP_INTAKE_URL="https://rum.example.com" # 可选,私有化 sourcemap 上传地址 ``` **方式 2:Gradle 属性 (gradle.properties)** @@ -56,7 +58,8 @@ FC_API_KEY=your-flashcat-api-key ```json { "apiKey": "your-flashcat-api-key", - "flashcatSite": "ci.flashcat.cloud" + "flashcatSite": "ci.flashcat.cloud", + "sourcemapEndpoint": "https://rum.example.com" } ``` @@ -80,6 +83,7 @@ flashcat { site = "CN" // 或 "STAGING" // 可选配置 + sourcemapEndpoint = "https://rum.example.com" versionName = "1.3.0" serviceName = "my-service" checkProjectDependencies = "warn" // "warn", "fail" 或 "none" @@ -118,6 +122,7 @@ flashcat { ```groovy flashcat { site = "CN" // 默认配置 + sourcemapEndpoint = "https://rum.example.com" // 默认私有化上传地址 variants { production { @@ -126,6 +131,7 @@ flashcat { } staging { site = "STAGING" + sourcemapEndpoint = "https://rum-staging.example.com/sourcemap/upload" serviceName = "my-app-staging" } }