Skip to content

Conversation

richherrera
Copy link
Contributor

@richherrera richherrera commented Mar 4, 2025

Summary of changes

  • Add basic Network Layer
  • Implement AnalyticsService class
  • Change FPTI Batch Model
  • Add UTs
  • Add TestPlan
Screenshot 2025-03-04 at 12 10 40 p m

Checklist

  • Added a changelog entry

Authors

@richherrera richherrera added the v3 label Mar 4, 2025
@richherrera richherrera self-assigned this Mar 4, 2025
@richherrera richherrera requested a review from a team as a code owner March 4, 2025 18:39
@@ -8,13 +8,16 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler {
/// Exposed for testing
var returnedWithURL: Bool = false

static var analyticsService: AnalyticsServiceable = AnalyticsService()
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this is static have we ensure it's being destroyed properly when this class is deinitalized? Just want to make sure we aren't keeping this around somehow.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Excellent observation, let me check. The reason i created this static property is because we need to send the started event when this class is instantiated. I couldn't find a way to use dependency injection, and this approach allows me to conduct the necessary tests

Copy link
Contributor Author

@richherrera richherrera Mar 6, 2025

Choose a reason for hiding this comment

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

Currently this property does not get destroyed, once created, it persists throughout the application's lifecycle. As I mentioned earlier, the reason it is static is that it's the only way I found to conduct tests. I need to test the start event sent in the public init, which requires injecting a mock of the AnalyticsService class. Any other ideas, or shall we continue with the static property?

The PopupBridge class never deinitializes properly. I tested this on the main branch, and the same issue occurs. It seems we do have some retain cycles, but they are not due to the integration of the static property (once we manage to deinitialize PopupBridge, we will be able to see if the AnalyticsService class deinitializes correctly).

Main branch:
Screenshot 2025-03-06 at 11 51 44 a m

Copy link
Contributor

Choose a reason for hiding this comment

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

🤔 yeah we should definitely address it not deinitializing or provide merchants more explicit instructions on destroying this class. If it's happening on main as well I don't want to make it a blocker, but can you make a ticket for us to dig into this a bit more post release?

Should we consider moving the started event out of the init and into the first thing that's called post init? Would that allow us to make this non static? Sounds like we'd still want to address the deinit of this class at a later time.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ticket DTMOBILES-1487

Maybe i can think of another approach. We have two flows: PayPal (via PayPal -> Pay with Credit Card) and Venmo, where there isn't another method called after the init that we could use to send the 'start' event.

Copy link
Contributor

Choose a reason for hiding this comment

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

Why does it need to be static? Can it just be an instance property?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The "start" event is triggered in the public init, and there's no way to inject the mock AnalyticsService for testing. Any suggestions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Please take a look at the public init,

Self.analyticsService.sendAnalyticsEvent(PopupBridgeAnalytics.started, sessionID: sessionID)

    public init(webView: WKWebView, prefersEphemeralWebBrowserSession: Bool = true) {
        self.webView = webView
        
        super.init()
        
        Self.analyticsService.sendAnalyticsEvent(PopupBridgeAnalytics.started, sessionID: sessionID)
        
        configureWebView()
        webAuthenticationSession.prefersEphemeralWebBrowserSession = prefersEphemeralWebBrowserSession
                
        returnBlock = { url in
            guard let script = self.constructJavaScriptCompletionResult(returnURL: url) else {
                return
            }

            self.injectWebView(webView: webView, withJavaScript: script)
            return
        }
    }

@@ -8,13 +8,16 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler {
/// Exposed for testing
var returnedWithURL: Bool = false

static var analyticsService: AnalyticsServiceable = AnalyticsService()
Copy link
Contributor

Choose a reason for hiding this comment

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

🤔 yeah we should definitely address it not deinitializing or provide merchants more explicit instructions on destroying this class. If it's happening on main as well I don't want to make it a blocker, but can you make a ticket for us to dig into this a bit more post release?

Should we consider moving the started event out of the init and into the first thing that's called post init? Would that allow us to make this non static? Sounds like we'd still want to address the deinit of this class at a later time.

Copy link
Contributor

@jaxdesmarais jaxdesmarais left a comment

Choose a reason for hiding this comment

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

One small docstrings cleanup, but assuming we verified we are seeing these events in FPTI, this looks great to me! 🚀

Co-authored-by: Jax DesMarais-Leder <jdesmarais@paypal.com>
@richherrera
Copy link
Contributor Author

One small docstrings cleanup, but assuming we verified we are seeing these events in FPTI, this looks great to me! 🚀

yeah, we are seeing these events in FPTI

Screenshot 2025-03-11 at 12 07 26 p m

import Foundation

enum NetworkError: Error {
case invalidResponse

Choose a reason for hiding this comment

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

Thoughts on passing the statusCode so it can be logged?

case invalidResponse(statusCode: Int)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this way it remains as basic as possible, which is what's expected, since im using this in the NetworkClient, the scope of httpResponse is limited within the guard block, so i cannot directly use it in the throw statement outside of that block. If in the future we need to add more endpoints, I think that would add more value.

45CD0C2C2D64F08F0072C5A4 /* FPTIBatchData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CD0C2B2D64F0810072C5A4 /* FPTIBatchData.swift */; };
45CD0C2E2D664D140072C5A4 /* Date+MilisecondTimestamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CD0C2D2D664CF50072C5A4 /* Date+MilisecondTimestamp.swift */; };
45CD0C302D6788A10072C5A4 /* Bundle+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CD0C2F2D67888F0072C5A4 /* Bundle+Extension.swift */; };
45CD0C322D6793FB0072C5A4 /* PopupBridgeAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CD0C312D6793F90072C5A4 /* PopupBridgeAnalytics.swift */; };
45FBAF272D6F9CCF000D550B /* AnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF262D6F9CC6000D550B /* AnalyticsService.swift */; };
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we add all of the new files that were specifically added for analytics to an Analytics folder under Sources/PopupBridge? nitpick, but would help navigation of the PopupBridge source files

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated: c8fd4cf

