-
Notifications
You must be signed in to change notification settings - Fork 659
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Connect] Emit analytic events #9873
Open
simond-stripe
wants to merge
23
commits into
master
Choose a base branch
from
simond/emit-events
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+651
−20
Open
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
531b194
Add analytics client and spec
simond-stripe 3ac75eb
Add javadoc
simond-stripe c0589c0
Add common params
simond-stripe 9e55604
Add merchant id hook
simond-stripe cc0cf23
Init analytics service
simond-stripe cb2e154
WIP
simond-stripe 10d6434
cleanup
simond-stripe 45388a1
Fix order of operations for initialization
simond-stripe fb92f91
Apply init changes to account onboarding
simond-stripe 10b2386
connect/src/main/java/com/stripe/android/connect/
simond-stripe a70c58c
suppress lint check
simond-stripe cee4006
Remove test emission
simond-stripe 03b3776
Update to singleton architecture
simond-stripe 1be17a0
add eof for lint
simond-stripe 8502807
Remove unneeded val
simond-stripe ce8f402
remove extra line
simond-stripe 162d768
Add test, remove out-of-date comment
simond-stripe 9c48437
events so far
simond-stripe bc80c32
Add error analytics
simond-stripe 3dc0efa
add deserialization analytics
simond-stripe 1034b22
Make interfaces internal, fix lint issues
simond-stripe 10e99ce
Unbreak tests
simond-stripe 580c36f
Add tests
simond-stripe File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) | ||
} | ||
} |
208 changes: 208 additions & 0 deletions
208
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,208 @@ | ||
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: Long | ||
) : 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: Long, | ||
val perceivedTimeToLoad: Long | ||
) : 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/android_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 error: String, | ||
val errorMessage: String? = null, | ||
) : ConnectAnalyticsEvent( | ||
"client_error", | ||
mapOf( | ||
"error" to error, | ||
"errorMessage" to errorMessage, | ||
) | ||
) | ||
} |
60 changes: 60 additions & 0 deletions
60
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,60 @@ | ||
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.AnalyticsRequestV2 | ||
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 | ||
|
||
/** | ||
* Service for logging [AnalyticsRequestV2] for the Connect SDK. | ||
* This service is very simple. Consumers should prefer [ComponentAnalyticsService] instead, | ||
* which uses this service internally. | ||
*/ | ||
internal class ConnectAnalyticsService(application: Application) { | ||
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) | ||
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" | ||
} | ||
} |
23 changes: 23 additions & 0 deletions
23
connect/src/main/java/com/stripe/android/connect/util/AndroidClock.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,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. | ||
*/ | ||
internal 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. | ||
*/ | ||
internal class AndroidClock : Clock { | ||
override fun millis(): Long = System.currentTimeMillis() | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👀 I talked with @mludowise-stripe and since this is a catch-all error for capturing unexpected issues, we agreed the schema doesn't need to match between iOS and Android. I picked a schema that makes sense for Android independent of what iOS has chosen.