Skip to content

feat(observability): non-fatal 에러 Crashlytics 원격화 + dSYM 업로드 - #109

Merged
gomminjae merged 1 commit into
developfrom
feat/crashlytics-nonfatal
Jul 4, 2026
Merged

gomminjae merged 1 commit into
developfrom
feat/crashlytics-nonfatal

Conversation

@gomminjae

Copy link
Copy Markdown
Collaborator

요약

프로덕션 크래시/에러 관측성 확보. 기존엔 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 추가. 없으면 크래시가 심볼화되지 않음.

동작 방식

  • 치명 크래시: SDK 링크 + configure()로 자동 수집 (코드 0).
  • non-fatal: 삼켜지던 ErrorLoggerRegistry 경로의 에러를 원격화.

검증

  • tuist generate + xcodebuild Debug 빌드 성공.
  • 전제 확인: GoogleService-Info.plist 존재, Firebase SPM 11.0.0 resolved, Release=dwarf-with-dsym.

후속 (별개)

  • App Store 제출 시 앱 개인정보 매니페스트 데이터 수집 공시 확인.
  • (선택) setUserID로 유저별 크래시 추적.

🤖 Generated with Claude Code

- 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>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +19 to +25
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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)

Comment on lines +19 to +23
#if DEBUG
ErrorLoggerRegistry.register(DefaultErrorLogger()) // 로컬 로깅만 (Crashlytics 오염 방지)
#else
ErrorLoggerRegistry.register(CrashlyticsErrorLogger()) // 로컬 + Crashlytics non-fatal
#endif

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

#if DEBUG 분기를 통해 개발 빌드에서는 DefaultErrorLogger만 등록하여 비치명적(non-fatal) 에러가 Crashlytics로 전송되는 것은 막았지만, 치명적(fatal) 크래시(예: 강제 언랩 실패, Out of Bounds 등)는 여전히 Crashlytics SDK에 의해 자동으로 수집되어 프로덕션 대시보드로 전송됩니다.\n\n개발 중 발생하는 크래시가 프로덕션 Crashlytics 대시보드를 오염시키는 것을 완전히 방지하려면, DEBUG 빌드에서 Crashlytics의 데이터 수집 자체를 비활성화하는 것이 안전합니다.

Suggested change
#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

@gomminjae
gomminjae merged commit 2a740b0 into develop Jul 4, 2026
1 check passed
@gomminjae
gomminjae deleted the feat/crashlytics-nonfatal branch July 4, 2026 05:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant