From 93602fed64a914ea6d4d05134dac83eb35f908be Mon Sep 17 00:00:00 2001 From: Simon Duchastel Date: Tue, 7 Jan 2025 17:45:38 -0500 Subject: [PATCH 1/8] events so far --- .../analytics/ConnectAnalyticsEvent.kt | 6 +-- .../android/connect/util/AndroidClock.kt | 23 ++++++++++ .../webview/StripeConnectWebViewContainer.kt | 8 +++- ...StripeConnectWebViewContainerController.kt | 43 +++++++++++++++++-- .../StripeConnectWebViewContainerState.kt | 10 ++++- 5 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 connect/src/main/java/com/stripe/android/connect/util/AndroidClock.kt diff --git a/connect/src/main/java/com/stripe/android/connect/analytics/ConnectAnalyticsEvent.kt b/connect/src/main/java/com/stripe/android/connect/analytics/ConnectAnalyticsEvent.kt index 5e820aa7511..4f996f96dc0 100644 --- a/connect/src/main/java/com/stripe/android/connect/analytics/ConnectAnalyticsEvent.kt +++ b/connect/src/main/java/com/stripe/android/connect/analytics/ConnectAnalyticsEvent.kt @@ -31,7 +31,7 @@ internal sealed class ConnectAnalyticsEvent( * Note: This should happen before component_loaded, so we won't yet have a page_view_id. */ data class WebPageLoaded( - val timeToLoad: Double + val timeToLoad: Long ) : ConnectAnalyticsEvent( "component.web.page_loaded", mapOf("time_to_load" to timeToLoad.toString()) @@ -43,8 +43,8 @@ internal sealed class ConnectAnalyticsEvent( */ data class WebComponentLoaded( val pageViewId: String, - val timeToLoad: Double, - val perceivedTimeToLoad: Double + val timeToLoad: Long, + val perceivedTimeToLoad: Long ) : ConnectAnalyticsEvent( "component.web.component_loaded", mapOf( diff --git a/connect/src/main/java/com/stripe/android/connect/util/AndroidClock.kt b/connect/src/main/java/com/stripe/android/connect/util/AndroidClock.kt new file mode 100644 index 00000000000..52e96153776 --- /dev/null +++ b/connect/src/main/java/com/stripe/android/connect/util/AndroidClock.kt @@ -0,0 +1,23 @@ +package com.stripe.android.connect.util + +/** + * [Clock] interface to be used to provide compatible `Clock` functionality, + * and one day be replaced by `java.time.Clock` when all consumers support > SDK 26. + * + * Also useful for mocking in tests. + */ +interface Clock { + + /** + * Return the current system time in milliseconds + */ + fun millis(): Long +} + +/** + * A [Clock] that depends on Android APIs. To be replaced by java.time.Clock when all consumers + * support > SDK 26. + */ +class AndroidClock : Clock { + override fun millis(): Long = System.currentTimeMillis() +} diff --git a/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainer.kt b/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainer.kt index 9263592d6ff..a247c4fed50 100644 --- a/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainer.kt +++ b/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainer.kt @@ -33,6 +33,7 @@ import com.stripe.android.connect.StripeEmbeddedComponentListener import com.stripe.android.connect.appearance.Appearance import com.stripe.android.connect.databinding.StripeConnectWebviewBinding import com.stripe.android.connect.toJsonObject +import com.stripe.android.connect.util.AndroidClock import com.stripe.android.connect.webview.serialization.AccountSessionClaimedMessage import com.stripe.android.connect.webview.serialization.ConnectInstanceJs import com.stripe.android.connect.webview.serialization.ConnectJson @@ -202,6 +203,7 @@ internal class StripeConnectWebViewContainerImpl( this.controller = StripeConnectWebViewContainerController( view = this, analyticsService = analyticsService, + clock = AndroidClock(), embeddedComponentManager = embeddedComponentManager, embeddedComponent = embeddedComponent, listener = listener, @@ -269,6 +271,10 @@ internal class StripeConnectWebViewContainerImpl( controller?.onPageStarted() } + override fun onPageFinished(view: WebView?, url: String?) { + controller?.onPageFinished() + } + override fun onReceivedHttpError( view: WebView, request: WebResourceRequest, @@ -388,7 +394,7 @@ internal class StripeConnectWebViewContainerImpl( val pageLoadMessage = ConnectJson.decodeFromString(message) logger.debug("Page did load: $pageLoadMessage") - controller?.onReceivedPageDidLoad() + controller?.onReceivedPageDidLoad(pageLoadMessage.pageViewId) } @JavascriptInterface diff --git a/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerController.kt b/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerController.kt index f57da1de02a..110ddec797f 100644 --- a/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerController.kt +++ b/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerController.kt @@ -18,10 +18,13 @@ import com.stripe.android.connect.PrivateBetaConnectSDK import com.stripe.android.connect.StripeEmbeddedComponent import com.stripe.android.connect.StripeEmbeddedComponentListener import com.stripe.android.connect.analytics.ComponentAnalyticsService +import com.stripe.android.connect.analytics.ConnectAnalyticsEvent +import com.stripe.android.connect.util.Clock import com.stripe.android.connect.webview.serialization.ConnectInstanceJs import com.stripe.android.connect.webview.serialization.SetOnLoadError import com.stripe.android.connect.webview.serialization.SetOnLoaderStart import com.stripe.android.connect.webview.serialization.SetterFunctionCalledMessage +import com.stripe.android.connect.webview.serialization.SetterFunctionCalledMessage.UnknownValue import com.stripe.android.core.Logger import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -36,6 +39,7 @@ import kotlinx.coroutines.withContext internal class StripeConnectWebViewContainerController( private val view: StripeConnectWebViewContainerInternal, private val analyticsService: ComponentAnalyticsService, + private val clock: Clock, private val embeddedComponentManager: EmbeddedComponentManager, private val embeddedComponent: StripeEmbeddedComponent, private val listener: Listener?, @@ -44,6 +48,10 @@ internal class StripeConnectWebViewContainerController updateState { copy(appearance = appearance) } - if (stateFlow.value.receivedPageDidLoad) { + if (stateFlow.value.pageViewId != null) { view.updateConnectInstance(appearance) } } @@ -215,9 +239,16 @@ internal class StripeConnectWebViewContainerController { + if (value is UnknownValue) { + analyticsService.track(ConnectAnalyticsEvent.WebWarnUnrecognizedSetter( + setter = message.setter, + pageViewId = stateFlow.value.pageViewId + )) + } with(listenerDelegate) { listener?.delegate(message) } diff --git a/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerState.kt b/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerState.kt index 052897530aa..f73eda7542c 100644 --- a/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerState.kt +++ b/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerState.kt @@ -9,9 +9,15 @@ import com.stripe.android.connect.util.getContrastingColor @OptIn(PrivateBetaConnectSDK::class) internal data class StripeConnectWebViewContainerState( /** - * True if we received the 'pageDidLoad' message. + * Non-null if we received the 'pageDidLoad' message, + * null otherwise. */ - val receivedPageDidLoad: Boolean = false, + val pageViewId: String? = null, + + /** + * The time the webview began loading, in milliseconds from midnight, January 1, 1970 UTC. + */ + val didBeginLoadingMillis: Long? = null, /** * True if we received the 'setOnLoaderStart' message. From dd7a73fa13031d475876f14c80a056b18002449c Mon Sep 17 00:00:00 2001 From: Simon Duchastel Date: Wed, 8 Jan 2025 11:59:56 -0500 Subject: [PATCH 2/8] Add error analytics # Conflicts: # connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerController.kt --- .../analytics/ConnectAnalyticsEvent.kt | 14 +++++------- .../webview/StripeConnectWebViewContainer.kt | 2 +- ...StripeConnectWebViewContainerController.kt | 22 ++++++++++++++++--- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/connect/src/main/java/com/stripe/android/connect/analytics/ConnectAnalyticsEvent.kt b/connect/src/main/java/com/stripe/android/connect/analytics/ConnectAnalyticsEvent.kt index 4f996f96dc0..18c1b8e9e33 100644 --- a/connect/src/main/java/com/stripe/android/connect/analytics/ConnectAnalyticsEvent.kt +++ b/connect/src/main/java/com/stripe/android/connect/analytics/ConnectAnalyticsEvent.kt @@ -183,7 +183,7 @@ internal sealed class ConnectAnalyticsEvent( /** * The web page navigated somewhere other than the component wrapper URL - * (e.g. https://connect-js.stripe.com/v1.0/ios-webview.html) + * (e.g. https://connect-js.stripe.com/v1.0/android_webview.html) */ data class WebErrorUnexpectedNavigation( val url: String @@ -196,17 +196,13 @@ internal sealed class ConnectAnalyticsEvent( * Catch-all event for unexpected client-side errors. */ data class ClientError( - val domain: String, - val code: Int, - val file: String, - val line: Int + val error: String, + val errorMessage: String? = null, ) : ConnectAnalyticsEvent( "client_error", mapOf( - "domain" to domain, - "code" to code.toString(), - "file" to file, - "line" to line.toString() + "error" to error, + "errorMessage" to errorMessage, ) ) } diff --git a/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainer.kt b/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainer.kt index a247c4fed50..fa29e8e01e6 100644 --- a/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainer.kt +++ b/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainer.kt @@ -268,7 +268,7 @@ internal class StripeConnectWebViewContainerImpl( @VisibleForTesting internal inner class StripeConnectWebViewClient : WebViewClient() { override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) { - controller?.onPageStarted() + controller?.onPageStarted(url) } override fun onPageFinished(view: WebView?, url: String?) { diff --git a/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerController.kt b/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerController.kt index 110ddec797f..a5359278511 100644 --- a/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerController.kt +++ b/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerController.kt @@ -74,8 +74,18 @@ internal class StripeConnectWebViewContainerController Date: Wed, 8 Jan 2025 12:24:05 -0500 Subject: [PATCH 3/8] add deserialization analytics --- .../webview/StripeConnectWebViewContainer.kt | 36 ++++++++++++++++--- ...StripeConnectWebViewContainerController.kt | 13 +++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainer.kt b/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainer.kt index fa29e8e01e6..59e3345c3e2 100644 --- a/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainer.kt +++ b/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainer.kt @@ -377,7 +377,10 @@ internal class StripeConnectWebViewContainerImpl( @JavascriptInterface fun onSetterFunctionCalled(message: String) { - val parsed = ConnectJson.decodeFromString(message) + val parsed = tryDeserializeWebMessage( + webFunctionName = "onSetterFunctionCalled", + message = message, + ) ?: return logger.debug("Setter function called: $parsed") controller?.onReceivedSetterFunctionCalled(parsed) @@ -385,13 +388,19 @@ internal class StripeConnectWebViewContainerImpl( @JavascriptInterface fun openSecureWebView(message: String) { - val secureWebViewData = ConnectJson.decodeFromString(message) + val secureWebViewData = tryDeserializeWebMessage( + webFunctionName = "openSecureWebView", + message = message, + ) logger.debug("Open secure web view with data: $secureWebViewData") } @JavascriptInterface fun pageDidLoad(message: String) { - val pageLoadMessage = ConnectJson.decodeFromString(message) + val pageLoadMessage = tryDeserializeWebMessage( + webFunctionName = "pageDidLoad", + message = message, + ) ?: return logger.debug("Page did load: $pageLoadMessage") controller?.onReceivedPageDidLoad(pageLoadMessage.pageViewId) @@ -399,7 +408,10 @@ internal class StripeConnectWebViewContainerImpl( @JavascriptInterface fun accountSessionClaimed(message: String) { - val accountSessionClaimedMessage = ConnectJson.decodeFromString(message) + val accountSessionClaimedMessage = tryDeserializeWebMessage( + webFunctionName = "accountSessionClaimed", + message = message, + ) ?: return logger.debug("Account session claimed: $accountSessionClaimedMessage") controller?.onMerchantIdChanged(accountSessionClaimedMessage.merchantId) @@ -413,6 +425,22 @@ internal class StripeConnectWebViewContainerImpl( } } + private inline fun tryDeserializeWebMessage( + webFunctionName: String, + message: String, + ): T? { + return try { + ConnectJson.decodeFromString(message) + } catch (e: IllegalArgumentException) { + controller?.onErrorDeserializingWebMessage( + webMessage = message, + error = "Unable to deserialize message from $webFunctionName", + errorMessage = e.message, + ) + null + } + } + private fun WebView.evaluateSdkJs(function: String, payload: JsonObject) { val command = "$ANDROID_JS_INTERFACE.$function($payload)" post { diff --git a/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerController.kt b/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerController.kt index a5359278511..b498276981e 100644 --- a/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerController.kt +++ b/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerController.kt @@ -129,6 +129,19 @@ internal class StripeConnectWebViewContainerController Date: Wed, 8 Jan 2025 13:38:47 -0500 Subject: [PATCH 4/8] Make interfaces internal, fix lint issues --- .../android/connect/util/AndroidClock.kt | 4 +- ...StripeConnectWebViewContainerController.kt | 70 +++++++++++-------- 2 files changed, 44 insertions(+), 30 deletions(-) diff --git a/connect/src/main/java/com/stripe/android/connect/util/AndroidClock.kt b/connect/src/main/java/com/stripe/android/connect/util/AndroidClock.kt index 52e96153776..d98dbf618a7 100644 --- a/connect/src/main/java/com/stripe/android/connect/util/AndroidClock.kt +++ b/connect/src/main/java/com/stripe/android/connect/util/AndroidClock.kt @@ -6,7 +6,7 @@ package com.stripe.android.connect.util * * Also useful for mocking in tests. */ -interface Clock { +internal interface Clock { /** * Return the current system time in milliseconds @@ -18,6 +18,6 @@ interface Clock { * A [Clock] that depends on Android APIs. To be replaced by java.time.Clock when all consumers * support > SDK 26. */ -class AndroidClock : Clock { +internal class AndroidClock : Clock { override fun millis(): Long = System.currentTimeMillis() } diff --git a/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerController.kt b/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerController.kt index b498276981e..32ade39ecb0 100644 --- a/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerController.kt +++ b/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerController.kt @@ -121,11 +121,13 @@ internal class StripeConnectWebViewContainerController { if (value is UnknownValue) { - analyticsService.track(ConnectAnalyticsEvent.WebWarnUnrecognizedSetter( - setter = message.setter, - pageViewId = stateFlow.value.pageViewId - )) + analyticsService.track( + ConnectAnalyticsEvent.WebWarnUnrecognizedSetter( + setter = message.setter, + pageViewId = stateFlow.value.pageViewId + ) + ) } with(listenerDelegate) { listener?.delegate(message) From f1b428e28d72930fc71d513805b83848e4cda2e2 Mon Sep 17 00:00:00 2001 From: Simon Duchastel Date: Wed, 8 Jan 2025 13:48:49 -0500 Subject: [PATCH 5/8] Unbreak tests --- .../StripeConnectWebViewContainerControllerTest.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/connect/src/test/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerControllerTest.kt b/connect/src/test/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerControllerTest.kt index d1adb49f1d6..d7985e7fd36 100644 --- a/connect/src/test/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerControllerTest.kt +++ b/connect/src/test/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerControllerTest.kt @@ -18,6 +18,7 @@ import com.stripe.android.connect.StripeEmbeddedComponent import com.stripe.android.connect.analytics.ComponentAnalyticsService import com.stripe.android.connect.appearance.Appearance import com.stripe.android.connect.appearance.Colors +import com.stripe.android.connect.util.Clock import com.stripe.android.connect.webview.serialization.SetOnLoadError import com.stripe.android.connect.webview.serialization.SetOnLoadError.LoadError import com.stripe.android.connect.webview.serialization.SetOnLoaderStart @@ -54,6 +55,7 @@ class StripeConnectWebViewContainerControllerTest { private val mockPermissionRequest: PermissionRequest = mock() private val view: StripeConnectWebViewContainerInternal = mock() private val analyticsService: ComponentAnalyticsService = mock() + private val androidClock: Clock = mock() private val embeddedComponentManager: EmbeddedComponentManager = mock() private val embeddedComponent: StripeEmbeddedComponent = StripeEmbeddedComponent.PAYOUTS @@ -74,10 +76,12 @@ class StripeConnectWebViewContainerControllerTest { Dispatchers.setMain(Dispatchers.Unconfined) whenever(embeddedComponentManager.appearanceFlow) doReturn appearanceFlow + whenever(embeddedComponentManager.getStripeURL(any())) doReturn "https://example.com" controller = StripeConnectWebViewContainerController( view = view, analyticsService = analyticsService, + clock = androidClock, embeddedComponentManager = embeddedComponentManager, embeddedComponent = embeddedComponent, listener = listener, @@ -165,7 +169,7 @@ class StripeConnectWebViewContainerControllerTest { @Test fun `should handle SetOnLoaderStart`() = runTest { val message = SetterFunctionCalledMessage(SetOnLoaderStart("")) - controller.onPageStarted() + controller.onPageStarted("https://example.com") controller.onReceivedSetterFunctionCalled(message) val state = controller.stateFlow.value @@ -227,11 +231,11 @@ class StripeConnectWebViewContainerControllerTest { appearanceFlow.emit(appearances[0]) controller.onViewAttached() - controller.onPageStarted() + controller.onPageStarted("https://example.com") verify(view, never()).updateConnectInstance(any()) // Should update appearance when pageDidLoad is received. - controller.onReceivedPageDidLoad() + controller.onReceivedPageDidLoad("page_view_id") // Should update again when appearance changes. appearanceFlow.emit(appearances[1]) From 684455301fc503854064d317929657664f714d51 Mon Sep 17 00:00:00 2001 From: Simon Duchastel Date: Wed, 8 Jan 2025 15:27:57 -0500 Subject: [PATCH 6/8] Add tests --- ...StripeConnectWebViewContainerController.kt | 12 +- ...peConnectWebViewContainerControllerTest.kt | 125 +++++++++++++++++- 2 files changed, 131 insertions(+), 6 deletions(-) diff --git a/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerController.kt b/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerController.kt index 32ade39ecb0..bafaa6d1bd2 100644 --- a/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerController.kt +++ b/connect/src/main/java/com/stripe/android/connect/webview/StripeConnectWebViewContainerController.kt @@ -80,9 +80,13 @@ internal class StripeConnectWebViewContainerController { on { url } doReturn uri } + controller.onReceivedPageDidLoad("page_view_id") val result = controller.shouldOverrideUrlLoading(mockContext, mockRequest) assertFalse(result) + verify(analyticsService).track( + ConnectAnalyticsEvent.ClientError( + error = "Unexpected pop-up", + errorMessage = "Received pop-up for allow-listed host: https://connect-js.stripe.com/allowlisted", + ) + ) } @Test @@ -263,12 +272,18 @@ class StripeConnectWebViewContainerControllerTest { } @Test - fun `onPermissionRequest denies permission when no supported permissions are requested`() = runTest { + fun `onPermissionRequest denies when no supported permissions requested, logs unexpected permissions`() = runTest { whenever(mockPermissionRequest.resources) doReturn arrayOf("unsupported_permission") controller.onPermissionRequest(mockContext, mockPermissionRequest) verify(mockPermissionRequest).deny() + verify(analyticsService).track( + ConnectAnalyticsEvent.ClientError( + error = "Unexpected permissions request", + errorMessage = "Unexpected permissions 'unsupported_permission' requested", + ) + ) } @Test @@ -302,4 +317,110 @@ class StripeConnectWebViewContainerControllerTest { controller.onMerchantIdChanged("merchant_id") verify(analyticsService).merchantId = "merchant_id" } + + @Test + fun `emit component created analytic on init`() { + verify(analyticsService).track(ConnectAnalyticsEvent.ComponentCreated) + } + + @Test + fun `emit component viewed analytic on view attach`() { + // page view id is null since we haven't received pageDidLoad yet + controller.onViewAttached() + verify(analyticsService).track(ConnectAnalyticsEvent.ComponentViewed(null)) + + // once we receive pageDidLoad, we should emit the page view id for subsequent analytics + controller.onReceivedPageDidLoad("id123") + controller.onViewAttached() + verify(analyticsService).track(ConnectAnalyticsEvent.ComponentViewed("id123")) + } + + @Test + fun `emit unexpected navigation analytic if non-stripe url page started`() { + whenever( + embeddedComponentManager.getStripeURL(embeddedComponent) + ) doReturn "https://stripe.com/test?foo=bar#mytest" + + controller.onPageStarted("https://example.com/test?foo=bar#mytest") + + // when emitting the analytic, we should strip the query params + verify(analyticsService).track( + ConnectAnalyticsEvent.WebErrorUnexpectedNavigation("https://example.com/test") + ) + } + + @Test + fun `dont emit unexpected navigation analytic if expected stripe url is used on page started`() { + whenever( + embeddedComponentManager.getStripeURL(any()) + ) doReturn "https://stripe.com/test?foo=bar#mytest" + + controller.onPageStarted("https://stripe.com/test") + + // when emitting the analytic, we should strip the query params + verify(analyticsService, never()).track( + ConnectAnalyticsEvent.WebErrorUnexpectedNavigation("https://stripe.com/test") + ) + } + + @Test + fun `emit web page loaded analytic on page finished`() { + whenever(androidClock.millis()) doReturn 100L + controller.onViewAttached() // register that the page was attached to capture the start of loading + + whenever(androidClock.millis()) doReturn 200L // difference of 100ms to start of load + controller.onPageFinished() + verify(analyticsService).track(ConnectAnalyticsEvent.WebPageLoaded(100L)) + } + + @Test + fun `emit web component loaded analytic when received pageDidLoad`() { + whenever(androidClock.millis()) doReturn 100L + controller.onViewAttached() // register that the page was attached to capture the start of loading + + whenever(androidClock.millis()) doReturn 200L // difference of 100ms to start of load + controller.onReceivedPageDidLoad("pageView123") + verify(analyticsService).track( + ConnectAnalyticsEvent.WebComponentLoaded( + pageViewId = "pageView123", + timeToLoad = 100L, + perceivedTimeToLoad = 100L, + ) + ) + } + + @Test + fun `emit web page error when main page receives an error`() { + controller.onReceivedError( + requestUrl = "https://stripe.com", + httpStatusCode = 404, + errorMessage = "Not Found", + isMainPageLoad = true + ) + verify(analyticsService).track( + ConnectAnalyticsEvent.WebErrorPageLoad( + status = 404, + error = "Not Found", + url = "https://stripe.com", + ) + ) + } + + @Test + fun `emit deserialization error on error to deserialize web message`() { + controller.onReceivedPageDidLoad("page_view_id") + controller.onErrorDeserializingWebMessage( + webMessage = "{ invalid: 4 ", + error = "Unable to deserialize", + errorMessage = "Error parsing JSON" + ) + verify(analyticsService).track( + ConnectAnalyticsEvent.WebErrorDeserializeMessage( + message = "{ invalid: 4 ", + error = "Unable to deserialize", + errorDescription = "Error parsing JSON", + pageViewId = "page_view_id", + ) + ) + } } From 2920d501dfba415f5748b0e5e6d9efc8a92713c0 Mon Sep 17 00:00:00 2001 From: Simon Duchastel Date: Fri, 10 Jan 2025 13:20:27 -0500 Subject: [PATCH 7/8] Fix test --- .../connect/analytics/ComponentAnalyticsServiceTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/connect/src/test/java/com/stripe/android/connect/analytics/ComponentAnalyticsServiceTest.kt b/connect/src/test/java/com/stripe/android/connect/analytics/ComponentAnalyticsServiceTest.kt index cfb823fd5eb..262d2f4a06d 100644 --- a/connect/src/test/java/com/stripe/android/connect/analytics/ComponentAnalyticsServiceTest.kt +++ b/connect/src/test/java/com/stripe/android/connect/analytics/ComponentAnalyticsServiceTest.kt @@ -86,8 +86,8 @@ class ComponentAnalyticsServiceTest { componentAnalyticsService.track( ConnectAnalyticsEvent.WebComponentLoaded( pageViewId = "pageViewId123", - timeToLoad = 100.0, - perceivedTimeToLoad = 50.0, + timeToLoad = 100L, + perceivedTimeToLoad = 50L, ) ) val mapCaptor = argumentCaptor>() From f4215233b27f44cd7b26bb8258ed7bd1af8a6a91 Mon Sep 17 00:00:00 2001 From: Simon Duchastel Date: Fri, 10 Jan 2025 13:42:09 -0500 Subject: [PATCH 8/8] Fix double->long in test --- .../connect/analytics/ComponentAnalyticsServiceTest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/connect/src/test/java/com/stripe/android/connect/analytics/ComponentAnalyticsServiceTest.kt b/connect/src/test/java/com/stripe/android/connect/analytics/ComponentAnalyticsServiceTest.kt index 262d2f4a06d..6410a91d558 100644 --- a/connect/src/test/java/com/stripe/android/connect/analytics/ComponentAnalyticsServiceTest.kt +++ b/connect/src/test/java/com/stripe/android/connect/analytics/ComponentAnalyticsServiceTest.kt @@ -96,14 +96,14 @@ class ComponentAnalyticsServiceTest { val expectedMetadata = mapOf( "page_view_id" to "pageViewId123", - "time_to_load" to "100.0", - "perceived_time_to_load" to "50.0", + "time_to_load" to "100", + "perceived_time_to_load" to "50", ) assertContains(params, "event_metadata") assertEquals(expectedMetadata, params["event_metadata"]) assertEquals("pageViewId123", params["page_view_id"]) - assertEquals("100.0", params["time_to_load"]) - assertEquals("50.0", params["perceived_time_to_load"]) + assertEquals("100", params["time_to_load"]) + assertEquals("50", params["perceived_time_to_load"]) } @Test