Metrickit
Use when collecting or analyzing production iOS or iPadOS performance telemetry with MetricKit, including iOS 27 MetricManager async metric or diagnostic reports, hang or crash triage, custom signposts, extended launch measurement, durable export, or iOS 26 MXMetricManager compatibility.
- Skill ID
- dpearson2699/swift-ios-skills/metrickit
- Publisher
- dpearson2699
- Repository
- swift-ios-skills
- Installs
- 0
- Files
- 3
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
dpearson2699/swift-ios-skills/metrickitInstalls these files- SKILL.md
- evals/evals.json
- references/metrickit-patterns.md
What this skill tells the agent
MetricKit
Use MetricKit for low-overhead production telemetry that complements local Instruments and Xcode Organizer analysis. On iOS and iPadOS 27, prefer the Swift-first MetricManager report sequences. Keep MXMetricManager only in an explicit iOS 26 compatibility branch.
Beta-sensitive: The iOS/iPadOS 27 surface below is based on Apple's current beta documentation. It has not been locally compiler-verified because Xcode 27 is unavailable in this environment. Re-check the linked Apple documentation and compile with the shipping Xcode 27 SDK before release.
Load MetricKit Extended and Compatibility Patterns when implementing durable ingestion, detailed report analysis, or the iOS 26 compatibility path.
Contents
- MetricManager Setup
- Receiving Metric Reports
- Receiving Diagnostic Reports
- Key Metric Results
- Call Stack Trees
- Custom Signpost Metrics
- Durable Export and Upload
- Extended Launch Measurement
- iOS 26 Compatibility
- Xcode Organizer
- Scope Boundaries
- Common Mistakes
- Review Checklist
- References
MetricManager Setup
At app launch, create and retain one long-lived MetricManager. Start exactly one consumer task for metricReports and one for diagnosticReports.
Both properties expose nonthrowing AsyncSequence values:
metricReports: some AsyncSequence<MetricReport, Never>diagnosticReports: some AsyncSequence<DiagnosticReport, Never>
Apple documents that concurrent consumers of one sequence can receive nondeterministic subsets. Fan out only after the single consumer receives and durably stores a report; delayed subscription can miss reports.
import MetricKit
@available(iOS 27.0, *)
final class MetricsService {
private let manager = MetricManager()
private var metricTask: Task<Void, Never>?
private var diagnosticTask: Task<Void, Never>?
func start(
persistMetric: @escaping @Sendable (MetricReport) async -> Void,
persistDiagnostic: @escaping @Sendable (DiagnosticReport) async -> Void
) {
guard metricTask == nil, diagnosticTask == nil else { return }
let manager = manager
metricTask = Task {
for await report in manager.metricReports {
await persistMetric(report)
}
}
diagnosticTask = Task {
for await report in manager.diagnosticReports {
await persistDiagnostic(report)
}
}
}
deinit {
metricTask?.cancel()
diagnosticTask?.cancel()
}
}The persistence closures are application-specific. Implement them with the durable-first workflow below rather than dropping, logging only, or directly uploading each report. If state-scoped metrics are needed, construct the manager with the documented init(enabledStateReportingDomains:) initializer and the required domains.
Receiving Metric Reports
MetricReport is Codable and Sendable. It describes an interval through:
timeRange: DateInterval- optional
environmentmetadata intervalEntriesfor full-day and shorter interval measurementsstateEntriesfor measurements associated with application states
Metric reports normally arrive on a daily cadence. Persist the complete report before extracting individual results.
For daily analysis, read the documented fullDayEntry and switch over its MetricResult values:
let entry = report.intervalEntries.fullDayEntry
for result in entry.values {
switch result {
case .hangTime(let metric):
analyzeHangTime(metric)
case .peakMemory(let metric):
analyzePeakMemory(metric)
case .timeToFirstDraw(let metric):
analyzeLaunch(metric)
case .signpostInterval(let metric):
analyzeSignpost(metric)
@unknown default:
preserveUnknownMetric(result)
}
}Use @unknown default so a beta or future result does not make the ingestion pipeline brittle. Preserve the raw encoded report even when the current app does not understand a result.
Receiving Diagnostic Reports
DiagnosticReport is Codable and Sendable. It contains a timeRange, required environment metadata, and one DiagnosticResult.
Diagnostics are individual, event-based reports intended for prompt delivery when MetricKit produces them. Do not assume every crash, hang, or resource event generates a report; system sampling and eligibility still apply.
After durable storage, route the result explicitly:
switch report.result {
case .crash(let diagnostic):
analyzeCrash(diagnostic)
case .hang(let diagnostic):
analyzeHang(diagnostic)
case .cpuException(let diagnostic):
analyzeCPUException(diagnostic)
case .diskWriteException(let diagnostic):
analyzeDiskWrites(diagnostic)
case .appLaunch(let diagnostic):
analyzeLaunch(diagnostic)
case .memoryException(let diagnostic):
analyzeMemory(diagnostic)
@unknown default:
preserveUnknownDiagnostic(report)
}The iOS/iPadOS 27 diagnostic types are CrashDiagnostic, HangDiagnostic, CPUExceptionDiagnostic, DiskWriteExceptionDiagnostic, AppLaunchDiagnostic, and MemoryExceptionDiagnostic. The memory-exception case is new in iOS 27.
Useful fields include:
| Diagnostic | Important fields |
|---|---|
CrashDiagnostic | callStackTree, exception type/code/reason, signal, virtual-memory region, termination category/reason |
HangDiagnostic | callStackTree, hangDuration |
CPUExceptionDiagnostic | callStackTree, totalCPUTime, totalSampledTime |
DiskWriteExceptionDiagnostic | callStackTree, totalBytesWritten |
AppLaunchDiagnostic | callStackTree, launchDuration |
MemoryExceptionDiagnostic | callStackTree |
Key Metric Results
Choose a small telemetry vocabulary that matches the investigation rather than exporting every result. Prioritize the relevant categories: responsiveness and terminations; runtime, CPU, memory, network, and storage; or launch, display/GPU, and custom intervals.
Aggregate by app version and environment metadata, compare distributions rather than single values, and correlate regressions with releases. Avoid treating a daily aggregate as a precise trace of one user action.
Load the Key Metric Result Catalog when mapping exact MetricResult cases into a parser or dashboard.
Call Stack Trees
The iOS 27 CallStackTree replaces MXCallStackTree. It is Codable and Sendable and exposes:
forEachFramefor frame traversalcallStackThreadsfor thread-oriented analysisbinaryInfofor image and binary metadata
Store the complete diagnostic report before flattening or symbolication. Preserve binary identifiers and offsets so server-side symbolication can use matching archives and dSYMs.
Custom Signpost Metrics
Create an OS log through the manager, then use mxSignpost for begin/end measurement:
let log = manager.logHandle(category: "ImagePipeline")
mxSignpost(.begin, log: log, name: "Decode")
await decodeImage()
mxSignpost(.end, log: log, name: "Decode")MetricKit exposes the aggregate as MetricResult.signpostInterval. Do not search for a signpostMetrics array on MetricReport.
Use mxSignpost when MetricKit resource measurement is required. Apple documents that intervals created with OSSignposter and a MetricKit log handle do not populate the resource-measurement fields that mxSignpost provides.
Keep high-volume local tracing separate from the small set of stable production intervals used for MetricKit aggregation.
More from dpearson2699
Use this skill in a project
Open any RiverX project, search the Skills panel for dpearson2699/swift-ios-skills/metrickit, and install it in one click.
