ewooycom
ewooycom

Reputation: 2721

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.

Upvotes: 11

Views: 4316

Answers (3)

Son of a Beach
Son of a Beach

Reputation: 1779

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 (my old custom Objective-C version of how to do this has been replaced with a more relevant Swift version in the "UPDATE" section below).
  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.

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)

UPDATE:

Logging in iOS is a LOT better than it used to be when this post was first written, so I've removed the clunky custom Objective-C NSLog() and stdout file redirection instructions I originally had in this post. Instead I now (using Swift) do something like this, at the global level (eg, in AppDelegate):

let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "app") (Optionally, you can have multiple loggers which will identify themselves in each log line. I also have another one in one of my apps to which I redirecdt all JavaScript logging from a web view, like:

let jsLogger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "JS")

Then to actually write to the logs, use something like:

logger.log("Some message being logged: \(some variable, privacy: .public)")

Then to gather actual logs (eg, if the user hit's the 'Send Logs' button in a 'Diagnostics' view of the 'Settings' UI):

guard let logStore = try? OSLogStore(scope: .currentProcessIdentifier) else { return }
let oneHourAgo = logStore.position(date: Date().addingTimeInterval(-3600))
guard let allEntries = try? logStore.getEntries(at: oneHourAgo) else { return }
let filteredEntries = allEntries.compactMap { $0 as? OSLogEntryLog }.filter { $0.subsystem == Bundle.main.bundleIdentifier! }
                
var logText = ""
                
for entry in filteredEntries {
    logText.append(contentsOf: "\(entry.date): \(entry.composedMessage)\n")
}
                
let logData = Data(logText.utf8)

Then attach the logData to an email ready to send to support (eg, using a MFMailComposeViewController.)

Upvotes: 6

tropicalfish
tropicalfish

Reputation: 1270

I had this issue and my case might be quite specific but I'll share my experience anyway.

The point here is the app only crashes in production, so it's something that's fired only on production that is messing up the build. In our case, the culprit was one of the React's dependencies that does minification.

Things to take away:

  1. Keep an eye on any updates of your dependencies
  2. Use crash log

Upvotes: 0

ewooycom
ewooycom

Reputation: 2721

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.

Upvotes: 3

Related Questions