
Error Monitoring
- 3 installs
- 591 repo stars
- Updated July 24, 2026
- rshankras/claude-code-apple-skills
Generates protocol-based crash and error monitoring with swappable providers like Sentry and Crashlytics for tracking production issues.
About
Generates a protocol-based error and crash monitoring layer that swaps between Sentry and Crashlytics without changing app code. A developer uses it to add crash reporting and production error tracking to an iOS/macOS app.
- Protocol architecture swaps between Sentry and Crashlytics
- Detects existing monitoring setup before generating
Error Monitoring by the numbers
- 3 all-time installs (skills.sh)
- +1 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #887 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/rshankras/claude-code-apple-skills --skill error-monitoringAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 591 |
| Last updated | July 24, 2026 |
| Repository | rshankras/claude-code-apple-skills ↗ |
What it does
Generates protocol-based crash and error monitoring with swappable providers like Sentry and Crashlytics for tracking production issues.
Files
Error Monitoring Generator
Generates a production-ready error monitoring infrastructure with protocol-based architecture for easy provider swapping.
When This Skill Activates
- User asks to "add crash reporting" or "error monitoring"
- User mentions "Sentry", "Crashlytics", or "crash analytics"
- User wants to "track errors in production"
- User asks about "debugging production issues"
Pre-Generation Checks (CRITICAL)
1. Project Context Detection
Before generating, ALWAYS check:
# Check for existing crash reporting
rg -l "Sentry|Crashlytics|CrashReporter" --type swift
# Check Package.swift for existing SDKs
cat Package.swift | grep -i "sentry\|firebase\|crashlytics"
# Check for existing error handling patterns
rg "captureError|recordError|logError" --type swift | head -52. Conflict Detection
If existing crash reporting found:
- Ask: Replace, wrap existing, or create parallel system?
Configuration Questions
Ask user via AskUserQuestion:
1. Initial provider?
- Sentry (recommended for indie devs)
- Firebase Crashlytics (if already using Firebase)
- None (set up infrastructure only)
2. Include breadcrumbs?
- Yes (track navigation, user actions)
- No (errors only)
3. Include user context?
- Yes (anonymized user ID, app state)
- No (minimal data collection)
Generation Process
Step 1: Create Core Files
Always generate:
Sources/ErrorMonitoring/
├── ErrorMonitoringService.swift # Protocol
├── ErrorContext.swift # Breadcrumbs, user info
└── NoOpErrorMonitoring.swift # Testing/privacyBased on provider selection:
Sources/ErrorMonitoring/Providers/
├── SentryErrorMonitoring.swift # If Sentry selected
└── CrashlyticsErrorMonitoring.swift # If Crashlytics selectedStep 2: Read Templates
Read templates from this skill:
templates/ErrorMonitoringService.swifttemplates/ErrorContext.swifttemplates/NoOpErrorMonitoring.swifttemplates/SentryErrorMonitoring.swift(if selected)templates/CrashlyticsErrorMonitoring.swift(if selected)
Step 3: Customize for Project
Adapt templates to match:
- Project naming conventions
- Existing error types
- Bundle identifier for Sentry DSN
Step 4: Integration
In App.swift:
import SwiftUI
@main
struct MyApp: App {
init() {
// Configure error monitoring
ErrorMonitoring.shared.configure()
}
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.errorMonitoring, ErrorMonitoring.shared.service)
}
}
}Capturing errors:
do {
try await riskyOperation()
} catch {
ErrorMonitoring.shared.service.captureError(error)
}Adding breadcrumbs:
ErrorMonitoring.shared.service.addBreadcrumb(
Breadcrumb(category: "navigation", message: "Opened settings")
)Provider Setup
Sentry
1. Create account at sentry.io 2. Create project for iOS/macOS 3. Get DSN from Project Settings > Client Keys 4. Add to Package.swift:
.package(url: "https://github.com/getsentry/sentry-cocoa", from: "8.0.0")Firebase Crashlytics
1. Add Firebase to your project via console.firebase.google.com 2. Download GoogleService-Info.plist 3. Add Firebase SDK via SPM or CocoaPods 4. Enable Crashlytics in Firebase console
Generated Code Patterns
Protocol (Stable Interface)
protocol ErrorMonitoringService: Sendable {
func configure()
func captureError(_ error: Error, context: ErrorContext?)
func captureMessage(_ message: String, level: ErrorLevel)
func addBreadcrumb(_ breadcrumb: Breadcrumb)
func setUser(_ user: MonitoringUser?)
func reset()
}Swapping Providers
// In ErrorMonitoring.swift
final class ErrorMonitoring {
static let shared = ErrorMonitoring()
// Change this ONE line to swap providers:
let service: ErrorMonitoringService = SentryErrorMonitoring()
// let service: ErrorMonitoringService = CrashlyticsErrorMonitoring()
// let service: ErrorMonitoringService = NoOpErrorMonitoring()
}Breadcrumb Tracking
// Automatic navigation breadcrumbs
struct ContentView: View {
@Environment(\.errorMonitoring) var errorMonitoring
var body: some View {
Button("Open Details") {
errorMonitoring.addBreadcrumb(
Breadcrumb(category: "ui", message: "Tapped details button")
)
showDetails = true
}
}
}Verification Checklist
After generation, verify:
- [ ] App compiles without errors
- [ ] Provider SDK added (if applicable)
- [ ] DSN/config set correctly
- [ ] Test error appears in dashboard
- [ ] Breadcrumbs captured correctly
- [ ] User context (if enabled) shows in reports
- [ ] NoOp mode works for debug/testing
Privacy Considerations
GDPR Compliance
- Use
NoOpErrorMonitoringfor EU users who opt out - Don't capture PII in error messages
- Anonymize user IDs
App Store Guidelines
- Disclose crash reporting in privacy policy
- Use App Tracking Transparency if combining with analytics
- Don't capture unnecessary device identifiers
Privacy Manifest (iOS 17+)
Add to PrivacyInfo.xcprivacy if using Sentry/Crashlytics:
<key>NSPrivacyCollectedDataTypes</key>
<array>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeCrashData</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<false/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
</array>
</dict>
</array>Common Customizations
Custom Error Types
enum AppError: Error {
case networkFailure(URLError)
case decodingFailure(DecodingError)
case authenticationRequired
var context: ErrorContext {
ErrorContext(
tags: ["error_type": String(describing: self)],
extra: ["recoverable": isRecoverable]
)
}
}
// Capture with context
errorMonitoring.captureError(error, context: error.context)Performance Monitoring
// Sentry supports performance monitoring
let transaction = SentrySDK.startTransaction(name: "Load Data", operation: "http")
defer { transaction.finish() }
let data = try await fetchData()Release Tracking
// Include version info
SentrySDK.start { options in
options.dsn = "YOUR_DSN"
options.releaseName = "\(Bundle.main.appVersion)-\(Bundle.main.buildNumber)"
}Troubleshooting
Errors Not Appearing in Dashboard
1. Check DSN is correct and not expired 2. Verify network connectivity 3. Check debug mode isn't suppressing uploads 4. Look for SDK initialization errors in console
Symbolication Not Working
1. Upload dSYMs to Sentry/Firebase 2. Enable "Upload Debug Symbols" build phase 3. Check build settings include debug info
High Volume / Costs
1. Filter common/expected errors 2. Sample errors (e.g., capture 10%) 3. Group similar errors
Related Skills
analytics-setup- Often combined with error monitoringlogging-setup- Use Logger for debug, error monitoring for production
References
Error Monitoring Patterns
Best practices for implementing error monitoring in iOS and macOS apps.
Protocol-Based Architecture
Why Protocols?
1. Testability - Use NoOpErrorMonitoring in tests 2. Privacy - Swap to NoOp for users who opt out 3. Flexibility - Change providers without code changes 4. Gradual adoption - Start with NoOp, add provider later
Core Protocol
protocol ErrorMonitoringService: Sendable {
/// Configure the service (call once at app launch)
func configure()
/// Capture an error with optional context
func captureError(_ error: Error, context: ErrorContext?)
/// Capture a message with severity level
func captureMessage(_ message: String, level: ErrorLevel)
/// Add a breadcrumb for debugging context
func addBreadcrumb(_ breadcrumb: Breadcrumb)
/// Set the current user (anonymized)
func setUser(_ user: MonitoringUser?)
/// Clear user and session data (on logout)
func reset()
}Provider Pattern
// Central access point
final class ErrorMonitoring {
static let shared = ErrorMonitoring()
// Swap providers by changing this line
#if DEBUG
let service: ErrorMonitoringService = NoOpErrorMonitoring()
#else
let service: ErrorMonitoringService = SentryErrorMonitoring()
#endif
private init() {}
func configure() {
service.configure()
}
}Error Context
Breadcrumbs
Breadcrumbs provide a trail of events leading to an error:
struct Breadcrumb: Sendable {
let timestamp: Date
let category: String // "navigation", "ui", "network", "user"
let message: String
let level: ErrorLevel
let data: [String: String]?
init(
category: String,
message: String,
level: ErrorLevel = .info,
data: [String: String]? = nil
) {
self.timestamp = .now
self.category = category
self.message = message
self.level = level
self.data = data
}
}Automatic Breadcrumbs
// Navigation breadcrumbs
extension View {
func trackNavigation(_ screenName: String) -> some View {
onAppear {
ErrorMonitoring.shared.service.addBreadcrumb(
Breadcrumb(
category: "navigation",
message: "Viewed \(screenName)"
)
)
}
}
}
// Usage
struct SettingsView: View {
var body: some View {
Form { ... }
.trackNavigation("Settings")
}
}User Context
struct MonitoringUser: Sendable {
let id: String // Anonymized user ID (not email!)
let username: String? // Optional display name
let segment: String? // User segment (free, premium)
// Never include PII like email, phone, real name
}
// Set on login
func userDidLogin(_ user: AppUser) {
let monitoringUser = MonitoringUser(
id: user.anonymizedID, // Hash of real ID
username: nil,
segment: user.subscriptionTier
)
ErrorMonitoring.shared.service.setUser(monitoringUser)
}
// Clear on logout
func userDidLogout() {
ErrorMonitoring.shared.service.reset()
}Error Tags and Extra Data
struct ErrorContext: Sendable {
var tags: [String: String] // Indexed, searchable
var extra: [String: Any] // Additional data
var fingerprint: [String]? // Custom grouping
init(
tags: [String: String] = [:],
extra: [String: Any] = [:],
fingerprint: [String]? = nil
) {
self.tags = tags
self.extra = extra
self.fingerprint = fingerprint
}
}
// Usage
let context = ErrorContext(
tags: [
"feature": "checkout",
"payment_method": "apple_pay"
],
extra: [
"cart_items": cartItems.count,
"total_amount": cart.total
]
)
errorMonitoring.captureError(error, context: context)Error Levels
enum ErrorLevel: String, Sendable {
case debug // Debugging info, not captured in production
case info // Informational, low priority
case warning // Something unexpected but not critical
case error // Error that affected user experience
case fatal // App crash or critical failure
}Capturing Errors
Basic Error Capture
do {
try await performOperation()
} catch {
ErrorMonitoring.shared.service.captureError(error)
// Show user-friendly error message
showErrorAlert(error)
}With Context
func purchaseProduct(_ product: Product) async {
let breadcrumb = Breadcrumb(
category: "purchase",
message: "Starting purchase",
data: ["product_id": product.id]
)
ErrorMonitoring.shared.service.addBreadcrumb(breadcrumb)
do {
try await storeManager.purchase(product)
} catch {
let context = ErrorContext(
tags: ["feature": "iap", "product": product.id],
extra: ["price": product.price.description]
)
ErrorMonitoring.shared.service.captureError(error, context: context)
}
}Capturing Messages
// For non-exception issues
ErrorMonitoring.shared.service.captureMessage(
"User attempted to access premium feature without subscription",
level: .warning
)Integration Patterns
App Lifecycle
@main
struct MyApp: App {
init() {
ErrorMonitoring.shared.configure()
}
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.errorMonitoring, ErrorMonitoring.shared.service)
}
}
}Environment Key
private struct ErrorMonitoringKey: EnvironmentKey {
static let defaultValue: ErrorMonitoringService = NoOpErrorMonitoring()
}
extension EnvironmentValues {
var errorMonitoring: ErrorMonitoringService {
get { self[ErrorMonitoringKey.self] }
set { self[ErrorMonitoringKey.self] = newValue }
}
}
// In views
struct FeatureView: View {
@Environment(\.errorMonitoring) var errorMonitoring
func handleError(_ error: Error) {
errorMonitoring.captureError(error)
}
}Global Error Handler
// Catch unhandled errors
func setupGlobalErrorHandling() {
// Swift errors (non-crashing)
NSSetUncaughtExceptionHandler { exception in
ErrorMonitoring.shared.service.captureMessage(
"Uncaught exception: \(exception.reason ?? "unknown")",
level: .fatal
)
}
// Note: Sentry/Crashlytics handle actual crashes automatically
}Testing
Mock for Unit Tests
final class MockErrorMonitoring: ErrorMonitoringService {
var capturedErrors: [Error] = []
var capturedMessages: [(String, ErrorLevel)] = []
var breadcrumbs: [Breadcrumb] = []
func configure() {}
func captureError(_ error: Error, context: ErrorContext?) {
capturedErrors.append(error)
}
func captureMessage(_ message: String, level: ErrorLevel) {
capturedMessages.append((message, level))
}
func addBreadcrumb(_ breadcrumb: Breadcrumb) {
breadcrumbs.append(breadcrumb)
}
func setUser(_ user: MonitoringUser?) {}
func reset() {
capturedErrors.removeAll()
capturedMessages.removeAll()
breadcrumbs.removeAll()
}
}
// In tests
func testErrorCapture() {
let mock = MockErrorMonitoring()
let viewModel = ViewModel(errorMonitoring: mock)
viewModel.performFailingOperation()
XCTAssertEqual(mock.capturedErrors.count, 1)
}Debug Mode
#if DEBUG
final class DebugErrorMonitoring: ErrorMonitoringService {
func captureError(_ error: Error, context: ErrorContext?) {
print("🔴 ERROR: \(error)")
if let context {
print(" Tags: \(context.tags)")
print(" Extra: \(context.extra)")
}
}
func captureMessage(_ message: String, level: ErrorLevel) {
let emoji: String = switch level {
case .debug: "🔍"
case .info: "ℹ️"
case .warning: "⚠️"
case .error: "🔴"
case .fatal: "💀"
}
print("\(emoji) \(level.rawValue.uppercased()): \(message)")
}
// ... other methods print to console
}
#endifPerformance Considerations
Sampling
For high-traffic apps, sample errors:
final class SampledErrorMonitoring: ErrorMonitoringService {
private let wrapped: ErrorMonitoringService
private let sampleRate: Double // 0.0 to 1.0
init(wrapped: ErrorMonitoringService, sampleRate: Double = 0.1) {
self.wrapped = wrapped
self.sampleRate = sampleRate
}
func captureError(_ error: Error, context: ErrorContext?) {
guard Double.random(in: 0...1) < sampleRate else { return }
wrapped.captureError(error, context: context)
}
// ... pass through other methods
}Filtering Known Errors
extension ErrorMonitoringService {
func captureErrorIfNotExpected(_ error: Error, context: ErrorContext? = nil) {
// Don't report expected/handled errors
guard !isExpectedError(error) else { return }
captureError(error, context: context)
}
private func isExpectedError(_ error: Error) -> Bool {
switch error {
case is CancellationError:
return true
case let urlError as URLError where urlError.code == .cancelled:
return true
case let storeError as StoreKitError where storeError == .userCancelled:
return true
default:
return false
}
}
}Breadcrumb Limits
final class BoundedBreadcrumbBuffer {
private var breadcrumbs: [Breadcrumb] = []
private let maxCount: Int = 100
func add(_ breadcrumb: Breadcrumb) {
breadcrumbs.append(breadcrumb)
if breadcrumbs.count > maxCount {
breadcrumbs.removeFirst()
}
}
var all: [Breadcrumb] { breadcrumbs }
}Privacy Best Practices
Never Capture
- Email addresses
- Phone numbers
- Real names
- Passwords or tokens
- Location data (unless essential)
- Financial information
Always Anonymize
extension String {
var anonymized: String {
// Hash for consistent anonymization
let hash = SHA256.hash(data: Data(self.utf8))
return hash.prefix(16).map { String(format: "%02x", $0) }.joined()
}
}
// Usage
let monitoringUser = MonitoringUser(
id: user.email.anonymized, // Hash, not real email
username: nil,
segment: user.tier
)Respect User Preferences
final class ConsentAwareErrorMonitoring: ErrorMonitoringService {
private let realService: ErrorMonitoringService
private let consentManager: ConsentManager
var activeService: ErrorMonitoringService {
consentManager.hasConsent ? realService : NoOpErrorMonitoring()
}
func captureError(_ error: Error, context: ErrorContext?) {
activeService.captureError(error, context: context)
}
// ... delegate other methods to activeService
}import Foundation
// import FirebaseCrashlytics // Uncomment after adding Firebase SDK
// import FirebaseCore
/// Firebase Crashlytics implementation of ErrorMonitoringService.
///
/// Prerequisites:
/// 1. Add Firebase SDK via SPM or CocoaPods
/// 2. Download GoogleService-Info.plist from Firebase Console
/// 3. Enable Crashlytics in Firebase Console
/// 4. Uncomment the imports and implementation below
///
/// Usage:
/// ```swift
/// let service: ErrorMonitoringService = CrashlyticsErrorMonitoring()
/// service.configure() // Call at app launch
/// ```
final class CrashlyticsErrorMonitoring: ErrorMonitoringService, @unchecked Sendable {
// MARK: - ErrorMonitoringService
func configure() {
// TODO: Uncomment after adding Firebase SDK
/*
// Configure Firebase (if not already done)
if FirebaseApp.app() == nil {
FirebaseApp.configure()
}
// Enable collection (respect user preference)
Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(true)
// Set custom keys
Crashlytics.crashlytics().setCustomValue(releaseVersion, forKey: "app_version")
#if DEBUG
// Disable in debug builds to avoid noise
Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(false)
#endif
*/
print("[Crashlytics] Would configure Firebase Crashlytics")
}
func captureError(_ error: Error, context: ErrorContext?) {
// TODO: Uncomment after adding Firebase SDK
/*
let nsError = error as NSError
// Record error with context
Crashlytics.crashlytics().record(error: nsError, userInfo: buildUserInfo(context))
// Also set custom keys from tags
if let context {
for (key, value) in context.tags {
Crashlytics.crashlytics().setCustomValue(value, forKey: key)
}
}
*/
print("[Crashlytics] Would record error: \(error)")
}
func captureMessage(_ message: String, level: ErrorLevel) {
// TODO: Uncomment after adding Firebase SDK
/*
// Crashlytics uses log() for non-fatal messages
Crashlytics.crashlytics().log("\(level.rawValue.uppercased()): \(message)")
// For fatal level, also record as exception
if level == .fatal {
let error = NSError(
domain: "AppError",
code: -1,
userInfo: [NSLocalizedDescriptionKey: message]
)
Crashlytics.crashlytics().record(error: error)
}
*/
print("[Crashlytics] Would log \(level.rawValue): \(message)")
}
func addBreadcrumb(_ breadcrumb: Breadcrumb) {
// TODO: Uncomment after adding Firebase SDK
/*
// Crashlytics uses log() for breadcrumbs
let message = "[\(breadcrumb.category)] \(breadcrumb.message)"
Crashlytics.crashlytics().log(message)
// Add data as custom keys if present
if let data = breadcrumb.data {
for (key, value) in data {
Crashlytics.crashlytics().setCustomValue(
value,
forKey: "breadcrumb_\(key)"
)
}
}
*/
print("[Crashlytics] Would log breadcrumb: [\(breadcrumb.category)] \(breadcrumb.message)")
}
func setUser(_ user: MonitoringUser?) {
// TODO: Uncomment after adding Firebase SDK
/*
if let user {
Crashlytics.crashlytics().setUserID(user.id)
if let segment = user.segment {
Crashlytics.crashlytics().setCustomValue(segment, forKey: "user_segment")
}
if let username = user.username {
Crashlytics.crashlytics().setCustomValue(username, forKey: "username")
}
} else {
Crashlytics.crashlytics().setUserID("")
}
*/
if let user {
print("[Crashlytics] Would set user ID: \(user.id)")
} else {
print("[Crashlytics] Would clear user ID")
}
}
func reset() {
// TODO: Uncomment after adding Firebase SDK
/*
Crashlytics.crashlytics().setUserID("")
// Clear custom keys
// Note: Crashlytics doesn't have a clear all method
// Set known keys to empty
Crashlytics.crashlytics().setCustomValue("", forKey: "user_segment")
Crashlytics.crashlytics().setCustomValue("", forKey: "username")
*/
print("[Crashlytics] Would reset user data")
}
// MARK: - Helpers
private var releaseVersion: String {
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "0"
return "\(version) (\(build))"
}
private func buildUserInfo(_ context: ErrorContext?) -> [String: Any] {
var userInfo: [String: Any] = [:]
if let context {
// Add tags
for (key, value) in context.tags {
userInfo["tag_\(key)"] = value
}
// Add extra (converted to strings for NSError userInfo)
for (key, value) in context.extra {
userInfo["extra_\(key)"] = String(describing: value)
}
}
return userInfo
}
}
// MARK: - Crashlytics Collection Control
extension CrashlyticsErrorMonitoring {
/// Enable or disable crash collection (for user consent).
func setCollectionEnabled(_ enabled: Bool) {
// TODO: Uncomment after adding Firebase SDK
/*
Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(enabled)
*/
print("[Crashlytics] Would set collection enabled: \(enabled)")
}
/// Check if crash collection is enabled.
var isCollectionEnabled: Bool {
// TODO: Uncomment after adding Firebase SDK
/*
return Crashlytics.crashlytics().isCrashlyticsCollectionEnabled()
*/
return true
}
}
import Foundation
/// Context information for error reports.
///
/// Includes breadcrumbs, tags, and extra data to help debug issues.
///
/// Usage:
/// ```swift
/// let context = ErrorContext(
/// tags: ["feature": "checkout", "step": "payment"],
/// extra: ["cart_items": 3, "total": 99.99]
/// )
/// errorMonitoring.captureError(error, context: context)
/// ```
struct ErrorContext: Sendable {
/// Indexed tags for filtering (string values only).
var tags: [String: String]
/// Additional data for debugging.
var extra: [String: any Sendable]
/// Custom fingerprint for error grouping.
/// Errors with the same fingerprint are grouped together.
var fingerprint: [String]?
init(
tags: [String: String] = [:],
extra: [String: any Sendable] = [:],
fingerprint: [String]? = nil
) {
self.tags = tags
self.extra = extra
self.fingerprint = fingerprint
}
}
// MARK: - Builder Pattern
extension ErrorContext {
/// Add a tag.
func tag(_ key: String, _ value: String) -> ErrorContext {
var copy = self
copy.tags[key] = value
return copy
}
/// Add extra data.
func with(_ key: String, _ value: any Sendable) -> ErrorContext {
var copy = self
copy.extra[key] = value
return copy
}
/// Set custom fingerprint.
func fingerprinted(_ values: String...) -> ErrorContext {
var copy = self
copy.fingerprint = values
return copy
}
}
// MARK: - Breadcrumb
/// A breadcrumb represents an event that happened before an error.
///
/// Breadcrumbs provide a trail of events leading to errors,
/// helping with debugging.
///
/// Usage:
/// ```swift
/// // UI event
/// let breadcrumb = Breadcrumb(
/// category: "ui",
/// message: "User tapped checkout button"
/// )
///
/// // Network event
/// let breadcrumb = Breadcrumb(
/// category: "network",
/// message: "API request completed",
/// data: ["endpoint": "/api/orders", "status": "200"]
/// )
/// ```
struct Breadcrumb: Sendable {
/// When the event occurred.
let timestamp: Date
/// Category of the event (e.g., "navigation", "ui", "network", "user").
let category: String
/// Human-readable description.
let message: String
/// Severity level.
let level: ErrorLevel
/// Additional data.
let data: [String: String]?
init(
category: String,
message: String,
level: ErrorLevel = .info,
data: [String: String]? = nil
) {
self.timestamp = .now
self.category = category
self.message = message
self.level = level
self.data = data
}
}
// MARK: - Common Breadcrumb Categories
extension Breadcrumb {
/// Navigation breadcrumb (screen changes).
static func navigation(_ screenName: String) -> Breadcrumb {
Breadcrumb(
category: "navigation",
message: "Viewed \(screenName)"
)
}
/// UI interaction breadcrumb.
static func ui(_ action: String, element: String? = nil) -> Breadcrumb {
var data: [String: String]? = nil
if let element {
data = ["element": element]
}
return Breadcrumb(
category: "ui",
message: action,
data: data
)
}
/// Network request breadcrumb.
static func network(
method: String,
url: String,
statusCode: Int? = nil
) -> Breadcrumb {
var data = ["method": method, "url": url]
if let statusCode {
data["status"] = String(statusCode)
}
return Breadcrumb(
category: "network",
message: "\(method) \(url)",
data: data
)
}
/// User action breadcrumb.
static func user(_ action: String, data: [String: String]? = nil) -> Breadcrumb {
Breadcrumb(
category: "user",
message: action,
data: data
)
}
/// State change breadcrumb.
static func state(_ description: String, data: [String: String]? = nil) -> Breadcrumb {
Breadcrumb(
category: "state",
message: description,
data: data
)
}
/// Error breadcrumb (for non-fatal errors).
static func error(_ description: String) -> Breadcrumb {
Breadcrumb(
category: "error",
message: description,
level: .error
)
}
}
// MARK: - SwiftUI Navigation Tracking
import SwiftUI
extension View {
/// Automatically add navigation breadcrumb when view appears.
func trackScreen(_ name: String) -> some View {
onAppear {
Task { @MainActor in
ErrorMonitoring.shared.service.addBreadcrumb(.navigation(name))
}
}
}
}
import Foundation
import SwiftUI
/// Protocol for error monitoring services.
///
/// Provides a consistent interface for capturing errors, messages,
/// and breadcrumbs across different providers (Sentry, Crashlytics, etc.).
///
/// Usage:
/// ```swift
/// // Capture an error
/// errorMonitoring.captureError(error)
///
/// // Capture with context
/// let context = ErrorContext(tags: ["feature": "checkout"])
/// errorMonitoring.captureError(error, context: context)
///
/// // Add breadcrumb
/// errorMonitoring.addBreadcrumb(
/// Breadcrumb(category: "navigation", message: "Opened settings")
/// )
/// ```
protocol ErrorMonitoringService: Sendable {
/// Configure the error monitoring service.
/// Call once at app launch.
func configure()
/// Capture an error with optional context.
func captureError(_ error: Error, context: ErrorContext?)
/// Capture a message with severity level.
func captureMessage(_ message: String, level: ErrorLevel)
/// Add a breadcrumb for debugging context.
func addBreadcrumb(_ breadcrumb: Breadcrumb)
/// Set the current user (use anonymized IDs only).
func setUser(_ user: MonitoringUser?)
/// Clear user and session data.
/// Call on logout.
func reset()
}
// MARK: - Default Implementations
extension ErrorMonitoringService {
/// Capture an error without additional context.
func captureError(_ error: Error) {
captureError(error, context: nil)
}
}
// MARK: - Error Level
/// Severity level for captured messages.
enum ErrorLevel: String, Sendable, CaseIterable {
case debug // Development debugging
case info // Informational
case warning // Unexpected but not critical
case error // Error affecting user experience
case fatal // Critical failure / crash
}
// MARK: - Monitoring User
/// User information for error reports.
/// Use anonymized IDs only - never include PII.
struct MonitoringUser: Sendable, Equatable {
/// Anonymized user identifier (hash of real ID).
let id: String
/// Optional username (if user consents to sharing).
let username: String?
/// User segment (e.g., "free", "premium").
let segment: String?
init(id: String, username: String? = nil, segment: String? = nil) {
self.id = id
self.username = username
self.segment = segment
}
}
// MARK: - Central Access
/// Central access point for error monitoring.
///
/// Usage:
/// ```swift
/// // At app launch
/// ErrorMonitoring.shared.configure()
///
/// // Capture errors
/// ErrorMonitoring.shared.service.captureError(error)
/// ```
@MainActor
final class ErrorMonitoring {
static let shared = ErrorMonitoring()
/// The active error monitoring service.
/// Change this to swap providers:
/// - `SentryErrorMonitoring()` for Sentry
/// - `CrashlyticsErrorMonitoring()` for Firebase
/// - `NoOpErrorMonitoring()` for testing/privacy
#if DEBUG
let service: ErrorMonitoringService = NoOpErrorMonitoring()
#else
let service: ErrorMonitoringService = NoOpErrorMonitoring()
// TODO: Replace with your provider:
// let service: ErrorMonitoringService = SentryErrorMonitoring()
#endif
private init() {}
/// Configure error monitoring. Call once at app launch.
func configure() {
service.configure()
}
}
// MARK: - Environment Key
private struct ErrorMonitoringKey: EnvironmentKey {
static let defaultValue: ErrorMonitoringService = NoOpErrorMonitoring()
}
extension EnvironmentValues {
/// Error monitoring service for capturing errors.
var errorMonitoring: ErrorMonitoringService {
get { self[ErrorMonitoringKey.self] }
set { self[ErrorMonitoringKey.self] = newValue }
}
}
import Foundation
/// No-op implementation of ErrorMonitoringService.
///
/// Use this for:
/// - Debug/development builds
/// - Users who opt out of crash reporting
/// - Unit tests
/// - Privacy-focused mode
///
/// Usage:
/// ```swift
/// let service: ErrorMonitoringService = NoOpErrorMonitoring()
/// service.captureError(error) // Does nothing
/// ```
final class NoOpErrorMonitoring: ErrorMonitoringService, Sendable {
// MARK: - Configuration
func configure() {
// No-op
#if DEBUG
print("[ErrorMonitoring] NoOp mode - errors will not be reported")
#endif
}
// MARK: - Error Capture
func captureError(_ error: Error, context: ErrorContext?) {
#if DEBUG
print("[ErrorMonitoring] Would capture error: \(error)")
if let context {
if !context.tags.isEmpty {
print(" Tags: \(context.tags)")
}
}
#endif
}
func captureMessage(_ message: String, level: ErrorLevel) {
#if DEBUG
print("[ErrorMonitoring] Would capture \(level.rawValue): \(message)")
#endif
}
// MARK: - Breadcrumbs
func addBreadcrumb(_ breadcrumb: Breadcrumb) {
#if DEBUG
print("[ErrorMonitoring] Breadcrumb: [\(breadcrumb.category)] \(breadcrumb.message)")
#endif
}
// MARK: - User
func setUser(_ user: MonitoringUser?) {
#if DEBUG
if let user {
print("[ErrorMonitoring] Would set user: \(user.id)")
} else {
print("[ErrorMonitoring] Would clear user")
}
#endif
}
func reset() {
#if DEBUG
print("[ErrorMonitoring] Would reset session")
#endif
}
}
// MARK: - Debug Error Monitoring
#if DEBUG
/// Debug implementation that prints all events to console.
///
/// Useful during development to verify error capture is working.
final class DebugErrorMonitoring: ErrorMonitoringService, Sendable {
private let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss.SSS"
return formatter
}()
func configure() {
print("🔧 [ErrorMonitoring] Debug mode configured")
}
func captureError(_ error: Error, context: ErrorContext?) {
let timestamp = dateFormatter.string(from: .now)
print("🔴 [\(timestamp)] ERROR: \(error)")
print(" Type: \(type(of: error))")
if let localizedError = error as? LocalizedError {
if let description = localizedError.errorDescription {
print(" Description: \(description)")
}
if let reason = localizedError.failureReason {
print(" Reason: \(reason)")
}
if let recovery = localizedError.recoverySuggestion {
print(" Recovery: \(recovery)")
}
}
if let context {
if !context.tags.isEmpty {
print(" Tags: \(context.tags)")
}
if !context.extra.isEmpty {
print(" Extra: \(context.extra)")
}
}
}
func captureMessage(_ message: String, level: ErrorLevel) {
let timestamp = dateFormatter.string(from: .now)
let emoji = switch level {
case .debug: "🔍"
case .info: "ℹ️"
case .warning: "⚠️"
case .error: "🔴"
case .fatal: "💀"
}
print("\(emoji) [\(timestamp)] \(level.rawValue.uppercased()): \(message)")
}
func addBreadcrumb(_ breadcrumb: Breadcrumb) {
let timestamp = dateFormatter.string(from: breadcrumb.timestamp)
print("🍞 [\(timestamp)] [\(breadcrumb.category)] \(breadcrumb.message)")
if let data = breadcrumb.data, !data.isEmpty {
print(" Data: \(data)")
}
}
func setUser(_ user: MonitoringUser?) {
if let user {
print("👤 Set user: id=\(user.id), segment=\(user.segment ?? "none")")
} else {
print("👤 Cleared user")
}
}
func reset() {
print("🔄 Session reset")
}
}
#endif
import Foundation
// import Sentry // Uncomment after adding Sentry SDK
/// Sentry implementation of ErrorMonitoringService.
///
/// Prerequisites:
/// 1. Add Sentry SDK to Package.swift:
/// `.package(url: "https://github.com/getsentry/sentry-cocoa", from: "8.0.0")`
/// 2. Get DSN from Sentry dashboard
/// 3. Uncomment the import and implementation below
///
/// Usage:
/// ```swift
/// let service: ErrorMonitoringService = SentryErrorMonitoring()
/// service.configure() // Call at app launch
/// ```
final class SentryErrorMonitoring: ErrorMonitoringService, @unchecked Sendable {
// MARK: - Configuration
/// Your Sentry DSN from Project Settings > Client Keys
private let dsn = "YOUR_SENTRY_DSN_HERE"
/// Sample rate for error events (0.0 to 1.0)
private let sampleRate: Float = 1.0
/// Sample rate for performance traces (0.0 to 1.0)
private let tracesSampleRate: Float = 0.1
// MARK: - ErrorMonitoringService
func configure() {
// TODO: Uncomment after adding Sentry SDK
/*
SentrySDK.start { options in
options.dsn = self.dsn
options.sampleRate = NSNumber(value: self.sampleRate)
options.tracesSampleRate = NSNumber(value: self.tracesSampleRate)
// Enable automatic breadcrumbs
options.enableAutoBreadcrumbTracking = true
options.enableUIViewControllerTracing = true
options.enableNetworkTracking = true
// Set release version
options.releaseName = self.releaseVersion
// Debug mode (disable in production)
#if DEBUG
options.debug = true
#endif
// Set environment
#if DEBUG
options.environment = "development"
#else
options.environment = "production"
#endif
}
*/
print("[Sentry] Would configure with DSN: \(dsn.prefix(20))...")
}
func captureError(_ error: Error, context: ErrorContext?) {
// TODO: Uncomment after adding Sentry SDK
/*
SentrySDK.capture(error: error) { scope in
if let context {
// Add tags
for (key, value) in context.tags {
scope.setTag(value: value, key: key)
}
// Add extra data
for (key, value) in context.extra {
scope.setExtra(value: value, key: key)
}
// Set custom fingerprint
if let fingerprint = context.fingerprint {
scope.setFingerprint(fingerprint)
}
}
}
*/
print("[Sentry] Would capture error: \(error)")
}
func captureMessage(_ message: String, level: ErrorLevel) {
// TODO: Uncomment after adding Sentry SDK
/*
SentrySDK.capture(message: message) { scope in
scope.setLevel(level.sentryLevel)
}
*/
print("[Sentry] Would capture \(level.rawValue): \(message)")
}
func addBreadcrumb(_ breadcrumb: Breadcrumb) {
// TODO: Uncomment after adding Sentry SDK
/*
let sentryBreadcrumb = Sentry.Breadcrumb()
sentryBreadcrumb.category = breadcrumb.category
sentryBreadcrumb.message = breadcrumb.message
sentryBreadcrumb.level = breadcrumb.level.sentryLevel
sentryBreadcrumb.timestamp = breadcrumb.timestamp
if let data = breadcrumb.data {
sentryBreadcrumb.data = data
}
SentrySDK.addBreadcrumb(sentryBreadcrumb)
*/
print("[Sentry] Would add breadcrumb: [\(breadcrumb.category)] \(breadcrumb.message)")
}
func setUser(_ user: MonitoringUser?) {
// TODO: Uncomment after adding Sentry SDK
/*
if let user {
let sentryUser = Sentry.User()
sentryUser.userId = user.id
sentryUser.username = user.username
sentryUser.segment = user.segment
SentrySDK.setUser(sentryUser)
} else {
SentrySDK.setUser(nil)
}
*/
if let user {
print("[Sentry] Would set user: \(user.id)")
} else {
print("[Sentry] Would clear user")
}
}
func reset() {
// TODO: Uncomment after adding Sentry SDK
/*
SentrySDK.setUser(nil)
SentrySDK.configureScope { scope in
scope.clear()
}
*/
print("[Sentry] Would reset session")
}
// MARK: - Helpers
private var releaseVersion: String {
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "0"
let bundleID = Bundle.main.bundleIdentifier ?? "unknown"
return "\(bundleID)@\(version)+\(build)"
}
}
// MARK: - Sentry Level Mapping
/*
// Uncomment after adding Sentry SDK
extension ErrorLevel {
var sentryLevel: SentryLevel {
switch self {
case .debug: return .debug
case .info: return .info
case .warning: return .warning
case .error: return .error
case .fatal: return .fatal
}
}
}
*/