-
Notifications
You must be signed in to change notification settings - Fork 662
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Connect] Add analytics client and spec (#9780)
* Add analytics client and spec * Add javadoc * Add common params * Add merchant id hook * Init analytics service * WIP * cleanup * Fix order of operations for initialization * Apply init changes to account onboarding * connect/src/main/java/com/stripe/android/connect/ * suppress lint check * Remove test emission * Update to singleton architecture * add eof for lint * Remove unneeded val * remove extra line * Add test, remove out-of-date comment * Update doc string * Add tests * Fix lint * Abstract service
- Loading branch information
1 parent
0333f85
commit 924207c
Showing
8 changed files
with
489 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
41 changes: 41 additions & 0 deletions
41
connect/src/main/java/com/stripe/android/connect/analytics/ComponentAnalyticsService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package com.stripe.android.connect.analytics | ||
|
||
import com.stripe.android.connect.StripeEmbeddedComponent | ||
import java.util.UUID | ||
|
||
/** | ||
* Service for logging [ConnectAnalyticsEvent] for the Connect SDK. Also keeps track | ||
* of shared parameters to pass alongside events. | ||
* There should be one instance of ComponentAnalyticsService per instantiation of [StripeEmbeddedComponent]. | ||
*/ | ||
internal class ComponentAnalyticsService( | ||
private val analyticsService: ConnectAnalyticsService, | ||
private val component: StripeEmbeddedComponent, | ||
private val publishableKey: String?, // can be null in cases where it should not be logged | ||
) { | ||
internal var merchantId: String? = null | ||
private var componentUUID = UUID.randomUUID() | ||
|
||
/** | ||
* Log an analytic [event]. | ||
*/ | ||
fun track(event: ConnectAnalyticsEvent) { | ||
val params = buildMap { | ||
// add common params | ||
put("merchantId", merchantId) | ||
put("component", component.componentName) | ||
put("componentInstance", componentUUID.toString()) | ||
put("publishableKey", publishableKey) | ||
|
||
// event-specific params should be included in both the top-level and event_metadata | ||
// blob so that we can use them in prometheus alerts (which are only available for | ||
// top-level events). | ||
if (event.params.isNotEmpty()) { | ||
putAll(event.params) | ||
put("event_metadata", event.params) | ||
} | ||
} | ||
|
||
analyticsService.track(event.eventName, params) | ||
} | ||
} |
212 changes: 212 additions & 0 deletions
212
connect/src/main/java/com/stripe/android/connect/analytics/ConnectAnalyticsEvent.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
package com.stripe.android.connect.analytics | ||
|
||
/** | ||
* Analytics event for the Connect SDK. One subclass per unique analytics event is expected. | ||
*/ | ||
internal sealed class ConnectAnalyticsEvent( | ||
val eventName: String, | ||
val params: Map<String, Any?> = mapOf(), | ||
) { | ||
/** | ||
* A component was instantiated via create{ComponentType}. | ||
* | ||
* The delta between this and component.viewed could tell us if apps are instantiating | ||
* components but never rendering them on screen. | ||
*/ | ||
data object ComponentCreated : ConnectAnalyticsEvent("component.created") | ||
|
||
/** | ||
* The component is viewed on screen (viewDidAppear lifecycle event on iOS) | ||
*/ | ||
data class ComponentViewed( | ||
val pageViewId: String? | ||
) : ConnectAnalyticsEvent( | ||
"component.viewed", | ||
mapOf("page_view_id" to pageViewId) | ||
) | ||
|
||
/** | ||
* The web page finished loading (didFinish navigation on iOS). | ||
* | ||
* Note: This should happen before component_loaded, so we won't yet have a page_view_id. | ||
*/ | ||
data class WebPageLoaded( | ||
val timeToLoad: Double | ||
) : ConnectAnalyticsEvent( | ||
"component.web.page_loaded", | ||
mapOf("time_to_load" to timeToLoad.toString()) | ||
) | ||
|
||
/** | ||
* The component is successfully loaded within the web view. Triggered from componentDidLoad | ||
* message handler from the web view. | ||
*/ | ||
data class WebComponentLoaded( | ||
val pageViewId: String, | ||
val timeToLoad: Double, | ||
val perceivedTimeToLoad: Double | ||
) : ConnectAnalyticsEvent( | ||
"component.web.component_loaded", | ||
mapOf( | ||
"page_view_id" to pageViewId, | ||
"time_to_load" to timeToLoad.toString(), | ||
"perceived_time_to_load" to perceivedTimeToLoad.toString() | ||
) | ||
) | ||
|
||
/** | ||
* The SDK receives a non-200 status code loading the web view, other than "Internet connectivity" errors. | ||
* | ||
* Intent is to alert if the URL the mobile client expects is suddenly unreachable. | ||
* The web view should always return a 200, even when displaying an error state. | ||
*/ | ||
data class WebErrorPageLoad( | ||
val status: Int?, | ||
val error: String?, | ||
val url: String | ||
) : ConnectAnalyticsEvent( | ||
"component.web.error.page_load", | ||
mapOf( | ||
"status" to status?.toString(), | ||
"error" to error, | ||
"url" to url | ||
) | ||
) | ||
|
||
/** | ||
* If the web view sends an onLoadError that can't be deserialized by the SDK. | ||
*/ | ||
data class WebWarnErrorUnexpectedLoad( | ||
val errorType: String, | ||
val pageViewId: String? | ||
) : ConnectAnalyticsEvent( | ||
"component.web.warnerror.unexpected_load_error_type", | ||
mapOf( | ||
"error_type" to errorType, | ||
"page_view_id" to pageViewId | ||
) | ||
) | ||
|
||
/** | ||
* If the web view calls onSetterFunctionCalled with a setter argument the SDK doesn't know how to handle. | ||
* | ||
* Note: It's expected to get this warning when web adds support for new setter functions not handled | ||
* in older SDK versions. But logging it could help us debug issues where we expect the SDK to handle | ||
* something it isn't. | ||
*/ | ||
data class WebWarnUnrecognizedSetter( | ||
val setter: String, | ||
val pageViewId: String? | ||
) : ConnectAnalyticsEvent( | ||
"component.web.warn.unrecognized_setter_function", | ||
mapOf( | ||
"setter" to setter, | ||
"page_view_id" to pageViewId | ||
) | ||
) | ||
|
||
/** | ||
* An error occurred deserializing the JSON payload from a web message. | ||
*/ | ||
data class WebErrorDeserializeMessage( | ||
val message: String, | ||
val error: String, | ||
val errorDescription: String?, | ||
val pageViewId: String? | ||
) : ConnectAnalyticsEvent( | ||
"component.web.error.deserialize_message", | ||
mapOf( | ||
"message" to message, | ||
"error" to error, | ||
"error_description" to errorDescription, | ||
"page_view_id" to pageViewId | ||
) | ||
) | ||
|
||
/** | ||
* A web view was opened when openWebView was called. | ||
*/ | ||
data class AuthenticatedWebOpened( | ||
val pageViewId: String?, | ||
val authenticatedViewId: String | ||
) : ConnectAnalyticsEvent( | ||
"component.authenticated_web.opened", | ||
mapOf( | ||
"page_view_id" to pageViewId, | ||
"authenticated_view_id" to authenticatedViewId | ||
) | ||
) | ||
|
||
/** | ||
* The web view successfully redirected back to the app. | ||
*/ | ||
data class AuthenticatedWebRedirected( | ||
val pageViewId: String?, | ||
val authenticatedViewId: String | ||
) : ConnectAnalyticsEvent( | ||
"component.authenticated_web.redirected", | ||
mapOf( | ||
"page_view_id" to pageViewId, | ||
"authenticated_view_id" to authenticatedViewId | ||
) | ||
) | ||
|
||
/** | ||
* The user closed the web view before getting redirected back to the app. | ||
*/ | ||
data class AuthenticatedWebCanceled( | ||
val pageViewId: String?, | ||
val viewId: String | ||
) : ConnectAnalyticsEvent( | ||
"component.authenticated_web.canceled", | ||
mapOf( | ||
"page_view_id" to pageViewId, | ||
"view_id" to viewId | ||
) | ||
) | ||
|
||
/** | ||
* The web view threw an error and was not successfully redirected back to the app. | ||
*/ | ||
data class AuthenticatedWebError( | ||
val error: String, | ||
val pageViewId: String?, | ||
val viewId: String | ||
) : ConnectAnalyticsEvent( | ||
"component.authenticated_web.error", | ||
mapOf( | ||
"error" to error, | ||
"page_view_id" to pageViewId, | ||
"view_id" to viewId | ||
) | ||
) | ||
|
||
/** | ||
* The web page navigated somewhere other than the component wrapper URL | ||
* (e.g. https://connect-js.stripe.com/v1.0/ios-webview.html) | ||
*/ | ||
data class WebErrorUnexpectedNavigation( | ||
val url: String | ||
) : ConnectAnalyticsEvent( | ||
"component.web.error.unexpected_navigation", | ||
mapOf("url" to url) | ||
) | ||
|
||
/** | ||
* Catch-all event for unexpected client-side errors. | ||
*/ | ||
data class ClientError( | ||
val domain: String, | ||
val code: Int, | ||
val file: String, | ||
val line: Int | ||
) : ConnectAnalyticsEvent( | ||
"client_error", | ||
mapOf( | ||
"domain" to domain, | ||
"code" to code.toString(), | ||
"file" to file, | ||
"line" to line.toString() | ||
) | ||
) | ||
} |
62 changes: 62 additions & 0 deletions
62
connect/src/main/java/com/stripe/android/connect/analytics/ConnectAnalyticsService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package com.stripe.android.connect.analytics | ||
|
||
import android.app.Application | ||
import com.stripe.android.core.BuildConfig | ||
import com.stripe.android.core.Logger | ||
import com.stripe.android.core.networking.AnalyticsRequestV2Factory | ||
import com.stripe.android.core.networking.DefaultAnalyticsRequestV2Executor | ||
import com.stripe.android.core.networking.DefaultStripeNetworkClient | ||
import com.stripe.android.core.networking.RealAnalyticsRequestV2Storage | ||
import com.stripe.android.core.utils.RealIsWorkManagerAvailable | ||
import kotlinx.coroutines.DelicateCoroutinesApi | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.GlobalScope | ||
import kotlinx.coroutines.launch | ||
|
||
/** | ||
* Analytics service configured for Connect SDK. | ||
* Consumers should prefer [ComponentAnalyticsService] instead as this service is very simple. | ||
*/ | ||
internal interface ConnectAnalyticsService { | ||
fun track(eventName: String, params: Map<String, Any?>) | ||
} | ||
|
||
internal class DefaultConnectAnalyticsService(application: Application) : ConnectAnalyticsService { | ||
private val analyticsRequestStorage = RealAnalyticsRequestV2Storage(application) | ||
private val logger = Logger.getInstance(enableLogging = BuildConfig.DEBUG) | ||
private val networkClient = DefaultStripeNetworkClient() | ||
private val isWorkerAvailable = RealIsWorkManagerAvailable( | ||
isEnabledForMerchant = { true } | ||
) | ||
|
||
private val requestExecutor = DefaultAnalyticsRequestV2Executor( | ||
application = application, | ||
networkClient = networkClient, | ||
logger = logger, | ||
storage = analyticsRequestStorage, | ||
isWorkManagerAvailable = isWorkerAvailable, | ||
) | ||
|
||
private val requestFactory = AnalyticsRequestV2Factory( | ||
context = application, | ||
clientId = CLIENT_ID, | ||
origin = ORIGIN, | ||
) | ||
|
||
@OptIn(DelicateCoroutinesApi::class) | ||
override fun track(eventName: String, params: Map<String, Any?>) { | ||
GlobalScope.launch(Dispatchers.IO) { | ||
val request = requestFactory.createRequest( | ||
eventName = eventName, | ||
additionalParams = params, | ||
includeSDKParams = true, | ||
) | ||
requestExecutor.enqueue(request) | ||
} | ||
} | ||
|
||
internal companion object { | ||
const val CLIENT_ID = "mobile_connect_sdk" | ||
const val ORIGIN = "stripe-connect-android" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.