Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
Expand Up @@ -313,6 +313,8 @@ public void start() {
Collection<AnalyticsEvent> queuedEvents;
EventManager eventManager = analyticsController.getEventManager();

// turn the flag on
FeatureFlag.enableFeature(FeatureFlag.DistributedTracing);

agentImpl.start();
assertEquals("Should contain app launch user action event", eventManager.getEventsRecorded(), 1);
Expand All @@ -336,6 +338,33 @@ public void start() {
assertEquals("Should contain lifecycle user action events", eventManager.getEventsRecorded(), 3);
queuedEvents = analyticsController.getEventManager().getQueuedEvents();
Assert.assertNotNull("Should contain app background event", getEventByActionType(queuedEvents, UserActionType.AppBackground));


// turn the flag back off
FeatureFlag.disableFeature(FeatureFlag.DistributedTracing);
eventManager.empty();
eventStore.clear();

agentImpl.start();
assertEquals("Should not contain app launch user action event", eventManager.getEventsRecorded(), 1);
queuedEvents = analyticsController.getEventManager().getQueuedEvents();
Assert.assertNull("Should contain app launch event", getEventByActionType(queuedEvents, UserActionType.AppLaunch));

agentImpl.applicationBackgrounded(e);
eventStore.clear();
assertEquals("Should not contain lifecycle user action events", eventManager.getEventsRecorded(), 1);
queuedEvents = analyticsController.getEventManager().getQueuedEvents();
Assert.assertNull("Should contain app background event", getEventByActionType(queuedEvents, UserActionType.AppBackground));

agentImpl.applicationForegrounded(e);
assertEquals("Should not contain foreground user action events", eventManager.getEventsRecorded(), 0);
queuedEvents = analyticsController.getEventManager().getQueuedEvents();
Assert.assertNull("Should contain foreground (app launch) event", getEventByActionType(queuedEvents, UserActionType.AppLaunch));

agentImpl.stop();
assertEquals("Should not contain lifecycle user action events", eventManager.getEventsRecorded(), 1);
queuedEvents = analyticsController.getEventManager().getQueuedEvents();
Assert.assertNull("Should contain app background event", getEventByActionType(queuedEvents, UserActionType.AppBackground));
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,12 @@ class BuildHelperTest extends PluginTest {
Assert.assertTrue(buildHelper.checkLibrary())
}

@Test
void checkReactNative() {
// Default should be false (no React Native in test project)
Assert.assertFalse(buildHelper.checkReactNative())
}

@Test
void getMapCompilerName() {
Assert.assertEquals(buildHelper.getMapCompilerName(), Proguard.Provider.DEFAULT)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,4 +166,46 @@ class NewRelicExtensionTest extends PluginTest {
Assert.assertFalse(ext.shouldIncludeMapUpload("KeViN"))
}

@Test
void reactNativeSourceMapUploadEnabled() {
// Default should be true
Assert.assertTrue(ext.reactNativeSourceMapUploadEnabled.get())

ext.reactNativeSourceMapUploadEnabled.set(false)
Assert.assertFalse(ext.reactNativeSourceMapUploadEnabled.get())

ext.reactNativeSourceMapUploadEnabled.set(true)
Assert.assertTrue(ext.reactNativeSourceMapUploadEnabled.get())
}

@Test
void shouldUploadReactNativeSourceMap() {
// Default should use same config as map uploads (release by default)
Assert.assertTrue(ext.shouldUploadReactNativeSourceMap("release"))
Assert.assertTrue(ext.shouldUploadReactNativeSourceMap("productionRelease"))
Assert.assertFalse(ext.shouldUploadReactNativeSourceMap("debug"))

// Test with custom variant configuration
ext.uploadMapsForVariant("staging", "production")
Assert.assertTrue(ext.shouldUploadReactNativeSourceMap("staging"))
Assert.assertTrue(ext.shouldUploadReactNativeSourceMap("production"))
Assert.assertFalse(ext.shouldUploadReactNativeSourceMap("release"))

// Test when disabled
ext.reactNativeSourceMapUploadEnabled.set(false)
Assert.assertFalse(ext.shouldUploadReactNativeSourceMap("staging"))
Assert.assertFalse(ext.shouldUploadReactNativeSourceMap("production"))
}

@Test
void shouldUploadReactNativeSourceMapUsesVariantMapConfig() {
// Verify React Native source map upload uses the same config as mapping file uploads
ext.uploadMapsForVariant("customRelease", "customStaging")

// Both should have same behavior
Assert.assertEquals(ext.shouldIncludeMapUpload("customRelease"), ext.shouldUploadReactNativeSourceMap("customRelease"))
Assert.assertEquals(ext.shouldIncludeMapUpload("customStaging"), ext.shouldUploadReactNativeSourceMap("customStaging"))
Assert.assertEquals(ext.shouldIncludeMapUpload("debug"), ext.shouldUploadReactNativeSourceMap("debug"))
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright (c) 2023. New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package com.newrelic.agent.android

import org.junit.Assert
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class NewRelicReactNativeSourceMapUploadTaskTest extends PluginTest {
def provider

NewRelicReactNativeSourceMapUploadTaskTest() {
super(true)
}

@BeforeEach
void setup() {
provider = plugin.buildHelper.variantAdapter.getReactNativeSourceMapUploadProvider("release").get()
}

@Test
void getVariantName() {
Assert.assertEquals("release", provider.getVariantName().get())
}

@Test
void getProjectRoot() {
Assert.assertEquals(project.layout.projectDirectory, provider.getProjectRoot().get())
}

@Test
void getBuildId() {
def buildId = provider.getBuildId().get()
Assert.assertFalse(buildId.isEmpty())
Assert.assertFalse(UUID.fromString(buildId).toString().isEmpty())
}

@Test
void getAppVersionId() {
def appVersion = provider.getAppVersionId().get()
Assert.assertNotNull(appVersion)
// Default version from test build.gradle or fallback
Assert.assertFalse(appVersion.isEmpty())
}

@Test
void getSourceMapFile() {
def sourceMapFile = provider.getSourceMapFile()
// Source map file provider should be set (though file may not exist in test)
Assert.assertNotNull(sourceMapFile)
}

@Test
void getLogger() {
Assert.assertEquals(provider.getLogger(), NewRelicGradlePlugin.LOGGER)
}

@Test
void wiredTaskNames() {
def taskNames = NewRelicReactNativeSourceMapUploadTask.wiredTaskNames("Release")
Assert.assertTrue(taskNames.contains("bundleReleaseJsAndAssets"))
Assert.assertTrue(taskNames.contains("createBundleReleaseJsAndAssets"))
}

@Test
void taskName() {
Assert.assertEquals("newrelicReactNativeSourceMapUpload", NewRelicReactNativeSourceMapUploadTask.NAME)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,45 @@ class BuildHelper {
return project.plugins.hasPlugin("com.android.library")
}

/**
* Detect if this is a React Native project.
* Checks for the presence of react.gradle in node_modules or React Native bundle tasks.
* @return true if React Native is detected
*/
boolean checkReactNative() {
// Method 1: Check for react.gradle file in node_modules
// React Native projects typically have node_modules at the project root level (../../ from android/app)
def reactGradlePaths = [
project.file("../../node_modules/react-native/react.gradle"),
project.file("../node_modules/react-native/react.gradle"),
project.file("node_modules/react-native/react.gradle"),
project.rootProject.file("node_modules/react-native/react.gradle"),
]

for (def reactGradle : reactGradlePaths) {
if (reactGradle.exists()) {
logger.debug("React Native detected via react.gradle at: ${reactGradle.absolutePath}")
return true
}
}

// Method 2: Check for React Native bundle tasks (created by react.gradle)
// These tasks are created when react.gradle is applied
try {
def bundleTaskNames = ["bundleReleaseJsAndAssets", "createBundleReleaseJsAndAssets"]
for (def taskName : bundleTaskNames) {
if (project.tasks.findByName(taskName) != null) {
logger.debug("React Native detected via task: ${taskName}")
return true
}
}
} catch (Exception ignored) {
// Task lookup may fail during configuration phase
}

return false
}


/**
* Returns literal name of obfuscation compiler
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ abstract class NewRelicExtension {
Property<Boolean> logInstrumentationEnabled
Property<Boolean> defaultInteractionsEnabled
Property<Boolean> webviewInstrumentationEnabled
Property<Boolean> reactNativeSourceMapUploadEnabled


NamedDomainObjectContainer<VariantConfiguration> variantConfigurations
Expand All @@ -56,6 +57,7 @@ abstract class NewRelicExtension {
this.logInstrumentationEnabled = objectFactory.property(Boolean.class).convention(true)
this.defaultInteractionsEnabled = objectFactory.property(Boolean.class).convention(true)
this.webviewInstrumentationEnabled = objectFactory.property(Boolean.class).convention(true)
this.reactNativeSourceMapUploadEnabled = objectFactory.property(Boolean.class).convention(true)
this.variantConfigurations = objectFactory.domainObjectContainer(VariantConfiguration, { name ->
objectFactory.newInstance(VariantConfiguration.class, name)
})
Expand Down Expand Up @@ -184,4 +186,17 @@ abstract class NewRelicExtension {
pkg.toLowerCase().startsWith(it) || pkg.toLowerCase().matches(it)
}.empty
}

/**
* Check if React Native source maps should be uploaded for the given variant.
* Uses the same variantMapUploads configuration as ProGuard/R8 mapping file uploads.
*/
boolean shouldUploadReactNativeSourceMap(String variantName) {
if (!reactNativeSourceMapUploadEnabled.get()) {
return false
}

// Reuse the same variant configuration as mapping file uploads
return shouldIncludeMapUpload(variantName)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ class NewRelicGradlePlugin implements Plugin<Project> {
LOGGER.info("DexGuard detected " + buildHelper.dexguardHelper?.currentVersion)
}

if (buildHelper.checkReactNative()) {
LOGGER.info("React Native detected. Source map uploads will be configured for applicable variants.")
}

if (buildHelper.checkApplication()) {
LOGGER.info("BuildMetrics[${buildHelper.getBuildMetrics()}]")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright (c) 2022 - present. New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package com.newrelic.agent.android

import com.google.common.io.BaseEncoding
import com.newrelic.agent.InstrumentationAgent
import com.newrelic.agent.android.obfuscation.ReactNativeSourceMap
import com.newrelic.agent.util.BuildId
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.logging.Logger
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.TaskAction

/**
* Gradle task to upload React Native source maps to New Relic Symbol Ingest API.
*/
abstract class NewRelicReactNativeSourceMapUploadTask extends DefaultTask {
final static String NAME = "newrelicReactNativeSourceMapUpload"

@InputFile
@Optional
abstract RegularFileProperty getSourceMapFile()

@Input
abstract Property<String> getVariantName()

@Input
abstract Property<String> getBuildId()

@Input
abstract Property<String> getAppVersionId()

@Internal
abstract DirectoryProperty getProjectRoot()

@TaskAction
def uploadReactNativeSourceMap() {
try {
def propertiesFound = false
def agentOptions = InstrumentationAgent.getAgentOptions()
def filePattern = ~/${ReactNativeSourceMap.NR_PROPERTIES}/

// Start search for properties at project's root dir
projectRoot.get().asFile.eachFileRecurse {
if (filePattern.matcher(it.name).find()) {
logger.debug("Found properties [${it.absolutePath}]")
agentOptions.put(ReactNativeSourceMap.PROJECT_ROOT_KEY, new String(BaseEncoding.base64().encode(it.getParent().bytes)))
propertiesFound = true
}
}

if (!propertiesFound) {
logger.error("newrelic.properties was not found! React Native source map for variant [${variantName.get()}] not uploaded.")
return
}

if (sourceMapFile.isPresent()) {
def sourceMap = sourceMapFile.asFile.get()

if (sourceMap?.exists()) {
logger.debug("React Native source map for variant [${variantName.get()}] detected: [${sourceMap.absolutePath}]")

new ReactNativeSourceMap(NewRelicGradlePlugin.LOGGER, agentOptions)
.uploadSourceMap(sourceMap, buildId.get(), appVersionId.get())
} else {
logger.debug("No React Native source map for variant [${variantName.get()}] detected at: [${sourceMap?.absolutePath}]")
}
} else {
logger.warn("React Native source map file not specified for variant [${variantName.get()}]")
}

} catch (Exception e) {
logger.error("NewRelicReactNativeSourceMapUploadTask: " + e)
}
}

@Internal
@Override
Logger getLogger() {
return NewRelicGradlePlugin.LOGGER
}

/**
* Returns the set of React Native bundle task names that this task should run after.
*/
static Set<String> wiredTaskNames(String vnc) {
return Set.of(
"bundle${vnc}JsAndAssets",
"createBundle${vnc}JsAndAssets",
)
}
}
Loading
Loading