From 80e71e5f44a9f93088f0f40c3903edd6da4c59b2 Mon Sep 17 00:00:00 2001 From: agedd <105314544+agedd@users.noreply.github.com> Date: Tue, 24 Sep 2024 14:26:59 -0500 Subject: [PATCH] Add Additional Parameters To ShopperInsights Button Analytics (#1415) * add additional analytic params to shopper insights flow for testing * address pr comments * address pr feedback * add analytics tests * cleanup * cleanup, add new param, and related unit test * cleanup docstrings * add changelog entry * cleanup * cleanup * cleanup * clarify docstrings * fix switflint warnings * code cleanup * address pr feedback * cleanup * address comments * resolve build failure --- CHANGELOG.md | 3 + .../ShopperInsightsViewController.swift | 24 +++++-- .../Analytics/FPTIBatchData.swift | 11 +++- Sources/BraintreeCore/BTAPIClient.swift | 4 ++ .../BTShopperInsightsClient.swift | 62 ++++++++++++++----- .../BTShopperInsightsClient_Tests.swift | 23 ++++++- .../BraintreeTestShared/MockAPIClient.swift | 6 ++ 7 files changed, 110 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed775e3156..4aa66e2841 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ ## unreleased * BraintreePayPal * Send `isVaultRequest` for App Switch events to PayPal's analytics service (FPTI) +* BraintreeShopperInsights (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 methods ## 6.23.3 (2024-08-12) * BraintreeCore diff --git a/Demo/Application/Features/ShopperInsightsViewController.swift b/Demo/Application/Features/ShopperInsightsViewController.swift index 1c127735fe..5e6a3787a4 100644 --- a/Demo/Application/Features/ShopperInsightsViewController.swift +++ b/Demo/Application/Features/ShopperInsightsViewController.swift @@ -67,9 +67,6 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController { stackView.distribution = .fillEqually stackView.translatesAutoresizingMaskIntoConstraints = false - shopperInsightsClient.sendPayPalPresentedEvent() - shopperInsightsClient.sendVenmoPresentedEvent() - return stackView } @@ -85,7 +82,15 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController { ) Task { do { - let result = try await shopperInsightsClient.getRecommendedPaymentMethods(request: request) + let sampleExperiment = + """ + [ + { "experimentName" : "payment ready conversion" }, + { "experimentID" : "a1b2c3" }, + { "treatmentName" : "control group 1" } + ] + """ + let result = try await shopperInsightsClient.getRecommendedPaymentMethods(request: request, experiment: sampleExperiment) // swiftlint:disable:next line_length progressBlock("PayPal Recommended: \(result.isPayPalRecommended)\nVenmo Recommended: \(result.isVenmoRecommended)\nEligible in PayPal Network: \(result.isEligibleInPayPalNetwork)") payPalVaultButton.isEnabled = result.isPayPalRecommended @@ -97,6 +102,16 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController { } @objc func payPalVaultButtonTapped(_ button: UIButton) { + let sampleExperiment = + """ + [ + { "experimentName" : "payment ready conversion experiment" }, + { "experimentID" : "a1b2c3" }, + { "treatmentName" : "treatment group 1" } + ] + """ + let paymentMethods = ["Apple Pay", "Card", "PayPal"] + shopperInsightsClient.sendPayPalPresentedEvent(paymentMethodsDisplayed: paymentMethods, experiment: sampleExperiment) progressBlock("Tapped PayPal Vault") shopperInsightsClient.sendPayPalSelectedEvent() @@ -113,6 +128,7 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController { } @objc func venmoButtonTapped(_ button: UIButton) { + shopperInsightsClient.sendVenmoPresentedEvent() progressBlock("Tapped Venmo") shopperInsightsClient.sendVenmoSelectedEvent() diff --git a/Sources/BraintreeCore/Analytics/FPTIBatchData.swift b/Sources/BraintreeCore/Analytics/FPTIBatchData.swift index 945dc08e05..64b55b6a07 100644 --- a/Sources/BraintreeCore/Analytics/FPTIBatchData.swift +++ b/Sources/BraintreeCore/Analytics/FPTIBatchData.swift @@ -30,7 +30,6 @@ struct FPTIBatchData: Codable { struct Event: Codable { /// UTC millisecond timestamp when a networking task started establishing a TCP connection. See [Apple's docs](https://developer.apple.com/documentation/foundation/urlsessiontasktransactionmetrics#3162615). - /// /// `nil` if a persistent connection is used. let connectionStartTime: Int? let correlationID: String? @@ -46,6 +45,10 @@ struct FPTIBatchData: Codable { let isVaultRequest: Bool? /// The type of link the SDK will be handling, currently deeplink or universal let linkType: String? + /// The experiment details associated with a shopper insights flow + let merchantExperiment: String? + /// The list of payment methods displayed, in the same order in which they are rendered on the page, associated with the `BTShopperInsights` flow. + let paymentMethodsDisplayed: String? /// Used for linking events from the client to server side request /// This value will be PayPal Order ID, Payment Token, EC token, Billing Agreement, or Venmo Context ID depending on the flow let payPalContextID: String? @@ -67,6 +70,8 @@ struct FPTIBatchData: Codable { isConfigFromCache: Bool? = nil, isVaultRequest: Bool? = nil, linkType: String? = nil, + merchantExperiment: String? = nil, + paymentMethodsDisplayed: String? = nil, payPalContextID: String? = nil, requestStartTime: Int? = nil, startTime: Int? = nil @@ -80,6 +85,8 @@ struct FPTIBatchData: Codable { self.isConfigFromCache = isConfigFromCache self.isVaultRequest = isVaultRequest self.linkType = linkType + self.merchantExperiment = merchantExperiment + self.paymentMethodsDisplayed = paymentMethodsDisplayed self.payPalContextID = payPalContextID self.requestStartTime = requestStartTime self.startTime = startTime @@ -93,6 +100,8 @@ struct FPTIBatchData: Codable { case isConfigFromCache = "config_cached" case isVaultRequest = "is_vault" case linkType = "link_type" + case merchantExperiment = "experiment" + case paymentMethodsDisplayed = "payment_methods_displayed" case payPalContextID = "paypal_context_id" case requestStartTime = "request_start_time" case timestamp = "t" diff --git a/Sources/BraintreeCore/BTAPIClient.swift b/Sources/BraintreeCore/BTAPIClient.swift index e3f6fb240e..5076de18d3 100644 --- a/Sources/BraintreeCore/BTAPIClient.swift +++ b/Sources/BraintreeCore/BTAPIClient.swift @@ -305,9 +305,11 @@ import Foundation _ eventName: String, correlationID: String? = nil, errorDescription: String? = nil, + merchantExperiment: String? = nil, isConfigFromCache: Bool? = nil, isVaultRequest: Bool? = nil, linkType: LinkType? = nil, + paymentMethodsDisplayed: String? = nil, payPalContextID: String? = nil ) { analyticsService.sendAnalyticsEvent( @@ -318,6 +320,8 @@ import Foundation isConfigFromCache: isConfigFromCache, isVaultRequest: isVaultRequest, linkType: linkType?.rawValue, + merchantExperiment: merchantExperiment, + paymentMethodsDisplayed: paymentMethodsDisplayed, payPalContextID: payPalContextID ) ) diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift index b7f875589d..e5dc83a6cc 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift @@ -27,16 +27,23 @@ public class BTShopperInsightsClient { /// This method confirms if the customer is a user of PayPal services using their email and phone number. /// - Parameters: - /// - request: A `BTShopperInsightsRequest` containing the buyer's user information + /// - request: Required: A `BTShopperInsightsRequest` containing the buyer's user information. + /// - experiment: Optional: A `JSONObject` passed in as a string containing details of the merchant experiment. /// - Returns: A `BTShopperInsightsResult` instance /// - Warning: 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. - public func getRecommendedPaymentMethods(request: BTShopperInsightsRequest) async throws -> BTShopperInsightsResult { - apiClient.sendAnalyticsEvent(BTShopperInsightsAnalytics.recommendedPaymentsStarted) + public func getRecommendedPaymentMethods( + request: BTShopperInsightsRequest, + experiment: String? = nil + ) async throws -> BTShopperInsightsResult { + apiClient.sendAnalyticsEvent( + BTShopperInsightsAnalytics.recommendedPaymentsStarted, + merchantExperiment: experiment + ) if apiClient.authorization.type != .clientToken { - throw notifyFailure(with: BTShopperInsightsError.invalidAuthorization) + throw notifyFailure(with: BTShopperInsightsError.invalidAuthorization, for: experiment) } let postParameters = BTEligiblePaymentsRequest( @@ -57,7 +64,7 @@ public class BTShopperInsightsClient { let eligibleMethodsJSON = json?["eligible_methods"].asDictionary(), eligibleMethodsJSON.count != 0 else { - throw self.notifyFailure(with: BTShopperInsightsError.emptyBodyReturned) + throw self.notifyFailure(with: BTShopperInsightsError.emptyBodyReturned, for: experiment) } // swiftlint:enable empty_count @@ -69,16 +76,24 @@ public class BTShopperInsightsClient { isVenmoRecommended: venmo?.recommended ?? false, isEligibleInPayPalNetwork: payPal?.eligibleInPayPalNetwork ?? false || venmo?.eligibleInPayPalNetwork ?? false ) - return self.notifySuccess(with: result) + return self.notifySuccess(with: result, for: experiment) } catch { - throw self.notifyFailure(with: error) + throw self.notifyFailure(with: error, for: experiment) } } /// 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. - public func sendPayPalPresentedEvent() { - apiClient.sendAnalyticsEvent(BTShopperInsightsAnalytics.payPalPresented) + /// - Parameters: + /// - paymentMethodsDisplayed: Optional: The list of available payment methods, rendered in the same order in which they are displayed i.e. ['Apple Pay', 'PayPal'] + /// - experiment: Optional: A `JSONObject` passed in as a string containing details of the merchant experiment. + public func sendPayPalPresentedEvent(paymentMethodsDisplayed: [String?] = [], experiment: String? = nil) { + let paymentMethodsDisplayedString = paymentMethodsDisplayed.compactMap { $0 }.joined(separator: ", ") + apiClient.sendAnalyticsEvent( + BTShopperInsightsAnalytics.payPalPresented, + merchantExperiment: experiment, + paymentMethodsDisplayed: paymentMethodsDisplayedString + ) } /// Call this method when the PayPal button has been selected/tapped by the buyer. @@ -88,9 +103,17 @@ public class BTShopperInsightsClient { } /// 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 - public func sendVenmoPresentedEvent() { - apiClient.sendAnalyticsEvent(BTShopperInsightsAnalytics.venmoPresented) + /// This method sends analytics to help improve the Shopper Insights feature experience. + /// - Parameters: + /// - paymentMethodsDisplayed: Optional: The list of available payment methods, rendered in the same order in which they are displayed. + /// - experiment: Optional: A `JSONObject` passed in as a string containing details of the merchant experiment. + public func sendVenmoPresentedEvent(paymentMethodsDisplayed: [String?] = [], experiment: String? = nil) { + let paymentMethodsDisplayedString = paymentMethodsDisplayed.compactMap { $0 }.joined(separator: ", ") + apiClient.sendAnalyticsEvent( + BTShopperInsightsAnalytics.venmoPresented, + merchantExperiment: experiment, + paymentMethodsDisplayed: paymentMethodsDisplayedString + ) } /// Call this method when the Venmo button has been selected/tapped by the buyer. @@ -101,13 +124,20 @@ public class BTShopperInsightsClient { // MARK: - Analytics Helper Methods - private func notifySuccess(with result: BTShopperInsightsResult) -> BTShopperInsightsResult { - apiClient.sendAnalyticsEvent(BTShopperInsightsAnalytics.recommendedPaymentsSucceeded) + private func notifySuccess(with result: BTShopperInsightsResult, for experiment: String?) -> BTShopperInsightsResult { + apiClient.sendAnalyticsEvent( + BTShopperInsightsAnalytics.recommendedPaymentsSucceeded, + merchantExperiment: experiment + ) return result } - private func notifyFailure(with error: Error) -> Error { - apiClient.sendAnalyticsEvent(BTShopperInsightsAnalytics.recommendedPaymentsFailed, errorDescription: error.localizedDescription) + private func notifyFailure(with error: Error, for experiment: String?) -> Error { + apiClient.sendAnalyticsEvent( + BTShopperInsightsAnalytics.recommendedPaymentsFailed, + errorDescription: error.localizedDescription, + merchantExperiment: experiment + ) return error } } diff --git a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift index a8127f57d2..168e282134 100644 --- a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift +++ b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift @@ -18,6 +18,17 @@ class BTShopperInsightsClient_Tests: XCTestCase { ) ) + let sampleExperiment = + """ + [ + { + "experimentName" : "payment ready conversion", + "experimentID" : "a1b2c3" , + "treatmentName" : "control group 1", + } + ] + """ + override func setUp() { super.setUp() mockAPIClient = MockAPIClient(authorization: clientToken) @@ -95,7 +106,7 @@ class BTShopperInsightsClient_Tests: XCTestCase { } } - func testGetRecommendedPaymentMethods_whenEligibleInPayPalNetworkTrue_returnsOnlyPayPalRecommended() async { + func testGetRecommendedPaymentMethods_whenEligibleInPayPalNetworkTrueANDMerchantExperimentSet_returnsOnlyPayPalRecommended() async { do { let mockPayPalRecommendedResponse = BTJSON( value: [ @@ -110,11 +121,12 @@ class BTShopperInsightsClient_Tests: XCTestCase { ] ) mockAPIClient.cannedResponseBody = mockPayPalRecommendedResponse - let result = try await sut.getRecommendedPaymentMethods(request: request) + let result = try await sut.getRecommendedPaymentMethods(request: request, experiment: sampleExperiment) XCTAssertTrue(result.isPayPalRecommended) XCTAssertFalse(result.isVenmoRecommended) XCTAssertTrue(result.isEligibleInPayPalNetwork) XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last, "shopper-insights:get-recommended-payments:succeeded") + XCTAssertEqual(mockAPIClient.postedMerchantExperiment, sampleExperiment) } catch { XCTFail("An error was not expected.") } @@ -194,6 +206,13 @@ class BTShopperInsightsClient_Tests: XCTestCase { XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.first, "shopper-insights:paypal-presented") } + func testSendPayPalPresentedEvent_whenPaymentMethodsDisplayedNotNil_sendsAnalytic() { + let paymentMethods = ["Apple Pay", "Card", "PayPal"] + sut.sendPayPalPresentedEvent(paymentMethodsDisplayed: paymentMethods) + XCTAssertEqual(mockAPIClient.postedPaymentMethodsDisplayed, paymentMethods.joined(separator: ", ")) + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.first, "shopper-insights:paypal-presented") + } + func testSendPayPalSelectedEvent_sendsAnalytic() { sut.sendPayPalSelectedEvent() XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.first, "shopper-insights:paypal-selected") diff --git a/UnitTests/BraintreeTestShared/MockAPIClient.swift b/UnitTests/BraintreeTestShared/MockAPIClient.swift index f46932572b..f796388e84 100644 --- a/UnitTests/BraintreeTestShared/MockAPIClient.swift +++ b/UnitTests/BraintreeTestShared/MockAPIClient.swift @@ -15,6 +15,8 @@ public class MockAPIClient: BTAPIClient { public var postedPayPalContextID: String? = nil public var postedLinkType: LinkType? = nil public var postedIsVaultRequest = false + public var postedMerchantExperiment: String? = nil + public var postedPaymentMethodsDisplayed: String? = nil @objc public var cannedConfigurationResponseBody : BTJSON? = nil @objc public var cannedConfigurationResponseError : NSError? = nil @@ -92,14 +94,18 @@ public class MockAPIClient: BTAPIClient { _ name: String, correlationID: String? = nil, errorDescription: String? = nil, + merchantExperiment experiment: String? = nil, isConfigFromCache: Bool? = nil, isVaultRequest: Bool? = nil, linkType: LinkType? = nil, + paymentMethodsDisplayed: String? = nil, payPalContextID: String? = nil ) { postedPayPalContextID = payPalContextID postedLinkType = linkType postedIsVaultRequest = isVaultRequest ?? false + postedMerchantExperiment = experiment + postedPaymentMethodsDisplayed = paymentMethodsDisplayed postedAnalyticsEvents.append(name) }