Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Track Exception APIs, and Error Identifier Attributes Span Processor #1172

Open
wants to merge 10 commits into
base: feature/next-gen
Choose a base branch
from
26 changes: 26 additions & 0 deletions app/src/main/java/com/splunk/app/ui/menu/MenuFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ class MenuFragment : BaseFragment<FragmentMenuBinding>() {
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")
Expand Down Expand Up @@ -126,6 +128,30 @@ class MenuFragment : BaseFragment<FragmentMenuBinding>() {
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!")
}
}
}

Expand Down
24 changes: 23 additions & 1 deletion app/src/main/res/layout/fragment_menu.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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" />

<TextView
android:id="@+id/track_exception"
style="@style/Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="5dp"
android:layout_marginRight="20dp"
android:text="@string/menu_track_exception"
app:layout_constraintTop_toBottomOf="@id/track_workflow" />

<TextView
android:id="@+id/track_exception_with_attributes"
style="@style/Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="5dp"
android:layout_marginRight="20dp"
android:text="@string/menu_track_exception_with_attributes"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/track_exception" />

</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
<string name="menu_custom_tracking_title">Custom Tracking</string>
<string name="menu_custom_event">Track Custom Event</string>
<string name="menu_workflow">Track Workflow</string>
<string name="menu_track_exception">Track Exception</string>
<string name="menu_track_exception_with_attributes">Track Exception With Attributes</string>

<string name="okhttp_title">OkHttp</string>
<string name="httpurlconnection_title">HttpURLConnection</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = AttributeKey.stringKey("workflow.name")
val COMPONENT_KEY: AttributeKey<String> = AttributeKey.stringKey("component")

val APPLICATION_ID_KEY: AttributeKey<String> = AttributeKey.stringKey("service.application_id")

Choose a reason for hiding this comment

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

I don't think it's a good idea to have attributes as global properties. It goes against the principle of an independent modular architecture.

Copy link
Contributor

Choose a reason for hiding this comment

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

Some of these are going to be used by multiple modules, so that's why it's helpful to keep them in a common module. I also came across another class we already have where we can park commonly used attributes (I Missed it in my PR as well where I added workflow.name) - AttributeConstants

val APP_VERSION_CODE_KEY: AttributeKey<String> = AttributeKey.stringKey("service.version_code")
val SPLUNK_OLLY_UUID_KEY: AttributeKey<String> = AttributeKey.stringKey("service.o11y.key")
}
28 changes: 28 additions & 0 deletions common/utils/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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<ConfigAndroidLibrary>()
apply<ConfigPublish>()

ext {
set(artifactIdProperty, "$artifactPrefix$commonPrefix${project.name}")
set(versionProperty, Configurations.sdkVersionName)
}

android {
namespace = "com.splunk.sdk.common.utils"
}

dependencies {
implementation(Dependencies.SessionReplay.commonLogger)
}
Empty file.
4 changes: 4 additions & 0 deletions common/utils/lint-baseline.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 7.3.1" type="baseline" client="gradle" dependencies="false" name="AGP (7.3.1)" variant="all" version="7.3.1">

</issues>
1 change: 1 addition & 0 deletions common/utils/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-repackageclasses 'com.splunk.sdk.common.utils'
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.splunk.sdk.utils
Copy link
Contributor

@surbhiia surbhiia Feb 14, 2025

Choose a reason for hiding this comment

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

nit: Should we add "common" word in the package name like the other common packages - com.splunk.sdk.common.utils ? If not, we might need to change the package name in pro guard-rules.pro


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 ErrorIdentifierExtractor(private val application: Application) {

Choose a reason for hiding this comment

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

Class named ErrorIdentifierExtractor provides applicationId, versionCode, customUUID. There is no any error.

Copy link
Contributor Author

@tonzhan2 tonzhan2 Feb 13, 2025

Choose a reason for hiding this comment

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

Please see above comment:

These fields:

-application ID
-version code
-uuid (if the customer adds it)

only need to be added to crash and error spans that have stacktraces. This is because these attributes are used to correlate an android stacktrace with the correct mapping file for deobfuscation in the backend.

They do not need to be added to any other spans, as they are only relevant to identifying errors. That is why this class is called the error identifier extractor. Because it extracts the attributes used to identify an error stacktrace

private val packageManager: PackageManager = application.packageManager
private val applicationInfo: ApplicationInfo?

init {
applicationInfo = try {
packageManager.getApplicationInfo(application.packageName, PackageManager.GET_META_DATA)
} catch (e: Exception) {
Logger.e(TAG, "Failed to initialize ErrorIdentifierExtractor: ${e.message}")
null
}
}

fun retrieveApplicationId(): String? {
if (applicationInfo != null) {
return applicationInfo.packageName
} else {
Logger.e(TAG, "ApplicationInfo is null, cannot extract applicationId")
}
return null
}

fun retrieveVersionCode(): String? {
try {
val packageInfo =
packageManager.getPackageInfo(application.packageName, 0)
return 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)
return null
}
}

fun retrieveCustomUUID(): String? {
if (applicationInfo == null) {
Logger.e(TAG, "ApplicationInfo is null; cannot retrieve Custom UUID.")
return null
}
val bundle = applicationInfo.metaData
if (bundle != null) {
return bundle.getString(SPLUNK_UUID_MANIFEST_KEY)
} else {
Logger.e(TAG, "Application MetaData bundle is null")
return null
}
}

private companion object {
private const val TAG = "ErrorIdentifier"

private const val SPLUNK_UUID_MANIFEST_KEY = "SPLUNK_O11Y_CUSTOM_UUID"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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() {

/**
Expand Down Expand Up @@ -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, Attributes.empty())

Choose a reason for hiding this comment

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

Wouldn't it be better to pass null?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It doesn't make a difference

Choose a reason for hiding this comment

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

According to the source code, using Attributes.empty() requires two additional comparisons to achieve the same result.

}

/**
* 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.
Expand Down
1 change: 1 addition & 0 deletions integration/agent/api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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.ErrorIdentifierExtractor
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 {
val extractor = ErrorIdentifierExtractor(application)

applicationId = extractor.retrieveApplicationId()
versionCode = extractor.retrieveVersionCode()
customUUID = extractor.retrieveCustomUUID()
}

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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<ModuleConfiguration>) {
if (agentConfiguration.isDebugLogsEnabled) {
Expand Down Expand Up @@ -68,6 +68,7 @@ internal object MRUMAgentCore {

OpenTelemetryInitializer(application)
.joinResources(finalConfiguration.toResource())
.addSpanProcessor(ErrorIdentifierAttributesSpanProcessor(application))
.addSpanProcessor(SessionIdSpanProcessor(agentIntegration.sessionManager))
.addSpanProcessor(GlobalAttributeSpanProcessor())
.addLogRecordProcessor(GenericAttributesLogProcessor())
Expand Down
4 changes: 3 additions & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Common modules
include ':common:otel',
':common:storage'
':common:storage',
':common:utils'

// Instrumentation modules
include ':instrumentation:runtime:startup',
Expand All @@ -24,3 +25,4 @@ include ':app'

// Main entry point
include ':agent'
include ':instrumentation:runtime:customtracking'