diff --git a/app/src/main/java/com/splunk/app/ui/menu/MenuFragment.kt b/app/src/main/java/com/splunk/app/ui/menu/MenuFragment.kt index bbafd8989..21fa5e60b 100644 --- a/app/src/main/java/com/splunk/app/ui/menu/MenuFragment.kt +++ b/app/src/main/java/com/splunk/app/ui/menu/MenuFragment.kt @@ -57,6 +57,8 @@ class MenuFragment : BaseFragment() { viewBinding.httpurlconnection.setOnClickListener(onClickListener) viewBinding.trackCustomEvent.setOnClickListener(onClickListener) viewBinding.trackWorkflow.setOnClickListener(onClickListener) + viewBinding.trackException.setOnClickListener(onClickListener) + viewBinding.trackExceptionWithAttributes.setOnClickListener(onClickListener) viewBinding.crashReportsIllegal.splunkRumId = "illegalButton" SplunkRUMAgent.instance.navigation.track("Menu") @@ -126,6 +128,30 @@ class MenuFragment : BaseFragment() { workflowSpan?.end() showDoneToast("Track Workflow, Done!") } + viewBinding.trackException.id -> { + val e = Exception("Custom Exception To Be Tracked"); + e.stackTrace = arrayOf( + StackTraceElement("android.fake.Crash", "crashMe", "NotARealFile.kt", 12), + StackTraceElement("android.fake.Class", "foo", "NotARealFile.kt", 34), + StackTraceElement("android.fake.Main", "main", "NotARealFile.kt", 56) + ) + SplunkRUMAgent.instance.customTracking.trackException(e) + showDoneToast("Track Exception, Done!") + } + viewBinding.trackExceptionWithAttributes.id -> { + val e = Exception("Custom Exception (with attributes) To Be Tracked"); + e.stackTrace = arrayOf( + StackTraceElement("android.fake.Crash", "crashMe", "NotARealFile.kt", 12), + StackTraceElement("android.fake.Class", "foo", "NotARealFile.kt", 34), + StackTraceElement("android.fake.Main", "main", "NotARealFile.kt", 56) + ) + val testAttributes = Attributes.builder() + .put("attribute.one", "value1") + .put("attribute.two", "12345") + .build() + SplunkRUMAgent.instance.customTracking.trackException(e, testAttributes) + showDoneToast("Track Exception with Attributes, Done!") + } } } diff --git a/app/src/main/res/layout/fragment_menu.xml b/app/src/main/res/layout/fragment_menu.xml index 00f6e9ad8..ed4b8eba6 100644 --- a/app/src/main/res/layout/fragment_menu.xml +++ b/app/src/main/res/layout/fragment_menu.xml @@ -182,8 +182,30 @@ android:layout_marginTop="5dp" android:layout_marginRight="20dp" android:text="@string/menu_workflow" - app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toBottomOf="@id/track_custom_event" /> + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7c61a7193..157ae9551 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -18,6 +18,8 @@ Custom Tracking Track Custom Event Track Workflow + Track Exception + Track Exception With Attributes OkHttp HttpURLConnection diff --git a/common/otel/src/main/java/com/splunk/sdk/common/otel/internal/RumConstants.kt b/common/otel/src/main/java/com/splunk/sdk/common/otel/internal/RumConstants.kt index 42c0dc623..fd3a15663 100644 --- a/common/otel/src/main/java/com/splunk/sdk/common/otel/internal/RumConstants.kt +++ b/common/otel/src/main/java/com/splunk/sdk/common/otel/internal/RumConstants.kt @@ -20,5 +20,12 @@ import io.opentelemetry.api.common.AttributeKey object RumConstants { const val RUM_TRACER_NAME: String = "SplunkRum" + const val COMPONENT_ERROR: String = "error" + const val COMPONENT_CRASH: String = "crash" val WORKFLOW_NAME_KEY: AttributeKey = AttributeKey.stringKey("workflow.name") + val COMPONENT_KEY: AttributeKey = AttributeKey.stringKey("component") + + val APPLICATION_ID_KEY: AttributeKey = AttributeKey.stringKey("service.application_id") + val APP_VERSION_CODE_KEY: AttributeKey = AttributeKey.stringKey("service.version_code") + val SPLUNK_OLLY_UUID_KEY: AttributeKey = AttributeKey.stringKey("service.o11y.key") } \ No newline at end of file diff --git a/common/utils/build.gradle.kts b/common/utils/build.gradle.kts new file mode 100644 index 000000000..057a701c3 --- /dev/null +++ b/common/utils/build.gradle.kts @@ -0,0 +1,28 @@ +import plugins.ConfigAndroidLibrary +import plugins.ConfigPublish +import utils.artifactIdProperty +import utils.artifactPrefix +import utils.commonPrefix +import utils.versionProperty + +plugins { + id("com.android.library") + id("kotlin-android") + id("kotlin-parcelize") +} + +apply() +apply() + +ext { + set(artifactIdProperty, "$artifactPrefix$commonPrefix${project.name}") + set(versionProperty, Configurations.sdkVersionName) +} + +android { + namespace = "com.splunk.sdk.common.utils" +} + +dependencies { + implementation(Dependencies.SessionReplay.commonLogger) +} \ No newline at end of file diff --git a/common/utils/consumer-rules.pro b/common/utils/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/common/utils/lint-baseline.xml b/common/utils/lint-baseline.xml new file mode 100644 index 000000000..27ab162a6 --- /dev/null +++ b/common/utils/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/common/utils/proguard-rules.pro b/common/utils/proguard-rules.pro new file mode 100644 index 000000000..298627d10 --- /dev/null +++ b/common/utils/proguard-rules.pro @@ -0,0 +1 @@ +-repackageclasses 'com.splunk.sdk.common.utils' \ No newline at end of file diff --git a/common/utils/src/main/java/com/splunk/sdk/utils/ApplicationInfoUtils.kt b/common/utils/src/main/java/com/splunk/sdk/utils/ApplicationInfoUtils.kt new file mode 100644 index 000000000..02882c153 --- /dev/null +++ b/common/utils/src/main/java/com/splunk/sdk/utils/ApplicationInfoUtils.kt @@ -0,0 +1,67 @@ +package com.splunk.sdk.utils + +import android.app.Application +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.os.Build +import com.cisco.android.common.logger.Logger + +class ApplicationInfoUtils { + companion object { + + private const val TAG = "ErrorIdentifier" + private const val SPLUNK_UUID_MANIFEST_KEY = "SPLUNK_O11Y_CUSTOM_UUID" + + fun retrieveApplicationId(application: Application): String? { + val packageManager: PackageManager = application.packageManager + val applicationInfo: ApplicationInfo? + + try { + applicationInfo = packageManager.getApplicationInfo(application.packageName, PackageManager.GET_META_DATA) + } catch (e: Exception) { + Logger.e(TAG, "Failed to retrieve ApplicationInfo: ${e.message}") + return null + } + + return applicationInfo.packageName ?: run { + Logger.e(TAG, "ApplicationInfo is null, cannot extract applicationId") + null + } + } + + fun retrieveVersionCode(application: Application): String? { + val packageManager: PackageManager = application.packageManager + + return try { + val packageInfo = packageManager.getPackageInfo(application.packageName, 0) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + packageInfo.longVersionCode.toString() + } else { + packageInfo.versionCode.toString() + } + } catch (e: Exception) { + Logger.e(TAG, "Failed to get application version code", e) + null + } + } + + fun retrieveCustomUUID(application: Application): String? { + val packageManager: PackageManager = application.packageManager + val applicationInfo: ApplicationInfo? + + try { + applicationInfo = packageManager.getApplicationInfo(application.packageName, PackageManager.GET_META_DATA) + } catch (e: Exception) { + Logger.e(TAG, "Failed to retrieve ApplicationInfo: ${e.message}") + return null + } + + return applicationInfo.metaData?.getString(SPLUNK_UUID_MANIFEST_KEY)?.takeIf { + it.isNotEmpty() + } ?: run { + Logger.e(TAG, "Application MetaData bundle is null or does not contain the UUID") + null + } + } + } +} diff --git a/instrumentation/runtime/customtracking/src/main/java/com/splunk/rum/customtracking/CustomTracking.kt b/instrumentation/runtime/customtracking/src/main/java/com/splunk/rum/customtracking/CustomTracking.kt index 86672082f..4a6dafb4b 100644 --- a/instrumentation/runtime/customtracking/src/main/java/com/splunk/rum/customtracking/CustomTracking.kt +++ b/instrumentation/runtime/customtracking/src/main/java/com/splunk/rum/customtracking/CustomTracking.kt @@ -23,7 +23,6 @@ import io.opentelemetry.api.common.Attributes import io.opentelemetry.api.trace.Span import io.opentelemetry.api.trace.Tracer - class CustomTracking internal constructor() { /** @@ -53,6 +52,42 @@ class CustomTracking internal constructor() { .startSpan() } + /** + * Add a custom exception to RUM monitoring. This can be useful for tracking custom error + * handling in your application. + * + * + * This event will be turned into a Span and sent to the RUM ingest along with other, + * auto-generated spans. + * + * @param throwable A [Throwable] associated with this event. + */ + fun trackException(throwable: Throwable) { + trackException(throwable, null) + } + + /** + * Add a custom exception to RUM monitoring. This can be useful for tracking custom error + * handling in your application. + * + * + * This event will be turned into a Span and sent to the RUM ingest along with other, + * auto-generated spans. + * + * @param throwable A [Throwable] associated with this event. + * @param attributes Any [Attributes] to associate with the event. + */ + fun trackException(throwable: Throwable, attributes: Attributes?) { + val tracer = getTracer() ?: return + val spanBuilder = tracer.spanBuilder(throwable.javaClass.simpleName) + attributes?.let { + spanBuilder.setAllAttributes(it) + } + spanBuilder.setAttribute(RumConstants.COMPONENT_KEY, RumConstants.COMPONENT_ERROR) + .startSpan() + .recordException(throwable) + .end() + } /** * Retrieves the Tracer instance for the application. diff --git a/integration/agent/api/build.gradle.kts b/integration/agent/api/build.gradle.kts index 1cfdd6c7d..177f87e63 100644 --- a/integration/agent/api/build.gradle.kts +++ b/integration/agent/api/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation(Dependencies.SessionReplay.commonLogger) implementation(Dependencies.SessionReplay.commonStorage) implementation(Dependencies.SessionReplay.commonUtils) + implementation(project(":common:utils")) compileOnly(Dependencies.Android.Compose.ui) } diff --git a/integration/agent/api/src/main/java/com/splunk/rum/integration/agent/api/attributes/ErrorIdentifierAttributesSpanProcessor.kt b/integration/agent/api/src/main/java/com/splunk/rum/integration/agent/api/attributes/ErrorIdentifierAttributesSpanProcessor.kt new file mode 100644 index 000000000..cfb07e2e7 --- /dev/null +++ b/integration/agent/api/src/main/java/com/splunk/rum/integration/agent/api/attributes/ErrorIdentifierAttributesSpanProcessor.kt @@ -0,0 +1,43 @@ +package com.splunk.rum.integration.agent.api.attributes + +import android.app.Application +import com.splunk.sdk.common.otel.internal.RumConstants +import com.splunk.sdk.utils.ApplicationInfoUtils +import io.opentelemetry.context.Context +import io.opentelemetry.sdk.trace.ReadWriteSpan +import io.opentelemetry.sdk.trace.ReadableSpan +import io.opentelemetry.sdk.trace.SpanProcessor + +internal class ErrorIdentifierAttributesSpanProcessor(application: Application) : SpanProcessor { + + private var applicationId: String? = null + private var versionCode: String? = null + private var customUUID: String? = null + + init { + applicationId = ApplicationInfoUtils.retrieveApplicationId(application) + versionCode = ApplicationInfoUtils.retrieveVersionCode(application) + customUUID = ApplicationInfoUtils.retrieveCustomUUID(application) + } + + override fun onStart(parentContext: Context, span: ReadWriteSpan) { + if (span.getAttribute(RumConstants.COMPONENT_KEY) == RumConstants.COMPONENT_ERROR || + span.getAttribute(RumConstants.COMPONENT_KEY) == RumConstants.COMPONENT_CRASH) { + applicationId?.let { + span.setAttribute(RumConstants.APPLICATION_ID_KEY, it) + } + versionCode?.let { + span.setAttribute(RumConstants.APP_VERSION_CODE_KEY, it) + } + customUUID?.let { + span.setAttribute(RumConstants.SPLUNK_OLLY_UUID_KEY, it) + } + } + } + + override fun isStartRequired(): Boolean = true + + override fun onEnd(span: ReadableSpan) = Unit + + override fun isEndRequired(): Boolean = false +} diff --git a/integration/agent/api/src/main/java/com/splunk/rum/integration/agent/api/internal/MRUMAgentCore.kt b/integration/agent/api/src/main/java/com/splunk/rum/integration/agent/api/internal/MRUMAgentCore.kt index 8e7d02bc7..e2bdd95e1 100644 --- a/integration/agent/api/src/main/java/com/splunk/rum/integration/agent/api/internal/MRUMAgentCore.kt +++ b/integration/agent/api/src/main/java/com/splunk/rum/integration/agent/api/internal/MRUMAgentCore.kt @@ -21,6 +21,7 @@ import com.cisco.android.common.logger.Logger import com.cisco.android.common.logger.consumers.AndroidLogConsumer import com.splunk.sdk.common.otel.OpenTelemetryInitializer import com.splunk.rum.integration.agent.api.AgentConfiguration +import com.splunk.rum.integration.agent.api.attributes.ErrorIdentifierAttributesSpanProcessor import com.splunk.rum.integration.agent.api.attributes.GenericAttributesLogProcessor import com.splunk.rum.integration.agent.api.configuration.ConfigurationManager import com.splunk.rum.integration.agent.api.extension.toResource @@ -39,7 +40,6 @@ import com.splunk.sdk.common.storage.AgentStorage internal object MRUMAgentCore { private const val TAG = "MRUMAgentCore" - private const val SERVICE_HASH_RESOURCE_KEY = "service.hash" fun install(application: Application, agentConfiguration: AgentConfiguration, moduleConfigurations: List) { if (agentConfiguration.isDebugLogsEnabled) { @@ -68,6 +68,7 @@ internal object MRUMAgentCore { OpenTelemetryInitializer(application) .joinResources(finalConfiguration.toResource()) + .addSpanProcessor(ErrorIdentifierAttributesSpanProcessor(application)) .addSpanProcessor(SessionIdSpanProcessor(agentIntegration.sessionManager)) .addSpanProcessor(GlobalAttributeSpanProcessor()) .addLogRecordProcessor(GenericAttributesLogProcessor()) diff --git a/settings.gradle b/settings.gradle index 58ffa91a2..442901a8d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,7 @@ // Common modules include ':common:otel', - ':common:storage' + ':common:storage', + ':common:utils' // Instrumentation modules include ':instrumentation:runtime:startup', @@ -24,3 +25,4 @@ include ':app' // Main entry point include ':agent' +include ':instrumentation:runtime:customtracking'