From ff5636846e0183708dd976032c168612c582c03a Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Tue, 16 Jun 2026 09:19:11 +0900 Subject: [PATCH 01/12] =?UTF-8?q?chore:=20Firebase=20Crashlytics=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tuist/ProjectDescriptionHelpers/Project+Packages.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tuist/ProjectDescriptionHelpers/Project+Packages.swift b/Tuist/ProjectDescriptionHelpers/Project+Packages.swift index 6c9bbdb1..dd115836 100644 --- a/Tuist/ProjectDescriptionHelpers/Project+Packages.swift +++ b/Tuist/ProjectDescriptionHelpers/Project+Packages.swift @@ -37,6 +37,7 @@ public enum DevLogPackages { .package(product: "FirebaseCore"), .package(product: "FirebaseFunctions"), .package(product: "FirebaseAuth"), + .package(product: "FirebaseCrashlytics"), .package(product: "FirebaseMessaging"), .package(product: "FirebaseFirestore"), .package(product: "GoogleSignIn"), From c5d771f7936f9acbc4aee96d3aa4b22c86a6ad01 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Tue, 16 Jun 2026 09:20:10 +0900 Subject: [PATCH 02/12] =?UTF-8?q?docs:=20=EB=AC=B8=EC=84=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 61e1f429..3975b15e 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,8 @@ Todo, 저장 링크, 오늘 할 일, 받은 알림, 누적 활동을 하나의 | Architecture | Tuist Modular based Clean Architecture | | UI | SwiftUI, MarkdownUI, WidgetKit, AppIntents | | State & Async | Observable, Combine, async/await, The Composable Architecture | -| Backend | Firebase Authentication, Firestore, Cloud Functions, Cloud Messaging, Analytics | +| Backend | Firebase Authentication, Firestore, Cloud Functions, Cloud Messaging | +| Monitoring | Firebase Analytics, Crashlytics | | Apple Frameworks | AuthenticationServices, UserNotifications, LinkPresentation, Network, CryptoKit, os.log | | External Packages | ComposableArchitecture, MarkdownUI, OrderedCollections, GoogleSignIn, Nexa | | Testing | swift-testing, TCA TestStore | From 90dd77176bff0b868b755f1885fb19f1c25578a4 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:28:20 +0900 Subject: [PATCH 03/12] =?UTF-8?q?chore:=20Firebase=20Crashlytics=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Application/DevLogApp/Project.swift | 1 + .../Project+Packages.swift | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/Application/DevLogApp/Project.swift b/Application/DevLogApp/Project.swift index 8e89fc3b..2242b51e 100644 --- a/Application/DevLogApp/Project.swift +++ b/Application/DevLogApp/Project.swift @@ -29,6 +29,7 @@ let project = Project( sourcePath: "Sources", configPath: "Sources/.swiftlint.yml" ), + DevLogScripts.firebaseCrashlyticsDSYMUpload(), ], dependencies: [ .project(target: "DevLogPresentation", path: "../DevLogPresentation"), diff --git a/Tuist/ProjectDescriptionHelpers/Project+Packages.swift b/Tuist/ProjectDescriptionHelpers/Project+Packages.swift index dd115836..2bd3474a 100644 --- a/Tuist/ProjectDescriptionHelpers/Project+Packages.swift +++ b/Tuist/ProjectDescriptionHelpers/Project+Packages.swift @@ -60,6 +60,25 @@ public enum DevLogPackages { } public enum DevLogScripts { + public static func firebaseCrashlyticsDSYMUpload() -> TargetScript { + TargetScript.post( + script: """ + "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run" + """, + name: "Firebase Crashlytics dSYM Upload", + inputPaths: [ + "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}", + "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}", + "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}.debug.dylib", + "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist", + "$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist", + "$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)", + ], + basedOnDependencyAnalysis: false, + shellPath: "/bin/bash" + ) + } + public static func swiftLint( sourcePath: String, configPath: String = "../../.swiftlint.yml" From 0d6d989a0b922bbaafa71511115f52e5da23081f Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:28:47 +0900 Subject: [PATCH 04/12] =?UTF-8?q?feat:=20Infra=20=EC=98=A4=EB=A5=98=20Cras?= =?UTF-8?q?hlytics=20=EA=B8=B0=EB=A1=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Common/FirebaseCrashlyticsHelper.swift | 59 ++++ .../Sources/Service/AuthServiceImpl.swift | 27 ++ .../Service/ProfileImageDataServiceImpl.swift | 35 ++- .../Service/PushMessagingServiceImpl.swift | 13 + .../Service/PushNotificationServiceImpl.swift | 37 +++ .../AppleAuthenticationServiceImpl.swift | 31 ++ .../GithubAuthenticationServiceImpl.swift | 29 ++ .../GoogleAuthenticationServiceImpl.swift | 30 ++ .../Service/TodoCategoryServiceImpl.swift | 23 ++ .../Sources/Service/TodoServiceImpl.swift | 281 ++++++++++-------- .../Sources/Service/UserServiceImpl.swift | 31 ++ .../Service/WebPageMetadataServiceImpl.swift | 34 ++- .../Sources/Service/WebPageServiceImpl.swift | 27 ++ 13 files changed, 527 insertions(+), 130 deletions(-) create mode 100644 Application/DevLogInfra/Sources/Common/FirebaseCrashlyticsHelper.swift diff --git a/Application/DevLogInfra/Sources/Common/FirebaseCrashlyticsHelper.swift b/Application/DevLogInfra/Sources/Common/FirebaseCrashlyticsHelper.swift new file mode 100644 index 00000000..2322b733 --- /dev/null +++ b/Application/DevLogInfra/Sources/Common/FirebaseCrashlyticsHelper.swift @@ -0,0 +1,59 @@ +// +// FirebaseCrashlyticsHelper.swift +// DevLogInfra +// +// Created by opfic on 6/16/26. +// + +import FirebaseCrashlytics +import Foundation + +enum FirebaseCrashlyticsHelper { + static func record( + _ error: Error, + domain: String, + code: Int, + metadata: [String: String] = [:], + logs: [String] = [] + ) { + let nsError = error as NSError + let report = NSError( + domain: domain, + code: code, + userInfo: userInfo(for: nsError, error: error, metadata: metadata) + ) + let crashlytics = Crashlytics.crashlytics() + + logs.forEach { + crashlytics.log($0) + } + + crashlytics.record(error: report) + } +} + +private extension FirebaseCrashlyticsHelper { + enum Key: String { + case underlyingType + case underlyingDomain + case underlyingCode + } + + static func userInfo( + for nsError: NSError, + error: Error, + metadata: [String: String] + ) -> [String: Any] { + var userInfo: [String: Any] = [ + Key.underlyingType.rawValue: String(describing: type(of: error)), + Key.underlyingDomain.rawValue: nsError.domain, + Key.underlyingCode.rawValue: nsError.code + ] + + metadata.forEach { + userInfo[$0.key] = $0.value + } + + return userInfo + } +} diff --git a/Application/DevLogInfra/Sources/Service/AuthServiceImpl.swift b/Application/DevLogInfra/Sources/Service/AuthServiceImpl.swift index c8f97147..28bb53ed 100644 --- a/Application/DevLogInfra/Sources/Service/AuthServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/AuthServiceImpl.swift @@ -13,6 +13,17 @@ import DevLogCore import DevLogData final class AuthServiceImpl: AuthService { + private enum CrashlyticsError { + static let domain = "DevLogInfra.AuthServiceImpl" + + enum Code: Int { + case getProviderID = 1 + case deleteCurrentUser + case deleteMessagingToken + case signOut + } + } + private let store = Firestore.firestore() private let messaging = Messaging.messaging() private let logger = Logger(category: "AuthServiceImpl") @@ -87,6 +98,7 @@ final class AuthServiceImpl: AuthService { return providerID } catch { logger.error("Failed to fetch provider ID", error: error) + record(error, code: .getProviderID) throw error } } @@ -103,6 +115,7 @@ final class AuthServiceImpl: AuthService { try await currentUser.delete() } catch { logger.error("Failed to delete FirebaseAuth current user", error: error) + record(error, code: .deleteCurrentUser) throw error } } @@ -114,12 +127,14 @@ final class AuthServiceImpl: AuthService { try await messaging.deleteToken() } catch { logger.error("Failed to delete FCM token while clearing session", error: error) + record(error, code: .deleteMessagingToken) } do { try Auth.auth().signOut() } catch { logger.error("Failed to sign out while clearing session", error: error) + record(error, code: .signOut) throw error } } @@ -127,6 +142,18 @@ final class AuthServiceImpl: AuthService { } private extension AuthServiceImpl { + private static func record(_ error: Error, code: CrashlyticsError.Code) { + FirebaseCrashlyticsHelper.record( + error, + domain: CrashlyticsError.domain, + code: code.rawValue + ) + } + + private func record(_ error: Error, code: CrashlyticsError.Code) { + Self.record(error, code: code) + } + func handleAuthStateChange(_ user: User?) { let signedIn = user != nil logger.info("Firebase auth state changed. signedIn: \(signedIn)") diff --git a/Application/DevLogInfra/Sources/Service/ProfileImageDataServiceImpl.swift b/Application/DevLogInfra/Sources/Service/ProfileImageDataServiceImpl.swift index aa4776b0..82888dfa 100644 --- a/Application/DevLogInfra/Sources/Service/ProfileImageDataServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/ProfileImageDataServiceImpl.swift @@ -10,16 +10,33 @@ import Nexa import DevLogData final class ProfileImageDataServiceImpl: ProfileImageDataService { + private enum CrashlyticsError { + static let domain = "DevLogInfra.ProfileImageDataServiceImpl" + + enum Code: Int { + case fetchImageData = 1 + } + } + func fetchImageData(from url: URL) async throws -> Data { - try await NXAPIClient( - configuration: NXClientConfiguration(baseURL: url) - ) - .get() - .timeout(10) - .intercept(ProfileImageDataCachePolicyInterceptor()) - .validate(.successStatusCode) - .raw() - .data + do { + return try await NXAPIClient( + configuration: NXClientConfiguration(baseURL: url) + ) + .get() + .timeout(10) + .intercept(ProfileImageDataCachePolicyInterceptor()) + .validate(.successStatusCode) + .raw() + .data + } catch { + FirebaseCrashlyticsHelper.record( + error, + domain: CrashlyticsError.domain, + code: CrashlyticsError.Code.fetchImageData.rawValue + ) + throw error + } } } diff --git a/Application/DevLogInfra/Sources/Service/PushMessagingServiceImpl.swift b/Application/DevLogInfra/Sources/Service/PushMessagingServiceImpl.swift index 0c1ff320..cb5a6164 100644 --- a/Application/DevLogInfra/Sources/Service/PushMessagingServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/PushMessagingServiceImpl.swift @@ -11,6 +11,14 @@ import FirebaseMessaging import UserNotifications final class PushMessagingServiceImpl: NSObject, PushMessagingService { + private enum CrashlyticsError { + static let domain = "DevLogInfra.PushMessagingServiceImpl" + + enum Code: Int { + case fetchFCMToken = 1 + } + } + private weak var delegate: PushMessagingServiceDelegate? func setDelegate(_ delegate: PushMessagingServiceDelegate?) { @@ -42,6 +50,11 @@ final class PushMessagingServiceImpl: NSObject, PushMessagingService { if error.isMissingAPNSTokenForFCMToken { return nil } + FirebaseCrashlyticsHelper.record( + error, + domain: CrashlyticsError.domain, + code: CrashlyticsError.Code.fetchFCMToken.rawValue + ) throw error } } diff --git a/Application/DevLogInfra/Sources/Service/PushNotificationServiceImpl.swift b/Application/DevLogInfra/Sources/Service/PushNotificationServiceImpl.swift index cd6652c5..484ec7af 100644 --- a/Application/DevLogInfra/Sources/Service/PushNotificationServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/PushNotificationServiceImpl.swift @@ -13,6 +13,22 @@ import DevLogCore import DevLogData final class PushNotificationServiceImpl: PushNotificationService { + private enum CrashlyticsError { + static let domain = "DevLogInfra.PushNotificationServiceImpl" + + enum Code: Int { + case fetchPushNotificationEnabled = 1 + case fetchPushNotificationTime + case updatePushNotificationSettings + case requestNotifications + case observeNotifications + case observeUnreadPushCount + case deleteNotification + case undoDeleteNotification + case toggleNotificationRead + } + } + private enum FunctionName: String { case requestPushNotificationDeletion case undoPushNotificationDeletion @@ -44,6 +60,7 @@ final class PushNotificationServiceImpl: PushNotificationService { throw FirestoreError.dataNotFound("allowPushNotification") } catch { logger.error("Failed to fetch push notification status", error: error) + record(error, code: .fetchPushNotificationEnabled) throw error } } @@ -72,6 +89,7 @@ final class PushNotificationServiceImpl: PushNotificationService { return DateComponents(hour: hour, minute: minute) } catch { logger.error("Failed to fetch push notification time", error: error) + record(error, code: .fetchPushNotificationTime) throw error } } @@ -102,6 +120,7 @@ final class PushNotificationServiceImpl: PushNotificationService { logger.info("Successfully updated push notification settings") } catch { logger.error("Failed to update push notification settings", error: error) + record(error, code: .updatePushNotificationSettings) throw error } } @@ -144,6 +163,7 @@ final class PushNotificationServiceImpl: PushNotificationService { return PushNotificationPageResponse(items: items, nextCursor: nextCursor) } catch { logger.error("Failed to request notifications", error: error) + record(error, code: .requestNotifications) throw error } } @@ -160,6 +180,7 @@ final class PushNotificationServiceImpl: PushNotificationService { .limit(to: pageLimit) .addSnapshotListener { [weak self] snapshot, error in if let error { + Self.record(error, code: .observeNotifications) subject.send(completion: .failure(error)) return } @@ -190,6 +211,7 @@ final class PushNotificationServiceImpl: PushNotificationService { .whereField(PushNotificationFieldKey.isDeleted.rawValue, isEqualTo: false) .addSnapshotListener { snapshot, error in if let error { + Self.record(error, code: .observeUnreadPushCount) subject.send(completion: .failure(error)) return } @@ -213,6 +235,7 @@ final class PushNotificationServiceImpl: PushNotificationService { _ = try await function.call(["notificationId": notificationID]) } catch { logger.error("Failed to request notification deletion", error: error) + record(error, code: .deleteNotification) throw error } } @@ -225,6 +248,7 @@ final class PushNotificationServiceImpl: PushNotificationService { _ = try await function.call(["notificationId": notificationID]) } catch { logger.error("Failed to undo notification deletion", error: error) + record(error, code: .undoDeleteNotification) throw error } } @@ -259,12 +283,25 @@ final class PushNotificationServiceImpl: PushNotificationService { logger.info("Successfully toggled notification read") } catch { logger.error("Failed to toggle notification read", error: error) + record(error, code: .toggleNotificationRead) throw error } } } private extension PushNotificationServiceImpl { + private static func record(_ error: Error, code: CrashlyticsError.Code) { + FirebaseCrashlyticsHelper.record( + error, + domain: CrashlyticsError.domain, + code: code.rawValue + ) + } + + private func record(_ error: Error, code: CrashlyticsError.Code) { + Self.record(error, code: code) + } + func makeQuery( uid: String, query: PushNotificationQuery diff --git a/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift b/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift index 914d1b6c..28cb3bf2 100644 --- a/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift @@ -16,6 +16,18 @@ import DevLogCore import DevLogData final class AppleAuthenticationServiceImpl: AuthenticationService { + private enum CrashlyticsError { + static let domain = "DevLogInfra.AppleAuthenticationServiceImpl" + + enum Code: Int { + case signIn = 1 + case signOut + case deleteAuth + case link + case unlink + } + } + private enum FunctionName: String { case requestAppleCustomToken case refreshAppleAccessToken @@ -94,6 +106,7 @@ final class AppleAuthenticationServiceImpl: AuthenticationService { return result.user.makeResponse(providerID: .apple) } catch { logger.error("Failed to sign in with Apple", error: error) + record(error, code: .signIn) throw error } } @@ -112,6 +125,7 @@ final class AppleAuthenticationServiceImpl: AuthenticationService { try Auth.auth().signOut() } catch { logger.error("Failed to sign out with Apple", error: error) + record(error, code: .signOut) throw error } } @@ -123,6 +137,7 @@ final class AppleAuthenticationServiceImpl: AuthenticationService { try await revokeAppleAccessToken(token: token) } catch { logger.error("Failed to delete Apple auth", error: error) + record(error, code: .deleteAuth) throw error } } @@ -157,6 +172,7 @@ final class AppleAuthenticationServiceImpl: AuthenticationService { try await user?.link(with: appleCredential) } catch { logger.error("Failed to link Apple account", error: error) + record(error, code: .link) if error.isFirebaseCredentialAlreadyInUse { throw DataLayerError.linkCredentialAlreadyInUse } @@ -188,6 +204,7 @@ final class AppleAuthenticationServiceImpl: AuthenticationService { _ = try await user?.unlink(fromProvider: providerID.rawValue) } catch { logger.error("Failed to unlink Apple account", error: error) + record(error, code: .unlink) throw error } } @@ -311,3 +328,17 @@ final class AppleAuthenticationServiceImpl: AuthenticationService { _ = try await revokeFunction.call(["token": token]) } } + +private extension AppleAuthenticationServiceImpl { + private static func record(_ error: Error, code: CrashlyticsError.Code) { + FirebaseCrashlyticsHelper.record( + error, + domain: CrashlyticsError.domain, + code: code.rawValue + ) + } + + private func record(_ error: Error, code: CrashlyticsError.Code) { + Self.record(error, code: code) + } +} diff --git a/Application/DevLogInfra/Sources/Service/SocialLogin/GithubAuthenticationServiceImpl.swift b/Application/DevLogInfra/Sources/Service/SocialLogin/GithubAuthenticationServiceImpl.swift index e3b0a847..25d39802 100644 --- a/Application/DevLogInfra/Sources/Service/SocialLogin/GithubAuthenticationServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/SocialLogin/GithubAuthenticationServiceImpl.swift @@ -16,6 +16,18 @@ import DevLogCore import DevLogData final class GithubAuthenticationServiceImpl: NSObject, AuthenticationService { + private enum CrashlyticsError { + static let domain = "DevLogInfra.GithubAuthenticationServiceImpl" + + enum Code: Int { + case signIn = 1 + case signOut + case deleteAuth + case link + case unlink + } + } + private enum FunctionName: String { case requestGithubTokens case revokeGithubAccessToken @@ -79,6 +91,7 @@ final class GithubAuthenticationServiceImpl: NSObject, AuthenticationService { ) } catch { logger.error("Failed to sign in with GitHub", error: error) + record(error, code: .signIn) throw error } } @@ -97,6 +110,7 @@ final class GithubAuthenticationServiceImpl: NSObject, AuthenticationService { try Auth.auth().signOut() } catch { logger.error("Failed to sign out with GitHub", error: error) + record(error, code: .signOut) throw error } } @@ -106,6 +120,7 @@ final class GithubAuthenticationServiceImpl: NSObject, AuthenticationService { try await revokeAccessToken() } catch { logger.error("Failed to delete GitHub auth", error: error) + record(error, code: .deleteAuth) throw error } } @@ -140,6 +155,7 @@ final class GithubAuthenticationServiceImpl: NSObject, AuthenticationService { logger.info("Successfully linked GitHub account") } catch { logger.error("Failed to link GitHub account", error: error) + record(error, code: .link) if error.isFirebaseCredentialAlreadyInUse { throw DataLayerError.linkCredentialAlreadyInUse } @@ -161,6 +177,7 @@ final class GithubAuthenticationServiceImpl: NSObject, AuthenticationService { _ = try await user?.unlink(fromProvider: providerID.rawValue) } catch { logger.error("Failed to unlink GitHub account", error: error) + record(error, code: .unlink) throw error } } @@ -304,6 +321,18 @@ final class GithubAuthenticationServiceImpl: NSObject, AuthenticationService { } private extension GithubAuthenticationServiceImpl { + private static func record(_ error: Error, code: CrashlyticsError.Code) { + FirebaseCrashlyticsHelper.record( + error, + domain: CrashlyticsError.domain, + code: code.rawValue + ) + } + + private func record(_ error: Error, code: CrashlyticsError.Code) { + Self.record(error, code: code) + } + struct GitHubUser: Codable { let login: String let name: String? diff --git a/Application/DevLogInfra/Sources/Service/SocialLogin/GoogleAuthenticationServiceImpl.swift b/Application/DevLogInfra/Sources/Service/SocialLogin/GoogleAuthenticationServiceImpl.swift index 50d63525..4c828411 100644 --- a/Application/DevLogInfra/Sources/Service/SocialLogin/GoogleAuthenticationServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/SocialLogin/GoogleAuthenticationServiceImpl.swift @@ -14,6 +14,18 @@ import DevLogCore import DevLogData final class GoogleAuthenticationServiceImpl: AuthenticationService { + private enum CrashlyticsError { + static let domain = "DevLogInfra.GoogleAuthenticationServiceImpl" + + enum Code: Int { + case signIn = 1 + case signOut + case deleteAuth + case link + case unlink + } + } + private let store = Firestore.firestore() private let messaging = Messaging.messaging() private var user: User? { Auth.auth().currentUser } @@ -55,6 +67,7 @@ final class GoogleAuthenticationServiceImpl: AuthenticationService { return result.user.makeResponse(providerID: .google) } catch { logger.error("Failed to sign in with Google", error: error) + record(error, code: .signIn) throw error } } @@ -76,6 +89,7 @@ final class GoogleAuthenticationServiceImpl: AuthenticationService { try Auth.auth().signOut() } catch { logger.error("Failed to sign out with Google", error: error) + record(error, code: .signOut) throw error } } @@ -86,6 +100,7 @@ final class GoogleAuthenticationServiceImpl: AuthenticationService { try await GIDSignIn.sharedInstance.disconnect() } catch { logger.error("Failed to delete Google auth", error: error) + record(error, code: .deleteAuth) throw error } } @@ -121,6 +136,7 @@ final class GoogleAuthenticationServiceImpl: AuthenticationService { try await user?.link(with: credential) } catch { logger.error("Failed to link Google account", error: error) + record(error, code: .link) if error.isFirebaseCredentialAlreadyInUse { throw DataLayerError.linkCredentialAlreadyInUse } @@ -138,8 +154,22 @@ final class GoogleAuthenticationServiceImpl: AuthenticationService { _ = try await user?.unlink(fromProvider: AuthProviderID.google.rawValue) } catch { logger.error("Failed to unlink Google account", error: error) + record(error, code: .unlink) throw error } } +} +private extension GoogleAuthenticationServiceImpl { + private static func record(_ error: Error, code: CrashlyticsError.Code) { + FirebaseCrashlyticsHelper.record( + error, + domain: CrashlyticsError.domain, + code: code.rawValue + ) + } + + private func record(_ error: Error, code: CrashlyticsError.Code) { + Self.record(error, code: code) + } } diff --git a/Application/DevLogInfra/Sources/Service/TodoCategoryServiceImpl.swift b/Application/DevLogInfra/Sources/Service/TodoCategoryServiceImpl.swift index e058eb2b..de48dd2b 100644 --- a/Application/DevLogInfra/Sources/Service/TodoCategoryServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/TodoCategoryServiceImpl.swift @@ -11,6 +11,15 @@ import DevLogCore import DevLogData final class TodoCategoryServiceImpl: TodoCategoryService { + private enum CrashlyticsError { + static let domain = "DevLogInfra.TodoCategoryServiceImpl" + + enum Code: Int { + case fetchCategoryPreferences = 1 + case updateCategoryPreferences + } + } + private enum Field: String { case items case kind @@ -57,6 +66,7 @@ final class TodoCategoryServiceImpl: TodoCategoryService { return preferences } catch { logger.error("Failed to fetch todo category preferences", error: error) + record(error, code: .fetchCategoryPreferences) throw error } } @@ -79,12 +89,25 @@ final class TodoCategoryServiceImpl: TodoCategoryService { logger.info("Successfully updated todo category preferences") } catch { logger.error("Failed to update todo category preferences", error: error) + record(error, code: .updateCategoryPreferences) throw error } } } private extension TodoCategoryServiceImpl { + private static func record(_ error: Error, code: CrashlyticsError.Code) { + FirebaseCrashlyticsHelper.record( + error, + domain: CrashlyticsError.domain, + code: code.rawValue + ) + } + + private func record(_ error: Error, code: CrashlyticsError.Code) { + Self.record(error, code: code) + } + func makePreference(_ items: [String: Any]) -> TodoCategoryPreferenceResponse? { guard let kindString = items[Field.kind.rawValue] as? String, diff --git a/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift b/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift index 7327d8fd..ecad46a1 100644 --- a/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift @@ -12,6 +12,19 @@ import DevLogCore import DevLogData final class TodoServiceImpl: TodoService { + private enum CrashlyticsError { + static let domain = "DevLogInfra.TodoServiceImpl" + + enum Code: Int { + case fetchTodos = 1 + case upsertTodo + case deleteTodo + case undoDeleteTodo + case fetchTodo + case fetchReferences + } + } + private enum FunctionName: String { case requestTodoDeletion case undoTodoDeletion @@ -49,122 +62,128 @@ final class TodoServiceImpl: TodoService { ] logger.info("Fetching todo page: \(logComponents.compactMap { $0 }.joined(separator: ", "))") - var firestoreQuery = makeQuery(uid: uid, query: query) + do { + var firestoreQuery = makeQuery(uid: uid, query: query) - if let categoryId = query.categoryId { - firestoreQuery = firestoreQuery.whereField( - TodoFieldKey.category.rawValue, - isEqualTo: categoryId - ) - } + if let categoryId = query.categoryId { + firestoreQuery = firestoreQuery.whereField( + TodoFieldKey.category.rawValue, + isEqualTo: categoryId + ) + } - if query.isPinned { - firestoreQuery = firestoreQuery.whereField("isPinned", isEqualTo: true) - } + if query.isPinned { + firestoreQuery = firestoreQuery.whereField("isPinned", isEqualTo: true) + } - if let isCompleted = query.completionFilter.isCompletedValue { - firestoreQuery = firestoreQuery.whereField("isCompleted", isEqualTo: isCompleted) - } + if let isCompleted = query.completionFilter.isCompletedValue { + firestoreQuery = firestoreQuery.whereField("isCompleted", isEqualTo: isCompleted) + } - switch query.dueDateFilter { - case .all: - break - case .withDueDate: - firestoreQuery = firestoreQuery.whereField( - "dueDate", - isGreaterThan: Timestamp(date: Date(timeIntervalSince1970: 0)) - ) - case .withoutDueDate: - firestoreQuery = firestoreQuery.whereField("dueDate", isEqualTo: NSNull()) - } + switch query.dueDateFilter { + case .all: + break + case .withDueDate: + firestoreQuery = firestoreQuery.whereField( + "dueDate", + isGreaterThan: Timestamp(date: Date(timeIntervalSince1970: 0)) + ) + case .withoutDueDate: + firestoreQuery = firestoreQuery.whereField("dueDate", isEqualTo: NSNull()) + } - if let sortDateFrom = query.sortDateFrom { - firestoreQuery = firestoreQuery.whereField( - query.sortTarget.fieldName, - isGreaterThanOrEqualTo: Timestamp(date: sortDateFrom) - ) - } + if let sortDateFrom = query.sortDateFrom { + firestoreQuery = firestoreQuery.whereField( + query.sortTarget.fieldName, + isGreaterThanOrEqualTo: Timestamp(date: sortDateFrom) + ) + } - if let sortDateTo = query.sortDateTo { - firestoreQuery = firestoreQuery.whereField( - query.sortTarget.fieldName, - isLessThan: Timestamp(date: sortDateTo) - ) - } + if let sortDateTo = query.sortDateTo { + firestoreQuery = firestoreQuery.whereField( + query.sortTarget.fieldName, + isLessThan: Timestamp(date: sortDateTo) + ) + } - if trimmedKeyword.isEmpty { - if query.fetchAllPages { - var allItems: [TodoResponse] = [] - var pageCursor = cursor - - while true { - var pageQuery = firestoreQuery - if let pageCursor { - guard let cursorValues = cursorValues( - for: query, - cursor: pageCursor - ) else { - logger.error("Failed to build cursor values for paginated todo fetch.") - break + if trimmedKeyword.isEmpty { + if query.fetchAllPages { + var allItems: [TodoResponse] = [] + var pageCursor = cursor + + while true { + var pageQuery = firestoreQuery + if let pageCursor { + guard let cursorValues = cursorValues( + for: query, + cursor: pageCursor + ) else { + logger.error("Failed to build cursor values for paginated todo fetch.") + break + } + pageQuery = pageQuery.start(after: cursorValues) } - pageQuery = pageQuery.start(after: cursorValues) - } - pageQuery = pageQuery.limit(to: query.pageSize) - let snapshot = try await pageQuery.getDocuments() - allItems.append(contentsOf: snapshot.documents.compactMap { makeResponse(from: $0) }) + pageQuery = pageQuery.limit(to: query.pageSize) + let snapshot = try await pageQuery.getDocuments() + allItems.append(contentsOf: snapshot.documents.compactMap { makeResponse(from: $0) }) - guard snapshot.documents.count == query.pageSize else { - break - } + guard snapshot.documents.count == query.pageSize else { + break + } - guard let lastDocument = snapshot.documents.last, - let nextCursor = makeCursor( - from: lastDocument, - query: query - ) else { - break + guard let lastDocument = snapshot.documents.last, + let nextCursor = makeCursor( + from: lastDocument, + query: query + ) else { + break + } + + pageCursor = nextCursor } - pageCursor = nextCursor + return TodoPageResponse(items: allItems, nextCursor: nil) } - return TodoPageResponse(items: allItems, nextCursor: nil) - } + if let cursor { + guard let cursorValues = cursorValues(for: query, cursor: cursor) else { + logger.error("Failed to build cursor values for todo fetch.") + return TodoPageResponse(items: [], nextCursor: nil) + } + firestoreQuery = firestoreQuery.start(after: cursorValues) + } - if let cursor { - guard let cursorValues = cursorValues(for: query, cursor: cursor) else { - logger.error("Failed to build cursor values for todo fetch.") - return TodoPageResponse(items: [], nextCursor: nil) + firestoreQuery = firestoreQuery.limit(to: query.pageSize) + let snapshot = try await firestoreQuery.getDocuments() + let items = snapshot.documents.compactMap { makeResponse(from: $0) } + let nextCursor = snapshot.documents.last.flatMap { + makeCursor(from: $0, query: query) } - firestoreQuery = firestoreQuery.start(after: cursorValues) - } - firestoreQuery = firestoreQuery.limit(to: query.pageSize) - let snapshot = try await firestoreQuery.getDocuments() - let items = snapshot.documents.compactMap { makeResponse(from: $0) } - let nextCursor = snapshot.documents.last.flatMap { - makeCursor(from: $0, query: query) + return TodoPageResponse(items: items, nextCursor: nextCursor) } - return TodoPageResponse(items: items, nextCursor: nextCursor) - } + let snapshot = try await firestoreQuery.getDocuments() + let todos = snapshot.documents.compactMap { makeResponse(from: $0) } - let snapshot = try await firestoreQuery.getDocuments() - let todos = snapshot.documents.compactMap { makeResponse(from: $0) } + let todoNumber = searchedTodoNumber(from: trimmedKeyword) + let filtered = todos.filter { todo in + if let todoNumber, todo.number == todoNumber { + return true + } - let todoNumber = searchedTodoNumber(from: trimmedKeyword) - let filtered = todos.filter { todo in - if let todoNumber, todo.number == todoNumber { - return true + return todo.title.localizedCaseInsensitiveContains(trimmedKeyword) + || todo.content.localizedCaseInsensitiveContains(trimmedKeyword) + || todo.tags.contains { $0.localizedCaseInsensitiveContains(trimmedKeyword) } } - return todo.title.localizedCaseInsensitiveContains(trimmedKeyword) - || todo.content.localizedCaseInsensitiveContains(trimmedKeyword) - || todo.tags.contains { $0.localizedCaseInsensitiveContains(trimmedKeyword) } + return TodoPageResponse(items: filtered, nextCursor: nil) + } catch { + logger.error("Failed to fetch todos", error: error) + record(error, code: .fetchTodos) + throw error } - - return TodoPageResponse(items: filtered, nextCursor: nil) } // swiftlint:enable function_body_length @@ -198,6 +217,7 @@ final class TodoServiceImpl: TodoService { logger.info("Successfully upserted todo") } catch { logger.error("Failed to upsert todo", error: error) + record(error, code: .upsertTodo) throw error } } @@ -214,6 +234,7 @@ final class TodoServiceImpl: TodoService { logger.info("Successfully requested todo deletion") } catch { logger.error("Failed to request todo deletion", error: error) + record(error, code: .deleteTodo) throw error } } @@ -230,6 +251,7 @@ final class TodoServiceImpl: TodoService { logger.info("Successfully undone todo deletion") } catch { logger.error("Failed to undo todo deletion", error: error) + record(error, code: .undoDeleteTodo) throw error } } @@ -253,6 +275,7 @@ final class TodoServiceImpl: TodoService { return todo } catch { logger.error("Failed to fetch todo", error: error) + record(error, code: .fetchTodo) throw error } } @@ -263,45 +286,63 @@ final class TodoServiceImpl: TodoService { let uniqueNumbers = Array(Set(numbers)).sorted() if uniqueNumbers.isEmpty { return [:] } - let collection = store.collection(FirestorePath.todos(uid)) - let snapshots = try await withThrowingTaskGroup(of: [QueryDocumentSnapshot].self) { group in - for chunk in uniqueNumbers.chunked(maxCount: 10) { - group.addTask { - let snapshot = try await collection - .whereField(TodoFieldKey.number.rawValue, in: chunk) - .whereField(TodoFieldKey.deletedAt.rawValue, isEqualTo: NSNull()) - .getDocuments() - return snapshot.documents + do { + let collection = store.collection(FirestorePath.todos(uid)) + let snapshots = try await withThrowingTaskGroup(of: [QueryDocumentSnapshot].self) { group in + for chunk in uniqueNumbers.chunked(maxCount: 10) { + group.addTask { + let snapshot = try await collection + .whereField(TodoFieldKey.number.rawValue, in: chunk) + .whereField(TodoFieldKey.deletedAt.rawValue, isEqualTo: NSNull()) + .getDocuments() + return snapshot.documents + } } - } - var documents = [QueryDocumentSnapshot]() - for try await chunkDocuments in group { - documents.append(contentsOf: chunkDocuments) + var documents = [QueryDocumentSnapshot]() + for try await chunkDocuments in group { + documents.append(contentsOf: chunkDocuments) + } + return documents } - return documents - } - return snapshots.reduce(into: [Int: TodoReferenceResponse]()) { partialResult, document in - let data = document.data() - guard - data[TodoFieldKey.deletedAt.rawValue] is NSNull, - let response = makeResponse(from: document) - else { - return - } + return snapshots.reduce(into: [Int: TodoReferenceResponse]()) { partialResult, document in + let data = document.data() + guard + data[TodoFieldKey.deletedAt.rawValue] is NSNull, + let response = makeResponse(from: document) + else { + return + } - partialResult[response.number] = TodoReferenceResponse( - id: response.id, - number: response.number, - title: response.title, - category: response.category - ) + partialResult[response.number] = TodoReferenceResponse( + id: response.id, + number: response.number, + title: response.title, + category: response.category + ) + } + } catch { + logger.error("Failed to fetch todo references", error: error) + record(error, code: .fetchReferences) + throw error } } } private extension TodoServiceImpl { + private static func record(_ error: Error, code: CrashlyticsError.Code) { + FirebaseCrashlyticsHelper.record( + error, + domain: CrashlyticsError.domain, + code: code.rawValue + ) + } + + private func record(_ error: Error, code: CrashlyticsError.Code) { + Self.record(error, code: code) + } + func upsertTodoWithNumberOnCreate( _ data: [String: Any], for todoRef: DocumentReference, diff --git a/Application/DevLogInfra/Sources/Service/UserServiceImpl.swift b/Application/DevLogInfra/Sources/Service/UserServiceImpl.swift index eefdceca..bfc7eebb 100644 --- a/Application/DevLogInfra/Sources/Service/UserServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/UserServiceImpl.swift @@ -11,6 +11,18 @@ import DevLogCore import DevLogData final class UserServiceImpl: UserService { + private enum CrashlyticsError { + static let domain = "DevLogInfra.UserServiceImpl" + + enum Code: Int { + case upsertUser = 1 + case fetchUserProfile + case upsertStatusMessage + case updateFCMToken + case updateUserTimeZone + } + } + private let store = Firestore.firestore() private let logger = Logger(category: "UserServiceImpl") @@ -112,6 +124,7 @@ final class UserServiceImpl: UserService { logger.info("Successfully upserted user: \(user.uid)") } catch { logger.error("Failed to upsert user", error: error) + record(error, code: .upsertUser) throw error } } @@ -150,6 +163,7 @@ final class UserServiceImpl: UserService { ) } catch { logger.error("Failed to fetch user profile", error: error) + record(error, code: .fetchUserProfile) throw error } } @@ -168,6 +182,7 @@ final class UserServiceImpl: UserService { logger.info("Successfully upserted status message") } catch { logger.error("Failed to upsert status message", error: error) + record(error, code: .upsertStatusMessage) throw error } } @@ -186,6 +201,7 @@ final class UserServiceImpl: UserService { logger.info("Successfully updated FCM token") } catch { logger.error("Failed to update FCM token", error: error) + record(error, code: .updateFCMToken) throw error } } @@ -207,7 +223,22 @@ final class UserServiceImpl: UserService { logger.info("Successfully updated timeZone") } catch { logger.error("Failed to update timeZone", error: error) + record(error, code: .updateUserTimeZone) throw error } } } + +private extension UserServiceImpl { + private static func record(_ error: Error, code: CrashlyticsError.Code) { + FirebaseCrashlyticsHelper.record( + error, + domain: CrashlyticsError.domain, + code: code.rawValue + ) + } + + private func record(_ error: Error, code: CrashlyticsError.Code) { + Self.record(error, code: code) + } +} diff --git a/Application/DevLogInfra/Sources/Service/WebPageMetadataServiceImpl.swift b/Application/DevLogInfra/Sources/Service/WebPageMetadataServiceImpl.swift index 6526a1f8..ac17a13d 100644 --- a/Application/DevLogInfra/Sources/Service/WebPageMetadataServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/WebPageMetadataServiceImpl.swift @@ -12,6 +12,16 @@ import DevLogCore import DevLogData final class WebPageMetadataServiceImpl: WebPageMetadataService { + private enum CrashlyticsError { + static let domain = "DevLogInfra.WebPageMetadataServiceImpl" + + enum Code: Int { + case fetchMetadata = 1 + case removeCachedImage + case cachedImageURL + } + } + private let imageStore: WebPageImageStore private let logger = Logger(category: "WebPageMetadataServiceImpl") @@ -42,6 +52,7 @@ final class WebPageMetadataServiceImpl: WebPageMetadataService { ) } catch { logger.error("Failed to fetch metadata", error: error) + record(error, code: .fetchMetadata) throw error } } @@ -60,6 +71,7 @@ final class WebPageMetadataServiceImpl: WebPageMetadataService { } } catch { logger.error("Failed to remove cached image", error: error) + record(error, code: .removeCachedImage) } } @@ -68,7 +80,13 @@ final class WebPageMetadataServiceImpl: WebPageMetadataService { throw URLError(.badURL) } - return try await imageStore.cachedImageURL(for: url) + do { + return try await imageStore.cachedImageURL(for: url) + } catch { + logger.error("Failed to fetch cached image URL", error: error) + record(error, code: .cachedImageURL) + throw error + } } private func extractImageURL(from imageProvider: NSItemProvider?, url: URL) async throws -> URL? { @@ -97,3 +115,17 @@ final class WebPageMetadataServiceImpl: WebPageMetadataService { } } } + +private extension WebPageMetadataServiceImpl { + private static func record(_ error: Error, code: CrashlyticsError.Code) { + FirebaseCrashlyticsHelper.record( + error, + domain: CrashlyticsError.domain, + code: code.rawValue + ) + } + + private func record(_ error: Error, code: CrashlyticsError.Code) { + Self.record(error, code: code) + } +} diff --git a/Application/DevLogInfra/Sources/Service/WebPageServiceImpl.swift b/Application/DevLogInfra/Sources/Service/WebPageServiceImpl.swift index 728c58b7..d576ddf6 100644 --- a/Application/DevLogInfra/Sources/Service/WebPageServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/WebPageServiceImpl.swift @@ -12,6 +12,17 @@ import DevLogCore import DevLogData final class WebPageServiceImpl: WebPageService { + private enum CrashlyticsError { + static let domain = "DevLogInfra.WebPageServiceImpl" + + enum Code: Int { + case fetchWebPages = 1 + case upsertWebPage + case deleteWebPage + case undoDeleteWebPage + } + } + private enum FunctionName: String { case requestWebPageDeletion case undoWebPageDeletion @@ -51,6 +62,7 @@ final class WebPageServiceImpl: WebPageService { return filtered } catch { logger.error("Failed to fetch web pages", error: error) + record(error, code: .fetchWebPages) throw error } } @@ -72,6 +84,7 @@ final class WebPageServiceImpl: WebPageService { logger.info("Successfully upserted web page") } catch { logger.error("Failed to upsert web page", error: error) + record(error, code: .upsertWebPage) throw error } } @@ -90,6 +103,7 @@ final class WebPageServiceImpl: WebPageService { logger.info("Successfully requested web page deletion") } catch { logger.error("Failed to request web page deletion", error: error) + record(error, code: .deleteWebPage) throw error } } @@ -108,6 +122,7 @@ final class WebPageServiceImpl: WebPageService { logger.info("Successfully undone web page deletion") } catch { logger.error("Failed to undo web page deletion", error: error) + record(error, code: .undoDeleteWebPage) throw error } } @@ -125,6 +140,18 @@ final class WebPageServiceImpl: WebPageService { } private extension WebPageServiceImpl { + private static func record(_ error: Error, code: CrashlyticsError.Code) { + FirebaseCrashlyticsHelper.record( + error, + domain: CrashlyticsError.domain, + code: code.rawValue + ) + } + + private func record(_ error: Error, code: CrashlyticsError.Code) { + Self.record(error, code: code) + } + func makeResponse(from snapshot: QueryDocumentSnapshot) -> WebPageResponse? { let data = snapshot.data() guard From 285b69dd93822f31ec8ef8427def2a92f7efd116 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:29:11 +0900 Subject: [PATCH 05/12] =?UTF-8?q?chore:=20Debug=20dSYM=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Application/DevLogApp/Project.swift | 1 + Tuist/ProjectDescriptionHelpers/Project+Templates.swift | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Application/DevLogApp/Project.swift b/Application/DevLogApp/Project.swift index 2242b51e..7c9e84ab 100644 --- a/Application/DevLogApp/Project.swift +++ b/Application/DevLogApp/Project.swift @@ -47,6 +47,7 @@ let project = Project( base: [ "ASSETCATALOG_COMPILER_APPICON_NAME": "AppIcon", "CODE_SIGN_STYLE": "Automatic", + "DEBUG_INFORMATION_FORMAT": "dwarf-with-dsym", "ENABLE_USER_SCRIPT_SANDBOXING": "NO", "PRODUCT_MODULE_NAME": "DevLogApp", ], diff --git a/Tuist/ProjectDescriptionHelpers/Project+Templates.swift b/Tuist/ProjectDescriptionHelpers/Project+Templates.swift index f6a53760..eb61054b 100644 --- a/Tuist/ProjectDescriptionHelpers/Project+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/Project+Templates.swift @@ -30,6 +30,9 @@ public extension Project { versionXcconfigPath: versionXcconfigPath, base: [ "ENABLE_USER_SCRIPT_SANDBOXING": "NO", + ], + debug: [ + "DEBUG_INFORMATION_FORMAT": "dwarf-with-dsym", ] ) ), From b3a4ddbb8f90c74a7b772893289c84648f40f636 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:29:30 +0900 Subject: [PATCH 06/12] =?UTF-8?q?fix:=20Debug=20=EB=B9=8C=EB=93=9C=20Crash?= =?UTF-8?q?lytics=20=EC=88=98=EC=A7=91=20=EB=B9=84=ED=99=9C=EC=84=B1?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Service/FirebaseAppServiceImpl.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Application/DevLogInfra/Sources/Service/FirebaseAppServiceImpl.swift b/Application/DevLogInfra/Sources/Service/FirebaseAppServiceImpl.swift index bdc7b6af..bbfd6698 100644 --- a/Application/DevLogInfra/Sources/Service/FirebaseAppServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/FirebaseAppServiceImpl.swift @@ -6,6 +6,7 @@ // import DevLogData +import FirebaseCrashlytics import FirebaseCore final class FirebaseAppServiceImpl: FirebaseAppService { @@ -15,6 +16,15 @@ final class FirebaseAppServiceImpl: FirebaseAppService { guard !Self.isConfigured else { return } FirebaseApp.configure() + configureCrashlyticsCollection() Self.isConfigured = true } } + +private extension FirebaseAppServiceImpl { + func configureCrashlyticsCollection() { + #if DEBUG + Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(false) + #endif + } +} From 0131c87ed8711009fe480bccefb07e6303d97a76 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:52:31 +0900 Subject: [PATCH 07/12] =?UTF-8?q?refactor:=20Crashlytics=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=84=B8=EB=B6=84?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Application/DevLogInfra/Sources/Service/AuthServiceImpl.swift | 2 +- .../Sources/Service/ProfileImageDataServiceImpl.swift | 2 +- .../DevLogInfra/Sources/Service/PushMessagingServiceImpl.swift | 2 +- .../Sources/Service/PushNotificationServiceImpl.swift | 2 +- .../Service/SocialLogin/AppleAuthenticationServiceImpl.swift | 2 +- .../Service/SocialLogin/GithubAuthenticationServiceImpl.swift | 2 +- .../Service/SocialLogin/GoogleAuthenticationServiceImpl.swift | 2 +- .../DevLogInfra/Sources/Service/TodoCategoryServiceImpl.swift | 2 +- Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift | 2 +- Application/DevLogInfra/Sources/Service/UserServiceImpl.swift | 2 +- .../Sources/Service/WebPageMetadataServiceImpl.swift | 2 +- .../DevLogInfra/Sources/Service/WebPageServiceImpl.swift | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Application/DevLogInfra/Sources/Service/AuthServiceImpl.swift b/Application/DevLogInfra/Sources/Service/AuthServiceImpl.swift index 28bb53ed..8103e66e 100644 --- a/Application/DevLogInfra/Sources/Service/AuthServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/AuthServiceImpl.swift @@ -145,7 +145,7 @@ private extension AuthServiceImpl { private static func record(_ error: Error, code: CrashlyticsError.Code) { FirebaseCrashlyticsHelper.record( error, - domain: CrashlyticsError.domain, + domain: "\(CrashlyticsError.domain).\(code)", code: code.rawValue ) } diff --git a/Application/DevLogInfra/Sources/Service/ProfileImageDataServiceImpl.swift b/Application/DevLogInfra/Sources/Service/ProfileImageDataServiceImpl.swift index 82888dfa..74a74cf9 100644 --- a/Application/DevLogInfra/Sources/Service/ProfileImageDataServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/ProfileImageDataServiceImpl.swift @@ -32,7 +32,7 @@ final class ProfileImageDataServiceImpl: ProfileImageDataService { } catch { FirebaseCrashlyticsHelper.record( error, - domain: CrashlyticsError.domain, + domain: "\(CrashlyticsError.domain).\(CrashlyticsError.Code.fetchImageData)", code: CrashlyticsError.Code.fetchImageData.rawValue ) throw error diff --git a/Application/DevLogInfra/Sources/Service/PushMessagingServiceImpl.swift b/Application/DevLogInfra/Sources/Service/PushMessagingServiceImpl.swift index cb5a6164..dd99ca00 100644 --- a/Application/DevLogInfra/Sources/Service/PushMessagingServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/PushMessagingServiceImpl.swift @@ -52,7 +52,7 @@ final class PushMessagingServiceImpl: NSObject, PushMessagingService { } FirebaseCrashlyticsHelper.record( error, - domain: CrashlyticsError.domain, + domain: "\(CrashlyticsError.domain).\(CrashlyticsError.Code.fetchFCMToken)", code: CrashlyticsError.Code.fetchFCMToken.rawValue ) throw error diff --git a/Application/DevLogInfra/Sources/Service/PushNotificationServiceImpl.swift b/Application/DevLogInfra/Sources/Service/PushNotificationServiceImpl.swift index 484ec7af..ece2ad88 100644 --- a/Application/DevLogInfra/Sources/Service/PushNotificationServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/PushNotificationServiceImpl.swift @@ -293,7 +293,7 @@ private extension PushNotificationServiceImpl { private static func record(_ error: Error, code: CrashlyticsError.Code) { FirebaseCrashlyticsHelper.record( error, - domain: CrashlyticsError.domain, + domain: "\(CrashlyticsError.domain).\(code)", code: code.rawValue ) } diff --git a/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift b/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift index 28cb3bf2..366eba9b 100644 --- a/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift @@ -333,7 +333,7 @@ private extension AppleAuthenticationServiceImpl { private static func record(_ error: Error, code: CrashlyticsError.Code) { FirebaseCrashlyticsHelper.record( error, - domain: CrashlyticsError.domain, + domain: "\(CrashlyticsError.domain).\(code)", code: code.rawValue ) } diff --git a/Application/DevLogInfra/Sources/Service/SocialLogin/GithubAuthenticationServiceImpl.swift b/Application/DevLogInfra/Sources/Service/SocialLogin/GithubAuthenticationServiceImpl.swift index 25d39802..8617c95a 100644 --- a/Application/DevLogInfra/Sources/Service/SocialLogin/GithubAuthenticationServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/SocialLogin/GithubAuthenticationServiceImpl.swift @@ -324,7 +324,7 @@ private extension GithubAuthenticationServiceImpl { private static func record(_ error: Error, code: CrashlyticsError.Code) { FirebaseCrashlyticsHelper.record( error, - domain: CrashlyticsError.domain, + domain: "\(CrashlyticsError.domain).\(code)", code: code.rawValue ) } diff --git a/Application/DevLogInfra/Sources/Service/SocialLogin/GoogleAuthenticationServiceImpl.swift b/Application/DevLogInfra/Sources/Service/SocialLogin/GoogleAuthenticationServiceImpl.swift index 4c828411..ae36a4b8 100644 --- a/Application/DevLogInfra/Sources/Service/SocialLogin/GoogleAuthenticationServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/SocialLogin/GoogleAuthenticationServiceImpl.swift @@ -164,7 +164,7 @@ private extension GoogleAuthenticationServiceImpl { private static func record(_ error: Error, code: CrashlyticsError.Code) { FirebaseCrashlyticsHelper.record( error, - domain: CrashlyticsError.domain, + domain: "\(CrashlyticsError.domain).\(code)", code: code.rawValue ) } diff --git a/Application/DevLogInfra/Sources/Service/TodoCategoryServiceImpl.swift b/Application/DevLogInfra/Sources/Service/TodoCategoryServiceImpl.swift index de48dd2b..3ee310c0 100644 --- a/Application/DevLogInfra/Sources/Service/TodoCategoryServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/TodoCategoryServiceImpl.swift @@ -99,7 +99,7 @@ private extension TodoCategoryServiceImpl { private static func record(_ error: Error, code: CrashlyticsError.Code) { FirebaseCrashlyticsHelper.record( error, - domain: CrashlyticsError.domain, + domain: "\(CrashlyticsError.domain).\(code)", code: code.rawValue ) } diff --git a/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift b/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift index ecad46a1..12aae818 100644 --- a/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift @@ -334,7 +334,7 @@ private extension TodoServiceImpl { private static func record(_ error: Error, code: CrashlyticsError.Code) { FirebaseCrashlyticsHelper.record( error, - domain: CrashlyticsError.domain, + domain: "\(CrashlyticsError.domain).\(code)", code: code.rawValue ) } diff --git a/Application/DevLogInfra/Sources/Service/UserServiceImpl.swift b/Application/DevLogInfra/Sources/Service/UserServiceImpl.swift index bfc7eebb..8e1c827a 100644 --- a/Application/DevLogInfra/Sources/Service/UserServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/UserServiceImpl.swift @@ -233,7 +233,7 @@ private extension UserServiceImpl { private static func record(_ error: Error, code: CrashlyticsError.Code) { FirebaseCrashlyticsHelper.record( error, - domain: CrashlyticsError.domain, + domain: "\(CrashlyticsError.domain).\(code)", code: code.rawValue ) } diff --git a/Application/DevLogInfra/Sources/Service/WebPageMetadataServiceImpl.swift b/Application/DevLogInfra/Sources/Service/WebPageMetadataServiceImpl.swift index ac17a13d..68c6fe0c 100644 --- a/Application/DevLogInfra/Sources/Service/WebPageMetadataServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/WebPageMetadataServiceImpl.swift @@ -120,7 +120,7 @@ private extension WebPageMetadataServiceImpl { private static func record(_ error: Error, code: CrashlyticsError.Code) { FirebaseCrashlyticsHelper.record( error, - domain: CrashlyticsError.domain, + domain: "\(CrashlyticsError.domain).\(code)", code: code.rawValue ) } diff --git a/Application/DevLogInfra/Sources/Service/WebPageServiceImpl.swift b/Application/DevLogInfra/Sources/Service/WebPageServiceImpl.swift index d576ddf6..955b3291 100644 --- a/Application/DevLogInfra/Sources/Service/WebPageServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/WebPageServiceImpl.swift @@ -143,7 +143,7 @@ private extension WebPageServiceImpl { private static func record(_ error: Error, code: CrashlyticsError.Code) { FirebaseCrashlyticsHelper.record( error, - domain: CrashlyticsError.domain, + domain: "\(CrashlyticsError.domain).\(code)", code: code.rawValue ) } From 3eb25f229a4a0fe1c5e4cd5b7b2ef8d40a198741 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:07:33 +0900 Subject: [PATCH 08/12] =?UTF-8?q?fix:=20Crashlytics=20=EC=9B=90=EB=B3=B8?= =?UTF-8?q?=20=EC=98=A4=EB=A5=98=20=EC=A0=95=EB=B3=B4=20=EB=B3=B4=EC=A1=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DevLogInfra/Sources/Common/FirebaseCrashlyticsHelper.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Application/DevLogInfra/Sources/Common/FirebaseCrashlyticsHelper.swift b/Application/DevLogInfra/Sources/Common/FirebaseCrashlyticsHelper.swift index 2322b733..217b239e 100644 --- a/Application/DevLogInfra/Sources/Common/FirebaseCrashlyticsHelper.swift +++ b/Application/DevLogInfra/Sources/Common/FirebaseCrashlyticsHelper.swift @@ -45,6 +45,7 @@ private extension FirebaseCrashlyticsHelper { metadata: [String: String] ) -> [String: Any] { var userInfo: [String: Any] = [ + NSUnderlyingErrorKey: nsError, Key.underlyingType.rawValue: String(describing: type(of: error)), Key.underlyingDomain.rawValue: nsError.domain, Key.underlyingCode.rawValue: nsError.code From 7be24689687109b05893fcb5cf9e742d49b7f6d8 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:11:57 +0900 Subject: [PATCH 09/12] =?UTF-8?q?fix:=20Crashlytics=20=EC=88=98=EC=A7=91?= =?UTF-8?q?=20=EC=A0=95=EC=B1=85=20=EC=84=A4=EC=A0=95=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Application/DevLogApp/Project.swift | 2 ++ .../Sources/Service/FirebaseAppServiceImpl.swift | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Application/DevLogApp/Project.swift b/Application/DevLogApp/Project.swift index 7c9e84ab..92aba7d5 100644 --- a/Application/DevLogApp/Project.swift +++ b/Application/DevLogApp/Project.swift @@ -53,9 +53,11 @@ let project = Project( ], debug: [ "APS_ENVIRONMENT": "development", + "INFOPLIST_KEY_FirebaseCrashlyticsCollectionEnabled": "NO", ], release: [ "APS_ENVIRONMENT": "production", + "INFOPLIST_KEY_FirebaseCrashlyticsCollectionEnabled": "YES", ] ) ), diff --git a/Application/DevLogInfra/Sources/Service/FirebaseAppServiceImpl.swift b/Application/DevLogInfra/Sources/Service/FirebaseAppServiceImpl.swift index bbfd6698..74af8043 100644 --- a/Application/DevLogInfra/Sources/Service/FirebaseAppServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/FirebaseAppServiceImpl.swift @@ -16,15 +16,15 @@ final class FirebaseAppServiceImpl: FirebaseAppService { guard !Self.isConfigured else { return } FirebaseApp.configure() - configureCrashlyticsCollection() + enableCrashlyticsCollectionIfNeeded() Self.isConfigured = true } } private extension FirebaseAppServiceImpl { - func configureCrashlyticsCollection() { - #if DEBUG - Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(false) + func enableCrashlyticsCollectionIfNeeded() { + #if !DEBUG + Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(true) #endif } } From 7a4254cf7db2615e0f531acde15ced0a9ea146d1 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:23:37 +0900 Subject: [PATCH 10/12] =?UTF-8?q?fix:=20Debug=20dSYM=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Application/DevLogApp/Project.swift | 3 ++- Tuist/ProjectDescriptionHelpers/Project+Templates.swift | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Application/DevLogApp/Project.swift b/Application/DevLogApp/Project.swift index 92aba7d5..48973157 100644 --- a/Application/DevLogApp/Project.swift +++ b/Application/DevLogApp/Project.swift @@ -47,16 +47,17 @@ let project = Project( base: [ "ASSETCATALOG_COMPILER_APPICON_NAME": "AppIcon", "CODE_SIGN_STYLE": "Automatic", - "DEBUG_INFORMATION_FORMAT": "dwarf-with-dsym", "ENABLE_USER_SCRIPT_SANDBOXING": "NO", "PRODUCT_MODULE_NAME": "DevLogApp", ], debug: [ "APS_ENVIRONMENT": "development", + "DEBUG_INFORMATION_FORMAT": "dwarf", "INFOPLIST_KEY_FirebaseCrashlyticsCollectionEnabled": "NO", ], release: [ "APS_ENVIRONMENT": "production", + "DEBUG_INFORMATION_FORMAT": "dwarf-with-dsym", "INFOPLIST_KEY_FirebaseCrashlyticsCollectionEnabled": "YES", ] ) diff --git a/Tuist/ProjectDescriptionHelpers/Project+Templates.swift b/Tuist/ProjectDescriptionHelpers/Project+Templates.swift index eb61054b..e97e9ef0 100644 --- a/Tuist/ProjectDescriptionHelpers/Project+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/Project+Templates.swift @@ -32,6 +32,9 @@ public extension Project { "ENABLE_USER_SCRIPT_SANDBOXING": "NO", ], debug: [ + "DEBUG_INFORMATION_FORMAT": "dwarf", + ], + release: [ "DEBUG_INFORMATION_FORMAT": "dwarf-with-dsym", ] ) From 8aa81f041b98ac877a8f604c9c91bc853ad38eb6 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:45:37 +0900 Subject: [PATCH 11/12] =?UTF-8?q?fix:=20Debug=20=EB=B9=8C=EB=93=9C=20dSYM?= =?UTF-8?q?=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=8A=A4=ED=81=AC=EB=A6=BD?= =?UTF-8?q?=ED=8A=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Application/DevLogApp/Project.swift | 1 - .../Project+Packages.swift | 19 ------------------- 2 files changed, 20 deletions(-) diff --git a/Application/DevLogApp/Project.swift b/Application/DevLogApp/Project.swift index 48973157..244f4c0a 100644 --- a/Application/DevLogApp/Project.swift +++ b/Application/DevLogApp/Project.swift @@ -29,7 +29,6 @@ let project = Project( sourcePath: "Sources", configPath: "Sources/.swiftlint.yml" ), - DevLogScripts.firebaseCrashlyticsDSYMUpload(), ], dependencies: [ .project(target: "DevLogPresentation", path: "../DevLogPresentation"), diff --git a/Tuist/ProjectDescriptionHelpers/Project+Packages.swift b/Tuist/ProjectDescriptionHelpers/Project+Packages.swift index 2bd3474a..dd115836 100644 --- a/Tuist/ProjectDescriptionHelpers/Project+Packages.swift +++ b/Tuist/ProjectDescriptionHelpers/Project+Packages.swift @@ -60,25 +60,6 @@ public enum DevLogPackages { } public enum DevLogScripts { - public static func firebaseCrashlyticsDSYMUpload() -> TargetScript { - TargetScript.post( - script: """ - "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run" - """, - name: "Firebase Crashlytics dSYM Upload", - inputPaths: [ - "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}", - "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}", - "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}.debug.dylib", - "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist", - "$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist", - "$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)", - ], - basedOnDependencyAnalysis: false, - shellPath: "/bin/bash" - ) - } - public static func swiftLint( sourcePath: String, configPath: String = "../../.swiftlint.yml" From abc46ea370ccdbb6107b306c9852cdf008f07918 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:50:56 +0900 Subject: [PATCH 12/12] =?UTF-8?q?chore:=20CI=20Homebrew=20trust=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81c2114a..07c1cde2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,6 +59,8 @@ jobs: cache: true - name: Install SwiftLint + env: + HOMEBREW_REQUIRE_TAP_TRUST: "1" shell: bash run: | set -euo pipefail @@ -264,6 +266,8 @@ jobs: cache: true - name: Install SwiftLint + env: + HOMEBREW_REQUIRE_TAP_TRUST: "1" shell: bash run: | set -euo pipefail