Crash Analytics and Feedback for ReactNative Apps

As I am starting to use ReactNative to write non-trivial, production facing apps, I realized that I needed solutions that would help me share beta versions of my app, get user feedback and analyze crash reports. For native apps, I had used HockeyApp and loved it for its simple API and multi-platform support.
My workflow with HockeyApp for native apps was pretty simple.
  1. Build the native app on a continuous server like Jenkins or Visual Studio Online.
  2. Upload the generated APK or iPA file using curl to the HockeyApp url.
  3. Beta testers use the HockeyApp for iOS or Android to download this beta app, and test it.
  4. HockeyApp SDK was integrated into my app for getting feedback, analyzing crashes, or notifying any changes that I make. 
I wanted similar capabilities for the ReactNative apps that I build. In my last post, I had written about the Cordova Plugin Adapter for React Native, and I was able to use that to integrate the HockeyApp SDK in app. I used this cordova plugin.

Given that the cordova plugin adapter did most of the setup work, I did not have to manually add permissions or modify my activity as specified in the docs. I just had to do  

$ npm install react-native-cordova-plugin 
$ node_modules/.bin/cordova-plugin add cordova-plugin-hockeyapp.

Then I simply require('react-native-cordova-plugin'); and started using the API as below
  1. Initialize the plugins using cordova.hockeyapp.start(success, fail, token) on componentDidMount.
  2. cordova.hockeyapp.checkForUpdate(success, failure); to check if there are new versions. I ran this on startup of the app, and also had a button to refresh the app.
  3., failure); opened the screen for users to provide feedback about the app and was hooked to a button. 
  4. For testing, I also used the cordova.hockeyapp.forceCrash(); API to simulate crashes. There was a bug in the app that made the app crash on a beta tester's phone and I did get the crash reports. 
The entire code showing each of this functions is available in this gist. I also created a demo video showing the APIs in action.

I was only able to get the HockeyApp working on the AndroidSDK since the react-native-plugin-adapter only works on Android for now. If you would like a version for iOS too, or would like to try integrating the HockeyApp into your ReactNative app, please do ping me and I will be happy to help.

Using Cordova Plugins in React Native (Android)

A tool use Cordova plugins with React Native - link
Apache Cordova has a pretty vibrant ecosystem of plugins. These plugins usually comprise of 2 parts - a native component that interacts with the device APIs and a JavaScript layer that makes these APIs simpler. Creating Native Modules with React Native also has a similar model of operation.
Given that more than 1000 Cordova plugins exist today to access device capabilities from JavaScript, it only seems logical to be able to reuse these for any ReactNative projects. 

This blog is about the internals on how Cordova plugins can be used with ReactNative. The scenario only works on Android for now, as shown in the video below. 

[Link to Video]
Update: Note that the demo adds plugins using node_modules/rncp/cli/cli.js. Now that the module is on npm, you could simply use node_modules/.bin/cordova-plugin add pluginname.

You can use this sample application to test the APIs.

Step 0: Installing CordovaPlugin Adapter

Using native modules in a ReactNative project usually means adding the dependencies in build.gradle, adding the sub project, etc. Instead of doing this for every single cordova plugin, I was able to leverage Cordova's node based plugin manager (called plugman). Hence, you just add the dependencies once to set up the "Cordova Adapter ReactNative" (lets call it CARN).
CARN is on npm and you can install it in the locally in the ReactNative project.

Step1 : Installing the plugins

With CARN installed, you can use it's command line interface to add plugins.
  1. Add plugins using $ node_modules/.bin/cordova-plugin add cordova-plugin-name. This command simply relies on plugman to download the plugin from npm.
  2. Once the plugin is downloaded, plugman looks at the plugin's plugin.xml file to determine any other plugins that need to be installed as dependencies - all these are stored in a assets/xml/config.xml to be used later. 
  3. Plugman then copies over the javascript, java file and JARs to the appropriate locations. These locations are defined in CARN's build.gradle. 
  4. Plugman also looks at plugin.xml to change config files like AndroidManifest.xml to add any activities or permissions that are needed for the plugin to work well. 
  5. Finally CARN combines all the Javascript files from all plugins using Cordova's module builder system (which is similar to browserify). 
  6. This is later "required" by the ReactNative's to start using the plugins. 

Step 2: Using the Plugin

  1. All Cordova plugins are available when CARN's javascript is "required" in ReactNative JavaScript. For example, the API that cordova-plugin-device exposes is typically called using require('react-native-cordova-plugin').navigator.device.getInfo(successCallback, failCallback).
  2. All the Javascript APIs that Cordova plugins expose eventually call a cordova.exec(service, action, callbackID) method, which in turns calls the exec method exposed on the Java side using the WebView.addJavascript Interface. Unlike Cordova, Reactnative does not have a webview. Hence we hijack this method and use ReactNative's Native Modules to expose a corresponding "exec" method on the Java side.
  3. Once the "exec" method on Java is invoked, it starts Cordova's Java PluginManager. This PluginManager consults the assets/xml/config.xml to look at the ClassName for the service (which is the plugin name) and calls the corresponding action on that classname provided by the plugin. 
  4. All results are asynchronous, so the plugin continues working on the method call. A plugin may have to invoke another activity like in the case of a ContactPicker or Camera using startActivityForResult.
  5. The plugin is finally done with its work. In case another activity (like Contact Picker) was  invoked, a contact is picked and our ReactNative's MainActivity is started again with the result from the previous activity. This result passed to the plugin's onActivityResult callback
  6. Now that we have the result, the plugin manager calls the WebView to deliver the result back. In our case, we instead have a MockWebView for ReactNative that uses the RTCEventEmitter to deliver the result back.
  7. We already have a listener on the JavaScript side that looks at the result that also contains a callbackID. We consult our list of callbackIDs, and call the successCallback or failCallback. 
The code itself is pretty straightforward and mostly reuses Cordova's plugman and PluginManager for most of the work.  The github repository has all this code, and the best place to start looking would be the CordovaPluginAdapter for Java side of things, and index.js for Javascript side of things.

Next Steps

The steps described above are Android specific, and I am assuming that we would have a very similar flow for iOS too. Including almost all of Cordova for Runtime may not be not most efficient, and I am working on trimming down the dependencies to make this even faster.
Would love to hear any feedback you may have and any suggestions or contributions for Android, or the iOS implementation.

[1] Note that the demo is on Windows. ReactNative still has issues to work with Windows, and I had to modify some files in ReactNative to get it to work on Windows. 
[2] If  you are using Windows, I would recommend using this Android Emulator - it works with HyperV on and is super fast !!