# MetricKit patterns

Worked examples for the `metrickit-diagnostics` skill ([../SKILL.md](../SKILL.md)).

## Reading call-stack trees

Every diagnostic class (crashes, hangs, CPU and disk-write exceptions, slow
launches) exposes a `callStackTree`.

```swift
import MetricKit

func describe(_ crash: MXCrashDiagnostic) -> CrashRecord {
    CrashRecord(
        stack: crash.callStackTree.jsonRepresentation(),
        exception: crash.exceptionType?.intValue,
        signal: crash.signal?.intValue,
        reason: crash.terminationReason
    )
}

func describe(_ hang: MXHangDiagnostic) -> HangRecord {
    let seconds = hang.hangDuration.converted(to: .seconds).value
    return HangRecord(stack: hang.callStackTree.jsonRepresentation(), seconds: seconds)
}

struct CrashRecord { let stack: Data; let exception: Int?; let signal: Int?; let reason: String? }
struct HangRecord { let stack: Data; let seconds: Double }
```

The stack JSON lists each frame with its binary name, offset into the binary
and address. That is what `atos` or a crash service needs, together with the
matching dSYMs.

## Custom signposts end to end

### Emitting

```swift
import MetricKit
import os

let syncLog = MXMetricManager.makeLogHandle(category: "Sync")

func pullLatestRoutes(from endpoint: URL) async throws -> Data {
    mxSignpost(.begin, log: syncLog, name: "PullRoutes")
    defer { mxSignpost(.end, log: syncLog, name: "PullRoutes") }
    let session = URLSession(configuration: .ephemeral)
    let (payload, _) = try await session.data(from: endpoint)
    return payload
}
```

### Reading them back

```swift
func logSignposts(in payload: MXMetricPayload) {
    for metric in payload.signpostMetrics ?? [] {
        let intervals = metric.signpostIntervalData
        telemetryLog.info("""
            \(metric.signpostCategory)/\(metric.signpostName): \(metric.totalCount) runs, \
            avg memory \(String(describing: intervals?.averageMemory)), \
            cpu \(String(describing: intervals?.cumulativeCPUTime))
            """)
    }
}

let telemetryLog = Logger(subsystem: "com.example.trailhead", category: "metrics")
```

The system caps how many custom signpost metrics each log may record, to keep
overhead low. Spend them on the few paths that matter.

## Exporting and uploading

Both payload classes also conform to `NSSecureCoding`, but JSON is the
format backends want.

```swift
enum PayloadArchive {
    static func save(_ json: Data, kind: String) throws -> URL {
        let stamp = ISO8601DateFormatter().string(from: .now)
        let file = FileManager.default.temporaryDirectory
            .appendingPathComponent("metrickit-\(kind)-\(stamp).json")
        try json.write(to: file, options: .atomic)
        return file
    }

    static func upload(_ file: URL, to collector: URL) {
        Task.detached(priority: .utility) {
            var request = URLRequest(url: collector)
            request.httpMethod = "POST"
            request.addValue("application/json", forHTTPHeaderField: "Content-Type")
            let json = try Data(contentsOf: file)
            let (_, reply) = try await URLSession.shared.upload(for: request, from: json)
            guard (reply as? HTTPURLResponse)?.statusCode == 200 else { return }
            try FileManager.default.removeItem(at: file)
        }
    }
}
```

In production prefer Application Support or Caches over the temporary
directory if files must survive until the next launch.

## Catching up on missed payloads

Deliveries that happened while no subscriber was registered are not lost;
they stay reachable through `MXMetricManager.shared.pastPayloads` and
`pastDiagnosticPayloads`, which cover everything produced since the shared
instance was last allocated.

```swift
func backfill() throws {
    let manager = MXMetricManager.shared
    for payload in manager.pastPayloads {
        _ = try PayloadArchive.save(payload.jsonRepresentation(), kind: "metric")
    }
    for payload in manager.pastDiagnosticPayloads {
        _ = try PayloadArchive.save(payload.jsonRepresentation(), kind: "diagnostic")
    }
}
```

## Extended launch rules

Typical tasks worth including: opening the database, restoring saved state,
fetching the data the first screen shows.

- Begin the earliest task no later than state restoration, while the first
  scene is still inactive.
- Up to 16 tasks can be registered.
- Task windows may overlap.
- Measurement ends once every running task has finished, so finish each one,
  including on error paths.

```swift
@MainActor
func restoreSession() async {
    let restore = MXLaunchTaskID("com.example.trailhead.restore")
    let routes = MXLaunchTaskID("com.example.trailhead.routes")
    try? MXMetricManager.extendLaunchMeasurement(forTaskID: restore)
    try? MXMetricManager.extendLaunchMeasurement(forTaskID: routes)

    await sessionStore.restore()
    try? MXMetricManager.finishExtendedLaunchMeasurement(forTaskID: restore)

    await routeCache.warm()
    try? MXMetricManager.finishExtendedLaunchMeasurement(forTaskID: routes)
}
```

## Xcode Organizer

| Tab | Shows |
|---|---|
| Metrics | Battery, performance and disk writes over time |
| Regressions | Regressions detected automatically between versions |
| Crashes | Symbolicated crash logs |

Organizer covers the aggregate view. Your own on-device collection adds what
it cannot: your own dashboards and alerts, plus slicing by user segment.

## Apple documentation

- https://developer.apple.com/documentation/metrickit
- https://developer.apple.com/documentation/metrickit/mxmetricmanager
- https://developer.apple.com/documentation/metrickit/mxmetricmanagersubscriber
- https://developer.apple.com/documentation/metrickit/mxcallstacktree
- https://developer.apple.com/documentation/metrickit/mxsignpostmetric
- https://developer.apple.com/documentation/metrickit/mxapplaunchmetric
- https://developer.apple.com/documentation/metrickit/mxcrashdiagnostic
- Apple article on analyzing a shipping app's performance, in the Xcode documentation
