BranchMetrics/ios-branch-deep-linking-attribution

Failure to get callbacks when upgrading from 3.0.0 to 3.3.0

Closed this issue · 7 comments

Describe the bug

Faced with issue after updating SDK version.
Old app contains SDK v3.0.0; new app version: v3.3.0.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        Branch.setUseTestBranchKey(true)
        Branch.getInstance().initSession(launchOptions: launchOptions, automaticallyDisplayDeepLinkController: false, deepLinkHandler: handleDeepLink)
}

func handleDeepLink(params: [AnyHashable: Any]?, error: Error?) {}

The main issue:
After app update, deepLinkHandler is never called. App restart not help.
The only solution is to delete app and install from scratch. But such case is NOT acceptable for app users.

Network finish operation https://api3.branch.io/v1/open 0.944s. Status 200 error (null).

Expected result: deepLinkHandler call just after app launch.

  • Not sure about behavior with TestBranch == false. I had added additional logs to track this issue on prod.
  • I had 1 simulator (iOS 17.2) and 2 devices (13 pro 15.7.1; 12 pro max 16.7.2) that have the same issue after first update.
  • But I'm not able to reproduce this issue on the same device a second time. Those.: Faced with issue -> remove the app -> install app version with SDK 3.0.0 and run -> Install App with SDK 3.3.0 and run -> All looks good. Maybe I need to use an app for some time, or receive some link before updating to reproduce the issue.

Log file:
Branch_io_log.txt

The same issue as users described here:
#760 (comment)
#914 (comment)
https://github.com/BranchMetrics/react-native-branch-deep-linking-attribution/issues/532#issue-554558963
And a lot of threads at stackoverflow.

Steps to reproduce

Expected behavior

deepLinkHandler call after app launch.

SDK Version

3.3.0

XCode Version

15.2

Device

iPhone 13 pro

OS

15.7.1

Additional Information/Context

No response

Screenshot 2024-03-07 at 00 01 40 lose_callback

Well, looks like SDK on findExistingInstallOrOpen take BranchOpenRequest with empty callback.
And at the same time urlString is nil as well, so if and else cases just NOT save initSessionCallback and we lose it

@Splash04
Mind opening a support ticket for this? The upgrade scenario should work.

Now the status 200, is actually the http status which is ok. That's just informational and not an error. So I'm going to rename this ticket to correctly describe it as a potential upgrade issue.

Looking at the logs and screenshots, the behavior is as expected. So I think a deeper analysis is necessary.

The else block is to handle a very specific lifecycle situation. Most of the time we actually do NOT want to create a new request. The reason it exists is if we have two lifecycle events come in very rapidly and the second lifecycle has link data, we cannot ignore like we normally do. We must make an attempt to resolve that one as well.

I'm still have one phone with this issue. So I will try to provide some additional details that maybe help:

  1. On App launch by some reason we load from file "BNCServerRequestQueue" just one BranchOpenRequest with empty fields:
    empty_request

  2. Init branch single with this data:
    init_branch_singletone

  3. Checking request [self.requestQueue findExistingInstallOrOpen] and not saving initSessionCallback:
    checking_request_in_queue

  4. Processing empty BranchOpenRequest:
    processing_empty_request_from_queue

  5. Send open event to server:
    send_open_event_to_server

  6. on processResponse we don't have errors and callback is null, so just load data and [BranchOpenRequest releaseOpenResponseLock]
    releaseOpenResponseLock

  7. Callback null, so do nothing here:
    empty_callback

  8. processNextQueueItem
    processNextQueueItem

So no callback call here.

When I compare this flow with clear setup flow, I had seen that findExistingInstallOrOpen don't return anything.
So we're creating BranchOpenRequest and saving initSessionCallback. And initSessionCallback call after the end.
fresh_install

For developers who faced with this issue. This actions should help.

  1. We should create timer to catch BranchIO freeze and force start the app. I had selected 15 sec timeout.
  2. We should clear the cache file for branch serverRequestQueue to avoid freeze on the next launch.
