MetricKit: A boon for iOS framework developers to get Crash Logs

Vipul Arvind
10 min readApr 26, 2023

--

Here are the main points that will be covered in this article…

  • How to integrate MetricKit into iOS Swift frameworks and receive the crash reports?
  • Hidden nuances of MetricKit testing
  • How to Symbolicate the crash data sent by MetricKit?

Frameworks are building blocks that are utilized by app developers to speed up their work. Apart from code reusability, they are also useful for code compartmentalization, better testing and overall project organization.

Despite the huge value they provide, there have been some limitations when it comes to iOS frameworks. One of the major bottlenecks faced by frameworks previously was the inability to access crash logs of the applications that contained them.

Problem Statement

Please take a look at the figure below:

The above image shows multiple applications containing an iOS framework. Whenever a crash happens in any of these applications, the crash log is posted to the account (app store or testflight) of the owner of these applications. The framework developer traditionally didn’t have access to these logs and would have to work with the app developers to get them.

The Solution

Come iOS 13 and Apple launched MetricKit framework. This framework is meant for receiving the on-device diagnostics and power and performance metrics. And guess what, yes it does provide crash reports as well !!!

How it works

The app (and framework) developers integrate the MetricKit in their projects and the system delivers metric reports about the previous 24 hours to these registered apps at most once per day, and delivers diagnostic reports immediately in iOS 15 and later and macOS 12 and later.

The Beauty:

Now, it’s not just the apps but the included frameworks can also get direct access to these crash logs. The framework can integrate the MetricKit in the similar way a regular iOS application would do and start receiving the MetricKit reports / crash logs.

Let us make two projects - a framework and an application. We will:

  • have the crash functionality inside the framework and have app call this method.
  • have our framework register for the MetricKit notifications so that it can received the crash log (data).
  • use the framework’s dSYM file to symbolicate this crash data. It is important to note that we will be doing the symbolication without the main application’s dSYM file.

Setting up the project(s)

The Framework

Open the XCode and go to File -> New -> Project. Choose Framework (under Framework & Library section). Tap Next. Give a name to your framework (I gave SampleFW). Tap Next again. Select any folder where you want your framework project to go and finally tap Create.

Compile and verify that you have Products folder in the project navigator. If you don’t it in XCode then follow these steps:

Quit Xcode
Open the bundle SampleFW.xcodeproj
Edit the file Projects.pbxproj
Search for Products keyword

Simply move the /* Products */ line to the top of the “children” list as shown below

Save the projects.pbxproj file and reopen the project in XCode. The products folder should be visible now.

Follow the following steps to update the framework project settings to generate the dSYM file:

Select the SampleFW project in navigator window and then go to “Build Settings”.

Search for “Debug Information Format”. You should see something like this

Change the debug setting to “DWARF with dSYM File”. And now it should look like

Your framework project is all set up. Let’s now move to the application project

The Application

  1. Close the framework project and while still in Xcode, lets start a new project.
  2. Go to File -> New -> Project. Choose App (under Application section). Tap Next. Give a name to your application (I gave SampleApp) and I chose Swift language. Tap Next again. Select any folder where you want your application to go and finally tap Create.

At the moment, there is nothing else needed for the application project.

The Common Project

The aim for common project workspace is to be able to access both SampleApp and SampleFW together without having 2 instances of XCode opened. This also allows for quick sharing of the framework output with the sample application. Please follow the following steps:

  1. We have the SampleApp application opened in XCode. Select File-> Add Files to SampleApp option.
  2. Go to the location where your SampleFW project is and Select SampleFW.xcodeproj.
  3. Please “check” Copy Items if needed option as shown below and then tap Add
  4. Please confirm that the SampleApp (under Add to targets) is also checked.

At this moment the Combined Common Project is all set and if you have followed all the steps, then it should look like

Please note that this last step of “Setting up the Combined Common project” is not necessary. And it is done only for organizing the project(s) better in one place. You could keep the 2 projects separate and manually link the compiled framework to the sample application if you prefer.

Let’s get to coding !!!

Framework

  1. Currently we have the combined project opened in the XCode.
  2. Please ensure to change the target back to SampleFW (since we are going to make changes in the framework first)
  3. Select the SampleFW group in Project Navigator. This will ensure that the new file(s) are going to be added to this sub-project
  4. Chose File -> New -> File. Choose Swift File (under Source section). Tap Next. Give a name (I chose FWUtility) and then Create. This file will contain the method(s) that we will utilize (call from) the application.
  5. Add the following code to FWUtility File
import MetricKit

public class FWUtility: NSObject {
public override init() {
super.init()

let metricManager = MXMetricManager.shared
metricManager.add(self)
}

public func crashTheSDK(){
fatalError("SDK Crashed!!!")
}
}

extension FWUtility: MXMetricManagerSubscriber {
public func didReceive(_ payloads: [MXMetricPayload]) {
guard let firstPayload = payloads.first else { return }
print(firstPayload.dictionaryRepresentation())
}


public func didReceive(_ payloads: [MXDiagnosticPayload]) {
guard let firstPayload = payloads.first else { return }
print(firstPayload.dictionaryRepresentation())

// send this JSON containing the crash info to your server here
}
}

Build your framework. It should compile without any errors and warnings.

