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
Original file line number Diff line number Diff line change
@@ -1,50 +1,19 @@
package io.embrace.android.embracesdk.internal.buildinfo

import android.content.res.Resources
import io.embrace.android.embracesdk.internal.AndroidResourcesService
import io.embrace.android.embracesdk.internal.config.instrumented.schema.InstrumentedConfig

internal class BuildInfoServiceImpl(
private val instrumentedConfig: InstrumentedConfig,
private val resources: AndroidResourcesService,
private val packageName: String,
) : BuildInfoService {

companion object {
private const val BUILD_INFO_RN_BUNDLE_ID: String = "emb_rn_bundle_id"
private const val RES_TYPE_STRING = "string"
}

private val info by lazy {
BuildInfo(
instrumentedConfig.project.getBuildId(),
instrumentedConfig.project.getBuildType(),
instrumentedConfig.project.getBuildFlavor(),
getBuildResource(resources, packageName, BUILD_INFO_RN_BUNDLE_ID),
instrumentedConfig.project.getReactNativeBundleId(),
)
}

override fun getBuildInfo(): BuildInfo = info

/**
* Given a build property name and a build property type, retrieves the embrace build resource value.
*/
fun getBuildResource(
resources: AndroidResourcesService,
packageName: String,
buildProperty: String,
): String? {
return try {
val resourceId =
resources.getIdentifier(buildProperty, RES_TYPE_STRING, packageName)
resources.getString(resourceId)
} catch (ex: NullPointerException) {
throw IllegalArgumentException(
"No resource found for $buildProperty property. Failed to create build info.",
ex
)
} catch (ex: Resources.NotFoundException) {
null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,6 @@ class CoreModuleImpl(
}

override val buildInfoService: BuildInfoService by lazy {
BuildInfoServiceImpl(initModule.instrumentedConfig, resources, context.packageName)
BuildInfoServiceImpl(initModule.instrumentedConfig)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,11 @@ interface ProjectConfig {
* This is not possible to specify in the embrace-config.json.
*/
fun getBuildFlavor(): String? = null

/**
* The project's React Native bundleId
*
* This is not possible to specify in the embrace-config.json.
*/
fun getReactNativeBundleId(): String? = null
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ class FakeBytecodeInstrumentationParams(
get() = TODO("Not yet implemented")
override val encodedSharedObjectFilesMap: RegularFileProperty
get() = TODO("Not yet implemented")
override val reactNativeBundleId: RegularFileProperty
get() = TODO("Not yet implemented")
override val classInstrumentationFilter: Property<ClassInstrumentationFilter>
get() = TODO("Not yet implemented")
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ android {
}

embrace {
autoAddEmbraceDependencies.set(false)
autoAddEmbraceDependencies.set(true)
}

dependencies {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import io.embrace.android.gradle.plugin.tasks.reactnative.EmbraceRnSourcemapGeneratorTask
import io.embrace.android.gradle.plugin.tasks.reactnative.GenerateRnSourcemapTask
import io.embrace.android.gradle.plugin.network.EmbraceEndpoint

plugins {
Expand All @@ -7,12 +7,9 @@ plugins {
id("io.embrace.android.testplugin")
}

project.tasks.register("testTask", EmbraceRnSourcemapGeneratorTask) { task ->
project.tasks.register("testTask", GenerateRnSourcemapTask) { task ->
integrationTest.configureGradleUploadTask(project, task, EmbraceEndpoint.SOURCE_MAP, "sourcemap.json")

task.generatedEmbraceResourcesDirectory.set(
project.layout.buildDirectory.dir("generated-embrace-resources")
)
task.sourcemap.set(
project.layout.projectDirectory.file("source.map")
)
Expand All @@ -22,4 +19,7 @@ project.tasks.register("testTask", EmbraceRnSourcemapGeneratorTask) { task ->
task.sourcemapAndBundleFile.set(
project.layout.buildDirectory.file("output")
)
task.bundleIdOutputFile.set(
project.layout.buildDirectory.file("bundleIdFile.txt")
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import com.squareup.moshi.JsonClass
import io.embrace.android.gradle.integration.framework.IntegrationTestDefaults
import io.embrace.android.gradle.integration.framework.PluginIntegrationTestRule
import io.embrace.android.gradle.integration.framework.buildFile
import io.embrace.android.gradle.integration.framework.file
import io.embrace.android.gradle.network.FormPart
import io.embrace.android.gradle.network.validateBodyApiToken
import io.embrace.android.gradle.network.validateBodyAppId
Expand All @@ -20,7 +19,7 @@ import org.junit.Rule
import org.junit.Test
import java.io.File

class RnSourcemapTaskIntegrationTest {
class GenerateRnSourcemapTaskIntegrationTest {

@Rule
@JvmField
Expand All @@ -31,19 +30,14 @@ class RnSourcemapTaskIntegrationTest {
rule.runTest(
fixture = "rn-upload-simple",
assertions = { projectDir ->
// 1. assert that RN bundle ID was injected as a string resource
val symbols = projectDir.buildFile("generated-embrace-resources/values/rn_sourcemap.xml")
val expectedResXml = projectDir.file("expected/rn_sourcemap.xml").readText()
assertEquals(expectedResXml, symbols.readText())

// 2. assert that the task output was the bundle + sourcemap file zipped as JSON
// 1. assert that the task output was the bundle + sourcemap file zipped as JSON
verifyOutputFile(projectDir.buildFile("output"))

// 3. assert that a request took place
// 2. assert that a request took place
val request = fetchRequest(EmbraceEndpoint.SOURCE_MAP)
assertHeaders(request, "multipart/form-data", "abcde")

// 4. assert the multipart form data contains bundle info
// 3. assert the multipart form data contains bundle info
val parts: List<FormPart> = readMultipartRequest(request)
assertEquals(4, parts.size)
parts[0].validateBodyAppId(IntegrationTestDefaults.APP_ID)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package io.embrace.android.gradle.integration.testcases

import io.embrace.android.gradle.integration.framework.ApkDisassembler
import io.embrace.android.gradle.integration.framework.PluginIntegrationTestRule
import org.junit.Assert.assertNotNull
import io.embrace.android.gradle.integration.framework.smali.SmaliConfigReader
import io.embrace.android.gradle.integration.framework.smali.SmaliMethod
import io.embrace.android.gradle.integration.framework.smali.SmaliParser
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import java.io.File
Expand Down Expand Up @@ -50,19 +52,11 @@ class ReactNativeAndroidTest {
verifyBuildTelemetryRequestSent(defaultExpectedVariants)
verifyHandshakes(defaultExpectedLibs, defaultExpectedArchs, defaultExpectedVariants)
verifyUploads(handshakeLibs, handshakeArchs, defaultExpectedVariants)

val filename = "app/build/outputs/apk/release/app-release.apk"
verifyBundleIdInjection(File(projectDir, filename))
verifyAsmInjection(projectDir)
}
)
}

private fun verifyBundleIdInjection(apkFile: File) {
val decodedApk = ApkDisassembler().disassembleApk(apkFile)
val bundleId = decodedApk.getStringResource("emb_rn_bundle_id")
assertNotNull(bundleId)
}

private fun installNodeModules(projectDir: File) {
val process = ProcessBuilder("npm", "install")
.directory(projectDir)
Expand All @@ -76,4 +70,20 @@ class ReactNativeAndroidTest {
error("npm install failed with exit code $exitCode\n$output")
}
}

private fun verifyAsmInjection(projectDir: File) {
// Read and parse the smali file containing the injected symbols
val smaliFile = SmaliConfigReader().readSmaliFiles(
File(projectDir, "app"),
listOf("/io/embrace/android/embracesdk/internal/config/instrumented/ProjectConfigImpl")
).first()

// Get the return value of the getBase64SharedObjectFilesMap method
val method = SmaliParser().parse(
smaliFile,
listOf(SmaliMethod("getReactNativeBundleId()Ljava/lang/String;"))
).methods.first()

assertEquals("765FB008173DC25D016D112F67250241", method.returnValue)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.android.build.api.instrumentation.FramesComputationMode
import com.android.build.api.instrumentation.InstrumentationScope
import io.embrace.android.gradle.plugin.gradle.tryGetTaskProvider
import io.embrace.android.gradle.plugin.tasks.ndk.EncodeFileToBase64Task
import io.embrace.android.gradle.plugin.tasks.reactnative.GenerateRnSourcemapTask
import io.embrace.android.gradle.plugin.tasks.registration.EmbraceTaskRegistration
import io.embrace.android.gradle.plugin.tasks.registration.RegistrationParams
import io.embrace.android.gradle.plugin.util.capitalizedString
Expand Down Expand Up @@ -56,6 +57,15 @@ class AsmTaskRegistration : EmbraceTaskRegistration {
it.outputFile
}
)

val reactNativeTask = project.tryGetTaskProvider(
"${GenerateRnSourcemapTask.NAME}${data.name.capitalizedString()}",
GenerateRnSourcemapTask::class.java
) ?: return@afterEvaluate

asmTransformationTask.configure { it.dependsOn(reactNativeTask) }

params.reactNativeBundleId.set(reactNativeTask.flatMap { it.bundleIdOutputFile })
}
}
} catch (exception: Exception) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ interface BytecodeInstrumentationParams : InstrumentationParameters {
@get:PathSensitive(PathSensitivity.NONE)
val encodedSharedObjectFilesMap: RegularFileProperty

/**
* React native bundle ID to be injected in the SDK.
*/
@get:Optional
@get:InputFiles
@get:PathSensitive(PathSensitivity.NONE)
val reactNativeBundleId: RegularFileProperty

/**
* Whether or not the plugin should operate for this variant.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.android.build.api.instrumentation.ClassContext
import com.android.build.api.instrumentation.ClassData
import io.embrace.android.gradle.plugin.instrumentation.config.ConfigClassVisitorFactory
import io.embrace.android.gradle.plugin.instrumentation.json.readBytecodeInstrumentationFeatures
import org.gradle.api.file.RegularFileProperty
import org.objectweb.asm.ClassVisitor

/**
Expand All @@ -28,12 +29,9 @@ abstract class EmbraceClassVisitorFactory : AsmClassVisitorFactory<BytecodeInstr
// Add a visitor if this is a config class provided by the SDK
val params = parameters.get()
val cfg = params.config.get()
val encodedSharedObjectFilesMap = parameters.get().encodedSharedObjectFilesMap.orNull
?.asFile
?.takeIf { it.exists() }
?.bufferedReader()
?.use { it.readText() }
ConfigClassVisitorFactory.createClassVisitor(className, cfg, encodedSharedObjectFilesMap, api, visitor)?.let {
val reactNativeBundleId = readTextFromFile(params.reactNativeBundleId)
val encodedSharedObjectFilesMap = readTextFromFile(params.encodedSharedObjectFilesMap)
ConfigClassVisitorFactory.createClassVisitor(className, cfg, encodedSharedObjectFilesMap, reactNativeBundleId, api, visitor)?.let {
visitor = it
}

Expand All @@ -57,4 +55,10 @@ abstract class EmbraceClassVisitorFactory : AsmClassVisitorFactory<BytecodeInstr
}
return true
}

private fun readTextFromFile(file: RegularFileProperty) =
file.orNull?.asFile
?.takeIf { it.exists() }
?.bufferedReader()
?.use { it.readText() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ object ConfigClassVisitorFactory {
fun createClassVisitor(
cfg: VariantConfig,
encodedSharedObjectFilesMap: String?,
reactNativeBundleId: String?,
api: Int,
cv: ClassVisitor?
cv: ClassVisitor?,
): ClassVisitor {
val instrumentation = when (this) {
BaseUrlConfig -> createBaseUrlConfigInstrumentation(cfg)
EnabledFeatureConfig -> createEnabledFeatureConfigInstrumentation(cfg)
NetworkCaptureConfig -> createNetworkCaptureConfigInstrumentation(cfg)
ProjectConfig -> createProjectConfigInstrumentation(cfg)
ProjectConfig -> createProjectConfigInstrumentation(cfg, reactNativeBundleId)
RedactionConfig -> createRedactionConfigInstrumentation(cfg)
SessionConfig -> createSessionConfigInstrumentation(cfg)
Base64SharedObjectFilesMap -> createSharedObjectFilesMapInstrumentation(encodedSharedObjectFilesMap)
Expand All @@ -50,10 +51,11 @@ object ConfigClassVisitorFactory {
className: String,
cfg: VariantConfig,
encodedSharedObjectFilesMap: String?,
reactNativeBundleId: String?,
api: Int,
cv: ClassVisitor?
cv: ClassVisitor?,
): ClassVisitor? {
val type = ConfigClassType.values().singleOrNull { it.className == className }
return type?.createClassVisitor(cfg, encodedSharedObjectFilesMap, api, cv)
return type?.createClassVisitor(cfg, encodedSharedObjectFilesMap, reactNativeBundleId, api, cv)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import io.embrace.android.gradle.plugin.instrumentation.config.arch.modelSdkConf
import io.embrace.android.gradle.plugin.instrumentation.config.arch.stringMethod
import io.embrace.android.gradle.plugin.instrumentation.config.model.VariantConfig

fun createProjectConfigInstrumentation(cfg: VariantConfig) = modelSdkConfigClass {
fun createProjectConfigInstrumentation(cfg: VariantConfig, reactNativeBundleId: String?) = modelSdkConfigClass {
Copy link
Copy Markdown
Contributor

@bidetofevil bidetofevil May 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why this is parallel to the config object rather than part of it?

It's not part of the Embrace config that we derive from the config file, but it certain seems parallel to things like buildId

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's mostly because we can't access it at the same time, but it'd be nice to aggregate the parameters in a class in the future, especially in createClassVisitor.

stringMethod("getAppId") { cfg.embraceConfig?.appId }
stringMethod("getAppFramework") { cfg.embraceConfig?.sdkConfig?.appFramework }
stringMethod("getBuildId") { cfg.buildId }
stringMethod("getBuildFlavor") { cfg.buildFlavor }
stringMethod("getBuildType") { cfg.buildType }
stringMethod("getReactNativeBundleId") { reactNativeBundleId }
}

This file was deleted.

Loading
Loading