Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React Native crashing in production

We built an app using React Native to improve UX and features of our previous Cordova app.

Everything went fine. Several months of development, QA, App review and then we published to App Store. It worked on all devices we tried, from iPhone 4s to iPhone 6s+, we tested on iOS 8.3 (earliest simulator you can download through xCode) to 10.0.

After release lots of users started reporting that app crashes before splash screen even goes away. Behaviour we haven't seen in the app review, testing or anywhere else before.

We investigated "crashes" in xCode and they obviously didn't show up, because hundreds of users experienced a crash and we were able to only see few - which seemed unrelated to startup.

We released an updated version with Crashlytics integrated, but that didn't help either. We do not get Crashlytics errors for this specific problem either, meaning that problem is probably happening before

Any ideas where should I look next? We really do not want to revert to the old version and lose months of work.

The app uses around ~100MB of memory when everything is loaded, so that shouldn't be a problem I presume. It is happening on all versions of iOS across all devices. We cannot isolate the error to only specific users.

like image 224
ewooycom Avatar asked Nov 21 '16 19:11

ewooycom


2 Answers

When there doesn't seem to be any other avenue of analysis, I resort to the humble fall-back of logging.

I have previously used the following technique in production iOS apps. This is a bit of work to set up, but once going it is tremendously useful for many other problems in the future. Not just crashes, but any other strange behaviour that users report which you cannot replicate in your testing environments.

  1. The very first thing the app should do is check if the PREVIOUS startup was successful or not by reading some values that should have been written to defaults at the beginning and end of the previous startup (details in the next step). If the PREVIOUS startup was NOT successful, give the user the option to run in some sort of 'safe mode' (what this means depends on what your app tries to do at startup, but for me it meant not loading any data, or doing anything much apart from displaying the UI devoid of any data-dependent items; for some apps, it could even go so far as to load a completely different UI which includes only diagnostics tools or data deletion/reset tools).
  2. The very next thing the app should do after determining that the the previous startup was OK (or that this is the first startup ever) is to write some sort of "startupBegan" status to defaults as soon as possible and then later some sort of "startupCompleted" status only when it has fully completed startup (what "fully completed startup" means is app-dependent, but you really want to be certain that the UI is fully responsive at this point, and is displaying everything it needs to; this can be a bit tricky to determine sometimes, as some things don't run until after the splash screen has disappeared, etc; if you cannot find any other way, I suppose you can trigger this with a timer, but that would be rather ugly - better to find some way to determine when startup really is fully completed). These values can be used to determine if the startup began, but did not complete and are what step 1 (above) uses to determine if the previous startup was successful or not.
  3. Include lots of logging in the app, and write logs to a file. I think there are 3rd party tools you can get to do this, but I wrote my own method (below) that simply redirects stderr to a file if running in production and not connected to XCode. Note that NSLog() writes to stderr, not stdout.
  4. Give the app the ability to email log files to your support email address - this must be available in the app's 'safe mode' (as well as in normal mode). In normal mode, I make this fairly obscure so as not to be noticed much by the user when all is well (eg, a button right at the bottom of a 'Settings' or 'About' view). I tell the user how to find the button when they've submitted a support request for which I really need the logs.
  5. Rotate logs on each startup to prevent them from consuming too much space, but be sure to keep a few rotations, otherwise you'll only get the log from the 'safe mode' startup, which is useless.

Many variations on this are possible. Including things like only enable logging if the user has configured a setting for it. You may have to add a whole lot of logging around particular areas of code sometimes when a user reports a particular problem, and then delete it again after the problem is resolved (if you are concerned about the performance/storage issues around logging).

For my (Objective-C) app, the places for including my code to write startup status to defaults were as below (there may be better places more suitable for your app):

  • "startupBegan" early in the app delegate's application:didFinishLaunchingWithOptions:
  • "startupCompleted" at end of view controller's viewDidAppear (NOT viewWillAppear! there's a lot of stuff can go wrong between these two being sent)

PS. My old log redirection and rotation method was something like this (Objective-C):

- (void)logRedirectRotate {
    // If stderr not going to an XCode console (then running in production)
    if ( ! isatty(STDERR_FILENO) ) {
        // Rotate logs

        int rotationsCount = 3;
        NSMutableArray *logRotations = [NSMutableArray array];

        for ( int i = 0; i < rotationsCount; i++ ) {
            [logRotations addObject:[pathToLogsDir stringByAppendingPathComponent:[NSString stringWithFormat:@"appnameorbundleid.%d.log", i]]];
        }

        [[NSFileManager defaultManager] removeItemAtPath:[logRotations lastObject] error:nil];

        for ( int i = rotationsCount - 1; i > 0; i-- ) {
            [[NSFileManager defaultManager] moveItemAtPath:[logRotations objectAtIndex:i - 1] toPath:[logRotations objectAtIndex:i] error:nil];
        }

        //  Redirect stderr to current log file rotation
        freopen([[logRotations objectAtIndex:0] cStringUsingEncoding:NSASCIIStringEncoding],"a+",stderr);
    }
}
like image 173
Son of a Beach Avatar answered Oct 13 '22 17:10

Son of a Beach


The problem took so long to resolve because of bad communication between us and our users. App was actually NOT crashing, just not starting up (which is the same in the eyes of some users).

After we discovered that, we realized that one of the events is not firing (the one that hides extended splash screen) and this is where users got stuck. One of the libraries we were using didn't correctly handle the error scenario and it made our job much harder. I was lucky to get into that state while testing and I could continue from there.

I updated the code to handle that scenario and the issue is now resolved.

like image 2
ewooycom Avatar answered Oct 13 '22 16:10

ewooycom