@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    private var branchIOLaunchTimer: Timer?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    	// code
    	startBranchTimeoutTimer()
    	Branch.getInstance().initSession(launchOptions: launchOptions, automaticallyDisplayDeepLinkController: false, deepLinkHandler: handleDeepLink)
    }

    func handleDeepLink(params: [AnyHashable: Any]?, error: Error?) {
    	stopBranchTimeoutTimer()
    	// handle DeepLink
    }
}

// ******************************* MARK: - Branch iOS workaround

extension AppDelegate {
	@objc
    private func onBranchTimerTimeout() {
        logError("Branch IO app launch Timeout")
        // Just additional safety (Queue should be already empty), basically do nothing for our issue
        Branch.getInstance().clearNetworkQueue()
        
        /*
         The main issue, that file for serverRequestQueue cache contains invalid event
         so we need to get path to this file and clear it
         */
        
        /* persistImmediately will not clean up the file, because it contains checking count != 0
            if let serverRequestQueue = (BranchSDK.BNCServerRequestQueue.getInstance() as? BranchSDK.BNCServerRequestQueue) {
                print("serverRequestQueue: \(serverRequestQueue.queueDepth())")
                serverRequestQueue.persistImmediately()
            }
         */
        
        // Load serverRequestQueue file path from ObjecitiveC code
        if let fileUrl = ObjectiveCUtils.serverRequestQueueURL() {
            logDebug("Remove cache file for branch serverRequestQueue", data: ["fileUrl" : fileUrl])
            removeFile(fileUrl: fileUrl)
        }
        
        // Force start the app
        handleDeepLink(params: [:], error: nil)
    }
    
    func removeFile(fileUrl: URL) {
        if FileManager.default.fileExists(atPath: fileUrl.path) {
            do {
                try FileManager.default.removeItem(atPath: fileUrl.path)
                print("File deleted: \(fileUrl)")
            } catch {
                print("Could not delete file, probably read-only filesystem")
            }
        } else {
            print("File not exist")
        }
    }
    
    fileprivate func startBranchTimeoutTimer() {
        if let _ = self.branchIOLaunchTimer { return }
        self.branchIOLaunchTimer = Timer.scheduledTimer(timeInterval: 10.0,
                                                        target: self,
                                                        selector: #selector(onBranchTimerTimeout),
                                                        userInfo: nil,
                                                        repeats: false)
    }
    
    private func stopBranchTimeoutTimer() {
        self.branchIOLaunchTimer?.invalidate()
        self.branchIOLaunchTimer = nil
    }
}

The main problem here that serverRequestQueue cache file path is a private method in objectiveC class. So, we have to use Objective-C Runtime Utilities function to get selector by name (NSSelectorFromString). Please, check that function name is not changed:
function_name

How to get this path:

  1. Create ObjectiveCUtils.h:
#import <Foundation/Foundation.h>

@interface ObjectiveCUtils : NSObject

+ (NSURL *) serverRequestQueueURL;

@end
  1. Accept to create -Bridging-Header.h when XCode asks you. It should contain just one line:
    #import "ObjectiveCUtils.h"
  2. Create ObjectiveCUtils.m
#import "ObjectiveCUtils.h"
#import "Branch.h"

@implementation ObjectiveCUtils

+ (NSURL *) serverRequestQueueURL {
    SEL privateSelector = NSSelectorFromString(@"URLForQueueFile");
    if ([BNCServerRequestQueue respondsToSelector: privateSelector]) {
        id url = [BNCServerRequestQueue performSelector: privateSelector];
        
        if ([url isKindOfClass: [NSURL class]]) {
            return url;
        } else {
            printf("Invalid class type for URLForQueueFile url\n");
            return NULL;
        }
    } else {
        printf("Selector for URLForQueueFile is not found\n");
    }
    
    return NULL;
}

@end

@Splash04
Thanks for the detailed logs and your workaround. Looking to see if I can reproduce this on my own test devices.
Either way, I think an additional safety check for invalid events in storage is necessary in the next patch.

A error recovery safety check similar to the one described here was added in the latest release.
https://github.com/BranchMetrics/ios-branch-deep-linking-attribution/releases/tag/3.4.1