diff --git a/.gitignore b/.gitignore index e2c1031..ddfb763 100644 --- a/.gitignore +++ b/.gitignore @@ -73,4 +73,7 @@ Tuist/Dependencies/graph.json ## XCFramework ## XCFramework/Cartfile.resolved -XCFramework/Carthage \ No newline at end of file +XCFramework/Carthage + +## Env ## +XCConfig/Env.xcconfig \ No newline at end of file diff --git a/Package.resolved b/Package.resolved index 9a840f1..f209af8 100644 --- a/Package.resolved +++ b/Package.resolved @@ -9,6 +9,15 @@ "version" : "1.0.0" } }, + { + "identity" : "dd-sdk-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Datadog/dd-sdk-ios.git", + "state" : { + "revision" : "c4cb52b90dbddde8e0ffc7d89f2b6e5680acce7e", + "version" : "2.16.0" + } + }, { "identity" : "lottie-spm", "kind" : "remoteSourceControl", @@ -18,6 +27,24 @@ "version" : "4.5.0" } }, + { + "identity" : "opentelemetry-swift-packages", + "kind" : "remoteSourceControl", + "location" : "https://github.com/DataDog/opentelemetry-swift-packages.git", + "state" : { + "revision" : "adcfc3d7fc6ad411bb576cfa4512c2d3f8b2dbc7", + "version" : "1.6.0" + } + }, + { + "identity" : "plcrashreporter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/microsoft/plcrashreporter.git", + "state" : { + "revision" : "6752f71de206f6a53fa6a758c3660fd9a7fe7527", + "version" : "1.11.2" + } + }, { "identity" : "rive-ios", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 7e244cd..3c7c256 100644 --- a/Package.swift +++ b/Package.swift @@ -18,6 +18,7 @@ let package: Package = .init( .package(url: "https://github.com/pointfreeco/swift-composable-architecture.git", exact: "1.11.2"), .package(url: "https://github.com/pointfreeco/swift-dependencies.git", exact: "1.3.3"), .package(url: "https://github.com/rive-app/rive-ios.git", exact: "5.15.1"), - .package(url: "https://github.com/airbnb/lottie-spm.git", exact: "4.5.0") + .package(url: "https://github.com/airbnb/lottie-spm.git", exact: "4.5.0"), + .package(url: "https://github.com/Datadog/dd-sdk-ios.git", exact: "2.16.0") ] ) diff --git a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/DEP+SPMTarget.swift b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/DEP+SPMTarget.swift index f3e91b5..b691e80 100644 --- a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/DEP+SPMTarget.swift +++ b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/DEP+SPMTarget.swift @@ -17,4 +17,6 @@ public extension DEP.SPMTarget { static let dependencies: TargetDependency = .external(name: "Dependencies") static let riveRuntime: TargetDependency = .external(name: "RiveRuntime") static let lottie: TargetDependency = .external(name: "Lottie") + static let datadogCore: TargetDependency = .external(name: "DatadogCore") + static let datadogRUM: TargetDependency = .external(name: "DatadogRUM") } diff --git a/Projects/Core/APIClient/Interface/APIBaseRequestInterface.swift b/Projects/Core/APIClient/Interface/APIBaseRequestInterface.swift index 469a2f0..5303d35 100644 --- a/Projects/Core/APIClient/Interface/APIBaseRequestInterface.swift +++ b/Projects/Core/APIClient/Interface/APIBaseRequestInterface.swift @@ -19,7 +19,7 @@ public protocol APIBaseRequest { } public enum API { - public static var apiBaseURL: String { + public static var apiBaseHost: String { guard let baseURL = Bundle.main.object(forInfoDictionaryKey: "BASE_URL") as? String else { fatalError("url missing") } diff --git a/Projects/Core/APIClient/Interface/APIClientURLSessionDelegate.swift b/Projects/Core/APIClient/Interface/APIClientURLSessionDelegate.swift new file mode 100644 index 0000000..c88a484 --- /dev/null +++ b/Projects/Core/APIClient/Interface/APIClientURLSessionDelegate.swift @@ -0,0 +1,12 @@ +// +// APIClientURLSessionDelegate.swift +// APIClientInterface +// +// Created by devMinseok on 9/8/24. +// Copyright © 2024 PomoNyang. All rights reserved. +// + +import Foundation + +final public class APIClientURLSessionDelegate: NSObject, URLSessionDataDelegate { +} diff --git a/Projects/Core/APIClient/Sources/APIClient.swift b/Projects/Core/APIClient/Sources/APIClient.swift index 780e977..f816a29 100644 --- a/Projects/Core/APIClient/Sources/APIClient.swift +++ b/Projects/Core/APIClient/Sources/APIClient.swift @@ -15,23 +15,27 @@ import Dependencies extension APIClient: DependencyKey { public static let liveValue: APIClient = .live() - + public static func live() -> Self { - + actor Session { nonisolated let tokenInterceptor: TokenInterceptor - + let session: URLSession = { let config = URLSessionConfiguration.default - return URLSession(configuration: config) + return URLSession( + configuration: config, + delegate: APIClientURLSessionDelegate(), + delegateQueue: nil + ) }() - + let decoder: JSONDecoder = JSONDecoder() - + init(tokenInterceptor: TokenInterceptor) { self.tokenInterceptor = tokenInterceptor } - + func sendRequest( _ request: APIBaseRequest, isWithInterceptor: Bool, @@ -40,21 +44,21 @@ extension APIClient: DependencyKey { guard retryCnt < 3 else { throw throwNetworkErr(.timeOutError, statusCode: -1) } - + var urlRequest = try await request.asURLRequest() if isWithInterceptor { urlRequest = try await tokenInterceptor.adapt(urlRequest) } Logger.shared.log(category: .network, urlRequest.asRequestLog()) - + let (data, response) = try await session.data(for: urlRequest) - + guard let httpResponse = response as? HTTPURLResponse else { let error = NetworkError.noResponseError Logger.shared.log(level: .error, category: .network, "API Error:\n\(dump(error))") throw error } - + switch httpResponse.statusCode { case 200..<300: Logger.shared.log(category: .network, urlRequest.asResponseLog(data, httpResponse)) @@ -76,10 +80,10 @@ extension APIClient: DependencyKey { return error } } - + let tokenInterceptor = TokenInterceptor() let session = Session(tokenInterceptor: tokenInterceptor) - + return .init( apiRequest: { request, isWithInterceptor in return try await session.sendRequest(request, isWithInterceptor: isWithInterceptor) diff --git a/Projects/Core/APIClient/Sources/TokenInterceptor.swift b/Projects/Core/APIClient/Sources/TokenInterceptor.swift index f909e8b..b4dc5f5 100644 --- a/Projects/Core/APIClient/Sources/TokenInterceptor.swift +++ b/Projects/Core/APIClient/Sources/TokenInterceptor.swift @@ -45,7 +45,7 @@ struct TokenInterceptor { throw NetworkError.authorizationError } - var urlRequest = URLRequest(url: URL(string: "https://" + API.apiBaseURL)!) + var urlRequest = URLRequest(url: URL(string: "https://" + API.apiBaseHost)!) urlRequest.httpMethod = HTTPMethod.post.rawValue urlRequest.setValue( ContentType.json.rawValue, diff --git a/Projects/Domain/AuthService/Interface/API/AuthAPIRequest.swift b/Projects/Domain/AuthService/Interface/API/AuthAPIRequest.swift index d026280..6eddcc6 100644 --- a/Projects/Domain/AuthService/Interface/API/AuthAPIRequest.swift +++ b/Projects/Domain/AuthService/Interface/API/AuthAPIRequest.swift @@ -15,7 +15,7 @@ public enum AuthAPIRequest { extension AuthAPIRequest: APIBaseRequest { public var baseURL: String { - return API.apiBaseURL + return API.apiBaseHost } public var path: String { diff --git a/Projects/Domain/CatService/Interface/API/CatAPI.swift b/Projects/Domain/CatService/Interface/API/CatAPI.swift index fa27e07..29579af 100644 --- a/Projects/Domain/CatService/Interface/API/CatAPI.swift +++ b/Projects/Domain/CatService/Interface/API/CatAPI.swift @@ -16,7 +16,7 @@ public enum CatAPI { extension CatAPI: APIBaseRequest { public var baseURL: String { - return API.apiBaseURL + return API.apiBaseHost } public var path: String { diff --git a/Projects/Domain/PomodoroService/Interface/API/CategoryAPI.swift b/Projects/Domain/PomodoroService/Interface/API/CategoryAPI.swift index 27cc1d4..7d7cd79 100644 --- a/Projects/Domain/PomodoroService/Interface/API/CategoryAPI.swift +++ b/Projects/Domain/PomodoroService/Interface/API/CategoryAPI.swift @@ -17,7 +17,7 @@ public enum CategoryAPI { extension CategoryAPI: APIBaseRequest { public var baseURL: String { - return API.apiBaseURL + return API.apiBaseHost } public var path: String { diff --git a/Projects/Domain/PomodoroService/Interface/API/FocusTimeAPI.swift b/Projects/Domain/PomodoroService/Interface/API/FocusTimeAPI.swift index 579ca09..823897d 100644 --- a/Projects/Domain/PomodoroService/Interface/API/FocusTimeAPI.swift +++ b/Projects/Domain/PomodoroService/Interface/API/FocusTimeAPI.swift @@ -16,7 +16,7 @@ public enum FocusTimeAPI { extension FocusTimeAPI: APIBaseRequest { public var baseURL: String { - return API.apiBaseURL + return API.apiBaseHost } public var path: String { diff --git a/Projects/Domain/UserService/Interface/API/UserAPI.swift b/Projects/Domain/UserService/Interface/API/UserAPI.swift index 36b0222..7f620bc 100644 --- a/Projects/Domain/UserService/Interface/API/UserAPI.swift +++ b/Projects/Domain/UserService/Interface/API/UserAPI.swift @@ -16,7 +16,7 @@ public enum UserAPIrequest { extension UserAPIrequest: APIBaseRequest { public var baseURL: String { - return API.apiBaseURL + return API.apiBaseHost } public var path: String { diff --git a/Projects/Feature/CatFeature/Sources/NamingCat/NamingCatView.swift b/Projects/Feature/CatFeature/Sources/NamingCat/NamingCatView.swift index 7d014f2..81c4d29 100644 --- a/Projects/Feature/CatFeature/Sources/NamingCat/NamingCatView.swift +++ b/Projects/Feature/CatFeature/Sources/NamingCat/NamingCatView.swift @@ -12,6 +12,7 @@ import DesignSystem import ComposableArchitecture import RiveRuntime +import DatadogRUM public struct NamingCatView: View { @Bindable var store: StoreOf @@ -66,5 +67,6 @@ public struct NamingCatView: View { .onAppear { store.send(.onAppear) } + .trackRUMView(name: "고양이 이름짓기") } } diff --git a/Projects/Feature/CatFeature/Sources/SelectCat/SelectCatView.swift b/Projects/Feature/CatFeature/Sources/SelectCat/SelectCatView.swift index 67e05bf..c45e17e 100644 --- a/Projects/Feature/CatFeature/Sources/SelectCat/SelectCatView.swift +++ b/Projects/Feature/CatFeature/Sources/SelectCat/SelectCatView.swift @@ -13,6 +13,7 @@ import DesignSystem import ComposableArchitecture import RiveRuntime +import DatadogRUM public struct SelectCatView: View { @Bindable var store: StoreOf @@ -85,7 +86,10 @@ public struct SelectCatView: View { ) { store in NamingCatView(store: store) } - .onAppear { store.send(.onAppear) } + .onAppear { + store.send(.onAppear) + } + .trackRUMView(name: "고양이 선택") } } diff --git a/Projects/Feature/Feature/Sources/AppDelegateCore.swift b/Projects/Feature/Feature/Sources/AppDelegateCore.swift index 5cd5a3d..f16e042 100644 --- a/Projects/Feature/Feature/Sources/AppDelegateCore.swift +++ b/Projects/Feature/Feature/Sources/AppDelegateCore.swift @@ -14,10 +14,13 @@ import UserNotificationClientInterface import KeychainClientInterface import DatabaseClientInterface import AppService +import APIClientInterface import ComposableArchitecture import FirebaseCore import FirebaseMessaging +import DatadogCore +import DatadogRUM @Reducer public struct AppDelegateCore { @@ -48,11 +51,13 @@ public struct AppDelegateCore { switch action { case .didFinishLaunching: UIApplication.shared.applicationIconBadgeNumber = 0 - FirebaseApp.configure() - let userNotificationEventStream = userNotificationClient.delegate() + firebaseInitilize() + datadogInitilize() Logger.shared.log("FCMToken: \(Messaging.messaging().fcmToken ?? "not generated")") + let userNotificationEventStream = userNotificationClient.delegate() + return .run { send in await withThrowingTaskGroup(of: Void.self) { group in group.addTask { @@ -93,4 +98,49 @@ public struct AppDelegateCore { return .none } } + + private func firebaseInitilize() { + FirebaseApp.configure() + } + + private func datadogInitilize() { + // TODO: - 환경 값 가져오는 부분 개선하기 + guard let appID = Bundle.main.object(forInfoDictionaryKey: "DATADOG_APP_ID") as? String, + let clientToken = Bundle.main.object(forInfoDictionaryKey: "DATADOG_TOKEN") as? String + else { return } + +#if DEV + let environment = "dev" +#else + let environment = "prod" +#endif + + Datadog.initialize( + with: Datadog.Configuration( + clientToken: clientToken, + env: environment, + site: .us5 + ), + trackingConsent: .granted + ) + + RUM.enable( + with: RUM.Configuration( + applicationID: appID, + uiKitViewsPredicate: DefaultUIKitRUMViewsPredicate(), + uiKitActionsPredicate: DefaultUIKitRUMActionsPredicate(), + urlSessionTracking: RUM.Configuration.URLSessionTracking( + firstPartyHostsTracing: .trace(hosts: [API.apiBaseHost], sampleRate: 20) + ), + trackBackgroundEvents: true + ) + ) + + URLSessionInstrumentation.enable( + with: URLSessionInstrumentation.Configuration( + delegateClass: APIClientURLSessionDelegate.self, + firstPartyHostsTracing: .trace(hosts: [API.apiBaseHost]) + ) + ) + } } diff --git a/Projects/Feature/HomeFeature/Sources/CategorySelect/CategorySelectView.swift b/Projects/Feature/HomeFeature/Sources/CategorySelect/CategorySelectView.swift index 7c38f9d..28ef813 100644 --- a/Projects/Feature/HomeFeature/Sources/CategorySelect/CategorySelectView.swift +++ b/Projects/Feature/HomeFeature/Sources/CategorySelect/CategorySelectView.swift @@ -11,6 +11,7 @@ import SwiftUI import DesignSystem import ComposableArchitecture +import DatadogRUM public struct CategorySelectView: View { @Bindable var store: StoreOf @@ -59,5 +60,6 @@ public struct CategorySelectView: View { .onAppear { store.send(.onAppear) } + .trackRUMView(name: "카테고리 변경") } } diff --git a/Projects/Feature/HomeFeature/Sources/Home/HomeView.swift b/Projects/Feature/HomeFeature/Sources/Home/HomeView.swift index 182fc8b..95df339 100644 --- a/Projects/Feature/HomeFeature/Sources/Home/HomeView.swift +++ b/Projects/Feature/HomeFeature/Sources/Home/HomeView.swift @@ -14,6 +14,7 @@ import DesignSystem import Utils import ComposableArchitecture +import DatadogRUM public struct HomeView: View { @Bindable var store: StoreOf @@ -171,5 +172,6 @@ public struct HomeView: View { .onAppear { store.send(.onAppear) } + .trackRUMView(name: "홈") } } diff --git a/Projects/Feature/HomeFeature/Sources/TimeSelect/TimeSelectView.swift b/Projects/Feature/HomeFeature/Sources/TimeSelect/TimeSelectView.swift index 1fc8e03..2b1d252 100644 --- a/Projects/Feature/HomeFeature/Sources/TimeSelect/TimeSelectView.swift +++ b/Projects/Feature/HomeFeature/Sources/TimeSelect/TimeSelectView.swift @@ -11,6 +11,7 @@ import SwiftUI import DesignSystem import ComposableArchitecture +import DatadogRUM public struct TimeSelectView: View { @Bindable var store: StoreOf @@ -51,5 +52,6 @@ public struct TimeSelectView: View { .onAppear { store.send(.onAppear) } + .trackRUMView(name: "시간 변경") } } diff --git a/Projects/Feature/MyPageFeature/Sources/MyCat/MyCatView.swift b/Projects/Feature/MyPageFeature/Sources/MyCat/MyCatView.swift index e044a31..3033d7f 100644 --- a/Projects/Feature/MyPageFeature/Sources/MyCat/MyCatView.swift +++ b/Projects/Feature/MyPageFeature/Sources/MyCat/MyCatView.swift @@ -12,6 +12,7 @@ import CatFeature import DesignSystem import ComposableArchitecture +import DatadogRUM public struct MyCatView: View { @Bindable var store: StoreOf @@ -67,5 +68,6 @@ public struct MyCatView: View { .onAppear { store.send(.onAppear) } + .trackRUMView(name: "나의 고양이") } } diff --git a/Projects/Feature/MyPageFeature/Sources/MyPage/MyPageView.swift b/Projects/Feature/MyPageFeature/Sources/MyPage/MyPageView.swift index d4e1ed1..3af23a5 100644 --- a/Projects/Feature/MyPageFeature/Sources/MyPage/MyPageView.swift +++ b/Projects/Feature/MyPageFeature/Sources/MyPage/MyPageView.swift @@ -11,6 +11,7 @@ import SwiftUI import DesignSystem import ComposableArchitecture +import DatadogRUM public struct MyPageView: View { @Bindable var store: StoreOf @@ -101,6 +102,7 @@ public struct MyPageView: View { .onAppear { store.send(.onAppear) } + .trackRUMView(name: "마이페이지") } } diff --git a/Projects/Feature/OnboardingFeature/Sources/Onboarding/OnboardingView.swift b/Projects/Feature/OnboardingFeature/Sources/Onboarding/OnboardingView.swift index c9a879d..a606c7b 100644 --- a/Projects/Feature/OnboardingFeature/Sources/Onboarding/OnboardingView.swift +++ b/Projects/Feature/OnboardingFeature/Sources/Onboarding/OnboardingView.swift @@ -12,6 +12,7 @@ import CatFeature import DesignSystem import ComposableArchitecture +import DatadogRUM public struct OnboardingView: View { @Namespace var backgroundFrame @@ -76,6 +77,7 @@ public struct OnboardingView: View { .onLoad { store.send(.onLoad) } + .trackRUMView(name: "온보딩") } } diff --git a/Projects/Feature/PomodoroFeature/Sources/FocusPomodoro/FocusPomodoroView.swift b/Projects/Feature/PomodoroFeature/Sources/FocusPomodoro/FocusPomodoroView.swift index 4f2a8fc..4b49fe1 100644 --- a/Projects/Feature/PomodoroFeature/Sources/FocusPomodoro/FocusPomodoroView.swift +++ b/Projects/Feature/PomodoroFeature/Sources/FocusPomodoro/FocusPomodoroView.swift @@ -12,6 +12,7 @@ import DesignSystem import ComposableArchitecture import RiveRuntime +import DatadogRUM public struct FocusPomodoroView: View { @Bindable var store: StoreOf @@ -99,5 +100,6 @@ public struct FocusPomodoroView: View { .onAppear { store.send(.onAppear) } + .trackRUMView(name: "집중화면") } } diff --git a/Projects/Feature/PomodoroFeature/Sources/RestPomodoro/RestPomodoroView.swift b/Projects/Feature/PomodoroFeature/Sources/RestPomodoro/RestPomodoroView.swift index df22f26..20a8f8a 100644 --- a/Projects/Feature/PomodoroFeature/Sources/RestPomodoro/RestPomodoroView.swift +++ b/Projects/Feature/PomodoroFeature/Sources/RestPomodoro/RestPomodoroView.swift @@ -11,6 +11,7 @@ import SwiftUI import DesignSystem import ComposableArchitecture +import DatadogRUM public struct RestPomodoroView: View { @Bindable var store: StoreOf @@ -123,5 +124,6 @@ public struct RestPomodoroView: View { .onAppear { store.send(.onAppear) } + .trackRUMView(name: "휴식화면") } } diff --git a/Projects/Feature/PomodoroFeature/Sources/RestWaiting/RestWaitingView.swift b/Projects/Feature/PomodoroFeature/Sources/RestWaiting/RestWaitingView.swift index 9b8c54b..6a1e770 100644 --- a/Projects/Feature/PomodoroFeature/Sources/RestWaiting/RestWaitingView.swift +++ b/Projects/Feature/PomodoroFeature/Sources/RestWaiting/RestWaitingView.swift @@ -12,6 +12,7 @@ import DesignSystem import ComposableArchitecture import Lottie +import DatadogRUM public struct RestWaitingView: View { @Bindable var store: StoreOf @@ -118,5 +119,6 @@ public struct RestWaitingView: View { .task { await store.send(.task).finish() } + .trackRUMView(name: "휴식대기화면") } } diff --git a/Projects/Feature/SplashFeature/Sources/SplashView.swift b/Projects/Feature/SplashFeature/Sources/SplashView.swift index b309a9f..dbe3fc0 100644 --- a/Projects/Feature/SplashFeature/Sources/SplashView.swift +++ b/Projects/Feature/SplashFeature/Sources/SplashView.swift @@ -11,6 +11,7 @@ import SwiftUI import DesignSystem import ComposableArchitecture +import DatadogRUM public struct SplashView: View { @Namespace var backgroundFrameID @@ -39,5 +40,6 @@ public struct SplashView: View { .task { await store.send(.task).finish() } + .trackRUMView(name: "스플래시") } } diff --git a/Projects/Shared/ThirdParty_SPM/Project.swift b/Projects/Shared/ThirdParty_SPM/Project.swift index b70524e..c181322 100644 --- a/Projects/Shared/ThirdParty_SPM/Project.swift +++ b/Projects/Shared/ThirdParty_SPM/Project.swift @@ -13,6 +13,8 @@ let project: Project = .makeProject( DEP.SPMTarget.composableArchitecture, DEP.SPMTarget.dependencies, DEP.SPMTarget.riveRuntime, - DEP.SPMTarget.lottie + DEP.SPMTarget.lottie, + DEP.SPMTarget.datadogCore, + DEP.SPMTarget.datadogRUM ] ) diff --git a/Tuist/ProjectDescriptionHelpers/InfoPlist/InfoPlist+Mohanyang.swift b/Tuist/ProjectDescriptionHelpers/InfoPlist/InfoPlist+Mohanyang.swift index ef436e7..bdc59db 100644 --- a/Tuist/ProjectDescriptionHelpers/InfoPlist/InfoPlist+Mohanyang.swift +++ b/Tuist/ProjectDescriptionHelpers/InfoPlist/InfoPlist+Mohanyang.swift @@ -11,9 +11,11 @@ extension InfoPlist { public enum Mohanyang { public static var app: InfoPlist { return .dictionary([ - // MARK: - Base URL + // MARK: - Environment Value "BASE_URL": "$(BASE_URL)", + "DATADOG_APP_ID": "$(DATADOG_APP_ID)", + "DATADOG_TOKEN": "$(DATADOG_TOKEN)", // MARK: - ThirdParty diff --git a/XCConfig/Project/Mohanyang.xcconfig b/XCConfig/Project/Mohanyang.xcconfig index 88ef16d..8cb4dd9 100644 --- a/XCConfig/Project/Mohanyang.xcconfig +++ b/XCConfig/Project/Mohanyang.xcconfig @@ -1,10 +1,8 @@ #include "../Shared.xcconfig" +#include "../Env.xcconfig" APP_NAME = 모하냥 -BASE_URL_DEV = -BASE_URL_PROD = -BASE_URL = $(BASE_URL_$(CONFIGURATION)) MARKETING_VERSION = 0.1.2 CURRENT_PROJECT_VERSION = 12 DEVELOPMENT_TEAM = 9KL4XS83LC