Skip to content

Commit d93fc59

Browse files
authored
[#[599] Firebase Crashlytics를 붙인다 (#617)
* chore: Firebase Crashlytics 의존성 추가 * docs: 문서 추가 * chore: Firebase Crashlytics 설정 추가 * feat: Infra 오류 Crashlytics 기록 추가 * chore: Debug dSYM 생성 설정 추가 * fix: Debug 빌드 Crashlytics 수집 비활성화 * refactor: Crashlytics 오류 도메인 세분화 * fix: Crashlytics 원본 오류 정보 보존 * fix: Crashlytics 수집 정책 설정 개선 * fix: Debug dSYM 생성 설정 정리 * fix: Debug 빌드 dSYM 업로드 스크립트 제거 * chore: CI Homebrew trust 설정 추가
1 parent b520981 commit d93fc59

19 files changed

Lines changed: 555 additions & 131 deletions

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ jobs:
5959
cache: true
6060

6161
- name: Install SwiftLint
62+
env:
63+
HOMEBREW_REQUIRE_TAP_TRUST: "1"
6264
shell: bash
6365
run: |
6466
set -euo pipefail
@@ -264,6 +266,8 @@ jobs:
264266
cache: true
265267

266268
- name: Install SwiftLint
269+
env:
270+
HOMEBREW_REQUIRE_TAP_TRUST: "1"
267271
shell: bash
268272
run: |
269273
set -euo pipefail

Application/DevLogApp/Project.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,13 @@ let project = Project(
5151
],
5252
debug: [
5353
"APS_ENVIRONMENT": "development",
54+
"DEBUG_INFORMATION_FORMAT": "dwarf",
55+
"INFOPLIST_KEY_FirebaseCrashlyticsCollectionEnabled": "NO",
5456
],
5557
release: [
5658
"APS_ENVIRONMENT": "production",
59+
"DEBUG_INFORMATION_FORMAT": "dwarf-with-dsym",
60+
"INFOPLIST_KEY_FirebaseCrashlyticsCollectionEnabled": "YES",
5761
]
5862
)
5963
),
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//
2+
// FirebaseCrashlyticsHelper.swift
3+
// DevLogInfra
4+
//
5+
// Created by opfic on 6/16/26.
6+
//
7+
8+
import FirebaseCrashlytics
9+
import Foundation
10+
11+
enum FirebaseCrashlyticsHelper {
12+
static func record(
13+
_ error: Error,
14+
domain: String,
15+
code: Int,
16+
metadata: [String: String] = [:],
17+
logs: [String] = []
18+
) {
19+
let nsError = error as NSError
20+
let report = NSError(
21+
domain: domain,
22+
code: code,
23+
userInfo: userInfo(for: nsError, error: error, metadata: metadata)
24+
)
25+
let crashlytics = Crashlytics.crashlytics()
26+
27+
logs.forEach {
28+
crashlytics.log($0)
29+
}
30+
31+
crashlytics.record(error: report)
32+
}
33+
}
34+
35+
private extension FirebaseCrashlyticsHelper {
36+
enum Key: String {
37+
case underlyingType
38+
case underlyingDomain
39+
case underlyingCode
40+
}
41+
42+
static func userInfo(
43+
for nsError: NSError,
44+
error: Error,
45+
metadata: [String: String]
46+
) -> [String: Any] {
47+
var userInfo: [String: Any] = [
48+
NSUnderlyingErrorKey: nsError,
49+
Key.underlyingType.rawValue: String(describing: type(of: error)),
50+
Key.underlyingDomain.rawValue: nsError.domain,
51+
Key.underlyingCode.rawValue: nsError.code
52+
]
53+
54+
metadata.forEach {
55+
userInfo[$0.key] = $0.value
56+
}
57+
58+
return userInfo
59+
}
60+
}

Application/DevLogInfra/Sources/Service/AuthServiceImpl.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@ import DevLogCore
1313
import DevLogData
1414

1515
final class AuthServiceImpl: AuthService {
16+
private enum CrashlyticsError {
17+
static let domain = "DevLogInfra.AuthServiceImpl"
18+
19+
enum Code: Int {
20+
case getProviderID = 1
21+
case deleteCurrentUser
22+
case deleteMessagingToken
23+
case signOut
24+
}
25+
}
26+
1627
private let store = Firestore.firestore()
1728
private let messaging = Messaging.messaging()
1829
private let logger = Logger(category: "AuthServiceImpl")
@@ -87,6 +98,7 @@ final class AuthServiceImpl: AuthService {
8798
return providerID
8899
} catch {
89100
logger.error("Failed to fetch provider ID", error: error)
101+
record(error, code: .getProviderID)
90102
throw error
91103
}
92104
}
@@ -103,6 +115,7 @@ final class AuthServiceImpl: AuthService {
103115
try await currentUser.delete()
104116
} catch {
105117
logger.error("Failed to delete FirebaseAuth current user", error: error)
118+
record(error, code: .deleteCurrentUser)
106119
throw error
107120
}
108121
}
@@ -114,19 +127,33 @@ final class AuthServiceImpl: AuthService {
114127
try await messaging.deleteToken()
115128
} catch {
116129
logger.error("Failed to delete FCM token while clearing session", error: error)
130+
record(error, code: .deleteMessagingToken)
117131
}
118132

