Manually instrument Android applications

Manually instrument Android applications to collect additional telemetry, sanitize Personal Identifiable Information (PII), add global attributes, and more.

You can manually instrument Android applications for Splunk RUM using the Android RUM agent to collect additional telemetry, sanitize Personal Identifiable Information (PII), add global attributes, and more.

Filter spans

You can modify or drop spans using the type (SpanData) -> SpanData . It allows you to provide a lambda or function that can inspect, modify, or filter SpanData objects before they are exported. For example, you can drop or redact spans that contain personally identifiable information (PII).

The following example shows how to remove a span:

Kotlin
// Remove all spans with name "SensitiveOperation"
val spanInterceptor: ((SpanData) -> SpanData?) = { spanData ->
    if (spanData.name == "SensitiveOperation") {
        null // Returning null signals that this span should be dropped
    } else {
        spanData
    }
}
val agent = SplunkRum.install(
    this,
    AgentConfiguration(
        ...,
        spanInterceptor = spanInterceptor
    )
)
Java
// Remove all spans with name "SensitiveOperation"
Function<SpanData, SpanData> spanInterceptor = spanData -> {
    if ("SensitiveOperation".equals(spanData.getName())) {
        return null; // Returning null signals that this span should be dropped
    } else {
        return spanData;
    }
};
SplunkRum agent = SplunkRum.install(
    this,
    new AgentConfiguration(
            ...,
            spanInterceptor
    )
);

The following example shows how to redact the value of an attribute to remove sensitive data:

Kotlin
// Redact "user.email" attribute to protect sensitive data
val spanInterceptor: ((SpanData) -> SpanData?) = { spanData ->
    val mutableSpan = spanData.toMutableSpanData()
    val mutableAttributes = mutableSpan.attributes.toMutableAttributes()
    // Check if user.email is present, then redact it
    if ("user.email" in mutableAttributes) {
        mutableAttributes["user.email"] = "[REDACTED]"
        mutableSpan.attributes = mutableAttributes
    }
    mutableSpan
  }
val agent = SplunkRum.install(
    this,
    AgentConfiguration(
        ...,
        spanInterceptor = spanInterceptor
    )
)
Java
// Redact "user.email" attribute to protect sensitive data
Function<SpanData, SpanData> spanInterceptor = spanData -> {
    MutableSpanData mutableSpan = spanData.toMutableSpanData();
    MutableAttributes mutableAttributes = mutableSpan.getAttributes().toMutableAttributes();
    // Check if user.email is present, then redact it
    if (mutableAttributes.containsKey("user.email")) {
        mutableAttributes.put("user.email", "[REDACTED]");
        mutableSpan.setAttributes(mutableAttributes);
    }
    return mutableSpan;
};
SplunkRum agent = SplunkRum.install(
    this,
    new AgentConfiguration(
            ...,
            spanInterceptor
    )
);

Manage global attributes

Global attributes are key-value pairs added to all reported data. Global attributes are useful for reporting application or user-specific values as tags.

The following examples show how to define a key-value pair as global attributes:

To add metadata during agent initialization:

Kotlin
val globalAttributes = MutableAttributes().apply {
    this["name"] = "John Doe"
    this["email"] = "john.doe@example.com"
}
val agent = SplunkRum.install(
    this,
    agentConfiguration = AgentConfiguration(
                endpoint = EndpointConfiguration(
                    realm = "your-splunk-realm",
                    rumAccessToken = "your_splunk_rum_access_token"
                ),
                appName = "your_app_name",
                deploymentEnvironment = "your_deployment_environment"
                globalAttributes = globalAttributes
    )
)
Java
MutableAttributes globalAttributes = new MutableAttributes();
globalAttributes.set("name", "John Doe");
globalAttributes.set("email", "john.doe@example.com");

SplunkRum agent = SplunkRum.install(
    this,
    new AgentConfiguration(
                new EndpointConfiguration(
                    "your_splunk_realm",
                    "your_splunk_rum_access_token"
                ),
                "your_app_name",
                "your_deployment_environment",
                globalAttributes
    )
);

To add metadata after agent initialization:

Kotlin
// Using map syntax
SplunkRum.instance.globalAttributes["enduser.id"] = "user-id-123456"

// Using a set method
SplunkRum.instance.globalAttributes.set("enduser.role", "premium")
Java
SplunkRum.getInstance().getGlobalAttributes().set("enduser.role", "premium")

Report custom events and workflows

You can report custom events and workflows happening in your Android application using the trackCustomEvent and trackWorkflow APIs. Additionally, you have the option to set custom attributes. See example below

Kotlin
val testAttributes = MutableAttributes()
testAttributes["attribute.one"] = "value1"
testAttributes["attribute.two"] = "12345"
SplunkRum.instance.customTracking.trackCustomEvent(
    "TestEvent",
    testAttributes
)
Java
MutableAttributes testAttributes = new MutableAttributes();
testAttributes.set("attribute.one", "value1");
testAttributes.set("attribute.two", "12345");