// MARK: - Private Properties

private let messageHandlerName = "POPPopupBridge"
private let hostName = "popupbridgev1"
private let hostName = "popupbridgev1"
private let sessionID = UUID().uuidString.replacingOccurrences(of: "-", with: "")
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this sessionID creation live in AnalyticsService instead? Since it isn't required by any of the login in POPPopupBridge.swift

Copy link
Contributor Author

Choose a reason for hiding this comment

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

At the moment, no, because the sessionID would always be the same, due to AnalyticsService being a static var.

If you have any suggestions for adding AnalyticsService that isn't a static var, I'd be happy to work on it. The reason it's a static variable is that in tests, I need to inject the mock of AnalyticsService. I can't inject it in the init, and the "start" event is triggered in the init :(

@@ -0,0 +1,24 @@
{
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this file be deleted?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeap, deleted: 5911546

func post(url: URL, body: FPTIBatchData) async throws
}

final class NetworkClient: Networkable {
Copy link
Contributor

Choose a reason for hiding this comment

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

If we know that PopupBridge doesn't do any other networking besides just sending a single POST for analytics, do we think it's worth having this as a separate layer from AnalyticsService? Thinking of the KISS principe

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated: 7bc9147

// MARK: - Private Properties

/// The FPTI URL to post all analytic events.
private let url = URL(string: "https://api.paypal.com/v1/tracking/batch/events")!
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we aren't actually batching, FPTI has a separate endpoint for single event upload /v1/tracking/events/. The format is slightly different from /v1/tracking/batch/events/. I'm not sure if there are performance benefits to using the single event API vs the multiple event API, but we could confirm in #help-fpti.

To see the structure of each, you can look at the API details in PPAAS (I also have it in a Postman collection)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it's the endpoint used in PPCP, however, we would need a custom encode method. I did a POC with those changes and was thinking that it would be better to have the same implementation we have in BT, which is why I opted for this implementation. While working on BT v7, we chose not to use a custom encode method. Do you think we could use it for this repo?

@richherrera
Copy link
Contributor Author

Events were sent successfully after removing the network client

Screenshot 2025-03-18 at 1 34 38 p m

@richherrera richherrera merged commit 6b53ebf into v3 Mar 20, 2025
4 checks passed
@richherrera richherrera deleted the add-networking branch March 20, 2025 15:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants