diff --git a/CHANGELOG.md b/CHANGELOG.md index a0d28f8a5..2417d5a43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,14 @@ Changelog ========= -## 5.X.X (TBD) +## 5.16.0 (TBD) + +This release alters the behaviour of the notifier to track sessions automatically. +A session will be automatically captured on each app launch and sent to [https://sessions.bugsnag.com](https://sessions.bugsnag.com). + +If you use Bugsnag On-Premise, it is now also recommended that you set your notify and session endpoints via `config.setEndpoints(notify:sessions:)`. The previous properties used to configure this, `config.notifyURL` and `config.sessionURL`, are now `readonly` and therefore no longer assignable. + +* Enable automatic session tracking by default [#286](https://github.com/bugsnag/bugsnag-cocoa/pull/286) ### Bug Fixes diff --git a/Source/BugsnagConfiguration.h b/Source/BugsnagConfiguration.h index e77c2ab8c..c3e905926 100644 --- a/Source/BugsnagConfiguration.h +++ b/Source/BugsnagConfiguration.h @@ -73,10 +73,6 @@ typedef NSDictionary *_Nullable (^BugsnagBeforeNotifyHook)( * The API key of a Bugsnag project */ @property(readwrite, retain, nullable) NSString *apiKey; -/** - * The URL used to notify Bugsnag - */ -@property(readwrite, retain, nullable) NSURL *notifyURL; /** * The release stage of the application, such as production, development, beta * et cetera @@ -142,15 +138,45 @@ BugsnagBreadcrumbs *breadcrumbs; @property BOOL autoNotify; /** - * Determines whether app sessions should be tracked automatically. By default this value is false. + * Determines whether app sessions should be tracked automatically. By default this value is true. */ @property BOOL shouldAutoCaptureSessions; /** - * Set the endpoint to which tracked sessions reports are sent. This defaults to https://sessions.bugsnag.com, - * but should be overridden if you are using Bugsnag On-premise, to point to your own Bugsnag endpoint. + * Retrieves the endpoint used to notify Bugsnag of errors + * + * NOTE: If you want to set this value, you should do so via setEndpointsForNotify:sessions: instead. + * + * @see setEndpointsForNotify:sessions: + */ +@property(readonly, retain, nullable) NSURL *notifyURL; + +/** + * Retrieves the endpoint used to send tracked sessions to Bugsnag + * + * NOTE: If you want to set this value, you should do so via setEndpointsForNotify:sessions: instead. + * + * @see setEndpointsForNotify:sessions: + */ +@property(readonly, retain, nullable) NSURL *sessionURL; + +/** + * Set the endpoints to send data to. By default we'll send error reports to + * https://notify.bugsnag.com, and sessions to https://sessions.bugsnag.com, but you can + * override this if you are using Bugsnag Enterprise to point to your own Bugsnag endpoint. + * + * Please note that it is recommended that you set both endpoints. If the notify endpoint is + * missing, an assertion will be thrown. If the session endpoint is missing, a warning will be + * logged and sessions will not be sent automatically. + * + * @param notify the notify endpoint + * @param sessions the sessions endpoint + * + * @throws an assertion if the notify endpoint is not a valid URL */ -@property(readwrite, retain, nullable) NSURL *sessionURL; + +- (void)setEndpointsForNotify:(NSString *_Nonnull)notify + sessions:(NSString *_Nonnull)sessions NS_SWIFT_NAME(setEndpoints(notify:sessions:)); /** * Set user metadata diff --git a/Source/BugsnagConfiguration.m b/Source/BugsnagConfiguration.m index bcd9ce382..654ed4f06 100644 --- a/Source/BugsnagConfiguration.m +++ b/Source/BugsnagConfiguration.m @@ -65,6 +65,8 @@ - (id)init { _notifyReleaseStages = nil; _breadcrumbs = [BugsnagBreadcrumbs new]; _automaticallyCollectBreadcrumbs = YES; + _shouldAutoCaptureSessions = YES; + if ([NSURLSession class]) { _session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration @@ -87,7 +89,7 @@ - (BOOL)shouldSendReports { - (void)setUser:(NSString *)userId withName:(NSString *)userName andEmail:(NSString *)userEmail { - + self.currentUser = [[BugsnagUser alloc] initWithUserId:userId name:userName emailAddress:userEmail]; [self.metaData addAttribute:BSGKeyId withValue:userId toTabWithName:BSGKeyUser]; @@ -225,7 +227,7 @@ - (BOOL)shouldAutoCaptureSessions { - (void)setShouldAutoCaptureSessions:(BOOL)shouldAutoCaptureSessions { @synchronized (self) { _shouldAutoCaptureSessions = shouldAutoCaptureSessions; - + if (shouldAutoCaptureSessions) { // track any existing sessions BugsnagSessionTracker *sessionTracker = [Bugsnag notifier].sessionTracker; [sessionTracker onAutoCaptureEnabled]; @@ -249,6 +251,22 @@ - (NSDictionary *)sessionApiHeaders { }; } +- (void)setEndpointsForNotify:(NSString *_Nonnull)notify sessions:(NSString *_Nonnull)sessions { + _notifyURL = [NSURL URLWithString:notify]; + _sessionURL = [NSURL URLWithString:sessions]; + + NSAssert([self isValidUrl:_notifyURL], @"Invalid URL supplied for notify endpoint"); + + if (![self isValidUrl:_sessionURL]) { + _sessionURL = nil; + } +} + +- (BOOL)isValidUrl:(NSURL *)url { + return url != nil && url.scheme != nil && url.host != nil; +} + + - (BOOL)hasValidApiKey { return [_apiKey length] > 0; } diff --git a/Source/BugsnagNotifier.m b/Source/BugsnagNotifier.m index 0459e4c6b..4a0525b28 100644 --- a/Source/BugsnagNotifier.m +++ b/Source/BugsnagNotifier.m @@ -216,9 +216,6 @@ - (id)initWithConfiguration:(BugsnagConfiguration *)initConfiguration { hasRecordedSessions = true; }]; - - [self.sessionTracker startNewSession:[NSDate date] withUser:nil autoCaptured:YES]; - [self metaDataChanged:self.configuration.metaData]; [self metaDataChanged:self.configuration.config]; [self metaDataChanged:self.state]; @@ -356,9 +353,10 @@ - (void)start { object:nil]; #endif + _started = YES; + // notification not received in time on initial startup, so trigger manually [self willEnterForeground:self]; - _started = YES; } - (void)watchLifecycleEvents:(NSNotificationCenter *)center { diff --git a/Source/BugsnagSessionTracker.m b/Source/BugsnagSessionTracker.m index bf5a4f28c..d7cb22361 100644 --- a/Source/BugsnagSessionTracker.m +++ b/Source/BugsnagSessionTracker.m @@ -11,6 +11,7 @@ #import "BSG_KSLogger.h" #import "BugsnagSessionTrackingPayload.h" #import "BugsnagSessionTrackingApiClient.h" +#import "BugsnagLogger.h" @interface BugsnagSessionTracker () @property BugsnagConfiguration *config; @@ -43,6 +44,10 @@ - (instancetype)initWithConfig:(BugsnagConfiguration *)config - (void)startNewSession:(NSDate *)date withUser:(BugsnagUser *)user autoCaptured:(BOOL)autoCaptured { + if (self.config.sessionURL == nil) { + bsg_log_err(@"The session tracking endpoint has not been set. Session tracking is disabled"); + return; + } _currentSession = [[BugsnagSession alloc] initWithId:[[NSUUID UUID] UUIDString] startDate:date @@ -58,7 +63,7 @@ - (void)startNewSession:(NSDate *)date - (void)trackSession { [self.sessionStore write:self.currentSession]; self.trackedFirstSession = YES; - + if (self.callback) { self.callback(self.currentSession); } @@ -87,34 +92,41 @@ - (void)incrementHandledError { } - (void)send { - @synchronized (self.sessionStore) { - NSMutableArray *sessions = [NSMutableArray new]; - NSArray *fileIds = [self.sessionStore fileIds]; + NSArray *fileIds = [self.sessionStore fileIds]; - for (NSDictionary *dict in [self.sessionStore allFiles]) { - [sessions addObject:[[BugsnagSession alloc] initWithDictionary:dict]]; - } - BugsnagSessionTrackingPayload *payload = [[BugsnagSessionTrackingPayload alloc] initWithSessions:sessions]; - - if (payload.sessions.count > 0) { - [self.apiClient sendData:payload - withPayload:[payload toJson] - toURL:self.config.sessionURL - headers:self.config.sessionApiHeaders - onCompletion:^(id data, BOOL success, NSError *error) { - - if (success && error == nil) { - NSLog(@"Sent sessions to Bugsnag"); - - for (NSString *fileId in fileIds) { - [self.sessionStore deleteFileWithId:fileId]; - } - } else { - NSLog(@"Failed to send sessions to Bugsnag: %@", error); + if (fileIds.count <= 0) { + return; + } + + dispatch_semaphore_t requestSemaphore = dispatch_semaphore_create(0); + NSMutableArray *sessions = [NSMutableArray new]; + + for (NSDictionary *dict in [self.sessionStore allFiles]) { + [sessions addObject:[[BugsnagSession alloc] initWithDictionary:dict]]; + } + BugsnagSessionTrackingPayload *payload = [[BugsnagSessionTrackingPayload alloc] initWithSessions:sessions]; + + if (payload.sessions.count > 0) { + [self.apiClient sendData:payload + withPayload:[payload toJson] + toURL:self.config.sessionURL + headers:self.config.sessionApiHeaders + onCompletion:^(id data, BOOL success, NSError *error) { + if (success && error == nil) { + NSLog(@"Sent sessions to Bugsnag"); + + for (NSString *fileId in fileIds) { + [self.sessionStore deleteFileWithId:fileId]; } - }]; - } + } else { + NSLog(@"Failed to send sessions to Bugsnag: %@", error); + } + dispatch_semaphore_signal(requestSemaphore); + }]; + } else { + dispatch_semaphore_signal(requestSemaphore); } + dispatch_semaphore_wait(requestSemaphore, DISPATCH_TIME_FOREVER); } @end diff --git a/Source/BugsnagUser.m b/Source/BugsnagUser.m index 957424c8e..5e6c14e20 100644 --- a/Source/BugsnagUser.m +++ b/Source/BugsnagUser.m @@ -14,7 +14,7 @@ @implementation BugsnagUser - (instancetype)initWithDictionary:(NSDictionary *)dict { if (self = [super init]) { _userId = dict[@"id"]; - _emailAddress = dict[@"emailAddress"]; + _emailAddress = dict[@"email"]; _name = dict[@"name"]; } return self; @@ -34,12 +34,12 @@ + (instancetype)userWithUserId:(NSString *)userId name:(NSString *)name emailAdd return [[self alloc] initWithUserId:userId name:name emailAddress:emailAddress]; } - - (NSDictionary *)toJson { NSMutableDictionary *dict = [NSMutableDictionary new]; BSGDictInsertIfNotNil(dict, self.userId, @"id"); - BSGDictInsertIfNotNil(dict, self.emailAddress, @"emailAddress"); + BSGDictInsertIfNotNil(dict, self.emailAddress, @"email"); BSGDictInsertIfNotNil(dict, self.name, @"name"); return [NSDictionary dictionaryWithDictionary:dict]; } + @end diff --git a/Tests/BugsnagConfigurationTests.m b/Tests/BugsnagConfigurationTests.m index 373bffbf2..3aadd2cc6 100644 --- a/Tests/BugsnagConfigurationTests.m +++ b/Tests/BugsnagConfigurationTests.m @@ -1,5 +1,6 @@ #import "Bugsnag.h" #import "BugsnagConfiguration.h" +#import "BugsnagSessionTracker.h" #import "BugsnagUser.h" #import @@ -56,7 +57,7 @@ - (void)testNotifyReleaseStagesExcludedSkipsSending { - (void)testDefaultSessionConfig { BugsnagConfiguration *config = [BugsnagConfiguration new]; - XCTAssertFalse([config shouldAutoCaptureSessions]); + XCTAssertTrue([config shouldAutoCaptureSessions]); } - (void)testErrorApiHeaders { @@ -81,10 +82,59 @@ - (void)testSessionEndpoints { // Default endpoints XCTAssertEqualObjects([NSURL URLWithString:@"https://sessions.bugsnag.com"], config.sessionURL); - // Setting an endpoint - NSURL *endpoint = [NSURL URLWithString:@"http://localhost:8000"]; - config.sessionURL = endpoint; - XCTAssertEqualObjects(endpoint, config.sessionURL); + // Test overriding the session endpoint (use dummy endpoints to avoid hitting production) + [config setEndpointsForNotify:@"http://localhost:1234" sessions:@"http://localhost:8000"]; + XCTAssertEqualObjects([NSURL URLWithString:@"http://localhost:8000"], config.sessionURL); +} + +- (void)testNotifyEndpoint { + BugsnagConfiguration *config = [BugsnagConfiguration new]; + XCTAssertEqualObjects([NSURL URLWithString:@"https://notify.bugsnag.com/"], config.notifyURL); + + // Test overriding the notify endpoint (use dummy endpoints to avoid hitting production) + [config setEndpointsForNotify:@"http://localhost:1234" sessions:@"http://localhost:8000"]; + XCTAssertEqualObjects([NSURL URLWithString:@"http://localhost:1234"], config.notifyURL); +} + +- (void)testSetEndpoints { + BugsnagConfiguration *config = [BugsnagConfiguration new]; + [config setEndpointsForNotify:@"http://notify.example.com" sessions:@"http://sessions.example.com"]; + XCTAssertEqualObjects([NSURL URLWithString:@"http://notify.example.com"], config.notifyURL); + XCTAssertEqualObjects([NSURL URLWithString:@"http://sessions.example.com"], config.sessionURL); +} + +- (void)testSetEmptyNotifyEndpoint { + BugsnagConfiguration *config = [BugsnagConfiguration new]; + XCTAssertThrowsSpecificNamed([config setEndpointsForNotify:@"" sessions:@"http://sessions.example.com"], + NSException, NSInternalInconsistencyException); +} + +- (void)testSetMalformedNotifyEndpoint { + BugsnagConfiguration *config = [BugsnagConfiguration new]; + XCTAssertThrowsSpecificNamed([config setEndpointsForNotify:@"http://" sessions:@"http://sessions.example.com"], + NSException, NSInternalInconsistencyException); +} + +- (void)testSetEmptySessionsEndpoint { + BugsnagConfiguration *config = [BugsnagConfiguration new]; + [config setEndpointsForNotify:@"http://notify.example.com" sessions:@""]; + BugsnagSessionTracker *sessionTracker + = [[BugsnagSessionTracker alloc] initWithConfig:config apiClient:nil callback:nil]; + + XCTAssertNil(sessionTracker.currentSession); + [sessionTracker startNewSession:[NSDate date] withUser:nil autoCaptured:NO]; + XCTAssertNil(sessionTracker.currentSession); +} + +- (void)testSetMalformedSessionsEndpoint { + BugsnagConfiguration *config = [BugsnagConfiguration new]; + [config setEndpointsForNotify:@"http://notify.example.com" sessions:@"f"]; + BugsnagSessionTracker *sessionTracker + = [[BugsnagSessionTracker alloc] initWithConfig:config apiClient:nil callback:nil]; + + XCTAssertNil(sessionTracker.currentSession); + [sessionTracker startNewSession:[NSDate date] withUser:nil autoCaptured:NO]; + XCTAssertNil(sessionTracker.currentSession); } - (void)testUser { diff --git a/Tests/BugsnagSinkTests.m b/Tests/BugsnagSinkTests.m index afe6c3bc6..b00a67053 100644 --- a/Tests/BugsnagSinkTests.m +++ b/Tests/BugsnagSinkTests.m @@ -39,7 +39,9 @@ - (void)setUp { config.autoNotify = NO; config.apiKey = @"apiKeyHere"; config.releaseStage = @"MagicalTestingTime"; - config.notifyURL = nil; + + // set a dummy endpoint, avoid hitting production + [config setEndpointsForNotify:@"http://localhost:1234" sessions:@"http://localhost:1234"]; [Bugsnag startBugsnagWithConfiguration:config]; BugsnagCrashReport *report = [[BugsnagCrashReport alloc] initWithKSReport:self.rawReportData]; diff --git a/Tests/BugsnagUserTest.m b/Tests/BugsnagUserTest.m index 5ec80554d..5e86edd22 100644 --- a/Tests/BugsnagUserTest.m +++ b/Tests/BugsnagUserTest.m @@ -19,7 +19,7 @@ - (void)testDictDeserialisation { NSDictionary *dict = @{ @"id": @"test", - @"emailAddress": @"fake@example.com", + @"email": @"fake@example.com", @"name": @"Tom Bombadil" }; BugsnagUser *user = [[BugsnagUser alloc] initWithDictionary:dict]; @@ -41,7 +41,7 @@ - (void)testPayloadSerialisation { XCTAssertEqual(3, [rootNode count]); XCTAssertEqualObjects(@"test", rootNode[@"id"]); - XCTAssertEqualObjects(@"fake@example.com", rootNode[@"emailAddress"]); + XCTAssertEqualObjects(@"fake@example.com", rootNode[@"email"]); XCTAssertEqualObjects(@"Tom Bombadil", rootNode[@"name"]); } diff --git a/examples/swift-ios/Podfile.lock b/examples/swift-ios/Podfile.lock index 7a0be889c..9aeeb27fd 100644 --- a/examples/swift-ios/Podfile.lock +++ b/examples/swift-ios/Podfile.lock @@ -1,16 +1,16 @@ PODS: - - Bugsnag (5.15.4) + - Bugsnag (5.15.6) DEPENDENCIES: - Bugsnag (from `../..`) EXTERNAL SOURCES: Bugsnag: - :path: ../.. + :path: "../.." SPEC CHECKSUMS: - Bugsnag: 904211a0230f254808b47f3adb4b684900772962 + Bugsnag: ff5f5e3059e6a9c9d27a899f3bf3774067553483 PODFILE CHECKSUM: 2107babfbfdb18f0288407b9d9ebd48cbee8661c -COCOAPODS: 1.4.0 +COCOAPODS: 1.5.0 diff --git a/features/fixtures/ios-swift-cocoapods/Podfile.lock b/features/fixtures/ios-swift-cocoapods/Podfile.lock index 3ee64957e..9ab551a9d 100644 --- a/features/fixtures/ios-swift-cocoapods/Podfile.lock +++ b/features/fixtures/ios-swift-cocoapods/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - Bugsnag (5.15.4) + - Bugsnag (5.15.6) DEPENDENCIES: - Bugsnag (from `../../..`) @@ -9,7 +9,7 @@ EXTERNAL SOURCES: :path: ../../.. SPEC CHECKSUMS: - Bugsnag: 904211a0230f254808b47f3adb4b684900772962 + Bugsnag: ff5f5e3059e6a9c9d27a899f3bf3774067553483 PODFILE CHECKSUM: 4d026fb83571f098c9fb4fa71c1564c72c55ab1a diff --git a/features/fixtures/ios-swift-cocoapods/iOSTestApp.xcodeproj/project.pbxproj b/features/fixtures/ios-swift-cocoapods/iOSTestApp.xcodeproj/project.pbxproj index 2fca34567..34f7afc9e 100644 --- a/features/fixtures/ios-swift-cocoapods/iOSTestApp.xcodeproj/project.pbxproj +++ b/features/fixtures/ios-swift-cocoapods/iOSTestApp.xcodeproj/project.pbxproj @@ -14,8 +14,10 @@ 8AB8866E20404DD30003E444 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8AB8866C20404DD30003E444 /* LaunchScreen.storyboard */; }; C4D0B5FF8E60C0835B86DFE9 /* Pods_iOSTestApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4994F05E0421A0B037DD2CC5 /* Pods_iOSTestApp.framework */; }; F429502603396F8671B333B3 /* HandledExceptionScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = F429526319377A8848136413 /* HandledExceptionScenario.swift */; }; + F4295109FCAB93708FDAFE12 /* DisabledSessionTrackingScenario.m in Sources */ = {isa = PBXBuildFile; fileRef = F429563584D9BC3A5B86BECF /* DisabledSessionTrackingScenario.m */; }; F42951A9FD696D9047149DA8 /* UndefinedInstructionScenario.m in Sources */ = {isa = PBXBuildFile; fileRef = F429538D19421F28D8EB0446 /* UndefinedInstructionScenario.m */; }; F42951BEB2518C610A85FE0D /* BuiltinTrapScenario.m in Sources */ = {isa = PBXBuildFile; fileRef = F42956D34274D4ED16B4D491 /* BuiltinTrapScenario.m */; }; + F42951BF19D7F35A03273CFB /* AutoSessionScenario.m in Sources */ = {isa = PBXBuildFile; fileRef = F4295EE3B0BD05E5FE513B20 /* AutoSessionScenario.m */; }; F4295218A62E41518DC3C057 /* AccessNonObjectScenario.m in Sources */ = {isa = PBXBuildFile; fileRef = F4295A20DE438C2B28167714 /* AccessNonObjectScenario.m */; }; F4295262625F84A80282E520 /* CorruptMallocScenario.m in Sources */ = {isa = PBXBuildFile; fileRef = F429581876FA1A9CFEE52615 /* CorruptMallocScenario.m */; }; F429532277D8B4DAE53F3F77 /* MinimalCrashReportScenario.m in Sources */ = {isa = PBXBuildFile; fileRef = F429514FFBD085C6FEB85BDC /* MinimalCrashReportScenario.m */; }; @@ -31,6 +33,7 @@ F429565A951303E2C3136D0D /* UserIdScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42957C58F98EECD3ADD84B2 /* UserIdScenario.swift */; }; F4295836C8AF75547C675E8D /* ReleasedObjectScenario.m in Sources */ = {isa = PBXBuildFile; fileRef = F4295E71D7B2DFE77057F3DA /* ReleasedObjectScenario.m */; }; F42958881D3F34A76CADE4EC /* SwiftCrash.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4295EEDC00E5ED3C166DBF0 /* SwiftCrash.swift */; }; + F42958BE5DDACDBF653CA926 /* ManualSessionScenario.m in Sources */ = {isa = PBXBuildFile; fileRef = F429570EE7A751B53D011481 /* ManualSessionScenario.m */; }; F42959124DB949EEF1420957 /* ReadOnlyPageScenario.m in Sources */ = {isa = PBXBuildFile; fileRef = F4295871D1BCF211398CAEBA /* ReadOnlyPageScenario.m */; }; F4295968571A4118D6A4606A /* UserEnabledScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42954E2B3FF0C5C0474DA74 /* UserEnabledScenario.swift */; }; F4295A036B228AF608641699 /* UserDisabledScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42951F865D328A43250640B /* UserDisabledScenario.swift */; }; @@ -74,10 +77,12 @@ F42954E8B66F3FB7F5333CF7 /* Scenario.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Scenario.m; sourceTree = ""; }; F429550B682F8F9677305881 /* CxxExceptionScenario.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = CxxExceptionScenario.mm; sourceTree = ""; }; F429556E677F030F72C4C5D0 /* AsyncSafeThreadScenario.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AsyncSafeThreadScenario.h; sourceTree = ""; }; + F429563584D9BC3A5B86BECF /* DisabledSessionTrackingScenario.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DisabledSessionTrackingScenario.m; sourceTree = ""; }; F429566550603ECAC2875333 /* PrivilegedInstructionScenario.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PrivilegedInstructionScenario.m; sourceTree = ""; }; F42956707AEC06754D9C2208 /* AccessNonObjectScenario.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AccessNonObjectScenario.h; sourceTree = ""; }; F42956D34274D4ED16B4D491 /* BuiltinTrapScenario.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BuiltinTrapScenario.m; sourceTree = ""; }; F4295703713DA0D0ED768230 /* MinimalCrashReportScenario.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MinimalCrashReportScenario.h; sourceTree = ""; }; + F429570EE7A751B53D011481 /* ManualSessionScenario.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ManualSessionScenario.m; sourceTree = ""; }; F42957A23DB8145B4B702F15 /* PrivilegedInstructionScenario.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PrivilegedInstructionScenario.h; sourceTree = ""; }; F42957C58F98EECD3ADD84B2 /* UserIdScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserIdScenario.swift; sourceTree = ""; }; F42957FA1A3724BFBDC22E14 /* NSExceptionScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSExceptionScenario.swift; sourceTree = ""; }; @@ -95,11 +100,15 @@ F4295C758B89038DD3A2A198 /* CorruptMallocScenario.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CorruptMallocScenario.h; sourceTree = ""; }; F4295CA6B6792DECA15C450B /* AsyncSafeThreadScenario.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AsyncSafeThreadScenario.m; sourceTree = ""; }; F4295CD1B5437B73A7C254F7 /* ReadGarbagePointerScenario.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ReadGarbagePointerScenario.h; sourceTree = ""; }; + F4295CF00D2B7AE6DE5BBFC5 /* ManualSessionScenario.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ManualSessionScenario.h; sourceTree = ""; }; F4295E2979ED53A2E82017CF /* BuiltinTrapScenario.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BuiltinTrapScenario.h; sourceTree = ""; }; + F4295E6960503E5BD75A2C35 /* DisabledSessionTrackingScenario.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DisabledSessionTrackingScenario.h; sourceTree = ""; }; F4295E71D7B2DFE77057F3DA /* ReleasedObjectScenario.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ReleasedObjectScenario.m; sourceTree = ""; }; F4295E86DC0BE9DC731B0D1C /* NullPointerScenario.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NullPointerScenario.m; sourceTree = ""; }; + F4295EE3B0BD05E5FE513B20 /* AutoSessionScenario.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AutoSessionScenario.m; sourceTree = ""; }; F4295EEDC00E5ED3C166DBF0 /* SwiftCrash.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftCrash.swift; sourceTree = ""; }; F4295F13EBCAC9CB0ABC4008 /* NonExistentMethodScenario.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NonExistentMethodScenario.m; sourceTree = ""; }; + F4295F22AA1863213F4A5F51 /* AutoSessionScenario.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AutoSessionScenario.h; sourceTree = ""; }; F4295F595986A279FA3BDEA7 /* UserEmailScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserEmailScenario.swift; sourceTree = ""; }; F4295FA8EBBA645EECF7B483 /* HandledErrorOverrideScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HandledErrorOverrideScenario.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -216,6 +225,12 @@ F42950D49A5F24FF7155EEE1 /* NonExistentMethodScenario.h */, F429566550603ECAC2875333 /* PrivilegedInstructionScenario.m */, F42957A23DB8145B4B702F15 /* PrivilegedInstructionScenario.h */, + F429570EE7A751B53D011481 /* ManualSessionScenario.m */, + F4295CF00D2B7AE6DE5BBFC5 /* ManualSessionScenario.h */, + F4295EE3B0BD05E5FE513B20 /* AutoSessionScenario.m */, + F4295F22AA1863213F4A5F51 /* AutoSessionScenario.h */, + F429563584D9BC3A5B86BECF /* DisabledSessionTrackingScenario.m */, + F4295E6960503E5BD75A2C35 /* DisabledSessionTrackingScenario.h */, ); path = scenarios; sourceTree = ""; @@ -232,7 +247,7 @@ 8AB8865D20404DD30003E444 /* Frameworks */, 8AB8865E20404DD30003E444 /* Resources */, 4D2139E03F8B071F7DB7C7A6 /* [CP] Embed Pods Frameworks */, - 421E0803F4159BB79BC285F5 /* [CP] Copy Pods Resources */, + F4E835AC090FCF28B6B12EC4 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -310,37 +325,37 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 421E0803F4159BB79BC285F5 /* [CP] Copy Pods Resources */ = { + 4D2139E03F8B071F7DB7C7A6 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-iOSTestApp/Pods-iOSTestApp-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/Bugsnag/Bugsnag.framework", ); - name = "[CP] Copy Pods Resources"; + name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Bugsnag.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-iOSTestApp/Pods-iOSTestApp-resources.sh\"\n"; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-iOSTestApp/Pods-iOSTestApp-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 4D2139E03F8B071F7DB7C7A6 /* [CP] Embed Pods Frameworks */ = { + F4E835AC090FCF28B6B12EC4 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-iOSTestApp/Pods-iOSTestApp-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/Bugsnag/Bugsnag.framework", ); - name = "[CP] Embed Pods Frameworks"; + name = "[CP] Copy Pods Resources"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Bugsnag.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-iOSTestApp/Pods-iOSTestApp-frameworks.sh\"\n"; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-iOSTestApp/Pods-iOSTestApp-resources.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -380,6 +395,9 @@ F42951BEB2518C610A85FE0D /* BuiltinTrapScenario.m in Sources */, F4295397AD31C1C1E64144F5 /* NonExistentMethodScenario.m in Sources */, F4295A0B0DA0AF3B5502D29C /* PrivilegedInstructionScenario.m in Sources */, + F42958BE5DDACDBF653CA926 /* ManualSessionScenario.m in Sources */, + F42951BF19D7F35A03273CFB /* AutoSessionScenario.m in Sources */, + F4295109FCAB93708FDAFE12 /* DisabledSessionTrackingScenario.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/features/fixtures/ios-swift-cocoapods/iOSTestApp/AppDelegate.swift b/features/fixtures/ios-swift-cocoapods/iOSTestApp/AppDelegate.swift index 0e022b6ce..e745a5158 100644 --- a/features/fixtures/ios-swift-cocoapods/iOSTestApp/AppDelegate.swift +++ b/features/fixtures/ios-swift-cocoapods/iOSTestApp/AppDelegate.swift @@ -48,10 +48,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { internal func prepareConfig(apiKey: String, mockAPIPath: String) -> BugsnagConfiguration { let config = BugsnagConfiguration() - let url = URL(string: mockAPIPath) config.apiKey = apiKey - config.notifyURL = url - config.sessionURL = url + config.setEndpoints(notify: mockAPIPath, sessions: mockAPIPath) + config.shouldAutoCaptureSessions = false; return config } diff --git a/features/fixtures/ios-swift-cocoapods/iOSTestApp/scenarios/AutoSessionScenario.h b/features/fixtures/ios-swift-cocoapods/iOSTestApp/scenarios/AutoSessionScenario.h new file mode 100644 index 000000000..723ee933b --- /dev/null +++ b/features/fixtures/ios-swift-cocoapods/iOSTestApp/scenarios/AutoSessionScenario.h @@ -0,0 +1,14 @@ +// +// Created by Jamie Lynch on 07/06/2018. +// Copyright (c) 2018 Bugsnag. All rights reserved. +// + +#import +#import "Scenario.h" + + +/** + * Sends an automatic session payload to Bugsnag. + */ +@interface AutoSessionScenario : Scenario +@end diff --git a/features/fixtures/ios-swift-cocoapods/iOSTestApp/scenarios/AutoSessionScenario.m b/features/fixtures/ios-swift-cocoapods/iOSTestApp/scenarios/AutoSessionScenario.m new file mode 100644 index 000000000..05fc4ff60 --- /dev/null +++ b/features/fixtures/ios-swift-cocoapods/iOSTestApp/scenarios/AutoSessionScenario.m @@ -0,0 +1,19 @@ +// +// Created by Jamie Lynch on 07/06/2018. +// Copyright (c) 2018 Bugsnag. All rights reserved. +// + +#import "AutoSessionScenario.h" + + +@implementation AutoSessionScenario + +- (void)startBugsnag { + self.config.shouldAutoCaptureSessions = YES; + [super startBugsnag]; +} + +- (void)run { +} + +@end diff --git a/features/fixtures/ios-swift-cocoapods/iOSTestApp/scenarios/DisabledSessionTrackingScenario.h b/features/fixtures/ios-swift-cocoapods/iOSTestApp/scenarios/DisabledSessionTrackingScenario.h new file mode 100644 index 000000000..66ee9b4e7 --- /dev/null +++ b/features/fixtures/ios-swift-cocoapods/iOSTestApp/scenarios/DisabledSessionTrackingScenario.h @@ -0,0 +1,11 @@ +// +// Created by Jamie Lynch on 07/06/2018. +// Copyright (c) 2018 Bugsnag. All rights reserved. +// + +#import +#import "Scenario.h" + + +@interface DisabledSessionTrackingScenario : Scenario +@end diff --git a/features/fixtures/ios-swift-cocoapods/iOSTestApp/scenarios/DisabledSessionTrackingScenario.m b/features/fixtures/ios-swift-cocoapods/iOSTestApp/scenarios/DisabledSessionTrackingScenario.m new file mode 100644 index 000000000..7270a246c --- /dev/null +++ b/features/fixtures/ios-swift-cocoapods/iOSTestApp/scenarios/DisabledSessionTrackingScenario.m @@ -0,0 +1,14 @@ +// +// Created by Jamie Lynch on 07/06/2018. +// Copyright (c) 2018 Bugsnag. All rights reserved. +// + +#import "DisabledSessionTrackingScenario.h" + + +@implementation DisabledSessionTrackingScenario + +- (void)run { +} + +@end diff --git a/features/fixtures/ios-swift-cocoapods/iOSTestApp/scenarios/ManualSessionScenario.h b/features/fixtures/ios-swift-cocoapods/iOSTestApp/scenarios/ManualSessionScenario.h new file mode 100644 index 000000000..64f8cc104 --- /dev/null +++ b/features/fixtures/ios-swift-cocoapods/iOSTestApp/scenarios/ManualSessionScenario.h @@ -0,0 +1,13 @@ +// +// Created by Jamie Lynch on 07/06/2018. +// Copyright (c) 2018 Bugsnag. All rights reserved. +// + +#import +#import "Scenario.h" + +/** + * Sends a manual session payload to Bugsnag. + */ +@interface ManualSessionScenario : Scenario +@end diff --git a/features/fixtures/ios-swift-cocoapods/iOSTestApp/scenarios/ManualSessionScenario.m b/features/fixtures/ios-swift-cocoapods/iOSTestApp/scenarios/ManualSessionScenario.m new file mode 100644 index 000000000..dd4cb91e0 --- /dev/null +++ b/features/fixtures/ios-swift-cocoapods/iOSTestApp/scenarios/ManualSessionScenario.m @@ -0,0 +1,15 @@ +// +// Created by Jamie Lynch on 07/06/2018. +// Copyright (c) 2018 Bugsnag. All rights reserved. +// + +#import "ManualSessionScenario.h" + +@implementation ManualSessionScenario + +- (void)run { + [self.config setUser:@"123" withName:@"Joe Bloggs" andEmail:@"user@example.com"]; + [Bugsnag startSession]; +} + +@end diff --git a/features/fixtures/ios-swift-cocoapods/iOSTestApp/scenarios/Scenario.h b/features/fixtures/ios-swift-cocoapods/iOSTestApp/scenarios/Scenario.h index 58a05ae81..d5adcad60 100644 --- a/features/fixtures/ios-swift-cocoapods/iOSTestApp/scenarios/Scenario.h +++ b/features/fixtures/ios-swift-cocoapods/iOSTestApp/scenarios/Scenario.h @@ -6,6 +6,10 @@ #import #import +@interface Bugsnag() ++ (id) notifier; +@end + @interface Scenario : NSObject @property (strong, nonatomic, nonnull) BugsnagConfiguration *config; @@ -22,4 +26,6 @@ - (void)startBugsnag; +- (void)flushAllSessions; + @end diff --git a/features/fixtures/ios-swift-cocoapods/iOSTestApp/scenarios/Scenario.m b/features/fixtures/ios-swift-cocoapods/iOSTestApp/scenarios/Scenario.m index 00feeff4e..5d98afa6f 100644 --- a/features/fixtures/ios-swift-cocoapods/iOSTestApp/scenarios/Scenario.m +++ b/features/fixtures/ios-swift-cocoapods/iOSTestApp/scenarios/Scenario.m @@ -6,7 +6,6 @@ #import "Scenario.h" - @implementation Scenario + (Scenario *)createScenarioNamed:(NSString *)className @@ -31,7 +30,6 @@ + (Scenario *)createScenarioNamed:(NSString *)className return [(Scenario *)obj initWithConfig:config]; } - - (instancetype)initWithConfig:(BugsnagConfiguration *)config { if (self = [super init]) { self.config = config; @@ -46,4 +44,10 @@ - (void)startBugsnag { [Bugsnag startBugsnagWithConfiguration:self.config]; } +- (void)flushAllSessions { + id notifier = [Bugsnag notifier]; + id sessionTracker = [notifier valueForKey:@"sessionTracker"]; + [sessionTracker performSelector:@selector(send)]; +} + @end diff --git a/features/session_tracking.feature b/features/session_tracking.feature new file mode 100644 index 000000000..5d09ec56b --- /dev/null +++ b/features/session_tracking.feature @@ -0,0 +1,34 @@ +Feature: Session Tracking + +Scenario: Automatic Session Tracking sends + When I run "AutoSessionScenario" with the defaults on "iPhone8-11.2" + And I wait for 65 seconds + Then I should receive a request + And the request is a valid for the session tracking API + And the "Bugsnag-API-Key" header equals "a35a2a72bd230ac0aa0f52715bbdc6aa" + And the payload field "notifier.name" equals "iOS Bugsnag Notifier" + And the payload field "sessions" is an array with 1 element + + # N.B. user.id is null by default if not configured by the developer. + And the session "user.id" is null + And the session "id" is not null + And the session "startedAt" is not null + +Scenario: Manual Session sends + When I run "ManualSessionScenario" with the defaults on "iPhone8-11.2" + And I wait for 65 seconds + Then I should receive a request + And the request is a valid for the session tracking API + And the "Bugsnag-API-Key" header equals "a35a2a72bd230ac0aa0f52715bbdc6aa" + And the payload field "notifier.name" equals "iOS Bugsnag Notifier" + And the payload field "sessions" is an array with 1 element + And the session "user.id" equals "123" + And the session "user.email" equals "user@example.com" + And the session "user.name" equals "Joe Bloggs" + And the session "id" is not null + And the session "startedAt" is not null + +Scenario: Disabled Session Tracking sends no requests + When I run "DisabledSessionTrackingScenario" with the defaults on "iPhone8-11.2" + And I wait for 65 seconds + Then I should receive 0 requests