CustomTracking.getInstance().trackCustomEvent(
    "TestEvent",
    testAttributes
);

The following example shows how to start a workflow for which metrics are recorded by Splunk RUM. To record the workflow you must end the OpenTelemetry span instance:

Kotlin
binding.buttonWork.setOnClickListener { 
    val hardWorkerSpan = SplunkRum.instance.customTracking.trackWorkflow("Main thread working hard")
    try {
        val random = Random()
        val startTime = System.currentTimeMillis()
        while (true) {
            random.nextDouble()
            if (System.currentTimeMillis() - startTime > 20_000) {
                break
            }
        }
    } finally {
        hardWorkerSpan.end()
    }
}
Java
binding.buttonWork.setOnClickListener(v -> {
   Span hardWorker =
         CustomTracking.getInstance().trackWorkflow("Main thread working hard");
   try {
      Random random = new Random();
      long startTime = System.currentTimeMillis();
      while (true) {
         random.nextDouble();
         if (System.currentTimeMillis() - startTime > 20_000) {
            break;
         }
      }
   } finally {
      hardWorker.end();
   }
});

Automatic navigation detection

Automatic navigation detection is deactivated by default. You can activate it by configuring the module Android Unified Agent Doc Updates (Private Preview) | Instrumentation Module settings The automatic navigation detection remains consistent with the legacy solution, ensuring backward compatibility for events.

Manually track navigation events

By default, the Android RUM agent does not track navigation events automatically. If you enable automatic tracking, the agent observes the lifecycle of Activity and Fragment instances. However, this approach does not work for certain UI frameworks, such as Jetpack Compose, which do not follow the same lifecycle patterns. For these frameworks, you can track navigations manually by specifying the exact screen names.

In general, for navigation events that don't involve a Fragment or Activity instance, or when the Activity and Fragment lifecycles don't match the actual navigation, the application developer agent calls:

Kotlin
SplunkRum.instance.navigation.track("example_screen")
Java
Navigation.getInstance().track("example_screen")

This sends a navigation span to RUM and remembers the screen name for subsequent spans.

When using Jetpack Compose, you can use the active route as a screen name, and the code will depend on the implementation. The following is a simplified example:

val navController = rememberNavController()
val currentBackEntry by navController.currentBackStackEntryAsState()
val currentRoute = currentBackEntry?.destination?.route

LaunchedEffect(currentRoute) {
   if (currentRoute != null) {
      lastRoute = currentRoute
      SplunkRum.instance.navigation.track(currentRoute)
   }
}

Configure error reporting

You can report handled errors, exceptions, and messages using the trackException(Throwable, Attributes?) method. You can set custom attributes for your reported exception. Exceptions appear as errors in the Splunk RUM UI, and error metrics are recorded.

The following example shows how to report the Unimplemented Feature: Settings error in a sample application:

Kotlin
private val SETTINGS_FEATURE_ATTRIBUTES = MutableAttributes().also { attributes ->
        attributes["feature.name"] = "Settings"
        attributes["feature.flag.enabled"] = true
        attributes["feature.used.count"] = 20
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
    val id = item.itemId
    if (id == R.id.action_settings) {
        SplunkRum.instance.customTracking.trackException(
                UnsupportedOperationException("Unimplemented Feature: Settings"),
                SETTINGS_FEATURE_ATTRIBUTES
            )
        return true
    }
    return super.onOptionsItemSelected(item)
}
Java
private static final Attributes SETTINGS_FEATURE_ATTRIBUTES = Attributes.of(
            AttributeKey.stringKey("feature.name"), "Settings",
            AttributeKey.stringKey("feature.flag.enabled"), "is_Settings_enabled",
            AttributeKey.stringKey("feature.usage"), "clicked"
);

public boolean onOptionsItemSelected(MenuItem item) {
    int id = item.getItemId();
    if (id == R.id.action_settings) {
        CustomTracking.getInstance()
            .trackException(
                new UnsupportedOperationException("Unimplemented Feature: Settings"),
                SETTINGS_FEATURE_ATTRIBUTES
            );
        return true;
    }
    return super.onOptionsItemSelected(item);
}

Add server trace context from Splunk APM

The Android RUM agent collects server trace context using back-end data provided by APM instrumentation through the Server-Timing header. In some cases, you might want to generate the header manually.

To create the Server-Timing header manually, provide a Server-Timing header with the name traceparent, where the desc field holds the version, the trace ID, the parent ID, and the trace flag.

Consider the following HTTP header:

Server-Timing: traceparent;desc="00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"

The example resolves to a context containing the following data:

version=00 trace-id=4bf92f3577b34da6a3ce929d0e0e4736parent-id=00f067aa0ba902b7 trace-flags=01

When generating a value for the traceparent header, make sure that it matches the following regular expression:

00-([0-9a-f]{32})-([0-9a-f]{16})-01
Server timing headers with values that don't match the pattern are automatically discarded. For more information, see the Server-Timing and traceparent documentation on the W3C website.

If multiple valid server-timing headers are found, the last valid one is used.