From 184e7555326f301f71585e2c6b5c09c587cc460d Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Thu, 17 Jul 2025 14:16:23 -0800 Subject: [PATCH] test: add in-app debug menu with DSN switcher --- EmpowerPlant.xcodeproj/project.pbxproj | 28 +++ EmpowerPlant/AppDelegate.swift | 41 +---- .../DSNDisplayViewController.swift | 168 ++++++++++++++++++ EmpowerPlant/InAppDebugMenu/DSNStorage.swift | 45 +++++ .../InAppDebugMenu/InAppDebugMenu.swift | 107 +++++++++++ EmpowerPlant/InAppDebugMenu/Toasts.swift | 42 +++++ EmpowerPlant/SceneDelegate.swift | 5 +- EmpowerPlant/SentrySDKWrapper.swift | 46 +++++ 8 files changed, 441 insertions(+), 41 deletions(-) create mode 100644 EmpowerPlant/InAppDebugMenu/DSNDisplayViewController.swift create mode 100644 EmpowerPlant/InAppDebugMenu/DSNStorage.swift create mode 100644 EmpowerPlant/InAppDebugMenu/InAppDebugMenu.swift create mode 100644 EmpowerPlant/InAppDebugMenu/Toasts.swift create mode 100644 EmpowerPlant/SentrySDKWrapper.swift diff --git a/EmpowerPlant.xcodeproj/project.pbxproj b/EmpowerPlant.xcodeproj/project.pbxproj index 9c9d9ba..3da282b 100644 --- a/EmpowerPlant.xcodeproj/project.pbxproj +++ b/EmpowerPlant.xcodeproj/project.pbxproj @@ -10,6 +10,11 @@ 843BD60F2AD08CE900B0098F /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843BD60E2AD08CE900B0098F /* Utils.swift */; }; 843BD6272AD7798C00B0098F /* jwt-deep-field.png in Resources */ = {isa = PBXBuildFile; fileRef = 843BD6262AD7798C00B0098F /* jwt-deep-field.png */; }; 846BEA1C2ABE611A0032F77F /* mobydick.txt in Resources */ = {isa = PBXBuildFile; fileRef = 846BEA1B2ABE611A0032F77F /* mobydick.txt */; }; + 846D3F662E2988EC00D4E7E3 /* InAppDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846D3F652E2988EC00D4E7E3 /* InAppDebugMenu.swift */; }; + 846D3F682E29893100D4E7E3 /* DSNDisplayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846D3F672E29893100D4E7E3 /* DSNDisplayViewController.swift */; }; + 846D3F6A2E29895C00D4E7E3 /* Toasts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846D3F692E29895C00D4E7E3 /* Toasts.swift */; }; + 846D3F6C2E29A43F00D4E7E3 /* DSNStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846D3F6B2E29A43F00D4E7E3 /* DSNStorage.swift */; }; + 846D3F712E29AB8700D4E7E3 /* SentrySDKWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846D3F702E29AB8300D4E7E3 /* SentrySDKWrapper.swift */; }; 848716A32AD8C3CD00756467 /* BigInt in Frameworks */ = {isa = PBXBuildFile; productRef = 848716A22AD8C3CD00756467 /* BigInt */; }; 848716B72ADFAA2000756467 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 848716B62ADFAA2000756467 /* Sentry */; }; 8B21663C29D3F8C80009C890 /* RandomErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B21663B29D3F8C80009C890 /* RandomErrors.swift */; }; @@ -44,6 +49,11 @@ 843BD6262AD7798C00B0098F /* jwt-deep-field.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "jwt-deep-field.png"; sourceTree = ""; }; 846BEA1A2ABE46880032F77F /* upload-symbols.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "upload-symbols.sh"; sourceTree = ""; }; 846BEA1B2ABE611A0032F77F /* mobydick.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = mobydick.txt; sourceTree = ""; }; + 846D3F652E2988EC00D4E7E3 /* InAppDebugMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppDebugMenu.swift; sourceTree = ""; }; + 846D3F672E29893100D4E7E3 /* DSNDisplayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DSNDisplayViewController.swift; sourceTree = ""; }; + 846D3F692E29895C00D4E7E3 /* Toasts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toasts.swift; sourceTree = ""; }; + 846D3F6B2E29A43F00D4E7E3 /* DSNStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DSNStorage.swift; sourceTree = ""; }; + 846D3F702E29AB8300D4E7E3 /* SentrySDKWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySDKWrapper.swift; sourceTree = ""; }; 8474F0482ACCE2D800F21E06 /* deploy_project.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = deploy_project.sh; sourceTree = ""; }; 8474F04D2ACE54F300F21E06 /* .github */ = {isa = PBXFileReference; lastKnownFileType = folder; path = .github; sourceTree = ""; }; 848A45262BBFC79E006AAAEC /* .codecov.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .codecov.yml; sourceTree = ""; }; @@ -90,6 +100,17 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 846D3F6F2E29AB7100D4E7E3 /* InAppDebugMenu */ = { + isa = PBXGroup; + children = ( + 846D3F652E2988EC00D4E7E3 /* InAppDebugMenu.swift */, + 846D3F672E29893100D4E7E3 /* DSNDisplayViewController.swift */, + 846D3F6B2E29A43F00D4E7E3 /* DSNStorage.swift */, + 846D3F692E29895C00D4E7E3 /* Toasts.swift */, + ); + path = InAppDebugMenu; + sourceTree = ""; + }; 8BA3AB2D2A201FE900BE1EA8 /* EmpowerPlantTests */ = { isa = PBXGroup; children = ( @@ -120,6 +141,8 @@ D17C73B127D8291D006650AF /* EmpowerPlant */ = { isa = PBXGroup; children = ( + 846D3F702E29AB8300D4E7E3 /* SentrySDKWrapper.swift */, + 846D3F6F2E29AB7100D4E7E3 /* InAppDebugMenu */, 8B21663B29D3F8C80009C890 /* RandomErrors.swift */, D17C73B227D8291D006650AF /* AppDelegate.swift */, D17C73B427D8291D006650AF /* SceneDelegate.swift */, @@ -285,9 +308,14 @@ files = ( D17C73CC27D82EB8006650AF /* EmpowerPlantViewController.swift in Sources */, D17C73D227D83321006650AF /* ListAppViewController.swift in Sources */, + 846D3F662E2988EC00D4E7E3 /* InAppDebugMenu.swift in Sources */, D15EDF14282BF80400FC13D6 /* Product+CoreDataClass.swift in Sources */, D15FCDA927E00F0D00258BF3 /* Model.xcdatamodeld in Sources */, + 846D3F6C2E29A43F00D4E7E3 /* DSNStorage.swift in Sources */, + 846D3F682E29893100D4E7E3 /* DSNDisplayViewController.swift in Sources */, D17C73CF27D82ED1006650AF /* CartViewController.swift in Sources */, + 846D3F6A2E29895C00D4E7E3 /* Toasts.swift in Sources */, + 846D3F712E29AB8700D4E7E3 /* SentrySDKWrapper.swift in Sources */, 8B21663C29D3F8C80009C890 /* RandomErrors.swift in Sources */, D19EBE6F2805ED52007022DC /* ShoppingCart.swift in Sources */, 843BD60F2AD08CE900B0098F /* Utils.swift in Sources */, diff --git a/EmpowerPlant/AppDelegate.swift b/EmpowerPlant/AppDelegate.swift index 765c9fb..2dd873b 100644 --- a/EmpowerPlant/AppDelegate.swift +++ b/EmpowerPlant/AppDelegate.swift @@ -11,50 +11,13 @@ import CoreData @main class AppDelegate: UIResponder, UIApplicationDelegate { - - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - - // the Sentry default is to enable swizzling. we'll use that as our default as well. we check for the launch arg to disable swizzling; if it's provided, then we'll disable swizzling. if it's absent, then swizzling will be enabled. - let enableSwizzling = !ProcessInfo.processInfo.arguments.contains("--disable-swizzling") - - SentrySDK.start { options in - options.dsn = "https://9b0dbdfd24daad3f475baa5f5adf1302@sandbox-mirror.sentry.gg/1" - - // set the SDK debug mode according to defaults and overrides. - #if DEBUG - // in debug builds, we default to enabling debug mode. the launch arg --no-debug-mode-in-debug-build is a way to override that and turn it off, like if you don't want to see the logs in the xcode console. - options.debug = !ProcessInfo.processInfo.arguments.contains("--no-debug-mode-in-debug-build") - #else - // in release builds, we default to disabling debug mode. the launch arg --debug-mode-in-release-build is a way to override that and turn it on. - options.debug = ProcessInfo.processInfo.arguments.contains("--debug-mode-in-release-build") - #endif + SentrySDKWrapper.start() - options.tracesSampleRate = 1.0 - options.profilesSampleRate = 1.0 - options.enableAppLaunchProfiling = true - options.attachScreenshot = true - options.attachViewHierarchy = true - options.enableSwizzling = enableSwizzling - options.enablePerformanceV2 = true - options.enableAutoPerformanceTracing = true - options.enableTimeToFullDisplayTracing = true - - // Enable Mobile Session Replay - options.sessionReplay.onErrorSampleRate = 1.0 - options.sessionReplay.sessionSampleRate = 1.0 - } - SentrySDK.configureScope{ scope in - scope.setTag(value: ["corporate", "enterprise", "self-serve"].randomElement() ?? "unknown", key: "customer.type") - scope.setTag(value: ProcessInfo.processInfo.environment["USER"] ?? "tda", key: "se") - scope.setTag(value: "\(enableSwizzling)", key: "enableSwizzling") - } - if ProcessInfo.processInfo.arguments.contains("--wipe-db") { wipeDB() } - + return true } diff --git a/EmpowerPlant/InAppDebugMenu/DSNDisplayViewController.swift b/EmpowerPlant/InAppDebugMenu/DSNDisplayViewController.swift new file mode 100644 index 0000000..4d02553 --- /dev/null +++ b/EmpowerPlant/InAppDebugMenu/DSNDisplayViewController.swift @@ -0,0 +1,168 @@ +import Sentry +import UIKit + +let fontSize: CGFloat = 12 + +class DSNDisplayViewController: UIViewController { + let dispatchQueue = DispatchQueue(label: "io.sentry.iOS-Swift.queue.dsn-management", attributes: .concurrent) + let label = UILabel(frame: .zero) + + override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + + if #available(iOS 13.0, *) { + view.backgroundColor = .systemBackground + } else { + view.backgroundColor = .lightGray + } + + label.numberOfLines = 0 + label.lineBreakMode = .byCharWrapping + label.textAlignment = .center + + let changeButton = UIButton(type: .roundedRect) + changeButton.setTitle("Change", for: .normal) + changeButton.addTarget(self, action: #selector(changeDSN), for: .touchUpInside) + + let resetButton = UIButton(type: .roundedRect) + resetButton.setTitle("Reset", for: .normal) + resetButton.addTarget(self, action: #selector(resetDSN), for: .touchUpInside) + + let buttonStack = UIStackView(arrangedSubviews: [ + changeButton, + resetButton + ]) + buttonStack.axis = .vertical + + let stack = UIStackView(arrangedSubviews: [ + label, + buttonStack + ]) + + view.addSubview(stack) + stack.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + stack.topAnchor.constraint(equalTo: view.topAnchor), + stack.bottomAnchor.constraint(lessThanOrEqualTo: view.bottomAnchor), + stack.leadingAnchor.constraint(equalTo: view.leadingAnchor), + stack.trailingAnchor.constraint(equalTo: view.trailingAnchor), + ]) + buttonStack.translatesAutoresizingMaskIntoConstraints = false + buttonStack.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.3).isActive = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + updateDSNLabel() + } + + @objc func dsnChanged(_ newDSN: String) { + let options = Options() + options.dsn = newDSN + + if let dsn = options.dsn { + dispatchQueue.async { + do { + try DSNStorage.shared.saveDSN(dsn: dsn) + DispatchQueue.main.async { + showToast(in: self, type: .success, message: "DSN changed!") + } + } catch { + SentrySDK.capture(error: error) + DispatchQueue.main.async { + showToast(in: self, type: .error, message: error.localizedDescription) + } + } + DispatchQueue.main.async { + self.updateDSNLabel() + } + SentrySDKWrapper.reload() + } + } else { + showToast(in: self, type: .warning, message: "Invalid DSN, reverting to the default.") + self.dispatchQueue.async { + do { + try DSNStorage.shared.deleteDSN() + } catch { + SentrySDK.capture(error: error) + DispatchQueue.main.async { + showToast(in: self, type: .error, message: error.localizedDescription) + } + } + DispatchQueue.main.async { + self.updateDSNLabel() + } + SentrySDKWrapper.reload() + } + } + } + + @objc func changeDSN() { + let alert = UIAlertController(title: "Change DSN", message: nil, preferredStyle: .alert) + var configuredTextField: UITextField? + alert.addTextField { textField in + configuredTextField = textField + } + alert.addAction(.init(title: "Save", style: .default, handler: { _ in + guard let dsn = configuredTextField?.text else { + return + } + self.dsnChanged(dsn) + })) + alert.addAction(.init(title: "Cancel", style: .destructive)) + present(alert, animated: true) + } + + @objc func resetDSN() { + self.dispatchQueue.async { + do { + try DSNStorage.shared.deleteDSN() + SentrySDKWrapper.reload() + DispatchQueue.main.async { + showToast(in: self, type: .success, message: "DSN reset to default!") + } + } catch { + SentrySDK.capture(error: error) + DispatchQueue.main.async { + showToast(in: self, type: .error, message: "Failed to reset DSN: \(error)") + } + } + DispatchQueue.main.async { + self.updateDSNLabel() + } + } + } + + func updateDSNLabel() { + let dsn = DSNStorage.shared.getDSN() + self.label.attributedText = dsnFieldTitleString(dsn: dsn) + } + + func dsnFieldTitleString(dsn: String) -> NSAttributedString { + let defaultAnnotation = "(default)" + let overriddenAnnotation = "(overridden)" + guard dsn != DSNStorage.defaultDSN else { + let title = "DSN \(defaultAnnotation):" + let stringContents = "\(title): \(dsn)" + let attributedString = NSMutableAttributedString(string: stringContents) + attributedString.setAttributes([.font: UIFont.boldSystemFont(ofSize: fontSize)], range: (stringContents as NSString).range(of: title)) + attributedString.setAttributes([.font: UIFont.systemFont(ofSize: fontSize)], range: (stringContents as NSString).range(of: dsn)) + return attributedString + } + + let title = "DSN \(overriddenAnnotation)" + let stringContents = "\(title): \(dsn)" + let attributedString = NSMutableAttributedString(string: stringContents) + + // attributes are stacked as last-one-wins since ranges overlap + attributedString.setAttributes([.font: UIFont.boldSystemFont(ofSize: fontSize)], range: (stringContents as NSString).range(of: title)) + attributedString.setAttributes([.foregroundColor: UIColor.red, .font: UIFont.boldSystemFont(ofSize: fontSize)], range: (stringContents as NSString).range(of: overriddenAnnotation)) + + attributedString.setAttributes([.font: UIFont.systemFont(ofSize: fontSize)], range: (stringContents as NSString).range(of: dsn)) + return attributedString + } +} diff --git a/EmpowerPlant/InAppDebugMenu/DSNStorage.swift b/EmpowerPlant/InAppDebugMenu/DSNStorage.swift new file mode 100644 index 0000000..21b0139 --- /dev/null +++ b/EmpowerPlant/InAppDebugMenu/DSNStorage.swift @@ -0,0 +1,45 @@ +import Foundation +import Sentry + +/** + * Stores the DSN to a file in the cache directory. + */ +public class DSNStorage { + static let defaultDSN = "https://9b0dbdfd24daad3f475baa5f5adf1302@sandbox-mirror.sentry.gg/1" + public static let shared = DSNStorage() + private let dsnFile: URL + + private init() { + // swiftlint:disable force_unwrapping + let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first! + // swiftlint:enable force_unwrapping + dsnFile = cachesDirectory.appendingPathComponent("dsn") + } + + public func saveDSN(dsn: String) throws { + try deleteDSN() + try dsn.write(to: dsnFile, atomically: true, encoding: .utf8) + } + + public func getDSN() -> String { + let fileManager = FileManager.default + + guard fileManager.fileExists(atPath: dsnFile.path) else { + return DSNStorage.defaultDSN + } + + do { + return try String(contentsOfFile: dsnFile.path) + } catch { + // TODO: error + return DSNStorage.defaultDSN + } + } + + public func deleteDSN() throws { + let fileManager = FileManager.default + if fileManager.fileExists(atPath: dsnFile.path) { + try fileManager.removeItem(at: dsnFile) + } + } +} diff --git a/EmpowerPlant/InAppDebugMenu/InAppDebugMenu.swift b/EmpowerPlant/InAppDebugMenu/InAppDebugMenu.swift new file mode 100644 index 0000000..3d31fc4 --- /dev/null +++ b/EmpowerPlant/InAppDebugMenu/InAppDebugMenu.swift @@ -0,0 +1,107 @@ +import UIKit + +public class InAppDebugMenu: NSObject { + public static let shared = InAppDebugMenu() + + static var displayingMenu = false + + let window = { + if #available(iOS 13.0, *) { + if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene { + return Window(windowScene: scene) + } + } + return Window() + }() + + lazy var rootVC = { + let uivc = UIViewController(nibName: nil, bundle: nil) + uivc.view.addSubview(button) + + button.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + button.leadingAnchor.constraint(equalTo: uivc.view.safeAreaLayoutGuide.leadingAnchor, constant: 25), + button.bottomAnchor.constraint(equalTo: uivc.view.safeAreaLayoutGuide.bottomAnchor, constant: -75), + button.widthAnchor.constraint(equalToConstant: 60), + button.heightAnchor.constraint(equalTo: button.widthAnchor, multiplier: 1) + ]) + + return uivc + }() + + lazy var button = { + let button = UIButton(type: .custom) + button.addTarget(self, action: #selector(displayDebugMenu), for: .touchUpInside) + button.setImage(UIImage(systemName: "wrench.and.screwdriver"), for: .normal) + button.contentMode = .scaleAspectFill + button.tintColor = .darkGray + + button.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(pannedButton(recognizer:)))) + + return button + }() + + @objc public func pannedButton(recognizer: UIPanGestureRecognizer) { + let deltas = recognizer.translation(in: rootVC.view); + let transform = CGAffineTransformMakeTranslation(deltas.x, deltas.y); + button.center = CGPointApplyAffineTransform(button.center, transform); + recognizer.setTranslation(.zero, in:rootVC.view); + + } + + @objc public func display() { + window.rootViewController = rootVC + window.isHidden = false + } + + @objc func displayDebugMenu() { + InAppDebugMenu.displayingMenu = true + + let listVC = DSNDisplayViewController(nibName: nil, bundle: nil) + listVC.presentationController?.delegate = self + rootVC.present(listVC, animated: true) + } + + class Window: UIWindow { + + @available(iOS 13.0, *) + override init(windowScene: UIWindowScene) { + super.init(windowScene: windowScene) + commonInit() + } + + init() { + super.init(frame: UIScreen.main.bounds) + commonInit() + } + + func commonInit() { + windowLevel = UIWindow.Level.alert + 1 + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard !InAppDebugMenu.displayingMenu else { + return super.hitTest(point, with: event) + } + + guard let result = super.hitTest(point, with: event) else { + return nil + } + guard result.isKind(of: UIButton.self) else { + return nil + } + return result + } + } +} + +extension InAppDebugMenu: UIAdaptivePresentationControllerDelegate { + public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + rootVC.dismiss(animated: true) + InAppDebugMenu.displayingMenu = false + } +} diff --git a/EmpowerPlant/InAppDebugMenu/Toasts.swift b/EmpowerPlant/InAppDebugMenu/Toasts.swift new file mode 100644 index 0000000..d642596 --- /dev/null +++ b/EmpowerPlant/InAppDebugMenu/Toasts.swift @@ -0,0 +1,42 @@ +import UIKit + +public enum ToastType { + case info + case success + case warning + case error +} + +public func showToast(in vc: UIViewController, type: ToastType, message: String) { + let title: String + var action: UIAlertAction? + switch type { + case .info: + title = "OBTW" + case .success: + title = "Success!" + case .warning: + title = "Warning" + action = .init(title: "OK", style: .default, handler: { _ in + + }) + case .error: + title = "Error" + action = .init(title: "OK", style: .default, handler: { _ in + + }) + } + let alert = UIAlertController(title: title, message: message, preferredStyle: .actionSheet) + if let action = action { + alert.addAction(action) + } + vc.present(alert, animated: true) { + switch type { + case .info, .success: + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + vc.dismiss(animated: true) + } + default: break + } + } +} diff --git a/EmpowerPlant/SceneDelegate.swift b/EmpowerPlant/SceneDelegate.swift index a8cffd1..5e21047 100644 --- a/EmpowerPlant/SceneDelegate.swift +++ b/EmpowerPlant/SceneDelegate.swift @@ -11,12 +11,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let _ = (scene as? UIWindowScene) else { return } + guard let scene = (scene as? UIWindowScene) else { return } + window?.windowScene = scene + InAppDebugMenu.shared.display() } func sceneDidDisconnect(_ scene: UIScene) { diff --git a/EmpowerPlant/SentrySDKWrapper.swift b/EmpowerPlant/SentrySDKWrapper.swift new file mode 100644 index 0000000..53b29f1 --- /dev/null +++ b/EmpowerPlant/SentrySDKWrapper.swift @@ -0,0 +1,46 @@ +import Sentry + +struct SentrySDKWrapper { + // the Sentry default is to enable swizzling. we'll use that as our default as well. we check for the launch arg to disable swizzling; if it's provided, then we'll disable swizzling. if it's absent, then swizzling will be enabled. + static let enableSwizzling = !ProcessInfo.processInfo.arguments.contains("--disable-swizzling") + + static func start() { + SentrySDK.start { options in + options.dsn = DSNStorage.shared.getDSN() + + // set the SDK debug mode according to defaults and overrides. + #if DEBUG + // in debug builds, we default to enabling debug mode. the launch arg --no-debug-mode-in-debug-build is a way to override that and turn it off, like if you don't want to see the logs in the xcode console. + options.debug = !ProcessInfo.processInfo.arguments.contains("--no-debug-mode-in-debug-build") + #else + // in release builds, we default to disabling debug mode. the launch arg --debug-mode-in-release-build is a way to override that and turn it on. + options.debug = ProcessInfo.processInfo.arguments.contains("--debug-mode-in-release-build") + #endif + + options.tracesSampleRate = 1.0 + options.profilesSampleRate = 1.0 + options.enableAppLaunchProfiling = true + options.attachScreenshot = true + options.attachViewHierarchy = true + options.enableSwizzling = enableSwizzling + options.enablePerformanceV2 = true + options.enableAutoPerformanceTracing = true + options.enableTimeToFullDisplayTracing = true + + // Enable Mobile Session Replay + options.sessionReplay.onErrorSampleRate = 1.0 + options.sessionReplay.sessionSampleRate = 1.0 + } + + SentrySDK.configureScope{ scope in + scope.setTag(value: ["corporate", "enterprise", "self-serve"].randomElement() ?? "unknown", key: "customer.type") + scope.setTag(value: ProcessInfo.processInfo.environment["USER"] ?? "tda", key: "se") + scope.setTag(value: "\(enableSwizzling)", key: "enableSwizzling") + } + } + + static func reload() { + SentrySDK.close() + start() + } +}