119133
do {
120134
try Auth.auth().signOut()
121135
} catch {
122136
logger.error("Failed to sign out while clearing session", error: error)
137+
record(error, code: .signOut)
123138
throw error
124139
}
125140
}
126141

127142
}
128143

129144
private extension AuthServiceImpl {
145+
private static func record(_ error: Error, code: CrashlyticsError.Code) {
146+
FirebaseCrashlyticsHelper.record(
147+
error,
148+
domain: "\(CrashlyticsError.domain).\(code)",
149+
code: code.rawValue
150+
)
151+
}
152+
153+
private func record(_ error: Error, code: CrashlyticsError.Code) {
154+
Self.record(error, code: code)
155+
}
156+
130157
func handleAuthStateChange(_ user: User?) {
131158
let signedIn = user != nil
132159
logger.info("Firebase auth state changed. signedIn: \(signedIn)")

Application/DevLogInfra/Sources/Service/FirebaseAppServiceImpl.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import DevLogData
9+
import FirebaseCrashlytics
910
import FirebaseCore
1011

1112
final class FirebaseAppServiceImpl: FirebaseAppService {
@@ -15,6 +16,15 @@ final class FirebaseAppServiceImpl: FirebaseAppService {
1516
guard !Self.isConfigured else { return }
1617

1718
FirebaseApp.configure()
19+
enableCrashlyticsCollectionIfNeeded()
1820
Self.isConfigured = true
1921
}
2022
}
23+
24+
private extension FirebaseAppServiceImpl {
25+
func enableCrashlyticsCollectionIfNeeded() {
26+
#if !DEBUG
27+
Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(true)
28+
#endif
29+
}
30+
}

Application/DevLogInfra/Sources/Service/ProfileImageDataServiceImpl.swift

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,33 @@ import Nexa
1010
import DevLogData
1111

1212
final class ProfileImageDataServiceImpl: ProfileImageDataService {
13+
private enum CrashlyticsError {
14+
static let domain = "DevLogInfra.ProfileImageDataServiceImpl"
15+
16+
enum Code: Int {
17+
case fetchImageData = 1
18+
}
19+
}
20+
1321
func fetchImageData(from url: URL) async throws -> Data {
14-
try await NXAPIClient(
15-
configuration: NXClientConfiguration(baseURL: url)
16-
)
17-
.get()
18-
.timeout(10)
19-
.intercept(ProfileImageDataCachePolicyInterceptor())
20-
.validate(.successStatusCode)
21-
.raw()
22-
.data
22+
do {
23+
return try await NXAPIClient(
24+
configuration: NXClientConfiguration(baseURL: url)
25+
)
26+
.get()
27+
.timeout(10)
28+
.intercept(ProfileImageDataCachePolicyInterceptor())
29+
.validate(.successStatusCode)
30+
.raw()
31+
.data
32+
} catch {
33+
FirebaseCrashlyticsHelper.record(
34+
error,
35+
domain: "\(CrashlyticsError.domain).\(CrashlyticsError.Code.fetchImageData)",
36+
code: CrashlyticsError.Code.fetchImageData.rawValue
37+
)
38+
throw error
39+
}
2340
}
2441
}
2542

Application/DevLogInfra/Sources/Service/PushMessagingServiceImpl.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ import FirebaseMessaging
1111
import UserNotifications
1212

1313
final class PushMessagingServiceImpl: NSObject, PushMessagingService {
14+
private enum CrashlyticsError {
15+
static let domain = "DevLogInfra.PushMessagingServiceImpl"
16+
17+
enum Code: Int {
18+
case fetchFCMToken = 1
19+
}
20+
}
21+
1422
private weak var delegate: PushMessagingServiceDelegate?
1523

1624
func setDelegate(_ delegate: PushMessagingServiceDelegate?) {
@@ -42,6 +50,11 @@ final class PushMessagingServiceImpl: NSObject, PushMessagingService {
4250
if error.isMissingAPNSTokenForFCMToken {
4351
return nil
4452
}
53+
FirebaseCrashlyticsHelper.record(
54+
error,
55+
domain: "\(CrashlyticsError.domain).\(CrashlyticsError.Code.fetchFCMToken)",
56+
code: CrashlyticsError.Code.fetchFCMToken.rawValue
57+
)
4558
throw error
4659
}
4760
}

Application/DevLogInfra/Sources/Service/PushNotificationServiceImpl.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,22 @@ import DevLogCore
1313
import DevLogData
1414

1515
final class PushNotificationServiceImpl: PushNotificationService {
16+
private enum CrashlyticsError {
17+
static let domain = "DevLogInfra.PushNotificationServiceImpl"
18+
19+
enum Code: Int {
20+
case fetchPushNotificationEnabled = 1
21+
case fetchPushNotificationTime
22+
case updatePushNotificationSettings
23+
case requestNotifications
24+
case observeNotifications
25+
case observeUnreadPushCount
26+
case deleteNotification
27+
case undoDeleteNotification
28+
case toggleNotificationRead
29+
}
30+
}
31+
1632
private enum FunctionName: String {
1733
case requestPushNotificationDeletion
1834
case undoPushNotificationDeletion
@@ -44,6 +60,7 @@ final class PushNotificationServiceImpl: PushNotificationService {
4460
throw FirestoreError.dataNotFound("allowPushNotification")
4561
} catch {
4662
logger.error("Failed to fetch push notification status", error: error)
63+
record(error, code: .fetchPushNotificationEnabled)
4764
throw error
4865
}
4966
}
@@ -72,6 +89,7 @@ final class PushNotificationServiceImpl: PushNotificationService {
7289
return DateComponents(hour: hour, minute: minute)
7390
} catch {
7491
logger.error("Failed to fetch push notification time", error: error)
92+
record(error, code: .fetchPushNotificationTime)
7593
throw error
7694
}
7795
}
@@ -102,6 +120,7 @@ final class PushNotificationServiceImpl: PushNotificationService {
102120
logger.info("Successfully updated push notification settings")
103121
} catch {
104122
logger.error("Failed to update push notification settings", error: error)
123+
record(error, code: .updatePushNotificationSettings)
105124
throw error
106125
}
107126
}
@@ -144,6 +163,7 @@ final class PushNotificationServiceImpl: PushNotificationService {
144163
return PushNotificationPageResponse(items: items, nextCursor: nextCursor)
145164
} catch {
146165
logger.error("Failed to request notifications", error: error)
166+
record(error, code: .requestNotifications)
147167
throw error
148168
}
149169
}
@@ -160,6 +180,7 @@ final class PushNotificationServiceImpl: PushNotificationService {
160180
.limit(to: pageLimit)
161181
.addSnapshotListener { [weak self] snapshot, error in
162182
if let error {
183+
Self.record(error, code: .observeNotifications)
163184
subject.send(completion: .failure(error))
164185
return
165186
}
@@ -190,6 +211,7 @@ final class PushNotificationServiceImpl: PushNotificationService {
190211
.whereField(PushNotificationFieldKey.isDeleted.rawValue, isEqualTo: false)
191212
.addSnapshotListener { snapshot, error in
192213
if let error {
214+
Self.record(error, code: .observeUnreadPushCount)
193215
subject.send(completion: .failure(error))
194216
return
195217
}
@@ -213,6 +235,7 @@ final class PushNotificationServiceImpl: PushNotificationService {
213235
_ = try await function.call(["notificationId": notificationID])
214236
} catch {
215237
logger.error("Failed to request notification deletion", error: error)
238+
record(error, code: .deleteNotification)
216239
throw error
217240
}
218241
}
@@ -225,6 +248,7 @@ final class PushNotificationServiceImpl: PushNotificationService {
225248
_ = try await function.call(["notificationId": notificationID])
226249
} catch {
227250
logger.error("Failed to undo notification deletion", error: error)
251+
record(error, code: .undoDeleteNotification)
228252
throw error
229253
}
230254
}
@@ -259,12 +283,25 @@ final class PushNotificationServiceImpl: PushNotificationService {
259283
logger.info("Successfully toggled notification read")
260284
} catch {
261285
logger.error("Failed to toggle notification read", error: error)
286+
record(error, code: .toggleNotificationRead)
262287
throw error
263288
}
264289
}
265290
}
266291

267292
private extension PushNotificationServiceImpl {
293+
private static func record(_ error: Error, code: CrashlyticsError.Code) {
294+
FirebaseCrashlyticsHelper.record(
295+
error,
296+
domain: "\(CrashlyticsError.domain).\(code)",
297+
code: code.rawValue
298+
)
299+
}
300+
301+
private func record(_ error: Error, code: CrashlyticsError.Code) {
302+
Self.record(error, code: code)
303+
}
304+
268305
func makeQuery(
269306
uid: String,
270307
query: PushNotificationQuery

0 commit comments

Comments
 (0)