diff --git a/paymentsheet/src/androidTest/java/com/stripe/android/checkout/CheckoutPaymentElementAnalyticsTest.kt b/paymentsheet/src/androidTest/java/com/stripe/android/checkout/CheckoutPaymentElementAnalyticsTest.kt index 2519086b50c..fe56eee43f9 100644 --- a/paymentsheet/src/androidTest/java/com/stripe/android/checkout/CheckoutPaymentElementAnalyticsTest.kt +++ b/paymentsheet/src/androidTest/java/com/stripe/android/checkout/CheckoutPaymentElementAnalyticsTest.kt @@ -49,27 +49,21 @@ internal class CheckoutPaymentElementAnalyticsTest { private val formPage = EmbeddedFormPage(testRules.compose) @Test - fun testSheetAnalyticsUsesCheckoutProductUsage() = runCheckoutPaymentElementScenario( + fun testSheetAnalyticsUsesPaymentElementProductUsageAndCheckoutSessionId() = runCheckoutPaymentElementScenario( networkRule = networkRule, setup = { controller -> - networkRule.validateAnalyticsRequest( - eventName = "mc_load_started", - productUsage = setOf("Checkout"), - ) - networkRule.validateAnalyticsRequest( - eventName = "mc_load_succeeded", - productUsage = setOf("Checkout"), - ) networkRule.validateAnalyticsRequest( eventName = "mc_initial_displayed_payment_methods", - productUsage = setOf("Checkout"), + productUsage = setOf("PaymentElement"), + analyticsPayloadField("checkout_session_id", DEFAULT_CHECKOUT_SESSION_ID), ) controller.configure(DEFAULT_CLIENT_SECRET).getOrThrow() }, ) { networkRule.validateAnalyticsRequest( eventName = "mc_carousel_payment_method_tapped", - productUsage = setOf("Checkout"), + productUsage = setOf("PaymentElement"), + analyticsPayloadField("checkout_session_id", DEFAULT_CHECKOUT_SESSION_ID), ) networkRule.validateAnalyticsRequest( eventName = "stripe_android.card_metadata_pk_available", @@ -77,7 +71,8 @@ internal class CheckoutPaymentElementAnalyticsTest { ) networkRule.validateAnalyticsRequest( eventName = "mc_form_shown", - productUsage = setOf("Checkout"), + productUsage = setOf("PaymentElement"), + analyticsPayloadField("checkout_session_id", DEFAULT_CHECKOUT_SESSION_ID), ) networkRule.validateAnalyticsRequest( eventName = "stripe_android.card_metadata_pk_available", @@ -85,7 +80,8 @@ internal class CheckoutPaymentElementAnalyticsTest { ) networkRule.validateAnalyticsRequest( eventName = "mc_cardscan_api_check_failed", - productUsage = setOf("Checkout"), + productUsage = setOf("PaymentElement"), + analyticsPayloadField("checkout_session_id", DEFAULT_CHECKOUT_SESSION_ID), ) contentPage.clickOnLpm("card") @@ -93,7 +89,8 @@ internal class CheckoutPaymentElementAnalyticsTest { networkRule.validateAnalyticsRequest( eventName = "mc_dismiss", - productUsage = setOf("Checkout"), + productUsage = setOf("PaymentElement"), + analyticsPayloadField("checkout_session_id", DEFAULT_CHECKOUT_SESSION_ID), ) Espresso.pressBack() formPage.waitUntilMissing() @@ -111,17 +108,10 @@ internal class CheckoutPaymentElementAnalyticsTest { }, checkoutInitResponse = ::billingTaxCheckoutInitResponse, setup = { controller -> - networkRule.validateAnalyticsRequest( - eventName = "mc_load_started", - productUsage = setOf("Checkout"), - ) - networkRule.validateAnalyticsRequest( - eventName = "mc_load_succeeded", - productUsage = setOf("Checkout"), - ) networkRule.validateAnalyticsRequest( eventName = "mc_initial_displayed_payment_methods", - productUsage = setOf("Checkout"), + productUsage = setOf("PaymentElement"), + analyticsPayloadField("checkout_session_id", DEFAULT_CHECKOUT_SESSION_ID), ) controller.configure(DEFAULT_CLIENT_SECRET).getOrThrow() }, @@ -163,15 +153,6 @@ internal class CheckoutPaymentElementAnalyticsTest { analyticsPayloadField("error_type", "checkoutSessionTotalChanged"), analyticsPayloadField("error_code", "checkout_session_total_changed"), ) - // The Checkout Session is refreshed after confirmation fails. - networkRule.validateAnalyticsRequest( - eventName = "mc_load_started", - productUsage = setOf("Checkout"), - ) - networkRule.validateAnalyticsRequest( - eventName = "mc_load_succeeded", - productUsage = setOf("Checkout"), - ) contentPage.clickOnLpm("google_pay") confirm() @@ -196,17 +177,10 @@ internal class CheckoutPaymentElementAnalyticsTest { } }, setup = { controller -> - networkRule.validateAnalyticsRequest( - eventName = "mc_load_started", - productUsage = setOf("Checkout"), - ) - networkRule.validateAnalyticsRequest( - eventName = "mc_load_succeeded", - productUsage = setOf("Checkout"), - ) networkRule.validateAnalyticsRequest( eventName = "mc_initial_displayed_payment_methods", - productUsage = setOf("Checkout"), + productUsage = setOf("PaymentElement"), + analyticsPayloadField("checkout_session_id", DEFAULT_CHECKOUT_SESSION_ID), ) controller.configure(DEFAULT_CLIENT_SECRET).getOrThrow() }, @@ -216,15 +190,6 @@ internal class CheckoutPaymentElementAnalyticsTest { eventName = "unexpected_error.checkout.saved_payment_method.missing_billing_address", productUsage = setOf("Checkout"), ) - // Changing the selection reloads the payment element. - networkRule.validateAnalyticsRequest( - eventName = "mc_load_started", - productUsage = setOf("Checkout"), - ) - networkRule.validateAnalyticsRequest( - eventName = "mc_load_succeeded", - productUsage = setOf("Checkout"), - ) // Billing-tax filtering keeps addressless saved payment methods out of the UI, so // select one directly. diff --git a/paymentsheet/src/androidTest/java/com/stripe/android/checkout/ExpressCheckoutElementAnalyticsTest.kt b/paymentsheet/src/androidTest/java/com/stripe/android/checkout/ExpressCheckoutElementAnalyticsTest.kt index 071fb952a61..7c21503673f 100644 --- a/paymentsheet/src/androidTest/java/com/stripe/android/checkout/ExpressCheckoutElementAnalyticsTest.kt +++ b/paymentsheet/src/androidTest/java/com/stripe/android/checkout/ExpressCheckoutElementAnalyticsTest.kt @@ -70,11 +70,6 @@ internal class ExpressCheckoutElementAnalyticsTest { analyticsPayloadField("request_id", "req_checkout_confirm"), ) - // We re-load after confirm succeeds, which triggers another two loads: one for ECE, one for PE. - repeat(2) { - validateLoadingAnalyticsRequests() - } - page.clickGooglePayButton() } @@ -101,8 +96,7 @@ internal class ExpressCheckoutElementAnalyticsTest { analyticsPayloadField("link_context", "wallet"), ) - // We re-load after confirm succeeds, which triggers another two loads: one for ECE, one for PE. - // We also re-load Link state since the confirmation was for a Link payment method. + // Confirmation reloads Link state for both PE and ECE. enqueueLoadingRequests(linkEnabled = true) page.clickLinkButton() @@ -129,7 +123,7 @@ internal class ExpressCheckoutElementAnalyticsTest { analyticsPayloadField("error_code", "1"), ) - // We re-load after confirmation fails, which triggers another two loads: one for ECE, one for PE. + // Confirmation reloads Link state for both PE and ECE. enqueueLoadingRequests(linkEnabled = true) page.clickGooglePayButton() @@ -161,11 +155,9 @@ internal class ExpressCheckoutElementAnalyticsTest { analyticsPayloadField("error_type", "java.lang.IllegalStateException"), ) - // We re-load after confirmation fails, which triggers another two loads: one for ECE, one for PE. - // We also re-load Link state since the confirmation was for a Link payment method. + // Confirmation reloads Link state for both PE and ECE. repeat(2) { networkRule.enqueueLinkAccountLookup() - validateLoadingAnalyticsRequests() validateLinkAccountLookupAnalyticsRequest() } @@ -228,11 +220,6 @@ internal class ExpressCheckoutElementAnalyticsTest { analyticsPayloadField("selected_lpm", "google_pay"), ) - // We re-load after confirm succeeds, which triggers another two loads: one for ECE, one for PE. - repeat(2) { - validateLoadingAnalyticsRequests() - } - page.clickGooglePayButton() } @@ -280,11 +267,6 @@ internal class ExpressCheckoutElementAnalyticsTest { analyticsPayloadField("selected_lpm", "google_pay"), ) - // We re-load after confirm succeeds, which triggers another two loads: one for ECE, one for PE. - repeat(2) { - validateLoadingAnalyticsRequests() - } - page.clickGooglePayButton() } @@ -322,11 +304,6 @@ internal class ExpressCheckoutElementAnalyticsTest { analyticsPayloadField("error_code", "checkout_session_total_changed"), ) - // We re-load after confirmation fails, which triggers another two loads: one for ECE, one for PE. - repeat(2) { - validateLoadingAnalyticsRequests() - } - page.clickGooglePayButton() } @@ -353,21 +330,15 @@ internal class ExpressCheckoutElementAnalyticsTest { } private fun enqueueLoadingRequests(linkEnabled: Boolean) { - // We load twice, once for PE and once for ECE. So all these requests are made twice. - repeat(2) { - if (linkEnabled) { + if (linkEnabled) { + // PE and ECE each load Link state. + repeat(2) { networkRule.enqueueLinkAccountLookup() validateLinkAccountLookupAnalyticsRequest() } - validateLoadingAnalyticsRequests() } } - private fun validateLoadingAnalyticsRequests() { - validateAnalyticsRequest(eventName = "mc_load_started") - validateAnalyticsRequest(eventName = "mc_load_succeeded") - } - private fun validateConfirmationAnalyticsRequest( eventName: String, vararg requestMatchers: RequestMatcher, diff --git a/paymentsheet/src/main/java/com/stripe/android/checkout/injection/CheckoutModule.kt b/paymentsheet/src/main/java/com/stripe/android/checkout/injection/CheckoutModule.kt index a9b1f1fef8c..7ae55ce8fe9 100644 --- a/paymentsheet/src/main/java/com/stripe/android/checkout/injection/CheckoutModule.kt +++ b/paymentsheet/src/main/java/com/stripe/android/checkout/injection/CheckoutModule.kt @@ -3,18 +3,35 @@ package com.stripe.android.checkout.injection import android.app.Application import android.content.Context import com.stripe.android.common.di.DisplayDensity +import com.stripe.android.core.ApiConfiguration import com.stripe.android.core.injection.ENABLE_LOGGING import com.stripe.android.core.injection.StripeNetworkClientModule +import com.stripe.android.networking.PaymentAnalyticsRequestFactory import com.stripe.android.payments.core.injection.PRODUCT_USAGE import com.stripe.android.paymentsheet.BuildConfig +import com.stripe.android.paymentsheet.analytics.PaymentElementAnalytics import com.stripe.android.uicore.image.DefaultStripeImageLoader import com.stripe.android.uicore.image.StripeImageLoader import dagger.Module import dagger.Provides import javax.inject.Named +import javax.inject.Provider @Module(includes = [StripeNetworkClientModule::class]) internal object CheckoutModule { + @Provides + @PaymentElementAnalytics + fun provideEventReporterAnalyticsRequestFactory( + context: Context, + apiConfigurationProvider: Provider, + ): PaymentAnalyticsRequestFactory { + return PaymentAnalyticsRequestFactory( + context = context, + defaultProductUsageTokens = setOf("PaymentElement"), + apiConfigurationProvider = apiConfigurationProvider, + ) + } + @Provides fun provideContext(application: Application): Context = application.applicationContext diff --git a/paymentsheet/src/main/java/com/stripe/android/common/taptoadd/TapToAddViewModelComponent.kt b/paymentsheet/src/main/java/com/stripe/android/common/taptoadd/TapToAddViewModelComponent.kt index 9b8af06e0d7..ffd5f9db294 100644 --- a/paymentsheet/src/main/java/com/stripe/android/common/taptoadd/TapToAddViewModelComponent.kt +++ b/paymentsheet/src/main/java/com/stripe/android/common/taptoadd/TapToAddViewModelComponent.kt @@ -66,6 +66,7 @@ import com.stripe.android.paymentsheet.DefaultPrefsRepository import com.stripe.android.paymentsheet.PrefsRepository import com.stripe.android.paymentsheet.analytics.DefaultEventReporter import com.stripe.android.paymentsheet.analytics.EventReporter +import com.stripe.android.paymentsheet.analytics.PaymentElementAnalytics import com.stripe.android.paymentsheet.injection.ApiConfigurationModule import com.stripe.android.paymentsheet.repositories.CustomerApiRepository import com.stripe.android.paymentsheet.repositories.CustomerRepository @@ -207,6 +208,12 @@ internal interface TapToAddViewModelModule { ): TapToAddConfirmationInteractor.Factory companion object { + @Provides + @PaymentElementAnalytics + fun provideEventReporterAnalyticsRequestFactory( + factory: PaymentAnalyticsRequestFactory, + ): PaymentAnalyticsRequestFactory = factory + @Provides @Named(ENABLE_LOGGING) fun provideEnabledLogging(): Boolean = BuildConfig.DEBUG diff --git a/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkModule.kt b/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkModule.kt index 999804dbcd3..6ea213a4777 100644 --- a/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkModule.kt +++ b/paymentsheet/src/main/java/com/stripe/android/link/injection/NativeLinkModule.kt @@ -58,6 +58,7 @@ import com.stripe.android.paymentsheet.addresselement.AutocompleteLauncher import com.stripe.android.paymentsheet.addresselement.DefaultAutocompleteLauncher import com.stripe.android.paymentsheet.analytics.DefaultEventReporter import com.stripe.android.paymentsheet.analytics.EventReporter +import com.stripe.android.paymentsheet.analytics.PaymentElementAnalytics import com.stripe.android.repository.ConsumersApiService import com.stripe.android.repository.ConsumersApiServiceImpl import com.stripe.android.uicore.image.DefaultStripeImageLoader @@ -152,6 +153,12 @@ internal interface NativeLinkModule { @SuppressWarnings("TooManyFunctions") companion object { + @Provides + @PaymentElementAnalytics + fun provideEventReporterAnalyticsRequestFactory( + factory: PaymentAnalyticsRequestFactory, + ): PaymentAnalyticsRequestFactory = factory + @Provides @NativeLinkScope fun providesLinkAccountHolder( diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityModule.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityModule.kt index 8ac3969f6af..8c1cc54b50e 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityModule.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityModule.kt @@ -14,13 +14,16 @@ import com.stripe.android.common.spms.SavedPaymentMethodLinkFormHelper import com.stripe.android.common.taptoadd.DefaultTapToAddHelper import com.stripe.android.common.taptoadd.TapToAddHelper import com.stripe.android.common.taptoadd.TapToAddMode +import com.stripe.android.core.ApiConfiguration import com.stripe.android.core.injection.ViewModelScope import com.stripe.android.core.utils.RealUserFacingLogger import com.stripe.android.core.utils.UserFacingLogger import com.stripe.android.link.account.LinkAccountHolder +import com.stripe.android.lpmfoundations.paymentmethod.IntegrationMetadata import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata import com.stripe.android.model.PaymentMethod import com.stripe.android.model.PaymentMethodMessagePromotion +import com.stripe.android.networking.PaymentAnalyticsRequestFactory import com.stripe.android.paymentelement.EmbeddedPaymentElement import com.stripe.android.paymentelement.confirmation.ConfirmationHandler import com.stripe.android.paymentelement.embedded.form.OnClickDelegateOverrideImpl @@ -44,6 +47,7 @@ import com.stripe.android.paymentelement.embedded.sheet.SheetActivityConfirmatio import com.stripe.android.paymentelement.embedded.sheet.SheetActivityContinueCoordinator import com.stripe.android.paymentelement.embedded.sheet.SheetActivityRegistrar import com.stripe.android.paymentelement.embedded.sheet.SheetActivityStateHolder +import com.stripe.android.payments.core.injection.PRODUCT_USAGE import com.stripe.android.payments.core.injection.STATUS_BAR_COLOR import com.stripe.android.paymentsheet.CustomerStateHolder import com.stripe.android.paymentsheet.DefaultPrefsRepository @@ -55,6 +59,7 @@ import com.stripe.android.paymentsheet.addresselement.StripeAutocompleteReposito import com.stripe.android.paymentsheet.addresselement.analytics.AddressLauncherEventReporter import com.stripe.android.paymentsheet.addresselement.analytics.DefaultAddressLauncherEventReporter import com.stripe.android.paymentsheet.analytics.EventReporter +import com.stripe.android.paymentsheet.analytics.PaymentElementAnalytics import com.stripe.android.paymentsheet.model.PaymentSelection import com.stripe.android.paymentsheet.repositories.CheckoutSessionResponse import com.stripe.android.paymentsheet.repositories.PaymentMethodMessagePromotionsHelper @@ -143,6 +148,25 @@ internal interface EmbeddedActivityModule { @Suppress("TooManyFunctions") companion object { + @Provides + @PaymentElementAnalytics + fun provideEventReporterAnalyticsRequestFactory( + context: Context, + paymentMethodMetadata: PaymentMethodMetadata, + @Named(PRODUCT_USAGE) productUsage: Set, + apiConfigurationProvider: Provider, + ): PaymentAnalyticsRequestFactory { + val eventProductUsage = when (paymentMethodMetadata.integrationMetadata) { + is IntegrationMetadata.CheckoutSession -> setOf("PaymentElement") + else -> productUsage + } + return PaymentAnalyticsRequestFactory( + context = context, + defaultProductUsageTokens = eventProductUsage, + apiConfigurationProvider = apiConfigurationProvider, + ) + } + @Provides fun providesContext(application: Application): Context { return application diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/content/EmbeddedPaymentElementViewModelComponent.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/content/EmbeddedPaymentElementViewModelComponent.kt index a8ebfda81b0..20149ae25a3 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/content/EmbeddedPaymentElementViewModelComponent.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/content/EmbeddedPaymentElementViewModelComponent.kt @@ -14,6 +14,7 @@ import com.stripe.android.googlepaylauncher.injection.GooglePayLauncherModule import com.stripe.android.link.account.LinkAccountHolder import com.stripe.android.link.injection.PaymentsIntegrityModule import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata +import com.stripe.android.networking.PaymentAnalyticsRequestFactory import com.stripe.android.paymentelement.callbacks.PaymentElementCallbackIdentifier import com.stripe.android.paymentelement.callbacks.PaymentElementCallbackReferences import com.stripe.android.paymentelement.confirmation.ConfirmationHandler @@ -30,6 +31,7 @@ import com.stripe.android.payments.core.injection.PRODUCT_USAGE import com.stripe.android.payments.core.injection.STATUS_BAR_COLOR import com.stripe.android.paymentsheet.DefaultPrefsRepository import com.stripe.android.paymentsheet.PrefsRepository +import com.stripe.android.paymentsheet.analytics.PaymentElementAnalytics import com.stripe.android.paymentsheet.injection.ApiConfigurationResolverModule import com.stripe.android.paymentsheet.injection.LinkHoldbackExposureModule import com.stripe.android.paymentsheet.injection.PaymentMethodMessagePromotionsExperimentHandlerModule @@ -212,6 +214,12 @@ internal interface EmbeddedPaymentElementViewModelModule { @Suppress("TooManyFunctions") companion object { + @Provides + @PaymentElementAnalytics + fun provideEventReporterAnalyticsRequestFactory( + factory: PaymentAnalyticsRequestFactory, + ): PaymentAnalyticsRequestFactory = factory + @Provides fun provideVerticalPaymentSelectionHandler( selectionHolder: EmbeddedSelectionHolder, diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/DefaultEventReporter.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/DefaultEventReporter.kt index f885b01a835..49682591a5e 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/DefaultEventReporter.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/DefaultEventReporter.kt @@ -39,7 +39,7 @@ internal class DefaultEventReporter @Inject internal constructor( private val mode: EventReporter.Mode, private val analyticsRequestExecutor: AnalyticsRequestExecutor, private val analyticsRequestV2Executor: AnalyticsRequestV2Executor, - private val paymentAnalyticsRequestFactory: PaymentAnalyticsRequestFactory, + @PaymentElementAnalytics private val paymentAnalyticsRequestFactory: PaymentAnalyticsRequestFactory, private val durationProvider: DurationProvider, private val analyticEventCallbackProvider: Provider, @IOContext private val workContext: CoroutineContext, @@ -497,11 +497,13 @@ internal class DefaultEventReporter @Inject internal constructor( } override fun onAnalyticsEvent(event: AnalyticsEvent) { + val paymentMethodMetadata = paymentMethodMetadataProvider.get() CoroutineScope(workContext).launch { analyticsRequestExecutor.executeAsync( paymentAnalyticsRequestFactory.createRequest( event = event, - additionalParams = paymentMethodMetadataProvider.get().defaultAnalyticsParams(), + additionalParams = paymentMethodMetadata.defaultAnalyticsParams(), + publishableKeyOverride = paymentMethodMetadata?.apiConfiguration?.publishableKey, ) ) } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/PaymentElementAnalytics.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/PaymentElementAnalytics.kt new file mode 100644 index 00000000000..95b9f6ea878 --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/PaymentElementAnalytics.kt @@ -0,0 +1,7 @@ +package com.stripe.android.paymentsheet.analytics + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class PaymentElementAnalytics diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/injection/PaymentSheetCommonModule.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/injection/PaymentSheetCommonModule.kt index aaef2e31729..682c9a3a88a 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/injection/PaymentSheetCommonModule.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/injection/PaymentSheetCommonModule.kt @@ -45,6 +45,7 @@ import com.stripe.android.paymentsheet.addresselement.analytics.DefaultAddressLa import com.stripe.android.paymentsheet.analytics.DefaultEventReporter import com.stripe.android.paymentsheet.analytics.EventReporter import com.stripe.android.paymentsheet.analytics.LoadingEventReporter +import com.stripe.android.paymentsheet.analytics.PaymentElementAnalytics import com.stripe.android.paymentsheet.flowcontroller.DefaultPaymentSelectionUpdater import com.stripe.android.paymentsheet.flowcontroller.PaymentSelectionUpdater import com.stripe.android.paymentsheet.paymentdatacollection.cvcrecollection.CvcRecollectionInteractor @@ -194,6 +195,12 @@ internal abstract class PaymentSheetCommonModule { @Suppress("TooManyFunctions") companion object { + @Provides + @PaymentElementAnalytics + fun provideEventReporterAnalyticsRequestFactory( + factory: PaymentAnalyticsRequestFactory, + ): PaymentAnalyticsRequestFactory = factory + @Provides @Singleton fun providesLinkAccountHolder(savedStateHandle: SavedStateHandle): LinkAccountHolder { diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/state/DefaultAnalyticsMetadataFactory.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/state/DefaultAnalyticsMetadataFactory.kt index 67dc3733c90..03347f3189d 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/state/DefaultAnalyticsMetadataFactory.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/state/DefaultAnalyticsMetadataFactory.kt @@ -84,6 +84,9 @@ internal class DefaultAnalyticsMetadataFactory @Inject constructor( put("is_decoupled", SimpleBoolean(integrationMetadata.isDeferred())) put("is_spt", SimpleBoolean(integrationMetadata.isSpt())) put("is_confirmation_tokens", SimpleBoolean(integrationMetadata.isConfirmationTokens())) + if (integrationMetadata is IntegrationMetadata.CheckoutSession) { + put("checkout_session_id", SimpleString(integrationMetadata.id)) + } } private fun intent( diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/state/PaymentElementLoader.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/state/PaymentElementLoader.kt index 4da0d45e07b..9c2af176f0e 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/state/PaymentElementLoader.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/state/PaymentElementLoader.kt @@ -312,8 +312,13 @@ internal class DefaultPaymentElementLoader @Inject constructor( integrationConfiguration: PaymentElementLoader.Configuration, metadata: PaymentElementLoader.Metadata, ): Result = runWithApiConfiguration( - integrationConfiguration.commonConfiguration + configuration = integrationConfiguration.commonConfiguration, + reportLoadingAnalytics = + initializationMode !is PaymentElementLoader.InitializationMode.CheckoutSession, ) { configuration, apiConfiguration -> + val reportLoadingAnalytics = + initializationMode !is PaymentElementLoader.InitializationMode.CheckoutSession + // Validate configuration before loading initializationMode.validate() configuration.validate( @@ -323,7 +328,9 @@ internal class DefaultPaymentElementLoader @Inject constructor( isTapToAddSupported = tapToAddConnectionStarter.isSupported(apiConfiguration), ) - eventReporter.onLoadStarted(metadata.initializedViaCompose, apiConfiguration.publishableKey) + if (reportLoadingAnalytics) { + eventReporter.onLoadStarted(metadata.initializedViaCompose, apiConfiguration.publishableKey) + } tapToAddConnectionStarter.start( configuration, apiConfiguration, @@ -487,6 +494,7 @@ internal class DefaultPaymentElementLoader @Inject constructor( isReloadingAfterProcessDeath = metadata.isReloadingAfterProcessDeath, paymentMethodMetadata = state.paymentMethodMetadata, publishableKey = apiConfiguration.publishableKey, + reportLoadingAnalytics = reportLoadingAnalytics, ) state @@ -494,11 +502,18 @@ internal class DefaultPaymentElementLoader @Inject constructor( private suspend fun runWithApiConfiguration( configuration: CommonConfiguration, + reportLoadingAnalytics: Boolean, block: suspend CoroutineScope.(CommonConfiguration, ApiConfiguration.State) -> PaymentElementLoader.State, ): Result { val apiConfiguration = apiConfigurationResolver.resolve(configuration.apiConfiguration) return workContext.runCatching( - onFailure = { error -> reportFailedLoad(error, apiConfiguration.publishableKey) }, + onFailure = { error -> + reportFailedLoad( + error = error, + publishableKey = apiConfiguration.publishableKey, + reportLoadingAnalytics = reportLoadingAnalytics, + ) + }, task = { block(configuration, apiConfiguration) }, ) } @@ -880,29 +895,39 @@ internal class DefaultPaymentElementLoader @Inject constructor( isReloadingAfterProcessDeath: Boolean, paymentMethodMetadata: PaymentMethodMetadata, publishableKey: String, + reportLoadingAnalytics: Boolean, ) { - elementsSession.sessionsError?.let { sessionsError -> - eventReporter.onElementsSessionLoadFailed(sessionsError) + if (reportLoadingAnalytics) { + elementsSession.sessionsError?.let { sessionsError -> + eventReporter.onElementsSessionLoadFailed(sessionsError) + } } val treatValidationErrorAsFailure = !state.stripeIntent.isConfirmed || isReloadingAfterProcessDeath if (state.validationError != null && treatValidationErrorAsFailure) { - eventReporter.onLoadFailed(state.validationError, publishableKey) - } else { + if (reportLoadingAnalytics) { + eventReporter.onLoadFailed(state.validationError, publishableKey) + } + } else if (reportLoadingAnalytics) { eventReporter.onLoadSucceeded( paymentSelection = state.paymentSelection, paymentMethodMetadata = paymentMethodMetadata, ) + } else { + durationProvider.start(DurationProvider.Key.Checkout) } } private fun reportFailedLoad( error: Throwable, publishableKey: String, + reportLoadingAnalytics: Boolean, ) { logger.error("Failure loading PaymentSheetState", error) - eventReporter.onLoadFailed(error, publishableKey) + if (reportLoadingAnalytics) { + eventReporter.onLoadFailed(error, publishableKey) + } } private fun logIfMissingExternalPaymentMethods( diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/analytics/DefaultEventReporterTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/analytics/DefaultEventReporterTest.kt index 0af4e334f84..20ff0919f45 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/analytics/DefaultEventReporterTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/analytics/DefaultEventReporterTest.kt @@ -14,6 +14,7 @@ import com.stripe.android.core.utils.UserFacingLogger import com.stripe.android.link.ui.LinkButtonState import com.stripe.android.link.ui.inline.UserInput import com.stripe.android.lpmfoundations.paymentmethod.AnalyticsMetadata +import com.stripe.android.lpmfoundations.paymentmethod.IntegrationMetadata import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadataFactory import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadataFixtures.DEFAULT_API_CONFIG @@ -33,9 +34,14 @@ import com.stripe.android.paymentelement.confirmation.intent.DeferredIntentConfi import com.stripe.android.paymentsheet.model.GooglePayButtonType import com.stripe.android.paymentsheet.model.PaymentSelection import com.stripe.android.paymentsheet.paymentdatacollection.ach.USBankAccountFormViewModel +import com.stripe.android.paymentsheet.repositories.CheckoutSessionResponseFactory import com.stripe.android.paymentsheet.state.WalletsState +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.jsonObject import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @@ -54,37 +60,47 @@ class DefaultEventReporterTest { mapOf("example_from_test" to AnalyticsMetadata.Value.SimpleBoolean(true)) ) ) + private val checkoutPaymentMethodMetadata = paymentMethodMetadataWithTestAnalyticsMetadata.copy( + integrationMetadata = IntegrationMetadata.CheckoutSession( + id = "cs_test_123", + instancesKey = "checkout_instances", + checkoutSessionResponse = CheckoutSessionResponseFactory.create(id = "cs_test_123"), + collectedEmail = null, + ), + analyticsMetadata = AnalyticsMetadata( + mapOf( + "example_from_test" to AnalyticsMetadata.Value.SimpleBoolean(true), + "checkout_session_id" to AnalyticsMetadata.Value.SimpleString("cs_test_123"), + ) + ), + apiConfiguration = DEFAULT_API_CONFIG.copy(publishableKey = "pk_test_checkout_metadata"), + ) @Test fun `onExperimentExposure enqueues V2 event`() = runScenario { paymentMethodMetadataStack.push(null) - eventReporter.onExperimentExposure( - LoggableExperiment.LinkHoldback( - arbId = "arb_123", - group = "treatment", - experiment = ExperimentAssignment.LINK_GLOBAL_HOLD_BACK, - isReturningLinkUser = false, - useLinkNative = true, - emailRecognitionSource = null, - providedDefaultValues = LoggableExperiment.LinkHoldback.ProvidedDefaultValues( - email = false, - name = false, - phone = false, - ), - spmEnabled = false, - integrationShape = "payment_sheet", - linkDisplayed = true, - elementsSessionId = "elements_session_123", - mobileSdkVersion = "1.0.0", - mobileSessionId = "mobile_session_123", - ) - ) + eventReporter.onExperimentExposure(createLinkHoldbackExperiment()) val request = analyticsRequestV2Executor.enqueueCalls.awaitItem() assertThat(request.eventName).isEqualTo("elements.experiment_exposure") } + @Test + fun `onExperimentExposure includes Checkout metadata without product usage in V2 event`() = runScenario( + defaultProductUsageTokens = setOf("Checkout"), + ) { + paymentMethodMetadataStack.push(checkoutPaymentMethodMetadata) + + eventReporter.onExperimentExposure(createLinkHoldbackExperiment()) + + val request = analyticsRequestV2Executor.enqueueCalls.awaitItem() + assertThat(request.eventName).isEqualTo("elements.experiment_exposure") + assertThat(request.params.jsonObject).containsKey("example_from_test") + assertThat(request.params.jsonObject).containsKey("checkout_session_id") + assertThat(request.params.jsonObject).doesNotContainKey("product_usage") + } + @Test fun `onLoadStarted fires event`() = runScenario { durationProvider.startCalls.push( @@ -1359,18 +1375,80 @@ class DefaultEventReporterTest { } @Test - fun `onAnalyticsEvent fires event`() = runScenario { - paymentMethodMetadataStack.push(paymentMethodMetadataWithTestAnalyticsMetadata) + fun `onAnalyticsEvent snapshots checkout metadata and API key before dispatch`() = runScenario( + defaultProductUsageTokens = setOf("PaymentElement"), + workContextFactory = { scheduler -> StandardTestDispatcher(scheduler) }, + ) { + paymentMethodMetadataStack.push(checkoutPaymentMethodMetadata) val testEvent = object : AnalyticsEvent { override val eventName: String = "test_analytics_event" } eventReporter.onAnalyticsEvent(testEvent) + val changedMetadata = paymentMethodMetadataWithTestAnalyticsMetadata.copy( + apiConfiguration = DEFAULT_API_CONFIG.copy(publishableKey = "pk_test_changed_metadata"), + analyticsMetadata = AnalyticsMetadata( + mapOf("changed_metadata" to AnalyticsMetadata.Value.SimpleBoolean(true)) + ), + ) + paymentMethodMetadataStack.push(changedMetadata) + testScheduler.advanceUntilIdle() val request = analyticsRequestExecutor.requestTurbine.awaitItem() assertThat(request.params).containsEntry("event", "test_analytics_event") assertThat(request.params).containsEntry("example_from_test", true) + assertThat(request.params).containsEntry("product_usage", "PaymentElement") + assertThat(request.params).containsEntry("checkout_session_id", "cs_test_123") + assertThat(request.params).containsEntry("publishable_key", "pk_test_checkout_metadata") + assertThat(request.params).doesNotContainKey("changed_metadata") + assertThat(paymentMethodMetadataStack.pop()).isSameInstanceAs(changedMetadata) + } + + @Test + fun `checkout UI events use PaymentElement factory usage and metadata context`() = runScenario( + defaultProductUsageTokens = setOf("PaymentElement"), + ) { + paymentMethodMetadataStack.push(checkoutPaymentMethodMetadata) + + eventReporter.onDismiss() + + val request = analyticsRequestExecutor.requestTurbine.awaitItem() + assertThat(request.params).containsEntry("event", "mc_dismiss") + assertThat(request.params).containsEntry("product_usage", "PaymentElement") + assertThat(request.params).containsEntry("checkout_session_id", "cs_test_123") + assertThat(request.params).containsEntry("example_from_test", true) + assertThat(request.params).containsEntry("payment_method_orientation", "horizontal") + } + + @Test + fun `Embedded UI event retains Embedded product usage`() = runScenario( + mode = EventReporter.Mode.Embedded, + defaultProductUsageTokens = setOf("EmbeddedPaymentElement"), + ) { + paymentMethodMetadataStack.push(paymentMethodMetadataWithTestAnalyticsMetadata) + + eventReporter.onShowManageSavedPaymentMethods() + + val request = analyticsRequestExecutor.requestTurbine.awaitItem() + assertThat(request.params).containsEntry("event", "mc_embedded_manage_savedpm_show") + assertThat(request.params).containsEntry("product_usage", "EmbeddedPaymentElement") + assertThat(request.params).doesNotContainKey("checkout_session_id") + } + + @Test + fun `FlowController event retains FlowController product usage`() = runScenario( + mode = EventReporter.Mode.Custom, + defaultProductUsageTokens = setOf("PaymentSheet.FlowController"), + ) { + paymentMethodMetadataStack.push(paymentMethodMetadataWithTestAnalyticsMetadata) + + eventReporter.onShowManageSavedPaymentMethods() + + val request = analyticsRequestExecutor.requestTurbine.awaitItem() + assertThat(request.params).containsEntry("event", "mc_custom_manage_savedpm_show") + assertThat(request.params).containsEntry("product_usage", "PaymentSheet.FlowController") + assertThat(request.params).doesNotContainKey("checkout_session_id") } @Test @@ -1511,18 +1589,43 @@ class DefaultEventReporterTest { assertThat(request.params).containsEntry("publishable_key", publishableKey) } + private fun createLinkHoldbackExperiment() = LoggableExperiment.LinkHoldback( + arbId = "arb_123", + group = "treatment", + experiment = ExperimentAssignment.LINK_GLOBAL_HOLD_BACK, + isReturningLinkUser = false, + useLinkNative = true, + emailRecognitionSource = null, + providedDefaultValues = LoggableExperiment.LinkHoldback.ProvidedDefaultValues( + email = false, + name = false, + phone = false, + ), + spmEnabled = false, + integrationShape = "payment_sheet", + linkDisplayed = true, + elementsSessionId = "elements_session_123", + mobileSdkVersion = "1.0.0", + mobileSessionId = "mobile_session_123", + ) + private fun runScenario( throwInAnalyticsCallback: Boolean = false, + mode: EventReporter.Mode = EventReporter.Mode.Complete, + defaultProductUsageTokens: Set = setOf(""), + workContextFactory: (TestCoroutineScheduler) -> TestDispatcher = { scheduler -> + UnconfinedTestDispatcher(scheduler) + }, block: suspend Scenario.() -> Unit ) = runTest { val context = ApplicationProvider.getApplicationContext() - val testDispatcher = UnconfinedTestDispatcher() + val testDispatcher = workContextFactory(testScheduler) val analyticsRequestExecutor = FakeAnalyticsRequestExecutor() val analyticsRequestV2Executor = FakeAnalyticsRequestV2Executor() val paymentAnalyticsRequestFactory = PaymentAnalyticsRequestFactory( context = context, publishableKey = DEFAULT_API_CONFIG.publishableKey, - defaultProductUsageTokens = setOf(""), + defaultProductUsageTokens = defaultProductUsageTokens, ) val durationProvider = FakeDurationProvider() @@ -1547,7 +1650,7 @@ class DefaultEventReporterTest { val eventReporter = DefaultEventReporter( context = context, - mode = EventReporter.Mode.Complete, + mode = mode, analyticsRequestExecutor = analyticsRequestExecutor, analyticsRequestV2Executor = analyticsRequestV2Executor, paymentAnalyticsRequestFactory = paymentAnalyticsRequestFactory, @@ -1566,6 +1669,7 @@ class DefaultEventReporterTest { paymentMethodMetadataStack = paymentMethodMetadataStack, analyticsEventTurbine = analyticsEventTurbine, userFacingLoggerTurbine = userFacingLoggerTurbine, + testScheduler = testScheduler, ) block(scenario) @@ -1586,6 +1690,7 @@ class DefaultEventReporterTest { val paymentMethodMetadataStack: Stack, val analyticsEventTurbine: Turbine, val userFacingLoggerTurbine: Turbine, + val testScheduler: TestCoroutineScheduler, ) private class FakeDurationProvider : DurationProvider { diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/analytics/PaymentElementAnalyticsRequestFactoryTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/analytics/PaymentElementAnalyticsRequestFactoryTest.kt new file mode 100644 index 00000000000..2cb97813225 --- /dev/null +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/analytics/PaymentElementAnalyticsRequestFactoryTest.kt @@ -0,0 +1,114 @@ +package com.stripe.android.paymentsheet.analytics + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import app.cash.turbine.Turbine +import com.google.common.truth.Truth.assertThat +import com.stripe.android.checkout.injection.CheckoutModule +import com.stripe.android.core.ApiConfiguration +import com.stripe.android.core.networking.AnalyticsEvent +import com.stripe.android.lpmfoundations.paymentmethod.IntegrationMetadata +import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata +import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadataFactory +import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadataFixtures.DEFAULT_API_CONFIG +import com.stripe.android.networking.PaymentAnalyticsRequestFactory +import com.stripe.android.paymentelement.embedded.EmbeddedActivityModule +import com.stripe.android.paymentsheet.repositories.CheckoutSessionResponseFactory +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import javax.inject.Provider + +@RunWith(RobolectricTestRunner::class) +class PaymentElementAnalyticsRequestFactoryTest { + + @Test + fun `Checkout factory defaults UI events to PaymentElement and resolves API configuration lazily`() = runTest { + val apiConfigurationProvider = FakeApiConfigurationProvider(DEFAULT_API_CONFIG) + val factory = CheckoutModule.provideEventReporterAnalyticsRequestFactory( + context = ApplicationProvider.getApplicationContext(), + apiConfigurationProvider = apiConfigurationProvider, + ) + + apiConfigurationProvider.getCalls.expectNoEvents() + + val request = createRequest(factory) + + assertThat(request.params).containsEntry("product_usage", "PaymentElement") + assertThat(request.params).containsEntry("publishable_key", DEFAULT_API_CONFIG.publishableKey) + apiConfigurationProvider.getCalls.awaitItem() + apiConfigurationProvider.ensureAllEventsConsumed() + } + + @Test + fun `Embedded factory uses PaymentElement for CheckoutSession metadata`() = runTest { + val apiConfigurationProvider = FakeApiConfigurationProvider(DEFAULT_API_CONFIG) + val factory = EmbeddedActivityModule.provideEventReporterAnalyticsRequestFactory( + context = ApplicationProvider.getApplicationContext(), + paymentMethodMetadata = checkoutPaymentMethodMetadata("cs_test_embedded"), + productUsage = setOf("EmbeddedPaymentElement"), + apiConfigurationProvider = apiConfigurationProvider, + ) + + val request = createRequest(factory) + + assertThat(request.params).containsEntry("product_usage", "PaymentElement") + apiConfigurationProvider.getCalls.awaitItem() + apiConfigurationProvider.ensureAllEventsConsumed() + } + + @Test + fun `Embedded factory preserves existing L3 product usage outside CheckoutSession`() = runTest { + val apiConfigurationProvider = FakeApiConfigurationProvider(DEFAULT_API_CONFIG) + val factory = EmbeddedActivityModule.provideEventReporterAnalyticsRequestFactory( + context = ApplicationProvider.getApplicationContext(), + paymentMethodMetadata = PaymentMethodMetadataFactory.create(), + productUsage = setOf("EmbeddedPaymentElement"), + apiConfigurationProvider = apiConfigurationProvider, + ) + + val request = createRequest(factory) + + assertThat(request.params).containsEntry("product_usage", "EmbeddedPaymentElement") + apiConfigurationProvider.getCalls.awaitItem() + apiConfigurationProvider.ensureAllEventsConsumed() + } + + private fun createRequest(factory: PaymentAnalyticsRequestFactory) = factory.createRequest( + event = TestEvent, + additionalParams = emptyMap(), + publishableKeyOverride = null, + ) + + private fun checkoutPaymentMethodMetadata(sessionId: String): PaymentMethodMetadata { + val response = CheckoutSessionResponseFactory.create(id = sessionId) + return PaymentMethodMetadataFactory.create( + integrationMetadata = IntegrationMetadata.CheckoutSession( + id = response.id, + instancesKey = "PaymentElementAnalyticsRequestFactoryTest", + checkoutSessionResponse = response, + collectedEmail = null, + ), + ) + } + + private object TestEvent : AnalyticsEvent { + override val eventName: String = "payment_element_analytics_factory_test" + } + + private class FakeApiConfigurationProvider( + private val apiConfiguration: ApiConfiguration.State, + ) : Provider { + val getCalls = Turbine() + + override fun get(): ApiConfiguration.State { + getCalls.add(Unit) + return apiConfiguration + } + + fun ensureAllEventsConsumed() { + getCalls.ensureAllEventsConsumed() + } + } +} diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/state/DefaultAnalyticsMetadataFactoryTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/state/DefaultAnalyticsMetadataFactoryTest.kt index 80136c687b8..9573a1a8542 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/state/DefaultAnalyticsMetadataFactoryTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/state/DefaultAnalyticsMetadataFactoryTest.kt @@ -37,6 +37,8 @@ import com.stripe.android.paymentsheet.PaymentSheet import com.stripe.android.paymentsheet.analytics.EventReporter import com.stripe.android.paymentsheet.cvcrecollection.CvcRecollectionHandler import com.stripe.android.paymentsheet.cvcrecollection.CvcRecollectionHandlerImpl +import com.stripe.android.paymentsheet.repositories.CheckoutSessionResponse +import com.stripe.android.paymentsheet.repositories.CheckoutSessionResponseFactory import com.stripe.android.paymentsheet.state.PaymentElementLoader.InitializationMode import org.junit.Test import javax.inject.Provider @@ -134,6 +136,50 @@ class DefaultAnalyticsMetadataFactoryTest { assertThat(resultMap["is_confirmation_tokens"]).isEqualTo(false) } + @Test + fun `create includes only checkout session id from session metadata`() = runScenario { + val response = CheckoutSessionResponseFactory.create( + id = "cs_test_123", + configId = "cfg_checkout_123", + currency = "cad", + paymentStatus = CheckoutSessionResponse.PaymentStatus.PAID, + status = CheckoutSessionResponse.Status.COMPLETE, + elementsSession = createElementsSession().copy(elementsSessionConfigId = "elements_config_123"), + adaptivePricingInfo = CheckoutSessionResponse.AdaptivePricingInfo( + activePresentmentCurrency = "eur", + integrationAmount = 1000, + integrationCurrency = "cad", + localCurrencyOptions = emptyList(), + ), + automaticTaxEnabled = true, + ) + val initializationMode = InitializationMode.CheckoutSession( + instancesKey = "checkout_instances", + checkoutSessionResponse = response, + collectedEmail = null, + ) + val resultMap = createAnalyticsMetadata( + initializationMode = initializationMode, + integrationMetadata = IntegrationMetadata.CheckoutSession( + id = response.id, + instancesKey = "checkout_instances", + checkoutSessionResponse = response, + collectedEmail = null, + ), + elementsSession = requireNotNull(response.elementsSession), + ) + + assertThat(resultMap).containsEntry("checkout_session_id", "cs_test_123") + assertThat(resultMap).doesNotContainKey("checkout_config_id") + assertThat(resultMap).doesNotContainKey("session_status") + assertThat(resultMap).doesNotContainKey("payment_status") + assertThat(resultMap).doesNotContainKey("elements_session_config_id") + assertThat(resultMap).doesNotContainKey("adaptive_pricing_active") + assertThat(resultMap).doesNotContainKey("automatic_tax_enabled") + assertThat(resultMap).doesNotContainKey("request_id") + assertThat(resultMap).doesNotContainKey("product_usage") + } + @Test fun `create returns expected values for setup intent`() = runScenario { val clientSecret = SetupIntentFixtures.SI_REQUIRES_PAYMENT_METHOD.clientSecret!! diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/state/DefaultPaymentElementLoaderTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/state/DefaultPaymentElementLoaderTest.kt index 7d3c56fab0b..8c7acaa3f25 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/state/DefaultPaymentElementLoaderTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/state/DefaultPaymentElementLoaderTest.kt @@ -283,7 +283,8 @@ internal class DefaultPaymentElementLoaderTest { @Test fun `load with CheckoutSession mode and non-null customer returns failure`() = runScenario { - val result = createPaymentElementLoader().load( + val durationProvider = FakeDurationProvider() + val result = createPaymentElementLoader(durationProvider = durationProvider).load( initializationMode = PaymentElementLoader.InitializationMode.CheckoutSession( collectedEmail = null, instancesKey = "DefaultPaymentElementLoaderTest", @@ -307,7 +308,8 @@ internal class DefaultPaymentElementLoaderTest { "Customer information is provided by the checkout session." ) - assertThat(eventReporter.loadFailedTurbine.awaitItem()).isNotNull() + expectNoLoadingEvents() + assertThat(durationProvider.has(FakeDurationProvider.Call.Start(DurationProvider.Key.Checkout, true))).isFalse() } @Test @@ -361,6 +363,27 @@ internal class DefaultPaymentElementLoaderTest { assertThat(eventReporter.loadSucceededTurbine.awaitItem()).isNotNull() } + @Test + fun `CheckoutSession Express Checkout Element load does not report loading analytics`() = runScenario { + val durationProvider = FakeDurationProvider() + val result = createPaymentElementLoader(durationProvider = durationProvider).load( + initializationMode = PaymentElementLoader.InitializationMode.CheckoutSession( + collectedEmail = null, + instancesKey = "DefaultPaymentElementLoaderTest", + checkoutSessionResponse = createCheckoutSessionResponse(canDetachPaymentMethod = true), + ), + integrationConfiguration = PaymentElementLoader.Configuration.ExpressCheckoutElement( + commonConfiguration = PaymentSheet.Configuration("Some name").asCommonConfiguration(), + ), + metadata = PaymentElementLoader.Metadata(initializedViaCompose = false), + ) + + assertThat(result.isSuccess).isTrue() + consumeLoadingEvents() + expectNoLoadingEvents() + assertThat(durationProvider.has(FakeDurationProvider.Call.Start(DurationProvider.Key.Checkout, true))).isTrue() + } + @Test fun `load with checkout session automatic tax billing and default billing details keeps google pay`() = runScenario { val userFacingLogger = FakeUserFacingLogger() @@ -400,8 +423,7 @@ internal class DefaultPaymentElementLoaderTest { consumeLoadingEvents() - assertThat(eventReporter.loadStartedTurbine.awaitItem()).isNotNull() - assertThat(eventReporter.loadSucceededTurbine.awaitItem()).isNotNull() + expectNoLoadingEvents() } @Test @@ -477,8 +499,7 @@ internal class DefaultPaymentElementLoaderTest { consumeLoadingEvents() - assertThat(eventReporter.loadStartedTurbine.awaitItem()).isNotNull() - assertThat(eventReporter.loadSucceededTurbine.awaitItem()).isNotNull() + expectNoLoadingEvents() } @Test @@ -3192,8 +3213,7 @@ internal class DefaultPaymentElementLoaderTest { assertThat(state.paymentMethodMetadata.customerMetadata?.canRemoveLastPaymentMethod) .isEqualTo(true) - assertThat(eventReporter.loadStartedTurbine.awaitItem()).isNotNull() - assertThat(eventReporter.loadSucceededTurbine.awaitItem()).isNotNull() + expectNoLoadingEvents() } } @@ -4984,6 +5004,13 @@ internal class DefaultPaymentElementLoaderTest { customerRepository.retrieveCustomerRequests.cancelAndIgnoreRemainingEvents() } + private suspend fun Scenario.expectNoLoadingEvents() { + eventReporter.loadStartedTurbine.expectNoEvents() + eventReporter.loadSucceededTurbine.expectNoEvents() + eventReporter.loadFailedTurbine.expectNoEvents() + eventReporter.elementsSessionLoadFailedTurbine.expectNoEvents() + } + private fun Scenario.createPaymentElementLoader( isGooglePayReady: Boolean = true, stripeIntent: StripeIntent = PaymentIntentFixtures.PI_REQUIRES_PAYMENT_METHOD,