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

Analytics - only fetch config before flushing events cache #1337

Merged
merged 3 commits into from
Jun 18, 2024

Conversation

scannillo
Copy link
Contributor

@scannillo scannillo commented Jun 14, 2024

Background

  • We have seen an unexpectedly high number of "core:api-request-latency" analytics for the /v1/configuration endpoint in FPTI, compared to the number of occurrences for each attempted payment method flow (see data below)
  • We haven't been able to replicate the exact issue we see in the FPTI dataset, but we do know that it is related to a quick succession of calls to /v1/configuration, while another is in-flight, resulting in duplicate/overlapping requests.

Screenshot 2024-06-05 at 11 12 54 AM

This PR reduces the number of calls to BTAPIClient.fetchConfiguration() in our Analytics layer. I don't know if this will solve 100% of the issue, but it should help.

Changes

  • Move BTAPIClient.fetchConfiguration() call in BTAnalyticsService
    • Before it was being called in every sendAnalyticsEvent() method call
    • Now it is only called right before we flush our batch of analytics (every 20 seconds)
    • Shoutout to @sshropshire for this idea in stand-up!

Checklist

  • Added a changelog entry

Authors

@scannillo

@scannillo scannillo marked this pull request as ready for review June 17, 2024 13:38
@scannillo scannillo requested a review from a team as a code owner June 17, 2024 13:38
@scannillo
Copy link
Contributor Author

We could add a unit test like below, but I don't think it brings that much value. I think we should make a ticket for a holistic integration test for analytics, the batch POST, & the config GET. Open to folks opinions though.

func testSendAnalyticsEvent_doesNotMakeGETRequestForConfig() {
    let stubAPIClient: MockAPIClient = stubbedAPIClientWithAnalyticsURL("test://do-not-send.url")
    let analyticsService = BTAnalyticsService(apiClient: stubAPIClient)
    
    analyticsService.sendAnalyticsEvent("any.analytics.event1")

    XCTAssertFalse(stubAPIClient.fetchOrReturnRemoteConfigCalled)
}

@KunJeongPark
Copy link
Contributor

KunJeongPark commented Jun 17, 2024

We could add a unit test like below, but I don't think it brings that much value. I think we should make a ticket for a holistic integration test for analytics, the batch POST, & the config GET. Open to folks opinions though.

func testSendAnalyticsEvent_doesNotMakeGETRequestForConfig() {
    let stubAPIClient: MockAPIClient = stubbedAPIClientWithAnalyticsURL("test://do-not-send.url")
    let analyticsService = BTAnalyticsService(apiClient: stubAPIClient)
    
    analyticsService.sendAnalyticsEvent("any.analytics.event1")

    XCTAssertFalse(stubAPIClient.fetchOrReturnRemoteConfigCalled)
}

I think in some functions, we call sendAnalytics event before the fetch function. So if tokenize is called immediately after BTAPIClient instantiation, we would have prefetch call from BTAPIClient instantiation and possibly fetch call from analytics. Correct me if I am wrong. We definitely want to move the first sendAnalyticsEvent call in tokenize functions in the fetch call completion handler. I know that batched call would introduce a delay, but it's hard to predict timing of these events especially with cache expiration.
And definitely really tough to write tests for different scenarios.

@scannillo
Copy link
Contributor Author

I think in some functions, we call sendAnalytics event before the fetch function. So if tokenize is called immediately after BTAPIClient instantiation, we would have prefetch call from BTAPIClient instantiation and possibly fetch call from analytics. Correct me if I am wrong.

I'm not following this. This PR is really just a small change/fix. We never actually needed to fetch the config as early as we were before this PR (on each call to sendAnalyticsEvent). We only need the config value when we construct the batch of events to send to FPTI.

This PR now only retrieves the config (ideally it's cached by now) before we're ready to batch upload our queue of analytic events. This batch happens on a 20 second timer.

Sources/BraintreeCore/Analytics/BTAnalyticsService.swift Outdated Show resolved Hide resolved
http?.post("v1/tracking/batch/events", parameters: postParameters) { _, _, _ in }
await BTAnalyticsService.events.removeAll()
do {
let configuration = try await apiClient.fetchConfiguration()
Copy link
Contributor

Choose a reason for hiding this comment

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

Would we also want to consider setting a property for this in the class and using an if let to continue to reuse the config if not nil? That could let us reuse the config for the entire flow vs fetching it every 20 seconds.

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 concern there is if the config had expired, we would be using an expired config

Copy link
Contributor

Choose a reason for hiding this comment

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

Since it's stored as a singleton in the ConfigurationCache couldn't we check that? Psudocode but something like:

if let configuration, configuration = try? ConfigurationCache.shared.getFromCache(authorization: self.authorization.bearer)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But honestly, does that even matter? Let's say a merchant uses our SDK with multiple merchantIDs (ie refreshing their clientToken would possible use a new merchantID). They would have to re-create a BTAPIClient with that refreshed clientToken which would create a new instance of BTAnalyticsService anyways.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, but in cases where merchants don't, which I believe is more common, it would avoid an additional hop to fetchConfiguration which while minor could reduce some latency. It is a take it or leave it comment so if you don't agree we don't need to implement it, was just asking a question.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh weird! My comment "But honestly, does that even matter?" was in response to questioning my previous comment here 😆 . I must not have refreshed to see your comment, my apologies!

Since it's stored as a singleton in the ConfigurationCache couldn't we check that? Psudocode but something like:

We could but I don't think we want to leak logic of checking the config cache into the analytics layer. I think @richherrera is working on moving to the ConfigurationLoader pattern, like Android, which we can call in here once that's ready.

Copy link
Contributor

Choose a reason for hiding this comment

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

Rich and I are pairing on working on the ConfigurationLoader pattern like you had suggested.

Copy link
Contributor

Choose a reason for hiding this comment

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

We could but I don't think we want to leak logic of checking the config cache into the analytics layer. I think @richherrera is working on moving to the ConfigurationLoader pattern, like Android, which we can call in here once that's ready.

That makes sense, we can punt on it until the ConfigurationLoader pattern is established!

@KunJeongPark
Copy link
Contributor

I think in some functions, we call sendAnalytics event before the fetch function. So if tokenize is called immediately after BTAPIClient instantiation, we would have prefetch call from BTAPIClient instantiation and possibly fetch call from analytics. Correct me if I am wrong.

I'm not following this. This PR is really just a small change/fix. We never actually needed to fetch the config as early as we were before this PR (on each call to sendAnalyticsEvent). We only need the config value when we construct the batch of events to send to FPTI.

This PR now only retrieves the config (ideally it's cached by now) before we're ready to batch upload our queue of analytic events. This batch happens on a 20 second timer.

Yes, I understand that. I was just trying to understand the timeline of whether we are trying to make these calls less frequent or injecting config value into BTAnalyticsService as we discussed as a possibility for scenario in this PR:
#1308

@scannillo
Copy link
Contributor Author

Yes, I understand that. I was just trying to understand the timeline of whether we are trying to make these calls less frequent or injecting config value into BTAnalyticsService as we discussed as a possibility for scenario in this PR:
#1308

Like Rich called out, there is a problem with injecting it, since it could be expired.

This PR is a simple fix that could help things. It doesn't change any public APIs within the SDK.

@scannillo scannillo merged commit 720486f into main Jun 18, 2024
6 of 7 checks passed
@scannillo scannillo deleted the reduce-analytics-config-calls branch June 18, 2024 18:44
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.

3 participants