Here is what we are doing in the framework source.

  1. We simply included the MetricKit framework
  2. We have implemented the FWUtility class and implemented the init method. The FWUtility class is declared public since we will be using this in our SampleApp.
  3. In init method, we are getting access to MXMetricManager.shared object and then adding ourself to it so that we can receive notifications.
  4. We have one public method (crashTheSDK). This method is declared public for the same reason; it will be called from our app to actually simulate a crash. Since we want its definition to be available to the outside callers; hence the use of pubic qualifier.
  5. Further, we added an extension (to FWUtility) which confirms to MXMetricManagerSubscriber and implements the didReceive method(s).

Please note that the crash information comes in the MXDiagnosticPayload payload.

Application

Please ensure to change the target to SampleApp (Since we are going to make changes in the Application now)

Select the Main storyBoard under SampleApp group in Project Navigator. And add a button with title “Crash SDK” in the interface builder as shown below

Edit the Viewcontroller.swift file and add following code

import UIKit
import SampleFW

class ViewController: UIViewController {
let fwUtility = FWUtility()


override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}

@IBAction func crashSDKTapped(_ sender: Any) {
fwUtility.crashTheSDK()
}
}

Please note that you would need to link the crashSDKTapped method to the Touch up Inside Event.

There is one more step left before we can compile and run our application. We need to tell the application where to find the framework so that it can access its functionality. With application (SampleApp) as current target, please open General section and drag and drop the SampleFW from the project navigator under Frameworks, Libraries and Embedded Content as shown below

Now compile the application. And it is ready for testing.

Summarizing what we have done so far

  1. We have a framework (SampleFW) which exports a method named CrashTheSDK
  2. We have a sample application which simply invokes the CrashTheSDK method on tap of the button.

Some important Points on testing the MetricKit

Testing this was a pain…:) And trust me when I say to follow the testing steps (below) very carefuly if you don’t want to feel that pain.

  1. First of all, the MerticKit doesn’t work on Simulator. Hence you have to install the application on the device. So, please connect a device to your macbook and install the application on it.
  2. During the debug cycle, we will need to make the app crash. We will also be restarting the device. And we need to verify that we are able to get the callback (didReceive) in the file FWUtility.swift. So, we will set up one breakpoint in the XCode Project. Please see the image below for the this. We will have it right before we are going to print the crash log data received from the metricKit.

Please also remember:

  1. MetricKit takes some time to register the application to receive callbacks. Hence it will be around 15–20 minutes (minimum)from your first install that your app will start receiving the callbacks.
  2. We will need to run multiple cycles to be able to get the control on the breakpoint. It won’t happen on the first go. You will see below why.

Ready to Test !!!

Go get your code juice(Coffee) cause this testing will take some time (about 30–45 minutes). Here are the detailed steps:

Install the app on the device by connecting the device to the macbook and using “Run” option (with the physical device selected as target) under the product menu.

Once you see the application on the phone screen, Select the “Stop” option under build menu (from XCode) to kill the application.

Now go to the physical device. And this time, we will start the app by tapping the “SampleApp” Icon (and not via XCode Run option) on the physical device. Once the app is up and running, tap on the “Crash SDK” button to crash the application. It is very important that we start the application from the device when we are trying to crash the application.

Come back to the XCode on macbook and select Windows-> “Devices and Simulators” option. Select “View Device Logs”. You should see the crash log for SampleApp application. If you don’t see it, then sort according to Date/Time to see the most recent crash on top.

Close this “device logs” window and then “device and simulators” window as well. We should be back to XCode window. Let’s run the app again, this time we will start from XCode. And if everything is set up correctly, then it should hit our breakpoint. This is because the crash has happened, we have seen it in the device logs and our framework is registered (with MetricKit) to get the callback. But, to our surpise, the control will not come to breakpoint. This is because there is some amount of registration time that MetricKit needs. Usually 15–20 minutes is ok but let’s wait for 30 minutes. You can stop the debug cycle and have the phone still connected to the macbook and drink your coffee…:)

Wait for 20 -30 minutes and restart your device.

Now perform these steps again :

  • start the app from device (by tapping app icon)
  • crash the app (by tapping Crash SDK button)
  • Start the app in debug mode from XCode and wait for the control to hit the break point

Once you get the control on the breakPoint, the testing is complete. We have successfully integrated the MerticKit inside the framework which is getting the crash logs whenever the application is crashing. If you still don’t see it, please wait for some more time and try again. It may take another restart of your device as well, but you will eventually get the control at the breakpoint.

Also please don’t forget to copy the text in the Console Output window to some text file. This is the crash data that you may eventually want to send to your server in a production application.

Symbolicating the Crash Log

The crash info that is provided by MetricKit is not symbolicated.

Here are the steps for symbolication of the crash data that is provided by MetricKit.

Open the text file in which you have stored the crash data and search for “SampleFW”. This is the name of our framework. Please note down the value of the field “offsetIntoBinaryTextSegment” (in my case it is 28832).

address = 4332335264;
binaryName = SampleFW;
binaryUUID = "F57341C3-AF1C-39FE-B05A-A1A41BEDC39E";
offsetIntoBinaryTextSegment = 28832;
sampleCount = 1;
subFrames =

Open the calculator on your mac and change the view to “Programmer”. Select decimal and type-in the value (28832). Change the mode to hexadecimal and add 1 to the value. The final value I got in my case is 0x70A1.

Open a terminal and go to the folder where you have copied your dsym file and run the following command:

atos -arch arm64 -o SampleFW.framework.dSYM/Contents/Resources/DWARF/SampleFW -l 0x1 0x70A1

And here are the results that I got

As you can see, it gives the method name (crashTheSDK) in which the crash happened.

And this is the end of this long article…:) Have fun…

--

--