Repository navigation
feat(observability): non-fatal 에러 Crashlytics 원격화 + dSYM 업로드 - #109
Conversation
- CrashlyticsErrorLogger: DefaultErrorLogger를 감싸 record(error:) + feature/operation/metadata 커스텀 키 전송 (Shared는 Firebase-free 유지) - NewdokApp: #if DEBUG 분기로 개발 빌드는 로컬 로깅만, 릴리스만 Crashlytics 등록 - Fastfile: release/dev 레인에 dSYM 업로드 추가 (크래시 심볼화) 치명 크래시는 SDK 링크 + FirebaseApp.configure()로 자동 수집됨. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request integrates Firebase Crashlytics to log non-fatal errors in production while retaining local logging in DEBUG mode, and updates Fastlane to upload dSYM symbols. Feedback suggests passing metadata via NSError's userInfo to prevent global state pollution and race conditions in Crashlytics, and disabling Crashlytics collection entirely in DEBUG builds to avoid polluting production crash data.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| let crashlytics = Crashlytics.crashlytics() | ||
| crashlytics.setCustomValue(context.feature, forKey: "feature") | ||
| crashlytics.setCustomValue(context.operation, forKey: "operation") | ||
| for (key, value) in context.metadata { | ||
| crashlytics.setCustomValue(value, forKey: key) | ||
| } | ||
| crashlytics.record(error: context.underlyingError) |
There was a problem hiding this comment.
Crashlytics.crashlytics().setCustomValue(_:forKey:)를 사용하여 커스텀 키를 설정하면, 이 값들은 Crashlytics 인스턴스에 전역(Global) 및 세션 단위로 저장됩니다.\n\n이 방식은 다음과 같은 심각한 문제를 야기할 수 있습니다:\n1. 데이터 오염 (Global Pollution): 비치명적(non-fatal) 에러가 발생한 후, 앱의 전혀 다른 부분에서 치명적(fatal) 크래시가 발생하면 이전에 설정된 feature, operation 및 메타데이터가 해당 크래시 리포트에 그대로 첨부되어 잘못된 분석 결과를 초래합니다.\n2. 경쟁 조건 (Race Condition): 여러 스레드에서 동시에 에러가 발생하여 logError가 호출될 경우, 전역 Crashlytics 인스턴스의 커스텀 키가 서로 덮어씌워져 record(error:) 시점에 엉뚱한 메타데이터가 기록될 수 있습니다.\n\n해결 방안:\nSwift Error를 NSError로 변환하고 userInfo에 메타데이터를 담아 record(error:)를 호출하면, Crashlytics가 해당 에러 이벤트에만 국한된 개별 커스텀 키(Keys)로 자동 파싱하여 기록하므로 전역 상태 오염과 경쟁 조건을 완벽히 방지할 수 있습니다.
let underlyingNSError = context.underlyingError as NSError
var userInfo: [String: Any] = [
"feature": context.feature,
"operation": context.operation,
"file": (context.file as NSString).lastPathComponent,
"line": context.line,
NSLocalizedDescriptionKey: context.underlyingError.localizedDescription
]
for (key, value) in context.metadata {
userInfo[key] = value
}
let nsError = NSError(
domain: "com.newdok.\(context.feature)",
code: underlyingNSError.code,
userInfo: userInfo
)
Crashlytics.crashlytics().record(error: nsError)| #if DEBUG | ||
| ErrorLoggerRegistry.register(DefaultErrorLogger()) // 로컬 로깅만 (Crashlytics 오염 방지) | ||
| #else | ||
| ErrorLoggerRegistry.register(CrashlyticsErrorLogger()) // 로컬 + Crashlytics non-fatal | ||
| #endif |
There was a problem hiding this comment.
#if DEBUG 분기를 통해 개발 빌드에서는 DefaultErrorLogger만 등록하여 비치명적(non-fatal) 에러가 Crashlytics로 전송되는 것은 막았지만, 치명적(fatal) 크래시(예: 강제 언랩 실패, Out of Bounds 등)는 여전히 Crashlytics SDK에 의해 자동으로 수집되어 프로덕션 대시보드로 전송됩니다.\n\n개발 중 발생하는 크래시가 프로덕션 Crashlytics 대시보드를 오염시키는 것을 완전히 방지하려면, DEBUG 빌드에서 Crashlytics의 데이터 수집 자체를 비활성화하는 것이 안전합니다.
| #if DEBUG | |
| ErrorLoggerRegistry.register(DefaultErrorLogger()) // 로컬 로깅만 (Crashlytics 오염 방지) | |
| #else | |
| ErrorLoggerRegistry.register(CrashlyticsErrorLogger()) // 로컬 + Crashlytics non-fatal | |
| #endif | |
| #if DEBUG | |
| Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(false) | |
| ErrorLoggerRegistry.register(DefaultErrorLogger()) // 로컬 로깅만 (Crashlytics 오염 방지) | |
| #else | |
| ErrorLoggerRegistry.register(CrashlyticsErrorLogger()) // 로컬 + Crashlytics non-fatal | |
| #endif |
요약
프로덕션 크래시/에러 관측성 확보. 기존엔
FirebaseApp.configure()만 있고 Crashlytics가 코드에서 전혀 안 쓰여 크래시가 리포팅되지 않던 상태를 해결.변경
CrashlyticsErrorLogger(App 모듈 신규) —DefaultErrorLogger를 감싸 로컬 로깅 보존 +Crashlytics.record(error:)및 feature/operation/metadata를 커스텀 키로 전송.Shared는 Firebase-free 유지 위해 어댑터를 App에 둠.NewdokApp—#if DEBUG분기: 개발 빌드는 로컬 로깅만(대시보드 오염 방지), 릴리스만 Crashlytics 등록.Fastfile—release·dev레인에upload_symbols_to_crashlytics추가. 없으면 크래시가 심볼화되지 않음.동작 방식
configure()로 자동 수집 (코드 0).ErrorLoggerRegistry경로의 에러를 원격화.검증
tuist generate+xcodebuildDebug 빌드 성공.GoogleService-Info.plist존재, Firebase SPM 11.0.0 resolved, Release=dwarf-with-dsym.후속 (별개)
setUserID로 유저별 크래시 추적.🤖 Generated with Claude Code