From f254eaf6331db8cc49751ee38e21bc044795a102 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Tue, 7 Jan 2025 17:20:15 -0900 Subject: [PATCH] ref(iOS-Swift): extract sdk config out of app delegate (#4677) --- .../iOS-Swift.xcodeproj/project.pbxproj | 6 + Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 338 +--------------- .../iOS-Swift/ExtraViewController.swift | 4 +- .../iOS-Swift/SentrySDKWrapper.swift | 370 ++++++++++++++++++ .../Tools/DSNDisplayViewController.swift | 4 +- .../iOS13-Swift/iOS13-Swift-Bridging-Header.h | 1 + 6 files changed, 388 insertions(+), 335 deletions(-) create mode 100644 Samples/iOS-Swift/iOS-Swift/SentrySDKWrapper.swift diff --git a/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj b/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj index 88802079bd7..e21817e2367 100644 --- a/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj +++ b/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj @@ -44,6 +44,8 @@ 84BE546F287503F100ACC735 /* SentrySDKPerformanceBenchmarkTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 84BE546E287503F100ACC735 /* SentrySDKPerformanceBenchmarkTests.m */; }; 84BE547E287645B900ACC735 /* SentryProcessInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 84BE54792876451D00ACC735 /* SentryProcessInfo.m */; }; 84DBC6252CE6D321000C4904 /* UserFeedbackUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DBC61F2CE6D31C000C4904 /* UserFeedbackUITests.swift */; }; + 84EEE6632D28B35700010A9D /* SentrySDKWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84EEE6612D28B35700010A9D /* SentrySDKWrapper.swift */; }; + 84EEE6642D2CABF500010A9D /* SentrySDKWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84EEE6612D28B35700010A9D /* SentrySDKWrapper.swift */; }; 84FB812A284001B800F3A94A /* SentryBenchmarking.mm in Sources */ = {isa = PBXBuildFile; fileRef = 84FB8129284001B800F3A94A /* SentryBenchmarking.mm */; }; 84FB812B284001B800F3A94A /* SentryBenchmarking.mm in Sources */ = {isa = PBXBuildFile; fileRef = 84FB8129284001B800F3A94A /* SentryBenchmarking.mm */; }; 8E8C57AF25EF16E6001CEEFA /* TraceTestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E8C57AE25EF16E6001CEEFA /* TraceTestViewController.swift */; }; @@ -290,6 +292,7 @@ 84BE54782876451D00ACC735 /* SentryProcessInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SentryProcessInfo.h; sourceTree = ""; }; 84BE54792876451D00ACC735 /* SentryProcessInfo.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryProcessInfo.m; sourceTree = ""; }; 84DBC61F2CE6D31C000C4904 /* UserFeedbackUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFeedbackUITests.swift; sourceTree = ""; }; + 84EEE6612D28B35700010A9D /* SentrySDKWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySDKWrapper.swift; sourceTree = ""; }; 84FB8125284001B800F3A94A /* SentryBenchmarking.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SentryBenchmarking.h; sourceTree = ""; }; 84FB8129284001B800F3A94A /* SentryBenchmarking.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SentryBenchmarking.mm; sourceTree = ""; }; 84FB812C2840021B00F3A94A /* iOS-Swift-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "iOS-Swift-Bridging-Header.h"; sourceTree = ""; }; @@ -483,6 +486,7 @@ D8DBDA73274D4DF900007380 /* ViewControllers */, 63F93AA9245AC91600A500DB /* iOS-Swift.entitlements */, 637AFDA9243B02760034958B /* AppDelegate.swift */, + 84EEE6612D28B35700010A9D /* SentrySDKWrapper.swift */, 637AFDAD243B02760034958B /* TransactionsViewController.swift */, 84AB90782A50031B0054C99A /* Profiling */, D80D021229EE93630084393D /* ErrorsViewController.swift */, @@ -1133,6 +1137,7 @@ 84BA71F12C8BC55A0045B828 /* Toasts.swift in Sources */, 629EC8AD2B0B537400858855 /* TriggerAppHang.swift in Sources */, D8AE48C92C57DC2F0092A2A6 /* WebViewController.swift in Sources */, + 84EEE6632D28B35700010A9D /* SentrySDKWrapper.swift in Sources */, D8DBDA78274D5FC400007380 /* SplitViewController.swift in Sources */, 84ACC43C2A73CB5900932A18 /* ProfilingNetworkScanner.swift in Sources */, D80D021A29EE936F0084393D /* ExtraViewController.swift in Sources */, @@ -1182,6 +1187,7 @@ 924857562C89A86300774AC3 /* MainViewController.swift in Sources */, D8F3D058274E57D600B56F8C /* TableViewController.swift in Sources */, 7B5525B62938B644006A2932 /* DiskWriteException.swift in Sources */, + 84EEE6642D2CABF500010A9D /* SentrySDKWrapper.swift in Sources */, D8269A58274C0FC700BD5BD5 /* TransactionsViewController.swift in Sources */, 844DA821282584C300E6B62E /* CoreDataViewController.swift in Sources */, D8444E55275F79570042F4DE /* SpanExtension.swift in Sources */, diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index d43a9392362..a42ad680bab 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -1,17 +1,22 @@ -import Sentry import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { private var randomDistributionTimer: Timer? var window: UIWindow? + + var args: [String] { + let args = ProcessInfo.processInfo.arguments + print("[iOS-Swift] [debug] launch arguments: \(args)") + return args + } func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { if args.contains("--io.sentry.wipe-data") { removeAppData() } if !args.contains("--skip-sentry-init") { - startSentry() + SentrySDKWrapper.shared.startSentry() } if #available(iOS 15.0, *) { @@ -63,332 +68,3 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } } - -// MARK: SDK Configuration -extension AppDelegate { - func startSentry() { - SentrySDK.start(configureOptions: configureSentryOptions(options:)) - } - - func configureSentryOptions(options: Options) { - options.dsn = dsn - options.beforeSend = { $0 } - options.beforeSendSpan = { $0 } - options.beforeCaptureScreenshot = { _ in true } - options.beforeCaptureViewHierarchy = { _ in true } - options.debug = true - - if #available(iOS 16.0, *), enableSessionReplay { - options.sessionReplay = SentryReplayOptions(sessionSampleRate: 0, onErrorSampleRate: 1, maskAllText: true, maskAllImages: true) - options.sessionReplay.quality = .high - } - - if #available(iOS 15.0, *), enableMetricKit { - options.enableMetricKit = true - options.enableMetricKitRawPayload = true - } - - options.tracesSampleRate = tracesSampleRate - options.tracesSampler = tracesSampler - options.profilesSampleRate = profilesSampleRate - options.profilesSampler = profilesSampler - options.enableAppLaunchProfiling = enableAppLaunchProfiling - - options.enableAutoSessionTracking = enableSessionTracking - if let sessionTrackingIntervalMillis = env["--io.sentry.sessionTrackingIntervalMillis"] { - options.sessionTrackingIntervalMillis = UInt((sessionTrackingIntervalMillis as NSString).integerValue) - } - - options.add(inAppInclude: "iOS_External") - - options.enableUserInteractionTracing = enableUITracing - options.enableAppHangTracking = enableANRTracking - options.enableWatchdogTerminationTracking = enableWatchdogTracking - options.enableAutoPerformanceTracing = enablePerformanceTracing - options.enablePreWarmedAppStartTracing = enablePrewarmedAppStartTracing - options.enableFileIOTracing = enableFileIOTracing - options.enableAutoBreadcrumbTracking = enableBreadcrumbs - options.enableUIViewControllerTracing = enableUIVCTracing - options.enableNetworkTracking = enableNetworkTracing - options.enableCoreDataTracing = enableCoreDataTracing - options.enableNetworkBreadcrumbs = enableNetworkBreadcrumbs - options.enableSwizzling = enableSwizzling - options.enableCrashHandler = enableCrashHandling - options.enableTracing = enableTracing - options.enablePersistingTracesWhenCrashing = true - options.attachScreenshot = enableAttachScreenshot - options.attachViewHierarchy = enableAttachViewHierarchy - options.enableTimeToFullDisplayTracing = enableTimeToFullDisplayTracing - options.enablePerformanceV2 = enablePerformanceV2 - options.failedRequestStatusCodes = [ HttpStatusCodeRange(min: 400, max: 599) ] - - options.beforeBreadcrumb = { breadcrumb in - //Raising notifications when a new breadcrumb is created in order to use this information - //to validate whether proper breadcrumb are being created in the right places. - NotificationCenter.default.post(name: .init("io.sentry.newbreadcrumb"), object: breadcrumb) - return breadcrumb - } - - options.initialScope = configureInitialScope(scope:) - options.configureUserFeedback = configureFeedback(config:) - } - - func configureInitialScope(scope: Scope) -> Scope { - if let environmentOverride = self.env["--io.sentry.sdk-environment"] { - scope.setEnvironment(environmentOverride) - } else if isBenchmarking { - scope.setEnvironment("benchmarking") - } else { -#if targetEnvironment(simulator) - scope.setEnvironment("simulator") -#else - scope.setEnvironment("device") -#endif // targetEnvironment(simulator) - } - - scope.setTag(value: "swift", key: "language") - - scope.injectGitInformation() - - let user = User(userId: "1") - user.email = self.env["--io.sentry.user.email"] ?? "tony@example.com" - // first check if the username has been overridden in the scheme for testing purposes; then try to use the system username so each person gets an automatic way to easily filter things on the dashboard; then fall back on a hardcoded value if none of these are present - let username = self.env["--io.sentry.user.username"] ?? (self.env["SIMULATOR_HOST_HOME"] as? NSString)? - .lastPathComponent ?? "cocoadev" - user.username = username - user.name = self.env["--io.sentry.user.name"] ?? "cocoa developer" - scope.setUser(user) - - if let path = Bundle.main.path(forResource: "Tongariro", ofType: "jpg") { - scope.addAttachment(Attachment(path: path, filename: "Tongariro.jpg", contentType: "image/jpeg")) - } - if let data = "hello".data(using: .utf8) { - scope.addAttachment(Attachment(data: data, filename: "log.txt")) - } - return scope - } -} - -// MARK: User feedback configuration -extension AppDelegate { - var layoutOffset: UIOffset { UIOffset(horizontal: 25, vertical: 75) } - - func configureFeedbackWidget(config: SentryUserFeedbackWidgetConfiguration) { - if args.contains("--io.sentry.feedback.auto-inject-widget") { - if Locale.current.languageCode == "ar" { // arabic - config.labelText = "﷽" - } else if Locale.current.languageCode == "ur" { // urdu - config.labelText = "نستعلیق" - } else if Locale.current.languageCode == "he" { // hebrew - config.labelText = "עִבְרִית‎" - } else if Locale.current.languageCode == "hi" { // Hindi - config.labelText = "नागरि" - } else { - config.labelText = "Report Jank" - } - config.widgetAccessibilityLabel = "io.sentry.iOS-Swift.button.report-jank" - config.layoutUIOffset = layoutOffset - } else { - config.autoInject = false - } - if args.contains("--io.sentry.feedback.no-widget-text") { - config.labelText = nil - } - if args.contains("--io.sentry.feedback.no-widget-icon") { - config.showIcon = false - } - } - - func configureFeedbackForm(config: SentryUserFeedbackFormConfiguration) { - config.formTitle = "Jank Report" - config.isEmailRequired = args.contains("--io.sentry.feedback.require-email") - config.isNameRequired = args.contains("--io.sentry.feedback.require-name") - config.submitButtonLabel = "Report that jank" - config.addScreenshotButtonLabel = "Show us the jank" - config.removeScreenshotButtonLabel = "Oof too nsfl" - config.cancelButtonLabel = "What, me worry?" - config.messagePlaceholder = "Describe the nature of the jank. Its essence, if you will." - config.namePlaceholder = "Yo name" - config.emailPlaceholder = "Yo email" - config.messageLabel = "Thy complaint" - config.emailLabel = "Thine email" - config.nameLabel = "Thy name" - } - - func configureFeedbackTheme(config: SentryUserFeedbackThemeConfiguration) { - let fontFamily: String - if Locale.current.languageCode == "ar" { // arabic; ar_EG - fontFamily = "Damascus" - } else if Locale.current.languageCode == "ur" { // urdu; ur_PK - fontFamily = "NotoNastaliq" - } else if Locale.current.languageCode == "he" { // hebrew; he_IL - fontFamily = "Arial Hebrew" - } else if Locale.current.languageCode == "hi" { // Hindi; hi_IN - fontFamily = "DevanagariSangamMN" - } else { - fontFamily = "ChalkboardSE-Regular" - } - config.fontFamily = fontFamily - config.outlineStyle = .init(outlineColor: .purple) - config.foreground = .purple - config.background = .init(red: 0.95, green: 0.9, blue: 0.95, alpha: 1) - config.submitBackground = .orange - config.submitForeground = .purple - config.buttonBackground = .purple - config.buttonForeground = .white - } - - func configureFeedback(config: SentryUserFeedbackConfiguration) { - guard !args.contains("--io.sentry.feedback.all-defaults") else { - config.configureWidget = { widget in - widget.layoutUIOffset = self.layoutOffset - } - return - } - - config.useSentryUser = args.contains("--io.sentry.feedback.use-sentry-user") - config.animations = !args.contains("--io.sentry.feedback.no-animations") - config.useShakeGesture = true - config.showFormForScreenshots = true - config.configureWidget = configureFeedbackWidget(config:) - config.configureForm = configureFeedbackForm(config:) - config.configureTheme = configureFeedbackTheme(config:) - config.onFormOpen = { - let appSupportDirectory = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true).first! - let dir = "\(appSupportDirectory)/io.sentry/feedback" - try! FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true) - assert(FileManager.default.createFile(atPath: "\(dir)/onFormOpen", contents: nil)) - } - config.onFormClose = { - let appSupportDirectory = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true).first! - let dir = "\(appSupportDirectory)/io.sentry/feedback" - try! FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true) - assert(FileManager.default.createFile(atPath: "\(dir)/onFormClose", contents: nil)) - } - config.onSubmitSuccess = { info in - let name = info["name"] ?? "$shakespearean_insult_name" - let alert = UIAlertController(title: "Thanks?", message: "We have enough jank of our own, we really didn't need yours too, \(name).", preferredStyle: .alert) - alert.addAction(.init(title: "Deal with it 🕶️", style: .default)) - self.window?.rootViewController?.present(alert, animated: true) - } - config.onSubmitError = { error in - let alert = UIAlertController(title: "D'oh", message: "You tried to report jank, and encountered more jank. The jank has you now: \(error).", preferredStyle: .alert) - alert.addAction(.init(title: "Derp", style: .default)) - self.window?.rootViewController?.present(alert, animated: true) - } - } -} - -// MARK: Convenience access to SDK configuration via launch arg / environment variable -extension AppDelegate { - static let defaultDSN = "https://6cc9bae94def43cab8444a99e0031c28@o447951.ingest.sentry.io/5428557" - - var args: [String] { - let args = ProcessInfo.processInfo.arguments - print("[iOS-Swift] [debug] launch arguments: \(args)") - return args - } - - var env: [String: String] { - let env = ProcessInfo.processInfo.environment - print("[iOS-Swift] [debug] environment: \(env)") - return env - } - - /// For testing purposes, we want to be able to change the DSN and store it to disk. In a real app, you shouldn't need this behavior. - var dsn: String? { - do { - if let dsn = env["--io.sentry.dsn"] { - try DSNStorage.shared.saveDSN(dsn: dsn) - } - return try DSNStorage.shared.getDSN() ?? AppDelegate.defaultDSN - } catch { - print("[iOS-Swift] Error encountered while reading stored DSN: \(error)") - } - return nil - } - - /// Whether or not profiling benchmarks are being run; this requires disabling certain other features for proper functionality. - var isBenchmarking: Bool { args.contains("--io.sentry.test.benchmarking") } - var isUITest: Bool { env["--io.sentry.sdk-environment"] == "ui-tests" } - - func checkDisabled(with arg: String) -> Bool { - args.contains("--io.sentry.disable-everything") || args.contains(arg) - } - - // MARK: features that care about simulator vs device, ui tests and profiling benchmarks - var enableSpotlight: Bool { -#if targetEnvironment(simulator) - !checkDisabled(with: "--disable-spotlight") -#else - false -#endif // targetEnvironment(simulator) - } - - /// - note: the benchmark test starts and stops a custom transaction using a UIButton, and automatic user interaction tracing stops the transaction that begins with that button press after the idle timeout elapses, stopping the profiler (only one profiler runs regardless of the number of concurrent transactions) - var enableUITracing: Bool { !isBenchmarking && !checkDisabled(with: "--disable-ui-tracing") } - var enablePrewarmedAppStartTracing: Bool { !isBenchmarking && !checkDisabled(with: "--disable-prewarmed-app-start-tracing") } - var enablePerformanceTracing: Bool { !isBenchmarking && !checkDisabled(with: "--disable-auto-performance-tracing") } - var enableTracing: Bool { !isBenchmarking && !checkDisabled(with: "--disable-tracing") } - /// - note: UI tests generate false OOMs - var enableWatchdogTracking: Bool { !isUITest && !isBenchmarking && !checkDisabled(with: "--disable-watchdog-tracking") } - /// - note: disable during benchmarks because we run CPU for 15 seconds at full throttle which can trigger ANRs - var enableANRTracking: Bool { !isBenchmarking && !checkDisabled(with: "--disable-anr-tracking") } - - // MARK: Other features - - var enableTimeToFullDisplayTracing: Bool { !checkDisabled(with: "--disable-time-to-full-display-tracing")} - var enableAttachScreenshot: Bool { !checkDisabled(with: "--disable-attach-screenshot")} - var enableAttachViewHierarchy: Bool { !checkDisabled(with: "--disable-attach-view-hierarchy")} - var enablePerformanceV2: Bool { !checkDisabled(with: "--disable-performance-v2")} - var enableSessionReplay: Bool { !checkDisabled(with: "--disable-session-replay") } - var enableMetricKit: Bool { !checkDisabled(with: "--disable-metrickit-integration") } - var enableSessionTracking: Bool { !checkDisabled(with: "--disable-automatic-session-tracking") } - var enableFileIOTracing: Bool { !checkDisabled(with: "--disable-file-io-tracing") } - var enableBreadcrumbs: Bool { !checkDisabled(with: "--disable-automatic-breadcrumbs") } - var enableUIVCTracing: Bool { !checkDisabled(with: "--disable-uiviewcontroller-tracing") } - var enableNetworkTracing: Bool { !checkDisabled(with: "--disable-network-tracking") } - var enableCoreDataTracing: Bool { !checkDisabled(with: "--disable-core-data-tracing") } - var enableNetworkBreadcrumbs: Bool { !checkDisabled(with: "--disable-network-breadcrumbs") } - var enableSwizzling: Bool { !checkDisabled(with: "--disable-swizzling") } - var enableCrashHandling: Bool { !checkDisabled(with: "--disable-crash-handler") } - - var tracesSampleRate: NSNumber { - guard let tracesSampleRateOverride = env["--io.sentry.tracesSampleRate"] else { - return 1 - } - return NSNumber(value: (tracesSampleRateOverride as NSString).integerValue) - } - - var tracesSampler: ((SamplingContext) -> NSNumber?)? { - guard let tracesSamplerValue = env["--io.sentry.tracesSamplerValue"] else { - return nil - } - - return { _ in - return NSNumber(value: (tracesSamplerValue as NSString).integerValue) - } - } - - var profilesSampleRate: NSNumber? { - if args.contains("--io.sentry.enableContinuousProfiling") { - return nil - } else if let profilesSampleRateOverride = env["--io.sentry.profilesSampleRate"] { - return NSNumber(value: (profilesSampleRateOverride as NSString).integerValue) - } else { - return 1 - } - } - - var profilesSampler: ((SamplingContext) -> NSNumber?)? { - guard let profilesSamplerValue = env["--io.sentry.profilesSamplerValue"] else { - return nil - } - - return { _ in - return NSNumber(value: (profilesSamplerValue as NSString).integerValue) - } - } - - var enableAppLaunchProfiling: Bool { args.contains("--profile-app-launches") } -} diff --git a/Samples/iOS-Swift/iOS-Swift/ExtraViewController.swift b/Samples/iOS-Swift/iOS-Swift/ExtraViewController.swift index d6c7b407f93..6a14cd56447 100644 --- a/Samples/iOS-Swift/iOS-Swift/ExtraViewController.swift +++ b/Samples/iOS-Swift/iOS-Swift/ExtraViewController.swift @@ -163,7 +163,7 @@ class ExtraViewController: UIViewController { @IBAction func startSDK(_ sender: UIButton) { highlightButton(sender) - (UIApplication.shared.delegate as? AppDelegate)?.startSentry() + SentrySDKWrapper.shared.startSentry() } @IBAction func causeFrozenFrames(_ sender: Any) { @@ -232,7 +232,7 @@ class ExtraViewController: UIViewController { return nil } let fm = FileManager.default - guard let dsnHash = try? SentryDsn(string: AppDelegate.defaultDSN).getHash() else { + guard let dsnHash = try? SentryDsn(string: SentrySDKWrapper.defaultDSN).getHash() else { displayError(message: "Couldn't compute DSN hash.") return nil } diff --git a/Samples/iOS-Swift/iOS-Swift/SentrySDKWrapper.swift b/Samples/iOS-Swift/iOS-Swift/SentrySDKWrapper.swift new file mode 100644 index 00000000000..e11b57fb984 --- /dev/null +++ b/Samples/iOS-Swift/iOS-Swift/SentrySDKWrapper.swift @@ -0,0 +1,370 @@ +import Sentry +import UIKit + +struct SentrySDKWrapper { + static let shared = SentrySDKWrapper() + + func startSentry() { + SentrySDK.start(configureOptions: configureSentryOptions(options:)) + } + + func configureSentryOptions(options: Options) { + options.dsn = dsn + options.beforeSend = { $0 } + options.beforeSendSpan = { $0 } + options.beforeCaptureScreenshot = { _ in true } + options.beforeCaptureViewHierarchy = { _ in true } + options.debug = true + + if #available(iOS 16.0, *), enableSessionReplay { + options.sessionReplay = SentryReplayOptions(sessionSampleRate: 0, onErrorSampleRate: 1, maskAllText: true, maskAllImages: true) + options.sessionReplay.quality = .high + } + + if #available(iOS 15.0, *), enableMetricKit { + options.enableMetricKit = true + options.enableMetricKitRawPayload = true + } + + options.tracesSampleRate = tracesSampleRate + options.tracesSampler = tracesSampler + options.profilesSampleRate = profilesSampleRate + options.profilesSampler = profilesSampler + options.enableAppLaunchProfiling = enableAppLaunchProfiling + + options.enableAutoSessionTracking = enableSessionTracking + if let sessionTrackingIntervalMillis = env["--io.sentry.sessionTrackingIntervalMillis"] { + options.sessionTrackingIntervalMillis = UInt((sessionTrackingIntervalMillis as NSString).integerValue) + } + + options.add(inAppInclude: "iOS_External") + + options.enableUserInteractionTracing = enableUITracing + options.enableAppHangTracking = enableANRTracking + options.enableWatchdogTerminationTracking = enableWatchdogTracking + options.enableAutoPerformanceTracing = enablePerformanceTracing + options.enablePreWarmedAppStartTracing = enablePrewarmedAppStartTracing + options.enableFileIOTracing = enableFileIOTracing + options.enableAutoBreadcrumbTracking = enableBreadcrumbs + options.enableUIViewControllerTracing = enableUIVCTracing + options.enableNetworkTracking = enableNetworkTracing + options.enableCoreDataTracing = enableCoreDataTracing + options.enableNetworkBreadcrumbs = enableNetworkBreadcrumbs + options.enableSwizzling = enableSwizzling + options.enableCrashHandler = enableCrashHandling + options.enableTracing = enableTracing + options.enablePersistingTracesWhenCrashing = true + options.attachScreenshot = enableAttachScreenshot + options.attachViewHierarchy = enableAttachViewHierarchy + options.enableTimeToFullDisplayTracing = enableTimeToFullDisplayTracing + options.enablePerformanceV2 = enablePerformanceV2 + options.failedRequestStatusCodes = [ HttpStatusCodeRange(min: 400, max: 599) ] + + options.beforeBreadcrumb = { breadcrumb in + //Raising notifications when a new breadcrumb is created in order to use this information + //to validate whether proper breadcrumb are being created in the right places. + NotificationCenter.default.post(name: .init("io.sentry.newbreadcrumb"), object: breadcrumb) + return breadcrumb + } + + options.initialScope = configureInitialScope(scope:) + options.configureUserFeedback = configureFeedback(config:) + } + + func configureInitialScope(scope: Scope) -> Scope { + if let environmentOverride = self.env["--io.sentry.sdk-environment"] { + scope.setEnvironment(environmentOverride) + } else if isBenchmarking { + scope.setEnvironment("benchmarking") + } else { +#if targetEnvironment(simulator) + scope.setEnvironment("simulator") +#else + scope.setEnvironment("device") +#endif // targetEnvironment(simulator) + } + + scope.setTag(value: "swift", key: "language") + + scope.injectGitInformation() + + let user = User(userId: "1") + user.email = self.env["--io.sentry.user.email"] ?? "tony@example.com" + // first check if the username has been overridden in the scheme for testing purposes; then try to use the system username so each person gets an automatic way to easily filter things on the dashboard; then fall back on a hardcoded value if none of these are present + let username = self.env["--io.sentry.user.username"] ?? (self.env["SIMULATOR_HOST_HOME"] as? NSString)? + .lastPathComponent ?? "cocoadev" + user.username = username + user.name = self.env["--io.sentry.user.name"] ?? "cocoa developer" + scope.setUser(user) + + if let path = Bundle.main.path(forResource: "Tongariro", ofType: "jpg") { + scope.addAttachment(Attachment(path: path, filename: "Tongariro.jpg", contentType: "image/jpeg")) + } + if let data = "hello".data(using: .utf8) { + scope.addAttachment(Attachment(data: data, filename: "log.txt")) + } + return scope + } +} + +// MARK: User feedback configuration +extension SentrySDKWrapper { + var layoutOffset: UIOffset { UIOffset(horizontal: 25, vertical: 75) } + + func configureFeedbackWidget(config: SentryUserFeedbackWidgetConfiguration) { + if args.contains("--io.sentry.feedback.auto-inject-widget") { + if Locale.current.languageCode == "ar" { // arabic + config.labelText = "﷽" + } else if Locale.current.languageCode == "ur" { // urdu + config.labelText = "نستعلیق" + } else if Locale.current.languageCode == "he" { // hebrew + config.labelText = "עִבְרִית‎" + } else if Locale.current.languageCode == "hi" { // Hindi + config.labelText = "नागरि" + } else { + config.labelText = "Report Jank" + } + config.widgetAccessibilityLabel = "io.sentry.iOS-Swift.button.report-jank" + config.layoutUIOffset = layoutOffset + } else { + config.autoInject = false + } + if args.contains("--io.sentry.feedback.no-widget-text") { + config.labelText = nil + } + if args.contains("--io.sentry.feedback.no-widget-icon") { + config.showIcon = false + } + } + + func configureFeedbackForm(config: SentryUserFeedbackFormConfiguration) { + config.formTitle = "Jank Report" + config.isEmailRequired = args.contains("--io.sentry.feedback.require-email") + config.isNameRequired = args.contains("--io.sentry.feedback.require-name") + config.submitButtonLabel = "Report that jank" + config.addScreenshotButtonLabel = "Show us the jank" + config.removeScreenshotButtonLabel = "Oof too nsfl" + config.cancelButtonLabel = "What, me worry?" + config.messagePlaceholder = "Describe the nature of the jank. Its essence, if you will." + config.namePlaceholder = "Yo name" + config.emailPlaceholder = "Yo email" + config.messageLabel = "Thy complaint" + config.emailLabel = "Thine email" + config.nameLabel = "Thy name" + } + + func configureFeedbackTheme(config: SentryUserFeedbackThemeConfiguration) { + let fontFamily: String + if Locale.current.languageCode == "ar" { // arabic; ar_EG + fontFamily = "Damascus" + } else if Locale.current.languageCode == "ur" { // urdu; ur_PK + fontFamily = "NotoNastaliq" + } else if Locale.current.languageCode == "he" { // hebrew; he_IL + fontFamily = "Arial Hebrew" + } else if Locale.current.languageCode == "hi" { // Hindi; hi_IN + fontFamily = "DevanagariSangamMN" + } else { + fontFamily = "ChalkboardSE-Regular" + } + config.fontFamily = fontFamily + config.outlineStyle = .init(outlineColor: .purple) + config.foreground = .purple + config.background = .init(red: 0.95, green: 0.9, blue: 0.95, alpha: 1) + config.submitBackground = .orange + config.submitForeground = .purple + config.buttonBackground = .purple + config.buttonForeground = .white + } + + func configureFeedback(config: SentryUserFeedbackConfiguration) { + guard !args.contains("--io.sentry.feedback.all-defaults") else { + config.configureWidget = { widget in + widget.layoutUIOffset = self.layoutOffset + } + configureHooks(config: config) + return + } + + config.useSentryUser = args.contains("--io.sentry.feedback.use-sentry-user") + config.animations = !args.contains("--io.sentry.feedback.no-animations") + config.useShakeGesture = true + config.showFormForScreenshots = true + config.configureWidget = configureFeedbackWidget(config:) + config.configureForm = configureFeedbackForm(config:) + config.configureTheme = configureFeedbackTheme(config:) + configureHooks(config: config) + } + + func configureHooks(config: SentryUserFeedbackConfiguration) { + config.onFormOpen = { + createHookFile(name: "onFormOpen") + } + config.onFormClose = { + createHookFile(name: "onFormClose") + } + config.onSubmitSuccess = { info in + let name = info["name"] ?? "$shakespearean_insult_name" + let alert = UIAlertController(title: "Thanks?", message: "We have enough jank of our own, we really didn't need yours too, \(name).", preferredStyle: .alert) + alert.addAction(.init(title: "Deal with it 🕶️", style: .default)) + UIApplication.shared.delegate?.window??.rootViewController?.present(alert, animated: true) + createHookFile(name: "onSubmitSuccess") + } + config.onSubmitError = { error in + let alert = UIAlertController(title: "D'oh", message: "You tried to report jank, and encountered more jank. The jank has you now: \(error).", preferredStyle: .alert) + alert.addAction(.init(title: "Derp", style: .default)) + UIApplication.shared.delegate?.window??.rootViewController?.present(alert, animated: true) + createHookFile(name: "onSubmitError") + } + } + + func createHookFile(name: String) { + guard let appSupportDirectory = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true).first else { + print("[iOS-Swift] Couldn't retrieve path to application support directory.") + return + } + let fm = FileManager.default + let dir = "\(appSupportDirectory)/io.sentry/feedback" + do { + try fm.createDirectory(atPath: dir, withIntermediateDirectories: true) + } catch { + print("[iOS-Swift] Couldn't create directory structure for user feedback form hook marker files: \(error).") + return + } + let path = "\(dir)/\(name)" + if !fm.createFile(atPath: path, contents: nil) { + print("[iOS-Swift] Couldn't create user feedback form hook marker file at \(path).") + } else { + print("[iOS-Swift] Created user feedback form hook marker file at \(path).") + } + + func removeHookFile(name: String) { + let path = "\(dir)/\(name)" + do { + try fm.removeItem(atPath: path) + } catch { + print("[iOS-Swift] Couldn't remove user feedback form hook marker file \(path): \(error).") + } + } + switch name { + case "onFormOpen": removeHookFile(name: "onFormClose") + case "onFormClose": removeHookFile(name: "onFormOpen") + case "onSubmitSuccess": removeHookFile(name: "onSubmitError") + case "onSubmitError": removeHookFile(name: "onSubmitSuccess") + default: fatalError("Unexpected marker file name") + } + } +} + +// MARK: Convenience access to SDK configuration via launch arg / environment variable +extension SentrySDKWrapper { + static let defaultDSN = "https://6cc9bae94def43cab8444a99e0031c28@o447951.ingest.sentry.io/5428557" + + var args: [String] { + let args = ProcessInfo.processInfo.arguments + print("[iOS-Swift] [debug] launch arguments: \(args)") + return args + } + + var env: [String: String] { + let env = ProcessInfo.processInfo.environment + print("[iOS-Swift] [debug] environment: \(env)") + return env + } + + /// For testing purposes, we want to be able to change the DSN and store it to disk. In a real app, you shouldn't need this behavior. + var dsn: String? { + do { + if let dsn = env["--io.sentry.dsn"] { + try DSNStorage.shared.saveDSN(dsn: dsn) + } + return try DSNStorage.shared.getDSN() ?? SentrySDKWrapper.defaultDSN + } catch { + print("[iOS-Swift] Error encountered while reading stored DSN: \(error)") + } + return nil + } + + /// Whether or not profiling benchmarks are being run; this requires disabling certain other features for proper functionality. + var isBenchmarking: Bool { args.contains("--io.sentry.test.benchmarking") } + var isUITest: Bool { env["--io.sentry.sdk-environment"] == "ui-tests" } + + func checkDisabled(with arg: String) -> Bool { + args.contains("--io.sentry.disable-everything") || args.contains(arg) + } + + // MARK: features that care about simulator vs device, ui tests and profiling benchmarks + var enableSpotlight: Bool { +#if targetEnvironment(simulator) + !checkDisabled(with: "--disable-spotlight") +#else + false +#endif // targetEnvironment(simulator) + } + + /// - note: the benchmark test starts and stops a custom transaction using a UIButton, and automatic user interaction tracing stops the transaction that begins with that button press after the idle timeout elapses, stopping the profiler (only one profiler runs regardless of the number of concurrent transactions) + var enableUITracing: Bool { !isBenchmarking && !checkDisabled(with: "--disable-ui-tracing") } + var enablePrewarmedAppStartTracing: Bool { !isBenchmarking && !checkDisabled(with: "--disable-prewarmed-app-start-tracing") } + var enablePerformanceTracing: Bool { !isBenchmarking && !checkDisabled(with: "--disable-auto-performance-tracing") } + var enableTracing: Bool { !isBenchmarking && !checkDisabled(with: "--disable-tracing") } + /// - note: UI tests generate false OOMs + var enableWatchdogTracking: Bool { !isUITest && !isBenchmarking && !checkDisabled(with: "--disable-watchdog-tracking") } + /// - note: disable during benchmarks because we run CPU for 15 seconds at full throttle which can trigger ANRs + var enableANRTracking: Bool { !isBenchmarking && !checkDisabled(with: "--disable-anr-tracking") } + + // MARK: Other features + + var enableTimeToFullDisplayTracing: Bool { !checkDisabled(with: "--disable-time-to-full-display-tracing")} + var enableAttachScreenshot: Bool { !checkDisabled(with: "--disable-attach-screenshot")} + var enableAttachViewHierarchy: Bool { !checkDisabled(with: "--disable-attach-view-hierarchy")} + var enablePerformanceV2: Bool { !checkDisabled(with: "--disable-performance-v2")} + var enableSessionReplay: Bool { !checkDisabled(with: "--disable-session-replay") } + var enableMetricKit: Bool { !checkDisabled(with: "--disable-metrickit-integration") } + var enableSessionTracking: Bool { !checkDisabled(with: "--disable-automatic-session-tracking") } + var enableFileIOTracing: Bool { !checkDisabled(with: "--disable-file-io-tracing") } + var enableBreadcrumbs: Bool { !checkDisabled(with: "--disable-automatic-breadcrumbs") } + var enableUIVCTracing: Bool { !checkDisabled(with: "--disable-uiviewcontroller-tracing") } + var enableNetworkTracing: Bool { !checkDisabled(with: "--disable-network-tracking") } + var enableCoreDataTracing: Bool { !checkDisabled(with: "--disable-core-data-tracing") } + var enableNetworkBreadcrumbs: Bool { !checkDisabled(with: "--disable-network-breadcrumbs") } + var enableSwizzling: Bool { !checkDisabled(with: "--disable-swizzling") } + var enableCrashHandling: Bool { !checkDisabled(with: "--disable-crash-handler") } + + var tracesSampleRate: NSNumber { + guard let tracesSampleRateOverride = env["--io.sentry.tracesSampleRate"] else { + return 1 + } + return NSNumber(value: (tracesSampleRateOverride as NSString).integerValue) + } + + var tracesSampler: ((SamplingContext) -> NSNumber?)? { + guard let tracesSamplerValue = env["--io.sentry.tracesSamplerValue"] else { + return nil + } + + return { _ in + return NSNumber(value: (tracesSamplerValue as NSString).integerValue) + } + } + + var profilesSampleRate: NSNumber? { + if args.contains("--io.sentry.enableContinuousProfiling") { + return nil + } else if let profilesSampleRateOverride = env["--io.sentry.profilesSampleRate"] { + return NSNumber(value: (profilesSampleRateOverride as NSString).integerValue) + } else { + return 1 + } + } + + var profilesSampler: ((SamplingContext) -> NSNumber?)? { + guard let profilesSamplerValue = env["--io.sentry.profilesSamplerValue"] else { + return nil + } + + return { _ in + return NSNumber(value: (profilesSamplerValue as NSString).integerValue) + } + } + + var enableAppLaunchProfiling: Bool { args.contains("--profile-app-launches") } +} diff --git a/Samples/iOS-Swift/iOS-Swift/Tools/DSNDisplayViewController.swift b/Samples/iOS-Swift/iOS-Swift/Tools/DSNDisplayViewController.swift index 9d0e7c7e15b..47beac220b5 100644 --- a/Samples/iOS-Swift/iOS-Swift/Tools/DSNDisplayViewController.swift +++ b/Samples/iOS-Swift/iOS-Swift/Tools/DSNDisplayViewController.swift @@ -137,7 +137,7 @@ class DSNDisplayViewController: UIViewController { func updateDSNLabel() { do { - let dsn = try DSNStorage.shared.getDSN() ?? AppDelegate.defaultDSN + let dsn = try DSNStorage.shared.getDSN() ?? SentrySDKWrapper.defaultDSN self.label.attributedText = dsnFieldTitleString(dsn: dsn) } catch { SentrySDK.capture(error: error) @@ -150,7 +150,7 @@ class DSNDisplayViewController: UIViewController { func dsnFieldTitleString(dsn: String) -> NSAttributedString { let defaultAnnotation = "(default)" let overriddenAnnotation = "(overridden)" - guard dsn != AppDelegate.defaultDSN else { + guard dsn != SentrySDKWrapper.defaultDSN else { let title = "DSN \(defaultAnnotation):" let stringContents = "\(title): \(dsn)" let attributedString = NSMutableAttributedString(string: stringContents) diff --git a/Samples/iOS-Swift/iOS13-Swift/iOS13-Swift-Bridging-Header.h b/Samples/iOS-Swift/iOS13-Swift/iOS13-Swift-Bridging-Header.h index dfdc4e5eeef..e40c9af3526 100644 --- a/Samples/iOS-Swift/iOS13-Swift/iOS13-Swift-Bridging-Header.h +++ b/Samples/iOS-Swift/iOS13-Swift/iOS13-Swift-Bridging-Header.h @@ -1,4 +1,5 @@ #import "SentryBenchmarking.h" #import "SentryUIApplication.h" #import +#import #import