Overview of iOS Crash Reporting Tools: Part 2/2
Thanks for joining me for the second part of this two-part series on crash reporting services!
The first part introduced you to the architecture of crash reporting services, including storage, symbolication, and server-side management. As well, I provided a basic overview and comparison chart of the most popular crash reporting services today.
In this second part, I’ll take you through the steps to get started with each service covered in the last article: Crashlytics, Crittercism, Bugsense, TestFlight and HockeyApp.
Throughout this article, you will work with a very simple iPhone application that just contains a table view, as shown in the screenshot below:
This particular app has been constructed to generate crash events. That’s bad news in the real word — but for our purposes, it will serve perfectly!
Getting Started
The app provides a pizza menu with two functions:
- Swipe to delete a pizza from the menu.
- Scroll to the bottom to load more pizzas.
The code for our pizza application can be found here.
Download the project, then build and run in the simulator. To cause a crash, swipe to delete a row, or simply scroll to the bottom of the list.
Note: The sample app is designed to run on iOS6 and above. If you are testing on an pre iOS6 device, you will need to disable AutoLayout.
To do so, open SMViewController.xib, select the File Inspector, then uncheck “Use AutoLayout”, as shown below:
Now when your app crashes, you’ll really mean for it to happen! :]
Although I know it’s killing you not to fix the bugs, don’t! You want the application to crash so that a reporting tool can help you to identify the source of the problem.
Here are a list of the crash reporting services that I’ll cover in this article:
- Crashlytics (www.crashlytics.com)
- Crittercism (www.crittercism.com)
- Bugsense (www.bugsense.com)
- TestFlight (www.testflightapp.com)
- HockeyApp (www.hockeyapp.net)
Keep in mind that there is a waiting period for Crashlytics where it may take several days for Crashlytics to contact you. However, if it does take a long time then no fear because Crashlytics has generously provided RayWenderlich.com with a special link that will help you jump the queue. Try that and see if you get in the VIP door a little faster! :]
Before continuing, make sure you create an account with each of the crash reporting frameworks and download each of the crash reporting SDKs.
Since some crash reporting tools require a unique identifier to function correctly, make sure to change the sample project’s bundle identifier to something of your own design, as shown below:
Once you have a set a custom bundle identifier, make sure to make a backup copy of the project. Each framework has their own installation instructions so it is best to use a “vanilla” project with each demo.
In order to test out these crash reporting frameworks, you will need to run the app on a real device. This requires an Apple developer account as well as a provisioned device. If you are a new to iOS development, you can learn how to create a developer account, as well as provisioning your own device in this tutorial.
Note: During this tutorial, do not run the application on the device through Xcode. Xcode will intercept the crash, then open the lldb debugger shell as usual. You won’t get any crash reports if Xcode intercepts the crash event!
To make all the examples below work, you have to build and run the application, then click the stop button on Xcode. This way you will have the latest version installed on the the device.
Once that is done, you can launch the app on the device itself, and then crash it all you want!
All the crashes on your iOS device will be caught and sent to the server component of the service that you have integrated into the app. Crash reports are usually sent to the server the next time you start the app, so the steps to follow to generate a crash report on the server are as follows:
- Build and run on Xcode.
- Press the stop button.
- Run the app on your iOS device.
- Make the app crash.
- Run the app again.
As well, make sure the device has connectivity via wi-fi or 3G so that the crash reports can be sent. Now, let’s get crashing! :]
Crashlytics
Recently bought by Twitter, Crashlytics is pretty famous in the iOS community. It’s used by well-known companies such as Path and Yammer. It is a full-stack service, meaning that the framework provides both client-side and server-side parts.
At the moment, Crashlytics supports only iOS, although the website does indicate that Android support is coming soon.
Crashlytics — Configuring the Project
Once you have logged into Crashlytics, you will be prompted to download a Mac OSX application that will help you to set up your first project.
To save you time, here’s the direct link to the Mac application. When running, the application appears in the menu bar on the top right as shown below:
Click on the icon and enter your credentials when prompted. If this is your first time using Crashlytics, you should see an empty list of applications and your account name at the bottom, as shown below:
Click on the New App button on the right. This will show you the list of recent projects opened in Xcode, like so:
If your app is not listed, you’ll need to perform the following steps:
- Click the “Other” button at the bottom to open a Finder window.
- Select the .xcodeproj file and click “Open”.
- Select your project from the list in Crashlytics and hit “Next”.
This will automatically open the project in Xcode and ask you to add a script to the Build Phase. From this point onward, the Crashlytics app will walk you through each step of installation process. Follow the instructions, and when you’re done, continue with the next step of the tutorial.
Crashlytics — Running the App
Now that you’ve completed the setup portion, you are now ready to try out Crashlytics. Run the application on your iOS device, swipe on a cell and tap Delete. The application will crash, as expected.
Restart the app and wait about a minute to make sure the crash report has propagated to the Crashlytics server. Then take a peek at the website to see if the crash report is live. You should also receive an email notification about a new crash in your application.
The new issue on the back-end should look like the following:
At first glance, you will see the guilty line of code (SMViewController.m at line 80), the number of users affected, and the number of occurrences of this crash. Launch the app and make it crash again. You’ll now see the number of crashes of the same issue increase.
Click the row of that issue and you’ll see the extended report, as shown below:
Looking at the crash log, it’s clear that you need to counterbalance the deletion of a cell with the deletion of its corresponding element in the array populating the table view. To fix this bug, open SMViewController.m and modify tableView:commitEditingStyle:forRowAtIndexPath:
to look like the following code:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { [pizzaOrder removeObjectAtIndex:indexPath.row]; [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; } } |
After you are done, build and run, push the app to your device, and test the application again. Once you have determined that you have fixed the issue, return to the Crashlytics crash report then set the issue status as closed, as shown below:
On to the second bug!
Crashlytics allows you to add log statements to your code to help track down bugs. OpenSMViewController.m and add the following import statement at the top of the file:
#import <Crashlytics/Crashlytics.h> |
Next, modify tableView:willDisplayCell:forRowAtIndexPath:
in SMViewController.m as follows.
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.row == pizzaOrder.count-1) { CLS_LOG(@"posting notification"); [Crashlytics setIntValue:pizzaOrder.count forKey:@"numberOfPizzas"]; [[NSNotificationCenter defaultCenter] postNotificationName:LOAD_MORE_NOTIFICATION object:nil]; } } |
CLS_LOG is a macro provided by the Crashlytics framework that enables remote logging. In debug builds, it acts just like NSLog, passing strings to the console. For release builds, the log is sent along with the crash reports and optimized to be as fast as possible. In fact, Crashlytics’ own documentation boasts a 10x improvement over using regular NSLog()
calls.
The method also uses custom logging which allows you to store information in key-value form. Think of this as a type of NSDictionary on the web server: you set the values of keys in the code, and you can read the values on the server when hunting down a bug.
In this case you are using setIntValue:forKey, but there are other methods available to store objects, floats and booleans as well. You can find more details about custom logging in the Crashlytics knowledgebase.
Run the application in Xcode and then stop it. Run it again on your device, scroll to the bottom, allow the app to crash, and restart the application after the crash.
The new issue will look like this on the Crashlytics dashboard:
It is clear that this is due to the lack of the method definition loadMore
in SMWebEngine.m. If you were to add the definition for this method, and the crash would disappear. Although that’s not the point of this tutorial, so don’t worry about actually implementing it!
Notice that the detailed view of each issue provides information about the device like iOS version, and free space on disk and in memory. All these details might help you when you are hunting down the root cause of a crash.
Click on “More details…” in the middle of the page. This will show even more details like the type of device, orientation, plus the keys and log statements that you have spread throughout your code, as shown in the following screenshot:
This way you can have a path of breadcrumbs that can help to hunt down the cause of the crash. It is important to note that logs and keys are sent to the server as attachments to a crash log, so you won’t see them if there aren’t any crashes.
Crashlytics, unlike other services like TestFlight, is not a generic remote logging application, but rather a true crash reporting application. So even though your app may be full of logging statements, if the app never crashes, you’ll never see any of your logs!
One useful feature for beta testing is the ability to ask your users to provide some data to identify themselves. Crashlytics provides three ways to attach this data to a crash log:
[Crashlytics setUserIdentifier:@”123456”]; [Crashlytics setUserName:@”cesarerocchi”]; [Crashlytics setUserEmail:@”cesare@mailaddress.com”]; |
This way, you can identify who is experiencing a crash, and contact that tester to get more details.
Finally, you can configure some logging features on the back-end. Each application has a settings section that you can open by clicking the gear button, as shown below:
From here you can disable reports from a specific version, or request a user for permission before sending data about the report. When you enable it (and you should!), you can customize the message as shown in the screenshot below:
Crashlytics — Summing It Up
This concludes the guided tour of Crashlytics. If you are just targeting the iOS market, I highly recommend that you consider Crashlytics. The service is very quick to upload crashes, there are no hassles with dSYM files, and the back-end service is quite intuitive.
Crittercism
Crittercism is another full-stack tool to keep track of your crash logs. It has been adopted by companies such as Netflix, Eventbrite and Linkedin. It provides support iOS, as well as Android, HTML5 and Windows 8 (which is in beta at the moment).
Crittercism — Configuring the Project
To get started with Crittercism, there are a few steps to follow:
- Register an application.
- Download and import the SDK.
- Configure the Xcode project.
After you login to Crittercism, assuming it’s your first time, you will end up at the following screen:
Tap the big blue button on the top left. You’ll be presented with the following screen:
This will ask you to assign a name; for this project, put “crashy”. Then select the iOS platform, and don’t bother to invite any collaborators. You’re just testing the application, so choose No for the question “In App Store?”. When you’re done, click the big blue Register App button at the bottom.
Next you will be prompted to download the most recent version of the SDK (which is 3.5.1 at the time of this writing). Here’s the link to the download page, just in case.
Open a new, unfixed copy of the crashy-starter project in Xcode, unzip the Crittercism SDK, drag and drop the folder named CrittercismSDK onto the root of our project, and make sure “Copy items into destination group’s folder” is checked.
Go to the Build Phases tab of the target. Open the Link Binary with Libraries section by clicking on it. You’ll notice that libCrittercism
is already linked. Add the SystemConfiguration framework and QuartzCore by clicking on the plus sign, as shown below:
Next, you need to find the App ID. This is the ID for Crittercism and has nothing to do with Apple’s application ID. Head to the Crittercism dashboard, select your application from the list, and click the settings tab on the left. You will see a section that looks like this:
This provides a portion of code to copy and paste into application:didFinishLaunchingWithOptions:
ofSMAppDelegate.m. First, add the following import statement at the top of SMAppDelegate.m:
#import "Crittercism.h" |
Now add the Crittercism supplied code to the top of application:didFinishLaunchingWithOptions:
:
[Crittercism enableWithAppID:@"<YOUR_CRITTERCISM_APP_ID>"]; |
And that’s it, your project is configured to work with Crittercism!
Crittercism — Uploading the dSYM
Unlike Crashlytics, you have to manually upload your dSYM file to the server. To find the dSYM file, follow the instructions in the first part of the tutorial in the Symbolication section.
Recall from the first part of this tutorial that a dSYM is actually a directory. For this reason, you’ll need to zip your dSYM folder first before uploading it to Crittercism. To upload your zipped dSYM file use the tab Upload dSYMs in the “Settings” section of your app, as shown below:
Note: If you are still having problems uploading your dSYM file, check out this YouTube video that screencasts the process, step by step.
Once you have uploaded the dSYM folder, run your application, either on your simulator or on your device, and the console should print the following message:
Crittercism successfully initialized. |
Let’s start by tracking down the first bug.
Crittercism — Running the App
As you did before, run the application without Xcode, attempt to delete a cell, and restart the application. Now visit the Crash Reports section of your application on the back-end. You should see your crash report as shown below:
The dashboard will show an overview of your application with a big graph at the top and the list of issues at the bottom. Just pretend you don’t know the what the source of the bug is, and click the issue in the list to inspect it. You’ll see a detailed report similar to the following:
The “Reason” column should lead you to think there is an issue with the table view. You might see more details, but the log is not yet symbolicated, so you don’t know which file and line of code is responsible for the crash.
You’ll need to add the log to a queue to be symbolicated, by clicking the Add to Symbolication Queue link highlighted in the screenshot above. Once it has been symbolicated, you will see the decoded thread of the crash, which indicated an issue on line 80 in file SMViewController.m, as shown below:
One shortcoming of Crittercism is that I couldn’t find a way to queue crash logs automatically as they get uploaded. If you’ve found a way around this, please let me know in the comments!
If you select the “Users” tab, you’ll notice that Crittercism automatically generates IDs to identify users, as shown in the following screenshot:
You can now mark the issue as resolved using the drop down menu on the left side of the graph as shown below:
If you’d like to collect more specific data about users, the Crittercism class also provides the following methods:
[Crittercism setUsername:(NSString *)username]; [Crittercism setEmail:(NSString *)email]; [Crittercism setGender:(NSString *)gender]; [Crittercism setAge:(int)age]; [Crittercism setValue:(NSString *)value forKey:(NSString *)key]; |
An interesting feature is the ability to log handled exceptions on the server as follows:
@try { [NSException raise:NSInvalidArgumentException format:@"Argument must be a string"]; } @catch (NSException *exc) { [Crittercism logHandledException:exc] } |
Crittercism also offers breadcrumb logging for enterprise accounts and an assortment of other features.
Crittercism — Summing it Up
This concludes the how-to portion on Crittercism. The Crittercism framework also supports Android, Windows 8 and HTML5.
I’ve found that the user interface for the back-end is a bit complex, requiring a few extra clicks to get to the required information. It’s a bit clumsy to manually upload the dSYM for each build, but overall, it’s a solid framework.
Okay Class — Time for a Break!
Okay, it’s time for a short break from your crash reporting studies. Answer the following tricky question.
Solution Inside: Is divide-by-zero an operation that causes a crash on ARM? | Show |
---|---|
BugSense
Bugsense is another full-stack service, used by big companies such as Samsung, Intel and Groupon. It supports iOS, Android, Windows 8, Windows Phone and HTML5.
Bugsense — Configuring the Project
Once you are logged in to Bugsense, you will end up in the dashboard which should be empty if this is your first time using Bugsense. Click the “Add New Project” button, enter a name for your project, select iOS for Technology, Testing for the stage, then click Submit, as shown below:
The website will then show a dashboard with no data, since you have not yet integrated the framework. When you first visit your project page, a welcoming lightbox will greet you which will contain your API key, as so:
Copy down this key as you’ll be needing it later!
Unfortunately there is no direct link to download the SDK, so you will have to click the Docs item in the top right bar of the dashboard. This will open the generic documentation page, from which you have to select iOS. You will end up at this link. This page should contain a URL to download the iOS SDK (which is version 3.2 at the time of this writing).
Unzip the downloaded ZIP file and it will create a folder named BugSense-iOS.framework. Next, open a new copy of the starter project and go to the Build Phases tab. As previously, expand Link Binary With Libraries, click the + button, choose Add other, and select the unzipped BugSense-iOS.framework folder. Also add SystemConfiguration and libz.dylib. The link section should now look like the following:
Switch to the Build Settings tab and make sure that “Strip Debug Symbols During Copy” and “Strip Linked Product” are both set to YES as shown here:
Now you just need to insert a few hooks to integrate your app with Bugsense. Insert the following statement at the top of SMAppDelegate.h:
#import <BugSense-iOS/BugSenseController.h> |
Next, open SMAppDelegate.m and add the following code to the beginning ofapplication:didFinishLaunchingWithOptions:
, using the API key you copied down earlier:
[BugSenseController sharedControllerWithBugSenseAPIKey:@"YOUR API KEY"]; |
Now you’re ready to test Bugsense with your first bug!
Bugsense — Running the App
Just like in the previous sections, build and archive your project, install the application (.ipa file) on the device, and zip the dSYM file for later upload. Once the application is installed, run it, make it crash by deleting a row, and restart the app. Head over to the dashboard of Bugsense and open your project. You’ll see the following report:
A nice graph shows the number of crashes per day. Underneath is the list of error logs — just one in this case. You can immediately see the generic reason behind this issue; click the issue to open the detailed report as shown below:
The stack trace as shown is complete, but has not yet been symbolicated. Click the Symbolicate link on the top right. In this case, it will fail to symbolicate the trace because the dSYM is not yet on the server, and the button will turn green with a label of “Upload Symbols”.
Click the Upload Symbols button, and you will be redirected to the settings section of the application, where you can upload your zipped dSYM folder, as shown below:
Once you have uploaded your dSYM files, head back to the detailed view of the error log and again click theSymbolicate link. This time you will see that line 7 is decoded, clearly showing the line responsible for the crash:
Below the crash log, there is also a handy table showing lots of details about the crash. This table can be customized using the gear icon on the right to show many more pieces of data like locale, milliseconds elapsed from the start of the application, data about the mobile carrier and WiFi status, as shown below:
Personally, I am a bit concerned about the UDID, which has been deprecated since iOS5.
To mark a crash as resolved, return to the application dashboard, select the crash in question, then press the button labeled Resolve Selected, as below:
You can also provide the number of the release that includes the fix by clicking the Settings link in the navigation bar then selecting Fix Notifications. You can even personalize the message to be sent via push notification like so:
Bugsense also supports breadcrumbs, but they are only accessible in higher paid plans.
Bugsense — Summing it Up
This ends the how-to portion of Bugsense. The platform is quite feature-rich and supports Windows8, Windows Phone and HTML5.
A desktop application to automate the upload of dSYMs would definitely be appreciated, as well as the automatic symbolication of crash logs when they are received on the server.
TestFlight
TestFlight was born as a tool to manage the distribution of beta releases. Over time the developers added many more features, like action logging and crash reporting.
TestFlight has been adopted by companies such as Adobe, Instagram and tumblr to manage over-the-air deployment, tracking and crash reporting. iOS is the only platform supported at the moment.
TestFlight — Configuring the Project
As mentioned in the previous part of this article, TestFlight is a bit more than a crash reporting system, for it also allows you to recruit testers and distribute your test builds. Once you have logged in, head to the SDK download page to download the SDK. The current version at the time of this writing is 1.2.4.
Open a new copy of the starter project, unzip the TestFlight SDK, drag the entire folder onto the root of the project, and make sure “Copy items into destination folder (if needed)” is checked.
In the target settings, select the Build Phases tab and then open the Link Binary With Libraries section.libTestFlight.a should appear in the list of linked libraries. Also click the “+” button and add the libz.dylib as shown below:
Select the Build Settings tab and select “NO” for the following flags:
- Deployment Postprocessing
- Strip Debug Symbols During Copy
- Strip Linked Product
Compile the application using Cmd+B to check if everything is ok. Next you’ll need a particular token to make your app work with TestFlight. To get this token, go to your TestFlight team dashboard and copy the team token displayed. That’s the key you’ll need to initialize the framework in the code.
Open SMAppDelegate.m and add the following import:
#import "TestFlight.h" |
Insert the following code at the beginning of application:didFinishLaunchingWithOptions:
inSMAppDelegate.m:
[TestFlight takeOff:@"YOUR TOKEN HERE"]; ///< Replacing "YOUR TOKEN HERE" with the token you found on your team dashboard |
Now you’re ready to test the application against the first bug. I suggest you download and install the companion desktop application before doing this, which you can download from here. This app is smart enough to detect when you have archived an application and offers to upload both the .ipa file and the dSYM. This will save you some pointing and clicking on the TestFlight website.
If you’ve never set up a binary for adhoc distribution, TestFlight has provided some detailed instructions to get you started.
If you use the desktop application (which I highly suggest you do!), follow the wizard’s instructions and at the end you’ll be prompted to copy out the share URL. That’s the URL of the new build you’ve just submitted — you can send this link to your testers.
To install the app to your device, simply open that URL on your device. It’s that easy! :]. Run the app, crash it by swiping and hitting “Delete”, and restart the app. Now check to see if your crash report shows up on the web back-end by heading to the TestFlight dashboard. If all went as expected, you should see something similar to the following:
Note: Crash logs sometimes take a while before showing up on the web interface. TestFlight support indicated that these issues will be resolved in the next release of the SDK.
This view already provides some interesting data about your build – the number of crashes, feedback and total installations. In your case, you should see one install and one crash. Click the Latest build link to get the detailed report, as shown below:
A menu on the left gives you an overview of the data related to the build, such as the number of session and the total number of crashes. In the middle section you can see the list of users and on the far right, a summary of the recent activity performed on the application by users.
Tap on the Crashes tab on the left and expand the log using the little arrow on the right. You’ll see the complete stack trace, as below:
Another interesting feature in TestFlight is the ability to add checkpoints. Checkpoints are much like breadcrumbs or events – you spread some log statements in key places in your code to get a better idea of what happened before a crash.
To place a checkpoint in your code, use the method passCheckpoint
.
Open SMViewController.m, import the TestFlight framework:
#import "TestFlight.h" |
Now insert the following code at the end of viewDidLoad
:
[TestFlight passCheckpoint:[NSString stringWithFormat:@"view loaded with %i pizzas", pizzaOrder.count]]; |
Create a new build and distribute it. Install it on your device, run it, scroll down the table view to make it crash, and restart the app to allow the sending of logs to the server. Now open the dashboard, select the new build and click the Checkpoints tab on the left to see the list of checkpoints. You’ll see something similar to the screenshot below:
If you click the Crashes tab, the second bug report will appear:
TestFlight offers a feature which is especially nice during beta testing – collecting feedback from your users directly in your app! Open SMViewController.m, and the following import statement to the top of the file:
#import "TestFlight.h" |
Now modify tableView:commitEditingStyle:forRowAtIndexPath: as follows:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { [TestFlight openFeedbackView]; } |
Create a new build and upload it. Open the app, swipe, and tap to delete. Once the delete button is tapped, a view controller like the following will appear:
From here the user can send you direct feedback about the current build. You can see the messages sent by users in the “Feedback” section of your build, as so:
TestFlight — Summing it Up
TestFlight is a complete service that covers distribution, crash reporting and remote logging. However, I have noticed that sometimes the dSYM is not uploaded successfully by the desktop application and you’ll have to re-upload it using the website.
HockeyApp
HockeyApp is pretty well known in the indie developer world — probably because it’s made by indie developers! :]
HockeyApp supports iOS, Android, MacOS, and Windows Phone. HockeyApp, like TestFlight, is more than a crash reporting tool. It also allows you to manage the distribution of builds to beta testers, as well as providing a platform to collect feedback.
HockeyApp — Configuring the Project
Once you have logged into the site, you will be greeted by an empty dashboard like so:
You can create a new app using the web site, but using the Desktop application is far easier. Here’s the steps to create your new app with HockeyApp:
- Create an API token.
- Download and install the desktop app.
- Configure your project with the API token.
- Configure your Xcode project.
- Archive the project.
First, click your account name on the top right and select “API Tokens”, as shown below:
Next, you can set the access privileges to the platform in the dialog presented below:
Leave the settings at their defaults (All Apps and Full Access) so that the API key will give you what is effectively “root access” to the platform. Click “Create” and the API token will appear. Copy it and save it somewhere.
Unfortunately there is no direct download link for the desktop application, but the link should be available in the “Installation” section on this knowledge base page.
Run the app and paste the API token in the Preferences pane, as shown here:
How cool would it be if after every archive was built in Xcode, the HockeyApp desktop app opened automatically, ready to upload the new build? Well, you can do that!
Copy your unmodified starter project and open it in Xcode. Select the root of the project, then selectProduct/Scheme/Edit Scheme (or CMD+<), expand the Archive action, and finally select Post-actions, as shown below:
Click the + button on the bottom left, select New Run Script Action and paste the following command into the field shown below:
open -a HockeyApp "${ARCHIVE_PATH}" |
Click OK to save. Now archive your current project. The desktop application should detect the archive and show the following dialog:
As you can see, the desktop application has automatically detected the applicationID, version number and has located both .ipa and .dSYM files to be uploaded on the server. Make sure “Download Allowed” is checked, and click Upload to send it to the server.
Open the dashboard and click on the build you just uploaded, as shown below:
Notice that users and devices have been automatically detected. Neat! Copy the “App ID” and save it somewhere. You’ll need it to configure the crash reporting system. Now the application is ready to be distributed. You’ll find the download phase to be quite streamlined as well.
Head to http://config.hockeyapp.net/ on your iPhone and tap the “Install” link. This will ask you to install the HockeyApp profile and register your device. It will also install a webclip that will appear as an application icon on your device. Once that’s complete, you’ll be able to see the list of applications available for your device, as below:
Tap your app to install it and check that it runs correctly.
So far you have been dealing just with the distribution part. It’s time to integrate crash reporting. Head tohttp://hockeyapp.net/releases/ to download the client SDK for iOS (the current version as of this writing is 3.0). Download the binary version and unzip it. Drag and drop the folder HockeySDK.embeddedframework onto your Xcode project and make sure “Copy items into destination group’s folder” is checked.
Select the root in Project Navigator, select Project and in the Info tab set Configurations to “HockeySDK” as shown below:
Open SMAppDelegate.h and add the following import to the top of the file:
#import <HockeySDK/HockeySDK.h> |
You now need to implement the HockeyApp protocols in the delegate. Open SMAppDelegate.h and modify the delegate interface as follows:
@interface SMAppDelegate : UIResponder <UIApplicationDelegate,BITHockeyManagerDelegate, BITUpdateManagerDelegate, BITCrashManagerDelegate> ... @end |
Next, open SMAppDelegate.m and add the following code to the beginning ofapplication:didFinishLaunchingWithOptions:
, using the App ID you saved earlier:
[[BITHockeyManager sharedHockeyManager] configureWithIdentifier:@"YOUR APP ID" delegate:self]; [[BITHockeyManager sharedHockeyManager] startManager]; |
Add the following method to SMAppDelegate.m:
- (NSString *)customDeviceIdentifierForUpdateManager:(BITUpdateManager *)updateManager { #ifndef CONFIGURATION_AppStore if ([[UIDevice currentDevice] respondsToSelector:@selector(uniqueIdentifier)]) return [[UIDevice currentDevice] performSelector:@selector(uniqueIdentifier)]; #endif return nil; } |
All of this code will allow you to collect data about the user installations only when the build is not targeting the App Store; that is, when you are in the beta testing phase. To distinguish your new app version from the old version, update the app version in APPNAME-Info.plist. Now you are ready to test your first crash report! First, archive your application and use the desktop application to upload it to the server.
Note: If you have already uploaded a version of the app to HockeyApp, you will need to increase the version number of the app. To do so, click on the root project. Select “crashy” underneath “Targets”, click the “Summary” tab, then increase your version number, as so:
On your device, delete the previous version of your pizza app, and open the web clip that was previously installed to your device. Alternately, you can visit https://rink.hockeyapp.net/apps to do the same thing. There you will see the new release.
Install the new app version, run it, make it crash by deleting a row, and restart the app to allow the crash reports to be uploaded to the sever. After the restart, the application will show an alert view to ask if you want to send a crash report. To do so, tap Send Report. Now visit the HockeyApp dashboard and select your build. It will appear like the following:
There are two versions of the app, as expected, and the newest version shows a crash. Click the row corresponding to the newest version and click the Crashes tab at the top, as shown below:
As you can see the crash report has already been symbolicated and shows the line responsible for the crash. Click it and it will show the details of the stack trace, as below:
Once you have fixed the bug, you can close it using the status drop-down at the top left. For the next crash, you will implement some logging. HockeyApp does not include a built-in logger so you will need to add one. You will use the well-known CocoaLumberjack. Download the zip file from GitHub, unzip it, and drag and drop the folder named “Lumberjack” onto the project root.
Now open SMAppDelegate.m and add the following import statements at the top:
#import "DDLog.h" #import "DDASLLogger.h" #import "DDTTYLogger.h" #import "DDFileLogger.h" |
Directly below the import statements in SMAppDelegate.m, add the following code:
@interface SMAppDelegate () @property (nonatomic, strong) DDFileLogger *fileLogger; @end |
Still working in SMAppDelegate.m, add the following code toapplication:didFinishLaunchingWithOptions:
just before the initialization code for HockeyApp:
_fileLogger = [[DDFileLogger alloc] init]; _fileLogger.maximumFileSize = (1024 * 64); _fileLogger.logFileManager.maximumNumberOfLogFiles = 1; [_fileLogger rollLogFile]; [DDLog addLogger:_fileLogger]; [DDLog addLogger:[DDASLLogger sharedInstance]]; [DDLog addLogger:[DDTTYLogger sharedInstance]]; |
Now add the following two methods to SMAppDelegate.m:
- (NSString *) getLogFilesContentWithMaxSize:(NSInteger)maxSize { NSMutableString *description = [NSMutableString string]; NSArray *sortedLogFileInfos = [[_fileLogger logFileManager] sortedLogFileInfos]; NSUInteger count = [sortedLogFileInfos count]; for (NSInteger index = count - 1; index >= 0; index--) { DDLogFileInfo *logFileInfo = [sortedLogFileInfos objectAtIndex:index]; NSData *logData = [[NSFileManager defaultManager] contentsAtPath:[logFileInfo filePath]]; if ([logData length] > 0) { NSString *result = [[NSString alloc] initWithBytes:[logData bytes] length:[logData length] encoding: NSUTF8StringEncoding]; [description appendString:result]; } } if ([description length] > maxSize) { description = (NSMutableString *)[description substringWithRange:NSMakeRange([description length]-maxSize-1, maxSize)]; } return description; } - (NSString *)applicationLogForCrashManager:(BITCrashManager *)crashManager { NSString *description = [self getLogFilesContentWithMaxSize:5000]; if ([description length] == 0) { return nil; } else { return description; } } |
The first method retrieves logged contents from a local file, while the second method implements a delegate method for the crash manager of HockeyApp.
The application is now set up to collect logs locally using the Lumberjack framework, and to send them to the server via the HockeyApp framework.
Open SMViewController.m and add the following import:
#import "DDLog.h" |
In SMViewController.m add the following statement to the end of viewDidLoad
:
DDLogVerbose(@"SMViewController did load"); |
This small logging statement will tell us if the view loaded successfully or not. But as it stands, you won’t actually be able to see that log line because the log level has not been set. Add the following code directly after the #import
statements in SMViewController.m.
static const int ddLogLevel = LOG_LEVEL_VERBOSE; |
I generally set the logging level to verbose, since I prefer to have as many details as possible when debugging.
Finally, modify tableView:willDisplayCell:forRowAtIndexPath:
to reflect the code below:
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.row == pizzaOrder.count-1) { DDLogError(@"willDisplayCell - pizzas are %i", pizzaOrder.count); [[NSNotificationCenter defaultCenter] postNotificationName:LOAD_MORE_NOTIFICATION object:nil]; } } |
This will log the number of pizzas before the notification is posted.
Note: DDLogError
is the only macro that works synchronously, so you can call it even right before a crash. All the other methods such as DDLogVerbose
and DDLogInfo
are asynchronous, so they might not do their job if they are called immediately before a crash.
Now update the application version, archive it and let the desktop application upload it to the server. Delete the old version on the device and install the new one using the web clip.
Note: HockeyApp will detect older versions of a build and will prompt you to update the build from within the app itself. This happens behind the scenes once you upload a new version, so users will not have to manually visit a web address. They just need to launch the app. Pretty convenient, eh?
Run the application, scroll to the bottom to make it crash, and restart the app to send the crash report. Now visit the dashboard of your app and you will see the crash reported as such:
Return to the HockeyApp dashboard. Click on the crash to get to the detailed view, then click Crash Logs in the tabs above. This will show the raw log. You should see three tabs below the crashes. Click theDescription tab and lo and behold, there are your log statements:
Sometimes applications crash during startup. That makes it impossible to send reports to the server, because the application literally has no time to detect a previous crash and send it over-the-air. HockeyApp is the only tool that allows you to delay the initialization phase (and thus delaying the crash) and gives precedence to the “send to server” procedure so that your crash reports can be uploaded.
You can find some more details on handling startup crashes with HockeyApp here.
HockeyApp — Summing it Up
HockeyApp is a great tool to manage distribution, remote logging and crash reporting. A disadvantage of the service is that it does not include built-in logging. I really appreciated the automation of hooking up with the archive action of Xcode, which saves you some time and stress when you create a new build.
Sometimes the information you need is a bit hidden, such as remote logs, and requires quite a number of clicks to be displayed. I have already expressed my preference for Crashlytics if you need just crash reporting and logging, but I’d definitely consider HockeyApp if you need also distribution management.
Conclusion
For your benefit, I’ll repeat the comparison table included in the first part of the tutorial:
I’ll confirm the verdict that I expressed in the first tutorial: Crashlytics is an excellent solution if you need to be up and running quickly and you like a well done back-end.
If you need more than just crash reports and remote logging, and if you are targeting platforms other than iOS, HockeyApp and Bugsense will work really well for you.