Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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"
}
}
Expand All @@ -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"
}
}
Expand Down Expand Up @@ -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)
Expand All @@ -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"
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -140,6 +146,7 @@ abstract class FileUploadTask @Inject constructor(
applyFlashcatCiConfig(it)
}
applySiteFromEnvironment()
applySourcemapEndpointFromEnvironment()
validateConfiguration()

check(!(apiKey.contains("\"") || apiKey.contains("'"))) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,19 @@ 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}")
}
LOGGER.info("\n")
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)
Expand Down Expand Up @@ -97,7 +99,7 @@ internal class OkHttpUploader : Uploader {
null
}

handleResponse(response, site, apiKey, identifier)
handleResponse(response, site, apiKey, identifier, uploadEndpoint, customSourcemapEndpoint != null)
}

// endregion
Expand Down Expand Up @@ -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 {
Expand All @@ -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; " +
Expand All @@ -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(
Expand Down Expand Up @@ -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"
)

Expand Down Expand Up @@ -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"
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ internal interface Uploader {
identifier: DdAppIdentifier,
repositoryInfo: RepositoryInfo?,
useGzip: Boolean = true,
emulateNetworkCall: Boolean = false
emulateNetworkCall: Boolean = false,
customSourcemapEndpoint: String? = null
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading