diff --git a/README.md b/README.md index 3561bd6cf..fa2bac98c 100644 --- a/README.md +++ b/README.md @@ -8,26 +8,26 @@

-The Embrace Android SDK provides instrumentation for Android apps. +The Embrace Android SDK gives you performance and stability insights into the user experience of your mobile apps. -[![codecov](https://codecov.io/gh/embrace-io/embrace-android-sdk/branch/master/graph/badge.svg?token=QycvaJjZgr)](https://codecov.io/gh/embrace-io/embrace-android-sdk) -[![android api](https://img.shields.io/badge/Android_API-16-green.svg "Android min API 21")](https://dash.embrace.io/signup/) +[![codecov](https://codecov.io/gh/embrace-io/embrace-android-sdk/graph/badge.svg?token=4kNC8ceoVB)](https://codecov.io/gh/embrace-io/embrace-android-sdk) +[![android api](https://img.shields.io/badge/Android_API-21-green.svg "Android min API 21")](https://dash.embrace.io/signup/) [![build](https://img.shields.io/github/actions/workflow/status/embrace-io/embrace-android-sdk/ci-gradle.yml)](https://github.com/embrace-io/embrace-android-sdk/actions) # Getting Started -> :warning: **This is for native android apps**: Leverage in our Unity, ReactNative and Flutter SDKs for cross-platform apps +> :warning: **This is for native Android apps**: Use our Unity, ReactNative and Flutter SDKs for cross-platform apps - [Go to our dashboard](https://dash.embrace.io/signup/) to create an account and get your API key -- Check our [guide](https://embrace.io/docs/android/integration/) to integrate the SDK into your app +- Check our [guide](https://embrace.io/docs/android/integration/) for instructions to integrate the SDK into your app ## Upgrading from 5.x -Follow our [upgrading guide](https://github.com/embrace-io/embrace-android-sdk/blob/master/UPGRADING.md) +- Follow our [upgrade guide](https://github.com/embrace-io/embrace-android-sdk/blob/master/UPGRADING.md) # Usage -- Refer to our [Features page](https://embrace.io/docs/android/features/) to learn about the features Embrace SDK provides +- Refer to our [features page](https://embrace.io/docs/android/features/) to learn about the features the Embrace Android SDK provides # Support @@ -36,27 +36,16 @@ Follow our [upgrading guide](https://github.com/embrace-io/embrace-android-sdk/b # Development -## Code Formatting +## Code Formatting and Linting -In most of our repos we are using Detekt to analyse our kotlin code. This analysis should be done before the new code is merged to master. It’s considered a good practice to run the command before pushing our code. Github workflows will also be running this check. +We use `detekt` to lint our Kotlin code. GitHub workflows will run this check automatically and flag any violations. To run the check locally, you can run the following command in the root directory of the project: `./gradlew detekt` -n some cases, the errors get fixed just by running the command, so if you run it again, you could get less errors than the first time. As a result, it will list the errors and you need to go and fix them if you consider appropriate. - -You can run the command until you get no errors or until you get only the errors you don’t want to fix. - -If you have errors you want to ignore, you need to run: - -```bash -./gradlew detektBaseline -``` - -This command will add a line per error to be ignored into the `baseline.xml` file. This way, this file will be updated and the code smell will be also ignored by Github Workflows. +In many cases, running the command will fix the errors, so if you run it again, you might get fewer errors, leaving only the ones you need to fix manually. ## License -See the [LICENSE](https://github.com/embrace-io/embrace-android-sdk/blob/master/LICENSE) -for details. +See the [LICENSE](https://github.com/embrace-io/embrace-android-sdk/blob/master/LICENSE) for details. diff --git a/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/EmbraceNodeIterator.kt b/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/EmbraceNodeIterator.kt index 3a8d13497..20b97b18e 100644 --- a/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/EmbraceNodeIterator.kt +++ b/embrace-android-compose/src/main/java/io/embrace/android/embracesdk/compose/internal/EmbraceNodeIterator.kt @@ -31,7 +31,7 @@ internal class EmbraceNodeIterator { backgroundWorker.submit { findClickedElement(semanticsNodes, x, y)?.let { val clickedView = ClickedView(it, x, y) - Embrace.getInstance().logComposeTap(Pair(clickedView.x, clickedView.y), clickedView.tag) + Embrace.getInstance().internalInterface.logComposeTap(Pair(clickedView.x, clickedView.y), clickedView.tag) } } } diff --git a/embrace-android-fcm/src/main/java/io/embrace/android/embracesdk/fcm/swazzle/callback/com/android/fcm/FirebaseSwazzledHooks.java b/embrace-android-fcm/src/main/java/io/embrace/android/embracesdk/fcm/swazzle/callback/com/android/fcm/FirebaseSwazzledHooks.java index 6f131068f..82da2ad05 100644 --- a/embrace-android-fcm/src/main/java/io/embrace/android/embracesdk/fcm/swazzle/callback/com/android/fcm/FirebaseSwazzledHooks.java +++ b/embrace-android-fcm/src/main/java/io/embrace/android/embracesdk/fcm/swazzle/callback/com/android/fcm/FirebaseSwazzledHooks.java @@ -32,7 +32,7 @@ public static void _onMessageReceived(@NonNull RemoteMessage message) { private static void handleRemoteMessage(@NonNull RemoteMessage message) { try { //flag process is already running to avoid track warm startup - Embrace.getInstance().setProcessStartedByNotification(); + Embrace.getInstance().getInternalInterface().setProcessStartedByNotification(); String messageId = null; try { diff --git a/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3ApplicationInterceptor.java b/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3ApplicationInterceptor.java index 37f578e0c..49317dc91 100644 --- a/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3ApplicationInterceptor.java +++ b/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3ApplicationInterceptor.java @@ -38,15 +38,12 @@ public class EmbraceOkHttp3ApplicationInterceptor implements Interceptor { static final String UNKNOWN_MESSAGE = "An error occurred during the execution of this network request"; final Embrace embrace; - private final SdkFacade sdkFacade; - public EmbraceOkHttp3ApplicationInterceptor() { - this(Embrace.getInstance(), new SdkFacade()); + this(Embrace.getInstance()); } - EmbraceOkHttp3ApplicationInterceptor(Embrace embrace, SdkFacade sdkFacade) { + EmbraceOkHttp3ApplicationInterceptor(Embrace embrace) { this.embrace = embrace; - this.sdkFacade = sdkFacade; } @Override @@ -69,7 +66,7 @@ public Response intercept(Chain chain) throws IOException { causeName(e, UNKNOWN_EXCEPTION), causeMessage(e, UNKNOWN_MESSAGE), request.header(embrace.getTraceIdHeader()), - sdkFacade.isNetworkSpanForwardingEnabled() ? request.header(TRACEPARENT_HEADER_NAME) : null, + embrace.getInternalInterface().isNetworkSpanForwardingEnabled() ? request.header(TRACEPARENT_HEADER_NAME) : null, null ) ); @@ -91,7 +88,7 @@ public Response intercept(Chain chain) throws IOException { errorType != null ? errorType : UNKNOWN_EXCEPTION, errorMessage != null ? errorMessage : UNKNOWN_MESSAGE, request.header(embrace.getTraceIdHeader()), - sdkFacade.isNetworkSpanForwardingEnabled() ? request.header(TRACEPARENT_HEADER_NAME) : null, + embrace.getInternalInterface().isNetworkSpanForwardingEnabled() ? request.header(TRACEPARENT_HEADER_NAME) : null, null ) ); diff --git a/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3NetworkInterceptor.java b/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3NetworkInterceptor.java index c667b2841..5e35c94ec 100644 --- a/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3NetworkInterceptor.java +++ b/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3NetworkInterceptor.java @@ -58,15 +58,13 @@ public final class EmbraceOkHttp3NetworkInterceptor implements Interceptor { }; final Embrace embrace; - private final SdkFacade sdkFacade; public EmbraceOkHttp3NetworkInterceptor() { - this(Embrace.getInstance(), new SdkFacade()); + this(Embrace.getInstance()); } - EmbraceOkHttp3NetworkInterceptor(Embrace embrace, SdkFacade sdkFacade) { + EmbraceOkHttp3NetworkInterceptor(Embrace embrace) { this.embrace = embrace; - this.sdkFacade = sdkFacade; } @Override @@ -77,7 +75,7 @@ public Response intercept(Chain chain) throws IOException { return chain.proceed(originalRequest); } - boolean networkSpanForwardingEnabled = sdkFacade.isNetworkSpanForwardingEnabled(); + boolean networkSpanForwardingEnabled = embrace.getInternalInterface().isNetworkSpanForwardingEnabled(); String traceparent = null; if (networkSpanForwardingEnabled && originalRequest.header(TRACEPARENT_HEADER_NAME) == null) { @@ -123,7 +121,8 @@ public Response intercept(Chain chain) throws IOException { contentLength = 0L; } - boolean shouldCaptureNetworkData = embrace.shouldCaptureNetworkBody(request.url().toString(), request.method()); + boolean shouldCaptureNetworkData = + embrace.getInternalInterface().shouldCaptureNetworkBody(request.url().toString(), request.method()); if (shouldCaptureNetworkData && ENCODING_GZIP.equalsIgnoreCase(networkResponse.header(CONTENT_ENCODING_HEADER_NAME)) && diff --git a/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/SdkFacade.java b/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/SdkFacade.java deleted file mode 100644 index d55e00268..000000000 --- a/embrace-android-okhttp3/src/main/java/io/embrace/android/embracesdk/okhttp3/SdkFacade.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.embrace.android.embracesdk.okhttp3; - -import io.embrace.android.embracesdk.Embrace; -import io.embrace.android.embracesdk.utils.NetworkUtils; - -/** - * Facade to call internal SDK methods that can be mocked for tests - */ -class SdkFacade { - boolean isNetworkSpanForwardingEnabled() { - return NetworkUtils.isNetworkSpanForwardingEnabled(Embrace.getInstance().getConfigService()); - } -} diff --git a/embrace-android-okhttp3/src/test/kotlin/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3InterceptorsTest.kt b/embrace-android-okhttp3/src/test/kotlin/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3InterceptorsTest.kt index 087efd9f5..a066c73ac 100644 --- a/embrace-android-okhttp3/src/test/kotlin/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3InterceptorsTest.kt +++ b/embrace-android-okhttp3/src/test/kotlin/io/embrace/android/embracesdk/okhttp3/EmbraceOkHttp3InterceptorsTest.kt @@ -1,6 +1,7 @@ package io.embrace.android.embracesdk.okhttp3 import io.embrace.android.embracesdk.Embrace +import io.embrace.android.embracesdk.internal.EmbraceInternalInterface import io.embrace.android.embracesdk.network.EmbraceNetworkRequest import io.embrace.android.embracesdk.network.http.NetworkCaptureData import io.embrace.android.embracesdk.okhttp3.EmbraceOkHttp3ApplicationInterceptor.UNKNOWN_EXCEPTION @@ -64,7 +65,7 @@ internal class EmbraceOkHttp3InterceptorsTest { private lateinit var postNetworkInterceptorTestInterceptor: Interceptor private lateinit var okHttpClient: OkHttpClient private lateinit var mockEmbrace: Embrace - private lateinit var mockSdkFacade: SdkFacade + private lateinit var mockInternalInterface: EmbraceInternalInterface private lateinit var getRequestBuilder: Request.Builder private lateinit var postRequestBuilder: Request.Builder private lateinit var capturedEmbraceNetworkRequest: CapturingSlot @@ -79,13 +80,16 @@ internal class EmbraceOkHttp3InterceptorsTest { fun setup() { server = MockWebServer() mockEmbrace = mockk(relaxed = true) - mockSdkFacade = mockk(relaxed = true) - applicationInterceptor = EmbraceOkHttp3ApplicationInterceptor(mockEmbrace, mockSdkFacade) + mockInternalInterface = mockk(relaxed = true) + every { mockInternalInterface.shouldCaptureNetworkBody(any(), "POST") } answers { true } + every { mockInternalInterface.shouldCaptureNetworkBody(any(), "GET") } answers { false } + every { mockInternalInterface.isNetworkSpanForwardingEnabled() } answers { isNetworkSpanForwardingEnabled } + applicationInterceptor = EmbraceOkHttp3ApplicationInterceptor(mockEmbrace) preNetworkInterceptorTestInterceptor = TestInspectionInterceptor( beforeRequestSent = { request -> preNetworkInterceptorBeforeRequestSupplier.invoke(request) }, afterResponseReceived = { response -> preNetworkInterceptorAfterResponseSupplier.invoke(response) } ) - networkInterceptor = EmbraceOkHttp3NetworkInterceptor(mockEmbrace, mockSdkFacade) + networkInterceptor = EmbraceOkHttp3NetworkInterceptor(mockEmbrace) postNetworkInterceptorTestInterceptor = TestInspectionInterceptor( beforeRequestSent = { request -> postNetworkInterceptorBeforeRequestSupplier.invoke(request) }, afterResponseReceived = { response -> postNetworkInterceptorAfterResponseSupplier.invoke(response) } @@ -106,11 +110,9 @@ internal class EmbraceOkHttp3InterceptorsTest { .header(requestHeaderName, requestHeaderValue) capturedEmbraceNetworkRequest = slot() every { mockEmbrace.isStarted } answers { isSDKStarted } - every { mockEmbrace.shouldCaptureNetworkBody(any(), "POST") } answers { true } - every { mockEmbrace.shouldCaptureNetworkBody(any(), "GET") } answers { false } every { mockEmbrace.recordNetworkRequest(capture(capturedEmbraceNetworkRequest)) } answers { } every { mockEmbrace.generateW3cTraceparent() } answers { GENERATED_TRACEPARENT } - every { mockSdkFacade.isNetworkSpanForwardingEnabled } answers { isNetworkSpanForwardingEnabled } + every { mockEmbrace.internalInterface } answers { mockInternalInterface } } @After @@ -184,8 +186,8 @@ internal class EmbraceOkHttp3InterceptorsTest { isSDKStarted = false server.enqueue(createBaseMockResponse()) runGetRequest() - verify(exactly = 0) { mockSdkFacade.isNetworkSpanForwardingEnabled } - verify(exactly = 0) { mockEmbrace.shouldCaptureNetworkBody(any(), any()) } + verify(exactly = 0) { mockInternalInterface.isNetworkSpanForwardingEnabled() } + verify(exactly = 0) { mockEmbrace.internalInterface.shouldCaptureNetworkBody(any(), any()) } } @Test diff --git a/embrace-android-sdk/api/embrace-android-sdk.api b/embrace-android-sdk/api/embrace-android-sdk.api index d8f3eb415..12bdf2556 100644 --- a/embrace-android-sdk/api/embrace-android-sdk.api +++ b/embrace-android-sdk/api/embrace-android-sdk.api @@ -37,6 +37,7 @@ public final class io/embrace/android/embracesdk/Embrace : io/embrace/android/em public fun getDeviceId ()Ljava/lang/String; public fun getFlutterInternalInterface ()Lio/embrace/android/embracesdk/FlutterInternalInterface; public static fun getInstance ()Lio/embrace/android/embracesdk/Embrace; + public fun getInternalInterface ()Lio/embrace/android/embracesdk/internal/EmbraceInternalInterface; public fun getLastRunEndState ()Lio/embrace/android/embracesdk/Embrace$LastRunEndState; public fun getReactNativeInternalInterface ()Lio/embrace/android/embracesdk/ReactNativeInternalInterface; public fun getSessionProperties ()Ljava/util/Map; @@ -44,7 +45,6 @@ public final class io/embrace/android/embracesdk/Embrace : io/embrace/android/em public fun getUnityInternalInterface ()Lio/embrace/android/embracesdk/UnityInternalInterface; public fun isStarted ()Z public fun isTracingAvailable ()Z - public fun logComposeTap (Landroid/util/Pair;Ljava/lang/String;)V public fun logCustomStacktrace ([Ljava/lang/StackTraceElement;)V public fun logCustomStacktrace ([Ljava/lang/StackTraceElement;Lio/embrace/android/embracesdk/Severity;)V public fun logCustomStacktrace ([Ljava/lang/StackTraceElement;Lio/embrace/android/embracesdk/Severity;Ljava/util/Map;)V @@ -77,12 +77,12 @@ public final class io/embrace/android/embracesdk/Embrace : io/embrace/android/em public fun removeSessionProperty (Ljava/lang/String;)Z public fun sampleCurrentThreadDuringAnrs ()V public fun setAppId (Ljava/lang/String;)Z - public fun setProcessStartedByNotification ()V + public fun setDartVersion (Ljava/lang/String;)V + public fun setEmbraceFlutterSdkVersion (Ljava/lang/String;)V public fun setUserAsPayer ()V public fun setUserEmail (Ljava/lang/String;)V public fun setUserIdentifier (Ljava/lang/String;)V public fun setUsername (Ljava/lang/String;)V - public fun shouldCaptureNetworkBody (Ljava/lang/String;Ljava/lang/String;)Z public fun start (Landroid/content/Context;)V public fun start (Landroid/content/Context;Z)V public fun start (Landroid/content/Context;ZLio/embrace/android/embracesdk/Embrace$AppFramework;)V @@ -177,6 +177,22 @@ public final class io/embrace/android/embracesdk/WebViewClientSwazzledHooks { public abstract interface annotation class io/embrace/android/embracesdk/annotation/StartupActivity : java/lang/annotation/Annotation { } +public abstract interface class io/embrace/android/embracesdk/internal/EmbraceInternalInterface { + public abstract fun getSdkCurrentTime ()J + public abstract fun isNetworkSpanForwardingEnabled ()Z + public abstract fun logComposeTap (Landroid/util/Pair;Ljava/lang/String;)V + public abstract fun logError (Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Z)V + public abstract fun logHandledException (Ljava/lang/Throwable;Lio/embrace/android/embracesdk/LogType;Ljava/util/Map;[Ljava/lang/StackTraceElement;)V + public abstract fun logInfo (Ljava/lang/String;Ljava/util/Map;)V + public abstract fun logWarning (Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)V + public abstract fun recordAndDeduplicateNetworkRequest (Ljava/lang/String;Lio/embrace/android/embracesdk/network/EmbraceNetworkRequest;)V + public abstract fun recordCompletedNetworkRequest (Ljava/lang/String;Ljava/lang/String;JJJJILjava/lang/String;Lio/embrace/android/embracesdk/network/http/NetworkCaptureData;)V + public abstract fun recordIncompleteNetworkRequest (Ljava/lang/String;Ljava/lang/String;JJLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/embrace/android/embracesdk/network/http/NetworkCaptureData;)V + public abstract fun recordIncompleteNetworkRequest (Ljava/lang/String;Ljava/lang/String;JJLjava/lang/Throwable;Ljava/lang/String;Lio/embrace/android/embracesdk/network/http/NetworkCaptureData;)V + public abstract fun setProcessStartedByNotification ()V + public abstract fun shouldCaptureNetworkBody (Ljava/lang/String;Ljava/lang/String;)Z +} + public final class io/embrace/android/embracesdk/network/EmbraceNetworkRequest { public static fun fromCompletedRequest (Ljava/lang/String;Lio/embrace/android/embracesdk/network/http/HttpMethod;JJJJI)Lio/embrace/android/embracesdk/network/EmbraceNetworkRequest; public static fun fromCompletedRequest (Ljava/lang/String;Lio/embrace/android/embracesdk/network/http/HttpMethod;JJJJILjava/lang/String;)Lio/embrace/android/embracesdk/network/EmbraceNetworkRequest; diff --git a/embrace-android-sdk/lint-baseline.xml b/embrace-android-sdk/lint-baseline.xml index 0458e5d1b..c7d934a43 100644 --- a/embrace-android-sdk/lint-baseline.xml +++ b/embrace-android-sdk/lint-baseline.xml @@ -8,65 +8,10 @@ errorLine2=" ~~~~~~~~~~~"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -206,7 +41,7 @@ errorLine2=" ~~~~~~~~~~~~"> @@ -217,7 +52,7 @@ errorLine2=" ~~~~~~~~~~~~~~~"> @@ -250,7 +85,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -261,87 +96,10 @@ errorLine2=" ~~~~~~"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -558,18 +294,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> - - - - @@ -595,17 +320,6 @@ column="16"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - + errorLine1=" public static HttpMethod fromString(String method) {" + errorLine2=" ~~~~~~~~~~"> + file="src/main/java/io/embrace/android/embracesdk/network/http/HttpMethod.java" + line="27" + column="19"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + errorLine1=" public static HttpMethod fromString(String method) {" + errorLine2=" ~~~~~~"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Unit): SessionMessage { +internal fun IntegrationTestRule.Harness.recordSession( + simulateAppStartup: Boolean = false, + action: () -> Unit +): SessionMessage { // get the activity service & simulate the lifecycle event that triggers a new session. val activityService = checkNotNull(Embrace.getImpl().activityService) + val activityController = if (simulateAppStartup) Robolectric.buildActivity(Activity::class.java) else null + + activityController?.create() + activityController?.start() activityService.onForeground() + activityController?.resume() // assert a session was started. val startSession = getLastSentSessionMessage() @@ -74,12 +85,15 @@ internal fun IntegrationTestRule.Harness.recordSession(action: () -> Unit): Sess // end session 30s later by entering background fakeClock.tick(30000) + activityController?.pause() activityService.onBackground() + activityController?.stop() val endSession = getLastSentSessionMessage() assertEquals("en", endSession.session.messageType) // TODO: future: increase number of assertions on what is always in a start message? + // return the session end message for further assertions. return endSession } diff --git a/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/EmbraceInternalInterfaceTest.kt b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/EmbraceInternalInterfaceTest.kt new file mode 100644 index 000000000..a4785c040 --- /dev/null +++ b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/EmbraceInternalInterfaceTest.kt @@ -0,0 +1,260 @@ +package io.embrace.android.embracesdk.testcases + +import android.os.Build +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.embrace.android.embracesdk.EmbraceEvent +import io.embrace.android.embracesdk.IntegrationTestRule +import io.embrace.android.embracesdk.LogType +import io.embrace.android.embracesdk.assertions.assertLogMessageReceived +import io.embrace.android.embracesdk.getSentLogMessages +import io.embrace.android.embracesdk.network.EmbraceNetworkRequest +import io.embrace.android.embracesdk.network.http.HttpMethod +import io.embrace.android.embracesdk.recordSession +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import java.net.SocketException + +/** + * Validation of the internal API + */ +@Config(sdk = [Build.VERSION_CODES.TIRAMISU]) +@RunWith(AndroidJUnit4::class) +internal class EmbraceInternalInterfaceTest { + @Rule + @JvmField + val testRule: IntegrationTestRule = IntegrationTestRule( + harnessSupplier = { + IntegrationTestRule.newHarness(startImmediately = false) + } + ) + + @Test + fun `no NPEs when SDK not started`() { + assertFalse(testRule.embrace.isStarted) + with(testRule.embrace.internalInterface) { + logInfo("", null) + logWarning("", null, null) + logError("", null, null, false) + logHandledException(NullPointerException(), LogType.ERROR, null, null) + recordCompletedNetworkRequest( + url = "", + httpMethod = "GET", + startTime = 0L, + endTime = 1L, + bytesSent = 0L, + bytesReceived = 0L, + statusCode = 200, + traceId = null, + networkCaptureData = null + ) + + recordIncompleteNetworkRequest( + url = "", + httpMethod = "GET", + startTime = 0L, + endTime = 1L, + error = null, + traceId = null, + networkCaptureData = null + ) + + recordIncompleteNetworkRequest( + url = "", + httpMethod = "GET", + startTime = 0L, + endTime = 1L, + errorType = null, + errorMessage = null, + traceId = null, + networkCaptureData = null + ) + + recordAndDeduplicateNetworkRequest( + callId = "", + embraceNetworkRequest = EmbraceNetworkRequest.fromCompletedRequest( + "", + HttpMethod.GET, + 0L, + 1L, + 0L, + 0L, + 200, + null + ) + ) + + logComposeTap(android.util.Pair.create(0.0f, 0.0f), "") + assertFalse(shouldCaptureNetworkBody("", "")) + setProcessStartedByNotification() + assertFalse(isNetworkSpanForwardingEnabled()) + getSdkCurrentTime() + } + } + + @Test + fun `internal logging methods work as expected`() { + with(testRule) { + embrace.start(harness.fakeCoreModule.context) + val expectedProperties = mapOf(Pair("key", "value")) + harness.recordSession { + embrace.internalInterface.logInfo("info", expectedProperties) + embrace.internalInterface.logWarning("warning", expectedProperties, null) + embrace.internalInterface.logError("error", expectedProperties, null, false) + embrace.internalInterface.logHandledException(NullPointerException(), LogType.ERROR, expectedProperties, null) + val logs = harness.getSentLogMessages(4) + + assertLogMessageReceived( + logs[0], + message = "info", + eventType = EmbraceEvent.Type.INFO_LOG, + properties = expectedProperties + ) + assertLogMessageReceived( + logs[1], + message = "warning", + eventType = EmbraceEvent.Type.WARNING_LOG, + properties = expectedProperties + ) + assertLogMessageReceived( + logs[2], + message = "error", + eventType = EmbraceEvent.Type.ERROR_LOG, + properties = expectedProperties + ) + assertLogMessageReceived( + logs[3], + message = "", + eventType = EmbraceEvent.Type.ERROR_LOG, + properties = expectedProperties + ) + } + } + } + + @Test + fun `network recording methods work as expected`() { + with(testRule) { + embrace.start(harness.fakeCoreModule.context) + val session = harness.recordSession { + harness.fakeClock.tick() + harness.fakeConfigService.updateListeners() + harness.fakeClock.tick() + embrace.internalInterface.recordCompletedNetworkRequest( + url = URL, + httpMethod = "GET", + startTime = START_TIME, + endTime = END_TIME, + bytesSent = 0L, + bytesReceived = 0L, + statusCode = 500, + traceId = null, + networkCaptureData = null + ) + + embrace.internalInterface.recordIncompleteNetworkRequest( + url = URL, + httpMethod = "GET", + startTime = START_TIME, + endTime = END_TIME, + error = NullPointerException(), + traceId = null, + networkCaptureData = null + ) + + embrace.internalInterface.recordIncompleteNetworkRequest( + url = URL, + httpMethod = "GET", + startTime = START_TIME, + endTime = END_TIME, + errorType = SocketException::class.java.canonicalName, + errorMessage = "", + traceId = null, + networkCaptureData = null + ) + + embrace.internalInterface.recordAndDeduplicateNetworkRequest( + callId = "", + embraceNetworkRequest = EmbraceNetworkRequest.fromCompletedRequest( + URL, + HttpMethod.POST, + START_TIME, + END_TIME, + 99L, + 301L, + 200, + null + ) + ) + } + + val requests = checkNotNull(session.performanceInfo?.networkRequests?.networkSessionV2?.requests) + assertEquals( + "Unexpected number of requests in sent session: ${requests.size}", + 4, + requests.size + ) + } + } + + @Test + fun `compose tap logging works as expected`() { + val expectedX = 10.0f + val expectedY = 99f + val expectedElementName = "button" + + with(testRule) { + embrace.start(harness.fakeCoreModule.context) + val session = harness.recordSession { + embrace.internalInterface.logComposeTap(android.util.Pair.create(expectedX, expectedY), expectedElementName) + } + + val tapBreadcrumb = checkNotNull(session.breadcrumbs?.tapBreadcrumbs?.last()) + assertEquals("10,99", tapBreadcrumb.location) + assertEquals(expectedElementName, tapBreadcrumb.tappedElementName) + } + } + + @Test + fun `access check methods work as expected`() { + with(testRule) { + embrace.start(harness.fakeCoreModule.context) + harness.recordSession { + assertTrue(embrace.internalInterface.shouldCaptureNetworkBody("capture.me", "GET")) + assertFalse(embrace.internalInterface.shouldCaptureNetworkBody("capture.me", "POST")) + assertFalse(embrace.internalInterface.shouldCaptureNetworkBody(URL, "GET")) + assertTrue(embrace.internalInterface.isNetworkSpanForwardingEnabled()) + } + } + } + + @Test + fun `set process as started by notification works as expected`() { + with(testRule) { + embrace.start(harness.fakeCoreModule.context) + embrace.internalInterface.setProcessStartedByNotification() + harness.recordSession(simulateAppStartup = true) { } + assertEquals(EmbraceEvent.Type.START, harness.fakeDeliveryModule.deliveryService.lastEventSentAsync?.event?.type) + } + } + + @Test + fun `test sdk time`() { + with(testRule) { + embrace.start(harness.fakeCoreModule.context) + assertEquals(harness.fakeClock.now(), embrace.internalInterface.getSdkCurrentTime()) + harness.fakeClock.tick() + assertEquals(harness.fakeClock.now(), embrace.internalInterface.getSdkCurrentTime()) + } + } + + companion object { + private const val URL = "https://embrace.io" + private const val START_TIME = 1692201601L + private const val END_TIME = 1692202600L + } +} \ No newline at end of file diff --git a/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/NetworkRequestApiTest.kt b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/NetworkRequestApiTest.kt index f30ff1833..e57bba65b 100644 --- a/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/NetworkRequestApiTest.kt +++ b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/NetworkRequestApiTest.kt @@ -1,275 +1,362 @@ package io.embrace.android.embracesdk.testcases -// -//import android.os.Build -//import androidx.test.ext.junit.runners.AndroidJUnit4 -//import io.embrace.android.embracesdk.IntegrationTestRule -//import io.embrace.android.embracesdk.network.EmbraceNetworkRequest -//import io.embrace.android.embracesdk.network.http.HttpMethod -//import io.embrace.android.embracesdk.network.http.NetworkCaptureData -//import io.embrace.android.embracesdk.payload.NetworkCallV2 -//import io.embrace.android.embracesdk.recordSession -//import org.junit.Assert.assertEquals -//import org.junit.Rule -//import org.junit.Test -//import org.junit.runner.RunWith -//import org.robolectric.annotation.Config -//import kotlin.math.max -// -//@RunWith(AndroidJUnit4::class) -//@Config(sdk = [Build.VERSION_CODES.TIRAMISU]) -//internal class NetworkRequestApiTest { -// @Rule -// @JvmField -// val testRule: IntegrationTestRule = IntegrationTestRule() -// -// @Test -// fun `record basic completed GET request`() { -// assertSingleNetworkRequestInSession( -// EmbraceNetworkRequest.fromCompletedRequest( -// URL, -// HttpMethod.GET, -// START_TIME, -// END_TIME, -// BYTES_SENT, -// BYTES_RECEIVED, -// 200 -// ) -// ) -// } -// -// @Test -// fun `record completed POST request with traceId`() { -// assertSingleNetworkRequestInSession( -// expectedRequest = EmbraceNetworkRequest.fromCompletedRequest( -// URL, -// HttpMethod.POST, -// START_TIME, -// END_TIME, -// BYTES_SENT, -// BYTES_RECEIVED, -// 200, -// TRACE_ID, -// ) -// ) -// } -// -// @Test -// fun `record completed request that failed with captured response`() { -// assertSingleNetworkRequestInSession( -// expectedRequest = EmbraceNetworkRequest.fromCompletedRequest( -// URL, -// HttpMethod.GET, -// START_TIME, -// END_TIME, -// BYTES_SENT, -// BYTES_RECEIVED, -// 500, -// TRACE_ID, -// NETWORK_CAPTURE_DATA -// ) -// ) -// } -// -// @Test -// fun `record completed request with traceparent`() { -// assertSingleNetworkRequestInSession( -// expectedRequest = EmbraceNetworkRequest.fromCompletedRequest( -// URL, -// HttpMethod.GET, -// START_TIME, -// END_TIME, -// BYTES_SENT, -// BYTES_RECEIVED, -// 200, -// TRACE_ID, -// TRACEPARENT, -// NETWORK_CAPTURE_DATA -// ) -// ) -// } -// -// @Test -// fun `record basic incomplete request`() { -// assertSingleNetworkRequestInSession( -// EmbraceNetworkRequest.fromIncompleteRequest( -// URL, -// HttpMethod.GET, -// START_TIME, -// END_TIME, -// NullPointerException::class.toString(), -// "Dang nothing there" -// ), -// completed = false -// ) -// } -// -// @Test -// fun `record incomplete POST request with trace ID`() { -// assertSingleNetworkRequestInSession( -// EmbraceNetworkRequest.fromIncompleteRequest( -// URL, -// HttpMethod.POST, -// START_TIME, -// END_TIME, -// NullPointerException::class.toString(), -// "Dang nothing there", -// TRACE_ID -// ), -// completed = false -// ) -// } -// -// @Test -// fun `record incomplete request with network capture`() { -// assertSingleNetworkRequestInSession( -// EmbraceNetworkRequest.fromIncompleteRequest( -// URL, -// HttpMethod.GET, -// START_TIME, -// END_TIME, -// NullPointerException::class.toString(), -// "Dang nothing there", -// TRACE_ID, -// NETWORK_CAPTURE_DATA -// ), -// completed = false -// ) -// } -// -// @Test -// fun `record incomplete request with traceparent`() { -// assertSingleNetworkRequestInSession( -// EmbraceNetworkRequest.fromIncompleteRequest( -// URL, -// HttpMethod.GET, -// START_TIME, -// END_TIME, -// NullPointerException::class.toString(), -// "Dang nothing there", -// TRACE_ID, -// TRACEPARENT, -// NETWORK_CAPTURE_DATA -// ), -// completed = false -// ) -// } -// -// @Test -// fun `disabled URLs not recorded`() { -// with(testRule) { -// harness.recordSession { -// harness.fakeConfigService.updateListeners() -// harness.fakeClock.tick(5) -// embrace.recordNetworkRequest( -// EmbraceNetworkRequest.fromCompletedRequest( -// DISABLED_URL, -// HttpMethod.GET, -// START_TIME, -// END_TIME, -// BYTES_SENT, -// BYTES_RECEIVED, -// 200 -// ) -// ) -// harness.fakeClock.tick(5) -// embrace.recordNetworkRequest( -// EmbraceNetworkRequest.fromIncompleteRequest( -// DISABLED_URL, -// HttpMethod.GET, -// START_TIME + 1, -// END_TIME, -// NullPointerException::class.toString(), -// "Dang nothing there" -// ) -// ) -// harness.fakeClock.tick(5) -// embrace.recordNetworkRequest( -// EmbraceNetworkRequest.fromCompletedRequest( -// URL, -// HttpMethod.GET, -// START_TIME + 2, -// END_TIME, -// BYTES_SENT, -// BYTES_RECEIVED, -// 200 -// ) -// ) -// } -// -// val networkCall = validateAndReturnExpectedNetworkCall(harness) -// assertEquals(URL, networkCall.url) -// } -// } -// -// private fun assertSingleNetworkRequestInSession(expectedRequest: EmbraceNetworkRequest, completed: Boolean = true) { -// with(testRule) { -// harness.recordSession { -// harness.fakeConfigService.updateListeners() -// harness.fakeClock.tick(5L) -// embrace.recordNetworkRequest(expectedRequest) -// } -// -// val networkCall = validateAndReturnExpectedNetworkCall(harness) -// with(networkCall) { -// assertEquals(expectedRequest.url, url) -// assertEquals(expectedRequest.httpMethod, httpMethod) -// assertEquals(expectedRequest.startTime, startTime) -// assertEquals(expectedRequest.endTime, endTime) -// assertEquals(max(expectedRequest.endTime - expectedRequest.startTime, 0L), duration) -// assertEquals(expectedRequest.traceId, traceId) -// assertEquals(expectedRequest.w3cTraceparent, w3cTraceparent) -// if (completed) { -// assertEquals(expectedRequest.responseCode, responseCode) -// assertEquals(expectedRequest.bytesSent, bytesSent) -// assertEquals(expectedRequest.bytesReceived, bytesReceived) -// assertEquals(null, errorType) -// assertEquals(null, errorMessage) -// } else { -// assertEquals(null, responseCode) -// assertEquals(0, bytesSent) -// assertEquals(0, bytesReceived) -// assertEquals(expectedRequest.errorType, errorType) -// assertEquals(expectedRequest.errorMessage, errorMessage) -// } -// } -// } -// } -// -// private fun validateAndReturnExpectedNetworkCall(harness: IntegrationTestRule.Harness): NetworkCallV2 { -// val lastSavedSessionRequestCount = -// harness.fakeDeliveryModule.deliveryService.lastSavedSession?.performanceInfo?.networkRequests?.networkSessionV2?.requests?.size -// ?: -1 -// val session = harness.fakeDeliveryModule.deliveryService.lastSentSessions[1].first -// val requests = checkNotNull(session.performanceInfo?.networkRequests?.networkSessionV2?.requests) -// val requestCount = requests.size -// val networkCall = requests.first() -// -// assertEquals( -// "Unexpected number of requests in sent session: $requestCount. Last saved session requests: $lastSavedSessionRequestCount", -// 1, -// requestCount -// ) -// -// return networkCall -// } -// -// companion object { -// private const val URL = "https://embrace.io" -// private const val DISABLED_URL = "https://dontlogmebro.pizza/yum" -// private const val START_TIME = 1692201601L -// private const val END_TIME = 1692202600L -// private const val BYTES_SENT = 100L -// private const val BYTES_RECEIVED = 500L -// private const val TRACE_ID = "rAnDoM-traceId" -// private const val TRACEPARENT = "00-c4ada96c31e1b6b9e351a1cffc99ae38-331f3a8acf49d295-01" -// -// private val NETWORK_CAPTURE_DATA = NetworkCaptureData( -// requestHeaders = mapOf(Pair("x-emb-test", "holla")), -// requestQueryParams = "trackMe=noooooo", -// capturedRequestBody = "haha".toByteArray(), -// responseHeaders = mapOf(Pair("x-emb-response-header", "alloh")), -// capturedResponseBody = "woohoo".toByteArray(), -// dataCaptureErrorMessage = null -// ) -// } -//} + +import android.os.Build +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.embrace.android.embracesdk.IntegrationTestRule +import io.embrace.android.embracesdk.network.EmbraceNetworkRequest +import io.embrace.android.embracesdk.network.http.HttpMethod +import io.embrace.android.embracesdk.network.http.NetworkCaptureData +import io.embrace.android.embracesdk.payload.NetworkCallV2 +import io.embrace.android.embracesdk.recordSession +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import java.util.UUID +import kotlin.math.max + +@RunWith(AndroidJUnit4::class) +@Config(sdk = [Build.VERSION_CODES.TIRAMISU]) +internal class NetworkRequestApiTest { + @Rule + @JvmField + val testRule: IntegrationTestRule = IntegrationTestRule() + + @Test + fun `record basic completed GET request`() { + assertSingleNetworkRequestInSession( + EmbraceNetworkRequest.fromCompletedRequest( + URL, + HttpMethod.GET, + START_TIME, + END_TIME, + BYTES_SENT, + BYTES_RECEIVED, + 200 + ) + ) + } + + @Test + fun `record completed POST request with traceId`() { + assertSingleNetworkRequestInSession( + expectedRequest = EmbraceNetworkRequest.fromCompletedRequest( + URL, + HttpMethod.POST, + START_TIME, + END_TIME, + BYTES_SENT, + BYTES_RECEIVED, + 200, + TRACE_ID, + ) + ) + } + + @Test + fun `record completed request that failed with captured response`() { + assertSingleNetworkRequestInSession( + expectedRequest = EmbraceNetworkRequest.fromCompletedRequest( + URL, + HttpMethod.GET, + START_TIME, + END_TIME, + BYTES_SENT, + BYTES_RECEIVED, + 500, + TRACE_ID, + NETWORK_CAPTURE_DATA + ) + ) + } + + @Test + fun `record completed request with traceparent`() { + assertSingleNetworkRequestInSession( + expectedRequest = EmbraceNetworkRequest.fromCompletedRequest( + URL, + HttpMethod.GET, + START_TIME, + END_TIME, + BYTES_SENT, + BYTES_RECEIVED, + 200, + TRACE_ID, + TRACEPARENT, + NETWORK_CAPTURE_DATA + ) + ) + } + + @Test + fun `record basic incomplete request`() { + assertSingleNetworkRequestInSession( + EmbraceNetworkRequest.fromIncompleteRequest( + URL, + HttpMethod.GET, + START_TIME, + END_TIME, + NullPointerException::class.toString(), + "Dang nothing there" + ), + completed = false + ) + } + + @Test + fun `record incomplete POST request with trace ID`() { + assertSingleNetworkRequestInSession( + EmbraceNetworkRequest.fromIncompleteRequest( + URL, + HttpMethod.POST, + START_TIME, + END_TIME, + NullPointerException::class.toString(), + "Dang nothing there", + TRACE_ID + ), + completed = false + ) + } + + @Test + fun `record incomplete request with network capture`() { + assertSingleNetworkRequestInSession( + EmbraceNetworkRequest.fromIncompleteRequest( + URL, + HttpMethod.GET, + START_TIME, + END_TIME, + NullPointerException::class.toString(), + "Dang nothing there", + TRACE_ID, + NETWORK_CAPTURE_DATA + ), + completed = false + ) + } + + @Test + fun `record incomplete request with traceparent`() { + assertSingleNetworkRequestInSession( + EmbraceNetworkRequest.fromIncompleteRequest( + URL, + HttpMethod.GET, + START_TIME, + END_TIME, + NullPointerException::class.toString(), + "Dang nothing there", + TRACE_ID, + TRACEPARENT, + NETWORK_CAPTURE_DATA + ), + completed = false + ) + } + + @Test + fun `disabled URLs not recorded`() { + with(testRule) { + harness.recordSession { + harness.fakeConfigService.updateListeners() + harness.fakeClock.tick(5) + embrace.recordNetworkRequest( + EmbraceNetworkRequest.fromCompletedRequest( + DISABLED_URL, + HttpMethod.GET, + START_TIME, + END_TIME, + BYTES_SENT, + BYTES_RECEIVED, + 200 + ) + ) + harness.fakeClock.tick(5) + embrace.recordNetworkRequest( + EmbraceNetworkRequest.fromIncompleteRequest( + DISABLED_URL, + HttpMethod.GET, + START_TIME + 1, + END_TIME, + NullPointerException::class.toString(), + "Dang nothing there" + ) + ) + harness.fakeClock.tick(5) + embrace.recordNetworkRequest( + EmbraceNetworkRequest.fromCompletedRequest( + URL, + HttpMethod.GET, + START_TIME + 2, + END_TIME, + BYTES_SENT, + BYTES_RECEIVED, + 200 + ) + ) + } + + val networkCall = validateAndReturnExpectedNetworkCall() + assertEquals(URL, networkCall.url) + } + } + + @Test + fun `ensure calls with same callId but different start times are deduped`() { + val expectedStartTime = START_TIME + 1 + with(testRule) { + harness.recordSession { + harness.fakeConfigService.updateListeners() + harness.fakeClock.tick(5) + + val callId = UUID.randomUUID().toString() + embrace.internalInterface.recordAndDeduplicateNetworkRequest( + callId, + EmbraceNetworkRequest.fromCompletedRequest( + "$URL/bad", + HttpMethod.GET, + START_TIME, + END_TIME, + BYTES_SENT, + BYTES_RECEIVED, + 200 + ) + ) + embrace.internalInterface.recordAndDeduplicateNetworkRequest( + callId, + EmbraceNetworkRequest.fromCompletedRequest( + URL, + HttpMethod.GET, + expectedStartTime, + expectedStartTime + 1, + BYTES_SENT, + BYTES_RECEIVED, + 200 + ) + ) + } + + val networkCall = validateAndReturnExpectedNetworkCall() + assertEquals(URL, networkCall.url) + assertEquals(expectedStartTime, networkCall.startTime) + } + } + + /** + * This reproduces the bug that will be fixed. Uncomment when ready. + */ + @Test + fun `ensure network calls with the same start time are recorded properly`() { + with(testRule) { + harness.recordSession { + harness.fakeConfigService.updateListeners() + harness.fakeClock.tick(5) + + val request = EmbraceNetworkRequest.fromCompletedRequest( + URL, + HttpMethod.GET, + START_TIME, + END_TIME, + BYTES_SENT, + BYTES_RECEIVED, + 200 + ) + + embrace.recordNetworkRequest(request) + embrace.recordNetworkRequest(request) + } + + val session = testRule.harness.fakeDeliveryModule.deliveryService.lastSentSessions[1].first + val requests = checkNotNull(session.performanceInfo?.networkRequests?.networkSessionV2?.requests) + assertEquals( + "Unexpected number of requests in sent session: ${requests.size}", + 2, + requests.size + ) + } + } + + private fun assertSingleNetworkRequestInSession( + expectedRequest: EmbraceNetworkRequest, + completed: Boolean = true + ) { + with(testRule) { + harness.recordSession { + harness.fakeClock.tick(2L) + harness.fakeConfigService.updateListeners() + harness.fakeClock.tick(5L) + embrace.recordNetworkRequest(expectedRequest) + } + + val networkCall = validateAndReturnExpectedNetworkCall() + with(networkCall) { + assertEquals(expectedRequest.url, url) + assertEquals(expectedRequest.httpMethod, httpMethod) + assertEquals(expectedRequest.startTime, startTime) + assertEquals(expectedRequest.endTime, endTime) + assertEquals(max(expectedRequest.endTime - expectedRequest.startTime, 0L), duration) + assertEquals(expectedRequest.traceId, traceId) + assertEquals(expectedRequest.w3cTraceparent, w3cTraceparent) + if (completed) { + assertEquals(expectedRequest.responseCode, responseCode) + assertEquals(expectedRequest.bytesSent, bytesSent) + assertEquals(expectedRequest.bytesReceived, bytesReceived) + assertEquals(null, errorType) + assertEquals(null, errorMessage) + } else { + assertEquals(null, responseCode) + assertEquals(0, bytesSent) + assertEquals(0, bytesReceived) + assertEquals(expectedRequest.errorType, errorType) + assertEquals(expectedRequest.errorMessage, errorMessage) + } + } + } + } + + private fun validateAndReturnExpectedNetworkCall(): NetworkCallV2 { + val session = testRule.harness.fakeDeliveryModule.deliveryService.lastSentSessions[1].first + + // Look for a specific error where the fetch from the cache returns a stale value + session.session.exceptionError?.exceptionErrors?.forEach { errorInfo -> + errorInfo.exceptions?.forEach { exception -> + val msg = exception.message + assertTrue( + "Wrong network call count returned: $msg", + msg?.startsWith("Cached network call count") == false + ) + } + } + + val requests = checkNotNull(session.performanceInfo?.networkRequests?.networkSessionV2?.requests) + assertEquals( + "Unexpected number of requests in sent session: ${requests.size}", + 1, + requests.size + ) + + return requests.first() + } + + companion object { + private const val URL = "https://embrace.io" + private const val DISABLED_URL = "https://dontlogmebro.pizza/yum" + private const val START_TIME = 1692201601L + private const val END_TIME = 1692202600L + private const val BYTES_SENT = 100L + private const val BYTES_RECEIVED = 500L + private const val TRACE_ID = "rAnDoM-traceId" + private const val TRACEPARENT = "00-c4ada96c31e1b6b9e351a1cffc99ae38-331f3a8acf49d295-01" + + private val NETWORK_CAPTURE_DATA = NetworkCaptureData( + requestHeaders = mapOf(Pair("x-emb-test", "holla")), + requestQueryParams = "trackMe=noooooo", + capturedRequestBody = "haha".toByteArray(), + responseHeaders = mapOf(Pair("x-emb-response-header", "alloh")), + capturedResponseBody = "woohoo".toByteArray(), + dataCaptureErrorMessage = null + ) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/Embrace.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/Embrace.java index 0cb59d5f5..5f30fab7e 100644 --- a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/Embrace.java +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/Embrace.java @@ -2,8 +2,8 @@ import static io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.logger; +import android.annotation.SuppressLint; import android.content.Context; -import android.util.Pair; import android.webkit.ConsoleMessage; import androidx.annotation.NonNull; @@ -13,6 +13,7 @@ import java.util.Map; import io.embrace.android.embracesdk.config.ConfigService; +import io.embrace.android.embracesdk.internal.EmbraceInternalInterface; import io.embrace.android.embracesdk.logging.InternalEmbraceLogger; import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger; import io.embrace.android.embracesdk.network.EmbraceNetworkRequest; @@ -27,13 +28,16 @@ *

* Contains a singleton instance of itself, and is used for initializing the SDK. */ +@SuppressLint("EmbracePublicApiPackageRule") @SuppressWarnings("unused") public final class Embrace implements EmbraceAndroidApi { /** * Singleton instance of the Embrace SDK. */ + @NonNull private static final Embrace embrace = new Embrace(); + private static EmbraceImpl impl = new EmbraceImpl(); @NonNull @@ -41,9 +45,6 @@ public final class Embrace implements EmbraceAndroidApi { static final String NULL_PARAMETER_ERROR_MESSAGE_TEMPLATE = " cannot be invoked because it contains null parameters"; - Embrace() { - } - /** * Gets the singleton instance of the Embrace SDK. * @@ -67,6 +68,9 @@ static void setImpl(@Nullable EmbraceImpl instance) { impl = instance; } + Embrace() { + } + @Override public void start(@NonNull Context context) { if (verifyNonNullParameters("start", context)) { @@ -288,16 +292,6 @@ public void logError(@NonNull String message) { } } - /** - * Logs a React Native Redux Action. - */ - public void logRnAction(@NonNull String name, long startTime, long endTime, - @NonNull Map properties, int bytesSent, @NonNull String output) { - if (verifyNonNullParameters("logRnAction", name, properties, output)) { - impl.logRnAction(name, startTime, endTime, properties, bytesSent, output); - } - } - @Override public void addBreadcrumb(@NonNull String message) { if (verifyNonNullParameters("addBreadcrumb", message)) { @@ -387,22 +381,6 @@ public void logCustomStacktrace(@NonNull StackTraceElement[] stacktraceElements, } } - /** - * Logs an internal error to the Embrace SDK - this is not intended for public use. - */ - @InternalApi - public void logInternalError(@Nullable String message, @Nullable String details) { - impl.logInternalError(message, details); - } - - /** - * Logs an internal error to the Embrace SDK - this is not intended for public use. - */ - @InternalApi - public void logInternalError(@NonNull Throwable error) { - impl.logInternalError(error); - } - @Override public synchronized void endSession() { endSession(false); @@ -435,30 +413,6 @@ public boolean endView(@NonNull String name) { return false; } - /** - * Logs the fact that a particular view was entered. - *

- * If the previously logged view has the same name, a duplicate view breadcrumb will not be - * logged. - * - * @param screen the name of the view to log - */ - @InternalApi - public void logRnView(@NonNull String screen) { - impl.logRnView(screen); - } - - @Nullable - @InternalApi - public ConfigService getConfigService() { - return impl.getConfigService(); - } - - @InternalApi - void installUnityThreadSampler() { - getImpl().installUnityThreadSampler(); - } - @Override public boolean isTracingAvailable() { return impl.tracer.getValue().isTracingAvailable(); @@ -560,26 +514,86 @@ public boolean recordCompletedSpan(@NonNull String name, long startTimeNanos, lo return false; } - /** - * The AppFramework that is in use. - */ - public enum AppFramework { - NATIVE(1), - REACT_NATIVE(2), - UNITY(3), - FLUTTER(4); - - private final int value; + @Override + public void logPushNotification(@Nullable String title, + @Nullable String body, + @Nullable String topic, + @Nullable String id, + @Nullable Integer notificationPriority, + @NonNull Integer messageDeliveredPriority, + @NonNull Boolean isNotification, + @NonNull Boolean hasData) { + if (verifyNonNullParameters("logPushNotification", messageDeliveredPriority, isNotification, hasData)) { + impl.logPushNotification( + title, + body, + topic, + id, + notificationPriority, + messageDeliveredPriority, + PushNotificationBreadcrumb.NotificationType.Builder.notificationTypeFor(hasData, isNotification) + ); + } + } - AppFramework(int value) { - this.value = value; + @Override + public void trackWebViewPerformance(@NonNull String tag, @NonNull ConsoleMessage consoleMessage) { + if (verifyNonNullParameters("trackWebViewPerformance", tag, consoleMessage)) { + if (consoleMessage.message() != null) { + trackWebViewPerformance(tag, consoleMessage.message()); + } else { + logger.logDebug("Empty WebView console message."); + } } + } - public int getValue() { - return value; + @Override + public void trackWebViewPerformance(@NonNull String tag, @NonNull String message) { + if (verifyNonNullParameters("trackWebViewPerformance", tag, message)) { + impl.trackWebViewPerformance(tag, message); } } + @Nullable + @Override + public String getCurrentSessionId() { + return impl.getCurrentSessionId(); + } + + @NonNull + @Override + public LastRunEndState getLastRunEndState() { + return impl.getLastRunEndState(); + } + + @NonNull + @InternalApi + public EmbraceInternalInterface getInternalInterface() { + return impl.getEmbraceInternalInterface(); + } + + /** + * Logs an internal error to the Embrace SDK - this is not intended for public use. + */ + @InternalApi + public void logInternalError(@Nullable String message, @Nullable String details) { + impl.logInternalError(message, details); + } + + /** + * Logs an internal error to the Embrace SDK - this is not intended for public use. + */ + @InternalApi + public void logInternalError(@NonNull Throwable error) { + impl.logInternalError(error); + } + + @Nullable + @InternalApi + public ConfigService getConfigService() { + return impl.getConfigService(); + } + /** * Gets the {@link ReactNativeInternalInterface} that should be used as the sole source of * communication with the Android SDK for React Native. @@ -590,6 +604,30 @@ public ReactNativeInternalInterface getReactNativeInternalInterface() { return impl.getReactNativeInternalInterface(); } + /** + * Logs a React Native Redux Action - this is not intended for public use. + */ + @InternalApi + public void logRnAction(@NonNull String name, long startTime, long endTime, + @NonNull Map properties, int bytesSent, @NonNull String output) { + if (verifyNonNullParameters("logRnAction", name, properties, output)) { + impl.logRnAction(name, startTime, endTime, properties, bytesSent, output); + } + } + + /** + * Logs the fact that a particular view was entered. + *

+ * If the previously logged view has the same name, a duplicate view breadcrumb will not be + * logged. + * + * @param screen the name of the view to log + */ + @InternalApi + public void logRnView(@NonNull String screen) { + impl.logRnView(screen); + } + /** * Gets the {@link UnityInternalInterface} that should be used as the sole source of * communication with the Android SDK for Unity. @@ -600,6 +638,11 @@ public UnityInternalInterface getUnityInternalInterface() { return impl.getUnityInternalInterface(); } + @InternalApi + void installUnityThreadSampler() { + getImpl().installUnityThreadSampler(); + } + /** * Gets the {@link FlutterInternalInterface} that should be used as the sole source of * communication with the Android SDK for Flutter. @@ -610,6 +653,22 @@ public FlutterInternalInterface getFlutterInternalInterface() { return impl.getFlutterInternalInterface(); } + /** + * Sets the Embrace Flutter SDK version - this is not intended for public use. + */ + @InternalApi + public void setEmbraceFlutterSdkVersion(@Nullable String version) { + impl.setEmbraceFlutterSdkVersion(version); + } + + /** + * Sets the Dart version - this is not intended for public use. + */ + @InternalApi + public void setDartVersion(@Nullable String version) { + impl.setDartVersion(version); + } + /** * Logs a handled Dart error to the Embrace SDK - this is not intended for public use. */ @@ -643,103 +702,41 @@ public void sampleCurrentThreadDuringAnrs() { impl.sampleCurrentThreadDuringAnrs(); } - /** - * Logs taps from Compose views - * @param point Position of the captured clicked - * @param elementName Name of the clicked element - */ - @InternalApi - public void logComposeTap(@NonNull Pair point, @NonNull String elementName) { - impl.getEmbraceInternalInterface().logComposeTap(point, elementName); - } - - /** - * Allows Unity customers to verify their integration. - */ - void verifyUnityIntegration() { - EmbraceSamples.verifyIntegration(); - } - - @Override - public void logPushNotification(@Nullable String title, - @Nullable String body, - @Nullable String topic, - @Nullable String id, - @Nullable Integer notificationPriority, - @NonNull Integer messageDeliveredPriority, - @NonNull Boolean isNotification, - @NonNull Boolean hasData) { - if (verifyNonNullParameters("logPushNotification", messageDeliveredPriority, isNotification, hasData)) { - impl.logPushNotification( - title, - body, - topic, - id, - notificationPriority, - messageDeliveredPriority, - PushNotificationBreadcrumb.NotificationType.Builder.notificationTypeFor(hasData, isNotification) - ); + private boolean verifyNonNullParameters(@NonNull String functionName, @NonNull Object... params) { + for (Object param : params) { + if (param == null) { + final String errorMessage = functionName + NULL_PARAMETER_ERROR_MESSAGE_TEMPLATE; + if (isStarted()) { + internalEmbraceLogger.logError(errorMessage, new IllegalArgumentException(errorMessage), true); + } else { + internalEmbraceLogger.logSDKNotInitialized(errorMessage); + } + return false; + } } + return true; } /** - * Determine if a network call should be captured based on the network capture rules - * - * @param url the url of the network call - * @param method the method of the network call - * @return the network capture rule to apply or null + * The AppFramework that is in use. */ - @InternalApi - public boolean shouldCaptureNetworkBody(@NonNull String url, @NonNull String method) { - if (isStarted()) { - return impl.shouldCaptureNetworkCall(url, method); - } else { - internalEmbraceLogger.logSDKNotInitialized("Embrace SDK is not initialized yet, cannot check for capture rules."); - return false; - } - } + public enum AppFramework { + NATIVE(1), + REACT_NATIVE(2), + UNITY(3), + FLUTTER(4); - @InternalApi - public void setProcessStartedByNotification() { - impl.setProcessStartedByNotification(); - } + private final int value; - @Override - public void trackWebViewPerformance(@NonNull String tag, @NonNull ConsoleMessage consoleMessage) { - if (verifyNonNullParameters("trackWebViewPerformance", tag, consoleMessage)) { - if (consoleMessage.message() != null) { - trackWebViewPerformance(tag, consoleMessage.message()); - } else { - logger.logDebug("Empty WebView console message."); - } + AppFramework(int value) { + this.value = value; } - } - @Override - public void trackWebViewPerformance(@NonNull String tag, @NonNull String message) { - if (verifyNonNullParameters("trackWebViewPerformance", tag, message)) { - impl.trackWebViewPerformance(tag, message); + public int getValue() { + return value; } } - /** - * Get the ID for the current session. - * Returns null if a session has not been started yet or the SDK hasn't been initialized. - * - * @return The ID for the current Session, if available. - */ - @Nullable - @Override - public String getCurrentSessionId() { - return impl.getCurrentSessionId(); - } - - @NonNull - @Override - public LastRunEndState getLastRunEndState() { - return impl.getLastRunEndState(); - } - /** * Enum representing the end state of the last run of the application. */ @@ -769,19 +766,4 @@ public int getValue() { return value; } } - - private boolean verifyNonNullParameters(@NonNull String functionName, @NonNull Object... params) { - for (Object param : params) { - if (param == null) { - final String errorMessage = functionName + NULL_PARAMETER_ERROR_MESSAGE_TEMPLATE; - if (isStarted()) { - internalEmbraceLogger.logError(errorMessage, new IllegalArgumentException(errorMessage), true); - } else { - internalEmbraceLogger.logSDKNotInitialized(errorMessage); - } - return false; - } - } - return true; - } } diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceImpl.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceImpl.java index 7bf68d2c9..d664ba6ea 100644 --- a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceImpl.java +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceImpl.java @@ -11,6 +11,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Pattern; @@ -53,12 +54,16 @@ import io.embrace.android.embracesdk.injection.InitModuleImpl; import io.embrace.android.embracesdk.injection.SdkObservabilityModule; import io.embrace.android.embracesdk.injection.SdkObservabilityModuleImpl; +import io.embrace.android.embracesdk.injection.SessionModule; +import io.embrace.android.embracesdk.injection.SessionModuleImpl; import io.embrace.android.embracesdk.injection.SystemServiceModule; import io.embrace.android.embracesdk.injection.SystemServiceModuleImpl; import io.embrace.android.embracesdk.internal.ApkToolsConfig; import io.embrace.android.embracesdk.internal.BuildInfo; import io.embrace.android.embracesdk.internal.DeviceArchitecture; import io.embrace.android.embracesdk.internal.DeviceArchitectureImpl; +import io.embrace.android.embracesdk.internal.EmbraceInternalInterface; +import io.embrace.android.embracesdk.internal.EmbraceInternalInterfaceKt; import io.embrace.android.embracesdk.internal.MessageType; import io.embrace.android.embracesdk.internal.TraceparentGenerator; import io.embrace.android.embracesdk.internal.crash.LastRunCrashVerifier; @@ -87,8 +92,6 @@ import io.embrace.android.embracesdk.session.EmbraceActivityService; import io.embrace.android.embracesdk.session.EmbraceSessionProperties; import io.embrace.android.embracesdk.session.EmbraceSessionService; -import io.embrace.android.embracesdk.injection.SessionModule; -import io.embrace.android.embracesdk.injection.SessionModuleImpl; import io.embrace.android.embracesdk.session.SessionService; import io.embrace.android.embracesdk.utils.PropertyUtils; import io.embrace.android.embracesdk.worker.ExecutorName; @@ -609,6 +612,7 @@ private void startImpl(@NonNull Context context, // initialize internal interfaces InternalInterfaceModuleImpl internalInterfaceModule = new InternalInterfaceModuleImpl( + initModule, coreModule, androidServicesModule, essentialServiceModule, @@ -1010,9 +1014,9 @@ public void clearUsername() { *

* The length of time a moment takes to execute is recorded. * - * @param name a name identifying the moment - * @param identifier an identifier distinguishing between multiple moments with the same name - * @param properties custom key-value pairs to provide with the moment + * @param name a name identifying the moment + * @param identifier an identifier distinguishing between multiple moments with the same name + * @param properties custom key-value pairs to provide with the moment */ public void startMoment(@NonNull String name, @Nullable String identifier, @@ -1071,14 +1075,19 @@ public String generateW3cTraceparent() { } public void recordNetworkRequest(@NonNull EmbraceNetworkRequest request) { - internalEmbraceLogger.logDeveloper("Embrace", "recordNetworkRequest()"); + if (isStarted() && embraceInternalInterface != null) { + embraceInternalInterface.recordAndDeduplicateNetworkRequest(UUID.randomUUID().toString(), request); + } + } + public void recordAndDeduplicateNetworkRequest(@NonNull String callId, @NonNull EmbraceNetworkRequest request) { if (request == null) { internalEmbraceLogger.logDeveloper("Embrace", "Request is null"); return; } logNetworkRequestImpl( + callId, request.getNetworkCaptureData(), request.getUrl(), request.getHttpMethod(), @@ -1094,7 +1103,8 @@ public void recordNetworkRequest(@NonNull EmbraceNetworkRequest request) { ); } - private void logNetworkRequestImpl(@Nullable NetworkCaptureData networkCaptureData, + private void logNetworkRequestImpl(@NonNull String callId, + @Nullable NetworkCaptureData networkCaptureData, String url, String httpMethod, Long startTime, @@ -1117,6 +1127,7 @@ private void logNetworkRequestImpl(@Nullable NetworkCaptureData networkCaptureDa !errorType.isEmpty() && !errorMessage.isEmpty()) { networkLoggingService.logNetworkError( + callId, url, httpMethod, startTime, @@ -1128,6 +1139,7 @@ private void logNetworkRequestImpl(@Nullable NetworkCaptureData networkCaptureDa networkCaptureData); } else { networkLoggingService.logNetworkCall( + callId, url, httpMethod, responseCode != null ? responseCode : 0, @@ -1558,7 +1570,12 @@ private Map normalizeProperties(@Nullable Map pr */ @NonNull EmbraceInternalInterface getEmbraceInternalInterface() { - return embraceInternalInterface; + if (isStarted() && embraceInternalInterface != null) { + return embraceInternalInterface; + } else { + return EmbraceInternalInterfaceKt.getDefaultImpl(); + } + } /** @@ -1596,6 +1613,26 @@ public void installUnityThreadSampler() { } } + /** + * Sets the Embrace Flutter SDK version - this is not intended for public use. + */ + @InternalApi + public void setEmbraceFlutterSdkVersion(@Nullable String version) { + if (flutterInternalInterface != null) { + flutterInternalInterface.setEmbraceFlutterSdkVersion(version); + } + } + + /** + * Sets the Dart version - this is not intended for public use. + */ + @InternalApi + public void setDartVersion(@Nullable String version) { + if (flutterInternalInterface != null) { + flutterInternalInterface.setDartVersion(version); + } + } + /** * Saves captured push notification information into session payload * @@ -1633,8 +1670,13 @@ private void onActivityReported() { } } - public boolean shouldCaptureNetworkCall(String url, String method) { - return !networkCaptureService.getNetworkCaptureRules(url, method).isEmpty(); + public boolean shouldCaptureNetworkCall(@NonNull String url, @NonNull String method) { + if (isStarted() && networkCaptureService != null) { + return !networkCaptureService.getNetworkCaptureRules(url, method).isEmpty(); + } else { + internalEmbraceLogger.logSDKNotInitialized("Embrace SDK is not initialized yet, cannot check for capture rules."); + return false; + } } public void setProcessStartedByNotification() { diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceInternalInterface.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceInternalInterface.java deleted file mode 100644 index 6349808b6..000000000 --- a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceInternalInterface.java +++ /dev/null @@ -1,215 +0,0 @@ -package io.embrace.android.embracesdk; - -import android.util.Pair; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.Map; - -import io.embrace.android.embracesdk.network.EmbraceNetworkRequest; -import io.embrace.android.embracesdk.network.http.HttpMethod; -import io.embrace.android.embracesdk.network.http.NetworkCaptureData; - -/** - * Provides an internal interface to Embrace that is intended for use by hosted SDKs as their - * sole source of communication with the Android SDK. - */ -interface EmbraceInternalInterface { - - /** - * {@see Embrace#logInfo} - */ - void logInfo(@NonNull String message, - @Nullable Map properties); - - /** - * {@see Embrace#logWarning} - */ - void logWarning(@NonNull String message, - @Nullable Map properties, - @Nullable String stacktrace); - - /** - * {@see Embrace#logError} - */ - void logError(@NonNull String message, - @Nullable Map properties, - @Nullable String stacktrace, - boolean isException); - - /** - * {@see Embrace#logHandledException} - */ - void logHandledException(@NonNull Throwable throwable, - @NonNull LogType type, - @Nullable Map properties, - @Nullable StackTraceElement[] customStackTrace); - - /** - * {@see Embrace#logBreadcrumb} - */ - void addBreadcrumb(@NonNull String message); - - /** - * {@see Embrace#getDeviceId} - */ - @NonNull - String getDeviceId(); - - /** - * {@see Embrace#setUsername} - */ - void setUsername(@Nullable String username); - - /** - * {@see Embrace#clearUsername} - */ - void clearUsername(); - - /** - * {@see Embrace#setUserIdentifier} - */ - void setUserIdentifier(@Nullable String userId); - - /** - * {@see Embrace#clearUserIdentifier} - */ - void clearUserIdentifier(); - - /** - * {@see Embrace#setUserEmail} - */ - void setUserEmail(@Nullable String email); - - /** - * {@see Embrace#clearUserEmail} - */ - void clearUserEmail(); - - /** - * {@see Embrace#setUserAsPayer} - */ - void setUserAsPayer(); - - /** - * {@see Embrace#clearUserAsPayer} - */ - void clearUserAsPayer(); - - /** - * {@see Embrace#addUserPersona} - */ - void addUserPersona(@NonNull String persona); - - /** - * {@see Embrace#clearUserPersona} - */ - void clearUserPersona(@NonNull String persona); - - /** - * {@see Embrace#clearAllUserPersonas} - */ - void clearAllUserPersonas(); - - /** - * {@see Embrace#addSessionProperty} - */ - boolean addSessionProperty(@NonNull String key, - @NonNull String value, - boolean permanent); - - /** - * {@see Embrace#removeSessionProperty} - */ - boolean removeSessionProperty(@NonNull String key); - - /** - * {@see Embrace#getSessionProperties} - */ - @Nullable - Map getSessionProperties(); - - /** - * {@see Embrace#startEvent} - */ - void startMoment(@NonNull String name, - @Nullable String identifier, - @Nullable Map properties); - - /** - * {@see Embrace#endMoment} - */ - void endMoment(@NonNull String name, - @Nullable String identifier, - @Nullable Map properties); - - /** - * {@see Embrace#startFragment} - */ - boolean startView(@NonNull String name); - - /** - * {@see Embrace#endFragment} - */ - boolean endView(@NonNull String name); - - /** - * {@see Embrace#endAppStartup} - */ - void endAppStartup(@NonNull Map properties); - - /** - * {@see Embrace#logInternalError} - */ - void logInternalError(@Nullable String message, @Nullable String details); - - /** - * {@see Embrace#endSession} - */ - void endSession(boolean clearUserInfo); - - /** - * See {@link Embrace#recordNetworkRequest(EmbraceNetworkRequest)} - */ - void recordCompletedNetworkRequest(@NonNull String url, - @NonNull String httpMethod, - long startTime, - long endTime, - long bytesSent, - long bytesReceived, - int statusCode, - @Nullable String traceId, - @Nullable NetworkCaptureData networkCaptureData); - - /** - * See {@link Embrace#recordNetworkRequest(EmbraceNetworkRequest)} - */ - void recordIncompleteNetworkRequest(@NonNull String url, - @NonNull String httpMethod, - long startTime, - long endTime, - @Nullable Throwable error, - @Nullable String traceId, - @Nullable NetworkCaptureData networkCaptureData); - - /** - * See {@link Embrace#recordNetworkRequest(EmbraceNetworkRequest)} - */ - void recordIncompleteNetworkRequest(@NonNull String url, - @NonNull String httpMethod, - long startTime, - long endTime, - @Nullable String errorType, - @Nullable String errorMessage, - @Nullable String traceId, - @Nullable NetworkCaptureData networkCaptureData); - - /** - * Logs a tap on a Compose screen element. - * - * @param point the coordinates of the screen tap - * @param elementName the name of the element which was tapped - */ - void logComposeTap(@NonNull Pair point, @NonNull String elementName); -} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceInternalInterfaceImpl.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceInternalInterfaceImpl.kt index e988cb435..df71f0f4d 100644 --- a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceInternalInterfaceImpl.kt +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/EmbraceInternalInterfaceImpl.kt @@ -1,17 +1,20 @@ package io.embrace.android.embracesdk import android.util.Pair +import io.embrace.android.embracesdk.injection.InitModule +import io.embrace.android.embracesdk.internal.EmbraceInternalInterface import io.embrace.android.embracesdk.network.EmbraceNetworkRequest import io.embrace.android.embracesdk.network.http.HttpMethod import io.embrace.android.embracesdk.network.http.NetworkCaptureData import io.embrace.android.embracesdk.payload.TapBreadcrumb internal class EmbraceInternalInterfaceImpl( - private val embrace: EmbraceImpl + private val embraceImpl: EmbraceImpl, + private val initModule: InitModule ) : EmbraceInternalInterface { override fun logInfo(message: String, properties: Map?) { - embrace.logMessage( + embraceImpl.logMessage( EmbraceEvent.Type.INFO_LOG, message, properties, @@ -28,7 +31,7 @@ internal class EmbraceInternalInterfaceImpl( properties: Map?, stacktrace: String? ) { - embrace.logMessage( + embraceImpl.logMessage( EmbraceEvent.Type.WARNING_LOG, message, properties, @@ -46,7 +49,7 @@ internal class EmbraceInternalInterfaceImpl( stacktrace: String?, isException: Boolean, ) { - embrace.logMessage( + embraceImpl.logMessage( EmbraceEvent.Type.ERROR_LOG, message, properties, @@ -62,9 +65,9 @@ internal class EmbraceInternalInterfaceImpl( throwable: Throwable, type: LogType, properties: Map?, - customStackTrace: Array? + customStackTrace: Array? ) { - embrace.logMessage( + embraceImpl.logMessage( type.toEventType(), throwable.message ?: "", properties, @@ -76,104 +79,8 @@ internal class EmbraceInternalInterfaceImpl( ) } - override fun addBreadcrumb(message: String) { - embrace.addBreadcrumb(message) - } - - override fun getDeviceId(): String { - return embrace.deviceId - } - - override fun setUserIdentifier(userId: String?) { - embrace.setUserIdentifier(userId) - } - - override fun clearUserIdentifier() { - embrace.clearUserIdentifier() - } - - override fun setUsername(username: String?) { - embrace.setUsername(username) - } - - override fun clearUsername() { - embrace.clearUsername() - } - - override fun setUserEmail(email: String?) { - embrace.setUserEmail(email) - } - - override fun clearUserEmail() { - embrace.clearUserEmail() - } - - override fun setUserAsPayer() { - embrace.setUserAsPayer() - } - - override fun clearUserAsPayer() { - embrace.clearUserAsPayer() - } - - override fun addUserPersona(persona: String) { - embrace.addUserPersona(persona) - } - - override fun clearUserPersona(persona: String) { - embrace.clearUserPersona(persona) - } - - override fun clearAllUserPersonas() { - embrace.clearAllUserPersonas() - } - - override fun addSessionProperty(key: String, value: String, permanent: Boolean): Boolean { - return embrace.addSessionProperty(key, value, permanent) - } - - override fun removeSessionProperty(key: String): Boolean { - return embrace.removeSessionProperty(key) - } - - override fun getSessionProperties(): Map? { - return embrace.sessionProperties - } - - override fun startMoment( - name: String, - identifier: String?, - properties: Map? - ) { - embrace.startMoment(name, identifier, properties) - } - - override fun endMoment(name: String, identifier: String?, properties: Map?) { - embrace.endMoment(name, identifier, properties) - } - - override fun startView(name: String): Boolean { - return embrace.startView(name) - } - - override fun endView(name: String): Boolean { - return embrace.endView(name) - } - - override fun endAppStartup(properties: Map) { - embrace.endAppStartup(properties) - } - - override fun logInternalError(message: String?, details: String?) { - embrace.logInternalError(message, details) - } - - override fun endSession(clearUserInfo: Boolean) { - embrace.endSession(clearUserInfo) - } - override fun logComposeTap(point: Pair, elementName: String) { - embrace.logTap(point, elementName, TapBreadcrumb.TapBreadcrumbType.TAP) + embraceImpl.logTap(point, elementName, TapBreadcrumb.TapBreadcrumbType.TAP) } override fun recordCompletedNetworkRequest( @@ -187,7 +94,7 @@ internal class EmbraceInternalInterfaceImpl( traceId: String?, networkCaptureData: NetworkCaptureData? ) { - embrace.recordNetworkRequest( + embraceImpl.recordNetworkRequest( EmbraceNetworkRequest.fromCompletedRequest( url, HttpMethod.fromString(httpMethod), @@ -212,7 +119,7 @@ internal class EmbraceInternalInterfaceImpl( traceId: String?, networkCaptureData: NetworkCaptureData? ) { - embrace.recordNetworkRequest( + embraceImpl.recordNetworkRequest( EmbraceNetworkRequest.fromIncompleteRequest( url, HttpMethod.fromString(httpMethod), @@ -237,7 +144,7 @@ internal class EmbraceInternalInterfaceImpl( traceId: String?, networkCaptureData: NetworkCaptureData? ) { - embrace.recordNetworkRequest( + embraceImpl.recordNetworkRequest( EmbraceNetworkRequest.fromIncompleteRequest( url, HttpMethod.fromString(httpMethod), @@ -251,4 +158,23 @@ internal class EmbraceInternalInterfaceImpl( ) ) } + + override fun recordAndDeduplicateNetworkRequest( + callId: String, + embraceNetworkRequest: EmbraceNetworkRequest + ) { + embraceImpl.recordAndDeduplicateNetworkRequest(callId, embraceNetworkRequest) + } + + override fun shouldCaptureNetworkBody(url: String, method: String): Boolean = embraceImpl.shouldCaptureNetworkCall(url, method) + + override fun setProcessStartedByNotification() { + embraceImpl.setProcessStartedByNotification() + } + + override fun isNetworkSpanForwardingEnabled(): Boolean { + return embraceImpl.configService?.networkSpanForwardingBehavior?.isNetworkSpanForwardingEnabled() ?: false + } + + override fun getSdkCurrentTime(): Long = initModule.clock.now() } diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/FlutterInternalInterface.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/FlutterInternalInterface.kt index 1a2e4a69c..b39fd4f27 100644 --- a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/FlutterInternalInterface.kt +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/FlutterInternalInterface.kt @@ -1,5 +1,7 @@ package io.embrace.android.embracesdk +import io.embrace.android.embracesdk.internal.EmbraceInternalInterface + /** * Provides an internal interface to Embrace that is intended for use by Flutter as its * sole source of communication with the Android SDK. diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/FlutterInternalInterfaceImpl.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/FlutterInternalInterfaceImpl.kt index 09f47f8f6..668844576 100644 --- a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/FlutterInternalInterfaceImpl.kt +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/FlutterInternalInterfaceImpl.kt @@ -1,6 +1,7 @@ package io.embrace.android.embracesdk import io.embrace.android.embracesdk.capture.metadata.MetadataService +import io.embrace.android.embracesdk.internal.EmbraceInternalInterface import io.embrace.android.embracesdk.logging.InternalEmbraceLogger internal class FlutterInternalInterfaceImpl( diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/InternalInterfaceModule.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/InternalInterfaceModule.kt index 6bdfe6c31..13527911c 100644 --- a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/InternalInterfaceModule.kt +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/InternalInterfaceModule.kt @@ -4,7 +4,9 @@ import io.embrace.android.embracesdk.injection.AndroidServicesModule import io.embrace.android.embracesdk.injection.CoreModule import io.embrace.android.embracesdk.injection.CrashModule import io.embrace.android.embracesdk.injection.EssentialServiceModule +import io.embrace.android.embracesdk.injection.InitModule import io.embrace.android.embracesdk.injection.singleton +import io.embrace.android.embracesdk.internal.EmbraceInternalInterface internal interface InternalInterfaceModule { val embraceInternalInterface: EmbraceInternalInterface @@ -14,6 +16,7 @@ internal interface InternalInterfaceModule { } internal class InternalInterfaceModuleImpl( + initModule: InitModule, coreModule: CoreModule, androidServicesModule: AndroidServicesModule, essentialServiceModule: EssentialServiceModule, @@ -22,7 +25,7 @@ internal class InternalInterfaceModuleImpl( ) : InternalInterfaceModule { override val embraceInternalInterface: EmbraceInternalInterface by singleton { - EmbraceInternalInterfaceImpl(embrace) + EmbraceInternalInterfaceImpl(embrace, initModule) } override val reactNativeInternalInterface: ReactNativeInternalInterface by singleton { diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ReactNativeInternalInterface.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ReactNativeInternalInterface.kt index 0cf73a40c..d6657936b 100644 --- a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ReactNativeInternalInterface.kt +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ReactNativeInternalInterface.kt @@ -1,6 +1,7 @@ package io.embrace.android.embracesdk import android.content.Context +import io.embrace.android.embracesdk.internal.EmbraceInternalInterface /** * Provides an internal interface to Embrace that is intended for use by React Native as its diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ReactNativeInternalInterfaceImpl.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ReactNativeInternalInterfaceImpl.kt index 67e94f494..104ba8a17 100644 --- a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ReactNativeInternalInterfaceImpl.kt +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/ReactNativeInternalInterfaceImpl.kt @@ -4,6 +4,7 @@ import android.content.Context import io.embrace.android.embracesdk.Embrace.AppFramework import io.embrace.android.embracesdk.capture.crash.CrashService import io.embrace.android.embracesdk.capture.metadata.MetadataService +import io.embrace.android.embracesdk.internal.EmbraceInternalInterface import io.embrace.android.embracesdk.logging.InternalEmbraceLogger import io.embrace.android.embracesdk.payload.JsException import io.embrace.android.embracesdk.prefs.PreferencesService diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/UnityInternalInterface.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/UnityInternalInterface.kt index a10e3f0b3..a220aa052 100644 --- a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/UnityInternalInterface.kt +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/UnityInternalInterface.kt @@ -1,5 +1,7 @@ package io.embrace.android.embracesdk +import io.embrace.android.embracesdk.internal.EmbraceInternalInterface + /** * Provides an internal interface to Embrace that is intended for use by Unity as its * sole source of communication with the Android SDK. diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/UnityInternalInterfaceImpl.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/UnityInternalInterfaceImpl.kt index c32e22413..ecf693a9f 100644 --- a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/UnityInternalInterfaceImpl.kt +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/UnityInternalInterfaceImpl.kt @@ -1,5 +1,6 @@ package io.embrace.android.embracesdk +import io.embrace.android.embracesdk.internal.EmbraceInternalInterface import io.embrace.android.embracesdk.logging.InternalEmbraceLogger import io.embrace.android.embracesdk.prefs.PreferencesService diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/TargetThreadHandler.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/TargetThreadHandler.kt index a1018a710..035d84908 100644 --- a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/TargetThreadHandler.kt +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/anr/detection/TargetThreadHandler.kt @@ -8,7 +8,6 @@ import androidx.annotation.VisibleForTesting import io.embrace.android.embracesdk.clock.Clock import io.embrace.android.embracesdk.config.ConfigService import io.embrace.android.embracesdk.internal.enforceThread -import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logDebug import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logError import java.util.concurrent.ExecutorService import java.util.concurrent.atomic.AtomicReference @@ -63,7 +62,6 @@ internal class TargetThreadHandler( // but if it does then we just log an internal error & consider the ANR ended at // this point. if (messageQueue == null || !installed) { - logDebug("Failed to obtain main thread MessageQueue - using fallback ANR strategy.") onMainThreadUnblocked() } } diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/EmbracePerformanceInfoService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/EmbracePerformanceInfoService.kt index 2357316d6..30eef7143 100644 --- a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/EmbracePerformanceInfoService.kt +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/EmbracePerformanceInfoService.kt @@ -37,12 +37,7 @@ internal class EmbracePerformanceInfoService( "EmbracePerformanceInfoService", "Session performance info start time: $sessionStart" ) - val requests = NetworkRequests( - networkLoggingService.getNetworkCallsForSession( - sessionStart, - sessionLastKnownTime - ) - ) + val requests = NetworkRequests(networkLoggingService.getNetworkCallsForSession()) val info = getPerformanceInfo(sessionStart, sessionLastKnownTime, coldStart) return info.copy( diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/PerformanceInfoService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/PerformanceInfoService.kt index 596d0e878..17893217f 100644 --- a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/PerformanceInfoService.kt +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/PerformanceInfoService.kt @@ -26,7 +26,6 @@ internal interface PerformanceInfoService { * * END * * INTERRUPT * - * * @param startTime the start time of the performance information to retrieve * @param endTime the end time of the performance information to retrieve * @return the performance information diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/EmbraceInternalInterface.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/EmbraceInternalInterface.kt new file mode 100644 index 000000000..e21964352 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/EmbraceInternalInterface.kt @@ -0,0 +1,196 @@ +package io.embrace.android.embracesdk.internal + +import android.util.Pair +import io.embrace.android.embracesdk.Embrace +import io.embrace.android.embracesdk.InternalApi +import io.embrace.android.embracesdk.LogType +import io.embrace.android.embracesdk.network.EmbraceNetworkRequest +import io.embrace.android.embracesdk.network.http.NetworkCaptureData + +/** + * Provides an internal interface to Embrace that is intended for use by hosted SDKs as their sole source of communication + * with the Android SDK. This is not publicly supported and methods can change at any time. + */ +@InternalApi +public interface EmbraceInternalInterface { + /** + * See [Embrace.logInfo] + */ + public fun logInfo( + message: String, + properties: Map? + ) + + /** + * See [Embrace.logWarning] + */ + public fun logWarning( + message: String, + properties: Map?, + stacktrace: String? + ) + + /** + * See [Embrace.logError] + */ + public fun logError( + message: String, + properties: Map?, + stacktrace: String?, + isException: Boolean + ) + + /** + * Backwards compatible way for hosted SDKs to log a handled exception with different log levels + */ + public fun logHandledException( + throwable: Throwable, + type: LogType, + properties: Map?, + customStackTrace: Array? + ) + + /** + * See [Embrace.recordNetworkRequest] + */ + public fun recordCompletedNetworkRequest( + url: String, + httpMethod: String, + startTime: Long, + endTime: Long, + bytesSent: Long, + bytesReceived: Long, + statusCode: Int, + traceId: String?, + networkCaptureData: NetworkCaptureData? + ) + + /** + * See [Embrace.recordNetworkRequest] + */ + public fun recordIncompleteNetworkRequest( + url: String, + httpMethod: String, + startTime: Long, + endTime: Long, + error: Throwable?, + traceId: String?, + networkCaptureData: NetworkCaptureData? + ) + + /** + * See [Embrace.recordNetworkRequest] + */ + public fun recordIncompleteNetworkRequest( + url: String, + httpMethod: String, + startTime: Long, + endTime: Long, + errorType: String?, + errorMessage: String?, + traceId: String?, + networkCaptureData: NetworkCaptureData? + ) + + /** + * Record a network request and overwrite any previously recorded request with the same callId + * + * @param callId the ID with which the request will be identified internally. The session will only contain one recorded + * request with a given ID - last writer wins. + * @param embraceNetworkRequest the request to be recorded + */ + public fun recordAndDeduplicateNetworkRequest( + callId: String, + embraceNetworkRequest: EmbraceNetworkRequest + ) + + /** + * Logs a tap on a Compose screen element. + * + * @param point the coordinates of the screen tap + * @param elementName the name of the element which was tapped + */ + public fun logComposeTap(point: Pair, elementName: String) + + /** + * For the given URL and method, whether the response body should be captured for network request logging + */ + public fun shouldCaptureNetworkBody(url: String, method: String): Boolean + + /** + * Mark that this application process was created in response to a notification + */ + public fun setProcessStartedByNotification() + + /** + * Whether the Network Span Forwarding feature is enabled + */ + public fun isNetworkSpanForwardingEnabled(): Boolean + + /** + * Return internal time the SDK is using in milliseconds. It is equivalent to [System.currentTimeMillis] assuming the system clock did + * not change after the SDK has started. + */ + public fun getSdkCurrentTime(): Long +} + +internal val defaultImpl = object : EmbraceInternalInterface { + + override fun logInfo(message: String, properties: Map?) { } + + override fun logWarning(message: String, properties: Map?, stacktrace: String?) { } + + override fun logError(message: String, properties: Map?, stacktrace: String?, isException: Boolean) { } + + override fun logHandledException( + throwable: Throwable, + type: LogType, + properties: Map?, + customStackTrace: Array? + ) { } + + override fun recordCompletedNetworkRequest( + url: String, + httpMethod: String, + startTime: Long, + endTime: Long, + bytesSent: Long, + bytesReceived: Long, + statusCode: Int, + traceId: String?, + networkCaptureData: NetworkCaptureData? + ) { } + + override fun recordIncompleteNetworkRequest( + url: String, + httpMethod: String, + startTime: Long, + endTime: Long, + error: Throwable?, + traceId: String?, + networkCaptureData: NetworkCaptureData? + ) { } + + override fun recordIncompleteNetworkRequest( + url: String, + httpMethod: String, + startTime: Long, + endTime: Long, + errorType: String?, + errorMessage: String?, + traceId: String?, + networkCaptureData: NetworkCaptureData? + ) { } + + override fun recordAndDeduplicateNetworkRequest(callId: String, embraceNetworkRequest: EmbraceNetworkRequest) { } + + override fun logComposeTap(point: Pair, elementName: String) { } + + override fun shouldCaptureNetworkBody(url: String, method: String): Boolean = false + + override fun setProcessStartedByNotification() { } + + override fun isNetworkSpanForwardingEnabled(): Boolean = false + + override fun getSdkCurrentTime(): Long = System.currentTimeMillis() +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride.java index 112e6210f..9c42fe6ba 100644 --- a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride.java +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverride.java @@ -21,6 +21,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; @@ -91,6 +92,9 @@ class EmbraceUrlConnectionOverride */ private final Embrace embrace; + @NonNull + private final String callId; + /** * A reference to the output stream wrapped in a counter, so we can determine the bytes sent. */ @@ -157,6 +161,7 @@ public EmbraceUrlConnectionOverride(@NonNull T connection, boolean enableWrapIoS this.createdTime = System.currentTimeMillis(); this.enableWrapIoStreams = enableWrapIoStreams; this.embrace = embrace; + this.callId = UUID.randomUUID().toString(); } @Override @@ -168,7 +173,7 @@ public void addRequestProperty(@NonNull String key, @Nullable String value) { public void connect() throws IOException { identifyTraceId(); try { - if (NetworkUtils.isNetworkSpanForwardingEnabled(embrace.getConfigService())) { + if (embrace.getInternalInterface().isNetworkSpanForwardingEnabled()) { traceparent = connection.getRequestProperty(TRACEPARENT_HEADER_NAME); } } catch (Exception e) { @@ -568,7 +573,8 @@ synchronized void internalLogNetworkCall(long startTime, long endTime, boolean o long contentLength = bytesIn == null ? Math.max(0, responseSize.get()) : bytesIn; if (inputStreamAccessException == null && lastConnectionAccessException == null && responseCode.get() != 0) { - embrace.recordNetworkRequest( + embrace.getInternalInterface().recordAndDeduplicateNetworkRequest( + callId, EmbraceNetworkRequest.fromCompletedRequest( url, HttpMethod.fromString(getRequestMethod()), @@ -598,7 +604,8 @@ synchronized void internalLogNetworkCall(long startTime, long endTime, boolean o String errorType = exceptionClass != null ? exceptionClass : "UnknownState"; String errorMessage = exceptionMessage != null ? exceptionMessage : "HTTP response state unknown"; - embrace.recordNetworkRequest( + embrace.getInternalInterface().recordAndDeduplicateNetworkRequest( + callId, EmbraceNetworkRequest.fromIncompleteRequest( url, HttpMethod.fromString(getRequestMethod()), @@ -816,7 +823,7 @@ private boolean hasNetworkCaptureRules() { String url = this.connection.getURL().toString(); String method = this.connection.getRequestMethod(); - return embrace.shouldCaptureNetworkBody(url, method); + return embrace.getInternalInterface().shouldCaptureNetworkBody(url, method); } /** diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandler.java b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandler.java index d171f526b..ae3dec1c4 100644 --- a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandler.java +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandler.java @@ -114,7 +114,7 @@ protected URLConnection openConnection(URL url, Proxy proxy) throws IOException } protected void injectTraceparent(@NonNull URLConnection connection) { - boolean networkSpanForwardingEnabled = NetworkUtils.isNetworkSpanForwardingEnabled(embrace.getConfigService()); + boolean networkSpanForwardingEnabled = embrace.getInternalInterface().isNetworkSpanForwardingEnabled(); if (networkSpanForwardingEnabled && !connection.getRequestProperties().containsKey(TRACEPARENT_HEADER_NAME)) { connection.addRequestProperty(TRACEPARENT_HEADER_NAME, embrace.generateW3cTraceparent()); } diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingService.kt index 1f6d3cc29..7738a8acb 100644 --- a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingService.kt +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingService.kt @@ -14,7 +14,6 @@ import io.embrace.android.embracesdk.utils.NetworkUtils.getValidTraceId import io.embrace.android.embracesdk.utils.NetworkUtils.isIpAddress import io.embrace.android.embracesdk.utils.NetworkUtils.stripUrl import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.ConcurrentSkipListMap import java.util.concurrent.atomic.AtomicInteger import kotlin.math.max @@ -32,11 +31,14 @@ internal class EmbraceNetworkLoggingService( private val networkCaptureService: NetworkCaptureService ) : NetworkLoggingService, MemoryCleanerListener { + private val callsStorageLastUpdate = AtomicInteger(0) + /** * Network calls per domain prepared for the session. */ - private val sessionNetworkCalls = ConcurrentSkipListMap() - private val networkCallCache = CacheableValue>(sessionNetworkCalls::size) + private val sessionNetworkCalls = ConcurrentHashMap() + + private val networkCallCache = CacheableValue> { callsStorageLastUpdate.get() } private val domainSettings = ConcurrentHashMap() @@ -44,13 +46,16 @@ internal class EmbraceNetworkLoggingService( private val ipAddressCount = AtomicInteger(0) - override fun getNetworkCallsForSession(startTime: Long, lastKnownTime: Long): NetworkSessionV2 { - logger.logDeveloper("EmbraceNetworkLoggingService", "getNetworkCallsForSession") - + override fun getNetworkCallsForSession(): NetworkSessionV2 { val calls = networkCallCache.value { - ArrayList(sessionNetworkCalls.subMap(startTime, lastKnownTime).values) + synchronized(callsStorageLastUpdate) { + sessionNetworkCalls.values.toList() + } } + val storedCallsSize = sessionNetworkCalls.size + val cachedCallsSize = calls.size + val overLimit = hashMapOf() for ((key, value) in callsPerDomain) { if (value.requestCount > value.captureLimit) { @@ -58,12 +63,18 @@ internal class EmbraceNetworkLoggingService( } } + if (cachedCallsSize != storedCallsSize) { + val msg = "Cached network call count different than expected: $cachedCallsSize instead of $storedCallsSize" + logger.logError(msg, IllegalStateException(msg), true) + } + // clear calls per domain and session network calls lists before be used by the next session callsPerDomain.clear() return NetworkSessionV2(calls, overLimit) } override fun logNetworkCall( + callId: String, url: String, httpMethod: String, statusCode: Int, @@ -102,11 +113,12 @@ internal class EmbraceNetworkLoggingService( ) } - processNetworkCall(startTime, networkCall) + processNetworkCall(callId, networkCall) storeSettings(url) } override fun logNetworkError( + callId: String, url: String, httpMethod: String, startTime: Long, @@ -143,19 +155,17 @@ internal class EmbraceNetworkLoggingService( errorMessage ) } - processNetworkCall(startTime, networkCall) + processNetworkCall(callId, networkCall) storeSettings(url) } /** * Process network calls to be ready when the session requests them. * - * @param startTime is the time when the network call was captured + * @param callId the unique ID that identifies the specific network call instance being recorded * @param networkCall that is going to be captured */ - private fun processNetworkCall(startTime: Long, networkCall: NetworkCallV2) { - logger.logDeveloper("EmbraceNetworkLoggingService", "processNetworkCall at: $startTime") - + private fun processNetworkCall(callId: String, networkCall: NetworkCallV2) { // Get the domain, if it can be successfully parsed val domain = networkCall.url?.let { getDomain(it) @@ -175,7 +185,7 @@ internal class EmbraceNetworkLoggingService( if (ipAddressCount.getAndIncrement() < captureLimit) { // only capture if the ipAddressCount has not exceeded defaultLimit logger.logDeveloper("EmbraceNetworkLoggingService", "capturing network call") - sessionNetworkCalls[startTime] = networkCall + storeNetworkCall(callId, networkCall) } else { logger.logDeveloper("EmbraceNetworkLoggingService", "capture limit exceeded") } @@ -185,7 +195,7 @@ internal class EmbraceNetworkLoggingService( val settings = domainSettings[domain] if (settings == null) { logger.logDeveloper("EmbraceNetworkLoggingService", "no domain settings") - sessionNetworkCalls[startTime] = networkCall + storeNetworkCall(callId, networkCall) } else { val suffix = settings.suffix val limit = settings.limit @@ -197,7 +207,7 @@ internal class EmbraceNetworkLoggingService( // Exclude if the network call exceeds the limit if (count.requestCount < limit) { - sessionNetworkCalls[startTime] = networkCall + storeNetworkCall(callId, networkCall) } else { logger.logDeveloper("EmbraceNetworkLoggingService", "capture limit exceeded") } @@ -241,10 +251,24 @@ internal class EmbraceNetworkLoggingService( } } + private fun storeNetworkCall(callId: String, networkCall: NetworkCallV2) { + synchronized(callsStorageLastUpdate) { + callsStorageLastUpdate.incrementAndGet() + sessionNetworkCalls[callId] = networkCall + } + } + + private fun clearNetworkCalls() { + synchronized(callsStorageLastUpdate) { + callsStorageLastUpdate.set(0) + sessionNetworkCalls.clear() + } + } + override fun cleanCollections() { domainSettings.clear() callsPerDomain.clear() - sessionNetworkCalls.clear() + clearNetworkCalls() // reset counters ipAddressCount.set(0) logger.logDeveloper("EmbraceNetworkLoggingService", "Collections cleaned") diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/NetworkLoggingService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/NetworkLoggingService.kt index 0ce9e192e..9bd992e51 100644 --- a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/NetworkLoggingService.kt +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/network/logging/NetworkLoggingService.kt @@ -10,18 +10,16 @@ import io.embrace.android.embracesdk.payload.NetworkSessionV2 internal interface NetworkLoggingService { /** - * Get the calls and counts of network calls (which exceed the limit) within the specified time - * range. + * Get the calls and counts of network calls (which exceed the limit) that haven't been associated with a session or background activity * - * @param startTime the start time - * @param lastKnownTime the end time * @return the network calls for the given session */ - fun getNetworkCallsForSession(startTime: Long, lastKnownTime: Long): NetworkSessionV2 + fun getNetworkCallsForSession(): NetworkSessionV2 /** * Logs a HTTP network call. * + * @param callId the unique ID of the call used for deduplication purposes * @param url the URL being called * @param httpMethod the HTTP method * @param statusCode the status code from the response @@ -33,7 +31,9 @@ internal interface NetworkLoggingService { * @param w3cTraceparent optional W3C-compliant traceparent representing the network call that is being recorded * @param networkCaptureData the additional data captured if network body capture is enabled for the URL */ + @Suppress("LongParameterList") fun logNetworkCall( + callId: String, url: String, httpMethod: String, statusCode: Int, @@ -49,6 +49,7 @@ internal interface NetworkLoggingService { /** * Logs an exception which occurred when attempting to make a network call. * + * @param callId the unique ID of the call used for deduplication purposes * @param url the URL being called * @param httpMethod the HTTP method * @param startTime the start time of the request @@ -60,6 +61,7 @@ internal interface NetworkLoggingService { * @param networkCaptureData the additional data captured if network body capture is enabled for the URL */ fun logNetworkError( + callId: String, url: String, httpMethod: String, startTime: Long, diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/SessionHandler.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/SessionHandler.kt index c60cc711e..72a132ac1 100644 --- a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/SessionHandler.kt +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/session/SessionHandler.kt @@ -335,7 +335,6 @@ internal class SessionHandler( infoLogsAttemptedToSend = remoteLogger.getInfoLogsAttemptedToSend(), warnLogsAttemptedToSend = remoteLogger.getWarnLogsAttemptedToSend(), errorLogsAttemptedToSend = remoteLogger.getErrorLogsAttemptedToSend(), - exceptionError = exceptionService.currentExceptionError, lastHeartbeatTime = clock.now(), properties = sessionProperties.get(), endType = endType, @@ -350,8 +349,7 @@ internal class SessionHandler( startupThreshold = startupThreshold, user = userService.getUserInfo(), betaFeatures = betaFeatures, - symbols = nativeThreadSamplerService?.getNativeSymbols(), - + symbols = nativeThreadSamplerService?.getNativeSymbols() ) val performanceInfo = performanceInfoService.getSessionPerformanceInfo( @@ -361,13 +359,19 @@ internal class SessionHandler( originSession.isReceivedTermination ) + val appInfo = metadataService.getAppInfo() + val deviceInfo = metadataService.getDeviceInfo() + val breadcrumbs = breadcrumbService.getBreadcrumbs(startTime, endTime) + + val endSessionWithAllErrors = endSession.copy(exceptionError = exceptionService.currentExceptionError) + return SessionMessage( - session = endSession, - userInfo = endSession.user, - appInfo = metadataService.getAppInfo(), - deviceInfo = metadataService.getDeviceInfo(), + session = endSessionWithAllErrors, + userInfo = endSessionWithAllErrors.user, + appInfo = appInfo, + deviceInfo = deviceInfo, performanceInfo = performanceInfo.copy(), - breadcrumbs = breadcrumbService.getBreadcrumbs(startTime, endTime), + breadcrumbs = breadcrumbs, spans = spans ) } diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/NetworkUtils.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/NetworkUtils.kt index 1df11d48f..d35525d6f 100644 --- a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/NetworkUtils.kt +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/utils/NetworkUtils.kt @@ -1,6 +1,5 @@ package io.embrace.android.embracesdk.utils -import io.embrace.android.embracesdk.config.ConfigService import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logDebug import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Companion.logWarning import java.net.MalformedURLException @@ -101,8 +100,4 @@ internal object NetworkUtils { (if (pathPos < 0) 0 else pathPos) + suffix.length.coerceAtMost(terminalPos) ) } - - @JvmStatic - fun isNetworkSpanForwardingEnabled(configService: ConfigService?): Boolean = - configService?.networkSpanForwardingBehavior?.isNetworkSpanForwardingEnabled() ?: false } diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceInternalInterfaceImplTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceInternalInterfaceImplTest.kt index 006c2a17a..1a1b42eb9 100644 --- a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceInternalInterfaceImplTest.kt +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/EmbraceInternalInterfaceImplTest.kt @@ -2,6 +2,10 @@ package io.embrace.android.embracesdk import android.net.Uri import android.webkit.URLUtil +import io.embrace.android.embracesdk.fakes.FakeClock +import io.embrace.android.embracesdk.fakes.injection.FakeInitModule +import io.embrace.android.embracesdk.injection.InitModule +import io.embrace.android.embracesdk.internal.defaultImpl import io.embrace.android.embracesdk.network.EmbraceNetworkRequest import io.embrace.android.embracesdk.network.http.HttpMethod import io.mockk.every @@ -11,6 +15,7 @@ import io.mockk.slot import io.mockk.verify import org.junit.Assert.assertEquals import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @@ -18,11 +23,15 @@ internal class EmbraceInternalInterfaceImplTest { private lateinit var impl: EmbraceInternalInterfaceImpl private lateinit var embrace: EmbraceImpl + private lateinit var fakeClock: FakeClock + private lateinit var initModule: InitModule @Before fun setUp() { embrace = mockk(relaxed = true) - impl = EmbraceInternalInterfaceImpl(embrace) + fakeClock = FakeClock(currentTime = beforeObjectInitTime) + initModule = FakeInitModule(clock = fakeClock) + impl = EmbraceInternalInterfaceImpl(embrace, initModule) } @Test @@ -94,144 +103,6 @@ internal class EmbraceInternalInterfaceImplTest { } } - @Test - fun testAddBreadcrumb() { - impl.addBreadcrumb("") - verify(exactly = 1) { embrace.addBreadcrumb("") } - } - - @Test - fun testGetDeviceId() { - every { embrace.deviceId } returns "test" - assertEquals("test", impl.deviceId) - } - - @Test - fun testSetUserIdentifier() { - impl.setUserIdentifier("") - verify(exactly = 1) { embrace.setUserIdentifier("") } - } - - @Test - fun testClearUserIdentifier() { - impl.clearUserIdentifier() - verify(exactly = 1) { embrace.clearUserIdentifier() } - } - - @Test - fun testSetUsername() { - impl.setUsername("") - verify(exactly = 1) { embrace.setUsername("") } - } - - @Test - fun testClearUsername() { - impl.clearUsername() - verify(exactly = 1) { embrace.clearUsername() } - } - - @Test - fun testSetUserEmail() { - impl.setUserEmail("") - verify(exactly = 1) { embrace.setUserEmail("") } - } - - @Test - fun testClearUserEmail() { - impl.clearUserEmail() - verify(exactly = 1) { embrace.clearUserEmail() } - } - - @Test - fun testSetUserAsPayer() { - impl.setUserAsPayer() - verify(exactly = 1) { embrace.setUserAsPayer() } - } - - @Test - fun testClearUserAsPayer() { - impl.clearUserAsPayer() - verify(exactly = 1) { embrace.clearUserAsPayer() } - } - - @Test - fun testAddUserPersona() { - impl.addUserPersona("") - verify(exactly = 1) { embrace.addUserPersona("") } - } - - @Test - fun testClearUserPersona() { - impl.clearUserPersona("") - verify(exactly = 1) { embrace.clearUserPersona("") } - } - - @Test - fun testClearAllUserPersonas() { - impl.clearAllUserPersonas() - verify(exactly = 1) { embrace.clearAllUserPersonas() } - } - - @Test - fun testAddSessionProperty() { - impl.addSessionProperty("key", "value", true) - verify(exactly = 1) { embrace.addSessionProperty("key", "value", true) } - } - - @Test - fun testRemoveSessionProperty() { - impl.removeSessionProperty("key") - verify(exactly = 1) { embrace.removeSessionProperty("key") } - } - - @Test - fun testGetSessionProperties() { - every { embrace.sessionProperties } returns mapOf() - assertEquals(mapOf(), impl.sessionProperties) - } - - @Test - fun testStartMoment() { - impl.startMoment("name", "id", mapOf()) - verify(exactly = 1) { embrace.startMoment("name", "id", mapOf()) } - } - - @Test - fun testEndMoment() { - impl.endMoment("name", "id", mapOf()) - verify(exactly = 1) { embrace.endMoment("name", "id", mapOf()) } - } - - @Test - fun testStartView() { - impl.startView("") - verify(exactly = 1) { embrace.startView("") } - } - - @Test - fun testEndView() { - impl.endView("") - verify(exactly = 1) { embrace.endView("") } - } - - @Test - fun testEndAppStartup() { - impl.endAppStartup(emptyMap()) - verify(exactly = 1) { embrace.endAppStartup(emptyMap()) } - } - - @Test - fun testLogInternalError() { - impl.logInternalError("msg", "details") - verify(exactly = 1) { embrace.logInternalError("msg", "details") } - } - - @Test - fun testEndSession() { - impl.endSession(true) - verify(exactly = 1) { embrace.endSession(true) } - } - @Test fun testCompletedNetworkRequest() { mockkStatic(Uri::class) @@ -297,4 +168,39 @@ internal class EmbraceInternalInterfaceImplTest { assertEquals("id-123", request.traceId) assertNull(request.networkCaptureData) } + + @Test + fun testRecordAndDeduplicateNetworkRequest() { + val url = "https://embrace.io" + val callId = "testID" + val captor = slot() + val networkRequest: EmbraceNetworkRequest = mockk() + every { networkRequest.url } answers { url } + + impl.recordAndDeduplicateNetworkRequest(callId, networkRequest) + + verify(exactly = 1) { + embrace.recordAndDeduplicateNetworkRequest(callId, capture(captor)) + } + + assertEquals(url, captor.captured.url) + } + + @Test + fun `check usage of SDK time`() { + assertEquals(beforeObjectInitTime, impl.getSdkCurrentTime()) + assertTrue(impl.getSdkCurrentTime() < System.currentTimeMillis()) + fakeClock.tick(10L) + assertEquals(fakeClock.now(), impl.getSdkCurrentTime()) + } + + @Test + fun `check default implementation`() { + assertTrue(beforeObjectInitTime < defaultImpl.getSdkCurrentTime()) + assertTrue(defaultImpl.getSdkCurrentTime() <= System.currentTimeMillis()) + } + + companion object { + val beforeObjectInitTime = System.currentTimeMillis() - 1 + } } diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/InternalInterfaceModuleImplTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/InternalInterfaceModuleImplTest.kt index d94f46a9f..158ef6621 100644 --- a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/InternalInterfaceModuleImplTest.kt +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/InternalInterfaceModuleImplTest.kt @@ -4,6 +4,7 @@ import io.embrace.android.embracesdk.fakes.injection.FakeAndroidServicesModule import io.embrace.android.embracesdk.fakes.injection.FakeCoreModule import io.embrace.android.embracesdk.fakes.injection.FakeCrashModule import io.embrace.android.embracesdk.fakes.injection.FakeEssentialServiceModule +import io.embrace.android.embracesdk.fakes.injection.FakeInitModule import org.junit.Assert.assertNotNull import org.junit.Test @@ -12,6 +13,7 @@ internal class InternalInterfaceModuleImplTest { @Test fun testModule() { val module: InternalInterfaceModule = InternalInterfaceModuleImpl( + FakeInitModule(), FakeCoreModule(), FakeAndroidServicesModule(), FakeEssentialServiceModule(), diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeClock.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeClock.kt index 4b7981b58..d66ad4934 100644 --- a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeClock.kt +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeClock.kt @@ -3,6 +3,7 @@ package io.embrace.android.embracesdk.fakes import io.embrace.android.embracesdk.clock.Clock internal class FakeClock( + @Volatile private var currentTime: Long = 0 ) : Clock { diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeNetworkLoggingService.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeNetworkLoggingService.kt index 3c578ff34..fbab2fcad 100644 --- a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeNetworkLoggingService.kt +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/FakeNetworkLoggingService.kt @@ -8,10 +8,11 @@ internal class FakeNetworkLoggingService : NetworkLoggingService { var data: NetworkSessionV2 = NetworkSessionV2(emptyList(), emptyMap()) - override fun getNetworkCallsForSession(startTime: Long, lastKnownTime: Long): NetworkSessionV2 = + override fun getNetworkCallsForSession(): NetworkSessionV2 = data override fun logNetworkCall( + callId: String, url: String, httpMethod: String, statusCode: Int, @@ -27,6 +28,7 @@ internal class FakeNetworkLoggingService : NetworkLoggingService { } override fun logNetworkError( + callId: String, url: String, httpMethod: String, startTime: Long, diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeCoreModule.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeCoreModule.kt index 8130bee9e..9cdf0aab9 100644 --- a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeCoreModule.kt +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/fakes/injection/FakeCoreModule.kt @@ -8,6 +8,7 @@ import io.embrace.android.embracesdk.injection.CoreModule import io.embrace.android.embracesdk.injection.isDebug import io.embrace.android.embracesdk.internal.EmbraceSerializer import io.embrace.android.embracesdk.logging.InternalEmbraceLogger +import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger import io.embrace.android.embracesdk.registry.ServiceRegistry import io.mockk.isMockKMock import io.mockk.mockk @@ -22,7 +23,7 @@ internal class FakeCoreModule( override val context: Context = if (isMockKMock(application)) mockk(relaxed = true) else application.applicationContext, override val appFramework: AppFramework = AppFramework.NATIVE, - override val logger: InternalEmbraceLogger = InternalEmbraceLogger(), + override val logger: InternalEmbraceLogger = InternalStaticEmbraceLogger.logger, override val serviceRegistry: ServiceRegistry = ServiceRegistry(), override val jsonSerializer: EmbraceSerializer = EmbraceSerializer(), override val resources: FakeAndroidResourcesService = FakeAndroidResourcesService(), diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverrideTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverrideTest.kt index 3f74de50d..87c65f1c5 100644 --- a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverrideTest.kt +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/http/EmbraceUrlConnectionOverrideTest.kt @@ -1,11 +1,8 @@ package io.embrace.android.embracesdk.network.http import io.embrace.android.embracesdk.Embrace -import io.embrace.android.embracesdk.config.ConfigService import io.embrace.android.embracesdk.config.behavior.NetworkSpanForwardingBehavior.Companion.TRACEPARENT_HEADER_NAME -import io.embrace.android.embracesdk.config.remote.NetworkSpanForwardingRemoteConfig -import io.embrace.android.embracesdk.fakes.FakeConfigService -import io.embrace.android.embracesdk.fakes.fakeNetworkSpanForwardingBehavior +import io.embrace.android.embracesdk.internal.EmbraceInternalInterface import io.embrace.android.embracesdk.network.EmbraceNetworkRequest import io.mockk.CapturingSlot import io.mockk.every @@ -13,37 +10,44 @@ import io.mockk.mockk import io.mockk.slot import io.mockk.verify import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test +import java.io.ByteArrayInputStream import java.io.IOException +import java.io.InputStream import java.util.concurrent.TimeoutException import javax.net.ssl.HttpsURLConnection internal class EmbraceUrlConnectionOverrideTest { private lateinit var mockEmbrace: Embrace - private lateinit var fakeConfigService: ConfigService + private lateinit var mockInternalInterface: EmbraceInternalInterface private lateinit var mockConnection: HttpsURLConnection + private lateinit var capturedCallId: MutableList private lateinit var capturedEmbraceNetworkRequest: CapturingSlot - private lateinit var remoteNetworkSpanForwardingConfig: NetworkSpanForwardingRemoteConfig private lateinit var embraceUrlConnectionOverride: EmbraceUrlConnectionOverride private lateinit var embraceUrlConnectionOverrideUnwrapped: EmbraceUrlConnectionOverride + private var shouldCaptureNetworkBody = false + private var isNetworkSpanForwardingEnabled = false @Before fun setup() { mockEmbrace = mockk(relaxed = true) + every { mockEmbrace.internalInterface } answers { mockInternalInterface } + shouldCaptureNetworkBody = false + isNetworkSpanForwardingEnabled = false + capturedCallId = mutableListOf() capturedEmbraceNetworkRequest = slot() - remoteNetworkSpanForwardingConfig = NetworkSpanForwardingRemoteConfig(pctEnabled = 0f) - fakeConfigService = FakeConfigService( - networkSpanForwardingBehavior = fakeNetworkSpanForwardingBehavior( - remoteConfig = { remoteNetworkSpanForwardingConfig } - ) - ) - every { mockEmbrace.recordNetworkRequest(capture(capturedEmbraceNetworkRequest)) } answers { } - every { mockEmbrace.configService } answers { fakeConfigService } - + mockInternalInterface = mockk(relaxed = true) + every { mockInternalInterface.shouldCaptureNetworkBody(any(), any()) } answers { shouldCaptureNetworkBody } + every { + mockInternalInterface.recordAndDeduplicateNetworkRequest(capture(capturedCallId), capture(capturedEmbraceNetworkRequest)) + } answers { } + every { mockInternalInterface.isNetworkSpanForwardingEnabled() } answers { isNetworkSpanForwardingEnabled } mockConnection = createMockConnection() embraceUrlConnectionOverride = EmbraceUrlConnectionOverride(mockConnection, true, mockEmbrace) embraceUrlConnectionOverrideUnwrapped = EmbraceUrlConnectionOverride(mockConnection, false, mockEmbrace) @@ -52,25 +56,41 @@ internal class EmbraceUrlConnectionOverrideTest { @Test fun `completed network call logged exactly once if connection connected with wrapped output stream`() { executeRequest() - verify(exactly = 1) { mockEmbrace.recordNetworkRequest(any()) } + verifyTwoCallsRecordedWithSameCallId() with(capturedEmbraceNetworkRequest.captured) { assertEquals(HttpMethod.POST.name, httpMethod) assertEquals(HTTP_OK, responseCode) - assertEquals(1L, bytesSent) - assertEquals(100L, bytesReceived) + assertEquals(requestBodySize.toLong(), bytesSent) + assertEquals(responseBodySize.toLong(), bytesReceived) assertNull(errorType) } } @Test - fun `completed network call logged exactly once if connection connected with unwrapped output stream`() { + fun `completed network call logged twice once if connection connected with wrapped output stream and network body captured`() { + shouldCaptureNetworkBody = true + executeRequest() + verifyTwoCallsRecordedWithSameCallId() + with(capturedEmbraceNetworkRequest.captured) { + assertEquals(HttpMethod.POST.name, httpMethod) + assertEquals(HTTP_OK, responseCode) + assertEquals(requestBodySize.toLong(), bytesSent) + assertEquals(responseBodySize.toLong(), bytesReceived) + assertNotNull(networkCaptureData) + assertNull(errorType) + } + } + + @Test + fun `completed network call logged exactly once with no request size if connection connected with unwrapped output stream`() { executeRequest(embraceOverride = embraceUrlConnectionOverrideUnwrapped) - verify(exactly = 1) { mockEmbrace.recordNetworkRequest(any()) } + verify(exactly = 1) { mockInternalInterface.recordAndDeduplicateNetworkRequest(any(), any()) } + assertTrue(capturedCallId[0].isNotBlank()) with(capturedEmbraceNetworkRequest.captured) { assertEquals(HttpMethod.POST.name, httpMethod) assertEquals(HTTP_OK, responseCode) assertEquals(0L, bytesSent) - assertEquals(100L, bytesReceived) + assertEquals(responseBodySize.toLong(), bytesReceived) assertNull(errorType) } } @@ -78,7 +98,8 @@ internal class EmbraceUrlConnectionOverrideTest { @Test fun `incomplete network call logged exactly once and response data not accessed if connection connected`() { executeRequest(exceptionOnInputStream = true) - verify(exactly = 1) { mockEmbrace.recordNetworkRequest(any()) } + verify(exactly = 1) { mockInternalInterface.recordAndDeduplicateNetworkRequest(any(), any()) } + assertTrue(capturedCallId[0].isNotBlank()) verify(exactly = 0) { mockConnection.responseCode } verify(exactly = 0) { mockConnection.contentLength } verify(exactly = 0) { mockConnection.headerFields } @@ -95,6 +116,8 @@ internal class EmbraceUrlConnectionOverrideTest { fun `disconnect called with uninitialized connection results in error request capture and no response access`() { embraceUrlConnectionOverride.disconnect() verifyIncompleteRequestLogged() + verify(exactly = 1) { mockInternalInterface.recordAndDeduplicateNetworkRequest(any(), any()) } + assertEquals(1, capturedCallId.size) } @Test @@ -102,6 +125,7 @@ internal class EmbraceUrlConnectionOverrideTest { every { mockConnection.contentLength } answers { throw TimeoutException() } executeRequest() verifyIncompleteRequestLogged(errorType = TIMEOUT_ERROR, noResponseAccess = false) + verifyTwoCallsRecordedWithSameCallId() } @Test @@ -109,6 +133,7 @@ internal class EmbraceUrlConnectionOverrideTest { every { mockConnection.responseCode } answers { throw TimeoutException() } executeRequest() verifyIncompleteRequestLogged(errorType = TIMEOUT_ERROR, noResponseAccess = false) + verifyTwoCallsRecordedWithSameCallId() } @Test @@ -116,6 +141,7 @@ internal class EmbraceUrlConnectionOverrideTest { every { mockConnection.headerFields } answers { throw TimeoutException() } executeRequest() verifyIncompleteRequestLogged(errorType = TIMEOUT_ERROR, noResponseAccess = false) + verifyTwoCallsRecordedWithSameCallId() } @Test @@ -145,7 +171,7 @@ internal class EmbraceUrlConnectionOverrideTest { @Test fun `check traceheaders are forwarded if feature flag is on`() { - remoteNetworkSpanForwardingConfig = NetworkSpanForwardingRemoteConfig(pctEnabled = 100f) + isNetworkSpanForwardingEnabled = true executeRequest() assertEquals(HTTP_OK, capturedEmbraceNetworkRequest.captured.responseCode) assertEquals(TRACEPARENT, capturedEmbraceNetworkRequest.captured.w3cTraceparent) @@ -153,7 +179,7 @@ internal class EmbraceUrlConnectionOverrideTest { @Test fun `check traceheaders are forwarded on errors if feature flag is on`() { - remoteNetworkSpanForwardingConfig = NetworkSpanForwardingRemoteConfig(pctEnabled = 100f) + isNetworkSpanForwardingEnabled = true executeRequest(exceptionOnInputStream = true) assertNull(capturedEmbraceNetworkRequest.captured.responseCode) assertEquals(TRACEPARENT, capturedEmbraceNetworkRequest.captured.w3cTraceparent) @@ -163,19 +189,21 @@ internal class EmbraceUrlConnectionOverrideTest { private fun createMockConnection(): HttpsURLConnection { val connection: HttpsURLConnection = mockk(relaxed = true) val mockOutputStream: CountingOutputStream = mockk(relaxed = true) - every { mockOutputStream.requestBody } answers { ByteArray(1) } + val inputStream: InputStream = ByteArrayInputStream(responseBody) + every { mockOutputStream.requestBody } answers { requestBody } every { connection.outputStream } answers { mockOutputStream } every { connection.getRequestProperty(TRACEPARENT_HEADER_NAME) } answers { TRACEPARENT } every { connection.requestMethod } answers { HttpMethod.POST.name } every { connection.responseCode } answers { HTTP_OK } - every { connection.contentLength } answers { 100 } + every { connection.contentLength } answers { responseBodySize } every { connection.headerFields } answers { mapOf( Pair("Content-Encoding", listOf("gzip")), - Pair("Content-Length", listOf("100")), + Pair("Content-Length", listOf(responseBodySize.toString())), Pair("myHeader", listOf("myValue")) ) } + every { connection.inputStream } answers { inputStream } return connection } @@ -185,14 +213,17 @@ internal class EmbraceUrlConnectionOverrideTest { ) { with(embraceOverride) { connect() - outputStream?.write(8) + outputStream?.write(requestBody) if (exceptionOnInputStream) { every { mockConnection.inputStream } answers { throw IOException() } assertThrows(IOException::class.java) { inputStream } } else { - inputStream + val input = inputStream headerFields responseCode + val b = ByteArray(8192) + input?.read(b) + assertEquals(-1, input?.read()) } disconnect() } @@ -204,14 +235,23 @@ internal class EmbraceUrlConnectionOverrideTest { verify(exactly = 0) { mockConnection.contentLength } verify(exactly = 0) { mockConnection.headerFields } } - verify(exactly = 1) { mockEmbrace.recordNetworkRequest(any()) } assertNull(capturedEmbraceNetworkRequest.captured.responseCode) assertEquals(errorType, capturedEmbraceNetworkRequest.captured.errorType) } + private fun verifyTwoCallsRecordedWithSameCallId() { + verify(exactly = 2) { mockInternalInterface.recordAndDeduplicateNetworkRequest(any(), any()) } + assertEquals(2, capturedCallId.size) + assertEquals(capturedCallId[0], capturedCallId[1]) + } + companion object { private const val TRACEPARENT = "00-3c72a77a7b51af6fb3778c06d4c165ce-4c1d710fffc88e35-01" private const val HTTP_OK = 200 + private val requestBody = "test".toByteArray() + private val requestBodySize = requestBody.size + private val responseBody = "responseresponse".toByteArray() + private val responseBodySize = responseBody.size private val IO_ERROR = checkNotNull(IOException::class.java.canonicalName) private val TIMEOUT_ERROR = checkNotNull(TimeoutException::class.java.canonicalName) } diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandlerTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandlerTest.kt index dc05ec5a6..a4fad739a 100644 --- a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandlerTest.kt +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/http/EmbraceUrlStreamHandlerTest.kt @@ -3,11 +3,8 @@ package io.embrace.android.embracesdk.network.http import android.os.Build.VERSION_CODES.TIRAMISU import androidx.test.ext.junit.runners.AndroidJUnit4 import io.embrace.android.embracesdk.Embrace -import io.embrace.android.embracesdk.config.ConfigService import io.embrace.android.embracesdk.config.behavior.NetworkSpanForwardingBehavior.Companion.TRACEPARENT_HEADER_NAME -import io.embrace.android.embracesdk.config.remote.NetworkSpanForwardingRemoteConfig -import io.embrace.android.embracesdk.fakes.FakeConfigService -import io.embrace.android.embracesdk.fakes.fakeNetworkSpanForwardingBehavior +import io.embrace.android.embracesdk.internal.EmbraceInternalInterface import io.embrace.android.embracesdk.network.EmbraceNetworkRequest import io.mockk.CapturingSlot import io.mockk.every @@ -25,23 +22,20 @@ import java.net.URL @RunWith(AndroidJUnit4::class) internal class EmbraceUrlStreamHandlerTest { private lateinit var mockEmbrace: Embrace - private lateinit var fakeConfigService: ConfigService + private lateinit var mockInternalInterface: EmbraceInternalInterface private lateinit var capturedEmbraceNetworkRequest: CapturingSlot - private lateinit var remoteNetworkSpanForwardingConfig: NetworkSpanForwardingRemoteConfig + private var isNetworkSpanForwardingEnabled = false @Before fun setup() { mockEmbrace = mockk(relaxed = true) + mockInternalInterface = mockk(relaxed = true) + every { mockInternalInterface.isNetworkSpanForwardingEnabled() } answers { isNetworkSpanForwardingEnabled } capturedEmbraceNetworkRequest = slot() - remoteNetworkSpanForwardingConfig = NetworkSpanForwardingRemoteConfig(pctEnabled = 0f) - fakeConfigService = FakeConfigService( - networkSpanForwardingBehavior = fakeNetworkSpanForwardingBehavior( - remoteConfig = { remoteNetworkSpanForwardingConfig } - ) - ) every { mockEmbrace.recordNetworkRequest(capture(capturedEmbraceNetworkRequest)) } answers { } - every { mockEmbrace.configService } answers { fakeConfigService } + every { mockEmbrace.internalInterface } answers { mockInternalInterface } every { mockEmbrace.generateW3cTraceparent() } answers { TRACEPARENT } + isNetworkSpanForwardingEnabled = false } @Test @@ -78,7 +72,7 @@ internal class EmbraceUrlStreamHandlerTest { @Test fun `check traceheader is injected into http request if feature flag is on`() { - remoteNetworkSpanForwardingConfig = NetworkSpanForwardingRemoteConfig(pctEnabled = 100f) + isNetworkSpanForwardingEnabled = true val url = URL( "http", "embrace.io", @@ -95,7 +89,7 @@ internal class EmbraceUrlStreamHandlerTest { @Test fun `check traceheader is injected into https request if feature flag is on`() { - remoteNetworkSpanForwardingConfig = NetworkSpanForwardingRemoteConfig(pctEnabled = 100f) + isNetworkSpanForwardingEnabled = true val url = URL( "https", "embrace.io", diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingServiceTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingServiceTest.kt index 6b4eb3e1e..faecdfa04 100644 --- a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingServiceTest.kt +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/network/logging/EmbraceNetworkLoggingServiceTest.kt @@ -22,6 +22,7 @@ import org.junit.Assert.assertNull import org.junit.Before import org.junit.BeforeClass import org.junit.Test +import java.util.UUID internal class EmbraceNetworkLoggingServiceTest { private lateinit var service: EmbraceNetworkLoggingService @@ -51,6 +52,7 @@ internal class EmbraceNetworkLoggingServiceTest { } @AfterClass + @JvmStatic fun tearDown() { unmockkAll() } @@ -76,18 +78,20 @@ internal class EmbraceNetworkLoggingServiceTest { } @Test - fun `test getNetworkCallsForSession only uses session between start and end time`() { + fun `test getNetworkCallsForSession returns all network calls current stored`() { logNetworkCall("www.example1.com", 100, 200) logNetworkCall("www.example2.com", 200, 300) logNetworkCall("www.example3.com", 300, 400) logNetworkCall("www.example4.com", 400, 500) - val result = service.getNetworkCallsForSession(200, 301) + val result = service.getNetworkCallsForSession() + assertEquals(4, result.requests.size) - // test use only session calls - assertEquals(2, result.requests.size) - assertEquals("www.example2.com", result.requests.at(0)?.url) - assertEquals("www.example3.com", result.requests.at(1)?.url) + val sortedRequests = result.requests.sortedBy { it.startTime } + assertEquals("www.example1.com", sortedRequests.at(0)?.url) + assertEquals("www.example2.com", sortedRequests.at(1)?.url) + assertEquals("www.example3.com", sortedRequests.at(2)?.url) + assertEquals("www.example4.com", sortedRequests.at(3)?.url) } @Test @@ -104,7 +108,7 @@ internal class EmbraceNetworkLoggingServiceTest { logNetworkCall("www.overLimit2.com") logNetworkCall("www.overLimit3.com") - val result = service.getNetworkCallsForSession(0, Long.MAX_VALUE) + val result = service.getNetworkCallsForSession() // overLimit1 has 4 calls. The limit is 2. val expectedOverLimit = DomainCount(4, 2) @@ -131,7 +135,7 @@ internal class EmbraceNetworkLoggingServiceTest { logNetworkCall("www.overLimit2.com") logNetworkCall("www.overLimit3.com") - val result = service.getNetworkCallsForSession(0, Long.MAX_VALUE) + val result = service.getNetworkCallsForSession() // overLimit1 has 4 calls. The local limit is 2. val expectedOverLimit = DomainCount(4, 2) @@ -150,6 +154,7 @@ internal class EmbraceNetworkLoggingServiceTest { val endTime = 20000L service.logNetworkError( + randomId(), url, httpMethod, startTime, @@ -161,7 +166,7 @@ internal class EmbraceNetworkLoggingServiceTest { null ) - val result = service.getNetworkCallsForSession(0, Long.MAX_VALUE) + val result = service.getNetworkCallsForSession() assertEquals(url, result.requests.at(0)?.url) } @@ -169,6 +174,7 @@ internal class EmbraceNetworkLoggingServiceTest { @Test fun `test logNetworkCall sends the network body if necessary`() { service.logNetworkCall( + randomId(), "www.example.com", "GET", 200, @@ -196,6 +202,7 @@ internal class EmbraceNetworkLoggingServiceTest { @Test fun `test logNetworkCall doesn't send the network body if null`() { service.logNetworkCall( + randomId(), "www.example.com", "GET", 200, @@ -229,14 +236,51 @@ internal class EmbraceNetworkLoggingServiceTest { service.cleanCollections() - val result = service.getNetworkCallsForSession(0, Long.MAX_VALUE) + val result = service.getNetworkCallsForSession() assertEquals(0, result.requests.size) assertEquals(0, result.requestCounts.size) } - private fun logNetworkCall(url: String, startTime: Long = 100, endTime: Long = 200) { + @Test + fun `network requests with the same start time will be recorded each time`() { + val startTime = 99L + val endTime = 300L + repeat(2) { + logNetworkCall(url = "https://embrace.io", startTime = startTime, endTime = endTime) + } + + repeat(2) { + logNetworkError(url = "https://embrace.io", startTime = startTime) + } + + assertEquals(4, service.getNetworkCallsForSession().requests.size) + } + + @Test + fun `network requests with the same callId will be logged once with last writer wins`() { + val callId = UUID.randomUUID().toString() + val expectedStartTime = 99L + val expectedEndTime = 300L + val expectedUrl = "https://embrace.io/forreal" + + logNetworkCall(url = "https://embrace.io", startTime = 50, endTime = 100, callId = callId) + logNetworkError(url = "https://embrace.io", startTime = 50, callId = callId) + logNetworkCall(url = expectedUrl, startTime = expectedStartTime, endTime = expectedEndTime, callId = callId) + + val result = service.getNetworkCallsForSession() + assertEquals(1, result.requests.size) + with(result.requests[0]) { + assertEquals(1, result.requests.size) + assertEquals(expectedStartTime, startTime) + assertEquals(expectedEndTime, endTime) + assertEquals(expectedUrl, url) + } + } + + private fun logNetworkCall(url: String, startTime: Long = 100, endTime: Long = 200, callId: String = randomId()) { service.logNetworkCall( + callId, url, "GET", 200, @@ -249,4 +293,21 @@ internal class EmbraceNetworkLoggingServiceTest { null ) } + + private fun logNetworkError(url: String, startTime: Long = 100, callId: String = randomId()) { + service.logNetworkError( + callId, + url, + "GET", + startTime, + 0, + NullPointerException::class.java.canonicalName, + "NPE baby", + null, + null, + null + ) + } + + private fun randomId(): String = UUID.randomUUID().toString() }