Collect and analyze on-device performance metrics and crash diagnostics using MetricKit. Use when setting up MXMetricManager, handling MXMetricPayload or…
MetricKit Diagnostics
Collect aggregated performance metrics and crash diagnostics from production
devices using MetricKit. The framework delivers daily metric payloads (CPU,
memory, launch time, hang rate, animation hitches, network usage) and
immediate diagnostic payloads (crashes, hangs, disk-write exceptions) with
full call-stack trees for triage.
Contents
Subscriber Setup
Receiving Metric Payloads
Receiving Diagnostic Payloads
Key Metrics
Call Stack Trees
Custom Signpost Metrics
Exporting and Uploading Payloads
Extended Launch Measurement
Xcode Organizer Integration
Common Mistakes
Review Checklist
References
Subscriber Setup
Register a subscriber as early as possible — ideally in
application(_:didFinishLaunchingWithOptions:) or App.init. MetricKit
starts accumulating reports after the first access to MXMetricManager.shared.
import MetricKit
final class MetricsSubscriber: NSObject, MXMetricManagerSubscriber {
static let shared = MetricsSubscriber()
func subscribe() {
MXMetricManager.shared.add(self)
}
func unsubscribe() {
MXMetricManager.shared.remove(self)
}
func didReceive(_ payloads: [MXMetricPayload]) {
// Handle daily metrics
}
func didReceive(_ payloads: [MXDiagnosticPayload]) {
// Handle diagnostics (crashes, hangs, disk writes)
}
}
UIKit Registration
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
MetricsSubscriber.shared.subscribe()
return true
}
SwiftUI Registration
@main
struct MyApp: App {
init() {
MetricsSubscriber.shared.subscribe()
}
var body: some Scene {
WindowGroup { ContentView() }
}
}
Receiving Metric Payloads
MXMetricPayload arrives approximately once per 24 hours containing
aggregated metrics. The array may contain multiple payloads if prior
deliveries were missed.
func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
let begin = payload.timeStampBegin
let end = payload.timeStampEnd
let version = payload.latestApplicationVersion
// Persist raw JSON before processing
let jsonData = payload.jsonRepresentation()
persistPayload(jsonData, from: begin, to: end)
processMetrics(payload)
}
}
Availability: MXMetricPayload — iOS 13.0+, macOS 10.15+, visionOS 1.0+
Receiving Diagnostic Payloads
MXDiagnosticPayload delivers crash, hang, CPU exception, disk-write, and
app-launch diagnostics. On iOS 15+ and macOS 12+, diagnostics arrive
immediately rather than bundled with the daily report.
func didReceive(_ payloads: [MXDiagnosticPayload]) {
for payload in payloads {
let jsonData = payload.jsonRepresentation()
persistPayload(jsonData)
if let crashes = payload.crashDiagnostics {
for crash in crashes {
handleCrash(crash)
}
}
if let hangs = payload.hangDiagnostics {
for hang in hangs {
handleHang(hang)
}
}
if let diskWrites = payload.diskWriteExceptionDiagnostics {
for diskWrite in diskWrites {
handleDiskWrite(diskWrite)
}
}
if let cpuExceptions = payload.cpuExceptionDiagnostics {
for cpuException in cpuExceptions {
handleCPUException(cpuException)
}
}
if let launchDiags = payload.appLaunchDiagnostics {
for launchDiag in launchDiags {
handleSlowLaunch(launchDiag)
}
}
}
}
Availability: MXDiagnosticPayload — iOS 14.0+, macOS 12.0+, visionOS 1.0+
Key Metrics
Launch Time — MXAppLaunchMetric
if let launch = payload.applicationLaunchMetrics {
let firstDraw = launch.histogrammedTimeToFirstDraw
let optimized = launch.histogrammedOptimizedTimeToFirstDraw
let resume = launch.histogrammedApplicationResumeTime
let extended = launch.histogrammedExtendedLaunch
}
Run Time — MXAppRunTimeMetric
if let runTime = payload.applicationTimeMetrics {
let fg = runTime.cumulativeForegroundTime // Measurement<UnitDuration>
let bg = runTime.cumulativeBackgroundTime
let bgAudio = runTime.cumulativeBackgroundAudioTime
let bgLocation = runTime.cumulativeBackgroundLocationTime
}
CPU, Memory, and Responsiveness
if let cpu = payload.cpuMetrics {
let cpuTime = cpu.cumulativeCPUTime // Measurement<UnitDuration>
}
if let memory = payload.memoryMetrics {
let peakMemory = memory.peakMemoryUsage // Measurement<UnitInformationStorage>
}
if let responsiveness = payload.applicationResponsivenessMetrics {
let hangTime = responsiveness.histogrammedApplicationHangTime
}
if let animation = payload.animationMetrics {
let scrollHitchRate = animation.scrollHitchTimeRatio // Measurement<Unit>
}
Network and Cellular
if let network = payload.networkTransferMetrics {
let wifiUp = network.cumulativeWifiUpload // Measurement<UnitInformationStorage>
let wifiDown = network.cumulativeWifiDownload
let cellUp = network.cumulativeCellularUpload
let cellDown = network.cumulativeCellularDownload
}
App Exit Metrics
if let exits = payload.applicationExitMetrics {
let fg = exits.foregroundExitData
let bg = exits.backgroundExitData
// Inspect normal, abnormal, watchdog, memory, etc.
}
Call Stack Trees
MXCallStackTree is attached to each diagnostic (crash, hang, CPU exception,
disk write, app launch). Use jsonRepresentation() to extract and symbolicate.
func handleCrash(_ crash: MXCrashDiagnostic) {
let tree = crash.callStackTree
let treeJSON = tree.jsonRepresentation()
let exceptionType = crash.exceptionType
let signal = crash.signal
let reason = crash.terminationReason
uploadDiagnostic(
type: "crash",
exceptionType: exceptionType,
signal: signal,
reason: reason,
callStack: treeJSON
)
}
func handleHang(_ hang: MXHangDiagnostic) {
let tree = hang.callStackTree
let duration = hang.hangDuration // Measurement<UnitDuration>
uploadDiagnostic(type: "hang", duration: duration, callStack: tree.jsonRepresentation())
}
The JSON structure contains an array of call stack frames with binary name,
offset, and address. Symbolicate using atos or upload dSYMs to your
analytics service.
Availability: MXCallStackTree — iOS 14.0+, macOS 12.0+, visionOS 1.0+
Custom Signpost Metrics
Use mxSignpost with a MetricKit log handle to capture custom performance
intervals. These appear in the daily MXMetricPayload under signpostMetrics.
Creating a Log Handle
let metricLog = MXMetricManager.makeLogHandle(category: "Networking")
Emitting Signposts
import os
func fetchData() async throws -> Data {
let signpostID = MXSignpostIntervalData.makeSignpostID(log: metricLog)
mxSignpost(.begin, log: metricLog, name: "DataFetch", signpostID: signpostID)
let data = try await URLSession.shared.data(from: url).0
mxSignpost(.end, log: metricLog, name: "DataFetch", signpostID: signpostID)
return data
}
Reading Custom Metrics from Payload
if let signposts = payload.signpostMetrics {
for metric in signposts {
let name = metric.signpostName // "DataFetch"
let category = metric.signpostCategory // "Networking"
let count = metric.totalCount
if let intervalData = metric.signpostIntervalData {
let avgMemory = intervalData.averageMemory
let cumulativeCPUTime = intervalData.cumulativeCPUTime
}
}
}
The system limits the number of custom signpost metrics per log to reduce
on-device overhead. Reserve custom metrics for critical code paths.
Exporting and Uploading Payloads
Both payload types conform to NSSecureCoding and provide
jsonRepresentation() for easy serialization.
func persistPayload(_ jsonData: Data, from: Date? = nil, to: Date? = nil) {
let fileName = "metrics_\(ISO8601DateFormatter().string(from: Date())).json"
let url = FileManager.default.temporaryDirectory.appending(path: fileName)
try? jsonData.write(to: url)
}
func uploadPayloads(_ jsonData: Data) {
Task.detached(priority: .utility) {
var request = URLRequest(url: URL(string: "https://api.example.com/metrics")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = jsonData
_ = try? await URLSession.shared.data(for: request)
}
}
Retrieving Past Payloads
If the subscriber was not registered when payloads arrived, retrieve them
using pastPayloads and pastDiagnosticPayloads. These return reports
generated since the last allocation of the shared manager.
let pastMetrics = MXMetricManager.shared.pastPayloads
let pastDiags = MXMetricManager.shared.pastDiagnosticPayloads
Extended Launch Measurement
Track post-first-draw setup work (loading databases, restoring state) as
part of the launch metric using extended launch measurement.
let taskID = MXLaunchTaskID("com.example.app.loadDatabase")
MXMetricManager.shared.extendLaunchMeasurement(forTaskID: taskID)
// Perform extended launch work...
await database.load()
MXMetricManager.shared.finishExtendedLaunchMeasurement(forTaskID: taskID)
Extended launch times appear under histogrammedExtendedLaunch in
MXAppLaunchMetric.
Xcode Organizer Integration
Xcode Organizer shows the same MetricKit data aggregated across all users
who have opted in to share diagnostics. Use Organizer for trend analysis:
Metrics tab: Battery, performance, and disk-write metrics over time
Regressions tab: Automatic detection of metric regressions per version
Crashes tab: Crash logs with symbolicated stack traces
MetricKit on-device collection complements Organizer by letting you route
raw data to your own backend for custom dashboards, alerting, and filtering
by user cohort.
Common Mistakes
DON'T: Subscribe to MXMetricManager too late
The system may deliver pending payloads shortly after launch. Subscribing
late (e.g., in a view controller) risks missing them entirely.
// WRONG — subscribing in a view controller
override func viewDidLoad() {
super.viewDidLoad()
MXMetricManager.shared.add(self)
}
// CORRECT — subscribe in application(_:didFinishLaunchingWithOptions:)
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions opts: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
MXMetricManager.shared.add(metricsSubscriber)
return true
}
DON'T: Ignore MXDiagnosticPayload
Only handling MXMetricPayload means you miss crash, hang, and disk-write
diagnostics — the most actionable data MetricKit provides.
// WRONG — only implementing metric callback
func didReceive(_ payloads: [MXMetricPayload]) { /* ... */ }
// CORRECT — implement both callbacks
func didReceive(_ payloads: [MXMetricPayload]) { /* ... */ }
func didReceive(_ payloads: [MXDiagnosticPayload]) { /* ... */ }
DON'T: Process payloads without persisting first
The system delivers each payload once. If your subscriber crashes during
processing, the data is lost permanently.
// WRONG — process inline, crash loses data
func didReceive(_ payloads: [MXDiagnosticPayload]) {
for p in payloads {
riskyProcessing(p) // If this crashes, payload is gone
}
}
// CORRECT — persist raw JSON first, then process
func didReceive(_ payloads: [MXDiagnosticPayload]) {
for p in payloads {
let json = p.jsonRepresentation()
try? json.write(to: localCacheURL()) // Safe on disk
Task.detached { self.processAsync(json) }
}
}
DON'T: Do heavy work synchronously in didReceive
The callback runs on an arbitrary thread. Blocking it with heavy processing
or synchronous network calls delays delivery of subsequent payloads.
// WRONG — synchronous upload in callback
func didReceive(_ payloads: [MXMetricPayload]) {
for p in payloads {
let data = p.jsonRepresentation()
URLSession.shared.uploadTask(with: request, from: data).resume() // sync wait
}
}
// CORRECT — persist and dispatch async
func didReceive(_ payloads: [MXMetricPayload]) {
for p in payloads {
let json = p.jsonRepresentation()
persistLocally(json)
Task.detached(priority: .utility) {
await self.uploadToBackend(json)
}
}
}
DON'T: Expect immediate data in development
MetricKit aggregates data over 24-hour windows. Payloads do not arrive
immediately after instrumenting. Use Xcode Organizer or simulated payloads
for faster iteration during development.
Review Checklist
MXMetricManager.shared.add(subscriber) called in application(_:didFinishLaunchingWithOptions:) or App.init
Subscriber conforms to MXMetricManagerSubscriber and inherits NSObject
Both didReceive(_: [MXMetricPayload]) and didReceive(_: [MXDiagnosticPayload]) implemented
Raw jsonRepresentation() persisted to disk before processing
Heavy processing dispatched asynchronously off the callback thread
MXCallStackTree JSON uploaded with dSYMs for symbolication
Custom signpost metrics limited to critical code paths
pastPayloads and pastDiagnosticPayloads checked on launch for missed deliveries
Extended launch tasks call both extendLaunchMeasurement and finishExtendedLaunchMeasurement
Analytics backend accepts and stores MetricKit JSON format
Xcode Organizer reviewed for regression trends alongside on-device data
References
MetricKit framework
MXMetricManager
MXMetricManagerSubscriber
MXMetricPayload
MXDiagnosticPayload
MXCallStackTree
MXSignpostMetric
MXAppLaunchMetric
MXAppRunTimeMetric
MXCrashDiagnostic
Analyzing the performance of your shipping appdon't have the plugin yet? install it then click "run inline in claude" again.
set up metrickit to collect aggregated performance metrics (cpu, memory, launch time, hang rate, animation hitches, network usage) and immediate diagnostic payloads (crashes, hangs, disk-write exceptions, cpu exceptions) from production devices. use this skill when you need on-device telemetry beyond what app store connect offers, want custom signpost metrics for critical code paths, or need to route raw diagnostic data to your own backend for custom dashboards and alerting.
metrickit framework
subscriber registration context
backend endpoint (optional but recommended)
dsym symbols (for crash symbolication)
log handle for custom signposts (optional)
input: nothing (base setup)
output: metricsubscriber class conforming to mxmetricmanagersubscriber
create a singleton class that inherits from nsobject and conforms to mxmetricmanagersubscriber protocol. implement four callback methods: subscribe(), unsubscribe(), didreceive(mxmetricpayload array), and didreceive(mxdiagnosticpayload array). these callbacks will be invoked by the system when payloads arrive.
import MetricKit
final class MetricsSubscriber: NSObject, MXMetricManagerSubscriber {
static let shared = MetricsSubscriber()
func subscribe() {
MXMetricManager.shared.add(self)
}
func unsubscribe() {
MXMetricManager.shared.remove(self)
}
func didReceive(_ payloads: [MXMetricPayload]) {
// Handle daily metrics (step 2)
}
func didReceive(_ payloads: [MXDiagnosticPayload]) {
// Handle diagnostics (step 3)
}
}
input: metricsubscriber singleton
output: subscriber registered with mxmetricmanager.shared
for uikit, call subscribe() in application(_:didfinishlaunchingwithoptions:). for swiftui, call it in app.init(). registration must happen before or immediately after app launch because the system may deliver pending payloads within seconds of startup.
uikit example:
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
MetricsSubscriber.shared.subscribe()
return true
}
swiftui example:
@main
struct MyApp: App {
init() {
MetricsSubscriber.shared.subscribe()
}
var body: some Scene {
WindowGroup { ContentView() }
}
}
input: mxmetricpayload array from didreceive callback
output: raw json files written to disk; processed metrics ready for upload
iterate over each mxmetricpayload. extract the timestamp window (timeStampBegin, timeStampEnd), application version, and raw json representation. persist the raw json to disk immediately before any processing to prevent data loss if your processing code crashes. dispatch async processing to avoid blocking the callback thread.
func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
let begin = payload.timeStampBegin
let end = payload.timeStampEnd
let version = payload.latestApplicationVersion
let jsonData = payload.jsonRepresentation()
persistPayload(jsonData, from: begin, to: end)
Task.detached(priority: .utility) {
self.processMetrics(payload)
}
}
}
func persistPayload(_ jsonData: Data, from: Date? = nil, to: Date? = nil) {
let fileName = "metrics_\(ISO8601DateFormatter().string(from: Date())).json"
let url = FileManager.default.temporaryDirectory.appending(path: fileName)
try? jsonData.write(to: url)
}
input: mxdiagnosticpayload array from didreceive callback
output: raw json files written to disk; individual diagnostics (crashes, hangs, disk writes, cpu exceptions, app launches) extracted and routed to handlers
iterate over each mxdiagnosticpayload. persist raw json first. then check for presence of each diagnostic type (crashdiagnostics, hangdiagnostics, diskwriteexceptiondiagnostics, cpuexceptiondiagnostics, applaunchdiagnostics) and handle each one. dispatch processing asynchronously to avoid blocking.
func didReceive(_ payloads: [MXDiagnosticPayload]) {
for payload in payloads {
let jsonData = payload.jsonRepresentation()
persistPayload(jsonData)
Task.detached(priority: .utility) {
if let crashes = payload.crashDiagnostics {
for crash in crashes {
self.handleCrash(crash)
}
}
if let hangs = payload.hangDiagnostics {
for hang in hangs {
self.handleHang(hang)
}
}
if let diskWrites = payload.diskWriteExceptionDiagnostics {
for diskWrite in diskWrites {
self.handleDiskWrite(diskWrite)
}
}
if let cpuExceptions = payload.cpuExceptionDiagnostics {
for cpuException in cpuExceptions {
self.handleCPUException(cpuException)
}
}
if let launchDiags = payload.appLaunchDiagnostics {
for launchDiag in launchDiags {
self.handleSlowLaunch(launchDiag)
}
}
}
}
}
input: mxcrashdiagnostic, mxhangdiagnostic, or other diagnostic objects with callstacktree property
output: json call stack data with exception type, signal, reason, hang duration; ready for symbolication
for each diagnostic type, extract the call stack tree via jsonrepresentation(). include metadata such as exception type, signal, termination reason (for crashes), or hang duration (for hangs). upload this data alongside your dsyms to an analytics backend for symbolication.
func handleCrash(_ crash: MXCrashDiagnostic) {
let tree = crash.callStackTree
let treeJSON = tree.jsonRepresentation()
let exceptionType = crash.exceptionType
let signal = crash.signal
let reason = crash.terminationReason
uploadDiagnostic(
type: "crash",
exceptionType: exceptionType,
signal: signal,
reason: reason,
callStack: treeJSON
)
}
func handleHang(_ hang: MXHangDiagnostic) {
let tree = hang.callStackTree
let duration = hang.hangDuration
uploadDiagnostic(
type: "hang",
duration: duration,
callStack: tree.jsonRepresentation()
)
}
input: mxmetricpayload object
output: extracted metric values (launch time histograms, cpu duration, peak memory, network bytes, hang time, animation hitch ratio, exit codes)
conditionally access metric properties on the payload. each property may be nil if not available on the platform or during the measurement window. extract values such as timeToFirstDraw, cumulativeCPUTime (measurement
if let launch = payload.applicationLaunchMetrics {
let firstDraw = launch.histogrammedTimeToFirstDraw
let optimized = launch.histogrammedOptimizedTimeToFirstDraw
let resume = launch.histogrammedApplicationResumeTime
let extended = launch.histogrammedExtendedLaunch
}
if let runTime = payload.applicationTimeMetrics {
let fg = runTime.cumulativeForegroundTime
let bg = runTime.cumulativeBackgroundTime
let bgAudio = runTime.cumulativeBackgroundAudioTime
let bgLocation = runTime.cumulativeBackgroundLocationTime
}
if let cpu = payload.cpuMetrics {
let cpuTime = cpu.cumulativeCPUTime
}
if let memory = payload.memoryMetrics {
let peakMemory = memory.peakMemoryUsage
}
if let responsiveness = payload.applicationResponsivenessMetrics {
let hangTime = responsiveness.histogrammedApplicationHangTime
}
if let animation = payload.animationMetrics {
let scrollHitchRate = animation.scrollHitchTimeRatio
}
if let network = payload.networkTransferMetrics {
let wifiUp = network.cumulativeWifiUpload
let wifiDown = network.cumulativeWifiDownload
let cellUp = network.cumulativeCellularUpload
let cellDown = network.cumulativeCellularDownload
}
if let exits = payload.applicationExitMetrics {
let fg = exits.foregroundExitData
let bg = exits.backgroundExitData
}
input: code path you want to measure (e.g., network fetch, database load), metricmanager log handle
output: custom signpost metric recorded in daily mxmetricpayload
create a log handle via mxmetricmanager.makeloghandle(category:). wrap your code with mxsignpost(.begin, ...) and mxsignpost(.end, ...) using the same log and signpost name. the system will aggregate these intervals and include them in the daily metric payload.
let metricLog = MXMetricManager.makeLogHandle(category: "Networking")
func fetchData() async throws -> Data {
let signpostID = MXSignpostIntervalData.makeSignpostID(log: metricLog)
mxSignpost(.begin, log: metricLog, name: "DataFetch", signpostID: signpostID)
let data = try await URLSession.shared.data(from: url).0
mxSignpost(.end, log: metricLog, name: "DataFetch", signpostID: signpostID)
return data
}
then read the aggregated signpost metrics from the payload:
if let signposts = payload.signpostMetrics {
for metric in signposts {
let name = metric.signpostName
let category = metric.signpostCategory
let count = metric.totalCount
if let intervalData = metric.signpostIntervalData {
let avgMemory = intervalData.averageMemory
let cumulativeCPUTime = intervalData.cumulativeCPUTime
}
}
}
input: json payload data (from jsonrepresentation() calls)
output: payload posted to remote endpoint; http status code received
create a url request to your backend endpoint with post method and content-type application/json. set the http body to the json data. dispatch the upload asynchronously at utility priority to avoid blocking the main thread or callback thread.
func uploadPayloads(_ jsonData: Data) {
Task.detached(priority: .utility) {
var request = URLRequest(url: URL(string: "https://api.example.com/metrics")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = jsonData
do {
let (_, response) = try await URLSession.shared.data(for: request)
if let httpResponse = response as? HTTPURLResponse, (200..<300).contains(httpResponse.statusCode) {
// Upload successful
} else {
// Handle non-2xx response
}
} catch {
// Handle network error (step 8 decision point: network failures)
}
}
}
input: mxmetricmanager.shared reference
output: array of mxmetricpayload and mxdiagnosticpayload objects missed since last manager allocation
if your subscriber was not registered when payloads were generated, check for missed data on the next app launch using pastPayloads and pastDiagnosticPayloads. these return reports generated since the last allocation of the shared manager instance.
func retrieveMissedPayloads() {
let pastMetrics = MXMetricManager.shared.pastPayloads
let pastDiags = MXMetricManager.shared.pastDiagnosticPayloads
for metric in pastMetrics {
didReceive([metric])
}
for diag in pastDiags {
didReceive([diag])
}
}
call this method in your subscriber initialization or early in app launch.
input: code that runs after first draw (database loading, state restoration, etc.)
output: launch time metric extended to include post-first-draw work
create an mxlaunchtaskid with a unique identifier. call extendlaunchmeasurement(fortaskid:) before the work, then finishextendedlaunchmeasurement(fortaskid:) after. the extended launch time will be recorded in histogrammedextendedlaunch within mxapplaunchmetric.
let taskID = MXLaunchTaskID("com.example.app.loadDatabase")
MXMetricManager.shared.extendLaunchMeasurement(forTaskID: taskID)
await database.load()
MXMetricManager.shared.finishExtendedLaunchMeasurement(forTaskID: taskID)
if you have a backend endpoint: upload json payloads asynchronously after persisting to disk. include auth headers or api keys if required by your backend.
else if you have no backend: persist json to disk and review payloads manually in your app's document folder, or use the xcode organizer for aggregated trends across all users who opted in to share diagnostics.
if network upload fails (connection timeout, 4xx, 5xx): log the error, keep the json file locally, and retry on the next app launch or on next metric delivery. do not discard the payload.
if you receive crash or hang diagnostics: extract the call stack tree, upload dsyms to your analytics backend (or use atos locally), and symbolicate the binary offsets into function names for readable triage.
if your callback subscriber crashes during processing: the system has already delivered the payload and will not re-deliver it. protect against data loss by persisting raw json before any processing code runs.
if the callback thread is blocked by heavy processing: payloads queue up and subsequent deliveries may be delayed. always dispatch processing asynchronously using task.detached(priority: .utility).
if you want to measure custom code paths: use custom signposts with mxmetricmanager.makeloghandle() and mxsignpost(). limit custom metrics to truly critical paths (network requests, disk i/o, background work) to minimize on-device overhead.
if your app was not launched when metric payloads arrived: check pastPayloads and pastDiagnosticPayloads on the next app launch to retrieve missed data.
if development iteration is slow (metrics take 24 hours to accumulate): use xcode organizer or simulated payloads for faster feedback during testing. metrickit aggregates over 24-hour windows; do not expect immediate data in development builds.
if you support ios 13 but need diagnostics (crashes, hangs): diagnostic payloads require ios 14.0+. metric payloads work on ios 13.0+. check platform availability before accessing diagnostic properties.
metric payloads (mxmetricpayload json):
diagnostic payloads (mxdiagnosticpayload json):