Skip to content
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

Shopper insights button analytics -- add new params #1187

Merged
merged 16 commits into from
Oct 17, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<String> = emptyList()
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,30 @@ package com.braintreepayments.api.core

import androidx.annotation.RestrictTo

/**
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tdchow does this look good?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good!

* 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,
var linkType: String? = null,
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<String> = emptyList()
)
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -43,17 +44,22 @@ 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
* UK merchants. Venmo recommendation is only available for US merchants.
*/
fun getRecommendedPaymentMethods(
request: ShopperInsightsRequest,
experiment: String? = null,
tdchow marked this conversation as resolved.
Show resolved Hide resolved
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(
Expand Down Expand Up @@ -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<String> = emptyList()
) {
braintreeClient.sendAnalyticsEvent(
PAYPAL_PRESENTED,
AnalyticsEventParams(
experiment = experiment,
paymentMethodsDisplayed = paymentMethodsDisplayed
)
)
}

/**
Expand All @@ -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<String> = emptyList()
) {
braintreeClient.sendAnalyticsEvent(
VENMO_PRESENTED,
AnalyticsEventParams(
experiment = experiment,
paymentMethodsDisplayed = paymentMethodsDisplayed
)
)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -442,15 +444,15 @@ class ShopperInsightsClientUnitTest {
val apiCallbackSlot = slot<EligiblePaymentsCallback>()
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)
}
}

Expand Down
Loading