diff --git a/CHANGELOG.md b/CHANGELOG.md index ccba38e27b2..31a6910ad68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,9 @@ ## unreleased -## 6.0.4 +- feat: Manually capturing User Feedback #804 +## 6.0.4 - fix: Sanitize UserInfo of NSError and NSException #770 - fix: Xcode 12 warnings for Cocoapods #791 diff --git a/Samples/iOS-ObjectiveC/iOS-ObjectiveC/Base.lproj/Main.storyboard b/Samples/iOS-ObjectiveC/iOS-ObjectiveC/Base.lproj/Main.storyboard index 17f222d5aff..9b2e8adeb85 100644 --- a/Samples/iOS-ObjectiveC/iOS-ObjectiveC/Base.lproj/Main.storyboard +++ b/Samples/iOS-ObjectiveC/iOS-ObjectiveC/Base.lproj/Main.storyboard @@ -1,9 +1,11 @@ - + - + + + @@ -16,44 +18,52 @@ - + - - - + - - - - + - - + diff --git a/Samples/macOS-Swift/macOS-Swift/ViewController.swift b/Samples/macOS-Swift/macOS-Swift/ViewController.swift index 21c07bd1e02..78080debce6 100644 --- a/Samples/macOS-Swift/macOS-Swift/ViewController.swift +++ b/Samples/macOS-Swift/macOS-Swift/ViewController.swift @@ -28,6 +28,20 @@ class ViewController: NSViewController { // otherwise nil print("\(String(describing: eventId))") } + + @IBAction func captureUserFeedback(_ sender: Any) { + let error = NSError(domain: "UserFeedbackErrorDomain", code: 0, userInfo: [NSLocalizedDescriptionKey: "This never happens."]) + + let eventId = SentrySDK.capture(error: error) { scope in + scope.setLevel(.fatal) + } + + let userFeedback = UserFeedack(eventId: eventId) + userFeedback.comments = "It broke on macOS-Swift. I don't know why, but this happens." + userFeedback.email = "john@me.com" + userFeedback.name = "John Me" + SentrySDK.capture(userFeedback: userFeedback) + } @IBAction func crashOnException(_ sender: Any) { let userInfo: [String: String] = ["user-info-key-1": "user-info-value-1", "user-info-key-2": "user-info-value-2"] diff --git a/Samples/tvOS-Swift/tvOS-Swift/ContentView.swift b/Samples/tvOS-Swift/tvOS-Swift/ContentView.swift index 5bdf70ee725..9f197838126 100644 --- a/Samples/tvOS-Swift/tvOS-Swift/ContentView.swift +++ b/Samples/tvOS-Swift/tvOS-Swift/ContentView.swift @@ -13,6 +13,20 @@ struct ContentView: View { SentrySDK.capture(message: "Yeah captured a message") } + var captureUserFeedbackAction: () -> Void = { + let error = NSError(domain: "UserFeedbackErrorDomain", code: 0, userInfo: [NSLocalizedDescriptionKey: "This never happens."]) + + let eventId = SentrySDK.capture(error: error) { scope in + scope.setLevel(.fatal) + } + + let userFeedback = UserFeedack(eventId: eventId) + userFeedback.comments = "It broke on tvOS-Swift. I don't know why, but this happens." + userFeedback.email = "john@me.com" + userFeedback.name = "John Me" + SentrySDK.capture(userFeedback: userFeedback) + } + var captureErrorAction: () -> Void = { let error = NSError(domain: "SampleErrorDomain", code: 1, userInfo: [NSLocalizedDescriptionKey: "Object does not exist"]) SentrySDK.capture(error: error) { (scope) in @@ -37,6 +51,10 @@ struct ContentView: View { Text("Capture Message") } + Button(action: captureUserFeedbackAction) { + Text("Capture User Feedback") + } + Button(action: captureErrorAction) { Text("Capture Error") } diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index b6fd011fe1c..4e620c887c8 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -312,6 +312,9 @@ 7BAF3DD7243DD4A1008A5414 /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF3DD6243DD4A1008A5414 /* TestConstants.swift */; }; 7BAF3DD92440AEC8008A5414 /* SentryRequestManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 7BAF3DD82440AEC8008A5414 /* SentryRequestManager.h */; }; 7BB42EF124F3B7B700D7B39A /* SentrySession+Equality.m in Sources */ = {isa = PBXBuildFile; fileRef = 7BB42EF024F3B7B700D7B39A /* SentrySession+Equality.m */; }; + 7BB654FB253DC14A00887E87 /* SentryUserFeedback.h in Headers */ = {isa = PBXBuildFile; fileRef = 7BB654FA253DC14A00887E87 /* SentryUserFeedback.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7BB65501253DC1B500887E87 /* SentryUserFeedback.m in Sources */ = {isa = PBXBuildFile; fileRef = 7BB65500253DC1B500887E87 /* SentryUserFeedback.m */; }; + 7BB6550D253EEB3900887E87 /* SentryUserFeedbackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BB6550C253EEB3900887E87 /* SentryUserFeedbackTests.swift */; }; 7BBD1889244841EC00427C76 /* SentryHttpDateParser.h in Headers */ = {isa = PBXBuildFile; fileRef = 7BBD1888244841EC00427C76 /* SentryHttpDateParser.h */; }; 7BBD188B244841FB00427C76 /* SentryHttpDateParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 7BBD188A244841FB00427C76 /* SentryHttpDateParser.m */; }; 7BBD188D2448453600427C76 /* SentryHttpDateParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBD188C2448453600427C76 /* SentryHttpDateParserTests.swift */; }; @@ -718,6 +721,9 @@ 7BAF3DD82440AEC8008A5414 /* SentryRequestManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryRequestManager.h; path = include/SentryRequestManager.h; sourceTree = ""; }; 7BB42EEF24F3B7B700D7B39A /* SentrySession+Equality.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SentrySession+Equality.h"; sourceTree = ""; }; 7BB42EF024F3B7B700D7B39A /* SentrySession+Equality.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "SentrySession+Equality.m"; sourceTree = ""; }; + 7BB654FA253DC14A00887E87 /* SentryUserFeedback.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryUserFeedback.h; path = Public/SentryUserFeedback.h; sourceTree = ""; }; + 7BB65500253DC1B500887E87 /* SentryUserFeedback.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryUserFeedback.m; sourceTree = ""; }; + 7BB6550C253EEB3900887E87 /* SentryUserFeedbackTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUserFeedbackTests.swift; sourceTree = ""; }; 7BBD1888244841EC00427C76 /* SentryHttpDateParser.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryHttpDateParser.h; path = include/SentryHttpDateParser.h; sourceTree = ""; }; 7BBD188A244841FB00427C76 /* SentryHttpDateParser.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryHttpDateParser.m; sourceTree = ""; }; 7BBD188C2448453600427C76 /* SentryHttpDateParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryHttpDateParserTests.swift; sourceTree = ""; }; @@ -895,6 +901,8 @@ 7B82D54624E2A1AB00EE670F /* SentryId.m */, 7BFC169A2524995700FF6266 /* SentryMessage.h */, 7BFC16A025249A9D00FF6266 /* SentryMessage.m */, + 7BB654FA253DC14A00887E87 /* SentryUserFeedback.h */, + 7BB65500253DC1B500887E87 /* SentryUserFeedback.m */, ); name = Protocol; sourceTree = ""; @@ -1420,6 +1428,7 @@ 7BFC16AC2524BCE700FF6266 /* SentryMessageTests.swift */, 7BFC16B82524D4AF00FF6266 /* SentryMessage+Equality.h */, 7BFC16B92524D4AF00FF6266 /* SentryMessage+Equality.m */, + 7BB6550C253EEB3900887E87 /* SentryUserFeedbackTests.swift */, ); path = Protocol; sourceTree = ""; @@ -1623,6 +1632,7 @@ 63FE712320DA4C1000CDBAE8 /* SentryCrashID.h in Headers */, 7DC27EC523997EB7006998B5 /* SentryAutoBreadcrumbTrackingIntegration.h in Headers */, 63FE707F20DA4C1000CDBAE8 /* SentryCrashVarArgs.h in Headers */, + 7BB654FB253DC14A00887E87 /* SentryUserFeedback.h in Headers */, 639FCFA01EBC804600778193 /* SentryException.h in Headers */, 7BAF3DB9243C9777008A5414 /* SentryTransport.h in Headers */, 6383953623ABA42C000C1594 /* SentryHttpTransport.h in Headers */, @@ -1897,6 +1907,7 @@ 7BE3C7712445C30D00A38442 /* SentryDefaultCurrentDateProvider.m in Sources */, 6344DDB11EC308E400D9160D /* SentryCrashInstallationReporter.m in Sources */, 7BAF3DCE243DCBFE008A5414 /* SentryTransportFactory.m in Sources */, + 7BB65501253DC1B500887E87 /* SentryUserFeedback.m in Sources */, 7D5C441A237C2E1F00DAB0A3 /* SentrySDK.m in Sources */, 7D65260E237F649E00113EA2 /* SentryScope.m in Sources */, 63FE712D20DA4C1100CDBAE8 /* SentryCrashJSONCodecObjC.m in Sources */, @@ -2014,6 +2025,7 @@ 7B0A542E2521C62400A71716 /* SentryFrameRemoverTests.swift in Sources */, 7BA61CCC247D14E600C130A8 /* SentryThreadInspectorTests.swift in Sources */, 15360CF52433C59B00112302 /* SentryInstallationTests.m in Sources */, + 7BB6550D253EEB3900887E87 /* SentryUserFeedbackTests.swift in Sources */, 7BBD18B7245180FF00427C76 /* SentryDsnTests.m in Sources */, 63FE721A20DA66EC00CDBAE8 /* SentryCrashSysCtl_Tests.m in Sources */, 7B88F30424BC8E6500ADF90A /* SentrySerializationTests.swift in Sources */, diff --git a/Sources/Sentry/Public/Sentry.h b/Sources/Sentry/Public/Sentry.h index 356d99ef1a5..06f57c5afae 100644 --- a/Sources/Sentry/Public/Sentry.h +++ b/Sources/Sentry/Public/Sentry.h @@ -32,3 +32,4 @@ FOUNDATION_EXPORT const unsigned char SentryVersionString[]; #import "SentryStacktrace.h" #import "SentryThread.h" #import "SentryUser.h" +#import "SentryUserFeedback.h" diff --git a/Sources/Sentry/Public/SentryClient.h b/Sources/Sentry/Public/SentryClient.h index 2e4e9a636e0..8bb0cc24d4d 100644 --- a/Sources/Sentry/Public/SentryClient.h +++ b/Sources/Sentry/Public/SentryClient.h @@ -3,7 +3,7 @@ #import "SentryDefines.h" @class SentryOptions, SentrySession, SentryEvent, SentryEnvelope, SentryScope, SentryFileManager, - SentryId; + SentryId, SentryUserFeedback; NS_ASSUME_NONNULL_BEGIN @@ -101,6 +101,14 @@ SENTRY_NO_INIT - (SentryId *)captureMessage:(NSString *)message withScope:(SentryScope *)scope NS_SWIFT_NAME(capture(message:scope:)); +/** + * Captures a manually created user feedback and sends it to Sentry. + * + * @param userFeedback The user feedback to send to Sentry. + */ +- (void)captureUserFeedback:(SentryUserFeedback *)userFeedback + NS_SWIFT_NAME(capture(userFeedback:)); + - (void)captureSession:(SentrySession *)session NS_SWIFT_NAME(capture(session:)); - (void)captureEnvelope:(SentryEnvelope *)envelope NS_SWIFT_NAME(capture(envelope:)); diff --git a/Sources/Sentry/Public/SentryEnvelope.h b/Sources/Sentry/Public/SentryEnvelope.h index f03532842e3..8ca72e538bd 100644 --- a/Sources/Sentry/Public/SentryEnvelope.h +++ b/Sources/Sentry/Public/SentryEnvelope.h @@ -2,7 +2,7 @@ #import "SentryDefines.h" -@class SentryEvent, SentrySession, SentrySdkInfo, SentryId; +@class SentryEvent, SentrySession, SentrySdkInfo, SentryId, SentryUserFeedback; NS_ASSUME_NONNULL_BEGIN @@ -61,6 +61,7 @@ SENTRY_NO_INIT - (instancetype)initWithEvent:(SentryEvent *)event; - (instancetype)initWithSession:(SentrySession *)session; +- (instancetype)initWithUserFeedback:(SentryUserFeedback *)userFeedback; - (instancetype)initWithHeader:(SentryEnvelopeItemHeader *)header data:(NSData *)data NS_DESIGNATED_INITIALIZER; @@ -109,6 +110,8 @@ SENTRY_NO_INIT // Convenience init for a single event - (instancetype)initWithEvent:(SentryEvent *)event; +- (instancetype)initWithUserFeedback:(SentryUserFeedback *)userFeedback; + /** * The envelope header. */ diff --git a/Sources/Sentry/Public/SentryEnvelopeItemType.h b/Sources/Sentry/Public/SentryEnvelopeItemType.h index bdf07a9a288..d642c9b189b 100644 --- a/Sources/Sentry/Public/SentryEnvelopeItemType.h +++ b/Sources/Sentry/Public/SentryEnvelopeItemType.h @@ -1,4 +1,5 @@ static NSString *const SentryEnvelopeItemTypeEvent = @"event"; static NSString *const SentryEnvelopeItemTypeSession = @"session"; +static NSString *const SentryEnvelopeItemTypeUserFeedback = @"user_report"; static NSString *const SentryEnvelopeItemTypeTransaction = @"transaction"; static NSString *const SentryEnvelopeItemTypeAttachment = @"attachment"; diff --git a/Sources/Sentry/Public/SentryHub.h b/Sources/Sentry/Public/SentryHub.h index 257ada2608b..24100153060 100644 --- a/Sources/Sentry/Public/SentryHub.h +++ b/Sources/Sentry/Public/SentryHub.h @@ -1,9 +1,8 @@ #import "SentryDefines.h" #import "SentryIntegrationProtocol.h" -@class SentryEvent, SentryClient, SentryScope, SentrySession, SentryUser, SentryBreadcrumb; - -@class SentryId; +@class SentryEvent, SentryClient, SentryScope, SentrySession, SentryUser, SentryBreadcrumb, + SentryId, SentryUserFeedback; NS_ASSUME_NONNULL_BEGIN @interface SentryHub : NSObject @@ -103,6 +102,14 @@ SENTRY_NO_INIT - (SentryId *)captureMessage:(NSString *)message withScope:(SentryScope *)scope NS_SWIFT_NAME(capture(message:scope:)); +/** + * Captures a manually created user feedback and sends it to Sentry. + * + * @param userFeedback The user feedback to send to Sentry. + */ +- (void)captureUserFeedback:(SentryUserFeedback *)userFeedback + NS_SWIFT_NAME(capture(userFeedback:)); + /** * Invokes the callback with a mutable reference to the scope for modifications. */ diff --git a/Sources/Sentry/Public/SentrySDK.h b/Sources/Sentry/Public/SentrySDK.h index f30d3e8ee43..0fa88747c4e 100644 --- a/Sources/Sentry/Public/SentrySDK.h +++ b/Sources/Sentry/Public/SentrySDK.h @@ -2,9 +2,8 @@ #import "SentryDefines.h" -@class SentryHub, SentryOptions, SentryEvent, SentryBreadcrumb, SentryScope, SentryUser; - -@class SentryId; +@class SentryHub, SentryOptions, SentryEvent, SentryBreadcrumb, SentryScope, SentryUser, SentryId, + SentryUserFeedback; NS_ASSUME_NONNULL_BEGIN @@ -183,6 +182,14 @@ SENTRY_NO_INIT withScopeBlock:(void (^)(SentryScope *scope))block NS_SWIFT_NAME(capture(message:block:)); +/** + * Captures a manually created user feedback and sends it to Sentry. + * + * @param userFeedback The user feedback to send to Sentry. + */ ++ (void)captureUserFeedback:(SentryUserFeedback *)userFeedback + NS_SWIFT_NAME(capture(userFeedback:)); + /** * Adds a SentryBreadcrumb to the current Scope on the `currentHub`. * If the total number of breadcrumbs exceeds the `max_breadcrumbs` setting, the diff --git a/Sources/Sentry/Public/SentryUserFeedback.h b/Sources/Sentry/Public/SentryUserFeedback.h new file mode 100644 index 00000000000..46fc49ef9a8 --- /dev/null +++ b/Sources/Sentry/Public/SentryUserFeedback.h @@ -0,0 +1,44 @@ +#import "SentryDefines.h" +#import "SentrySerializable.h" + +NS_ASSUME_NONNULL_BEGIN + +@class SentryId; + +/** + * Adds additional information about what happened to an event. + */ +NS_SWIFT_NAME(UserFeedack) +@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. + */ +- (instancetype)initWithEventId:(SentryId *)eventId; + +/** + * The eventId of the event to which the user feedback is associated. + */ +@property (readonly, nonatomic, strong) SentryId *eventId; + +/** + * The name of the user. + */ +@property (nonatomic, copy) NSString *name; + +/** + * The email of the user. + */ +@property (nonatomic, copy) NSString *email; + +/** + * Comments of the user about what happened. + */ +@property (nonatomic, copy) NSString *comments; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index bb7a1a9d229..024481bd90c 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -237,6 +237,11 @@ - (void)captureEnvelope:(SentryEnvelope *)envelope [self.transport sendEnvelope:envelope]; } +- (void)captureUserFeedback:(SentryUserFeedback *)userFeedback +{ + [self.transport sendUserFeedback:userFeedback]; +} + /** * returns BOOL chance of YES is defined by sampleRate. * if sample rate isn't within 0.0 - 1.0 it returns YES (like if sampleRate diff --git a/Sources/Sentry/SentryEnvelope.m b/Sources/Sentry/SentryEnvelope.m index 16076a3b10f..3b570477977 100644 --- a/Sources/Sentry/SentryEnvelope.m +++ b/Sources/Sentry/SentryEnvelope.m @@ -8,6 +8,7 @@ #import "SentrySdkInfo.h" #import "SentrySerialization.h" #import "SentrySession.h" +#import "SentryUserFeedback.h" NS_ASSUME_NONNULL_BEGIN @@ -127,6 +128,27 @@ - (instancetype)initWithSession:(SentrySession *)session length:json.length] data:json]; } + +- (instancetype)initWithUserFeedback:(SentryUserFeedback *)userFeedback +{ + + NSError *error = nil; + NSData *json = [NSJSONSerialization dataWithJSONObject:[userFeedback serialize] + options:0 + error:&error]; + + if (nil != error) { + [SentryLog logWithMessage:@"Couldn't serialize user feedback." + andLevel:kSentryLogLevelError]; + json = [NSData new]; + } + + return [self initWithHeader:[[SentryEnvelopeItemHeader alloc] + initWithType:SentryEnvelopeItemTypeUserFeedback + length:json.length] + data:json]; +} + @end @implementation SentryEnvelope @@ -155,6 +177,13 @@ - (instancetype)initWithEvent:(SentryEvent *)event singleItem:item]; } +- (instancetype)initWithUserFeedback:(SentryUserFeedback *)userFeedback +{ + SentryEnvelopeItem *item = [[SentryEnvelopeItem alloc] initWithUserFeedback:userFeedback]; + + return [self initWithHeader:[[SentryEnvelopeHeader alloc] initWithId:nil] singleItem:item]; +} + - (instancetype)initWithId:(SentryId *_Nullable)id singleItem:(SentryEnvelopeItem *)item { return [self initWithHeader:[[SentryEnvelopeHeader alloc] initWithId:id] singleItem:item]; diff --git a/Sources/Sentry/SentryHttpTransport.m b/Sources/Sentry/SentryHttpTransport.m index 41c1cfc2e4a..c5c6b02a817 100644 --- a/Sources/Sentry/SentryHttpTransport.m +++ b/Sources/Sentry/SentryHttpTransport.m @@ -71,6 +71,12 @@ - (void)sendEvent:(SentryEvent *)event withSession:(SentrySession *)session [self sendEnvelope:envelope]; } +- (void)sendUserFeedback:(SentryUserFeedback *)userFeedback +{ + SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithUserFeedback:userFeedback]; + [self sendEnvelope:envelope]; +} + - (void)sendEnvelope:(SentryEnvelope *)envelope { envelope = [self.envelopeRateLimit removeRateLimitedItems:envelope]; diff --git a/Sources/Sentry/SentryHub.m b/Sources/Sentry/SentryHub.m index 5e183140b60..bfbe15ab761 100644 --- a/Sources/Sentry/SentryHub.m +++ b/Sources/Sentry/SentryHub.m @@ -279,6 +279,14 @@ - (SentryId *)captureException:(NSException *)exception withScope:(SentryScope * return SentryId.empty; } +- (void)captureUserFeedback:(SentryUserFeedback *)userFeedback +{ + SentryClient *client = [self getClient]; + if (nil != client) { + [client captureUserFeedback:userFeedback]; + } +} + - (void)addBreadcrumb:(SentryBreadcrumb *)crumb { SentryBeforeBreadcrumbCallback callback = [[[self client] options] beforeBreadcrumb]; diff --git a/Sources/Sentry/SentrySDK.m b/Sources/Sentry/SentrySDK.m index e1e4847f5fe..7abc697f8ef 100644 --- a/Sources/Sentry/SentrySDK.m +++ b/Sources/Sentry/SentrySDK.m @@ -152,6 +152,11 @@ + (SentryId *)captureMessage:(NSString *)message withScope:(SentryScope *)scope return [SentrySDK.currentHub captureMessage:message withScope:scope]; } ++ (void)captureUserFeedback:(SentryUserFeedback *)userFeedback +{ + [SentrySDK.currentHub captureUserFeedback:userFeedback]; +} + + (void)addBreadcrumb:(SentryBreadcrumb *)crumb { [SentrySDK.currentHub addBreadcrumb:crumb]; diff --git a/Sources/Sentry/SentryUserFeedback.m b/Sources/Sentry/SentryUserFeedback.m new file mode 100644 index 00000000000..521adcaa771 --- /dev/null +++ b/Sources/Sentry/SentryUserFeedback.m @@ -0,0 +1,27 @@ +#import "SentryUserFeedback.h" +#import "SentryId.h" +#import + +@implementation SentryUserFeedback + +- (instancetype)initWithEventId:(SentryId *)eventId +{ + if (self = [super init]) { + _eventId = eventId; + } + return self; +} + +- (NSDictionary *)serialize +{ + NSMutableDictionary *data = [[NSMutableDictionary alloc] init]; + + [data setValue:self.eventId.sentryIdString forKey:@"event_id"]; + [data setValue:self.email forKey:@"email"]; + [data setValue:self.name forKey:@"name"]; + [data setValue:self.comments forKey:@"comments"]; + + return data; +} + +@end diff --git a/Sources/Sentry/include/SentryTransport.h b/Sources/Sentry/include/SentryTransport.h index ea984761f15..00b342b83cc 100644 --- a/Sources/Sentry/include/SentryTransport.h +++ b/Sources/Sentry/include/SentryTransport.h @@ -1,6 +1,6 @@ #import -@class SentryEnvelope, SentryEvent, SentrySession; +@class SentryEnvelope, SentryEvent, SentrySession, SentryUserFeedback; NS_ASSUME_NONNULL_BEGIN @@ -25,6 +25,8 @@ NS_SWIFT_NAME(Transport) - (void)sendEvent:(SentryEvent *)event withSession:(SentrySession *)session; +- (void)sendUserFeedback:(SentryUserFeedback *)userFeedback NS_SWIFT_NAME(send(userFeedback:)); + - (void)sendEnvelope:(SentryEnvelope *)envelope NS_SWIFT_NAME(send(envelope:)); @end diff --git a/Tests/SentryTests/Networking/SentryHttpTransportTests.swift b/Tests/SentryTests/Networking/SentryHttpTransportTests.swift index f44c0d40bc6..8a4af740b6a 100644 --- a/Tests/SentryTests/Networking/SentryHttpTransportTests.swift +++ b/Tests/SentryTests/Networking/SentryHttpTransportTests.swift @@ -20,6 +20,9 @@ class SentryHttpTransportTests: XCTestCase { let options: Options let requestManager: TestRequestManager let rateLimits: DefaultRateLimits + + let userFeedback: UserFeedack + let userFeedbackRequest: SentryNSURLRequest init() { currentDateProvider = TestCurrentDateProvider() @@ -45,6 +48,13 @@ class SentryHttpTransportTests: XCTestCase { requestManager = TestRequestManager(session: URLSession(configuration: URLSessionConfiguration.ephemeral)) rateLimits = DefaultRateLimits(retryAfterHeaderParser: RetryAfterHeaderParser(httpDateParser: HttpDateParser()), andRateLimitParser: RateLimitParser()) + + userFeedback = UserFeedack(eventId: SentryId()) + userFeedback.comments = "It doesn't really" + userFeedback.email = "john@me.com" + userFeedback.name = "John Me" + + userFeedbackRequest = buildRequest(SentryEnvelope(userFeedback: userFeedback)) } var sut: SentryHttpTransport { @@ -114,7 +124,7 @@ class SentryHttpTransportTests: XCTestCase { func testSendEventWithSession_SentInOneEnvelope() { sut.send(fixture.event, with: fixture.session) -waitForAllRequests() + waitForAllRequests() assertRequestsSent(requestCount: 1) assertEnvelopesStored(envelopeCount: 0) @@ -396,6 +406,16 @@ waitForAllRequests() XCTAssertEqual(210, fixture.requestManager.requests.count) } + + func testSendUserFeedback() { + sut.send(userFeedback: fixture.userFeedback) + waitForAllRequests() + + XCTAssertEqual(1, fixture.requestManager.requests.count) + + let actualRequest = fixture.requestManager.requests.last + XCTAssertEqual(fixture.userFeedbackRequest.httpBody, actualRequest?.httpBody, "Request for user feedback is faulty.") + } private func givenRetryAfterResponse() -> HTTPURLResponse { let response = TestResponseFactory.createRetryAfterResponse(headerValue: "1") diff --git a/Tests/SentryTests/Networking/TestTransport.swift b/Tests/SentryTests/Networking/TestTransport.swift index aed7c86c642..a5f8b41b313 100644 --- a/Tests/SentryTests/Networking/TestTransport.swift +++ b/Tests/SentryTests/Networking/TestTransport.swift @@ -15,6 +15,11 @@ public class TestTransport: NSObject, Transport { sentEventsWithSession.append(Pair(event, session)) } + var sentUserFeedback: [UserFeedack] = [] + public func send(userFeedback: UserFeedack) { + sentUserFeedback.append(userFeedback) + } + public func send(envelope: SentryEnvelope) { lastSentEnvelope = envelope } diff --git a/Tests/SentryTests/Protocol/SentryEnvelopeTests.swift b/Tests/SentryTests/Protocol/SentryEnvelopeTests.swift index 7ed1e1ea86e..731688cfa37 100644 --- a/Tests/SentryTests/Protocol/SentryEnvelopeTests.swift +++ b/Tests/SentryTests/Protocol/SentryEnvelopeTests.swift @@ -4,6 +4,14 @@ class SentryEnvelopeTests: XCTestCase { private class Fixture { let sdkVersion = "sdkVersion" + let userFeedback: UserFeedack + + init() { + userFeedback = UserFeedack(eventId: SentryId()) + userFeedback.comments = "It doesn't work!" + userFeedback.email = "john@me.com" + userFeedback.name = "John Me" + } var breadcrumb: Breadcrumb { get { @@ -49,6 +57,7 @@ class SentryEnvelopeTests: XCTestCase { event.platform = "platform" return event } + } private let fixture = Fixture() @@ -216,6 +225,25 @@ class SentryEnvelopeTests: XCTestCase { json.assertContains(eventTimestamp.sentry_toIso8601String(), "timestamp") } } + + func testInitWithUserFeedback() throws { + let userFeedback = fixture.userFeedback + + let envelope = SentryEnvelope(userFeedback: userFeedback) + XCTAssertNil(envelope.header.eventId) + XCTAssertEqual(defaultSdkInfo, envelope.header.sdkInfo) + + XCTAssertEqual(1, envelope.items.count) + let item = envelope.items.first + XCTAssertEqual("user_report", item?.header.type) + XCTAssertNotNil(item?.data) + + let expectedData = try SentrySerialization.data(withJSONObject: userFeedback.serialize()) + + let actual = String(data: item?.data ?? Data(), encoding: .utf8)?.sorted() + let expected = String(data: expectedData, encoding: .utf8)?.sorted() + XCTAssertEqual(expected, actual) + } private func assertContainsBreadcrumbForDroppingContextAndSDK(_ json: String) { json.assertContains("A value set to the context or sdk is not serializable. Dropping context and sdk.", "breadcrumb message") diff --git a/Tests/SentryTests/Protocol/SentryUserFeedbackTests.swift b/Tests/SentryTests/Protocol/SentryUserFeedbackTests.swift new file mode 100644 index 00000000000..a69e7dab602 --- /dev/null +++ b/Tests/SentryTests/Protocol/SentryUserFeedbackTests.swift @@ -0,0 +1,18 @@ +import XCTest + +class SentryUserFeedbackTests: XCTestCase { + + func testSerialize() { + let userFeedback = UserFeedack(eventId: SentryId()) + userFeedback.comments = "Fix this please." + userFeedback.email = "john@me.com" + userFeedback.name = "John Me" + + let actual = userFeedback.serialize() + + XCTAssertEqual(userFeedback.eventId.sentryIdString, actual["event_id"] as? String) + XCTAssertEqual(userFeedback.comments, actual["comments"] as? String) + XCTAssertEqual(userFeedback.email, actual["email"] as? String) + XCTAssertEqual(userFeedback.name, actual["name"] as? String) + } +} diff --git a/Tests/SentryTests/SentrySDKTests.swift b/Tests/SentryTests/SentrySDKTests.swift index a2c720c7648..e65a1ec65be 100644 --- a/Tests/SentryTests/SentrySDKTests.swift +++ b/Tests/SentryTests/SentrySDKTests.swift @@ -11,6 +11,7 @@ class SentrySDKTests: XCTestCase { let hub: SentryHub let error: Error = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Object does not exist"]) let exception = NSException(name: NSExceptionName("My Custom exeption"), reason: "User clicked the button", userInfo: nil) + let userFeedback: UserFeedack init() { event = Event() @@ -21,8 +22,12 @@ class SentrySDKTests: XCTestCase { client = TestClient(options: Options())! hub = SentryHub(client: client, andScope: scope) + + userFeedback = UserFeedack(eventId: SentryId()) + userFeedback.comments = "Again really?" + userFeedback.email = "tim@apple.com" + userFeedback.name = "Tim Apple" } - } private var fixture: Fixture! @@ -197,6 +202,21 @@ class SentrySDKTests: XCTestCase { assertExceptionCaptured(expectedScope: scope) } + func testCaptureUserFeedback() { + givenSdkWithHub() + + SentrySDK.capture(userFeedback: fixture.userFeedback) + let client = fixture.client + XCTAssertEqual(1, client.capturedUserFeedback.count) + if let actual = client.capturedUserFeedback.first { + let expected = fixture.userFeedback + XCTAssertEqual(expected.eventId, actual.eventId) + XCTAssertEqual(expected.name, actual.name) + XCTAssertEqual(expected.email, actual.email) + XCTAssertEqual(expected.comments, actual.comments) + } + } + private func givenSdkWithHub() { SentrySDK.setCurrentHub(fixture.hub) } diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index fb19574307a..23f9efea62e 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -61,3 +61,4 @@ #import "SentryThreadInspector.h" #import "SentryTransport.h" #import "SentryTransportFactory.h" +#import "SentryUserFeedback.h" diff --git a/Tests/SentryTests/TestClient.swift b/Tests/SentryTests/TestClient.swift index 1adfc97a3c0..f1a8f94439b 100644 --- a/Tests/SentryTests/TestClient.swift +++ b/Tests/SentryTests/TestClient.swift @@ -77,6 +77,10 @@ class TestClient: Client { return SentryId() } + var capturedUserFeedback: [UserFeedack] = [] + override func capture(userFeedback: UserFeedack) { + capturedUserFeedback.append(userFeedback) + } } class TestFileManager: SentryFileManager {