From a320a827939da14ad64e20311847b22a29d5c69d Mon Sep 17 00:00:00 2001 From: Surbhi A <138259843+surbhiia@users.noreply.github.com> Date: Mon, 10 Feb 2025 09:28:28 -0800 Subject: [PATCH] Add custom tracking module (#1168) * Add custom tracking module * Minor corrections * Improve MenuFragment code --- agent/build.gradle.kts | 1 + app/build.gradle.kts | 1 + app/src/main/java/com/splunk/app/App.kt | 5 +- .../com/splunk/app/ui/menu/MenuFragment.kt | 52 +++++++++++- app/src/main/res/layout/fragment_menu.xml | 35 +++++++- app/src/main/res/values/strings.xml | 3 + common/otel/build.gradle.kts | 2 +- .../sdk/common/otel/internal/RumConstants.kt | 24 ++++++ .../runtime/customtracking/build.gradle.kts | 31 +++++++ .../runtime/customtracking/lint-baseline.xml | 4 + .../rum/customtracking/CustomTracking.kt | 83 +++++++++++++++++++ .../extension/SplunkRUMAgentExt.kt | 27 ++++++ settings.gradle | 1 + 13 files changed, 265 insertions(+), 4 deletions(-) create mode 100644 common/otel/src/main/java/com/splunk/sdk/common/otel/internal/RumConstants.kt create mode 100644 instrumentation/runtime/customtracking/build.gradle.kts create mode 100644 instrumentation/runtime/customtracking/lint-baseline.xml create mode 100644 instrumentation/runtime/customtracking/src/main/java/com/splunk/rum/customtracking/CustomTracking.kt create mode 100644 instrumentation/runtime/customtracking/src/main/java/com/splunk/rum/customtracking/extension/SplunkRUMAgentExt.kt diff --git a/agent/build.gradle.kts b/agent/build.gradle.kts index 5179c58cf..6aee68bc1 100644 --- a/agent/build.gradle.kts +++ b/agent/build.gradle.kts @@ -27,5 +27,6 @@ dependencies { api(project(":integration:networkrequest")) api(project(":integration:startup")) api(project(":integration:interactions")) + api(project(":instrumentation:runtime:customtracking")) } diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4ae8cd136..fae3c93c7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/java/com/splunk/app/App.kt b/app/src/main/java/com/splunk/app/App.kt index 15a95711b..f884ed38e 100644 --- a/app/src/main/java/com/splunk/app/App.kt +++ b/app/src/main/java/com/splunk/app/App.kt @@ -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() @@ -37,7 +40,7 @@ class App : Application() { isDebugLogsEnabled = true, ) - val agent = SplunkRUMAgent.install( + agent = SplunkRUMAgent.install( application = this, agentConfiguration = agentConfig, moduleConfigurations = arrayOf( 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 51a3aef0e..6cdb4cc61 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 @@ -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() { @@ -35,6 +41,10 @@ class MenuFragment : BaseFragment() { 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) @@ -49,7 +59,8 @@ class MenuFragment : BaseFragment() { viewBinding.anrEvent.setOnClickListener(onClickListener) viewBinding.okhttpSampleCalls.setOnClickListener(onClickListener) viewBinding.httpurlconnection.setOnClickListener(onClickListener) - + viewBinding.trackCustomEvent.setOnClickListener(onClickListener) + viewBinding.trackWorkflow.setOnClickListener(onClickListener) viewBinding.crashReportsIllegal.splunkRumId = "illegalButton" } @@ -57,10 +68,13 @@ class MenuFragment : BaseFragment() { 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( @@ -70,16 +84,19 @@ class MenuFragment : BaseFragment() { ) 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") @@ -87,8 +104,10 @@ class MenuFragment : BaseFragment() { throw IllegalArgumentException("Simulated error in exception 2", e) } } + viewBinding.crashReportsNull.id -> throw NullPointerException("I am null!") + viewBinding.anrEvent.id -> { try { Thread.sleep(6000) @@ -96,10 +115,41 @@ class MenuFragment : BaseFragment() { 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() } } } diff --git a/app/src/main/res/layout/fragment_menu.xml b/app/src/main/res/layout/fragment_menu.xml index 8e96fdf65..00f6e9ad8 100644 --- a/app/src/main/res/layout/fragment_menu.xml +++ b/app/src/main/res/layout/fragment_menu.xml @@ -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" /> + + + + + + \ 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 f79c2a09b..7c61a7193 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -15,6 +15,9 @@ Okhttp sample calls Network Requests HttpURLConnection + Custom Tracking + Track Custom Event + Track Workflow OkHttp HttpURLConnection diff --git a/common/otel/build.gradle.kts b/common/otel/build.gradle.kts index 2584845f4..d7982c77c 100644 --- a/common/otel/build.gradle.kts +++ b/common/otel/build.gradle.kts @@ -15,7 +15,7 @@ apply() apply() ext { - set(artifactIdProperty, "$artifactPrefix${commonPrefix}otel-${project.name}") + set(artifactIdProperty, "$artifactPrefix${commonPrefix}${project.name}") set(versionProperty, Configurations.sdkVersionName) } 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 new file mode 100644 index 000000000..42c0dc623 --- /dev/null +++ b/common/otel/src/main/java/com/splunk/sdk/common/otel/internal/RumConstants.kt @@ -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 = AttributeKey.stringKey("workflow.name") +} \ No newline at end of file diff --git a/instrumentation/runtime/customtracking/build.gradle.kts b/instrumentation/runtime/customtracking/build.gradle.kts new file mode 100644 index 000000000..3ba9d3d18 --- /dev/null +++ b/instrumentation/runtime/customtracking/build.gradle.kts @@ -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() +apply() + +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) +} \ No newline at end of file diff --git a/instrumentation/runtime/customtracking/lint-baseline.xml b/instrumentation/runtime/customtracking/lint-baseline.xml new file mode 100644 index 000000000..1d527bcea --- /dev/null +++ b/instrumentation/runtime/customtracking/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + 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 new file mode 100644 index 000000000..86672082f --- /dev/null +++ b/instrumentation/runtime/customtracking/src/main/java/com/splunk/rum/customtracking/CustomTracking.kt @@ -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. + * + *

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() } + } +} \ No newline at end of file diff --git a/instrumentation/runtime/customtracking/src/main/java/com/splunk/rum/customtracking/extension/SplunkRUMAgentExt.kt b/instrumentation/runtime/customtracking/src/main/java/com/splunk/rum/customtracking/extension/SplunkRUMAgentExt.kt new file mode 100644 index 000000000..8a51b9139 --- /dev/null +++ b/instrumentation/runtime/customtracking/src/main/java/com/splunk/rum/customtracking/extension/SplunkRUMAgentExt.kt @@ -0,0 +1,27 @@ +/* + * 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.extension + +import com.splunk.rum.customtracking.CustomTracking +import com.splunk.rum.integration.agent.api.SplunkRUMAgent + +/** + * Extension property to access the [CustomTracking] instance via [SplunkRUMAgent]. + */ +@Suppress("UnusedReceiverParameter") +val SplunkRUMAgent.customTracking: CustomTracking + get() = CustomTracking.instance diff --git a/settings.gradle b/settings.gradle index a8e5945dd..c52674521 100644 --- a/settings.gradle +++ b/settings.gradle @@ -22,3 +22,4 @@ include ':app' // Main entry point include ':agent' +include ':instrumentation:runtime:customtracking'