From 97ab95466ededebe2ce5e4725b0d20e83ea03b1c Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Thu, 12 Dec 2024 11:40:41 -0900 Subject: [PATCH 01/27] prefill user info --- .../xcshareddata/xcschemes/iOS-Swift.xcscheme | 4 ++++ Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 1 + .../UserFeedback/SentryUserFeedbackForm.swift | 8 ++++++++ 3 files changed, 13 insertions(+) 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 75721f0cd2a..ee025e48e4e 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,10 @@ argument = "--disable-file-io-tracing" isEnabled = "NO"> + + diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index 523bdd81d85..bac656e65ff 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -174,6 +174,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return } config.animations = !args.contains("--io.sentry.feedback.no-animations") + config.useSentryUser = args.contains("--io.sentry.feedback.use-sentry-user") config.useShakeGesture = true config.showFormForScreenshots = true config.configureWidget = { widget in diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift index b49555afcef..cde3ee3de82 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift @@ -261,6 +261,10 @@ class SentryUserFeedbackForm: UIViewController { field.accessibilityLabel = config.formConfig.nameTextFieldAccessibilityLabel field.accessibilityIdentifier = "io.sentry.feedback.form.name" field.delegate = self + field.autocapitalizationType = .words + if config.useSentryUser { + field.text = SentrySDK.currentHub().scope.userObject?.name + } return field }() @@ -277,6 +281,10 @@ class SentryUserFeedbackForm: UIViewController { field.accessibilityIdentifier = "io.sentry.feedback.form.email" field.delegate = self field.keyboardType = .emailAddress + field.autocapitalizationType = .none + if config.useSentryUser { + field.text = SentrySDK.currentHub().scope.userObject?.email + } return field }() From 8a9fe4909bca74db249d6f39d9a578822acec227 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Thu, 12 Dec 2024 12:27:17 -0900 Subject: [PATCH 02/27] fix build and add ui test --- .../iOS-Swift-UITests/UserFeedbackUITests.swift | 13 +++++++++++++ .../xcshareddata/xcschemes/iOS-Swift.xcscheme | 9 +++++++++ Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 1 + Sources/Sentry/include/SentryPrivate.h | 2 ++ 4 files changed, 25 insertions(+) diff --git a/Samples/iOS-Swift/iOS-Swift-UITests/UserFeedbackUITests.swift b/Samples/iOS-Swift/iOS-Swift-UITests/UserFeedbackUITests.swift index 4ca524f2ee9..bd8cf37ae3a 100644 --- a/Samples/iOS-Swift/iOS-Swift-UITests/UserFeedbackUITests.swift +++ b/Samples/iOS-Swift/iOS-Swift-UITests/UserFeedbackUITests.swift @@ -54,7 +54,9 @@ class UserFeedbackUITests: BaseUITest { // Input field placeholders XCTAssertEqual(try XCTUnwrap(nameField.placeholderValue), "Your Name") + XCTAssertNil(nameField.value) XCTAssertEqual(try XCTUnwrap(emailField.placeholderValue), "your.email@example.org") + XCTAssertNil(emailField.value) XCTAssert(app.staticTexts["What's the bug? What did you expect?"].exists) // Input field labels @@ -97,6 +99,17 @@ class UserFeedbackUITests: BaseUITest { XCTAssertFalse(app.staticTexts["Thy name (Required)"].exists) } + func testPrefilledUserInformation() throws { + launchApp(args: ["--io.sentry.feedback.use-sentry-user"], env: [ + "--io.sentry.user.name": "ui test user", + "--io.sentry.user.email": "ui-testing@sentry.io" + ]) + + widgetButton.tap() + XCTAssertEqual(try XCTUnwrap(nameField.value as? String), "ui test user") + XCTAssertEqual(try XCTUnwrap(emailField.value as? String), "ui-testing@sentry.io") + } + // MARK: Tests validating happy path / successful submission func testSubmitFullyFilledForm() throws { 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 ee025e48e4e..a091d4c0abb 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 @@ -69,6 +69,10 @@ + + @@ -240,6 +244,11 @@ value = "" isEnabled = "NO"> + + Date: Thu, 12 Dec 2024 15:52:56 -0900 Subject: [PATCH 03/27] dont assert nil value bc it uses the placeholder string when empty --- Samples/iOS-Swift/iOS-Swift-UITests/UserFeedbackUITests.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Samples/iOS-Swift/iOS-Swift-UITests/UserFeedbackUITests.swift b/Samples/iOS-Swift/iOS-Swift-UITests/UserFeedbackUITests.swift index bd8cf37ae3a..a5f1eb50b69 100644 --- a/Samples/iOS-Swift/iOS-Swift-UITests/UserFeedbackUITests.swift +++ b/Samples/iOS-Swift/iOS-Swift-UITests/UserFeedbackUITests.swift @@ -54,9 +54,7 @@ class UserFeedbackUITests: BaseUITest { // Input field placeholders XCTAssertEqual(try XCTUnwrap(nameField.placeholderValue), "Your Name") - XCTAssertNil(nameField.value) XCTAssertEqual(try XCTUnwrap(emailField.placeholderValue), "your.email@example.org") - XCTAssertNil(emailField.value) XCTAssert(app.staticTexts["What's the bug? What did you expect?"].exists) // Input field labels From a2764d80ee5718b243ff35450b68f211e799cb93 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Thu, 12 Dec 2024 17:05:20 -0900 Subject: [PATCH 04/27] default user.name in addition to user.username --- Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index c5d4c1a2cc6..3db52b6b001 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -152,9 +152,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { user.email = 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 = env["--io.sentry.user.username"] ?? (env["SIMULATOR_HOST_HOME"] as? NSString)? - .lastPathComponent ?? "cocoa developer" + .lastPathComponent ?? "cocoadev" user.username = username - user.name = env["--io.sentry.user.name"] + user.name = env["--io.sentry.user.name"] ?? "cocoa developer" scope.setUser(user) if let path = Bundle.main.path(forResource: "Tongariro", ofType: "jpg") { From 237f9b4278f5e137751a91dba71c3245af66c592 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Thu, 12 Dec 2024 17:31:53 -0900 Subject: [PATCH 05/27] these should not have been in the public SentrySDK header --- Sources/Sentry/Public/SentrySDK.h | 31 +++++++--------------- Sources/Sentry/include/SentrySDK+Private.h | 22 +++++++++++++++ 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/Sources/Sentry/Public/SentrySDK.h b/Sources/Sentry/Public/SentrySDK.h index d11431f7074..b93a8243349 100644 --- a/Sources/Sentry/Public/SentrySDK.h +++ b/Sources/Sentry/Public/SentrySDK.h @@ -6,11 +6,18 @@ @protocol SentrySpan; -@class SentryOptions, SentryEvent, SentryBreadcrumb, SentryScope, SentryUser, SentryId, - SentryUserFeedback, SentryTransactionContext; +@class SentryBreadcrumb; +@class SentryEvent; +@class SentryFeedback; +@class SentryId; @class SentryMetricsAPI; -@class UIView; +@class SentryOptions; @class SentryReplayApi; +@class SentryScope; +@class SentryTransactionContext; +@class SentryUser; +@class SentryUserFeedback; +@class UIView; NS_ASSUME_NONNULL_BEGIN @@ -246,28 +253,10 @@ SENTRY_NO_INIT /** * Captures user feedback that was manually gathered and sends it to Sentry. * @param userFeedback The user feedback to send to Sentry. - * @note If you'd prefer not to have to build the UI required to gather the feedback from the user, - * consider using `showUserFeedbackForm`, which delivers a prepackaged user feedback experience. See - * @c SentryOptions.configureUserFeedback to customize a fully managed integration. See - * https://docs.sentry.io/platforms/apple/user-feedback/#user-feedback-api and (TODO: add link to - * new docs) for more information on each approach. */ + (void)captureUserFeedback:(SentryUserFeedback *)userFeedback NS_SWIFT_NAME(capture(userFeedback:)); -/** - * Display a form to gather information from an end user in the app to send to Sentry as a user - * feedback event. - * @see @c SentryOptions.enableUserFeedbackIntegration and @c SentryOptions.configureUserFeedback to - * enable the functionality and customize the experience. - * @note If @c SentryOptions.enableUserFeedbackIntegration is @c NO, this method is a no-op. - * @note This is a fully managed user feedback flow; there will be no need to call - * @c SentrySDK.captureUserFeedback . See - * https://docs.sentry.io/platforms/apple/user-feedback/#user-feedback-api and (TODO: add link to - * new docs) for more information on each approach. - */ -+ (void)showUserFeedbackForm; - /** * Adds a Breadcrumb to the current Scope of the current Hub. If the total number of breadcrumbs * exceeds the @c SentryOptions.maxBreadcrumbs the SDK removes the oldest breadcrumb. diff --git a/Sources/Sentry/include/SentrySDK+Private.h b/Sources/Sentry/include/SentrySDK+Private.h index c4e486e89c9..374b8d0c107 100644 --- a/Sources/Sentry/include/SentrySDK+Private.h +++ b/Sources/Sentry/include/SentrySDK+Private.h @@ -50,6 +50,28 @@ NS_ASSUME_NONNULL_BEGIN * Needed by hybrid SDKs as react-native to synchronously capture an envelope. */ + (void)captureEnvelope:(SentryEnvelope *)envelope; +/** + * Captures user feedback that was manually gathered and sends it to Sentry. + * @param feedback The feedback to send to Sentry. + * @note If you'd prefer not to have to build the UI required to gather the feedback from the user, + * consider using `showUserFeedbackForm`, which delivers a prepackaged user feedback experience. See + * @c SentryOptions.configureUserFeedback to customize a fully managed integration. See + * https://docs.sentry.io/platforms/apple/user-feedback/ for more information. + */ ++ (void)captureFeedback:(SentryFeedback *)feedback NS_SWIFT_NAME(capture(feedback:)); + +#if TARGET_OS_IOS && SENTRY_HAS_UIKIT +/** + * Display a form to gather information from an end user in the app to send to Sentry as a user + * feedback event. + * @see @c SentryOptions.configureUserFeedback to customize the experience, currently only on iOS. + * @warning This is an experimental feature and may still have bugs. + * @note This is a fully managed user feedback flow; there will be no need to call + * @c SentrySDK.captureUserFeedback . See + * https://docs.sentry.io/platforms/apple/user-feedback/ for more information. + */ ++ (void)showUserFeedbackForm; +#endif // TARGET_OS_IOS && SENTRY_HAS_UIKIT @end From 71f473a4a461e17ad4f402c793af2f02a59d8629 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Thu, 14 Nov 2024 15:36:01 -0900 Subject: [PATCH 06/27] capture envelopes --- .../Sentry/SentryUserFeedbackIntegration.m | 22 +++++++++++++- .../UserFeedback/SentryUserFeedbackForm.swift | 4 +-- .../SentryUserFeedbackIntegrationDriver.swift | 29 ++++++++++--------- .../SentryUserFeedbackWidget.swift | 18 ++++++++---- 4 files changed, 51 insertions(+), 22 deletions(-) diff --git a/Sources/Sentry/SentryUserFeedbackIntegration.m b/Sources/Sentry/SentryUserFeedbackIntegration.m index 9bf8b13dcfb..071723ca292 100644 --- a/Sources/Sentry/SentryUserFeedbackIntegration.m +++ b/Sources/Sentry/SentryUserFeedbackIntegration.m @@ -1,9 +1,16 @@ #import "SentryUserFeedbackIntegration.h" #import "SentryOptions+Private.h" #import "SentrySwift.h" +#import "SentryEnvelope.h" +#import "SentrySDK+Private.h" +#import "SentryUserFeedback.h" #if TARGET_OS_IOS && SENTRY_HAS_UIKIT +@interface SentryUserFeedbackIntegration() + +@end + @implementation SentryUserFeedbackIntegration { SentryUserFeedbackIntegrationDriver *_driver; } @@ -15,10 +22,23 @@ - (BOOL)installWithOptions:(SentryOptions *)options } _driver = [[SentryUserFeedbackIntegrationDriver alloc] - initWithConfiguration:options.userFeedbackConfiguration]; + initWithConfiguration:options.userFeedbackConfiguration delegate:self]; return YES; } +#pragma mark - SentryUserFeedbackIntegrationDriverDelegate + +- (void)captureFeedbackWithMessage:(NSString * _Nonnull)message name:(NSString * _Nullable)name email:(NSString * _Nullable)email hints:(NSDictionary * _Nullable)hints { + NSError *error = [[NSError alloc] initWithDomain:@"user-feedback" code:1 userInfo:nil]; + SentryId *eventId = [SentrySDK captureError:error]; +// SentryId *eventId = [[SentryId alloc] init]; + SentryUserFeedback *uf = [[SentryUserFeedback alloc] initWithEventId:eventId]; + uf.name = name; + uf.comments = message; + uf.email = email; + [SentrySDK captureUserFeedback:uf]; +} + @end #endif // TARGET_OS_IOS && SENTRY_HAS_UIKIT diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift index cde3ee3de82..284b0528d91 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift @@ -9,7 +9,7 @@ import UIKit @available(iOS 13.0, *) protocol SentryUserFeedbackFormDelegate: NSObjectProtocol { func cancelled() - func confirmed() + func captureFeedback(message: String, name: String?, email: String?, hints: [String: Any]?) } @available(iOS 13.0, *) @@ -133,7 +133,7 @@ class SentryUserFeedbackForm: UIViewController { return } - delegate?.confirmed() + delegate?.captureFeedback(message: messageTextView.text, name: fullNameTextField.text, email: emailTextField.text, hints: nil) } func cancelButtonTapped() { diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift index f9b3d7167c8..efa3ce96ba0 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift @@ -3,6 +3,10 @@ import Foundation @_implementationOnly import _SentryPrivate import UIKit +@objc public protocol SentryUserFeedbackIntegrationDriverDelegate: NSObjectProtocol { + func captureFeedback(message: String, name: String?, email: String?, hints: [String: Any]?) +} + /** * An integration managing a workflow for end users to report feedback via Sentry. * - note: The default method to show the feedback form is via a floating widget placed in the bottom trailing corner of the screen. See the configuration classes for alternative options. @@ -12,9 +16,11 @@ import UIKit class SentryUserFeedbackIntegrationDriver: NSObject { let configuration: SentryUserFeedbackConfiguration private var window: SentryUserFeedbackWidget.Window? + weak var delegate: (any SentryUserFeedbackIntegrationDriverDelegate)? - public init(configuration: SentryUserFeedbackConfiguration) { + public init(configuration: SentryUserFeedbackConfiguration, delegate: any SentryUserFeedbackIntegrationDriverDelegate) { self.configuration = configuration + self.delegate = delegate super.init() if let widgetConfigBuilder = configuration.configureWidget { @@ -49,7 +55,7 @@ class SentryUserFeedbackIntegrationDriver: NSObject { * If `SentryUserFeedbackConfiguration.autoInject` is `false`, this must be called explicitly. */ func createWidget() { - window = SentryUserFeedbackWidget.Window(config: configuration) + window = SentryUserFeedbackWidget.Window(config: configuration, delegate: self) window?.isHidden = false } @@ -60,18 +66,6 @@ class SentryUserFeedbackIntegrationDriver: NSObject { } - /** - * Captures feedback using custom UI. This method allows you to submit feedback data directly. - * - Parameters: - * - message: The feedback message (required). - * - name: The name of the user (optional). - * - email: The email of the user (optional). - * - hints: Additional hints or metadata for the feedback submission (optional). - */ - func captureFeedback(message: String, name: String? = nil, email: String? = nil, hints: [String: Any]? = nil) { - // Implementation to capture feedback - } - private func validate(_ config: SentryUserFeedbackWidgetConfiguration) { let noOpposingHorizontals = config.location.contains(.trailing) && !config.location.contains(.leading) || !config.location.contains(.trailing) && config.location.contains(.leading) @@ -92,4 +86,11 @@ class SentryUserFeedbackIntegrationDriver: NSObject { } } +@available(iOS 13.0, *) +extension SentryUserFeedbackIntegrationDriver: SentryUserFeedbackWidget.Delegate { + func captureFeedback(message: String, name: String?, email: String?, hints: [String : Any]?) { + self.delegate?.captureFeedback(message: message, name: name, email: email, hints: hints) + } +} + #endif // os(iOS) && !SENTRY_NO_UIKIT diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift index 0233bd50aa1..03c40412365 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift @@ -9,6 +9,11 @@ var displayingForm = false @available(iOS 13.0, *) struct SentryUserFeedbackWidget { + + protocol Delegate: NSObjectProtocol { + func captureFeedback(message: String, name: String?, email: String?, hints: [String: Any]?) + } + class Window: UIWindow { class RootViewController: UIViewController, SentryUserFeedbackFormDelegate, UIAdaptivePresentationControllerDelegate { let defaultWidgetSpacing: CGFloat = 8 @@ -22,8 +27,11 @@ struct SentryUserFeedbackWidget { let config: SentryUserFeedbackConfiguration - init(config: SentryUserFeedbackConfiguration) { + weak var delegate: (any Delegate)? + + init(config: SentryUserFeedbackConfiguration, delegate: any Delegate) { self.config = config + self.delegate = delegate super.init(nibName: nil, bundle: nil) view.addSubview(button) @@ -73,9 +81,9 @@ struct SentryUserFeedbackWidget { } //swiftlint:disable todo - func confirmed() { - // TODO: submit + func captureFeedback(message: String, name: String?, email: String?, hints: [String : Any]?) { closeForm() + self.delegate?.captureFeedback(message: message, name: name, email: email, hints: hints) } //swiftlint:enable todo @@ -86,9 +94,9 @@ struct SentryUserFeedbackWidget { } } - init(config: SentryUserFeedbackConfiguration) { + init(config: SentryUserFeedbackConfiguration, delegate: Delegate) { super.init(frame: UIScreen.main.bounds) - rootViewController = RootViewController(config: config) + rootViewController = RootViewController(config: config, delegate: delegate) windowLevel = config.widgetConfig.windowLevel } From 9220f5851fe2ecb930e6c895b69a66ae33c5e81f Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Thu, 14 Nov 2024 15:45:30 -0900 Subject: [PATCH 07/27] just send from the form, no need for delegation --- Sources/Sentry/SentryUserFeedbackIntegration.m | 17 ++--------------- .../UserFeedback/SentryUserFeedbackForm.swift | 15 ++++++++++----- .../SentryUserFeedbackIntegrationDriver.swift | 17 ++--------------- .../UserFeedback/SentryUserFeedbackWidget.swift | 16 ++++------------ 4 files changed, 18 insertions(+), 47 deletions(-) diff --git a/Sources/Sentry/SentryUserFeedbackIntegration.m b/Sources/Sentry/SentryUserFeedbackIntegration.m index 071723ca292..8c52d3297b0 100644 --- a/Sources/Sentry/SentryUserFeedbackIntegration.m +++ b/Sources/Sentry/SentryUserFeedbackIntegration.m @@ -7,7 +7,7 @@ #if TARGET_OS_IOS && SENTRY_HAS_UIKIT -@interface SentryUserFeedbackIntegration() +@interface SentryUserFeedbackIntegration() @end @@ -22,23 +22,10 @@ - (BOOL)installWithOptions:(SentryOptions *)options } _driver = [[SentryUserFeedbackIntegrationDriver alloc] - initWithConfiguration:options.userFeedbackConfiguration delegate:self]; + initWithConfiguration:options.userFeedbackConfiguration]; return YES; } -#pragma mark - SentryUserFeedbackIntegrationDriverDelegate - -- (void)captureFeedbackWithMessage:(NSString * _Nonnull)message name:(NSString * _Nullable)name email:(NSString * _Nullable)email hints:(NSDictionary * _Nullable)hints { - NSError *error = [[NSError alloc] initWithDomain:@"user-feedback" code:1 userInfo:nil]; - SentryId *eventId = [SentrySDK captureError:error]; -// SentryId *eventId = [[SentryId alloc] init]; - SentryUserFeedback *uf = [[SentryUserFeedback alloc] initWithEventId:eventId]; - uf.name = name; - uf.comments = message; - uf.email = email; - [SentrySDK captureUserFeedback:uf]; -} - @end #endif // TARGET_OS_IOS && SENTRY_HAS_UIKIT diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift index 284b0528d91..97311f6867b 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift @@ -8,8 +8,7 @@ import UIKit @available(iOS 13.0, *) protocol SentryUserFeedbackFormDelegate: NSObjectProtocol { - func cancelled() - func captureFeedback(message: String, name: String?, email: String?, hints: [String: Any]?) + func finished() } @available(iOS 13.0, *) @@ -132,12 +131,18 @@ class SentryUserFeedbackForm: UIViewController { present(alert, animated: config.animations) return } - - delegate?.captureFeedback(message: messageTextView.text, name: fullNameTextField.text, email: emailTextField.text, hints: nil) + let error = NSError(domain: "user-feedback", code: 1) + let eventId = SentrySDK.capture(error: error) + let uf = UserFeedback(eventId: eventId) + uf.name = name + uf.email = email + uf.comments = messageTextView.text + SentrySDK.capture(userFeedback: uf) + delegate?.finished() } func cancelButtonTapped() { - delegate?.cancelled() + delegate?.finished() } // MARK: Layout diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift index efa3ce96ba0..6e7b887a310 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift @@ -3,10 +3,6 @@ import Foundation @_implementationOnly import _SentryPrivate import UIKit -@objc public protocol SentryUserFeedbackIntegrationDriverDelegate: NSObjectProtocol { - func captureFeedback(message: String, name: String?, email: String?, hints: [String: Any]?) -} - /** * An integration managing a workflow for end users to report feedback via Sentry. * - note: The default method to show the feedback form is via a floating widget placed in the bottom trailing corner of the screen. See the configuration classes for alternative options. @@ -16,11 +12,9 @@ import UIKit class SentryUserFeedbackIntegrationDriver: NSObject { let configuration: SentryUserFeedbackConfiguration private var window: SentryUserFeedbackWidget.Window? - weak var delegate: (any SentryUserFeedbackIntegrationDriverDelegate)? - public init(configuration: SentryUserFeedbackConfiguration, delegate: any SentryUserFeedbackIntegrationDriverDelegate) { + public init(configuration: SentryUserFeedbackConfiguration) { self.configuration = configuration - self.delegate = delegate super.init() if let widgetConfigBuilder = configuration.configureWidget { @@ -55,7 +49,7 @@ class SentryUserFeedbackIntegrationDriver: NSObject { * If `SentryUserFeedbackConfiguration.autoInject` is `false`, this must be called explicitly. */ func createWidget() { - window = SentryUserFeedbackWidget.Window(config: configuration, delegate: self) + window = SentryUserFeedbackWidget.Window(config: configuration) window?.isHidden = false } @@ -86,11 +80,4 @@ class SentryUserFeedbackIntegrationDriver: NSObject { } } -@available(iOS 13.0, *) -extension SentryUserFeedbackIntegrationDriver: SentryUserFeedbackWidget.Delegate { - func captureFeedback(message: String, name: String?, email: String?, hints: [String : Any]?) { - self.delegate?.captureFeedback(message: message, name: name, email: email, hints: hints) - } -} - #endif // os(iOS) && !SENTRY_NO_UIKIT diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift index 03c40412365..922fd236600 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift @@ -9,11 +9,6 @@ var displayingForm = false @available(iOS 13.0, *) struct SentryUserFeedbackWidget { - - protocol Delegate: NSObjectProtocol { - func captureFeedback(message: String, name: String?, email: String?, hints: [String: Any]?) - } - class Window: UIWindow { class RootViewController: UIViewController, SentryUserFeedbackFormDelegate, UIAdaptivePresentationControllerDelegate { let defaultWidgetSpacing: CGFloat = 8 @@ -27,11 +22,8 @@ struct SentryUserFeedbackWidget { let config: SentryUserFeedbackConfiguration - weak var delegate: (any Delegate)? - - init(config: SentryUserFeedbackConfiguration, delegate: any Delegate) { + init(config: SentryUserFeedbackConfiguration) { self.config = config - self.delegate = delegate super.init(nibName: nil, bundle: nil) view.addSubview(button) @@ -76,7 +68,7 @@ struct SentryUserFeedbackWidget { // MARK: SentryUserFeedbackFormDelegate - func cancelled() { + func finished() { closeForm() } @@ -94,9 +86,9 @@ struct SentryUserFeedbackWidget { } } - init(config: SentryUserFeedbackConfiguration, delegate: Delegate) { + init(config: SentryUserFeedbackConfiguration) { super.init(frame: UIScreen.main.bounds) - rootViewController = RootViewController(config: config, delegate: delegate) + rootViewController = RootViewController(config: config) windowLevel = config.widgetConfig.windowLevel } From f66ad3b82e8a82911ed27496ee0a8d26c8535fce Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Thu, 14 Nov 2024 15:47:01 -0900 Subject: [PATCH 08/27] put back integration objc --- Sources/Sentry/SentryUserFeedbackIntegration.m | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Sources/Sentry/SentryUserFeedbackIntegration.m b/Sources/Sentry/SentryUserFeedbackIntegration.m index 8c52d3297b0..9bf8b13dcfb 100644 --- a/Sources/Sentry/SentryUserFeedbackIntegration.m +++ b/Sources/Sentry/SentryUserFeedbackIntegration.m @@ -1,16 +1,9 @@ #import "SentryUserFeedbackIntegration.h" #import "SentryOptions+Private.h" #import "SentrySwift.h" -#import "SentryEnvelope.h" -#import "SentrySDK+Private.h" -#import "SentryUserFeedback.h" #if TARGET_OS_IOS && SENTRY_HAS_UIKIT -@interface SentryUserFeedbackIntegration() - -@end - @implementation SentryUserFeedbackIntegration { SentryUserFeedbackIntegrationDriver *_driver; } @@ -22,7 +15,7 @@ - (BOOL)installWithOptions:(SentryOptions *)options } _driver = [[SentryUserFeedbackIntegrationDriver alloc] - initWithConfiguration:options.userFeedbackConfiguration]; + initWithConfiguration:options.userFeedbackConfiguration]; return YES; } From bc430c5102b109a5afe11745aa097a08432180e9 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Fri, 15 Nov 2024 17:12:11 -0900 Subject: [PATCH 09/27] add new SentryFeedback interface and thread callpath to capture in envelope --- Sentry.xcodeproj/project.pbxproj | 4 ++ Sources/Sentry/SentryClient.m | 10 ++++ Sources/Sentry/SentryEnvelope.m | 18 ++++++ Sources/Sentry/SentryHub.m | 8 +++ Sources/Sentry/SentrySDK.m | 5 ++ Sources/Sentry/SentryTransportAdapter.m | 11 ++++ .../include/HybridPublic/SentryEnvelope.h | 10 ++-- .../HybridPublic/SentryEnvelopeItemType.h | 1 + Sources/Sentry/include/SentryClient+Private.h | 18 +++++- Sources/Sentry/include/SentryHub+Private.h | 2 + Sources/Sentry/include/SentrySDK+Private.h | 8 ++- .../Sentry/include/SentryTransportAdapter.h | 12 +++- .../UserFeedback/SentryFeedback.swift | 57 +++++++++++++++++++ 13 files changed, 155 insertions(+), 9 deletions(-) create mode 100644 Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index fad17aaa0dd..93a8c527874 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -708,6 +708,7 @@ 84CFA4CA2C9DF884008DA5F4 /* SentryUserFeedbackWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CFA4C92C9DF884008DA5F4 /* SentryUserFeedbackWidget.swift */; }; 84CFA4CD2C9E0CA3008DA5F4 /* SentryUserFeedbackIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = 84CFA4CC2C9E0CA3008DA5F4 /* SentryUserFeedbackIntegration.m */; }; 84CFA4CE2C9E0CA3008DA5F4 /* SentryUserFeedbackIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = 84CFA4CB2C9E0CA3008DA5F4 /* SentryUserFeedbackIntegration.h */; }; + 84DBC62C2CE82F12000C4904 /* SentryFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DBC62B2CE82F0E000C4904 /* SentryFeedback.swift */; }; 84DEE86B2B686BD400A7BC17 /* SentrySamplerDecision.h in Headers */ = {isa = PBXBuildFile; fileRef = 84DEE86A2B686BD400A7BC17 /* SentrySamplerDecision.h */; }; 84DEE8762B69AD6400A7BC17 /* SentryLaunchProfiling.h in Headers */ = {isa = PBXBuildFile; fileRef = 84DEE8752B69AD6400A7BC17 /* SentryLaunchProfiling.h */; }; 84E13B842CBF1D91003B52EC /* SentryUserFeedbackWidgetButtonMegaphoneIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E13B832CBF1D91003B52EC /* SentryUserFeedbackWidgetButtonMegaphoneIconView.swift */; }; @@ -1776,6 +1777,7 @@ 84CFA4C92C9DF884008DA5F4 /* SentryUserFeedbackWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUserFeedbackWidget.swift; sourceTree = ""; }; 84CFA4CB2C9E0CA3008DA5F4 /* SentryUserFeedbackIntegration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryUserFeedbackIntegration.h; path = ../../../Sentry/include/SentryUserFeedbackIntegration.h; sourceTree = ""; }; 84CFA4CC2C9E0CA3008DA5F4 /* SentryUserFeedbackIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SentryUserFeedbackIntegration.m; path = ../../../Sentry/SentryUserFeedbackIntegration.m; sourceTree = ""; }; + 84DBC62B2CE82F0E000C4904 /* SentryFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryFeedback.swift; sourceTree = ""; }; 84DEE86A2B686BD400A7BC17 /* SentrySamplerDecision.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentrySamplerDecision.h; path = include/SentrySamplerDecision.h; sourceTree = ""; }; 84DEE8752B69AD6400A7BC17 /* SentryLaunchProfiling.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryLaunchProfiling.h; path = Sources/Sentry/include/SentryLaunchProfiling.h; sourceTree = SOURCE_ROOT; }; 84E13B832CBF1D91003B52EC /* SentryUserFeedbackWidgetButtonMegaphoneIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUserFeedbackWidgetButtonMegaphoneIconView.swift; sourceTree = ""; }; @@ -3522,6 +3524,7 @@ children = ( 849B8F9E2C70091A00148E1F /* Configuration */, 849B8F962C6E906900148E1F /* SentryUserFeedbackIntegrationDriver.swift */, + 84DBC62B2CE82F0E000C4904 /* SentryFeedback.swift */, 84CFA4CB2C9E0CA3008DA5F4 /* SentryUserFeedbackIntegration.h */, 84CFA4CC2C9E0CA3008DA5F4 /* SentryUserFeedbackIntegration.m */, 84CFA4C92C9DF884008DA5F4 /* SentryUserFeedbackWidget.swift */, @@ -4564,6 +4567,7 @@ 7B6438AB26A70F24000D0F65 /* UIViewController+Sentry.m in Sources */, 84302A812B5767A50027A629 /* SentryLaunchProfiling.m in Sources */, 63AA76A31EB9CBAA00D153DE /* SentryDsn.m in Sources */, + 84DBC62C2CE82F12000C4904 /* SentryFeedback.swift in Sources */, 63B818FA1EC34639002FDF4C /* SentryDebugMeta.m in Sources */, 7B98D7D325FB65AE00C5A389 /* SentryWatchdogTerminationTracker.m in Sources */, 8E564AE8267AF22600FE117D /* SentryNetworkTrackingIntegration.m in Sources */, diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 0f57f8309b9..6d921e2656f 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -585,6 +585,16 @@ - (void)captureUserFeedback:(SentryUserFeedback *)userFeedback [self.transportAdapter sendUserFeedback:userFeedback]; } +- (void)captureFeedback:(SentryFeedback *)feedback +{ + if ([self isDisabled]) { + [self logDisabledMessage]; + return; + } + + [self.transportAdapter sendFeedback:feedback]; +} + - (void)storeEnvelope:(SentryEnvelope *)envelope { [self.fileManager storeEnvelope:envelope]; diff --git a/Sources/Sentry/SentryEnvelope.m b/Sources/Sentry/SentryEnvelope.m index 7b63610d2ed..a91e0dba9f6 100644 --- a/Sources/Sentry/SentryEnvelope.m +++ b/Sources/Sentry/SentryEnvelope.m @@ -129,6 +129,24 @@ - (instancetype)initWithUserFeedback:(SentryUserFeedback *)userFeedback data:json]; } +- (instancetype)initWithFeedback:(SentryFeedback *)feedback +{ + NSError *error = nil; + NSData *json = [NSJSONSerialization dataWithJSONObject:[feedback serialize] + options:0 + error:&error]; + + if (nil != error) { + SENTRY_LOG_ERROR(@"Couldn't serialize feedback."); + json = [NSData new]; + } + + return [self initWithHeader:[[SentryEnvelopeItemHeader alloc] + initWithType:SentryEnvelopeItemTypeFeedback + length:json.length] + data:json]; +} + - (instancetype)initWithClientReport:(SentryClientReport *)clientReport { NSError *error = nil; diff --git a/Sources/Sentry/SentryHub.m b/Sources/Sentry/SentryHub.m index a83f66c71c0..cc622a4a927 100644 --- a/Sources/Sentry/SentryHub.m +++ b/Sources/Sentry/SentryHub.m @@ -504,6 +504,14 @@ - (void)captureUserFeedback:(SentryUserFeedback *)userFeedback } } +- (void)captureFeedback:(SentryFeedback *)feedback +{ + SentryClient *client = _client; + if (client != nil) { + [client captureFeedback:feedback]; + } +} + - (void)addBreadcrumb:(SentryBreadcrumb *)crumb { SentryOptions *options = [[self client] options]; diff --git a/Sources/Sentry/SentrySDK.m b/Sources/Sentry/SentrySDK.m index e77108f05de..1eee3e66fa8 100644 --- a/Sources/Sentry/SentrySDK.m +++ b/Sources/Sentry/SentrySDK.m @@ -405,6 +405,11 @@ + (void)captureUserFeedback:(SentryUserFeedback *)userFeedback [SentrySDK.currentHub captureUserFeedback:userFeedback]; } ++ (void)captureFeedback:(SentryFeedback *)feedback +{ + [SentrySDK.currentHub captureFeedback:feedback]; +} + + (void)showUserFeedbackForm { // TODO: implement diff --git a/Sources/Sentry/SentryTransportAdapter.m b/Sources/Sentry/SentryTransportAdapter.m index 0a1027430bf..be87c76480f 100644 --- a/Sources/Sentry/SentryTransportAdapter.m +++ b/Sources/Sentry/SentryTransportAdapter.m @@ -3,6 +3,7 @@ #import "SentryEvent.h" #import "SentryOptions.h" #import "SentryUserFeedback.h" +#import "SentrySwift.h" #import NS_ASSUME_NONNULL_BEGIN @@ -102,6 +103,16 @@ - (void)sendUserFeedback:(SentryUserFeedback *)userFeedback [self sendEnvelope:envelope]; } +- (void)sendFeedback:(SentryFeedback *)feedback +{ + SentryEnvelopeItem *item = [[SentryEnvelopeItem alloc] initWithFeedback:feedback]; + SentryEnvelopeHeader *envelopeHeader = + [[SentryEnvelopeHeader alloc] initWithId:feedback.eventId]; + SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:envelopeHeader + singleItem:item]; + [self sendEnvelope:envelope]; +} + - (void)sendEnvelope:(SentryEnvelope *)envelope { for (id transport in self.transports) { diff --git a/Sources/Sentry/include/HybridPublic/SentryEnvelope.h b/Sources/Sentry/include/HybridPublic/SentryEnvelope.h index b1be5d43da9..dca89098901 100644 --- a/Sources/Sentry/include/HybridPublic/SentryEnvelope.h +++ b/Sources/Sentry/include/HybridPublic/SentryEnvelope.h @@ -16,13 +16,14 @@ #endif -@class SentryEvent; -@class SentrySession; -@class SentryId; -@class SentryUserFeedback; @class SentryAttachment; @class SentryEnvelopeItemHeader; +@class SentryEvent; +@class SentryFeedback; +@class SentryId; +@class SentrySession; @class SentryTraceContext; +@class SentryUserFeedback; NS_ASSUME_NONNULL_BEGIN @@ -91,6 +92,7 @@ SENTRY_NO_INIT - (instancetype)initWithEvent:(SentryEvent *)event; - (instancetype)initWithSession:(SentrySession *)session; - (instancetype)initWithUserFeedback:(SentryUserFeedback *)userFeedback; +- (instancetype)initWithFeedback:(SentryFeedback *)feedback; - (_Nullable instancetype)initWithAttachment:(SentryAttachment *)attachment maxAttachmentSize:(NSUInteger)maxAttachmentSize; - (instancetype)initWithHeader:(SentryEnvelopeItemHeader *)header diff --git a/Sources/Sentry/include/HybridPublic/SentryEnvelopeItemType.h b/Sources/Sentry/include/HybridPublic/SentryEnvelopeItemType.h index e7747588cca..3499a206150 100644 --- a/Sources/Sentry/include/HybridPublic/SentryEnvelopeItemType.h +++ b/Sources/Sentry/include/HybridPublic/SentryEnvelopeItemType.h @@ -6,6 +6,7 @@ static NSString *const SentryEnvelopeItemTypeEvent = @"event"; static NSString *const SentryEnvelopeItemTypeSession = @"session"; static NSString *const SentryEnvelopeItemTypeUserFeedback = @"user_report"; +static NSString *const SentryEnvelopeItemTypeFeedback = @"feedback"; static NSString *const SentryEnvelopeItemTypeTransaction = @"transaction"; static NSString *const SentryEnvelopeItemTypeAttachment = @"attachment"; static NSString *const SentryEnvelopeItemTypeClientReport = @"client_report"; diff --git a/Sources/Sentry/include/SentryClient+Private.h b/Sources/Sentry/include/SentryClient+Private.h index e554b751f8e..077461be403 100644 --- a/Sources/Sentry/include/SentryClient+Private.h +++ b/Sources/Sentry/include/SentryClient+Private.h @@ -2,8 +2,15 @@ #import "SentryDataCategory.h" #import "SentryDiscardReason.h" -@class SentrySession, SentryEnvelopeItem, SentryId, SentryAttachment, SentryThreadInspector, - SentryReplayEvent, SentryReplayRecording, SentryEnvelope; +@class SentryAttachment; +@class SentryEnvelope; +@class SentryEnvelopeItem; +@class SentryFeedback; +@class SentryId; +@class SentryReplayEvent; +@class SentryReplayRecording; +@class SentrySession; +@class SentryThreadInspector; NS_ASSUME_NONNULL_BEGIN @@ -67,6 +74,13 @@ NS_ASSUME_NONNULL_BEGIN - (void)addAttachmentProcessor:(id)attachmentProcessor; - (void)removeAttachmentProcessor:(id)attachmentProcessor; +/** + * Captures a new-style user feedback and sends it to Sentry. + * @param feedback The user feedback to send to Sentry. + */ +- (void)captureFeedback:(SentryFeedback *)feedback + NS_SWIFT_NAME(capture(feedback:)); + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryHub+Private.h b/Sources/Sentry/include/SentryHub+Private.h index 531ec23b6da..bb4a88662f1 100644 --- a/Sources/Sentry/include/SentryHub+Private.h +++ b/Sources/Sentry/include/SentryHub+Private.h @@ -6,6 +6,7 @@ @class SentryTransaction; @class SentryDispatchQueueWrapper; @class SentryEnvelope; +@class SentryFeedback; @class SentryNSTimerFactory; @class SentrySession; @class SentryTracer; @@ -63,6 +64,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)storeEnvelope:(SentryEnvelope *)envelope; - (void)captureEnvelope:(SentryEnvelope *)envelope; +- (void)captureFeedback:(SentryFeedback *)feedback; - (void)registerSessionListener:(id)listener; - (void)unregisterSessionListener:(id)listener; diff --git a/Sources/Sentry/include/SentrySDK+Private.h b/Sources/Sentry/include/SentrySDK+Private.h index 374b8d0c107..bebbaf11e4c 100644 --- a/Sources/Sentry/include/SentrySDK+Private.h +++ b/Sources/Sentry/include/SentrySDK+Private.h @@ -10,7 +10,11 @@ # import "SentrySDK.h" #endif -@class SentryHub, SentryId, SentryAppStartMeasurement, SentryEnvelope; +@class SentryAppStartMeasurement; +@class SentryEnvelope; +@class SentryFeedback; +@class SentryHub; +@class SentryId; NS_ASSUME_NONNULL_BEGIN @@ -73,6 +77,8 @@ NS_ASSUME_NONNULL_BEGIN + (void)showUserFeedbackForm; #endif // TARGET_OS_IOS && SENTRY_HAS_UIKIT ++ (void)captureFeedback:(SentryFeedback *)feedback; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryTransportAdapter.h b/Sources/Sentry/include/SentryTransportAdapter.h index 2182eb630f8..1db0e83dd39 100644 --- a/Sources/Sentry/include/SentryTransportAdapter.h +++ b/Sources/Sentry/include/SentryTransportAdapter.h @@ -3,8 +3,15 @@ #import "SentryDiscardReason.h" #import "SentryTransport.h" -@class SentryEnvelope, SentryEnvelopeItem, SentryEvent, SentrySession, SentryUserFeedback, - SentryAttachment, SentryTraceContext, SentryOptions; +@class SentryAttachment; +@class SentryEnvelope; +@class SentryEnvelopeItem; +@class SentryEvent; +@class SentryFeedback; +@class SentryOptions; +@class SentrySession; +@class SentryTraceContext; +@class SentryUserFeedback; NS_ASSUME_NONNULL_BEGIN @@ -42,6 +49,7 @@ SENTRY_NO_INIT - (void)storeEvent:(SentryEvent *)event traceContext:(nullable SentryTraceContext *)traceContext; - (void)sendUserFeedback:(SentryUserFeedback *)userFeedback NS_SWIFT_NAME(send(userFeedback:)); +- (void)sendFeedback:(SentryFeedback *)feedback NS_SWIFT_NAME(send(feedback:)); - (void)sendEnvelope:(SentryEnvelope *)envelope NS_SWIFT_NAME(send(envelope:)); diff --git a/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift b/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift new file mode 100644 index 00000000000..df2a4b451c7 --- /dev/null +++ b/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift @@ -0,0 +1,57 @@ +import Foundation +#if os(iOS) && !SENTRY_NO_UIKIT +@_implementationOnly import _SentryPrivate +import UIKit + +/** + * A user feedback item that serializes to an envelope with the new format described at + * https://develop.sentry.dev/application/feedback-architecture/#feedback-events. + */ +@objcMembers +class SentryFeedback: NSObject, SentrySerializable { + enum Source: String { + case widget + case custom + } + + var name: String? + var email: String? + var message: String + var hints: [String: Any]? + var source: Source + let eventId: SentryId + + /// The event id that this feedback is associated with, like a crash report. + var associatedEventId: String? + + init(name: String?, email: String?, message: String, hints: [String: Any]?, source: Source, associatedEventId: String?) { + self.eventId = SentryId() + self.name = name + self.email = email + self.message = message + self.hints = hints + self.source = source + self.associatedEventId = associatedEventId + super.init() + } + + func serialize() -> [String : Any] { + var dict: [String: Any] = [ + "message": message + ] + if let name = name { + dict["name"] = name + } + if let email = email { + dict["contact_email"] = email + } + if let associatedEventId = associatedEventId { + dict["associated_event_id"] = associatedEventId + } + dict["source"] = source.rawValue + + return dict + } +} + +#endif // os(iOS) && !SENTRY_NO_UIKIT From b78655ca26d8c563f4a506e38b33e78fa4f6ce63 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Fri, 15 Nov 2024 17:29:16 -0900 Subject: [PATCH 10/27] fix headerdoc in SentrySDK.h; use new SDK capture method in form submission --- Sources/Sentry/Public/SentryUserFeedback.h | 12 +++++- Sources/Sentry/SentryUserFeedback.m | 40 +++++++++++++++---- Sources/Sentry/include/SentryPrivate.h | 1 + .../UserFeedback/SentryFeedback.swift | 2 +- .../UserFeedback/SentryUserFeedbackForm.swift | 10 ++--- 5 files changed, 49 insertions(+), 16 deletions(-) diff --git a/Sources/Sentry/Public/SentryUserFeedback.h b/Sources/Sentry/Public/SentryUserFeedback.h index 6bd777f1285..e915b18a6d5 100644 --- a/Sources/Sentry/Public/SentryUserFeedback.h +++ b/Sources/Sentry/Public/SentryUserFeedback.h @@ -15,14 +15,24 @@ NS_ASSUME_NONNULL_BEGIN */ NS_SWIFT_NAME(UserFeedback) @interface SentryUserFeedback : NSObject -SENTRY_NO_INIT /** * Initializes SentryUserFeedback and sets the required eventId. * @param eventId The eventId of the event to which the user feedback is associated. + * @note Uses the old envelope format descried at + * https://develop.sentry.dev/sdk/data-model/envelope-items/#user-feedback and will be deprecated in + * the future. */ - (instancetype)initWithEventId:(SentryId *)eventId; +/** + * Initializes a new `SentryUserFeedback` as its own event, instead of one attached to a transaction + * or error event. + * @note Uses the new envelope format described at + * https://develop.sentry.dev/application/feedback-architecture/#feedback-events. + */ +- (instancetype)init; + /** * The eventId of the event to which the user feedback is associated. */ diff --git a/Sources/Sentry/SentryUserFeedback.m b/Sources/Sentry/SentryUserFeedback.m index fc43f4bb2c8..f25e35b904a 100644 --- a/Sources/Sentry/SentryUserFeedback.m +++ b/Sources/Sentry/SentryUserFeedback.m @@ -2,7 +2,25 @@ #import "SentrySwift.h" #import -@implementation SentryUserFeedback +typedef enum : NSUInteger { + /** A user feedback attached to a transaction or error event. */ + kSentryUserFeedbackTypeAttached, + + /** A user feedback sent as its own event independent of any other event. */ + kSentryUserFeedbackTypeStandalone, +} SentryUserFeedbackType; + +@implementation SentryUserFeedback { + SentryUserFeedbackType _type; +} + +- (instancetype)init { + if (self = [super init]) { + _type = kSentryUserFeedbackTypeStandalone; + _eventId = [[SentryId alloc] init]; + } + return self; +} - (instancetype)initWithEventId:(SentryId *)eventId { @@ -11,18 +29,26 @@ - (instancetype)initWithEventId:(SentryId *)eventId _email = @""; _name = @""; _comments = @""; + _type = kSentryUserFeedbackTypeAttached; } return self; } - (NSDictionary *)serialize { - return @{ - @"event_id" : self.eventId.sentryIdString, - @"email" : self.email, - @"name" : self.name, - @"comments" : self.comments - }; + switch (_type) { + case kSentryUserFeedbackTypeAttached: + return @{ + @"event_id" : self.eventId.sentryIdString, + @"email" : self.email, + @"name" : self.name, + @"comments" : self.comments + }; + case kSentryUserFeedbackTypeStandalone: + return @{ + + }; + } } @end diff --git a/Sources/Sentry/include/SentryPrivate.h b/Sources/Sentry/include/SentryPrivate.h index 537fae6d525..b541c0668c1 100644 --- a/Sources/Sentry/include/SentryPrivate.h +++ b/Sources/Sentry/include/SentryPrivate.h @@ -6,6 +6,7 @@ #import "SentrySDK+Private.h" #import "SentryScope+Private.h" #import "SentryTime.h" +#import "SentrySDK+Private.h" // Headers that also import SentryDefines should be at the end of this list // otherwise it wont compile diff --git a/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift b/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift index df2a4b451c7..c052cb0b8f7 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift @@ -24,7 +24,7 @@ class SentryFeedback: NSObject, SentrySerializable { /// The event id that this feedback is associated with, like a crash report. var associatedEventId: String? - init(name: String?, email: String?, message: String, hints: [String: Any]?, source: Source, associatedEventId: String?) { + init(name: String?, email: String?, message: String, hints: [String: Any]? = nil, source: Source = .widget, associatedEventId: String? = nil) { self.eventId = SentryId() self.name = name self.email = email diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift index 97311f6867b..0ba8a9a63dd 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift @@ -131,13 +131,9 @@ class SentryUserFeedbackForm: UIViewController { present(alert, animated: config.animations) return } - let error = NSError(domain: "user-feedback", code: 1) - let eventId = SentrySDK.capture(error: error) - let uf = UserFeedback(eventId: eventId) - uf.name = name - uf.email = email - uf.comments = messageTextView.text - SentrySDK.capture(userFeedback: uf) + + let feedback = SentryFeedback(name: fullNameTextField.text, email: emailTextField.text, message: messageTextView.text, hints: nil) + SentrySDK.capture(feedback: feedback) delegate?.finished() } From ec031c49f1ccc0c47c1c325952a82cf1ce20e016 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Sat, 16 Nov 2024 02:30:26 +0000 Subject: [PATCH 11/27] Format code --- Sources/Sentry/SentryEnvelope.m | 8 ++++---- Sources/Sentry/SentryTransportAdapter.m | 2 +- Sources/Sentry/SentryUserFeedback.m | 7 ++++--- Sources/Sentry/include/SentryClient+Private.h | 3 +-- Sources/Sentry/include/SentryPrivate.h | 1 - .../Swift/Integrations/UserFeedback/SentryFeedback.swift | 2 +- 6 files changed, 11 insertions(+), 12 deletions(-) diff --git a/Sources/Sentry/SentryEnvelope.m b/Sources/Sentry/SentryEnvelope.m index a91e0dba9f6..72181c0afd7 100644 --- a/Sources/Sentry/SentryEnvelope.m +++ b/Sources/Sentry/SentryEnvelope.m @@ -141,10 +141,10 @@ - (instancetype)initWithFeedback:(SentryFeedback *)feedback json = [NSData new]; } - return [self initWithHeader:[[SentryEnvelopeItemHeader alloc] - initWithType:SentryEnvelopeItemTypeFeedback - length:json.length] - data:json]; + return [self + initWithHeader:[[SentryEnvelopeItemHeader alloc] initWithType:SentryEnvelopeItemTypeFeedback + length:json.length] + data:json]; } - (instancetype)initWithClientReport:(SentryClientReport *)clientReport diff --git a/Sources/Sentry/SentryTransportAdapter.m b/Sources/Sentry/SentryTransportAdapter.m index be87c76480f..6ed7c8eee1b 100644 --- a/Sources/Sentry/SentryTransportAdapter.m +++ b/Sources/Sentry/SentryTransportAdapter.m @@ -2,8 +2,8 @@ #import "SentryEnvelope.h" #import "SentryEvent.h" #import "SentryOptions.h" -#import "SentryUserFeedback.h" #import "SentrySwift.h" +#import "SentryUserFeedback.h" #import NS_ASSUME_NONNULL_BEGIN diff --git a/Sources/Sentry/SentryUserFeedback.m b/Sources/Sentry/SentryUserFeedback.m index f25e35b904a..307574a61f5 100644 --- a/Sources/Sentry/SentryUserFeedback.m +++ b/Sources/Sentry/SentryUserFeedback.m @@ -5,7 +5,7 @@ typedef enum : NSUInteger { /** A user feedback attached to a transaction or error event. */ kSentryUserFeedbackTypeAttached, - + /** A user feedback sent as its own event independent of any other event. */ kSentryUserFeedbackTypeStandalone, } SentryUserFeedbackType; @@ -14,7 +14,8 @@ @implementation SentryUserFeedback { SentryUserFeedbackType _type; } -- (instancetype)init { +- (instancetype)init +{ if (self = [super init]) { _type = kSentryUserFeedbackTypeStandalone; _eventId = [[SentryId alloc] init]; @@ -46,7 +47,7 @@ - (instancetype)initWithEventId:(SentryId *)eventId }; case kSentryUserFeedbackTypeStandalone: return @{ - + }; } } diff --git a/Sources/Sentry/include/SentryClient+Private.h b/Sources/Sentry/include/SentryClient+Private.h index 077461be403..e3fc83b8c5c 100644 --- a/Sources/Sentry/include/SentryClient+Private.h +++ b/Sources/Sentry/include/SentryClient+Private.h @@ -78,8 +78,7 @@ NS_ASSUME_NONNULL_BEGIN * Captures a new-style user feedback and sends it to Sentry. * @param feedback The user feedback to send to Sentry. */ -- (void)captureFeedback:(SentryFeedback *)feedback - NS_SWIFT_NAME(capture(feedback:)); +- (void)captureFeedback:(SentryFeedback *)feedback NS_SWIFT_NAME(capture(feedback:)); @end diff --git a/Sources/Sentry/include/SentryPrivate.h b/Sources/Sentry/include/SentryPrivate.h index b541c0668c1..537fae6d525 100644 --- a/Sources/Sentry/include/SentryPrivate.h +++ b/Sources/Sentry/include/SentryPrivate.h @@ -6,7 +6,6 @@ #import "SentrySDK+Private.h" #import "SentryScope+Private.h" #import "SentryTime.h" -#import "SentrySDK+Private.h" // Headers that also import SentryDefines should be at the end of this list // otherwise it wont compile diff --git a/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift b/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift index c052cb0b8f7..a4869523af0 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift @@ -35,7 +35,7 @@ class SentryFeedback: NSObject, SentrySerializable { super.init() } - func serialize() -> [String : Any] { + func serialize() -> [String: Any] { var dict: [String: Any] = [ "message": message ] From 0ac677a7435f7c5d19ab9a9289ae57912c7375d0 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Tue, 19 Nov 2024 13:31:56 -0900 Subject: [PATCH 12/27] pr feedback and updates --- Sources/Sentry/Public/SentryUserFeedback.h | 12 +----- Sources/Sentry/SentryDataCategoryMapper.m | 20 ++++++--- Sources/Sentry/SentryUserFeedback.m | 41 ++++--------------- Sources/Sentry/include/SentryDataCategory.h | 3 +- Sources/Sentry/include/SentrySDK+Private.h | 2 - .../UserFeedback/SentryFeedback.swift | 2 +- 6 files changed, 25 insertions(+), 55 deletions(-) diff --git a/Sources/Sentry/Public/SentryUserFeedback.h b/Sources/Sentry/Public/SentryUserFeedback.h index e915b18a6d5..6bd777f1285 100644 --- a/Sources/Sentry/Public/SentryUserFeedback.h +++ b/Sources/Sentry/Public/SentryUserFeedback.h @@ -15,24 +15,14 @@ NS_ASSUME_NONNULL_BEGIN */ NS_SWIFT_NAME(UserFeedback) @interface SentryUserFeedback : NSObject +SENTRY_NO_INIT /** * Initializes SentryUserFeedback and sets the required eventId. * @param eventId The eventId of the event to which the user feedback is associated. - * @note Uses the old envelope format descried at - * https://develop.sentry.dev/sdk/data-model/envelope-items/#user-feedback and will be deprecated in - * the future. */ - (instancetype)initWithEventId:(SentryId *)eventId; -/** - * Initializes a new `SentryUserFeedback` as its own event, instead of one attached to a transaction - * or error event. - * @note Uses the new envelope format described at - * https://develop.sentry.dev/application/feedback-architecture/#feedback-events. - */ -- (instancetype)init; - /** * The eventId of the event to which the user feedback is associated. */ diff --git a/Sources/Sentry/SentryDataCategoryMapper.m b/Sources/Sentry/SentryDataCategoryMapper.m index 2af6068f1fc..9899ec1d7c4 100644 --- a/Sources/Sentry/SentryDataCategoryMapper.m +++ b/Sources/Sentry/SentryDataCategoryMapper.m @@ -18,6 +18,7 @@ NSString *const kSentryDataCategoryNameReplay = @"replay"; NSString *const kSentryDataCategoryNameMetricBucket = @"metric_bucket"; NSString *const kSentryDataCategoryNameSpan = @"span"; +NSString *const kSentryDataCategoryNameFeedback = @"feedback"; NSString *const kSentryDataCategoryNameUnknown = @"unknown"; NS_ASSUME_NONNULL_BEGIN @@ -46,6 +47,9 @@ if ([itemType isEqualToString:SentryEnvelopeItemTypeReplayVideo]) { return kSentryDataCategoryReplay; } + if ([itemType isEqualToString:SentryEnvelopeItemTypeFeedback]) { + return kSentryDataCategoryFeedback; + } // The envelope item type used for metrics is statsd whereas the client report category for // discarded events is metric_bucket. if ([itemType isEqualToString:SentryEnvelopeItemTypeStatsd]) { @@ -104,6 +108,9 @@ if ([value isEqualToString:kSentryDataCategoryNameSpan]) { return kSentryDataCategorySpan; } + if ([value isEqualToString:kSentryDataCategoryNameFeedback]) { + return kSentryDataCategoryFeedback; + } return kSentryDataCategoryUnknown; } @@ -111,13 +118,10 @@ NSString * nameForSentryDataCategory(SentryDataCategory category) { - if (category < kSentryDataCategoryAll && category > kSentryDataCategoryUnknown) { - return kSentryDataCategoryNameUnknown; - } - switch (category) { case kSentryDataCategoryAll: return kSentryDataCategoryNameAll; + case kSentryDataCategoryDefault: return kSentryDataCategoryNameDefault; case kSentryDataCategoryError: @@ -136,12 +140,16 @@ return kSentryDataCategoryNameProfileChunk; case kSentryDataCategoryMetricBucket: return kSentryDataCategoryNameMetricBucket; - case kSentryDataCategoryUnknown: - return kSentryDataCategoryNameUnknown; case kSentryDataCategoryReplay: return kSentryDataCategoryNameReplay; case kSentryDataCategorySpan: return kSentryDataCategoryNameSpan; + case kSentryDataCategoryFeedback: + return kSentryDataCategoryNameFeedback; + + default: // !!!: fall-through! + case kSentryDataCategoryUnknown: + return kSentryDataCategoryNameUnknown; } } diff --git a/Sources/Sentry/SentryUserFeedback.m b/Sources/Sentry/SentryUserFeedback.m index 307574a61f5..fc43f4bb2c8 100644 --- a/Sources/Sentry/SentryUserFeedback.m +++ b/Sources/Sentry/SentryUserFeedback.m @@ -2,26 +2,7 @@ #import "SentrySwift.h" #import -typedef enum : NSUInteger { - /** A user feedback attached to a transaction or error event. */ - kSentryUserFeedbackTypeAttached, - - /** A user feedback sent as its own event independent of any other event. */ - kSentryUserFeedbackTypeStandalone, -} SentryUserFeedbackType; - -@implementation SentryUserFeedback { - SentryUserFeedbackType _type; -} - -- (instancetype)init -{ - if (self = [super init]) { - _type = kSentryUserFeedbackTypeStandalone; - _eventId = [[SentryId alloc] init]; - } - return self; -} +@implementation SentryUserFeedback - (instancetype)initWithEventId:(SentryId *)eventId { @@ -30,26 +11,18 @@ - (instancetype)initWithEventId:(SentryId *)eventId _email = @""; _name = @""; _comments = @""; - _type = kSentryUserFeedbackTypeAttached; } return self; } - (NSDictionary *)serialize { - switch (_type) { - case kSentryUserFeedbackTypeAttached: - return @{ - @"event_id" : self.eventId.sentryIdString, - @"email" : self.email, - @"name" : self.name, - @"comments" : self.comments - }; - case kSentryUserFeedbackTypeStandalone: - return @{ - - }; - } + return @{ + @"event_id" : self.eventId.sentryIdString, + @"email" : self.email, + @"name" : self.name, + @"comments" : self.comments + }; } @end diff --git a/Sources/Sentry/include/SentryDataCategory.h b/Sources/Sentry/include/SentryDataCategory.h index 3a384add2cb..63b18153c1b 100644 --- a/Sources/Sentry/include/SentryDataCategory.h +++ b/Sources/Sentry/include/SentryDataCategory.h @@ -18,5 +18,6 @@ typedef NS_ENUM(NSUInteger, SentryDataCategory) { kSentryDataCategoryReplay = 9, kSentryDataCategoryProfileChunk = 10, kSentryDataCategorySpan = 11, - kSentryDataCategoryUnknown = 12, + kSentryDataCategoryFeedback = 12, + kSentryDataCategoryUnknown = 13, }; diff --git a/Sources/Sentry/include/SentrySDK+Private.h b/Sources/Sentry/include/SentrySDK+Private.h index bebbaf11e4c..26bb3a89eb5 100644 --- a/Sources/Sentry/include/SentrySDK+Private.h +++ b/Sources/Sentry/include/SentrySDK+Private.h @@ -77,8 +77,6 @@ NS_ASSUME_NONNULL_BEGIN + (void)showUserFeedbackForm; #endif // TARGET_OS_IOS && SENTRY_HAS_UIKIT -+ (void)captureFeedback:(SentryFeedback *)feedback; - @end NS_ASSUME_NONNULL_END diff --git a/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift b/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift index a4869523af0..7270944dd3d 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift @@ -4,7 +4,7 @@ import Foundation import UIKit /** - * A user feedback item that serializes to an envelope with the new format described at + * A user feedback item that serializes to an envelope with the format described at * https://develop.sentry.dev/application/feedback-architecture/#feedback-events. */ @objcMembers From de56160c99d2ccd7c6c4337dbcbfa6726753bcfd Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Wed, 20 Nov 2024 14:03:04 -0900 Subject: [PATCH 13/27] remove hints; ordering in init --- Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift | 4 +--- .../Integrations/UserFeedback/SentryUserFeedbackForm.swift | 4 +++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift b/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift index 7270944dd3d..47410685937 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift @@ -17,19 +17,17 @@ class SentryFeedback: NSObject, SentrySerializable { var name: String? var email: String? var message: String - var hints: [String: Any]? var source: Source let eventId: SentryId /// The event id that this feedback is associated with, like a crash report. var associatedEventId: String? - init(name: String?, email: String?, message: String, hints: [String: Any]? = nil, source: Source = .widget, associatedEventId: String? = nil) { + init(message: String, name: String?, email: String?, source: Source = .widget, associatedEventId: String? = nil) { self.eventId = SentryId() self.name = name self.email = email self.message = message - self.hints = hints self.source = source self.associatedEventId = associatedEventId super.init() diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift index 0ba8a9a63dd..43e640190c5 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift @@ -25,6 +25,7 @@ class SentryUserFeedbackForm: UIViewController { updateLayout() } + //swiftlint:disable function_body_length init(config: SentryUserFeedbackConfiguration, delegate: any SentryUserFeedbackFormDelegate) { self.config = config self.delegate = delegate @@ -82,6 +83,7 @@ class SentryUserFeedbackForm: UIViewController { $0.setTitleColor(config.theme.buttonForeground, for: .normal) } } + //swiftlint:enable function_body_length // MARK: Actions @@ -132,7 +134,7 @@ class SentryUserFeedbackForm: UIViewController { return } - let feedback = SentryFeedback(name: fullNameTextField.text, email: emailTextField.text, message: messageTextView.text, hints: nil) + let feedback = SentryFeedback(message: messageTextView.text, name: fullNameTextField.text, email: emailTextField.text) SentrySDK.capture(feedback: feedback) delegate?.finished() } From c128d60770e5814e5d7b4f7e8c2f20a192d592e3 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Thu, 5 Dec 2024 13:52:05 -0900 Subject: [PATCH 14/27] fix bad conflict resolution --- .../UserFeedback/SentryUserFeedbackWidget.swift | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift index 922fd236600..eac1226cd3a 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift @@ -72,13 +72,6 @@ struct SentryUserFeedbackWidget { closeForm() } -//swiftlint:disable todo - func captureFeedback(message: String, name: String?, email: String?, hints: [String : Any]?) { - closeForm() - self.delegate?.captureFeedback(message: message, name: name, email: email, hints: hints) - } -//swiftlint:enable todo - // MARK: UIAdaptivePresentationControllerDelegate func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { From 9dc92aa587ef375d53352c2f033f02c8345748d6 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Wed, 11 Dec 2024 17:09:37 -0900 Subject: [PATCH 15/27] refactor method to reuse for grabbing attachments for an event --- Sources/Sentry/SentryClient.m | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 6d921e2656f..8bd36bdb4b4 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -457,13 +457,7 @@ - (SentryId *)sendEvent:(SentryEvent *)event SentryTraceContext *traceContext = [self getTraceStateWithEvent:event withScope:scope]; - NSArray *attachments = scope.attachments; - if (self.attachmentProcessors.count) { - for (id attachmentProcessor in self.attachmentProcessors) { - attachments = [attachmentProcessor processAttachments:attachments - forEvent:preparedEvent]; - } - } + NSArray *attachments = [self attachmentsForEvent:preparedEvent scope:scope]; [self.transportAdapter sendEvent:preparedEvent traceContext:traceContext @@ -473,18 +467,23 @@ - (SentryId *)sendEvent:(SentryEvent *)event return preparedEvent.eventId; } +- (NSArray *)attachmentsForEvent:(SentryEvent *)event scope:(SentryScope *)scope +{ + NSArray *attachments = scope.attachments; + if (self.attachmentProcessors.count) { + for (id attachmentProcessor in self.attachmentProcessors) { + attachments = [attachmentProcessor processAttachments:attachments forEvent:event]; + } + } + return attachments; +} + - (SentryId *)sendEvent:(SentryEvent *)event withSession:(SentrySession *)session withScope:(SentryScope *)scope { if (nil != event) { - NSArray *attachments = scope.attachments; - if (self.attachmentProcessors.count) { - for (id attachmentProcessor in self - .attachmentProcessors) { - attachments = [attachmentProcessor processAttachments:attachments forEvent:event]; - } - } + NSArray *attachments = [self attachmentsForEvent:event scope:scope]; if (event.isCrashEvent && event.context[@"replay"] && [event.context[@"replay"] isKindOfClass:NSDictionary.class]) { From 1299f1a245083d091302d49302843b4f148094ac Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Wed, 11 Dec 2024 17:12:21 -0900 Subject: [PATCH 16/27] wip implementing new feedback envelope capture --- Sources/Sentry/SentryClient.m | 48 +++++++++-- Sources/Sentry/SentryEnvelope.m | 18 ---- Sources/Sentry/SentryHub.m | 2 +- Sources/Sentry/SentryTransportAdapter.m | 10 --- .../include/HybridPublic/SentryEnvelope.h | 1 - Sources/Sentry/include/SentryClient+Private.h | 3 +- .../Sentry/include/SentryTransportAdapter.h | 1 - .../UserFeedback/SentryFeedback.swift | 85 ++++++++++++++++++- 8 files changed, 127 insertions(+), 41 deletions(-) diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 8bd36bdb4b4..8b627666bdd 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -584,14 +584,48 @@ - (void)captureUserFeedback:(SentryUserFeedback *)userFeedback [self.transportAdapter sendUserFeedback:userFeedback]; } -- (void)captureFeedback:(SentryFeedback *)feedback +- (void)captureFeedback:(SentryFeedback *)feedback withScope:(SentryScope *)scope { - if ([self isDisabled]) { - [self logDisabledMessage]; - return; - } - - [self.transportAdapter sendFeedback:feedback]; + // what i was doing previously when directly sending the envelope. but, it appears i need to + // construct a dedicated event type and then package that inside an envelope + // SentryEnvelopeItem *item = [[SentryEnvelopeItem alloc] initWithFeedback:feedback] + // SentryEnvelopeHeader *envelopeHeader = + // [[SentryEnvelopeHeader alloc] initWithId:feedback.eventId]; + // NSArray *attachments = [feedback attachments]; + // NSArray *items = [@[item] arrayByAddingObjectsFromArray:attachments]; + // SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:envelopeHeader + // items:items]; + + SentryEvent *feedbackEvent = [[SentryEvent alloc] init]; + feedbackEvent.eventId = feedback.eventId; + feedbackEvent.type = SentryEnvelopeItemTypeFeedback; + + NSDictionary *serializedFeedback = [feedback serialize]; + + NSMutableDictionary *context = [NSMutableDictionary dictionary]; + context[@"feedback"] = serializedFeedback; + feedbackEvent.context = context; + + // the following is basically copied from the implementation for + // -[sendEvent:withScope:alwaysAttachStacktrace:isCrashEvent:additionalEnvelopeItems:]. we can't + // simply call through to that because the feedback screenshot must be send as an envelope + // attachment, but there are no other methods in this class that allow specifying attachments + // that aren't already on the scope, and i didn't want to attach the screenshot to the scope in + // case another event would also pick it up. the reference implementation does show a trace + // context so i thought it best to keep it here too + // https://github.com/getsentry/sentry-javascript/blob/be9edf161f72bb0b9ccf38d70297b798054b3ce3/packages/feedback/src/core/sendFeedback.ts#L107. + // it also shows a replay context, which i'm not sure how to retrieve yet in this sdk + + SentryEvent *preparedEvent = [self prepareEvent:feedbackEvent + withScope:scope + alwaysAttachStacktrace:NO]; + SentryTraceContext *traceContext = [self getTraceStateWithEvent:preparedEvent withScope:scope]; + NSArray *attachments = [[self attachmentsForEvent:preparedEvent scope:scope] + arrayByAddingObjectsFromArray:[feedback attachments]]; + [self.transportAdapter sendEvent:preparedEvent + traceContext:traceContext + attachments:attachments + additionalEnvelopeItems:@[]]; } - (void)storeEnvelope:(SentryEnvelope *)envelope diff --git a/Sources/Sentry/SentryEnvelope.m b/Sources/Sentry/SentryEnvelope.m index 72181c0afd7..7b63610d2ed 100644 --- a/Sources/Sentry/SentryEnvelope.m +++ b/Sources/Sentry/SentryEnvelope.m @@ -129,24 +129,6 @@ - (instancetype)initWithUserFeedback:(SentryUserFeedback *)userFeedback data:json]; } -- (instancetype)initWithFeedback:(SentryFeedback *)feedback -{ - NSError *error = nil; - NSData *json = [NSJSONSerialization dataWithJSONObject:[feedback serialize] - options:0 - error:&error]; - - if (nil != error) { - SENTRY_LOG_ERROR(@"Couldn't serialize feedback."); - json = [NSData new]; - } - - return [self - initWithHeader:[[SentryEnvelopeItemHeader alloc] initWithType:SentryEnvelopeItemTypeFeedback - length:json.length] - data:json]; -} - - (instancetype)initWithClientReport:(SentryClientReport *)clientReport { NSError *error = nil; diff --git a/Sources/Sentry/SentryHub.m b/Sources/Sentry/SentryHub.m index cc622a4a927..86f584962f7 100644 --- a/Sources/Sentry/SentryHub.m +++ b/Sources/Sentry/SentryHub.m @@ -508,7 +508,7 @@ - (void)captureFeedback:(SentryFeedback *)feedback { SentryClient *client = _client; if (client != nil) { - [client captureFeedback:feedback]; + [client captureFeedback:feedback withScope:self.scope]; } } diff --git a/Sources/Sentry/SentryTransportAdapter.m b/Sources/Sentry/SentryTransportAdapter.m index 6ed7c8eee1b..6a683330f0e 100644 --- a/Sources/Sentry/SentryTransportAdapter.m +++ b/Sources/Sentry/SentryTransportAdapter.m @@ -103,16 +103,6 @@ - (void)sendUserFeedback:(SentryUserFeedback *)userFeedback [self sendEnvelope:envelope]; } -- (void)sendFeedback:(SentryFeedback *)feedback -{ - SentryEnvelopeItem *item = [[SentryEnvelopeItem alloc] initWithFeedback:feedback]; - SentryEnvelopeHeader *envelopeHeader = - [[SentryEnvelopeHeader alloc] initWithId:feedback.eventId]; - SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:envelopeHeader - singleItem:item]; - [self sendEnvelope:envelope]; -} - - (void)sendEnvelope:(SentryEnvelope *)envelope { for (id transport in self.transports) { diff --git a/Sources/Sentry/include/HybridPublic/SentryEnvelope.h b/Sources/Sentry/include/HybridPublic/SentryEnvelope.h index dca89098901..8414cc16510 100644 --- a/Sources/Sentry/include/HybridPublic/SentryEnvelope.h +++ b/Sources/Sentry/include/HybridPublic/SentryEnvelope.h @@ -92,7 +92,6 @@ SENTRY_NO_INIT - (instancetype)initWithEvent:(SentryEvent *)event; - (instancetype)initWithSession:(SentrySession *)session; - (instancetype)initWithUserFeedback:(SentryUserFeedback *)userFeedback; -- (instancetype)initWithFeedback:(SentryFeedback *)feedback; - (_Nullable instancetype)initWithAttachment:(SentryAttachment *)attachment maxAttachmentSize:(NSUInteger)maxAttachmentSize; - (instancetype)initWithHeader:(SentryEnvelopeItemHeader *)header diff --git a/Sources/Sentry/include/SentryClient+Private.h b/Sources/Sentry/include/SentryClient+Private.h index e3fc83b8c5c..61dff8034f7 100644 --- a/Sources/Sentry/include/SentryClient+Private.h +++ b/Sources/Sentry/include/SentryClient+Private.h @@ -78,7 +78,8 @@ NS_ASSUME_NONNULL_BEGIN * Captures a new-style user feedback and sends it to Sentry. * @param feedback The user feedback to send to Sentry. */ -- (void)captureFeedback:(SentryFeedback *)feedback NS_SWIFT_NAME(capture(feedback:)); +- (void)captureFeedback:(SentryFeedback *)feedback + withScope:(SentryScope *)scope NS_SWIFT_NAME(capture(feedback:scope:)); @end diff --git a/Sources/Sentry/include/SentryTransportAdapter.h b/Sources/Sentry/include/SentryTransportAdapter.h index 1db0e83dd39..c94bd77ad42 100644 --- a/Sources/Sentry/include/SentryTransportAdapter.h +++ b/Sources/Sentry/include/SentryTransportAdapter.h @@ -49,7 +49,6 @@ SENTRY_NO_INIT - (void)storeEvent:(SentryEvent *)event traceContext:(nullable SentryTraceContext *)traceContext; - (void)sendUserFeedback:(SentryUserFeedback *)userFeedback NS_SWIFT_NAME(send(userFeedback:)); -- (void)sendFeedback:(SentryFeedback *)feedback NS_SWIFT_NAME(send(feedback:)); - (void)sendEnvelope:(SentryEnvelope *)envelope NS_SWIFT_NAME(send(envelope:)); diff --git a/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift b/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift index 47410685937..36433d7dcac 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift @@ -5,7 +5,63 @@ import UIKit /** * A user feedback item that serializes to an envelope with the format described at - * https://develop.sentry.dev/application/feedback-architecture/#feedback-events. + * https://develop.sentry.dev/application-architecture/feedback-architecture/#feedback-events. + * - seealso: Reference implementation: https://github.com/getsentry/sentry-javascript/blob/be9edf161f72bb0b9ccf38d70297b798054b3ce3/packages/feedback/src/core/sendFeedback.ts#L77-L116 + * + * Schema of the event envelope: + * + ``` + event[”contexts”][”feedback”] = { + "name": , + "contact_email": , + "message": , + "url": , + "source": , + "associated_event_id": + ``` + * A more complete example from the javascript reference implementation: + ``` + { + "type": "feedback", + "event_id": "d2132d31b39445f1938d7e21b6bf0ec4", + "timestamp": 1597977777.6189718, + "dist": "1.12", + "platform": "javascript", + "environment": "production", + "release": 42, + "tags": {"transaction": "/organizations/:orgId/performance/:eventSlug/"}, + "sdk": {"name": "name", "version": "version"}, + "user": { + "id": "123", + "username": "user", + "email": "user@site.com", + "ip_address": "192.168.11.12", + }, + "request": { + "url": None, + "headers": { + "user-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15" + }, + }, + "contexts": { + "feedback": { + "message": "test message", + "contact_email": "test@example.com", + "type": "feedback", + }, + "trace": { + "trace_id": "4C79F60C11214EB38604F4AE0781BFB2", + "span_id": "FA90FDEAD5F74052", + "type": "trace", + }, + "replay": { + "replay_id": "e2d42047b1c5431c8cba85ee2a8ab25d", + }, + }, + } + ``` + * - note: screenshots are provided as envelope attachments, at most 1 per feedback. +} */ @objcMembers class SentryFeedback: NSObject, SentrySerializable { @@ -20,16 +76,21 @@ class SentryFeedback: NSObject, SentrySerializable { var source: Source let eventId: SentryId + /// PNG data for the screenshot image + var screenshot: Data? + /// The event id that this feedback is associated with, like a crash report. var associatedEventId: String? - init(message: String, name: String?, email: String?, source: Source = .widget, associatedEventId: String? = nil) { + /// - parameter screenshot Image encoded as PNG data. + init(message: String, name: String?, email: String?, source: Source = .widget, associatedEventId: String? = nil, screenshot: Data? = nil) { self.eventId = SentryId() self.name = name self.email = email self.message = message self.source = source self.associatedEventId = associatedEventId + self.screenshot = screenshot super.init() } @@ -50,6 +111,26 @@ class SentryFeedback: NSObject, SentrySerializable { return dict } + + /** + * - note: Currently there is only a single attachment possible, for the screenshot, of which there can be only one. + * - seealso: Reference implementation: https://github.com/getsentry/sentry-javascript/blob/be9edf161f72bb0b9ccf38d70297b798054b3ce3/packages/feedback/src/screenshot/integration.ts#L31-L36 + ``` + const attachment: Attachment = { + data, + filename: 'screenshot.png', + contentType: 'application/png', + // attachmentType?: string; + }; + ``` + */ + func attachments() -> [Attachment] { + var items = [Attachment]() + if let screenshot = screenshot { + items.append(Attachment(data: screenshot, filename: "screenshot.png", contentType: "application/png")) + } + return items + } } #endif // os(iOS) && !SENTRY_NO_UIKIT From 00ef3dd46cd316712d00d71bfb5101cde143485a Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Thu, 12 Dec 2024 17:02:48 -0900 Subject: [PATCH 17/27] necessary tweaks to events/envelopes to get feedback events ingested and showing up in the dashboard --- Sources/Sentry/SentryClient.m | 7 +++++-- Sources/Sentry/SentryEnvelope.m | 2 ++ .../Integrations/UserFeedback/SentryUserFeedbackForm.swift | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 8b627666bdd..56bf6de6995 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -686,6 +686,8 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event = event.type == nil || ![event.type isEqualToString:SentryEnvelopeItemTypeTransaction]; BOOL eventIsNotReplay = event.type == nil || ![event.type isEqualToString:SentryEnvelopeItemTypeReplayVideo]; + BOOL eventIsNotUserFeedback + = event.type == nil || ![event.type isEqualToString:SentryEnvelopeItemTypeFeedback]; // Transactions and replays have their own sampleRate if (eventIsNotATransaction && eventIsNotReplay && [self isSampled:self.options.sampleRate]) { @@ -715,8 +717,9 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event [self setSdk:event]; - // We don't want to attach debug meta and stacktraces for transactions and replays. - if (eventIsNotATransaction && eventIsNotReplay) { + // We don't want to attach debug meta and stacktraces for transactions, replays or user + // feedback. + if (eventIsNotATransaction && eventIsNotReplay && eventIsNotUserFeedback) { BOOL shouldAttachStacktrace = alwaysAttachStacktrace || self.options.attachStacktrace || (nil != event.exceptions && [event.exceptions count] > 0); diff --git a/Sources/Sentry/SentryEnvelope.m b/Sources/Sentry/SentryEnvelope.m index 7b63610d2ed..9a4fce2bb30 100644 --- a/Sources/Sentry/SentryEnvelope.m +++ b/Sources/Sentry/SentryEnvelope.m @@ -95,6 +95,8 @@ - (instancetype)initWithEvent:(SentryEvent *)event // default. In any case in the envelope type it should be event. Except for transactions NSString *envelopeType = [event.type isEqualToString:SentryEnvelopeItemTypeTransaction] ? SentryEnvelopeItemTypeTransaction + : [event.type isEqualToString:SentryEnvelopeItemTypeFeedback] + ? SentryEnvelopeItemTypeFeedback : SentryEnvelopeItemTypeEvent; return [self initWithHeader:[[SentryEnvelopeItemHeader alloc] initWithType:envelopeType diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift index 43e640190c5..a95b493a093 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift @@ -134,7 +134,7 @@ class SentryUserFeedbackForm: UIViewController { return } - let feedback = SentryFeedback(message: messageTextView.text, name: fullNameTextField.text, email: emailTextField.text) + let feedback = SentryFeedback(message: messageTextView.text, name: fullNameTextField.text, email: emailTextField.text, screenshot: screenshotImageView.image?.pngData()) SentrySDK.capture(feedback: feedback) delegate?.finished() } From 5cb9ada04d0bcbe608198d6c444362a41257289b Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Thu, 12 Dec 2024 17:30:10 -0900 Subject: [PATCH 18/27] remove some comments to pr/issue --- Sources/Sentry/SentryClient.m | 20 ------ .../UserFeedback/SentryFeedback.swift | 69 ------------------- 2 files changed, 89 deletions(-) diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 56bf6de6995..5800167628b 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -586,16 +586,6 @@ - (void)captureUserFeedback:(SentryUserFeedback *)userFeedback - (void)captureFeedback:(SentryFeedback *)feedback withScope:(SentryScope *)scope { - // what i was doing previously when directly sending the envelope. but, it appears i need to - // construct a dedicated event type and then package that inside an envelope - // SentryEnvelopeItem *item = [[SentryEnvelopeItem alloc] initWithFeedback:feedback] - // SentryEnvelopeHeader *envelopeHeader = - // [[SentryEnvelopeHeader alloc] initWithId:feedback.eventId]; - // NSArray *attachments = [feedback attachments]; - // NSArray *items = [@[item] arrayByAddingObjectsFromArray:attachments]; - // SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:envelopeHeader - // items:items]; - SentryEvent *feedbackEvent = [[SentryEvent alloc] init]; feedbackEvent.eventId = feedback.eventId; feedbackEvent.type = SentryEnvelopeItemTypeFeedback; @@ -606,16 +596,6 @@ - (void)captureFeedback:(SentryFeedback *)feedback withScope:(SentryScope *)scop context[@"feedback"] = serializedFeedback; feedbackEvent.context = context; - // the following is basically copied from the implementation for - // -[sendEvent:withScope:alwaysAttachStacktrace:isCrashEvent:additionalEnvelopeItems:]. we can't - // simply call through to that because the feedback screenshot must be send as an envelope - // attachment, but there are no other methods in this class that allow specifying attachments - // that aren't already on the scope, and i didn't want to attach the screenshot to the scope in - // case another event would also pick it up. the reference implementation does show a trace - // context so i thought it best to keep it here too - // https://github.com/getsentry/sentry-javascript/blob/be9edf161f72bb0b9ccf38d70297b798054b3ce3/packages/feedback/src/core/sendFeedback.ts#L107. - // it also shows a replay context, which i'm not sure how to retrieve yet in this sdk - SentryEvent *preparedEvent = [self prepareEvent:feedbackEvent withScope:scope alwaysAttachStacktrace:NO]; diff --git a/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift b/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift index 36433d7dcac..b4fb03a149a 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift @@ -3,66 +3,6 @@ import Foundation @_implementationOnly import _SentryPrivate import UIKit -/** - * A user feedback item that serializes to an envelope with the format described at - * https://develop.sentry.dev/application-architecture/feedback-architecture/#feedback-events. - * - seealso: Reference implementation: https://github.com/getsentry/sentry-javascript/blob/be9edf161f72bb0b9ccf38d70297b798054b3ce3/packages/feedback/src/core/sendFeedback.ts#L77-L116 - * - * Schema of the event envelope: - * - ``` - event[”contexts”][”feedback”] = { - "name": , - "contact_email": , - "message": , - "url": , - "source": , - "associated_event_id": - ``` - * A more complete example from the javascript reference implementation: - ``` - { - "type": "feedback", - "event_id": "d2132d31b39445f1938d7e21b6bf0ec4", - "timestamp": 1597977777.6189718, - "dist": "1.12", - "platform": "javascript", - "environment": "production", - "release": 42, - "tags": {"transaction": "/organizations/:orgId/performance/:eventSlug/"}, - "sdk": {"name": "name", "version": "version"}, - "user": { - "id": "123", - "username": "user", - "email": "user@site.com", - "ip_address": "192.168.11.12", - }, - "request": { - "url": None, - "headers": { - "user-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15" - }, - }, - "contexts": { - "feedback": { - "message": "test message", - "contact_email": "test@example.com", - "type": "feedback", - }, - "trace": { - "trace_id": "4C79F60C11214EB38604F4AE0781BFB2", - "span_id": "FA90FDEAD5F74052", - "type": "trace", - }, - "replay": { - "replay_id": "e2d42047b1c5431c8cba85ee2a8ab25d", - }, - }, - } - ``` - * - note: screenshots are provided as envelope attachments, at most 1 per feedback. -} - */ @objcMembers class SentryFeedback: NSObject, SentrySerializable { enum Source: String { @@ -114,15 +54,6 @@ class SentryFeedback: NSObject, SentrySerializable { /** * - note: Currently there is only a single attachment possible, for the screenshot, of which there can be only one. - * - seealso: Reference implementation: https://github.com/getsentry/sentry-javascript/blob/be9edf161f72bb0b9ccf38d70297b798054b3ce3/packages/feedback/src/screenshot/integration.ts#L31-L36 - ``` - const attachment: Attachment = { - data, - filename: 'screenshot.png', - contentType: 'application/png', - // attachmentType?: string; - }; - ``` */ func attachments() -> [Attachment] { var items = [Attachment]() From 4dcee7a9b88d971f0a4bd450fd2bf0057b988fb0 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Thu, 12 Dec 2024 17:33:19 -0900 Subject: [PATCH 19/27] ios-only build requirements needed some places, but actually not for SentryFeedback --- Sources/Sentry/SentrySDK.m | 2 ++ Sources/Sentry/include/SentryOptions+Private.h | 2 ++ .../Swift/Integrations/UserFeedback/SentryFeedback.swift | 6 +----- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/Sentry/SentrySDK.m b/Sources/Sentry/SentrySDK.m index 1eee3e66fa8..44632a8125a 100644 --- a/Sources/Sentry/SentrySDK.m +++ b/Sources/Sentry/SentrySDK.m @@ -410,10 +410,12 @@ + (void)captureFeedback:(SentryFeedback *)feedback [SentrySDK.currentHub captureFeedback:feedback]; } +#if TARGET_OS_IOS && SENTRY_HAS_UIKIT + (void)showUserFeedbackForm { // TODO: implement } +#endif // TARGET_OS_IOS && SENTRY_HAS_UIKIT + (void)addBreadcrumb:(SentryBreadcrumb *)crumb { diff --git a/Sources/Sentry/include/SentryOptions+Private.h b/Sources/Sentry/include/SentryOptions+Private.h index 7748f635764..932417b530a 100644 --- a/Sources/Sentry/include/SentryOptions+Private.h +++ b/Sources/Sentry/include/SentryOptions+Private.h @@ -21,6 +21,7 @@ FOUNDATION_EXPORT NSString *const kSentryDefaultEnvironment; - (BOOL)isContinuousProfilingEnabled; #endif // SENTRY_TARGET_PROFILING_SUPPORTED +#if TARGET_OS_IOS && SENTRY_HAS_UIKIT /** * A block that can be defined that receives a user feedback configuration object to modify. * @warning This is an experimental feature and may still have bugs. @@ -31,6 +32,7 @@ FOUNDATION_EXPORT NSString *const kSentryDefaultEnvironment; */ @property (nonatomic, copy, nullable) SentryUserFeedbackConfigurationBlock configureUserFeedback API_AVAILABLE(ios(13.0)); +#endif // TARGET_OS_IOS && SENTRY_HAS_UIKIT @property (nonatomic, readonly, class) NSArray *defaultIntegrationClasses; diff --git a/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift b/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift index b4fb03a149a..7c14dc28e36 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift @@ -1,7 +1,5 @@ -import Foundation -#if os(iOS) && !SENTRY_NO_UIKIT @_implementationOnly import _SentryPrivate -import UIKit +import Foundation @objcMembers class SentryFeedback: NSObject, SentrySerializable { @@ -63,5 +61,3 @@ class SentryFeedback: NSObject, SentrySerializable { return items } } - -#endif // os(iOS) && !SENTRY_NO_UIKIT From 64ab6604c1fe4b6b650c792e05215a9d828695b9 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Thu, 12 Dec 2024 18:15:43 -0900 Subject: [PATCH 20/27] add serialization test --- Sentry.xcodeproj/project.pbxproj | 8 ++++++++ .../Feedback/SentryFeedbackTests.swift | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 Tests/SentryTests/Integrations/Feedback/SentryFeedbackTests.swift diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 93a8c527874..5a234e8349b 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -2034,6 +2034,10 @@ D8FFE50B2703DAAE00607131 /* SwizzlingCallTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwizzlingCallTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 843FB3142D0BDA3900558F18 /* Feedback */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Feedback; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 63AA75971EB8AEF500D153DE /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -2959,6 +2963,7 @@ 7B944FA924697E9700A10721 /* Integrations */ = { isa = PBXGroup; children = ( + 843FB3142D0BDA3900558F18 /* Feedback */, 7BF6505D292B77D100BBA5A8 /* MetricKit */, D808FB85281AB2EF009A2A33 /* UIEvents */, D8AB40D92806EBDC00E5E9F7 /* Screenshot */, @@ -4351,6 +4356,9 @@ D84DAD5C2B1742C1003CF120 /* PBXTargetDependency */, D85153032CA2B5F60070F669 /* PBXTargetDependency */, ); + fileSystemSynchronizedGroups = ( + 843FB3142D0BDA3900558F18 /* Feedback */, + ); name = SentryTests; packageProductDependencies = ( ); diff --git a/Tests/SentryTests/Integrations/Feedback/SentryFeedbackTests.swift b/Tests/SentryTests/Integrations/Feedback/SentryFeedbackTests.swift new file mode 100644 index 00000000000..43730639a34 --- /dev/null +++ b/Tests/SentryTests/Integrations/Feedback/SentryFeedbackTests.swift @@ -0,0 +1,20 @@ +import Foundation +@testable import Sentry +import XCTest + +class SentryFeedbackTests: XCTestCase { + func testSerialize() throws { + let sut = SentryFeedback(message: "Test feedback message", name: "Test feedback provider", email: "test-feedback-provider@sentry.io", screenshot: Data()) + + let serialization = sut.serialize() + XCTAssertEqual(try XCTUnwrap(serialization["message"] as? String), "Test feedback message") + XCTAssertEqual(try XCTUnwrap(serialization["name"] as? String), "Test feedback provider") + XCTAssertEqual(try XCTUnwrap(serialization["contact_email"] as? String), "test-feedback-provider@sentry.io") + XCTAssertEqual(try XCTUnwrap(serialization["source"] as? String), "widget") + + let attachments = sut.attachments() + XCTAssertEqual(attachments.count, 1) + XCTAssertEqual(try XCTUnwrap(attachments.first).filename, "screenshot.png") + XCTAssertEqual(try XCTUnwrap(attachments.first).contentType, "application/png") + } +} From df7f17708b63ca6bdc65648530a638fd1303baba Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Wed, 18 Dec 2024 12:35:27 -0900 Subject: [PATCH 21/27] pr feedback --- Sources/Sentry/SentryClient.m | 11 ++++++++++- Sources/Sentry/SentryTransportAdapter.m | 1 - Sources/Sentry/include/SentryDataCategoryMapper.h | 1 + .../UserFeedback/SentryFeedback.swift | 6 +++--- .../Feedback/SentryFeedbackTests.swift | 15 ++++++++++++++- .../SentryDataCategoryMapperTests.swift | 6 +++++- 6 files changed, 33 insertions(+), 7 deletions(-) diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 5800167628b..16492d7a09f 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -592,8 +592,16 @@ - (void)captureFeedback:(SentryFeedback *)feedback withScope:(SentryScope *)scop NSDictionary *serializedFeedback = [feedback serialize]; - NSMutableDictionary *context = [NSMutableDictionary dictionary]; + NSUInteger optionalItems = (scope.span == nil ? 0 : 1) + (scope.replayId == nil ? 0 : 1); + NSMutableDictionary *context = [NSMutableDictionary dictionaryWithCapacity:1 + optionalItems]; context[@"feedback"] = serializedFeedback; + + if (scope.replayId != nil) { + NSMutableDictionary *replayContext = [NSMutableDictionary dictionaryWithCapacity:1]; + replayContext[@"replay_id"] = scope.replayId; + context[@"replay"] = replayContext; + } + feedbackEvent.context = context; SentryEvent *preparedEvent = [self prepareEvent:feedbackEvent @@ -602,6 +610,7 @@ - (void)captureFeedback:(SentryFeedback *)feedback withScope:(SentryScope *)scop SentryTraceContext *traceContext = [self getTraceStateWithEvent:preparedEvent withScope:scope]; NSArray *attachments = [[self attachmentsForEvent:preparedEvent scope:scope] arrayByAddingObjectsFromArray:[feedback attachments]]; + [self.transportAdapter sendEvent:preparedEvent traceContext:traceContext attachments:attachments diff --git a/Sources/Sentry/SentryTransportAdapter.m b/Sources/Sentry/SentryTransportAdapter.m index 6a683330f0e..0a1027430bf 100644 --- a/Sources/Sentry/SentryTransportAdapter.m +++ b/Sources/Sentry/SentryTransportAdapter.m @@ -2,7 +2,6 @@ #import "SentryEnvelope.h" #import "SentryEvent.h" #import "SentryOptions.h" -#import "SentrySwift.h" #import "SentryUserFeedback.h" #import diff --git a/Sources/Sentry/include/SentryDataCategoryMapper.h b/Sources/Sentry/include/SentryDataCategoryMapper.h index 677996907e2..3f9b6bbddc8 100644 --- a/Sources/Sentry/include/SentryDataCategoryMapper.h +++ b/Sources/Sentry/include/SentryDataCategoryMapper.h @@ -13,6 +13,7 @@ FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameUserFeedback; FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameProfile; FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameProfileChunk; FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameReplay; +FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameFeedback; FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameMetricBucket; FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameSpan; FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameUnknown; diff --git a/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift b/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift index 7c14dc28e36..33140e1e203 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift @@ -33,9 +33,9 @@ class SentryFeedback: NSObject, SentrySerializable { } func serialize() -> [String: Any] { - var dict: [String: Any] = [ - "message": message - ] + let numberOfOptionalItems = (name == nil ? 0 : 1) + (email == nil ? 0 : 1) + (associatedEventId == nil ? 0 : 1) + var dict = [String: Any](minimumCapacity: 2 + numberOfOptionalItems) + dict["message"] = message if let name = name { dict["name"] = name } diff --git a/Tests/SentryTests/Integrations/Feedback/SentryFeedbackTests.swift b/Tests/SentryTests/Integrations/Feedback/SentryFeedbackTests.swift index 43730639a34..001279d7c51 100644 --- a/Tests/SentryTests/Integrations/Feedback/SentryFeedbackTests.swift +++ b/Tests/SentryTests/Integrations/Feedback/SentryFeedbackTests.swift @@ -3,7 +3,7 @@ import Foundation import XCTest class SentryFeedbackTests: XCTestCase { - func testSerialize() throws { + func testSerializeWithAllFields() throws { let sut = SentryFeedback(message: "Test feedback message", name: "Test feedback provider", email: "test-feedback-provider@sentry.io", screenshot: Data()) let serialization = sut.serialize() @@ -17,4 +17,17 @@ class SentryFeedbackTests: XCTestCase { XCTAssertEqual(try XCTUnwrap(attachments.first).filename, "screenshot.png") XCTAssertEqual(try XCTUnwrap(attachments.first).contentType, "application/png") } + + func testSerializeWithNoOptionalFields() throws { + let sut = SentryFeedback(message: "Test feedback message", name: nil, email: nil) + + let serialization = sut.serialize() + XCTAssertEqual(try XCTUnwrap(serialization["message"] as? String), "Test feedback message") + XCTAssertNil(serialization["name"]) + XCTAssertNil(serialization["contact_email"]) + XCTAssertEqual(try XCTUnwrap(serialization["source"] as? String), "widget") + + let attachments = sut.attachments() + XCTAssertEqual(attachments.count, 0) + } } diff --git a/Tests/SentryTests/Networking/SentryDataCategoryMapperTests.swift b/Tests/SentryTests/Networking/SentryDataCategoryMapperTests.swift index cb0b6d6099b..1fc9a8999a5 100644 --- a/Tests/SentryTests/Networking/SentryDataCategoryMapperTests.swift +++ b/Tests/SentryTests/Networking/SentryDataCategoryMapperTests.swift @@ -12,6 +12,7 @@ class SentryDataCategoryMapperTests: XCTestCase { XCTAssertEqual(sentryDataCategoryForEnvelopItemType("profile_chunk"), .profileChunk) XCTAssertEqual(sentryDataCategoryForEnvelopItemType("statsd"), .metricBucket) XCTAssertEqual(sentryDataCategoryForEnvelopItemType("replay_video"), .replay) + XCTAssertEqual(sentryDataCategoryForEnvelopItemType("feedback"), .feedback) XCTAssertEqual(sentryDataCategoryForEnvelopItemType("unknown item type"), .default) } @@ -28,7 +29,8 @@ class SentryDataCategoryMapperTests: XCTestCase { XCTAssertEqual(sentryDataCategoryForNSUInteger(9), .replay) XCTAssertEqual(sentryDataCategoryForNSUInteger(10), .profileChunk) XCTAssertEqual(sentryDataCategoryForNSUInteger(11), .span) - XCTAssertEqual(sentryDataCategoryForNSUInteger(12), .unknown) + XCTAssertEqual(sentryDataCategoryForNSUInteger(12), .feedback) + XCTAssertEqual(sentryDataCategoryForNSUInteger(13), .unknown) XCTAssertEqual(.unknown, sentryDataCategoryForNSUInteger(13), "Failed to map unknown category number to case .unknown") } @@ -45,6 +47,7 @@ class SentryDataCategoryMapperTests: XCTestCase { XCTAssertEqual(sentryDataCategoryForString(kSentryDataCategoryNameProfileChunk), .profileChunk) XCTAssertEqual(sentryDataCategoryForString(kSentryDataCategoryNameMetricBucket), .metricBucket) XCTAssertEqual(sentryDataCategoryForString(kSentryDataCategoryNameReplay), .replay) + XCTAssertEqual(sentryDataCategoryForString(kSentryDataCategoryNameFeedback), .feedback) XCTAssertEqual(sentryDataCategoryForString(kSentryDataCategoryNameSpan), .span) XCTAssertEqual(sentryDataCategoryForString(kSentryDataCategoryNameUnknown), .unknown) @@ -63,6 +66,7 @@ class SentryDataCategoryMapperTests: XCTestCase { XCTAssertEqual(nameForSentryDataCategory(.profileChunk), kSentryDataCategoryNameProfileChunk) XCTAssertEqual(nameForSentryDataCategory(.metricBucket), kSentryDataCategoryNameMetricBucket) XCTAssertEqual(nameForSentryDataCategory(.replay), kSentryDataCategoryNameReplay) + XCTAssertEqual(nameForSentryDataCategory(.feedback), kSentryDataCategoryNameFeedback) XCTAssertEqual(nameForSentryDataCategory(.span), kSentryDataCategoryNameSpan) XCTAssertEqual(nameForSentryDataCategory(.unknown), kSentryDataCategoryNameUnknown) } From 5d1ff6e3e59575db88c6c8366ee2a57b852467d7 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Wed, 18 Dec 2024 13:06:41 -0900 Subject: [PATCH 22/27] wip: intercepting cached envelopes to validate in UI tests --- .../UserFeedbackUITests.swift | 23 ++++++++++ .../iOS-Swift/Base.lproj/Main.storyboard | 42 ++++++++++++------- .../iOS-Swift/ExtraViewController.swift | 27 ++++++++++++ Sentry.xcodeproj/project.pbxproj | 6 ++- SentryTestUtils/TestStoreOnlyTransport.swift | 24 +++++++++++ SentryTestUtils/TestTransportAdapter.swift | 4 -- .../Sentry/SentryQueueableRequestManager.m | 9 ++++ 7 files changed, 114 insertions(+), 21 deletions(-) create mode 100644 SentryTestUtils/TestStoreOnlyTransport.swift diff --git a/Samples/iOS-Swift/iOS-Swift-UITests/UserFeedbackUITests.swift b/Samples/iOS-Swift/iOS-Swift-UITests/UserFeedbackUITests.swift index a5f1eb50b69..4b41f98aea6 100644 --- a/Samples/iOS-Swift/iOS-Swift-UITests/UserFeedbackUITests.swift +++ b/Samples/iOS-Swift/iOS-Swift-UITests/UserFeedbackUITests.swift @@ -134,6 +134,11 @@ class UserFeedbackUITests: BaseUITest { XCTAssertEqual(try XCTUnwrap(emailField.value as? String), "your.email@example.org") 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 } func testSubmitWithOnlyRequiredFieldsFilled() { @@ -333,6 +338,24 @@ class UserFeedbackUITests: BaseUITest { XCTAssert(widgetButton.waitForExistence(timeout: 1)) } + // MARK: Tests validating configuration hooks + + func testOnFormOpen() { + + } + + func testOnFormClosed() { + + } + + func testOnSubmitSuccess() { + + } + + func testOnSubmitError() { + + } + // MARK: Private var cancelButton: XCUIElement { diff --git a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard index 30fe801f7c5..692fe31a42c 100644 --- a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard +++ b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard @@ -903,10 +903,10 @@ - + - + + - + + + + + + + + + + + + + @@ -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 692fe31a42c..9d20ad7e705 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 dc839476a51..29955471085 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 a9dfec89546..9b0f6f52818 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 ae71cf89dc4..72c13ca8baf 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 985194c2a9c..d70a95d7da4 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/Sentry/SentryUserFeedbackIntegration.m b/Sources/Sentry/SentryUserFeedbackIntegration.m index 9bf8b13dcfb..8f213dbad7f 100644 --- a/Sources/Sentry/SentryUserFeedbackIntegration.m +++ b/Sources/Sentry/SentryUserFeedbackIntegration.m @@ -1,9 +1,13 @@ #import "SentryUserFeedbackIntegration.h" #import "SentryOptions+Private.h" +#import "SentrySDK+Private.h" #import "SentrySwift.h" #if TARGET_OS_IOS && SENTRY_HAS_UIKIT +@interface SentryUserFeedbackIntegration () +@end + @implementation SentryUserFeedbackIntegration { SentryUserFeedbackIntegrationDriver *_driver; } @@ -15,10 +19,18 @@ - (BOOL)installWithOptions:(SentryOptions *)options } _driver = [[SentryUserFeedbackIntegrationDriver alloc] - initWithConfiguration:options.userFeedbackConfiguration]; + initWithConfiguration:options.userFeedbackConfiguration + delegate:self]; return YES; } +// MARK: SentryUserFeedbackIntegrationDriverDelegate + +- (void)captureWithFeedback:(SentryFeedback *)feedback +{ + [SentrySDK captureFeedback:feedback]; +} + @end #endif // TARGET_OS_IOS && SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/include/SentryPrivate.h b/Sources/Sentry/include/SentryPrivate.h index a9e4eb9cbd3..27c7ad8d640 100644 --- a/Sources/Sentry/include/SentryPrivate.h +++ b/Sources/Sentry/include/SentryPrivate.h @@ -4,8 +4,6 @@ #import "SentryDispatchQueueWrapper.h" #import "SentryNSDataUtils.h" #import "SentryRandom.h" -#import "SentrySDK+Private.h" -#import "SentryScope+Private.h" #import "SentryTime.h" #import "SentryUserAccess.h" diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift index 4891586afc3..4e591f080ab 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackForm.swift @@ -8,7 +8,7 @@ import UIKit @available(iOS 13.0, *) protocol SentryUserFeedbackFormDelegate: NSObjectProtocol { - func finished() + func finished(with feedback: SentryFeedback?) } @available(iOS 13.0, *) @@ -135,12 +135,12 @@ class SentryUserFeedbackForm: UIViewController { } let feedback = SentryFeedback(message: messageTextView.text, name: fullNameTextField.text, email: emailTextField.text, screenshot: screenshotImageView.image?.pngData()) - SentrySDK.capture(feedback: feedback) - delegate?.finished() + SentryLog.log(message: "Sending user feedback", andLevel: .debug) + delegate?.finished(with: feedback) } func cancelButtonTapped() { - delegate?.finished() + delegate?.finished(with: nil) } // MARK: Layout diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift index 6e7b887a310..efda466e6e2 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift @@ -3,18 +3,25 @@ import Foundation @_implementationOnly import _SentryPrivate import UIKit +@available(iOS 13.0, *) @objc +protocol SentryUserFeedbackIntegrationDriverDelegate: NSObjectProtocol { + func capture(feedback: SentryFeedback) +} + /** * An integration managing a workflow for end users to report feedback via Sentry. * - note: The default method to show the feedback form is via a floating widget placed in the bottom trailing corner of the screen. See the configuration classes for alternative options. */ @available(iOS 13.0, *) @objcMembers -class SentryUserFeedbackIntegrationDriver: NSObject { +class SentryUserFeedbackIntegrationDriver: NSObject, SentryUserFeedbackWidgetDelegate { let configuration: SentryUserFeedbackConfiguration private var window: SentryUserFeedbackWidget.Window? + weak var delegate: (any SentryUserFeedbackIntegrationDriverDelegate)? - public init(configuration: SentryUserFeedbackConfiguration) { + public init(configuration: SentryUserFeedbackConfiguration, delegate: any SentryUserFeedbackIntegrationDriverDelegate) { self.configuration = configuration + self.delegate = delegate super.init() if let widgetConfigBuilder = configuration.configureWidget { @@ -49,7 +56,7 @@ class SentryUserFeedbackIntegrationDriver: NSObject { * If `SentryUserFeedbackConfiguration.autoInject` is `false`, this must be called explicitly. */ func createWidget() { - window = SentryUserFeedbackWidget.Window(config: configuration) + window = SentryUserFeedbackWidget.Window(config: configuration, delegate: self) window?.isHidden = false } @@ -78,6 +85,12 @@ class SentryUserFeedbackIntegrationDriver: NSObject { SentryLog.warning("Invalid widget location specified: \(config.location). Must specify either one edge or one corner of the screen rect to place the widget.") } } + + // MARK: SentryUserFeedbackWidgetDelegate + + func capture(feedback: SentryFeedback) { + delegate?.capture(feedback: feedback) + } } #endif // os(iOS) && !SENTRY_NO_UIKIT diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift index eac1226cd3a..99e218db662 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackWidget.swift @@ -7,6 +7,10 @@ import UIKit var displayingForm = false +protocol SentryUserFeedbackWidgetDelegate: NSObjectProtocol { + func capture(feedback: SentryFeedback) +} + @available(iOS 13.0, *) struct SentryUserFeedbackWidget { class Window: UIWindow { @@ -22,8 +26,11 @@ struct SentryUserFeedbackWidget { let config: SentryUserFeedbackConfiguration - init(config: SentryUserFeedbackConfiguration) { + weak var delegate: (any SentryUserFeedbackWidgetDelegate)? + + init(config: SentryUserFeedbackConfiguration, delegate: any SentryUserFeedbackWidgetDelegate) { self.config = config + self.delegate = delegate super.init(nibName: nil, bundle: nil) view.addSubview(button) @@ -68,8 +75,12 @@ struct SentryUserFeedbackWidget { // MARK: SentryUserFeedbackFormDelegate - func finished() { + func finished(with feedback: SentryFeedback?) { closeForm() + + if let feedback = feedback { + delegate?.capture(feedback: feedback) + } } // MARK: UIAdaptivePresentationControllerDelegate @@ -79,9 +90,9 @@ struct SentryUserFeedbackWidget { } } - init(config: SentryUserFeedbackConfiguration) { + init(config: SentryUserFeedbackConfiguration, delegate: any SentryUserFeedbackWidgetDelegate) { super.init(frame: UIScreen.main.bounds) - rootViewController = RootViewController(config: config) + rootViewController = RootViewController(config: config, delegate: delegate) windowLevel = config.widgetConfig.windowLevel }