diff --git a/Samples/iOS-Swift/iOS-Swift-UITests/UserFeedbackUITests.swift b/Samples/iOS-Swift/iOS-Swift-UITests/UserFeedbackUITests.swift index e40f1671fe..c41deb9691 100644 --- a/Samples/iOS-Swift/iOS-Swift-UITests/UserFeedbackUITests.swift +++ b/Samples/iOS-Swift/iOS-Swift-UITests/UserFeedbackUITests.swift @@ -111,18 +111,21 @@ class UserFeedbackUITests: BaseUITest { // MARK: Tests validating happy path / successful submission func testSubmitFullyFilledForm() throws { - launchApp(args: ["--io.sentry.feedback.all-defaults"]) + launchApp(args: ["--io.sentry.feedback.all-defaults", "--io.sentry.disable-everything", "--io.sentry.wipe-data", "--io.sentry.base64-attachment-data"]) widgetButton.tap() nameField.tap() - nameField.typeText("Andrew") + let testName = "Andrew" + nameField.typeText(testName) emailField.tap() - emailField.typeText("andrew.mcknight@sentry.io") + let testContactEmail = "andrew.mcknight@sentry.io" + emailField.typeText(testContactEmail) messageTextView.tap() - messageTextView.typeText("UITest user feedback") + let testMessage = "UITest user feedback" + messageTextView.typeText(testMessage) sendButton.tap() @@ -135,10 +138,20 @@ class UserFeedbackUITests: BaseUITest { XCTAssertEqual(try XCTUnwrap(messageTextView.value as? String), "", "The UITextView shouldn't have any initial text functioning as a placeholder; as UITextView has no placeholder property, the \"placeholder\" is a label on top of it.") - // TODO: go to Extras view - app.buttons["io.sentry.ui-test.button.get-feedback-envelope"].tap() - // TODO: pull contents out of text field - // TODO: validate contents + cancelButton.tap() + + app.buttons["Extra"].tap() + app.buttons["io.sentry.ui-test.button.get-latest-envelope"].tap() + let marshaledDataBase64 = try XCTUnwrap(app.textFields["io.sentry.ui-test.text-field.data-marshaling.latest-envelope"].value as? String) + let data = try XCTUnwrap(Data(base64Encoded: marshaledDataBase64)) + let dict = try XCTUnwrap(JSONSerialization.jsonObject(with: data) as? [String: Any]) + XCTAssertEqual(try XCTUnwrap(dict["event_type"] as? String), "feedback") + XCTAssertEqual(try XCTUnwrap(dict["message"] as? String), testMessage) + XCTAssertEqual(try XCTUnwrap(dict["contact_email"] as? String), testContactEmail) + XCTAssertEqual(try XCTUnwrap(dict["source"] as? String), "widget") + XCTAssertEqual(try XCTUnwrap(dict["name"] as? String), testName) + XCTAssertNotNil(dict["event_id"]) + XCTAssertEqual(try XCTUnwrap(dict["item_header_type"] as? String), "feedback") } func testSubmitWithOnlyRequiredFieldsFilled() { diff --git a/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme b/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme index 6e1f1501d9..cdd03802f8 100644 --- a/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme +++ b/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme @@ -73,6 +73,30 @@ argument = "--io.sentry.disable-everything" isEnabled = "NO"> + + + + + + + + + + + + @@ -240,11 +264,6 @@ value = "" isEnabled = "NO"> - - Bool { - args.contains("--disable-everything") || args.contains(arg) + args.contains("--io.sentry.disable-everything") || args.contains(arg) } // MARK: features that care about simulator vs device, ui tests and profiling benchmarks @@ -312,7 +312,7 @@ extension AppDelegate { /// - 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 } + 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 @@ -322,6 +322,10 @@ extension AppDelegate { // 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") } diff --git a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard index 692fe31a42..9d20ad7e70 100644 --- a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard +++ b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard @@ -899,231 +899,267 @@ - - + + - - + + - - + + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + - + - + + - - - + + @@ -1131,7 +1167,10 @@ + + + diff --git a/Samples/iOS-Swift/iOS-Swift/ExtraViewController.swift b/Samples/iOS-Swift/iOS-Swift/ExtraViewController.swift index dc839476a5..2995547108 100644 --- a/Samples/iOS-Swift/iOS-Swift/ExtraViewController.swift +++ b/Samples/iOS-Swift/iOS-Swift/ExtraViewController.swift @@ -9,7 +9,10 @@ class ExtraViewController: UIViewController { @IBOutlet weak var uiTestNameLabel: UILabel! @IBOutlet weak var anrFullyBlockingButton: UIButton! @IBOutlet weak var anrFillingRunLoopButton: UIButton! - + @IBOutlet weak var envelopeDataMarshalingField: UITextField! + @IBOutlet weak var dataMarshalingStatusLabel: UILabel! + @IBOutlet weak var dataMarshalingErrorLabel: UILabel! + @IBOutlet weak var dsnView: UIView! private let dispatchQueue = DispatchQueue(label: "ExtraViewControllers", attributes: .concurrent) @@ -17,6 +20,7 @@ class ExtraViewController: UIViewController { super.viewDidLoad() if let uiTestName = ProcessInfo.processInfo.environment["--io.sentry.ui-test.test-name"] { uiTestNameLabel.text = uiTestName + uiTestNameLabel.isHidden = false } Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in @@ -186,30 +190,99 @@ class ExtraViewController: UIViewController { return pi } - // copied from ProfilingViewController.withProfile - @IBAction func getAllEnvelopes(_ sender: Any) { - let cachesDirectory = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first! + enum EnvelopeContent { + case image(Data) + case rawText(String) + case json([String: Any]) + } + + func displayError(message: String) { + dataMarshalingStatusLabel.isHidden = false + dataMarshalingStatusLabel.text = "❌" + dataMarshalingErrorLabel.isHidden = false + dataMarshalingErrorLabel.text = message + print("[iOS-Swift] \(message)") + } + + @IBAction func getLatestEnvelope(_ sender: Any) { + guard let latestEnvelopePath = latestEnvelopePath() else { return } + guard let base64String = base64EncodedStructuredUITestData(envelopePath: latestEnvelopePath) else { return } + envelopeDataMarshalingField.text = base64String + envelopeDataMarshalingField.isHidden = false + dataMarshalingStatusLabel.isHidden = false + dataMarshalingStatusLabel.text = "✅" + dataMarshalingErrorLabel.isHidden = true + } + + func latestEnvelopePath() -> String? { + guard let cachesDirectory = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first else { + displayError(message: "No user caches directory found on device.") + return nil + } let fm = FileManager.default - let dir = "\(cachesDirectory)/io.sentry/profiles" - let count = try! fm.contentsOfDirectory(atPath: dir).count - //swiftlint:disable empty_count -// guard first || count > 0 else { -// //swiftlint:enable empty_count -// profilingUITestDataMarshalingTextField.text = "" -// return -// } -// let fileName = "profile\(first ? 0 : count - 1)" -// let fullPath = "\(dir)/\(fileName)" -// -// if fm.fileExists(atPath: fullPath) { -// let url = NSURL.fileURL(withPath: fullPath) -// block(url) -// do { -// try fm.removeItem(atPath: fullPath) -// } catch { -// SentrySDK.capture(error: error) -// } -// return -// } + guard let dsnHash = try? SentryDsn(string: AppDelegate.defaultDSN).getHash() else { + displayError(message: "Couldn't compute DSN hash.") + return nil + } + let dir = "\(cachesDirectory)/io.sentry/\(dsnHash)/envelopes" + guard let contents = try? fm.contentsOfDirectory(atPath: dir) else { + displayError(message: "\(dir) has no contents.") + return nil + } + guard let latest = contents.compactMap({ path -> (String, Date)? in + guard let attr = try? fm.attributesOfItem(atPath: "\(dir)/\(path)"), let date = attr[FileAttributeKey.modificationDate] as? Date else { + return nil + } + return (path, date) + }).sorted(by: { a, b in + return a.1.compare(b.1) == .orderedAscending + }).last else { + displayError(message: "Could not find any envelopes in \(dir).") + return nil + } + return "\(dir)/\(latest.0)" + } + + func base64EncodedStructuredUITestData(envelopePath: String) -> String? { + guard let envelopeFileContents = try? String(contentsOfFile: envelopePath) else { + displayError(message: "\(envelopePath) had no contents.") + return nil + } + let parsedEnvelopeContents = envelopeFileContents.split(separator: "\n").map { line in + if let imageData = Data(base64Encoded: String(line), options: []) { + return EnvelopeContent.image(imageData) + } else if let data = line.data(using: .utf8), let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { + return EnvelopeContent.json(json) + } else { + return EnvelopeContent.rawText(String(line)) + } + } + let contentsForUITest = parsedEnvelopeContents.reduce(into: [String: Any]()) { result, item in + if case let .json(json) = item { + insertValues(from: json, into: &result) + } + } + guard let data = try? JSONSerialization.data(withJSONObject: contentsForUITest) else { + displayError(message: "Couldn't serialize marshaling dictionary.") + return nil + } + + return data.base64EncodedString() + } + + func insertValues(from json: [String: Any], into result: inout [String: Any]) { + if let eventContexts = json["contexts"] as? [String: Any] { + result["event_type"] = json["type"] + if let feedback = eventContexts["feedback"] as? [String: Any] { + result["message"] = feedback["message"] + result["contact_email"] = feedback["contact_email"] + result["source"] = feedback["source"] + result["name"] = feedback["name"] + } + } else if let itemHeaderEventId = json["event_id"] { + result["event_id"] = itemHeaderEventId + } else if let _ = json["length"], let type = json["type"] as? String, type == "feedback" { + result["item_header_type"] = json["type"] + } } } diff --git a/Sources/Sentry/SentryEnvelope.m b/Sources/Sentry/SentryEnvelope.m index a9dfec8954..9b0f6f5281 100644 --- a/Sources/Sentry/SentryEnvelope.m +++ b/Sources/Sentry/SentryEnvelope.m @@ -161,7 +161,17 @@ - (_Nullable instancetype)initWithAttachment:(SentryAttachment *)attachment return nil; } +#if DEBUG || TEST || TESTCI + if ([NSProcessInfo.processInfo.arguments + containsObject:@"--io.sentry.base64-attachment-data"]) { + data = [[attachment.data base64EncodedStringWithOptions:0] + dataUsingEncoding:NSUTF8StringEncoding]; + } else { + data = attachment.data; + } +#else data = attachment.data; +#endif // DEBUG || TEST || TESTCI } else if (nil != attachment.path) { NSError *error = nil; @@ -186,7 +196,17 @@ - (_Nullable instancetype)initWithAttachment:(SentryAttachment *)attachment return nil; } +#if DEBUG || TEST || TESTCI + if ([NSProcessInfo.processInfo.arguments + containsObject:@"--io.sentry.base64-attachment-data"]) { + data = [[[[NSFileManager defaultManager] contentsAtPath:attachment.path] + base64EncodedStringWithOptions:0] dataUsingEncoding:NSUTF8StringEncoding]; + } else { + data = [[NSFileManager defaultManager] contentsAtPath:attachment.path]; + } +#else data = [[NSFileManager defaultManager] contentsAtPath:attachment.path]; +#endif // DEBUG || TEST || TESTCI } if (data == nil) { diff --git a/Sources/Sentry/SentryHttpTransport.m b/Sources/Sentry/SentryHttpTransport.m index ae71cf89dc..72c13ca8ba 100644 --- a/Sources/Sentry/SentryHttpTransport.m +++ b/Sources/Sentry/SentryHttpTransport.m @@ -289,10 +289,14 @@ - (void)sendAllCachedEnvelopes SENTRY_LOG_DEBUG(@"sendAllCachedEnvelopes start."); @synchronized(self) { - if (self.isSending || ![self.requestManager isReady]) { + if (self.isSending) { SENTRY_LOG_DEBUG(@"Already sending."); return; } + if (![self.requestManager isReady]) { + SENTRY_LOG_DEBUG(@"Request manager not ready."); + return; + } self.isSending = YES; } diff --git a/Sources/Sentry/SentryQueueableRequestManager.m b/Sources/Sentry/SentryQueueableRequestManager.m index 985194c2a9..d70a95d7da 100644 --- a/Sources/Sentry/SentryQueueableRequestManager.m +++ b/Sources/Sentry/SentryQueueableRequestManager.m @@ -34,6 +34,11 @@ - (BOOL)isReady isEqualToString:@"ui-tests"]) { return NO; } +#elif DEBUG + if ([NSProcessInfo.processInfo.arguments + containsObject:@"--io.sentry.disable-http-transport"]) { + return NO; + } #endif // TEST || TESTCI // We always have at least one operation in the queue when calling this diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift index 4891586afc..26ca060cba 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift @@ -135,6 +135,7 @@ class SentryUserFeedbackForm: UIViewController { } let feedback = SentryFeedback(message: messageTextView.text, name: fullNameTextField.text, email: emailTextField.text, screenshot: screenshotImageView.image?.pngData()) + SentryLog.log(message: "Sending user feedback", andLevel: .debug) SentrySDK.capture(feedback: feedback) delegate?.finished() }