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 custom tracking module #1168

Merged
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
1 change: 1 addition & 0 deletions agent/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ dependencies {
api(project(":integration:networkrequest"))
api(project(":integration:startup"))
api(project(":integration:interactions"))
api(project(":instrumentation:runtime:customtracking"))
}

1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ dependencies {

implementation(project(":agent"))
implementation(project(":integration:sessionreplay"))
implementation(project(":instrumentation:runtime:customtracking"))

implementation(Dependencies.SessionReplay.commonLogger)
implementation(Dependencies.SessionReplay.commonUtils)
Expand Down
5 changes: 4 additions & 1 deletion app/src/main/java/com/splunk/app/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import com.splunk.rum.integration.sessionreplay.api.sessionReplay
import java.net.URL

class App : Application() {
lateinit var agent: SplunkRUMAgent
private set

override fun onCreate() {
super.onCreate()

Expand All @@ -37,7 +40,7 @@ class App : Application() {
isDebugLogsEnabled = true,
)

val agent = SplunkRUMAgent.install(
agent = SplunkRUMAgent.install(
application = this,
agentConfiguration = agentConfig,
moduleConfigurations = arrayOf(
Expand Down
52 changes: 51 additions & 1 deletion app/src/main/java/com/splunk/app/ui/menu/MenuFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,18 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import com.cisco.android.common.utils.runOnUiThread
import com.splunk.app.App
import com.splunk.app.R
import com.splunk.app.databinding.FragmentMenuBinding
import com.splunk.app.ui.BaseFragment
import com.splunk.app.ui.httpurlconnection.HttpURLConnectionFragment
import com.splunk.app.ui.okhttp.OkHttpFragment
import com.splunk.app.util.FragmentAnimation
import com.splunk.rum.customtracking.extension.customTracking
import com.splunk.rum.integration.agent.api.SplunkRUMAgent
import io.opentelemetry.api.common.Attributes
import com.splunk.rum.integration.agent.api.extension.splunkRumId

class MenuFragment : BaseFragment<FragmentMenuBinding>() {
Expand All @@ -35,6 +41,10 @@ class MenuFragment : BaseFragment<FragmentMenuBinding>() {

override val titleRes: Int = R.string.menu_title

private val agent: SplunkRUMAgent? by lazy {
(requireActivity().application as? App)?.agent
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

Expand All @@ -49,18 +59,22 @@ class MenuFragment : BaseFragment<FragmentMenuBinding>() {
viewBinding.anrEvent.setOnClickListener(onClickListener)
viewBinding.okhttpSampleCalls.setOnClickListener(onClickListener)
viewBinding.httpurlconnection.setOnClickListener(onClickListener)

viewBinding.trackCustomEvent.setOnClickListener(onClickListener)
viewBinding.trackWorkflow.setOnClickListener(onClickListener)
viewBinding.crashReportsIllegal.splunkRumId = "illegalButton"
}

private val onClickListener = View.OnClickListener {
when (it.id) {
viewBinding.crashReportsIllegal.id ->
throw IllegalArgumentException("Illegal Argument Exception Thrown!")

viewBinding.crashReportsMainThread.id ->
throw RuntimeException("Crashing on main thread")

viewBinding.crashReportsInBackground.id ->
Thread { throw RuntimeException("Attempt to crash background thread") }.start()

viewBinding.crashReportsNoAppCode.id -> {
val e = java.lang.RuntimeException("No Application Code")
e.stackTrace = arrayOf(
Expand All @@ -70,36 +84,72 @@ class MenuFragment : BaseFragment<FragmentMenuBinding>() {
)
throw e
}

viewBinding.crashReportsNoStacktrace.id -> {
val e = java.lang.RuntimeException("No Stack Trace")
e.stackTrace = arrayOfNulls(0)
throw e
}

viewBinding.crashReportsOutOfMemoryError.id -> {
val e = OutOfMemoryError("out of memory")
e.stackTrace = arrayOfNulls(0)
throw e
}

viewBinding.crashReportsWithChainedExceptions.id -> {
try {
throw NullPointerException("Simulated error in exception 1")
} catch (e: NullPointerException) {
throw IllegalArgumentException("Simulated error in exception 2", e)
}
}

viewBinding.crashReportsNull.id ->
throw NullPointerException("I am null!")

viewBinding.anrEvent.id -> {
try {
Thread.sleep(6000)
} catch (e: InterruptedException) {
throw RuntimeException(e)
}
}

viewBinding.okhttpSampleCalls.id ->
navigateTo(OkHttpFragment(), FragmentAnimation.FADE)

viewBinding.httpurlconnection.id ->
navigateTo(HttpURLConnectionFragment(), FragmentAnimation.FADE)

viewBinding.trackCustomEvent.id -> {
agent?.let {
val testAttributes = Attributes.builder()
.put("attribute.one", "value1")
.put("attribute.two", "12345")
.build()
it.customTracking.trackCustomEvent("TestEvent", testAttributes)
showDoneToast("Track Custom Event, Done!")
} ?: showDoneToast("Agent is null, cannot track")
}

viewBinding.trackWorkflow.id -> {
agent?.let {
val workflowSpan = it.customTracking.trackWorkflow("Test Workflow")
workflowSpan?.setAttribute("workflow.start.time", System.currentTimeMillis())
// Simulate some processing time
Thread.sleep(125)
workflowSpan?.setAttribute("workflow.end.time", System.currentTimeMillis())
workflowSpan?.end()
showDoneToast("Track Workflow, Done!")
} ?: showDoneToast("Agent is null, cannot track")
}
}
}

private fun showDoneToast(message: String) {
runOnUiThread {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}
}
35 changes: 34 additions & 1 deletion app/src/main/res/layout/fragment_menu.xml
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,41 @@
android:layout_marginTop="5dp"
android:layout_marginRight="20dp"
android:text="@string/menu_httpurlconnection_title"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/okhttp_sample_calls" />

<TextView
android:id="@+id/custom_tracking_title"
style="@style/Header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="@string/menu_custom_tracking_title"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintTop_toBottomOf="@+id/httpurlconnection" />

<TextView
android:id="@+id/track_custom_event"
style="@style/Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="20dp"
android:layout_marginRight="20dp"
android:text="@string/menu_custom_event"
app:layout_constraintTop_toBottomOf="@id/custom_tracking_title" />

<TextView
android:id="@+id/track_workflow"
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_workflow"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/track_custom_event" />

</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
<string name="menu_okhttp_sample_calls">Okhttp sample calls</string>
<string name="menu_network_request_title">Network Requests</string>
<string name="menu_httpurlconnection_title">HttpURLConnection</string>
<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="okhttp_title">OkHttp</string>
<string name="httpurlconnection_title">HttpURLConnection</string>
Expand Down
2 changes: 1 addition & 1 deletion common/otel/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ apply<ConfigAndroidLibrary>()
apply<ConfigPublish>()

ext {
set(artifactIdProperty, "$artifactPrefix${commonPrefix}otel-${project.name}")
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe I missed the context here but how come we're removing the 'otel' part of this name?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As mentioned in the what PR covers bullet points - The published library name was earlier - rum-common-otel-otel which was changed to rum-common-otel

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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2025 Splunk Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.splunk.sdk.common.otel.internal

import io.opentelemetry.api.common.AttributeKey

object RumConstants {
const val RUM_TRACER_NAME: String = "SplunkRum"
val WORKFLOW_NAME_KEY: AttributeKey<String> = AttributeKey.stringKey("workflow.name")
}
31 changes: 31 additions & 0 deletions instrumentation/runtime/customtracking/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import plugins.ConfigAndroidLibrary
import plugins.ConfigPublish
import utils.artifactIdProperty
import utils.artifactPrefix
import utils.instrumentationPrefix
import utils.versionProperty

plugins {
id("com.android.library")
id("kotlin-android")
id("kotlin-parcelize")
}

apply<ConfigAndroidLibrary>()
apply<ConfigPublish>()

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

android {
namespace = "com.splunk.rum.customtracking"
}

dependencies {
api(Dependencies.Otel.api)
implementation(project(":integration:agent:api"))
implementation(project(":common:otel"))
implementation(Dependencies.SessionReplay.commonLogger)
}
4 changes: 4 additions & 0 deletions instrumentation/runtime/customtracking/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.4.0" type="baseline" client="gradle" dependencies="false" name="AGP (7.4.0)" variant="all" version="7.4.0">

</issues>
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright 2025 Splunk Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.splunk.rum.customtracking

import com.cisco.android.common.logger.Logger
import com.splunk.sdk.common.otel.OpenTelemetry
import com.splunk.sdk.common.otel.internal.RumConstants
import io.opentelemetry.api.common.Attributes
import io.opentelemetry.api.trace.Span
import io.opentelemetry.api.trace.Tracer


class CustomTracking internal constructor() {

/**
* Add a custom event. This can be useful to capture business events.
*
* <p>This event will be turned into a Span and sent to the RUM ingest along with other,
* auto-generated spans.
*
* @param name The name of the event.
* @param attributes Any {@link Attributes} to associate with the event.
*/
fun trackCustomEvent(name: String, attributes: Attributes) {
val tracer = getTracer() ?: return
tracer.spanBuilder(name).setAllAttributes(attributes).startSpan().end()
}

/**
* Start a Span to track a named workflow.
*
* @param workflowName The name of the workflow to start.
* @return A {@link Span} that has been started.
*/
fun trackWorkflow(workflowName: String): Span? {
val tracer = getTracer() ?: return null
return tracer.spanBuilder(workflowName)
.setAttribute(RumConstants.WORKFLOW_NAME_KEY, workflowName)
.startSpan()
}


/**
* Retrieves the Tracer instance for the application.
*
* @return A Tracer instance if available, or null if the OpenTelemetry instance is null.
*/
private fun getTracer(): Tracer? {
return OpenTelemetry.instance?.sdkTracerProvider?.get(RumConstants.RUM_TRACER_NAME).also {
if (it == null) {
Logger.e(
TAG,
"Opentelemetry instance is null. Cannot track custom events/workflow."
)
}
}
}

companion object {

private const val TAG = "CustomTracking"

/**
* The instance of [CustomTracking].
*/
@JvmStatic
val instance by lazy { CustomTracking() }
}
}
Loading