diff --git a/sample-app/src/main/java/com/splunk/android/sample/SampleApplication.java b/sample-app/src/main/java/com/splunk/android/sample/SampleApplication.java index 4fe47cfd0..cb21dc851 100644 --- a/sample-app/src/main/java/com/splunk/android/sample/SampleApplication.java +++ b/sample-app/src/main/java/com/splunk/android/sample/SampleApplication.java @@ -19,7 +19,6 @@ import static io.opentelemetry.api.common.AttributeKey.stringKey; import android.app.Application; -import com.splunk.rum.Config; import com.splunk.rum.SplunkRum; import com.splunk.rum.StandardAttributes; import io.opentelemetry.api.common.Attributes; @@ -35,42 +34,35 @@ public class SampleApplication extends Application { public void onCreate() { super.onCreate(); - Config config = - SplunkRum.newConfigBuilder() - // note: for these values to be resolved, put them in your local.properties - // file as - // rum.beacon.url and rum.access.token - .realm(getResources().getString(R.string.rum_realm)) - .slowRenderingDetectionPollInterval(Duration.ofMillis(1000)) - .rumAccessToken(getResources().getString(R.string.rum_access_token)) - .applicationName("Android Demo App") - .debugEnabled(true) - .diskBufferingEnabled(true) - .deploymentEnvironment("demo") - .limitDiskUsageMegabytes(1) - .globalAttributes( - Attributes.builder() - .put("vendor", "Splunk") - .put( - StandardAttributes.APP_VERSION, - BuildConfig.VERSION_NAME) - .build()) - .filterSpans( - spanFilter -> - spanFilter - .removeSpanAttribute(stringKey("http.user_agent")) - .rejectSpansByName( - spanName -> spanName.contains("ignored")) - // sensitive data in the login http.url attribute - // will be redacted before it hits the exporter - .replaceSpanAttribute( - StandardAttributes.HTTP_URL, - value -> - HTTP_URL_SENSITIVE_DATA_PATTERN - .matcher(value) - .replaceAll( - "$1="))) - .build(); - SplunkRum.initialize(config, this); + SplunkRum.builder() + // note: for these values to be resolved, put them in your local.properties + // file as rum.beacon.url and rum.access.token + .setRealm(getResources().getString(R.string.rum_realm)) + .setApplicationName("Android Demo App") + .setRumAccessToken(getResources().getString(R.string.rum_access_token)) + .enableDebug() + .enableDiskBuffering() + .setSlowRenderingDetectionPollInterval(Duration.ofMillis(1000)) + .setDeploymentEnvironment("demo") + .limitDiskUsageMegabytes(1) + .setGlobalAttributes( + Attributes.builder() + .put("vendor", "Splunk") + .put(StandardAttributes.APP_VERSION, BuildConfig.VERSION_NAME) + .build()) + .filterSpans( + spanFilter -> + spanFilter + .removeSpanAttribute(stringKey("http.user_agent")) + .rejectSpansByName(spanName -> spanName.contains("ignored")) + // sensitive data in the login http.url attribute + // will be redacted before it hits the exporter + .replaceSpanAttribute( + StandardAttributes.HTTP_URL, + value -> + HTTP_URL_SENSITIVE_DATA_PATTERN + .matcher(value) + .replaceAll("$1="))) + .build(this); } } diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/Config.java b/splunk-otel-android/src/main/java/com/splunk/rum/Config.java index 2f0747a54..0303b6f61 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/Config.java +++ b/splunk-otel-android/src/main/java/com/splunk/rum/Config.java @@ -31,7 +31,11 @@ * *

Both the beaconUrl and the rumAuthToken are mandatory configuration settings. Trying to build * a Config instance without both of these items specified will result in an exception being thrown. + * + * @deprecated Use {@link #builder()} and the {@link SplunkRumBuilder} to configure a {@link + * SplunkRum} instance. */ +@Deprecated public class Config { private final String beaconEndpoint; @@ -43,6 +47,7 @@ public class Config { private final boolean anrDetectionEnabled; private final Attributes globalAttributes; private final Function spanFilterExporterDecorator; + private final Consumer spanFilterBuilderConfigurer; private final boolean slowRenderingDetectionEnabled; private final Duration slowRenderingDetectionPollInterval; private final boolean diskBufferingEnabled; @@ -62,6 +67,7 @@ private Config(Builder builder) { this.slowRenderingDetectionPollInterval = builder.slowRenderingDetectionPollInterval; this.slowRenderingDetectionEnabled = builder.slowRenderingDetectionEnabled; this.spanFilterExporterDecorator = builder.spanFilterBuilder.build(); + this.spanFilterBuilderConfigurer = builder.spanFilterBuilderConfigurer; this.diskBufferingEnabled = builder.diskBufferingEnabled; this.maxUsageMegabytes = builder.maxUsageMegabytes; this.sessionBasedSamplerEnabled = builder.sessionBasedSamplerEnabled; @@ -170,11 +176,48 @@ public static Builder builder() { return new Builder(); } - SpanExporter decorateWithSpanFilter(SpanExporter exporter) { - return spanFilterExporterDecorator.apply(exporter); + SplunkRumBuilder toSplunkRumBuilder() { + SplunkRumBuilder splunkRumBuilder = + new SplunkRumBuilder() + .setApplicationName(applicationName) + .setBeaconEndpoint(beaconEndpoint) + .setRumAccessToken(rumAccessToken); + if (debugEnabled) { + splunkRumBuilder.enableDebug(); + } + if (diskBufferingEnabled) { + splunkRumBuilder.enableDiskBuffering(); + } + if (!crashReportingEnabled) { + splunkRumBuilder.disableCrashReporting(); + } + if (!networkMonitorEnabled) { + splunkRumBuilder.disableNetworkMonitorEnabled(); + } + if (!anrDetectionEnabled) { + splunkRumBuilder.disableAnrDetection(); + } + if (!slowRenderingDetectionEnabled) { + splunkRumBuilder.disableSlowRenderingDetection(); + } + splunkRumBuilder + .setSlowRenderingDetectionPollInterval(slowRenderingDetectionPollInterval) + .setGlobalAttributes(globalAttributes) + .filterSpans(spanFilterBuilderConfigurer) + .limitDiskUsageMegabytes(maxUsageMegabytes); + if (sessionBasedSamplerEnabled) { + splunkRumBuilder.enableSessionBasedSampling(sessionBasedSamplerRatio); + } + return splunkRumBuilder; } - /** Builder class for the Splunk RUM {@link Config} class. */ + /** + * Builder class for the Splunk RUM {@link Config} class. + * + * @deprecated Use {@link #builder()} and the {@link SplunkRumBuilder} to configure a {@link + * SplunkRum} instance. + */ + @Deprecated public static class Builder { private static final Duration DEFAULT_SLOW_RENDERING_DETECTION_POLL_INTERVAL = @@ -192,6 +235,7 @@ public static class Builder { private Attributes globalAttributes = Attributes.empty(); private String deploymentEnvironment; private final SpanFilterBuilder spanFilterBuilder = new SpanFilterBuilder(); + private Consumer spanFilterBuilderConfigurer = f -> {}; private String realm; private Duration slowRenderingDetectionPollInterval = DEFAULT_SLOW_RENDERING_DETECTION_POLL_INTERVAL; @@ -379,6 +423,8 @@ public Builder deploymentEnvironment(String environment) { * @return {@code this}. */ public Builder filterSpans(Consumer configurer) { + Consumer previous = this.spanFilterBuilderConfigurer; + this.spanFilterBuilderConfigurer = previous.andThen(configurer); configurer.accept(spanFilterBuilder); return this; } diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/ConnectionUtil.java b/splunk-otel-android/src/main/java/com/splunk/rum/ConnectionUtil.java index 67c2c2579..05610b630 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/ConnectionUtil.java +++ b/splunk-otel-android/src/main/java/com/splunk/rum/ConnectionUtil.java @@ -16,6 +16,8 @@ package com.splunk.rum; +import android.app.Application; +import android.content.Context; import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkCapabilities; @@ -137,4 +139,16 @@ public void onLost(@NonNull Network network) { } } } + + static class Factory { + + ConnectionUtil createAndStart(Application application) { + Context context = application.getApplicationContext(); + ConnectionUtil connectionUtil = new ConnectionUtil(NetworkDetector.create(context)); + connectionUtil.startMonitoring( + ConnectionUtil::createNetworkMonitoringRequest, + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE)); + return connectionUtil; + } + } } diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/RumInitializer.java b/splunk-otel-android/src/main/java/com/splunk/rum/RumInitializer.java index 657783047..ab8a37eb9 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/RumInitializer.java +++ b/splunk-otel-android/src/main/java/com/splunk/rum/RumInitializer.java @@ -63,29 +63,30 @@ class RumInitializer { // assuming 128 chars per line static final int MAX_ATTRIBUTE_LENGTH = 256 * 128; - private final Config config; + private final SplunkRumBuilder builder; private final AtomicReference globalAttributes; private final Application application; private final AppStartupTimer startupTimer; private final List initializationEvents = new ArrayList<>(); private final AnchoredClock timingClock; - RumInitializer(Config config, Application application, AppStartupTimer startupTimer) { - this.config = config; - this.globalAttributes = new AtomicReference<>(config.getGlobalAttributes()); + RumInitializer( + SplunkRumBuilder builder, Application application, AppStartupTimer startupTimer) { + this.builder = builder; + this.globalAttributes = builder.buildGlobalAttributesRef(); this.application = application; this.startupTimer = startupTimer; this.timingClock = startupTimer.startupClock; } - SplunkRum initialize(Supplier connectionUtilSupplier, Looper mainLooper) { + SplunkRum initialize(ConnectionUtil.Factory connectionUtilFactory, Looper mainLooper) { String rumVersion = detectRumVersion(); VisibleScreenTracker visibleScreenTracker = new VisibleScreenTracker(); long startTimeNanos = timingClock.now(); List appStateListeners = new ArrayList<>(); - ConnectionUtil connectionUtil = connectionUtilSupplier.get(); + ConnectionUtil connectionUtil = connectionUtilFactory.createAndStart(application); initializationEvents.add( new InitializationEvent("connectionUtilInitialized", timingClock.now())); @@ -117,7 +118,7 @@ SplunkRum initialize(Supplier connectionUtilSupplier, Looper mai new RumInitializer.InitializationEvent( "openTelemetrySdkInitialized", timingClock.now())); - if (config.isAnrDetectionEnabled()) { + if (builder.anrDetectionEnabled) { appStateListeners.add(initializeAnrReporting(mainLooper)); initializationEvents.add( new RumInitializer.InitializationEvent( @@ -127,7 +128,7 @@ SplunkRum initialize(Supplier connectionUtilSupplier, Looper mai Tracer tracer = openTelemetrySdk.getTracer(SplunkRum.RUM_TRACER_NAME); sessionId.setSessionIdChangeListener(new SessionIdChangeTracer(tracer)); - if (config.isNetworkMonitorEnabled()) { + if (builder.networkMonitorEnabled) { NetworkMonitor networkMonitor = new NetworkMonitor(connectionUtil); networkMonitor.addConnectivityListener(tracer); appStateListeners.add(networkMonitor); @@ -136,7 +137,7 @@ SplunkRum initialize(Supplier connectionUtilSupplier, Looper mai "networkMonitorInitialized", timingClock.now())); } - SlowRenderingDetector slowRenderingDetector = buildSlowRenderingDetector(config, tracer); + SlowRenderingDetector slowRenderingDetector = buildSlowRenderingDetector(tracer); slowRenderingDetector.start(); if (Build.VERSION.SDK_INT < 29) { @@ -158,20 +159,20 @@ SplunkRum initialize(Supplier connectionUtilSupplier, Looper mai new RumInitializer.InitializationEvent( "activityLifecycleCallbacksInitialized", timingClock.now())); - if (config.isCrashReportingEnabled()) { + if (builder.crashReportingEnabled) { CrashReporter.initializeCrashReporting(tracer, openTelemetrySdk); initializationEvents.add( new RumInitializer.InitializationEvent( "crashReportingInitialized", timingClock.now())); } - recordInitializationSpans(startTimeNanos, initializationEvents, tracer, config); + recordInitializationSpans(startTimeNanos, initializationEvents, tracer); return new SplunkRum(openTelemetrySdk, sessionId, globalAttributes); } - private SlowRenderingDetector buildSlowRenderingDetector(Config config, Tracer tracer) { - if (!config.isSlowRenderingDetectionEnabled()) { + private SlowRenderingDetector buildSlowRenderingDetector(Tracer tracer) { + if (!builder.slowRenderingDetectionEnabled) { Log.w(LOG_TAG, "Slow/frozen rendering detection has been disabled by user."); return SlowRenderingDetector.NO_OP; } @@ -181,7 +182,7 @@ private SlowRenderingDetector buildSlowRenderingDetector(Config config, Tracer t "slowRenderingDetectorInitialized", timingClock.now())); Class.forName("androidx.core.app.FrameMetricsAggregator"); return new SlowRenderingDetectorImpl( - tracer, config.getSlowRenderingDetectionPollInterval()); + tracer, builder.slowRenderingDetectionPollInterval); } catch (ClassNotFoundException e) { Log.w( LOG_TAG, @@ -232,10 +233,7 @@ private String detectRumVersion() { } private void recordInitializationSpans( - long startTimeNanos, - List initializationEvents, - Tracer tracer, - Config config) { + long startTimeNanos, List initializationEvents, Tracer tracer) { Span overallAppStart = startupTimer.start(tracer); Span span = tracer.spanBuilder("SplunkRum.initialize") @@ -246,19 +244,19 @@ private void recordInitializationSpans( String configSettings = "[debug:" - + config.isDebugEnabled() + + builder.debugEnabled + "," + "crashReporting:" - + config.isCrashReportingEnabled() + + builder.crashReportingEnabled + "," + "anrReporting:" - + config.isAnrDetectionEnabled() + + builder.anrDetectionEnabled + "," + "slowRenderingDetector:" - + config.isSlowRenderingDetectionEnabled() + + builder.slowRenderingDetectionEnabled + "," + "networkMonitor:" - + config.isNetworkMonitorEnabled() + + builder.networkMonitorEnabled + "]"; span.setAttribute("config_settings", configSettings); @@ -285,7 +283,7 @@ private SdkTracerProvider buildTracerProvider( RumAttributeAppender attributeAppender = new RumAttributeAppender( - config.getApplicationName(), + builder.applicationName, globalAttributes::get, sessionId, rumVersion, @@ -297,7 +295,7 @@ private SdkTracerProvider buildTracerProvider( Resource resource = Resource.getDefault().toBuilder() - .put("service.name", config.getApplicationName()) + .put("service.name", builder.applicationName) .build(); initializationEvents.add( new RumInitializer.InitializationEvent("resourceInitialized", timingClock.now())); @@ -316,16 +314,15 @@ private SdkTracerProvider buildTracerProvider( new RumInitializer.InitializationEvent( "tracerProviderBuilderInitialized", timingClock.now())); - if (config.isSessionBasedSamplerEnabled()) { + if (builder.sessionBasedSamplerEnabled) { tracerProviderBuilder.setSampler( - new SessionIdRatioBasedSampler( - config.getSessionBasedSamplerRatio(), sessionId)); + new SessionIdRatioBasedSampler(builder.sessionBasedSamplerRatio, sessionId)); } - if (config.isDebugEnabled()) { + if (builder.debugEnabled) { tracerProviderBuilder.addSpanProcessor( SimpleSpanProcessor.create( - config.decorateWithSpanFilter(LoggingSpanExporter.create()))); + builder.decorateWithSpanFilter(LoggingSpanExporter.create()))); initializationEvents.add( new RumInitializer.InitializationEvent( "debugSpanExporterInitialized", timingClock.now())); @@ -336,14 +333,14 @@ private SdkTracerProvider buildTracerProvider( // visible for testing SpanExporter buildFilteringExporter(ConnectionUtil connectionUtil) { SpanExporter exporter = buildExporter(connectionUtil); - SpanExporter filteredExporter = config.decorateWithSpanFilter(exporter); + SpanExporter filteredExporter = builder.decorateWithSpanFilter(exporter); initializationEvents.add( new InitializationEvent("zipkin exporter initialized", timingClock.now())); return filteredExporter; } private SpanExporter buildExporter(ConnectionUtil connectionUtil) { - if (!config.isDebugEnabled()) { + if (builder.debugEnabled) { // tell the Zipkin exporter to shut up already. We're on mobile, network stuff happens. // we'll do our best to hang on to the spans with the wrapping BufferingExporter. ZipkinSpanExporter.baseLogger.setLevel(Level.SEVERE); @@ -351,7 +348,7 @@ private SpanExporter buildExporter(ConnectionUtil connectionUtil) { new InitializationEvent("logger setup complete", timingClock.now())); } - if (config.isDiskBufferingEnabled()) { + if (builder.diskBufferingEnabled) { return buildStorageBufferingExporter(connectionUtil); } @@ -379,7 +376,7 @@ private SpanExporter buildStorageBufferingExporter(ConnectionUtil connectionUtil @NonNull private String getEndpoint() { - return config.getBeaconEndpoint() + "?auth=" + config.getRumAccessToken(); + return builder.beaconEndpoint + "?auth=" + builder.rumAccessToken; } private SpanExporter buildMemoryBufferingThrottledExporter(ConnectionUtil connectionUtil) { @@ -395,7 +392,9 @@ private SpanExporter buildMemoryBufferingThrottledExporter(ConnectionUtil connec SpanExporter getToDiskExporter() { return new LazyInitSpanExporter( - () -> ZipkinWriteToDiskExporterFactory.create(application, config)); + () -> + ZipkinWriteToDiskExporterFactory.create( + application, builder.maxUsageMegabytes)); } // visible for testing diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/SplunkRum.java b/splunk-otel-android/src/main/java/com/splunk/rum/SplunkRum.java index e71705fb7..086f061c7 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/SplunkRum.java +++ b/splunk-otel-android/src/main/java/com/splunk/rum/SplunkRum.java @@ -20,9 +20,7 @@ import static io.opentelemetry.api.common.AttributeKey.stringKey; import android.app.Application; -import android.content.Context; import android.location.Location; -import android.net.ConnectivityManager; import android.os.Handler; import android.os.Looper; import android.util.Log; @@ -41,12 +39,11 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; -import java.util.function.Supplier; import okhttp3.Call; import okhttp3.Interceptor; import okhttp3.OkHttpClient; -/** Entrypoint for Splunk's Android RUM (Real User Monitoring) support. */ +/** Entrypoint for the Splunk OpenTelemetry Instrumentation for Android. */ public class SplunkRum { // initialize this here, statically, to make sure we capture the earliest possible timestamp for // startup. @@ -93,7 +90,18 @@ public class SplunkRum { this.globalAttributes = globalAttributes; } - /** Create a new {@link Config.Builder} instance. */ + /** Creates a new {@link SplunkRumBuilder}, used to set up a {@link SplunkRum} instance. */ + public static SplunkRumBuilder builder() { + return new SplunkRumBuilder(); + } + + /** + * Create a new {@link Config.Builder} instance. + * + * @deprecated Use {@link #builder()} and the {@link SplunkRumBuilder} to configure a {@link + * SplunkRum} instance. + */ + @Deprecated public static Config.Builder newConfigBuilder() { return Config.builder(); } @@ -106,38 +114,29 @@ public static Config.Builder newConfigBuilder() { * @param config The {@link Config} options to use for initialization. * @param application The {@link Application} to be monitored. * @return A fully initialized {@link SplunkRum} instance, ready for use. + * @deprecated Use {@link #builder()} and the {@link SplunkRumBuilder} to configure a {@link + * SplunkRum} instance. */ + @Deprecated public static SplunkRum initialize(Config config, Application application) { - return initialize( - config, - application, - () -> { - Context context = application.getApplicationContext(); - ConnectionUtil connectionUtil = - new ConnectionUtil(NetworkDetector.create(context)); - connectionUtil.startMonitoring( - ConnectionUtil::createNetworkMonitoringRequest, - (ConnectivityManager) - context.getSystemService(Context.CONNECTIVITY_SERVICE)); - return connectionUtil; - }); + return config.toSplunkRumBuilder().build(application); } // for testing purposes static SplunkRum initialize( - Config config, + SplunkRumBuilder builder, Application application, - Supplier connectionUtilSupplier) { + ConnectionUtil.Factory connectionUtilFactory) { if (INSTANCE != null) { Log.w(LOG_TAG, "Singleton SplunkRum instance has already been initialized."); return INSTANCE; } INSTANCE = - new RumInitializer(config, application, startupTimer) - .initialize(connectionUtilSupplier, Looper.getMainLooper()); + new RumInitializer(builder, application, startupTimer) + .initialize(connectionUtilFactory, Looper.getMainLooper()); - if (config.isDebugEnabled()) { + if (builder.debugEnabled) { Log.i( LOG_TAG, "Splunk RUM monitoring initialized with session ID: " + INSTANCE.sessionId); diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/SplunkRumBuilder.java b/splunk-otel-android/src/main/java/com/splunk/rum/SplunkRumBuilder.java new file mode 100644 index 000000000..0b2c85c02 --- /dev/null +++ b/splunk-otel-android/src/main/java/com/splunk/rum/SplunkRumBuilder.java @@ -0,0 +1,323 @@ +/* + * Copyright 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; + +import static com.splunk.rum.DeviceSpanStorageLimiter.DEFAULT_MAX_STORAGE_USE_MB; + +import android.app.Application; +import android.util.Log; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +/** A builder of {@link SplunkRum}. */ +public final class SplunkRumBuilder { + + private static final Duration DEFAULT_SLOW_RENDERING_DETECTION_POLL_INTERVAL = + Duration.ofSeconds(1); + + String applicationName; + String beaconEndpoint; + String rumAccessToken; + private String realm; + boolean debugEnabled = false; + boolean diskBufferingEnabled = false; + boolean crashReportingEnabled = true; + boolean networkMonitorEnabled = true; + boolean anrDetectionEnabled = true; + boolean slowRenderingDetectionEnabled = true; + Duration slowRenderingDetectionPollInterval = DEFAULT_SLOW_RENDERING_DETECTION_POLL_INTERVAL; + private Attributes globalAttributes = Attributes.empty(); + private String deploymentEnvironment; + private final SpanFilterBuilder spanFilterBuilder = new SpanFilterBuilder(); + int maxUsageMegabytes = DEFAULT_MAX_STORAGE_USE_MB; + boolean sessionBasedSamplerEnabled = false; + double sessionBasedSamplerRatio = 1.0; + + /** + * Sets the application name that will be used to identify your application in the Splunk RUM + * UI. + * + * @return {@code this} + */ + public SplunkRumBuilder setApplicationName(String applicationName) { + this.applicationName = applicationName; + return this; + } + + /** + * Sets the "beacon" endpoint URL to be used by the RUM library. + * + *

Note that if you are using standard Splunk ingest, it is simpler to just use {@link + * #setRealm(String)} and let this configuration set the full endpoint URL for you. + * + * @return {@code this} + */ + public SplunkRumBuilder setBeaconEndpoint(String beaconEndpoint) { + if (realm != null) { + Log.w( + SplunkRum.LOG_TAG, + "Explicitly setting the beaconEndpoint will override the realm configuration."); + realm = null; + } + this.beaconEndpoint = beaconEndpoint; + return this; + } + + /** + * Sets the realm for the beacon to send RUM telemetry to. This should be used in place of the + * {@link #setBeaconEndpoint(String)} method in most cases. + * + * @param realm A valid Splunk "realm" + * @return {@code this} + */ + public SplunkRumBuilder setRealm(String realm) { + if (beaconEndpoint != null && this.realm == null) { + Log.w( + SplunkRum.LOG_TAG, + "beaconEndpoint has already been set. Realm configuration will be ignored."); + return this; + } + this.beaconEndpoint = "https://rum-ingest." + realm + ".signalfx.com/v1/rum"; + this.realm = realm; + return this; + } + + /** + * Sets the RUM auth token to be used by the RUM library. + * + * @return {@code this} + */ + public SplunkRumBuilder setRumAccessToken(String rumAuthToken) { + this.rumAccessToken = rumAuthToken; + return this; + } + + /** + * Enables debugging information to be emitted from the RUM library. + * + *

This feature is disabled by default. You can enable it by calling this configuration + * method with a {@code true} value. + * + * @return {@code this} + */ + public SplunkRumBuilder enableDebug() { + this.debugEnabled = true; + return this; + } + + /** + * Enables the storage-based buffering of telemetry. If this feature is enabled, telemetry is + * buffered in the local storage until it is exported; otherwise, it is buffered in memory and + * throttled. + * + *

This feature is disabled by default. You can enable it by calling this configuration + * method with a {@code true} value. + * + * @return {@code this} + */ + public SplunkRumBuilder enableDiskBuffering() { + this.diskBufferingEnabled = true; + return this; + } + + /** + * Disables the crash reporting feature. + * + *

This feature is enabled by default. You can disable it by calling this configuration + * method with a {@code true} value. + * + * @return {@code this} + */ + public SplunkRumBuilder disableCrashReporting() { + this.crashReportingEnabled = false; + return this; + } + + /** + * Disables the network monitoring feature. + * + *

This feature is enabled by default. You can disable it by calling this configuration + * method with a {@code true} value. + * + * @return {@code this} + */ + public SplunkRumBuilder disableNetworkMonitorEnabled() { + this.networkMonitorEnabled = false; + return this; + } + + /** + * Disables the ANR (application not responding) detection feature. If enabled, when the main + * thread is unresponsive for 5 seconds or more, an event including the main thread's stack + * trace will be reported to the RUM system. + * + *

This feature is enabled by default. You can disable it by calling this configuration + * method with a {@code true} value. + * + * @return {@code this} + */ + public SplunkRumBuilder disableAnrDetection() { + this.anrDetectionEnabled = false; + return this; + } + + /** + * Disables the slow rendering detection feature. + * + *

This feature is enabled by default. You can disable it by calling this configuration + * method with a {@code true} value. + * + * @return {@code this} + */ + public SplunkRumBuilder disableSlowRenderingDetection() { + slowRenderingDetectionEnabled = false; + return this; + } + + /** + * Configures the rate at which frame render durations are polled. + * + * @param interval The period that should be used for polling + * @return {@code this} + */ + public SplunkRumBuilder setSlowRenderingDetectionPollInterval(Duration interval) { + if (interval.toMillis() <= 0) { + Log.e( + SplunkRum.LOG_TAG, + "invalid slowRenderPollingDuration: " + interval + " is not positive"); + return this; + } + this.slowRenderingDetectionPollInterval = interval; + return this; + } + + /** + * Provides a set of global {@link Attributes} that will be applied to every span generated by + * the RUM instrumentation. + * + * @return {@code this} + */ + public SplunkRumBuilder setGlobalAttributes(Attributes attributes) { + this.globalAttributes = attributes == null ? Attributes.empty() : attributes; + return this; + } + + /** + * Sets the deployment environment for this RUM instance. Deployment environment is passed along + * as a span attribute to help identify in the Splunk RUM UI. + * + * @param environment The deployment environment name + * @return {@code this} + */ + public SplunkRumBuilder setDeploymentEnvironment(String environment) { + this.deploymentEnvironment = environment; + return this; + } + + /** + * Configures span data filtering. + * + * @param configurer A function that will configure the passed {@link SpanFilterBuilder} + * @return {@code this} + */ + public SplunkRumBuilder filterSpans(Consumer configurer) { + configurer.accept(spanFilterBuilder); + return this; + } + + /** + * Sets the limit of the max number of megabytes that will be used to buffer telemetry data in + * storage. When this value is exceeded, older telemetry will be deleted until the usage is + * reduced. + * + * @return {@code this} + */ + public SplunkRumBuilder limitDiskUsageMegabytes(int maxUsageMegabytes) { + this.maxUsageMegabytes = maxUsageMegabytes; + return this; + } + + /** + * Sets the ratio of sessions that get sampled. Valid values range from 0.0 to 1.0, where 0 + * means no sessions are sampled, and 1 means all sessions are sampled. + * + *

This feature is disabled by default - i.e. by default, all sessions are sampled, which is + * equivalent to {@code ratio = 1.0}. + * + * @return {@code this} + */ + public SplunkRumBuilder enableSessionBasedSampling(double ratio) { + if (ratio < 0.0) { + Log.e( + SplunkRum.LOG_TAG, + "invalid sessionBasedSamplingRatio: " + ratio + " must not be negative"); + return this; + } else if (ratio > 1.0) { + Log.e( + SplunkRum.LOG_TAG, + "invalid sessionBasedSamplingRatio: " + + ratio + + " must not be greater than 1.0"); + return this; + } + + this.sessionBasedSamplerEnabled = true; + this.sessionBasedSamplerRatio = ratio; + return this; + } + + /** + * Creates a new instance of {@link SplunkRum} with the settings of this {@link + * SplunkRumBuilder}. + * + *

You must configure at least the {@linkplain #setApplicationName(String) application name}, + * the {@linkplain #setRealm(String) realm} or the {@linkplain #setBeaconEndpoint(String) beacon + * endpoint}, and the {@linkplain #setRumAccessToken(String) access token} before calling this + * method. Trying to build a {@link SplunkRum} instance without any of these will result in an + * exception being thrown. + * + *

The returned {@link SplunkRum} is set as the global instance {@link + * SplunkRum#getInstance()}. If there was a global {@link SplunkRum} instance configured before, + * this method does not initialize a new one and simply returns the existing instance. + */ + public SplunkRum build(Application application) { + if (rumAccessToken == null || beaconEndpoint == null || applicationName == null) { + throw new IllegalStateException( + "You must provide a rumAccessToken, a realm (or full beaconEndpoint), and an applicationName to create a valid Config instance."); + } + return SplunkRum.initialize(this, application, new ConnectionUtil.Factory()); + } + + SpanExporter decorateWithSpanFilter(SpanExporter exporter) { + return spanFilterBuilder.build().apply(exporter); + } + + AtomicReference buildGlobalAttributesRef() { + Attributes attrs = globalAttributes; + if (deploymentEnvironment != null) { + attrs = + attrs.toBuilder() + .put(ResourceAttributes.DEPLOYMENT_ENVIRONMENT, deploymentEnvironment) + .build(); + } + return new AtomicReference<>(attrs); + } +} diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/StandardAttributes.java b/splunk-otel-android/src/main/java/com/splunk/rum/StandardAttributes.java index a71ff8fdf..64b4d2882 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/StandardAttributes.java +++ b/splunk-otel-android/src/main/java/com/splunk/rum/StandardAttributes.java @@ -28,7 +28,7 @@ public final class StandardAttributes { /** * The version of your app. Useful for adding to global attributes. * - * @see Config.Builder#globalAttributes(Attributes) + * @see SplunkRumBuilder#setGlobalAttributes(Attributes) */ public static final AttributeKey APP_VERSION = AttributeKey.stringKey("app.version"); @@ -36,7 +36,7 @@ public final class StandardAttributes { * The build type of your app (typically one of debug or release). Useful for adding to global * attributes. * - * @see Config.Builder#globalAttributes(Attributes) + * @see SplunkRumBuilder#setGlobalAttributes(Attributes) */ public static final AttributeKey APP_BUILD_TYPE = AttributeKey.stringKey("app.build.type"); diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/ZipkinWriteToDiskExporterFactory.java b/splunk-otel-android/src/main/java/com/splunk/rum/ZipkinWriteToDiskExporterFactory.java index 0b1ca9522..e3396deba 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/ZipkinWriteToDiskExporterFactory.java +++ b/splunk-otel-android/src/main/java/com/splunk/rum/ZipkinWriteToDiskExporterFactory.java @@ -30,7 +30,7 @@ class ZipkinWriteToDiskExporterFactory { private ZipkinWriteToDiskExporterFactory() {} - static ZipkinSpanExporter create(Application application, Config config) { + static ZipkinSpanExporter create(Application application, int maxUsageMegabytes) { File spansPath = FileUtils.getSpansDirectory(application); if (!spansPath.exists()) { if (!spansPath.mkdirs()) { @@ -48,7 +48,7 @@ static ZipkinSpanExporter create(Application application, Config config) { DeviceSpanStorageLimiter.builder() .fileUtils(fileUtils) .path(spansPath) - .maxStorageUseMb(config.getMaxUsageMegabytes()) + .maxStorageUseMb(maxUsageMegabytes) .build(); Sender sender = ZipkinToDiskSender.builder() diff --git a/splunk-otel-android/src/test/java/com/splunk/rum/RumInitializerTest.java b/splunk-otel-android/src/test/java/com/splunk/rum/RumInitializerTest.java index f2963983e..79b4e1c22 100644 --- a/splunk-otel-android/src/test/java/com/splunk/rum/RumInitializerTest.java +++ b/splunk-otel-android/src/test/java/com/splunk/rum/RumInitializerTest.java @@ -48,17 +48,16 @@ public class RumInitializerTest { @Test public void initializationSpan() { - Config config = - Config.builder() - .realm("dev") - .applicationName("testApp") - .rumAccessToken("accessToken") - .build(); + SplunkRumBuilder splunkRumBuilder = + new SplunkRumBuilder() + .setRealm("dev") + .setApplicationName("testApp") + .setRumAccessToken("accessToken"); Application application = mock(Application.class); InMemorySpanExporter testExporter = InMemorySpanExporter.create(); AppStartupTimer startupTimer = new AppStartupTimer(); RumInitializer testInitializer = - new RumInitializer(config, application, startupTimer) { + new RumInitializer(splunkRumBuilder, application, startupTimer) { @Override SpanExporter buildFilteringExporter(ConnectionUtil connectionUtil) { return testExporter; @@ -66,7 +65,7 @@ SpanExporter buildFilteringExporter(ConnectionUtil connectionUtil) { }; SplunkRum splunkRum = testInitializer.initialize( - () -> mock(ConnectionUtil.class, RETURNS_DEEP_STUBS), mock(Looper.class)); + mock(ConnectionUtil.Factory.class, RETURNS_DEEP_STUBS), mock(Looper.class)); startupTimer.runCompletionCallback(); splunkRum.flushSpans(); @@ -103,17 +102,16 @@ private void checkEventExists(List events, String eventName) { @Test public void spanLimitsAreConfigured() { - Config config = - Config.builder() - .realm("dev") - .applicationName("testApp") - .rumAccessToken("accessToken") - .build(); + SplunkRumBuilder splunkRumBuilder = + new SplunkRumBuilder() + .setRealm("dev") + .setApplicationName("testApp") + .setRumAccessToken("accessToken"); Application application = mock(Application.class); InMemorySpanExporter testExporter = InMemorySpanExporter.create(); AppStartupTimer startupTimer = new AppStartupTimer(); RumInitializer testInitializer = - new RumInitializer(config, application, startupTimer) { + new RumInitializer(splunkRumBuilder, application, startupTimer) { @Override SpanExporter buildFilteringExporter(ConnectionUtil connectionUtil) { return testExporter; @@ -121,7 +119,7 @@ SpanExporter buildFilteringExporter(ConnectionUtil connectionUtil) { }; SplunkRum splunkRum = testInitializer.initialize( - () -> mock(ConnectionUtil.class, RETURNS_DEEP_STUBS), mock(Looper.class)); + mock(ConnectionUtil.Factory.class, RETURNS_DEEP_STUBS), mock(Looper.class)); splunkRum.flushSpans(); testExporter.reset(); @@ -146,18 +144,17 @@ SpanExporter buildFilteringExporter(ConnectionUtil connectionUtil) { /** Verify that we have buffering in place in our exporter implementation. */ @Test public void verifyExporterBuffering() { - Config config = - Config.builder() - .realm("dev") - .applicationName("testApp") - .rumAccessToken("accessToken") - .build(); + SplunkRumBuilder splunkRumBuilder = + new SplunkRumBuilder() + .setRealm("dev") + .setApplicationName("testApp") + .setRumAccessToken("accessToken"); Application application = mock(Application.class); AppStartupTimer startupTimer = new AppStartupTimer(); InMemorySpanExporter testExporter = InMemorySpanExporter.create(); RumInitializer testInitializer = - new RumInitializer(config, application, startupTimer) { + new RumInitializer(splunkRumBuilder, application, startupTimer) { @Override SpanExporter getCoreSpanExporter(String endpoint) { return testExporter; @@ -205,27 +202,27 @@ private TestSpanData createTestSpan(long startTimeNanos) { public void shouldTranslateExceptionEventsToSpanAttributes() { InMemorySpanExporter spanExporter = InMemorySpanExporter.create(); - Config config = - Config.builder() - .realm("us0") - .rumAccessToken("secret!") - .applicationName("test") - .debugEnabled(true) - .build(); + SplunkRumBuilder splunkRumBuilder = + new SplunkRumBuilder() + .setRealm("us0") + .setRumAccessToken("secret!") + .setApplicationName("test"); Application application = mock(Application.class); ConnectionUtil connectionUtil = mock(ConnectionUtil.class, RETURNS_DEEP_STUBS); when(connectionUtil.refreshNetworkStatus().isOnline()).thenReturn(true); + ConnectionUtil.Factory connectionUtilFactory = mock(ConnectionUtil.Factory.class); + when(connectionUtilFactory.createAndStart(application)).thenReturn(connectionUtil); AppStartupTimer appStartupTimer = new AppStartupTimer(); RumInitializer initializer = - new RumInitializer(config, application, appStartupTimer) { + new RumInitializer(splunkRumBuilder, application, appStartupTimer) { @Override SpanExporter getCoreSpanExporter(String endpoint) { return spanExporter; } }; - SplunkRum splunkRum = initializer.initialize(() -> connectionUtil, mock(Looper.class)); + SplunkRum splunkRum = initializer.initialize(connectionUtilFactory, mock(Looper.class)); appStartupTimer.runCompletionCallback(); Exception e = new IllegalArgumentException("booom!"); diff --git a/splunk-otel-android/src/test/java/com/splunk/rum/SplunkRumBuilderTest.java b/splunk-otel-android/src/test/java/com/splunk/rum/SplunkRumBuilderTest.java new file mode 100644 index 000000000..d98dbfd83 --- /dev/null +++ b/splunk-otel-android/src/test/java/com/splunk/rum/SplunkRumBuilderTest.java @@ -0,0 +1,91 @@ +/* + * Copyright 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +import android.app.Application; +import io.opentelemetry.api.common.Attributes; +import org.junit.Test; + +public class SplunkRumBuilderTest { + + @Test + public void buildingRequiredFields() { + Application app = mock(Application.class); + + assertThrows(IllegalStateException.class, () -> SplunkRum.builder().build(app)); + assertThrows( + IllegalStateException.class, + () -> + SplunkRum.builder() + .setRumAccessToken("abc123") + .setBeaconEndpoint("http://backend") + .build(app)); + assertThrows( + IllegalStateException.class, + () -> + SplunkRum.builder() + .setBeaconEndpoint("http://backend") + .setApplicationName("appName") + .build(app)); + assertThrows( + IllegalStateException.class, + () -> + SplunkRum.builder() + .setApplicationName("appName") + .setRumAccessToken("abc123") + .build(app)); + } + + @Test + public void defaultValues() { + SplunkRumBuilder builder = SplunkRum.builder(); + + assertFalse(builder.debugEnabled); + assertFalse(builder.diskBufferingEnabled); + assertTrue(builder.crashReportingEnabled); + assertTrue(builder.networkMonitorEnabled); + assertTrue(builder.anrDetectionEnabled); + assertTrue(builder.slowRenderingDetectionEnabled); + assertEquals(Attributes.empty(), builder.buildGlobalAttributesRef().get()); + assertFalse(builder.sessionBasedSamplerEnabled); + } + + @Test + public void handleNullAttributes() { + SplunkRumBuilder builder = SplunkRum.builder().setGlobalAttributes(null); + assertEquals(Attributes.empty(), builder.buildGlobalAttributesRef().get()); + } + + @Test + public void setBeaconFromRealm() { + SplunkRumBuilder builder = SplunkRum.builder().setRealm("us0"); + assertEquals("https://rum-ingest.us0.signalfx.com/v1/rum", builder.beaconEndpoint); + } + + @Test + public void beaconOverridesRealm() { + SplunkRumBuilder builder = + SplunkRum.builder().setRealm("us0").setBeaconEndpoint("http://beacon"); + assertEquals("http://beacon", builder.beaconEndpoint); + } +} diff --git a/splunk-otel-android/src/test/java/com/splunk/rum/SplunkRumTest.java b/splunk-otel-android/src/test/java/com/splunk/rum/SplunkRumTest.java index 838dfbc07..a32c565a7 100644 --- a/splunk-otel-android/src/test/java/com/splunk/rum/SplunkRumTest.java +++ b/splunk-otel-android/src/test/java/com/splunk/rum/SplunkRumTest.java @@ -23,7 +23,6 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; @@ -49,13 +48,11 @@ import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; import java.io.File; -import java.time.Duration; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.mockito.internal.stubbing.answers.ReturnsArgumentAt; public class SplunkRumTest { @@ -74,19 +71,23 @@ public void setup() { @Test public void initialization_onlyOnce() { Application application = mock(Application.class, RETURNS_DEEP_STUBS); - Config config = mock(Config.class); - ConnectionUtil connectionUtil = mock(ConnectionUtil.class, RETURNS_DEEP_STUBS); + ConnectionUtil.Factory connectionUtilFactory = + mock(ConnectionUtil.Factory.class, RETURNS_DEEP_STUBS); Context context = mock(Context.class); - when(config.getBeaconEndpoint()).thenReturn("http://backend"); - when(config.isDebugEnabled()).thenReturn(true); - when(config.decorateWithSpanFilter(any())).then(new ReturnsArgumentAt(0)); - when(config.getSlowRenderingDetectionPollInterval()).thenReturn(Duration.ofMillis(1000)); + SplunkRumBuilder splunkRumBuilder = + new SplunkRumBuilder() + .setApplicationName("appName") + .setBeaconEndpoint("http://backend") + .setRumAccessToken("abracadabra") + .disableAnrDetection(); + when(application.getApplicationContext()).thenReturn(context); when(context.getFilesDir()).thenReturn(new File("/my/storage/spot")); - SplunkRum singleton = SplunkRum.initialize(config, application, () -> connectionUtil); - SplunkRum sameInstance = SplunkRum.initialize(config, application); + SplunkRum singleton = + SplunkRum.initialize(splunkRumBuilder, application, connectionUtilFactory); + SplunkRum sameInstance = splunkRumBuilder.build(application); assertSame(singleton, sameInstance); } @@ -100,41 +101,49 @@ public void getInstance_preConfig() { @Test public void getInstance() { Application application = mock(Application.class, RETURNS_DEEP_STUBS); - Config config = mock(Config.class); + ConnectionUtil.Factory connectionUtilFactory = + mock(ConnectionUtil.Factory.class, RETURNS_DEEP_STUBS); Context context = mock(Context.class); - when(config.getBeaconEndpoint()).thenReturn("http://backend"); - when(config.decorateWithSpanFilter(any())).then(new ReturnsArgumentAt(0)); - when(config.getSlowRenderingDetectionPollInterval()).thenReturn(Duration.ofMillis(1000)); + SplunkRumBuilder splunkRumBuilder = + new SplunkRumBuilder() + .setApplicationName("appName") + .setBeaconEndpoint("http://backend") + .setRumAccessToken("abracadabra") + .disableAnrDetection(); + when(application.getApplicationContext()).thenReturn(context); when(context.getFilesDir()).thenReturn(new File("/my/storage/spot")); SplunkRum singleton = - SplunkRum.initialize( - config, application, () -> mock(ConnectionUtil.class, RETURNS_DEEP_STUBS)); + SplunkRum.initialize(splunkRumBuilder, application, connectionUtilFactory); assertSame(singleton, SplunkRum.getInstance()); } @Test - public void newConfigBuilder() { - assertNotNull(SplunkRum.newConfigBuilder()); + public void newBuilder() { + assertNotNull(SplunkRum.builder()); } @Test public void nonNullMethods() { Application application = mock(Application.class, RETURNS_DEEP_STUBS); - Config config = mock(Config.class); + ConnectionUtil.Factory connectionUtilFactory = + mock(ConnectionUtil.Factory.class, RETURNS_DEEP_STUBS); Context context = mock(Context.class); - when(config.getBeaconEndpoint()).thenReturn("http://backend"); - when(config.decorateWithSpanFilter(any())).then(new ReturnsArgumentAt(0)); - when(config.getSlowRenderingDetectionPollInterval()).thenReturn(Duration.ofMillis(1000)); when(application.getApplicationContext()).thenReturn(context); when(context.getFilesDir()).thenReturn(new File("/my/storage/spot")); + SplunkRumBuilder splunkRumBuilder = + new SplunkRumBuilder() + .setApplicationName("appName") + .setBeaconEndpoint("http://backend") + .setRumAccessToken("abracadabra") + .disableAnrDetection(); + SplunkRum splunkRum = - SplunkRum.initialize( - config, application, () -> mock(ConnectionUtil.class, RETURNS_DEEP_STUBS)); + SplunkRum.initialize(splunkRumBuilder, application, connectionUtilFactory); assertNotNull(splunkRum.getOpenTelemetry()); assertNotNull(splunkRum.getRumSessionId()); } @@ -267,19 +276,23 @@ public void createAndEnd() { @Test public void integrateWithBrowserRum() { Application application = mock(Application.class, RETURNS_DEEP_STUBS); - Config config = mock(Config.class); - WebView webView = mock(WebView.class); + ConnectionUtil.Factory connectionUtilFactory = + mock(ConnectionUtil.Factory.class, RETURNS_DEEP_STUBS); Context context = mock(Context.class); + WebView webView = mock(WebView.class); - when(config.getBeaconEndpoint()).thenReturn("http://backend"); - when(config.decorateWithSpanFilter(any())).then(new ReturnsArgumentAt(0)); - when(config.getSlowRenderingDetectionPollInterval()).thenReturn(Duration.ofMillis(1000)); when(application.getApplicationContext()).thenReturn(context); when(context.getFilesDir()).thenReturn(new File("/my/storage/spot")); + SplunkRumBuilder splunkRumBuilder = + new SplunkRumBuilder() + .setApplicationName("appName") + .setBeaconEndpoint("http://backend") + .setRumAccessToken("abracadabra") + .disableAnrDetection(); + SplunkRum splunkRum = - SplunkRum.initialize( - config, application, () -> mock(ConnectionUtil.class, RETURNS_DEEP_STUBS)); + SplunkRum.initialize(splunkRumBuilder, application, connectionUtilFactory); splunkRum.integrateWithBrowserRum(webView); verify(webView)