Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions EmpowerPlant.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -44,6 +49,11 @@
843BD6262AD7798C00B0098F /* jwt-deep-field.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "jwt-deep-field.png"; sourceTree = "<group>"; };
846BEA1A2ABE46880032F77F /* upload-symbols.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "upload-symbols.sh"; sourceTree = "<group>"; };
846BEA1B2ABE611A0032F77F /* mobydick.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = mobydick.txt; sourceTree = "<group>"; };
846D3F652E2988EC00D4E7E3 /* InAppDebugMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppDebugMenu.swift; sourceTree = "<group>"; };
846D3F672E29893100D4E7E3 /* DSNDisplayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DSNDisplayViewController.swift; sourceTree = "<group>"; };
846D3F692E29895C00D4E7E3 /* Toasts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toasts.swift; sourceTree = "<group>"; };
846D3F6B2E29A43F00D4E7E3 /* DSNStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DSNStorage.swift; sourceTree = "<group>"; };
846D3F702E29AB8300D4E7E3 /* SentrySDKWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySDKWrapper.swift; sourceTree = "<group>"; };
8474F0482ACCE2D800F21E06 /* deploy_project.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = deploy_project.sh; sourceTree = "<group>"; };
8474F04D2ACE54F300F21E06 /* .github */ = {isa = PBXFileReference; lastKnownFileType = folder; path = .github; sourceTree = "<group>"; };
848A45262BBFC79E006AAAEC /* .codecov.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .codecov.yml; sourceTree = "<group>"; };
Expand Down Expand Up @@ -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 = "<group>";
};
8BA3AB2D2A201FE900BE1EA8 /* EmpowerPlantTests */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -120,6 +141,8 @@
D17C73B127D8291D006650AF /* EmpowerPlant */ = {
isa = PBXGroup;
children = (
846D3F702E29AB8300D4E7E3 /* SentrySDKWrapper.swift */,
846D3F6F2E29AB7100D4E7E3 /* InAppDebugMenu */,
8B21663B29D3F8C80009C890 /* RandomErrors.swift */,
D17C73B227D8291D006650AF /* AppDelegate.swift */,
D17C73B427D8291D006650AF /* SceneDelegate.swift */,
Expand Down Expand Up @@ -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 */,
Expand Down
41 changes: 2 additions & 39 deletions EmpowerPlant/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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://[email protected]/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
}

Expand Down
168 changes: 168 additions & 0 deletions EmpowerPlant/InAppDebugMenu/DSNDisplayViewController.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
45 changes: 45 additions & 0 deletions EmpowerPlant/InAppDebugMenu/DSNStorage.swift
Original file line number Diff line number Diff line change
@@ -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://[email protected]/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)
}
}
}
Loading
Loading