diff --git a/CHANGELOG.md b/CHANGELOG.md index b31c1230856..59794f90337 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Record changes to network connectivity in breadcrumbs (#3232) + ## 8.11.0 ### Features diff --git a/Sources/Sentry/SentryBreadcrumbTracker.m b/Sources/Sentry/SentryBreadcrumbTracker.m index 8efbdaa9aee..d337b494d77 100644 --- a/Sources/Sentry/SentryBreadcrumbTracker.m +++ b/Sources/Sentry/SentryBreadcrumbTracker.m @@ -6,6 +6,7 @@ #import "SentryDependencyContainer.h" #import "SentryHub.h" #import "SentryLog.h" +#import "SentryReachability.h" #import "SentryScope.h" #import "SentrySwift.h" #import "SentrySwizzle.h" @@ -15,7 +16,7 @@ # import #elif TARGET_OS_OSX || TARGET_OS_MACCATALYST # import -#endif +#endif // !TARGET_OS_WATCH NS_ASSUME_NONNULL_BEGIN @@ -31,16 +32,14 @@ @implementation SentryBreadcrumbTracker -- (instancetype)init -{ - return [super init]; -} - - (void)startWithDelegate:(id)delegate { _delegate = delegate; [self addEnabledCrumb]; [self trackApplicationUIKitNotifications]; +#if !TARGET_OS_WATCH + [self trackNetworkConnectivityChanges]; +#endif // !TARGET_OS_WATCH } - (void)startSwizzle @@ -56,7 +55,7 @@ - (void)stop #if SENTRY_HAS_UIKIT [SentryDependencyContainer.sharedInstance.swizzleWrapper removeSwizzleSendActionForKey:SentryBreadcrumbTrackerSwizzleSendAction]; -#endif +#endif // SENTRY_HAS_UIKIT _delegate = nil; } @@ -70,10 +69,10 @@ - (void)trackApplicationUIKitNotifications // Will resign Active notification is the nearest one to // UIApplicationDidEnterBackgroundNotification NSNotificationName backgroundNotificationName = NSApplicationWillResignActiveNotification; -#else +#else // TARGET_OS_WATCH SENTRY_LOG_DEBUG(@"NO UIKit, OSX and Catalyst -> [SentryBreadcrumbTracker " @"trackApplicationUIKitNotifications] does nothing."); -#endif +#endif // !TARGET_OS_WATCH // not available for macOS #if SENTRY_HAS_UIKIT @@ -90,9 +89,9 @@ - (void)trackApplicationUIKitNotifications crumb.message = @"Low memory"; [self.delegate addBreadcrumb:crumb]; }]; -#endif +#endif // SENTRY_HAS_UIKIT -#if SENTRY_HAS_UIKIT || TARGET_OS_OSX || TARGET_OS_MACCATALYST +#if !TARGET_OS_WATCH [NSNotificationCenter.defaultCenter addObserverForName:backgroundNotificationName object:nil queue:nil @@ -114,8 +113,24 @@ - (void)trackApplicationUIKitNotifications withDataKey:@"state" withDataValue:@"foreground"]; }]; -#endif +#endif // !TARGET_OS_WATCH +} + +#if !TARGET_OS_WATCH +- (void)trackNetworkConnectivityChanges +{ + [SentryDependencyContainer.sharedInstance.reachability + monitorURL:[NSURL URLWithString:@"https://sentry.io"] + usingCallback:^(BOOL connected, NSString *_Nonnull typeDescription) { + SentryBreadcrumb *crumb = + [[SentryBreadcrumb alloc] initWithLevel:kSentryLevelInfo + category:@"device.connectivity"]; + crumb.type = @"connectivity"; + crumb.data = [NSDictionary dictionaryWithObject:typeDescription forKey:@"connectivity"]; + [self.delegate addBreadcrumb:crumb]; + }]; } +#endif // !TARGET_OS_WATCH - (void)addBreadcrumbWithType:(NSString *)type withCategory:(NSString *)category @@ -155,7 +170,7 @@ + (BOOL)avoidSender:(id)sender forTarget:(id)target action:(NSString *)action } return NO; } -#endif +#endif // SENTRY_HAS_UIKIT - (void)swizzleSendAction { @@ -183,9 +198,9 @@ - (void)swizzleSendAction } forKey:SentryBreadcrumbTrackerSwizzleSendAction]; -#else +#else // !SENTRY_HAS_UIKIT SENTRY_LOG_DEBUG(@"NO UIKit -> [SentryBreadcrumbTracker swizzleSendAction] does nothing."); -#endif +#endif // SENTRY_HAS_UIKIT } - (void)swizzleViewDidAppear @@ -223,9 +238,9 @@ - (void)swizzleViewDidAppear }), mode, swizzleViewDidAppearKey); # pragma clang diagnostic pop -#else +#else // !SENTRY_HAS_UIKIT SENTRY_LOG_DEBUG(@"NO UIKit -> [SentryBreadcrumbTracker swizzleViewDidAppear] does nothing."); -#endif +#endif // SENTRY_HAS_UIKIT } #if SENTRY_HAS_UIKIT @@ -287,7 +302,7 @@ + (NSDictionary *)fetchInfoAboutViewController:(UIViewController *)controller return info; } -#endif +#endif // SENTRY_HAS_UIKIT @end diff --git a/Sources/Sentry/SentryDependencyContainer.m b/Sources/Sentry/SentryDependencyContainer.m index 5f566ce23da..778755882f6 100644 --- a/Sources/Sentry/SentryDependencyContainer.m +++ b/Sources/Sentry/SentryDependencyContainer.m @@ -35,6 +35,10 @@ # import "SentryUIDeviceWrapper.h" #endif // TARGET_OS_IOS +#if !TARGET_OS_WATCH +# import "SentryReachability.h" +#endif // !TARGET_OS_WATCH + @implementation SentryDependencyContainer static SentryDependencyContainer *instance; @@ -368,4 +372,18 @@ - (SentryMXManager *)metricKitManager #endif // SENTRY_HAS_METRIC_KIT +#if !TARGET_OS_WATCH +- (SentryReachability *)reachability +{ + if (_reachability == nil) { + @synchronized(sentryDependencyContainerLock) { + if (_reachability == nil) { + _reachability = [[SentryReachability alloc] init]; + } + } + } + return _reachability; +} +#endif // !TARGET_OS_WATCH + @end diff --git a/Sources/Sentry/SentryReachability.m b/Sources/Sentry/SentryReachability.m index 1ec317392fb..4355f019e22 100644 --- a/Sources/Sentry/SentryReachability.m +++ b/Sources/Sentry/SentryReachability.m @@ -36,9 +36,9 @@ static SCNetworkReachabilityFlags sentry_current_reachability_state = kSCNetworkReachabilityFlagsUninitialized; -static NSString *const SentryConnectivityCellular = @"cellular"; -static NSString *const SentryConnectivityWiFi = @"wifi"; -static NSString *const SentryConnectivityNone = @"none"; +NSString *const SentryConnectivityCellular = @"cellular"; +NSString *const SentryConnectivityWiFi = @"wifi"; +NSString *const SentryConnectivityNone = @"none"; /** * Check whether the connectivity change should be noted or ignored. diff --git a/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h b/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h index 213a898d323..052d4faa678 100644 --- a/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h +++ b/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h @@ -34,6 +34,10 @@ @class SentryUIDeviceWrapper; #endif // TARGET_OS_IOS +#if !TARGET_OS_WATCH +@class SentryReachability; +#endif // !TARGET_OS_WATCH + NS_ASSUME_NONNULL_BEGIN @interface SentryDependencyContainer : NSObject @@ -76,6 +80,10 @@ SENTRY_NO_INIT @property (nonatomic, strong) SentryUIDeviceWrapper *uiDeviceWrapper; #endif // TARGET_OS_IOS +#if !TARGET_OS_WATCH +@property (nonatomic, strong) SentryReachability *reachability; +#endif // !TARGET_OS_WATCH + - (SentryANRTracker *)getANRTracker:(NSTimeInterval)timeout; #if SENTRY_HAS_METRIC_KIT diff --git a/Sources/Sentry/include/SentryReachability.h b/Sources/Sentry/include/SentryReachability.h index 46fc6501938..e50ea52d8cd 100644 --- a/Sources/Sentry/include/SentryReachability.h +++ b/Sources/Sentry/include/SentryReachability.h @@ -24,6 +24,7 @@ // THE SOFTWARE. // +#import "SentryDefines.h" #import #if !TARGET_OS_WATCH @@ -38,6 +39,10 @@ NSString *SentryConnectivityFlagRepresentation(SCNetworkReachabilityFlags flags) BOOL SentryConnectivityShouldReportChange(SCNetworkReachabilityFlags flags); +SENTRY_EXTERN NSString *const SentryConnectivityCellular; +SENTRY_EXTERN NSString *const SentryConnectivityWiFi; +SENTRY_EXTERN NSString *const SentryConnectivityNone; + /** * Function signature to connectivity monitoring callback of @c SentryReachability * @param connected @c YES if the monitored URL is reachable diff --git a/Tests/SentryTests/Integrations/Breadcrumbs/SentryBreadcrumbTrackerTests.swift b/Tests/SentryTests/Integrations/Breadcrumbs/SentryBreadcrumbTrackerTests.swift index 270cf3bb613..213a4e366d6 100644 --- a/Tests/SentryTests/Integrations/Breadcrumbs/SentryBreadcrumbTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Breadcrumbs/SentryBreadcrumbTrackerTests.swift @@ -29,6 +29,31 @@ class SentryBreadcrumbTrackerTests: XCTestCase { XCTAssertEqual(0, dict?.count) } + func testNetworkConnectivityChangeBreadcrumbs() throws { + let testReachability = TestSentryReachability() + SentryDependencyContainer.sharedInstance().reachability = testReachability + let sut = SentryBreadcrumbTracker() + sut.start(with: delegate) + let states = [SentryConnectivityCellular, + SentryConnectivityWiFi, + SentryConnectivityNone, + SentryConnectivityWiFi, + SentryConnectivityCellular, + SentryConnectivityWiFi + ] + states.forEach { + testReachability.setReachabilityState(state: $0) + } + sut.stop() + XCTAssertEqual(delegate.addCrumbInvocations.count, states.count + 1) // 1 breadcrumb for the tracker start + try states.enumerated().forEach { + let crumb = delegate.addCrumbInvocations.invocations[$0.offset + 1] + XCTAssertEqual(crumb.type, "connectivity") + XCTAssertEqual(crumb.category, "device.connectivity") + XCTAssertEqual(try XCTUnwrap(crumb.data?["connectivity"] as? String), $0.element) + } + } + func testSwizzlingStarted_ViewControllerAppears_AddsUILifeCycleBreadcrumb() { let scope = Scope() let client = TestClient(options: Options()) diff --git a/Tests/SentryTests/Networking/SentryReachabilityTests.m b/Tests/SentryTests/Networking/SentryReachabilityTests.m index 79c33ad8545..32996220dbe 100644 --- a/Tests/SentryTests/Networking/SentryReachabilityTests.m +++ b/Tests/SentryTests/Networking/SentryReachabilityTests.m @@ -20,20 +20,20 @@ - (void)tearDown - (void)testConnectivityRepresentations { - XCTAssertEqualObjects(@"none", SentryConnectivityFlagRepresentation(0)); - XCTAssertEqualObjects( - @"none", SentryConnectivityFlagRepresentation(kSCNetworkReachabilityFlagsIsDirect)); + XCTAssertEqualObjects(SentryConnectivityNone, SentryConnectivityFlagRepresentation(0)); + XCTAssertEqualObjects(SentryConnectivityNone, + SentryConnectivityFlagRepresentation(kSCNetworkReachabilityFlagsIsDirect)); # if SENTRY_HAS_UIKIT // kSCNetworkReachabilityFlagsIsWWAN does not exist on macOS - XCTAssertEqualObjects( - @"none", SentryConnectivityFlagRepresentation(kSCNetworkReachabilityFlagsIsWWAN)); - XCTAssertEqualObjects(@"cellular", + XCTAssertEqualObjects(SentryConnectivityNone, + SentryConnectivityFlagRepresentation(kSCNetworkReachabilityFlagsIsWWAN)); + XCTAssertEqualObjects(SentryConnectivityCellular, SentryConnectivityFlagRepresentation( kSCNetworkReachabilityFlagsIsWWAN | kSCNetworkReachabilityFlagsReachable)); # endif // SENTRY_HAS_UIKIT - XCTAssertEqualObjects( - @"wifi", SentryConnectivityFlagRepresentation(kSCNetworkReachabilityFlagsReachable)); - XCTAssertEqualObjects(@"wifi", + XCTAssertEqualObjects(SentryConnectivityWiFi, + SentryConnectivityFlagRepresentation(kSCNetworkReachabilityFlagsReachable)); + XCTAssertEqualObjects(SentryConnectivityWiFi, SentryConnectivityFlagRepresentation( kSCNetworkReachabilityFlagsReachable | kSCNetworkReachabilityFlagsIsDirect)); } diff --git a/Tests/SentryTests/Networking/TestSentryReachability.swift b/Tests/SentryTests/Networking/TestSentryReachability.swift index e56304086a3..afccf41a936 100644 --- a/Tests/SentryTests/Networking/TestSentryReachability.swift +++ b/Tests/SentryTests/Networking/TestSentryReachability.swift @@ -9,8 +9,12 @@ class TestSentryReachability: SentryReachability { self.block = block } + func setReachabilityState(state: String) { + block?(state != SentryConnectivityNone, state) + } + func triggerNetworkReachable() { - block?(true, "wifi") + block?(true, SentryConnectivityWiFi) } var stopMonitoringInvocations = Invocations()