MetricsKit Diagnostics

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

In the last segment, you implemented the delegate to receive metrics payloads. In this segment, you’ll implement the 2nd delegate in MXMetricManagerSubscriber to receive app diagnostics.

Those payloads can be delivered to your app immediately if the issue wasn’t a crash, or in the next app launch if it was. Unlike metrics payloads that are sent at most once per day.

In MetricsKitService.swift add this method inside the extension:

public func didReceive(_ payloads: [MXDiagnosticPayload]) {
  payloads.forEach { payload in

  }
}

Payloads are also sent as a collection.

Each payload can contain those information:

  • CPU Exception Diagnostics
  • Disk Write Exception Diagnostics
  • Hang Diagnostics
  • App Launch Diagnostics
  • Crash Diagnostics

They’re all rather similar, and they are all collections too. Start by Crash Diagnostics.

Add the following in the loop:

payload.crashDiagnostics?.forEach { crashDiagnostic in
  var crashMessage = ""
  
  if let terminationReason = crashDiagnostic.terminationReason {
    crashMessage += "Termination Reason: \(terminationReason)"
  }
  
  if let exceptionType = crashDiagnostic.exceptionType {
    crashMessage += "\nException Type: \(exceptionType)"
  }
  
  if let exceptionCode = crashDiagnostic.exceptionCode {
    crashMessage += "\nException Code: \(exceptionCode)"
  }
  if let stackTrace = String(data: crashDiagnostic.callStackTree.jsonRepresentation(), encoding: .utf8) {
    crashMessage += "\nStackTrace: \(stackTrace)"
  }
  
  OTelLogs.sendLog(scope: "Crash Diagnostic", message: crashMessage)
}

You’re basically constructing a string to send as part of the log report. Since those values are optionals, you’ll add them only if they exist. The most interesting one is the Stack Trace as you may have already expected.

Before you build and run the app, change the project’s build settings for Debug Information Format for Debug to be DWARF with dSYM File

Set 'Debug Information Format' to be 'DWARF with dSYM File'
Set 'Debug Information Format' to be 'DWARF with dSYM File'

Build and run the app on your phone. The build log should show the location of where the dSYM file is generated. The file will be generated in the derived data folder. Open the build log and make sure that All Messages is selected and filter for “dSYM”. The path should be similar to this

/Users/[YOUR_USER]/Library/Developer/Xcode/DerivedData/TheMet-[RANDOM_LETTERS]/Build/Products/Debug-iphoneos/TheMet.app.dSYM

Go to that folder in the Finder app, and open the Terminal as you’ll use it shortly.

When the app starts stop the debug session, then launch the app directly from the phone without connecting it to Xcode. Press the red Crash button on the top. This button accesses a far index in store.objects its intentional to crash the app so you can see the crash logs.

Open the app again to receive the diagnostics and the app will automatically send it to Grafana.

Open Logs under Drilldown on Grafana and open the logs for TheMet:

The 'Logs' graph on Grafana showing the crash
The 'Logs' graph on Grafana showing the crash

If you click on the small eye button on the left of the log, it will open the full crash log message that you built from the app.

The stack trace is impossible to understand in this format. Making it meaningful directly on Grafana is quite complicated and its not worth the trouble. But its good to learn what this means

{
  "binaryUUID" : "720E87B1-09B9-3041-AC7D-1D6E74DB7BF5",
  "offsetIntoBinaryTextSegment" : 50088,
  "sampleCount" : 1,
  "subFrames" : [ ]
  "binaryName" : "TheMet.debug.dylib",
  "address" : 4316251048
}

This is an example of a stack trace entry, aka: Frame:

  • binaryUUID in a GUID representing the ID of the framework or a binary.
  • offsetIntoBinaryTextSegment is the memory address where the symbol exists.
  • subFrames are the nested stack traces. They have the same structure as this one.
  • binaryName is the name of the binary that contains the address.

Inside the crash log look and find the first entry that has a binary name of TheMet.debug.dylib. You may need to copy the JSON text into new xcode file or in VSCode, and fold the subFrames array to find the frame you need. Its probably the 6th from the top.

Take the value from offsetIntoBinaryTextSegment and convert it to its hex value and run this command in the Terminal:

atos -arch arm64 -o [PATH TO DSYM FILE] --offset [HEX VALUE]

atos is the command to identify the symbols from a crash report. You pass it the architecture, path of the symbols file and the address to identify, and it will give you the line of code.

The output on terminal should be:

closure #1 in closure #2 in closure #1 in closure #1 in ContentView.body.getter (in TheMet.debug.dylib) (ContentView.swift:66)

Open ContentView.swift on line 66 and you’ll find the reason of the crash:

print(store.objects[50])

You’ll notice that the usual crash message of “index out of bounds” didn’t exist, reading every frame to find its symbol from the terminal isn’t trivial and defies the purpose. But its worth knowing that any crash reporting tool like Crashlytics or Sentry does this for you. This is why they require the dSYM files. Without them, you’ll be seeing crash logs just the same way as you saw on your log.

The final project has the implementation of the other 4 diagnostics and their logs are uploaded in the same format.

You can build your own dashboards to track those diagnostics by sending them as events with an event name so you can filter them the same way as the metrics, or you can filter them with scope_name since they all have different values there. Or you can rely on other tools like Crashlytics, Sentry or many of the others that have large solutions to make your life easier so you can focus on making your apps better, not spend a lot of time and effort to build those tools yourself and end up reinventing the wheel. :]

See forum comments
Download course materials from Github
Previous: MetricsKit Metrics Next: Conclusion