diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/core/AnalyticsClient.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/core/AnalyticsClient.kt index 1e32b0741d..517fd69540 100644 --- a/BraintreeCore/src/main/java/com/braintreepayments/api/core/AnalyticsClient.kt +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/core/AnalyticsClient.kt @@ -218,6 +218,9 @@ internal class AnalyticsClient( .putOpt(FPTI_KEY_START_TIME, event.startTime) .putOpt(FPTI_KEY_END_TIME, event.endTime) .putOpt(FPTI_KEY_ENDPOINT, event.endpoint) + .putOpt(FPTI_KEY_MERCHANT_EXPERIMENT, event.experiment) + .putOpt(FPTI_KEY_MERCHANT_PAYMENT_METHODS_DISPLAYED, + event.paymentMethodsDisplayed.ifEmpty { null }) return json.toString() } @@ -265,6 +268,8 @@ internal class AnalyticsClient( private const val FPTI_KEY_START_TIME = "start_time" private const val FPTI_KEY_END_TIME = "end_time" private const val FPTI_KEY_ENDPOINT = "endpoint" + private const val FPTI_KEY_MERCHANT_EXPERIMENT = "experiment" + private const val FPTI_KEY_MERCHANT_PAYMENT_METHODS_DISPLAYED = "payment_methods_displayed" private const val FPTI_BATCH_KEY_VENMO_INSTALLED = "venmo_installed" private const val FPTI_BATCH_KEY_PAYPAL_INSTALLED = "paypal_installed" diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/core/AnalyticsEvent.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/core/AnalyticsEvent.kt index 689eede1bc..ba56b2a6ff 100644 --- a/BraintreeCore/src/main/java/com/braintreepayments/api/core/AnalyticsEvent.kt +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/core/AnalyticsEvent.kt @@ -1,5 +1,10 @@ package com.braintreepayments.api.core +/** + * DTO for analytics events. See also: [AnalyticsEventParams] + * This class is internal to core module and is used in [AnalyticsClient] to construct the analytics + * payload to be sent to the backend. + */ internal data class AnalyticsEvent( val name: String, val timestamp: Long, @@ -8,5 +13,7 @@ internal data class AnalyticsEvent( val isVaultRequest: Boolean = false, val startTime: Long? = null, val endTime: Long? = null, - val endpoint: String? = null + val endpoint: String? = null, + val experiment: String? = null, + val paymentMethodsDisplayed: List = emptyList() ) diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/core/AnalyticsEventParams.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/core/AnalyticsEventParams.kt index 92f70d3aa5..a8a9cd4605 100644 --- a/BraintreeCore/src/main/java/com/braintreepayments/api/core/AnalyticsEventParams.kt +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/core/AnalyticsEventParams.kt @@ -2,6 +2,22 @@ package com.braintreepayments.api.core import androidx.annotation.RestrictTo +/** + * DTO for analytics events. See also: [AnalyticsEvent] + * It is a catch-all data class for any parameters any of the modules wants to send. As such, not + * all parameters are required at each call site. + * + * @property payPalContextId Used for linking events from the client to server side request. + * @property linkType Indicates whether a deeplink or an app link was used to launch the app. Also see [LinkType]. + * @property isVaultRequest Indicates whether the request was a BillingAgreement(BA)/Vault request. + * @property startTime [HttpResponseTiming] start time. + * @property endTime [HttpResponseTiming] end time. + * @property endpoint The endpoint being called. + * @property experiment Currently a ShopperInsights module specific event that indicates + * the experiment, as a JSON string, that the merchant sent to the us. + * @property paymentMethodsDisplayed A ShopperInsights module specific event that indicates the + * order of payment methods displayed to the shopper by the merchant. + */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) data class AnalyticsEventParams @JvmOverloads constructor( var payPalContextId: String? = null, @@ -9,5 +25,7 @@ data class AnalyticsEventParams @JvmOverloads constructor( var isVaultRequest: Boolean = false, var startTime: Long? = null, var endTime: Long? = null, - var endpoint: String? = null + var endpoint: String? = null, + val experiment: String? = null, + val paymentMethodsDisplayed: List = emptyList() ) diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/core/BraintreeClient.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/core/BraintreeClient.kt index f7e58e4e2f..4cdfc055be 100644 --- a/BraintreeCore/src/main/java/com/braintreepayments/api/core/BraintreeClient.kt +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/core/BraintreeClient.kt @@ -149,6 +149,8 @@ class BraintreeClient @VisibleForTesting internal constructor( startTime = params.startTime, endTime = params.endTime, endpoint = params.endpoint, + experiment = params.experiment, + paymentMethodsDisplayed = params.paymentMethodsDisplayed ) sendAnalyticsEvent(event, configuration, authorization) } diff --git a/CHANGELOG.md b/CHANGELOG.md index f919ecc7b5..a805edfe62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Braintree Android SDK Release Notes +## unreleased + +* Shopper Insights (BETA) + * For analytics, send `experiment` as a parameter to `getRecommendedPaymentMethods` method + * For analytics, send `experiment` and `paymentMethodsDisplayed` analytic metrics to FPTI via the button presented event methods + ## 5.1.0 (2024-10-15) * PayPal diff --git a/Demo/src/main/java/com/braintreepayments/demo/ShopperInsightsFragment.kt b/Demo/src/main/java/com/braintreepayments/demo/ShopperInsightsFragment.kt index b24a38fca3..172a1f126e 100644 --- a/Demo/src/main/java/com/braintreepayments/demo/ShopperInsightsFragment.kt +++ b/Demo/src/main/java/com/braintreepayments/demo/ShopperInsightsFragment.kt @@ -184,18 +184,25 @@ class ShopperInsightsFragment : BaseFragment() { } shopperInsightsClient.getRecommendedPaymentMethods( - request + request, + "dummy_experiment" ) { result -> when (result) { is ShopperInsightsResult.Success -> { if (result.response.isPayPalRecommended) { payPalVaultButton.isEnabled = true - shopperInsightsClient.sendPayPalPresentedEvent() + shopperInsightsClient.sendPayPalPresentedEvent( + "dummy_paypal_presented_experiment", + listOf("PayPal, Apple Pay, Google Pay") + ) } if (result.response.isVenmoRecommended) { venmoButton.isEnabled = true - shopperInsightsClient.sendVenmoPresentedEvent() + shopperInsightsClient.sendVenmoPresentedEvent( + "dummy_venmo_presented_experiment", + listOf("Apple Pay, Venmo, Google Pay") + ) } responseTextView.text = diff --git a/ShopperInsights/src/main/java/com/braintreepayments/api/shopperinsights/ShopperInsightsClient.kt b/ShopperInsights/src/main/java/com/braintreepayments/api/shopperinsights/ShopperInsightsClient.kt index 324bf26b05..becd62b804 100644 --- a/ShopperInsights/src/main/java/com/braintreepayments/api/shopperinsights/ShopperInsightsClient.kt +++ b/ShopperInsights/src/main/java/com/braintreepayments/api/shopperinsights/ShopperInsightsClient.kt @@ -1,6 +1,7 @@ package com.braintreepayments.api.shopperinsights import android.content.Context +import com.braintreepayments.api.core.AnalyticsEventParams import com.braintreepayments.api.core.AnalyticsParamRepository import com.braintreepayments.api.core.BraintreeClient import com.braintreepayments.api.core.BraintreeException @@ -43,6 +44,7 @@ class ShopperInsightsClient internal constructor( * Retrieves recommended payment methods based on the provided shopper insights request. * * @param request The [ShopperInsightsRequest] containing information about the shopper. + * @param experiment optional JSON string representing an experiment you want to run * @return A [ShopperInsightsResult] object indicating the recommended payment methods. * Note: This feature is in beta. Its public API may change or be removed in future releases * PayPal recommendation is only available for US, AU, FR, DE, ITA, NED, ESP, Switzerland and @@ -50,10 +52,14 @@ class ShopperInsightsClient internal constructor( */ fun getRecommendedPaymentMethods( request: ShopperInsightsRequest, + experiment: String? = null, callback: ShopperInsightsCallback ) { analyticsParamRepository.resetSessionId() - braintreeClient.sendAnalyticsEvent(GET_RECOMMENDED_PAYMENTS_STARTED) + braintreeClient.sendAnalyticsEvent( + GET_RECOMMENDED_PAYMENTS_STARTED, + AnalyticsEventParams(experiment = experiment) + ) if (request.email == null && request.phone == null) { callbackFailure( @@ -153,9 +159,22 @@ class ShopperInsightsClient internal constructor( /** * Call this method when the PayPal button has been successfully displayed to the buyer. * This method sends analytics to help improve the Shopper Insights feature experience. + * + * @param experiment optional JSON string representing an experiment you want to run + * @param paymentMethodsDisplayed optional The list of available payment methods, + * rendered in the same order in which they are displayed */ - fun sendPayPalPresentedEvent() { - braintreeClient.sendAnalyticsEvent(PAYPAL_PRESENTED) + fun sendPayPalPresentedEvent( + experiment: String? = null, + paymentMethodsDisplayed: List = emptyList() + ) { + braintreeClient.sendAnalyticsEvent( + PAYPAL_PRESENTED, + AnalyticsEventParams( + experiment = experiment, + paymentMethodsDisplayed = paymentMethodsDisplayed + ) + ) } /** @@ -169,9 +188,22 @@ class ShopperInsightsClient internal constructor( /** * Call this method when the Venmo button has been successfully displayed to the buyer. * This method sends analytics to help improve the Shopper Insights feature experience. + * + * @param experiment optional JSON string representing an experiment you want to run + * @param paymentMethodsDisplayed optional The list of available payment methods, + * rendered in the same order in which they are displayed */ - fun sendVenmoPresentedEvent() { - braintreeClient.sendAnalyticsEvent(VENMO_PRESENTED) + fun sendVenmoPresentedEvent( + experiment: String? = null, + paymentMethodsDisplayed: List = emptyList() + ) { + braintreeClient.sendAnalyticsEvent( + VENMO_PRESENTED, + AnalyticsEventParams( + experiment = experiment, + paymentMethodsDisplayed = paymentMethodsDisplayed + ) + ) } /** diff --git a/ShopperInsights/src/test/java/com/braintreepayments/api/shopperinsights/ShopperInsightsClientUnitTest.kt b/ShopperInsights/src/test/java/com/braintreepayments/api/shopperinsights/ShopperInsightsClientUnitTest.kt index 43b18c6801..ea6f81abc7 100644 --- a/ShopperInsights/src/test/java/com/braintreepayments/api/shopperinsights/ShopperInsightsClientUnitTest.kt +++ b/ShopperInsights/src/test/java/com/braintreepayments/api/shopperinsights/ShopperInsightsClientUnitTest.kt @@ -2,6 +2,7 @@ package com.braintreepayments.api.shopperinsights import android.content.Context import androidx.test.core.app.ApplicationProvider +import com.braintreepayments.api.core.AnalyticsEventParams import com.braintreepayments.api.core.AnalyticsParamRepository import com.braintreepayments.api.core.Authorization import com.braintreepayments.api.core.BraintreeClient @@ -51,23 +52,24 @@ class ShopperInsightsClientUnitTest { @Test fun `when getRecommendedPaymentMethods is called, session id is reset`() { - sut.getRecommendedPaymentMethods(mockk(relaxed = true), mockk(relaxed = true)) + sut.getRecommendedPaymentMethods(mockk(relaxed = true), "some_experiment", mockk(relaxed = true)) verify { analyticsParamRepository.resetSessionId() } } @Test fun `when getRecommendedPaymentMethods is called, started event is sent`() { - sut.getRecommendedPaymentMethods(mockk(relaxed = true), mockk(relaxed = true)) + val experiment = "some_experiment" + sut.getRecommendedPaymentMethods(mockk(relaxed = true), experiment, mockk(relaxed = true)) - verifyStartedAnalyticsEvent() + verifyStartedAnalyticsEvent(AnalyticsEventParams(experiment = experiment)) } @Test fun `when getRecommendedPaymentMethods is called, failed event is sent`() { val request = ShopperInsightsRequest(null, null) - sut.getRecommendedPaymentMethods(request, mockk(relaxed = true)) + sut.getRecommendedPaymentMethods(request, "some_experiment", mockk(relaxed = true)) verifyFailedAnalyticsEvent() } @@ -442,15 +444,15 @@ class ShopperInsightsClientUnitTest { val apiCallbackSlot = slot() every { api.findEligiblePayments(any(), capture(apiCallbackSlot)) } just runs - sut.getRecommendedPaymentMethods(request, callback) + sut.getRecommendedPaymentMethods(request, "some_experiment", callback) apiCallbackSlot.captured.onResult(result = result, error = error) } - private fun verifyStartedAnalyticsEvent() { + private fun verifyStartedAnalyticsEvent(params: AnalyticsEventParams = AnalyticsEventParams()) { verify { braintreeClient - .sendAnalyticsEvent("shopper-insights:get-recommended-payments:started") + .sendAnalyticsEvent("shopper-insights:get-recommended-payments:started", params) } }