diff --git a/.buildkite/pipeline.full.yml b/.buildkite/pipeline.full.yml index 0c7d6ddf8..b4797f529 100644 --- a/.buildkite/pipeline.full.yml +++ b/.buildkite/pipeline.full.yml @@ -22,7 +22,6 @@ steps: - "--app=/app/build/iOSTestApp.ipa" - "--farm=bs" - "--device=IOS_13" - - "--resilient" - "--appium-version=1.17.0" - "--fail-fast" - "--exclude=features/[h-z].*.feature" @@ -49,7 +48,6 @@ steps: - "--app=/app/build/iOSTestApp.ipa" - "--farm=bs" - "--device=IOS_13" - - "--resilient" - "--appium-version=1.17.0" - "--fail-fast" - "--exclude=features/[a-g].*.feature" @@ -76,7 +74,6 @@ steps: - "--app=/app/build/iOSTestApp.ipa" - "--farm=bs" - "--device=IOS_12" - - "--resilient" - "--appium-version=1.17.0" - "--fail-fast" - "--exclude=features/[h-z].*.feature" @@ -103,7 +100,6 @@ steps: - "--app=/app/build/iOSTestApp.ipa" - "--farm=bs" - "--device=IOS_12" - - "--resilient" - "--appium-version=1.17.0" - "--fail-fast" - "--exclude=features/[a-g].*.feature" diff --git a/.buildkite/pipeline.quick.yml b/.buildkite/pipeline.quick.yml index c03e523a7..dccf0b589 100644 --- a/.buildkite/pipeline.quick.yml +++ b/.buildkite/pipeline.quick.yml @@ -150,7 +150,6 @@ steps: - "--app=/app/build/iOSTestApp.ipa" - "--farm=bs" - "--device=IOS_14" - - "--resilient" - "--appium-version=1.17.0" - "--fail-fast" - "--exclude=features/[h-z].*.feature" @@ -178,7 +177,6 @@ steps: - "--app=/app/build/iOSTestApp.ipa" - "--farm=bs" - "--device=IOS_14" - - "--resilient" - "--appium-version=1.17.0" - "--fail-fast" - "--exclude=features/[a-g].*.feature" diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 5adc31b9a..81a876563 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -99,7 +99,7 @@ steps: agents: queue: opensource-mac-cocoa-10.13 commands: - - ./scripts/run-unit-tests.sh PLATFORM=iOS OS=9.3 DEVICE=iPhone\ 5s + - ./scripts/run-unit-tests.sh PLATFORM=iOS OS=9.3 DEVICE=iPhone\ 5s XCODEBUILD_EXTRA_ARGS=-skip-testing:BugsnagNetworkRequestPlugin-iOSTests artifact_paths: - logs/* @@ -117,7 +117,7 @@ steps: agents: queue: opensource-mac-cocoa-10.13 commands: - - ./scripts/run-unit-tests.sh PLATFORM=tvOS OS=9.2 + - ./scripts/run-unit-tests.sh PLATFORM=tvOS OS=9.2 XCODEBUILD_EXTRA_ARGS=-skip-testing:BugsnagNetworkRequestPlugin-tvOSTests artifact_paths: - logs/* @@ -143,7 +143,6 @@ steps: - "--app=/app/build/iOSTestApp.ipa" - "--farm=bs" - "--device=IOS_14" - - "--resilient" - "--appium-version=1.17.0" - "--fail-fast" - "--order=random" diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 6182bce9a..bcc7bd307 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -4,6 +4,9 @@ on: [pull_request] jobs: build: runs-on: macos-latest + env: + # Infer 1.0.1 cannot parse the iOS 15 SDK headers + DEVELOPER_DIR: /Applications/Xcode_12.5.1.app steps: - name: Checkout base branch uses: actions/checkout@v2 diff --git a/.jazzy.yaml b/.jazzy.yaml index 685015290..a1626a427 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -2,11 +2,11 @@ author_url: "https://www.bugsnag.com" author: "Bugsnag Inc" clean: false # avoid deleting docs/.git framework_root: "Bugsnag" -github_file_prefix: "https://github.com/bugsnag/bugsnag-cocoa/tree/v6.14.0/Bugsnag" +github_file_prefix: "https://github.com/bugsnag/bugsnag-cocoa/tree/v6.14.1/Bugsnag" github_url: "https://github.com/bugsnag/bugsnag-cocoa" hide_documentation_coverage: true module: "Bugsnag" -module_version: "6.14.0" +module_version: "6.14.1" objc: true output: "docs" readme: "README.md" diff --git a/Bugsnag.podspec.json b/Bugsnag.podspec.json index 7a256ca57..5ee43d4d2 100644 --- a/Bugsnag.podspec.json +++ b/Bugsnag.podspec.json @@ -1,6 +1,6 @@ { "name": "Bugsnag", - "version": "6.14.0", + "version": "6.14.1", "summary": "The Bugsnag crash reporting framework for Apple platforms.", "homepage": "https://bugsnag.com", "license": "MIT", @@ -9,7 +9,7 @@ }, "source": { "git": "https://github.com/bugsnag/bugsnag-cocoa.git", - "tag": "v6.14.0" + "tag": "v6.14.1" }, "frameworks": [ "Foundation", diff --git a/Bugsnag.xcodeproj/project.pbxproj b/Bugsnag.xcodeproj/project.pbxproj index be8afc1f6..1b98d4f55 100644 --- a/Bugsnag.xcodeproj/project.pbxproj +++ b/Bugsnag.xcodeproj/project.pbxproj @@ -873,7 +873,6 @@ E74628FD248907C100F92D67 /* BSG_KSJSONCodecObjC.m in Sources */ = {isa = PBXBuildFile; fileRef = 008969082486DAD000DC48C2 /* BSG_KSJSONCodecObjC.m */; }; E74628FF248907C100F92D67 /* BSG_KSMachHeaders.c in Sources */ = {isa = PBXBuildFile; fileRef = 008969102486DAD000DC48C2 /* BSG_KSMachHeaders.c */; }; E7462900248907C100F92D67 /* NSError+BSG_SimpleConstructor.m in Sources */ = {isa = PBXBuildFile; fileRef = 0089691E2486DAD000DC48C2 /* NSError+BSG_SimpleConstructor.m */; }; - E7462901248907C100F92D67 /* BSG_KSLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 008969262486DAD000DC48C2 /* BSG_KSLogger.m */; }; E7462901248907C100F92D67 /* BSG_KSLogger.c in Sources */ = {isa = PBXBuildFile; fileRef = 008969262486DAD000DC48C2 /* BSG_KSLogger.c */; }; E7462902248907C100F92D67 /* BSG_KSCrashState.m in Sources */ = {isa = PBXBuildFile; fileRef = 008969292486DAD000DC48C2 /* BSG_KSCrashState.m */; }; E7462904248907C100F92D67 /* BSG_KSCrashIdentifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 008969302486DAD000DC48C2 /* BSG_KSCrashIdentifier.m */; }; @@ -1092,7 +1091,6 @@ /* Begin PBXFileReference section */ 004E35392487B375007FBAE4 /* Tests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Tests-Bridging-Header.h"; sourceTree = ""; }; - 004E35482487B79B007FBAE4 /* .travis.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .travis.yml; sourceTree = SOURCE_ROOT; }; 008966A42486D43400DC48C2 /* BugsnagDeviceTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugsnagDeviceTest.m; sourceTree = ""; }; 008966A52486D43400DC48C2 /* BugsnagClientPayloadInfoTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugsnagClientPayloadInfoTest.m; sourceTree = ""; }; 008966A62486D43400DC48C2 /* BugsnagMetadataRedactionTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugsnagMetadataRedactionTest.m; sourceTree = ""; }; @@ -1152,7 +1150,6 @@ 008966E82486D43700DC48C2 /* KSString_Tests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KSString_Tests.m; sourceTree = ""; }; 008966E92486D43700DC48C2 /* KSCrashIdentifierTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KSCrashIdentifierTests.m; sourceTree = ""; }; 008966EA2486D43700DC48C2 /* BSG_KSMachTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BSG_KSMachTests.m; sourceTree = ""; }; - 008967AE2486D6E100DC48C2 /* Makefile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Makefile; sourceTree = SOURCE_ROOT; }; 008967B22486D9D700DC48C2 /* BugsnagBreadcrumbs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugsnagBreadcrumbs.m; sourceTree = ""; }; 008967B32486D9D700DC48C2 /* BugsnagBreadcrumbs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BugsnagBreadcrumbs.h; sourceTree = ""; }; 008967BB2486DA1900DC48C2 /* BugsnagClient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugsnagClient.m; sourceTree = ""; }; @@ -1278,7 +1275,6 @@ 00896A3F2486DBDD00DC48C2 /* BSGConfigurationBuilderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BSGConfigurationBuilderTests.m; sourceTree = ""; }; 00896A432486DBF000DC48C2 /* BugsnagConfigurationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugsnagConfigurationTests.m; sourceTree = ""; }; 00AD1C7224869B0E00A27979 /* Bugsnag.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Bugsnag.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 00AD1C7624869B0E00A27979 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = ../Framework/Info.plist; sourceTree = ""; }; 00AD1C7B24869B0E00A27979 /* Bugsnag-iOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Bugsnag-iOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 00AD1CAD24869C1200A27979 /* Bugsnag.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Bugsnag.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 00AD1CB524869C1200A27979 /* Bugsnag-macOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Bugsnag-macOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1290,16 +1286,8 @@ 00AD1EFF2486A17900A27979 /* BugsnagCrashSentry.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugsnagCrashSentry.m; sourceTree = ""; }; 00AD1F002486A17900A27979 /* BugsnagCrashSentry.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BugsnagCrashSentry.h; sourceTree = ""; }; 00AD1F012486A17900A27979 /* BugsnagSessionTracker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugsnagSessionTracker.m; sourceTree = ""; }; - 00AD209E2486A7CD00A27979 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; name = module.modulemap; path = ../Framework/module.modulemap; sourceTree = ""; }; 00E636B2248702A1006CBF1A /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 00E636B3248702A1006CBF1A /* LICENSE.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE.txt; sourceTree = ""; }; - 00E636B4248702A1006CBF1A /* UPGRADING.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = UPGRADING.md; path = ../UPGRADING.md; sourceTree = ""; }; - 00E636B8248702DA006CBF1A /* ORGANIZATION.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = ORGANIZATION.md; path = ../ORGANIZATION.md; sourceTree = ""; }; - 00E636B9248702DA006CBF1A /* CONTRIBUTING.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = CONTRIBUTING.md; path = ../CONTRIBUTING.md; sourceTree = ""; }; - 00E636BA248702DA006CBF1A /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = CHANGELOG.md; path = ../CHANGELOG.md; sourceTree = ""; }; - 00E636BB248702DA006CBF1A /* TESTING.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = TESTING.md; path = ../TESTING.md; sourceTree = ""; }; - 00E636C02487031D006CBF1A /* Bugsnag.podspec.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Bugsnag.podspec.json; sourceTree = SOURCE_ROOT; }; - 00E636C12487031D006CBF1A /* docker-compose.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = "docker-compose.yml"; sourceTree = SOURCE_ROOT; }; 00E636C324878FFC006CBF1A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 010FF28225ED2A8D00E4F2B0 /* BSGAppHangDetector.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BSGAppHangDetector.h; sourceTree = ""; }; 010FF28325ED2A8D00E4F2B0 /* BSGAppHangDetector.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BSGAppHangDetector.m; sourceTree = ""; }; @@ -1673,7 +1661,6 @@ 00E636B3248702A1006CBF1A /* LICENSE.txt */, 00AD1C7424869B0E00A27979 /* Bugsnag */, 00AD1C7F24869B0E00A27979 /* Tests */, - 00AD1CFA24869F2500A27979 /* Extras */, 00AD1C7324869B0E00A27979 /* Products */, E746292324890BFE00F92D67 /* Frameworks */, ); @@ -1954,24 +1941,6 @@ path = Storage; sourceTree = ""; }; - 00AD1CFA24869F2500A27979 /* Extras */ = { - isa = PBXGroup; - children = ( - 004E35482487B79B007FBAE4 /* .travis.yml */, - 00E636BA248702DA006CBF1A /* CHANGELOG.md */, - 00E636B9248702DA006CBF1A /* CONTRIBUTING.md */, - 00E636B8248702DA006CBF1A /* ORGANIZATION.md */, - 00E636BB248702DA006CBF1A /* TESTING.md */, - 00E636B4248702A1006CBF1A /* UPGRADING.md */, - 008967AE2486D6E100DC48C2 /* Makefile */, - 00AD209E2486A7CD00A27979 /* module.modulemap */, - 00E636C12487031D006CBF1A /* docker-compose.yml */, - 00E636C02487031D006CBF1A /* Bugsnag.podspec.json */, - 00AD1C7624869B0E00A27979 /* Info.plist */, - ); - path = Extras; - sourceTree = ""; - }; 3A700A7E24A63A8E0068CD1B /* include */ = { isa = PBXGroup; children = ( @@ -3141,7 +3110,7 @@ E74628FD248907C100F92D67 /* BSG_KSJSONCodecObjC.m in Sources */, E74628FF248907C100F92D67 /* BSG_KSMachHeaders.c in Sources */, E7462900248907C100F92D67 /* NSError+BSG_SimpleConstructor.m in Sources */, - E7462901248907C100F92D67 /* BSG_KSLogger.m in Sources */, + E7462901248907C100F92D67 /* BSG_KSLogger.c in Sources */, E7462901248907C100F92D67 /* BSG_KSLogger.c in Sources */, E7462902248907C100F92D67 /* BSG_KSCrashState.m in Sources */, E7462904248907C100F92D67 /* BSG_KSCrashIdentifier.m in Sources */, diff --git a/Bugsnag.xcodeproj/xcshareddata/xcschemes/Bugsnag-iOS.xcscheme b/Bugsnag.xcodeproj/xcshareddata/xcschemes/Bugsnag-iOS.xcscheme index 247e0a162..ef7bec63c 100644 --- a/Bugsnag.xcodeproj/xcshareddata/xcschemes/Bugsnag-iOS.xcscheme +++ b/Bugsnag.xcodeproj/xcshareddata/xcschemes/Bugsnag-iOS.xcscheme @@ -3,8 +3,8 @@ LastUpgradeVersion = "1140" version = "1.3"> + parallelizeBuildables = "NO" + buildImplicitDependencies = "NO"> + + + + - - - - - - - - - - - - - - - - - - - - diff --git a/Bugsnag.xcodeproj/xcshareddata/xcschemes/Bugsnag-macOS.xcscheme b/Bugsnag.xcodeproj/xcshareddata/xcschemes/Bugsnag-macOS.xcscheme index 07ad78b54..50267e893 100644 --- a/Bugsnag.xcodeproj/xcshareddata/xcschemes/Bugsnag-macOS.xcscheme +++ b/Bugsnag.xcodeproj/xcshareddata/xcschemes/Bugsnag-macOS.xcscheme @@ -3,8 +3,8 @@ LastUpgradeVersion = "1140" version = "1.3"> + parallelizeBuildables = "NO" + buildImplicitDependencies = "NO"> - - - - + disableMainThreadChecker = "YES" + codeCoverageEnabled = "YES" + shouldUseLaunchSchemeArgsEnv = "YES"> @@ -46,7 +40,24 @@ ReferencedContainer = "container:Bugsnag.xcodeproj"> + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - diff --git a/Bugsnag.xcodeproj/xcshareddata/xcschemes/Bugsnag-tvOS.xcscheme b/Bugsnag.xcodeproj/xcshareddata/xcschemes/Bugsnag-tvOS.xcscheme index b88443e77..33e09f99c 100644 --- a/Bugsnag.xcodeproj/xcshareddata/xcschemes/Bugsnag-tvOS.xcscheme +++ b/Bugsnag.xcodeproj/xcshareddata/xcschemes/Bugsnag-tvOS.xcscheme @@ -3,8 +3,8 @@ LastUpgradeVersion = "1140" version = "1.3"> + parallelizeBuildables = "NO" + buildImplicitDependencies = "NO"> + + + + - - - - - - - - - - - - - - - - - - - - diff --git a/Bugsnag/BugsnagCrashSentry.m b/Bugsnag/BugsnagCrashSentry.m index c3170a77f..4b75fccc7 100644 --- a/Bugsnag/BugsnagCrashSentry.m +++ b/Bugsnag/BugsnagCrashSentry.m @@ -11,6 +11,7 @@ #import "BSGFileLocations.h" #import "BSG_KSCrashAdvanced.h" +#import "BSG_KSMach.h" #import "BugsnagConfiguration.h" #import "BugsnagErrorTypes.h" #import "BugsnagLogger.h" @@ -28,12 +29,19 @@ - (void)install:(BugsnagConfiguration *)config onCrash:(BSGReportCallback)onCras // applies to unhandled errors ksCrash.threadTracingEnabled = config.sendThreads != BSGThreadSendPolicyNever; - BSG_KSCrashType crashTypes = config.autoDetectErrors ? [self mapKSToBSGCrashTypes:config.enabledErrorTypes] : 0; + BSG_KSCrashType crashTypes = 0; + if (config.autoDetectErrors) { + if (bsg_ksmachisBeingTraced()) { + bsg_log_info(@"Unhandled errors will not be reported because a debugger is attached"); + } else { + crashTypes = [self mapKSToBSGCrashTypes:config.enabledErrorTypes]; + } + } // In addition to installing crash handlers, -[BSG_KSCrash install:] initializes various // subsystems that Bugsnag relies on, so needs to be called even if autoDetectErrors is disabled. if ((![ksCrash install:crashTypes directory:[BSGFileLocations current].kscrashReports] && crashTypes)) { - bsg_log_err(@"Failed to install crash handler. No exceptions will be reported!"); + bsg_log_err(@"Failed to install crash handlers; no exceptions or crashes will be reported"); } } diff --git a/Bugsnag/BugsnagSessionTracker.m b/Bugsnag/BugsnagSessionTracker.m index 7015709c3..ed89859a6 100644 --- a/Bugsnag/BugsnagSessionTracker.m +++ b/Bugsnag/BugsnagSessionTracker.m @@ -8,6 +8,7 @@ #import "BugsnagSessionTracker.h" +#import "BSGFileLocations.h" #import "BSG_KSSystemInfo.h" #import "BugsnagApp+Private.h" #import "BugsnagClient+Private.h" @@ -19,7 +20,6 @@ #import "BugsnagSessionFileStore.h" #import "BugsnagSessionTrackingApiClient.h" #import "BugsnagSessionTrackingPayload.h" -#import "BSGFileLocations.h" #if TARGET_OS_IOS || TARGET_OS_TV #import "BSGUIKit.h" @@ -67,6 +67,15 @@ - (instancetype)initWithConfig:(BugsnagConfiguration *)config } - (void)startWithNotificationCenter:(NSNotificationCenter *)notificationCenter isInForeground:(BOOL)isInForeground { + if ([BSG_KSSystemInfo isRunningInAppExtension]) { + // UIApplication lifecycle notifications and UIApplicationState, which the automatic session tracking logic + // depends on, are not available in app extensions. + if (self.config.autoTrackSessions) { + bsg_log_info(@"Automatic session tracking is not supported in app extensions"); + } + return; + } + if (isInForeground) { [self startNewSessionIfAutoCaptureEnabled]; } else { diff --git a/Bugsnag/Helpers/BSGInternalErrorReporter.h b/Bugsnag/Helpers/BSGInternalErrorReporter.h index 781750169..912af1d6c 100644 --- a/Bugsnag/Helpers/BSGInternalErrorReporter.h +++ b/Bugsnag/Helpers/BSGInternalErrorReporter.h @@ -39,6 +39,9 @@ FOUNDATION_EXPORT NSString *BSGErrorDescription(NSError *error); @property (class, nonatomic) BSGInternalErrorReporter *sharedInstance; +/// Runs the block immediately if sharedInstance exists, otherwise runs the block once sharedInstance has been created. ++ (void)performBlock:(void (^)(BSGInternalErrorReporter *))block; + - (instancetype)initWithDataSource:(id)dataSource NS_DESIGNATED_INITIALIZER; - (instancetype)init UNAVAILABLE_ATTRIBUTE; diff --git a/Bugsnag/Helpers/BSGInternalErrorReporter.m b/Bugsnag/Helpers/BSGInternalErrorReporter.m index 237c5de96..1188e744c 100644 --- a/Bugsnag/Helpers/BSGInternalErrorReporter.m +++ b/Bugsnag/Helpers/BSGInternalErrorReporter.m @@ -51,6 +51,7 @@ @interface BSGInternalErrorReporter () @implementation BSGInternalErrorReporter static BSGInternalErrorReporter *sharedInstance_; +static void (^ startupBlock_)(BSGInternalErrorReporter *); + (BSGInternalErrorReporter *)sharedInstance { return sharedInstance_; @@ -58,6 +59,18 @@ + (BSGInternalErrorReporter *)sharedInstance { + (void)setSharedInstance:(BSGInternalErrorReporter *)sharedInstance { sharedInstance_ = sharedInstance; + if (startupBlock_ && sharedInstance_) { + startupBlock_(sharedInstance_); + startupBlock_ = nil; + } +} + ++ (void)performBlock:(void (^)(BSGInternalErrorReporter *))block { + if (sharedInstance_) { + block(sharedInstance_); + } else { + startupBlock_ = [block copy]; + } } - (instancetype)initWithDataSource:(id)dataSource { diff --git a/Bugsnag/Helpers/BugsnagKeys.h b/Bugsnag/Helpers/BugsnagKeys.h index d4c09a8ef..4cd149fbb 100644 --- a/Bugsnag/Helpers/BugsnagKeys.h +++ b/Bugsnag/Helpers/BugsnagKeys.h @@ -74,7 +74,6 @@ extern NSString *const BSGKeyPersistUser; extern NSString *const BSGKeyProduction; extern NSString *const BSGKeyReason; extern NSString *const BSGKeyRedactedKeys; -extern NSString *const BSGKeyRedaction; extern NSString *const BSGKeyReleaseStage; extern NSString *const BSGKeySendThreads; extern NSString *const BSGKeySession; diff --git a/Bugsnag/Helpers/BugsnagKeys.m b/Bugsnag/Helpers/BugsnagKeys.m index d1891df41..0ae56b646 100644 --- a/Bugsnag/Helpers/BugsnagKeys.m +++ b/Bugsnag/Helpers/BugsnagKeys.m @@ -73,7 +73,6 @@ NSString *const BSGKeyProduction = @"production"; NSString *const BSGKeyReason = @"reason"; NSString *const BSGKeyRedactedKeys = @"redactedKeys"; -NSString *const BSGKeyRedaction = @"[REDACTED]"; NSString *const BSGKeyReleaseStage = @"releaseStage"; NSString *const BSGKeySendThreads = @"sendThreads"; NSString *const BSGKeySession = @"session"; diff --git a/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSCrashC.c b/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSCrashC.c index 5933b2cbe..935cf4812 100644 --- a/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSCrashC.c +++ b/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSCrashC.c @@ -165,8 +165,10 @@ BSG_KSCrashType bsg_kscrash_setHandlingCrashTypes(BSG_KSCrashType crashTypes) { if (bsg_g_installed) { bsg_kscrashsentry_uninstall(~crashTypes); - crashTypes = bsg_kscrashsentry_installWithContext( - &context->crash, crashTypes, (void(*)(void *))bsg_kscrash_i_onCrash); + if (crashTypes) { + crashTypes = bsg_kscrashsentry_installWithContext( + &context->crash, crashTypes, (void(*)(void *))bsg_kscrash_i_onCrash); + } } return crashTypes; diff --git a/Bugsnag/Payload/BugsnagEvent.m b/Bugsnag/Payload/BugsnagEvent.m index a5633fb50..09af8afed 100644 --- a/Bugsnag/Payload/BugsnagEvent.m +++ b/Bugsnag/Payload/BugsnagEvent.m @@ -39,6 +39,7 @@ #import "BugsnagThread+Private.h" #import "BugsnagUser+Private.h" +static NSString * const RedactedMetadataValue = @"[REDACTED]"; id BSGLoadConfigValue(NSDictionary *report, NSString *valueName) { NSString *keypath = [NSString stringWithFormat:@"user.config.%@", valueName]; @@ -458,8 +459,17 @@ - (BOOL)shouldBeSent { (self.enabledReleaseStages.count == 0); } -- (NSArray *)serializeBreadcrumbs { - return [[self breadcrumbs] valueForKeyPath:NSStringFromSelector(@selector(objectValue))]; +- (NSArray *)serializeBreadcrumbsWithRedactedKeys:(NSSet *)redactedKeys { + return BSGArrayMap(self.breadcrumbs, ^NSDictionary * (BugsnagBreadcrumb *breadcrumb) { + NSMutableDictionary *dictionary = [[breadcrumb objectValue] mutableCopy]; + NSDictionary *metadata = dictionary[BSGKeyMetadata]; + NSMutableDictionary *redactedMetadata = [NSMutableDictionary dictionary]; + for (NSString *key in metadata) { + redactedMetadata[key] = [self redactedMetadataValue:metadata[key] forKey:key redactedKeys:redactedKeys]; + } + dictionary[BSGKeyMetadata] = redactedMetadata; + return dictionary; + }); } - (void)attachCustomStacktrace:(NSArray *)frames withType:(NSString *)type { @@ -540,15 +550,13 @@ - (NSDictionary *)toJsonWithRedactedKeys:(NSSet *)redactedKeys { }); event[BSGKeyThreads] = [BugsnagThread serializeThreads:self.threads]; - - // Build Event event[BSGKeySeverity] = BSGFormatSeverity(self.severity); - event[BSGKeyBreadcrumbs] = [self serializeBreadcrumbs]; + event[BSGKeyBreadcrumbs] = [self serializeBreadcrumbsWithRedactedKeys:redactedKeys]; - // add metadata NSMutableDictionary *metadata = [[[self metadata] toDictionary] mutableCopy]; @try { - event[BSGKeyMetadata] = [self sanitiseMetadata:metadata redactedKeys:redactedKeys]; + [self redactKeys:redactedKeys inMetadata:metadata]; + event[BSGKeyMetadata] = metadata; } @catch (NSException *exception) { bsg_log_err(@"An exception was thrown while sanitising metadata: %@", exception); } @@ -599,7 +607,7 @@ - (NSDictionary *)toJsonWithRedactedKeys:(NSSet *)redactedKeys { return event; } -- (NSMutableDictionary *)sanitiseMetadata:(NSMutableDictionary *)metadata redactedKeys:(NSSet *)redactedKeys { +- (void)redactKeys:(NSSet *)redactedKeys inMetadata:(NSMutableDictionary *)metadata { for (NSString *sectionKey in [metadata allKeys]) { if ([metadata[sectionKey] isKindOfClass:[NSDictionary class]]) { metadata[sectionKey] = [metadata[sectionKey] mutableCopy]; @@ -614,21 +622,19 @@ - (NSMutableDictionary *)sanitiseMetadata:(NSMutableDictionary *)metadata redact if (section != nil) { // redact sensitive metadata values for (NSString *objKey in [section allKeys]) { - section[objKey] = [self sanitiseMetadataValue:section[objKey] key:objKey redactedKeys:redactedKeys]; + section[objKey] = [self redactedMetadataValue:section[objKey] forKey:objKey redactedKeys:redactedKeys]; } } } - return metadata; } -- (id)sanitiseMetadataValue:(id)value key:(NSString *)key redactedKeys:(NSSet *)redactedKeys { - if ([self isRedactedKey:key redactedKeys:redactedKeys]) { - return BSGKeyRedaction; +- (id)redactedMetadataValue:(id)value forKey:(NSString *)key redactedKeys:(NSSet *)redactedKeys { + if ([self redactedKeys:redactedKeys matches:key]) { + return RedactedMetadataValue; } else if ([value isKindOfClass:[NSDictionary class]]) { NSMutableDictionary *nestedDict = [(NSDictionary *)value mutableCopy]; - for (NSString *nestedKey in [nestedDict allKeys]) { - nestedDict[nestedKey] = [self sanitiseMetadataValue:nestedDict[nestedKey] key:nestedKey redactedKeys:redactedKeys]; + nestedDict[nestedKey] = [self redactedMetadataValue:nestedDict[nestedKey] forKey:nestedKey redactedKeys:redactedKeys]; } return nestedDict; } else { @@ -636,7 +642,7 @@ - (id)sanitiseMetadataValue:(id)value key:(NSString *)key redactedKeys:(NSSet *) } } -- (BOOL)isRedactedKey:(NSString *)key redactedKeys:(NSSet *)redactedKeys { +- (BOOL)redactedKeys:(NSSet *)redactedKeys matches:(NSString *)key { for (id obj in redactedKeys) { if ([obj isKindOfClass:[NSString class]]) { if ([[key lowercaseString] isEqualToString:[obj lowercaseString]]) { diff --git a/Bugsnag/Payload/BugsnagNotifier.m b/Bugsnag/Payload/BugsnagNotifier.m index 08350a753..4014137a2 100644 --- a/Bugsnag/Payload/BugsnagNotifier.m +++ b/Bugsnag/Payload/BugsnagNotifier.m @@ -23,7 +23,7 @@ - (instancetype)init { #else _name = @"Bugsnag Objective-C"; #endif - _version = @"6.14.0"; + _version = @"6.14.1"; _url = @"https://github.com/bugsnag/bugsnag-cocoa"; _dependencies = @[]; } diff --git a/Bugsnag/Storage/BSGFileLocations.m b/Bugsnag/Storage/BSGFileLocations.m index bb87ca6c3..f842ba3ae 100644 --- a/Bugsnag/Storage/BSGFileLocations.m +++ b/Bugsnag/Storage/BSGFileLocations.m @@ -15,7 +15,9 @@ static void ReportInternalError(NSString *errorClass, NSError *error) { NSString *file = @(__FILE__).lastPathComponent; NSString *message = BSGErrorDescription(error); NSString *groupingHash = [NSString stringWithFormat:@"%@: %@: %@ %ld", file, errorClass, error.domain, (long)error.code]; - [BSGInternalErrorReporter.sharedInstance reportErrorWithClass:errorClass message:message diagnostics:error.userInfo groupingHash:groupingHash]; + [BSGInternalErrorReporter performBlock:^(BSGInternalErrorReporter *reporter) { + [reporter reportErrorWithClass:errorClass message:message diagnostics:error.userInfo groupingHash:groupingHash]; + }]; } static BOOL ensureDirExists(NSString *path) { @@ -34,7 +36,6 @@ static BOOL ensureDirExists(NSString *path) { static NSString *rootDirectory(NSString *fsVersion) { // Default to an unusable location that will always fail. - static NSString* defaultRootPath = @"/"; static NSString* rootPath = @"/"; static dispatch_once_t onceToken; @@ -47,7 +48,7 @@ static BOOL ensureDirExists(NSString *path) { NSSearchPathDirectory directory = NSApplicationSupportDirectory; #endif NSError *error = nil; - NSURL *url = [NSFileManager.defaultManager URLForDirectory:directory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:&error]; + NSURL *url = [NSFileManager.defaultManager URLForDirectory:directory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:&error]; if (!url) { bsg_log_err(@"Could not locate directory for storage: %@", error); return; @@ -59,10 +60,7 @@ static BOOL ensureDirExists(NSString *path) { NSBundle.mainBundle.bundleIdentifier ?: NSProcessInfo.processInfo.processName, fsVersion]; - // If we can't even create the root dir, all is lost, and no file ops can be allowed. - if(!ensureDirExists(rootPath)) { - rootPath = defaultRootPath; - } + ensureDirExists(rootPath); }); return rootPath; @@ -70,11 +68,8 @@ static BOOL ensureDirExists(NSString *path) { static NSString *getAndCreateSubdir(NSString *rootPath, NSString *relativePath) { NSString *subdirPath = [rootPath stringByAppendingPathComponent:relativePath]; - if (ensureDirExists(subdirPath)) { - return subdirPath; - } - // Make the best of it, just return the root dir. - return rootPath; + ensureDirExists(subdirPath); + return subdirPath; } @implementation BSGFileLocations diff --git a/Bugsnag/include/Bugsnag/BugsnagConfiguration.h b/Bugsnag/include/Bugsnag/BugsnagConfiguration.h index bda89ff92..5bbe70a48 100644 --- a/Bugsnag/include/Bugsnag/BugsnagConfiguration.h +++ b/Bugsnag/include/Bugsnag/BugsnagConfiguration.h @@ -223,6 +223,8 @@ typedef BOOL (^BugsnagOnSessionBlock)(BugsnagSession *_Nonnull session); * Determines whether app sessions should be tracked automatically. By default this value is true. * If this value is updated after +[Bugsnag start] is called, only subsequent automatic sessions * will be captured. + * + * Note: automatic session tracking is not available in App Extensions. */ @property (nonatomic) BOOL autoTrackSessions; diff --git a/BugsnagNetworkRequestPlugin.podspec.json b/BugsnagNetworkRequestPlugin.podspec.json index f0b97a826..0e0fcf225 100644 --- a/BugsnagNetworkRequestPlugin.podspec.json +++ b/BugsnagNetworkRequestPlugin.podspec.json @@ -1,16 +1,16 @@ { "name": "BugsnagNetworkRequestPlugin", - "version": "6.14.0", + "version": "6.14.1", "summary": "Network request monitoring support for Bugsnag.", "homepage": "https://bugsnag.com", "license": "MIT", "authors": { "Bugsnag": "notifiers@bugsnag.com" }, - "readme": "https://raw.githubusercontent.com/bugsnag/bugsnag-cocoa/v6.14.0/BugsnagNetworkRequestPlugin/README.md", + "readme": "https://raw.githubusercontent.com/bugsnag/bugsnag-cocoa/v6.14.1/BugsnagNetworkRequestPlugin/README.md", "source": { "git": "https://github.com/bugsnag/bugsnag-cocoa.git", - "tag": "v6.14.0" + "tag": "v6.14.1" }, "dependencies": { "Bugsnag": "~> 6.13" diff --git a/BugsnagNetworkRequestPlugin/BugsnagNetworkRequestPlugin.xcodeproj/project.pbxproj b/BugsnagNetworkRequestPlugin/BugsnagNetworkRequestPlugin.xcodeproj/project.pbxproj index 9e9d8cf55..be14ec506 100644 --- a/BugsnagNetworkRequestPlugin/BugsnagNetworkRequestPlugin.xcodeproj/project.pbxproj +++ b/BugsnagNetworkRequestPlugin/BugsnagNetworkRequestPlugin.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 016113862716EFBE002A50D4 /* BSGURLSessionTracingProxyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 016113852716EFBE002A50D4 /* BSGURLSessionTracingProxyTests.m */; }; + 016113872716EFBE002A50D4 /* BSGURLSessionTracingProxyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 016113852716EFBE002A50D4 /* BSGURLSessionTracingProxyTests.m */; }; + 016113882716EFBE002A50D4 /* BSGURLSessionTracingProxyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 016113852716EFBE002A50D4 /* BSGURLSessionTracingProxyTests.m */; }; 0196AA1026FB395E008E54FC /* BSGURLSessionTracingDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0196AA0F26FB395E008E54FC /* BSGURLSessionTracingDelegateTests.m */; }; 0196AA1126FB395E008E54FC /* BSGURLSessionTracingDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0196AA0F26FB395E008E54FC /* BSGURLSessionTracingDelegateTests.m */; }; 0196AA1226FB395E008E54FC /* BSGURLSessionTracingDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0196AA0F26FB395E008E54FC /* BSGURLSessionTracingDelegateTests.m */; }; @@ -86,6 +89,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 016113852716EFBE002A50D4 /* BSGURLSessionTracingProxyTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BSGURLSessionTracingProxyTests.m; sourceTree = ""; }; 0196AA0F26FB395E008E54FC /* BSGURLSessionTracingDelegateTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BSGURLSessionTracingDelegateTests.m; sourceTree = ""; }; CB2430D726F1DA8A00CB5EC4 /* libBugsnagNetworkRequestPluginStatic.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libBugsnagNetworkRequestPluginStatic.a; sourceTree = BUILT_PRODUCTS_DIR; }; CB487A5926D7B109004F6B87 /* BugsnagNetworkRequestPlugin.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BugsnagNetworkRequestPlugin.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -213,6 +217,7 @@ isa = PBXGroup; children = ( 0196AA0F26FB395E008E54FC /* BSGURLSessionTracingDelegateTests.m */, + 016113852716EFBE002A50D4 /* BSGURLSessionTracingProxyTests.m */, CB487A6726D7B109004F6B87 /* BugsnagNetworkRequestPluginTests.m */, CB487A6926D7B109004F6B87 /* Info.plist */, ); @@ -536,6 +541,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 016113862716EFBE002A50D4 /* BSGURLSessionTracingProxyTests.m in Sources */, CB487AC226D7B9DB004F6B87 /* BugsnagNetworkRequestPluginTests.m in Sources */, 0196AA1026FB395E008E54FC /* BSGURLSessionTracingDelegateTests.m in Sources */, ); @@ -556,6 +562,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 016113872716EFBE002A50D4 /* BSGURLSessionTracingProxyTests.m in Sources */, CB487AC326D7B9DC004F6B87 /* BugsnagNetworkRequestPluginTests.m in Sources */, 0196AA1126FB395E008E54FC /* BSGURLSessionTracingDelegateTests.m in Sources */, ); @@ -576,6 +583,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 016113882716EFBE002A50D4 /* BSGURLSessionTracingProxyTests.m in Sources */, CB487AC426D7B9DD004F6B87 /* BugsnagNetworkRequestPluginTests.m in Sources */, 0196AA1226FB395E008E54FC /* BSGURLSessionTracingDelegateTests.m in Sources */, ); diff --git a/BugsnagNetworkRequestPlugin/BugsnagNetworkRequestPlugin/BSGURLSessionTracingDelegate.m b/BugsnagNetworkRequestPlugin/BugsnagNetworkRequestPlugin/BSGURLSessionTracingDelegate.m index 09384a20d..d993fbdea 100644 --- a/BugsnagNetworkRequestPlugin/BugsnagNetworkRequestPlugin/BSGURLSessionTracingDelegate.m +++ b/BugsnagNetworkRequestPlugin/BugsnagNetworkRequestPlugin/BSGURLSessionTracingDelegate.m @@ -36,13 +36,13 @@ - (BOOL)canTrace { + (nonnull NSString *)messageForResponse:(NSHTTPURLResponse *)response { if (response) { if (100 <= response.statusCode && response.statusCode < 400) { - return @"NSURLSession succeeded"; + return @"NSURLSession request succeeded"; } if (400 <= response.statusCode && response.statusCode < 500) { - return @"NSURLSession failed"; + return @"NSURLSession request failed"; } } - return @"NSURLSession error"; + return @"NSURLSession request error"; } + (nullable NSDictionary *)urlParamsForQueryItems:(nullable NSArray *)queryItems { @@ -83,9 +83,7 @@ + (nonnull NSString *)URLStringWithoutQueryForComponents:(nonnull NSURLComponent return string; } -- (void)URLSession:(NSURLSession *)session - task:(NSURLSessionTask *)task -didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics +- (void)URLSession:(__unused NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)) { if (g_sink != nil) { // Note: Cannot use metrics transaction request because it might have a 0 length HTTP body. diff --git a/BugsnagNetworkRequestPlugin/BugsnagNetworkRequestPlugin/BSGURLSessionTracingProxy.m b/BugsnagNetworkRequestPlugin/BugsnagNetworkRequestPlugin/BSGURLSessionTracingProxy.m index d68464f30..d01e8a366 100644 --- a/BugsnagNetworkRequestPlugin/BugsnagNetworkRequestPlugin/BSGURLSessionTracingProxy.m +++ b/BugsnagNetworkRequestPlugin/BugsnagNetworkRequestPlugin/BSGURLSessionTracingProxy.m @@ -11,7 +11,7 @@ @interface BSGURLSessionTracingProxy () -@property (nonatomic, strong, nonnull) id delegate; +@property (nonatomic, strong, nonnull) id delegate; @property (nonatomic, strong, nonnull) BSGURLSessionTracingDelegate *tracingDelegate; @end @@ -47,20 +47,17 @@ - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { return [(NSObject *)self.delegate methodSignatureForSelector:aSelector]; } -- (void)forwardInvocation:(NSInvocation *)invocation { - if (sel_isEqual(invocation.selector, METRICS_SELECTOR)) { - if (self.tracingDelegate.canTrace) { - invocation.target = self.tracingDelegate; - [invocation invoke]; - } +- (void)forwardInvocation:(NSInvocation *)invocation { + [invocation invokeWithTarget:self.delegate]; +} - if (![self.delegate respondsToSelector:METRICS_SELECTOR]) { - return; - } +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics +API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)) { + [self.tracingDelegate URLSession:session task:task didFinishCollectingMetrics:metrics]; + + if ([self.delegate respondsToSelector:METRICS_SELECTOR]) { + [self.delegate URLSession:session task:task didFinishCollectingMetrics:metrics]; } - - invocation.target = self.delegate; - [invocation invoke]; } @end diff --git a/BugsnagNetworkRequestPlugin/BugsnagNetworkRequestPluginTests/BSGURLSessionTracingProxyTests.m b/BugsnagNetworkRequestPlugin/BugsnagNetworkRequestPluginTests/BSGURLSessionTracingProxyTests.m new file mode 100644 index 000000000..d1b1c60e3 --- /dev/null +++ b/BugsnagNetworkRequestPlugin/BugsnagNetworkRequestPluginTests/BSGURLSessionTracingProxyTests.m @@ -0,0 +1,132 @@ +// +// BSGURLSessionTracingProxyTests.m +// BugsnagNetworkRequestPlugin +// +// Created by Nick Dowell on 13/10/2021. +// + +#import + +#import + +#include "BSGURLSessionTracingProxy.h" + +#ifndef XCTSkip +#define XCTSkip(...) NSLog(__VA_ARGS__); return +#endif + +@interface BSGURLSessionTracingProxyTests_DidReceiveDataStub : NSObject +@property (nonatomic) BOOL didReceiveDataWasCalled; +@end + +@interface BSGURLSessionTracingProxyTests_DidFinishCollectingMetricsStub : NSObject +@property (nonatomic) BOOL didFinishCollectingMetricsWasCalled; +@end + +@interface BSGURLSessionTracingProxyTests_TracingStub : NSObject +@property (nonatomic) BOOL didFinishCollectingMetricsWasCalled; +@end + +#pragma mark - + +@interface BSGURLSessionTracingProxyTests : XCTestCase +@end + +@implementation BSGURLSessionTracingProxyTests + +- (void)testDidReceiveDataIsForwarded { + BSGURLSessionTracingProxyTests_DidReceiveDataStub *sessionDelegate = [[BSGURLSessionTracingProxyTests_DidReceiveDataStub alloc] init]; + BSGURLSessionTracingProxyTests_TracingStub *tracingDelegate = [[BSGURLSessionTracingProxyTests_TracingStub alloc] init]; + id proxy = [[BSGURLSessionTracingProxy alloc] initWithDelegate:sessionDelegate tracingDelegate:(id)tracingDelegate]; + + NSURLSession *session = [NSURLSession sessionWithConfiguration:NSURLSessionConfiguration.ephemeralSessionConfiguration]; + NSURLSessionDataTask *task = [[NSURLSessionDataTask alloc] init]; + + XCTAssertTrue([proxy respondsToSelector:@selector(URLSession:dataTask:didReceiveData:)]); + [proxy URLSession:session dataTask:task didReceiveData:[NSData data]]; + XCTAssertTrue(sessionDelegate.didReceiveDataWasCalled, @"The session delegate's method should be called"); +} + +- (void)testDidFinishCollectingMetricsIsForwardedToSessionDelegate { + if (@available(macOS 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *)) { + BSGURLSessionTracingProxyTests_DidFinishCollectingMetricsStub *sessionDelegate = + [[BSGURLSessionTracingProxyTests_DidFinishCollectingMetricsStub alloc] init]; + BSGURLSessionTracingProxyTests_TracingStub *tracingDelegate = [[BSGURLSessionTracingProxyTests_TracingStub alloc] init]; + id proxy = [[BSGURLSessionTracingProxy alloc] initWithDelegate:sessionDelegate tracingDelegate:(id)tracingDelegate]; + + NSURLSession *session = [NSURLSession sessionWithConfiguration:NSURLSessionConfiguration.ephemeralSessionConfiguration]; + NSURLSessionDataTask *task = [[NSURLSessionDataTask alloc] init]; + + XCTAssertFalse([proxy respondsToSelector:@selector(URLSession:dataTask:didReceiveData:)]); + XCTAssertTrue([proxy respondsToSelector:@selector(URLSession:task:didFinishCollectingMetrics:)]); + [proxy URLSession:session task:task didFinishCollectingMetrics:[[NSURLSessionTaskMetrics alloc] init]]; + XCTAssertTrue(sessionDelegate.didFinishCollectingMetricsWasCalled, @"The session delegate's method should be called"); + XCTAssertTrue(tracingDelegate.didFinishCollectingMetricsWasCalled, @"The tracing delegate's method should be called"); + } else { + XCTSkip(@"Required API is not available for this test."); + }; +} + +- (void)testDidFinishCollectingMetricsIsNotImplementedBySessionDelegate { + if (@available(macOS 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *)) { + BSGURLSessionTracingProxyTests_DidReceiveDataStub *sessionDelegate = + [[BSGURLSessionTracingProxyTests_DidReceiveDataStub alloc] init]; + BSGURLSessionTracingProxyTests_TracingStub *tracingDelegate = [[BSGURLSessionTracingProxyTests_TracingStub alloc] init]; + id proxy = [[BSGURLSessionTracingProxy alloc] initWithDelegate:sessionDelegate tracingDelegate:(id)tracingDelegate]; + + NSURLSession *session = [NSURLSession sessionWithConfiguration:NSURLSessionConfiguration.ephemeralSessionConfiguration]; + NSURLSessionDataTask *task = [[NSURLSessionDataTask alloc] init]; + + XCTAssertTrue([proxy respondsToSelector:@selector(URLSession:task:didFinishCollectingMetrics:)]); + XCTAssertNoThrow([proxy URLSession:session task:task didFinishCollectingMetrics:[[NSURLSessionTaskMetrics alloc] init]]); + XCTAssertTrue(tracingDelegate.didFinishCollectingMetricsWasCalled, @"The tracing delegate's method should be called"); + } else { + XCTSkip(@"Required API is not available for this test."); + }; +} + +- (void)testExceptionIsThrownForUnimplementedMethod { + BSGURLSessionTracingProxyTests_DidReceiveDataStub *sessionDelegate = + [[BSGURLSessionTracingProxyTests_DidReceiveDataStub alloc] init]; + BSGURLSessionTracingProxyTests_TracingStub *tracingDelegate = [[BSGURLSessionTracingProxyTests_TracingStub alloc] init]; + id proxy = [[BSGURLSessionTracingProxy alloc] initWithDelegate:sessionDelegate tracingDelegate:(id)tracingDelegate]; + XCTAssertThrowsSpecificNamed([proxy testExceptionIsThrownForUnimplementedMethod], NSException, NSInvalidArgumentException); +} + +@end + +#pragma mark - + +@implementation BSGURLSessionTracingProxyTests_DidReceiveDataStub + +- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { + self.didReceiveDataWasCalled = YES; +} + +@end + +#pragma mark - + +@implementation BSGURLSessionTracingProxyTests_DidFinishCollectingMetricsStub + +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics +API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)) { + self.didFinishCollectingMetricsWasCalled = YES; +} + +@end + +#pragma mark - + +@implementation BSGURLSessionTracingProxyTests_TracingStub + +- (BOOL)canTrace { + return YES; +} + +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics +API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)) { + self.didFinishCollectingMetricsWasCalled = YES; +} + +@end diff --git a/BugsnagNetworkRequestPlugin/BugsnagNetworkRequestPluginTests/BugsnagNetworkRequestPluginTests.m b/BugsnagNetworkRequestPlugin/BugsnagNetworkRequestPluginTests/BugsnagNetworkRequestPluginTests.m index a7f3976c7..048cd118c 100644 --- a/BugsnagNetworkRequestPlugin/BugsnagNetworkRequestPluginTests/BugsnagNetworkRequestPluginTests.m +++ b/BugsnagNetworkRequestPlugin/BugsnagNetworkRequestPluginTests/BugsnagNetworkRequestPluginTests.m @@ -387,7 +387,7 @@ - (void)runUploadTasksWithURL:(NSString *)urlString - (void)testBadURL { [self resetBreadcrumbs]; [self fetchAndWaitURL:@"xxxxxxx" usingConfig:self.defaultConfig]; - [self expectMessage:@"NSURLSession error" + [self expectMessage:@"NSURLSession request error" method:@"GET" reqLength:nil respLength:nil @@ -404,7 +404,7 @@ - (void)testDataTaskEmpty { statusCode:200 data:[self dataOfLength:0] validator:^{ - [self expectMessage:@"NSURLSession succeeded" + [self expectMessage:@"NSURLSession request succeeded" method:@"GET" reqLength:nil respLength:@0 @@ -422,7 +422,7 @@ - (void)testDataTaskNonEmpty { statusCode:200 data:[self dataOfLength:10] validator:^{ - [self expectMessage:@"NSURLSession succeeded" + [self expectMessage:@"NSURLSession request succeeded" method:@"GET" reqLength:nil respLength:@10 @@ -435,11 +435,11 @@ - (void)testDataTaskNonEmpty { - (void)testTaskStatusCodes { NSArray *expectedMessages = @[ @"", - @"NSURLSession succeeded", - @"NSURLSession succeeded", - @"NSURLSession succeeded", - @"NSURLSession failed", - @"NSURLSession error", + @"NSURLSession request succeeded", + @"NSURLSession request succeeded", + @"NSURLSession request succeeded", + @"NSURLSession request failed", + @"NSURLSession request error", ]; NSString *urlString = @"https://bugsnag.com"; @@ -516,7 +516,7 @@ - (void)testTaskMethods { statusCode:200 data:[self dataOfLength:0] validator:^{ - [self expectMessage:@"NSURLSession succeeded" + [self expectMessage:@"NSURLSession request succeeded" method:method reqLength:nil respLength:@0 @@ -531,7 +531,7 @@ - (void)testTaskMethods { statusCode:200 data:[self dataOfLength:0] validator:^{ - [self expectMessage:@"NSURLSession succeeded" + [self expectMessage:@"NSURLSession request succeeded" method:method reqLength:nil respLength:@0 diff --git a/CHANGELOG.md b/CHANGELOG.md index a1b25b942..10b1870d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,25 @@ Changelog ========= +## 6.14.1 (2021-10-20) + +### Bug fixes + +* Disable automatic session tracking in app extensions (it was not working as intended.) + [#1211](https://github.com/bugsnag/bugsnag-cocoa/pull/1211) + +* Stop logging "[ERROR] Failed to install crash handler..." if a debugger is attached. + [#1210](https://github.com/bugsnag/bugsnag-cocoa/pull/1210) + +* Include the word "request" in network request breadcrumb messages. + [#1209](https://github.com/bugsnag/bugsnag-cocoa/pull/1209) + +* Prevent a crash that can occur when `-[BSGURLSessionTracingProxy forwardInvocation:]` calls `-[NSInvocation selector]`. + [#1208](https://github.com/bugsnag/bugsnag-cocoa/pull/1208) + +* Apply `redactedKeys` to breadcrumb metadata. + [#1204](https://github.com/bugsnag/bugsnag-cocoa/pull/1204) + ## 6.14.0 (2021-10-06) ### Enhancements diff --git a/Framework/Info.plist b/Framework/Info.plist index cddd53450..2fa1bac49 100644 --- a/Framework/Info.plist +++ b/Framework/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 6.14.0 + 6.14.1 CFBundleVersion 1 diff --git a/Gemfile b/Gemfile index b63e86aa3..2c5a5434f 100644 --- a/Gemfile +++ b/Gemfile @@ -7,10 +7,10 @@ gem 'xcpretty' # A reference to Maze Runner is only needed for running tests locally and if committed it must be # portable for CI, e.g. a specific release. However, leaving it commented out would mean quicker CI. -gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner', tag: 'v5.11.0' +gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner', tag: 'v6.0.0' # Use a development branch -#gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner', branch: 'tms/intelligent-document-server' +#gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner', branch: 'tms/update-cucumber' # Locally, you can run against Maze Runner branches and uncommitted changes: #gem 'bugsnag-maze-runner', path: '../maze-runner' diff --git a/Gemfile.lock b/Gemfile.lock index cb85a973f..4a883f9ed 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,14 +1,13 @@ GIT remote: https://github.com/bugsnag/maze-runner - revision: 2656fec8c2f523ad60b58dcce1fc33e8d9d87ca9 - tag: v5.11.0 + revision: 772e63f26bdb5a9e2b0893765763fdcc54e83cd4 + tag: v6.0.0 specs: - bugsnag-maze-runner (5.11.0) + bugsnag-maze-runner (6.0.0) appium_lib (~> 11.2.0) - cucumber (~> 3.1.2) + cucumber (~> 7.1) cucumber-expressions (~> 6.0.0) curb (~> 0.9.6) - gherkin (~> 5.1.0) minitest (~> 5.0) optimist (~> 3.0.1) os (~> 1.0.0) @@ -41,7 +40,6 @@ GEM faye-websocket (~> 0.11.0) selenium-webdriver (~> 3.14, >= 3.14.1) atomos (0.1.3) - backports (3.21.0) builder (3.2.4) childprocess (3.0.0) claide (1.0.3) @@ -90,22 +88,38 @@ GEM concurrent-ruby (1.1.9) cork (0.3.0) colored2 (~> 3.1) - cucumber (3.1.2) - builder (>= 2.1.2) - cucumber-core (~> 3.2.0) - cucumber-expressions (~> 6.0.1) - cucumber-wire (~> 0.0.1) - diff-lcs (~> 1.3) - gherkin (~> 5.1.0) - multi_json (>= 1.7.5, < 2.0) - multi_test (>= 0.1.2) - cucumber-core (3.2.1) - backports (>= 3.8.0) - cucumber-tag_expressions (~> 1.1.0) - gherkin (~> 5.0) + cucumber (7.1.0) + builder (~> 3.2, >= 3.2.4) + cucumber-core (~> 10.1, >= 10.1.0) + cucumber-create-meta (~> 6.0, >= 6.0.1) + cucumber-cucumber-expressions (~> 14.0, >= 14.0.0) + cucumber-gherkin (~> 22.0, >= 22.0.0) + cucumber-html-formatter (~> 17.0, >= 17.0.0) + cucumber-messages (~> 17.1, >= 17.1.1) + cucumber-wire (~> 6.2, >= 6.2.0) + diff-lcs (~> 1.4, >= 1.4.4) + mime-types (~> 3.3, >= 3.3.1) + multi_test (~> 0.1, >= 0.1.2) + sys-uname (~> 1.2, >= 1.2.2) + cucumber-core (10.1.0) + cucumber-gherkin (~> 22.0, >= 22.0.0) + cucumber-messages (~> 17.1, >= 17.1.1) + cucumber-tag-expressions (~> 4.0, >= 4.0.2) + cucumber-create-meta (6.0.2) + cucumber-messages (~> 17.1, >= 17.1.1) + sys-uname (~> 1.2, >= 1.2.2) + cucumber-cucumber-expressions (14.0.0) cucumber-expressions (6.0.1) - cucumber-tag_expressions (1.1.1) - cucumber-wire (0.0.1) + cucumber-gherkin (22.0.0) + cucumber-messages (~> 17.1, >= 17.1.1) + cucumber-html-formatter (17.0.0) + cucumber-messages (~> 17.1, >= 17.1.0) + cucumber-messages (17.1.1) + cucumber-tag-expressions (4.1.0) + cucumber-wire (6.2.0) + cucumber-core (~> 10.1, >= 10.1.0) + cucumber-cucumber-expressions (~> 14.0, >= 14.0.0) + cucumber-messages (~> 17.1, >= 17.1.1) curb (0.9.11) danger (8.3.1) claide (~> 1.0) @@ -153,7 +167,6 @@ GEM fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - gherkin (5.1.0) git (1.9.1) rchardet (~> 1.8) httpclient (2.8.3) @@ -175,10 +188,12 @@ GEM kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) liferaft (0.0.6) + mime-types (3.3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2021.0901) mini_portile2 (2.6.1) minitest (5.14.4) molinillo (0.8.0) - multi_json (1.15.0) multi_test (0.1.2) multipart-post (2.1.1) mustache (1.1.1) @@ -217,6 +232,8 @@ GEM childprocess (>= 0.5, < 4.0) rubyzip (>= 1.2.2) sqlite3 (1.4.2) + sys-uname (1.2.2) + ffi (~> 1.1) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) test-unit (3.3.9) diff --git a/Makefile b/Makefile index b0682426f..613399501 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ OS?=latest TEST_CONFIGURATION?=Debug XCODEBUILD_EXTRA_ARGS?= DATA_PATH=DerivedData -BUILD_FLAGS=-project Bugsnag.xcodeproj -scheme Bugsnag-$(PLATFORM) -derivedDataPath $(DATA_PATH) $(XCODEBUILD_EXTRA_ARGS) +BUILD_FLAGS=-workspace Bugsnag.xcworkspace -scheme Bugsnag-$(PLATFORM) -derivedDataPath $(DATA_PATH) $(XCODEBUILD_EXTRA_ARGS) ifeq ($(PLATFORM),macOS) SDK?=macosx @@ -120,6 +120,14 @@ test-fixtures: ## Build the end-to-end test fixture @./features/scripts/export_ios_app.sh @./features/scripts/export_mac_app.sh +e2e_ios_local: + @./features/scripts/export_ios_app.sh + bundle exec maze-runner --app=features/fixtures/ios/output/iOSTestApp.ipa --farm=local --os=ios --os-version=14 --apple-team-id=372ZUL2ZB7 --udid="$(shell idevice_id -l)" $(FEATURES) + +e2e_macos: + ./features/scripts/export_mac_app.sh + bundle exec maze-runner --app=macOSTestApp --farm=local --os=macOS --os-version=11 $(FEATURES) + #-------------------------------------------------------------------------- # Release # diff --git a/Tests/BSGInternalErrorReporterTests.m b/Tests/BSGInternalErrorReporterTests.m index eb4cca17e..01d0a3df8 100644 --- a/Tests/BSGInternalErrorReporterTests.m +++ b/Tests/BSGInternalErrorReporterTests.m @@ -24,6 +24,10 @@ @interface BSGInternalErrorReporterTests : XCTestCase +#import "BugsnagBreadcrumb+Private.h" #import "BugsnagEvent+Private.h" @interface BugsnagMetadataRedactionTest : XCTestCase @@ -130,4 +131,27 @@ - (void)testCaseInsensitiveKeys { XCTAssertEqualObjects(@"ba09", section[@"somekey"]); } +- (void)testBreadcrumbMetadataRedaction { + BugsnagBreadcrumb *breadcrumb = [[BugsnagBreadcrumb alloc] init]; + breadcrumb.message = @"message cannot be empty"; + breadcrumb.metadata = @{ + @"foo" : @"not redacted", + @"password" : @"secret", + @"x" : @{ + @"bar" : @"not redacted", + @"password" : @"123456" + } + }; + + BugsnagEvent *event = [self generateEventWithMetadata:@{}]; + event.breadcrumbs = @[breadcrumb]; + + NSDictionary *eventPayload = [event toJsonWithRedactedKeys:[NSSet setWithArray:@[@"password"]]]; + NSDictionary *metaData = eventPayload[@"breadcrumbs"][0][@"metaData"]; + XCTAssertEqualObjects(metaData[@"foo"], @"not redacted"); + XCTAssertEqualObjects(metaData[@"password"], @"[REDACTED]"); + XCTAssertEqualObjects(metaData[@"x"][@"bar"], @"not redacted"); + XCTAssertEqualObjects(metaData[@"x"][@"password"], @"[REDACTED]"); +} + @end diff --git a/Tests/Info.plist b/Tests/Info.plist index 732a3d397..1e7a6a313 100644 --- a/Tests/Info.plist +++ b/Tests/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 6.14.0 + 6.14.1 CFBundleVersion 1 diff --git a/VERSION b/VERSION index 68390495f..bbf0c5a54 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.14.0 +6.14.1 diff --git a/docker-compose.yml b/docker-compose.yml index f6392545e..f42a7cace 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '3.6' services: cocoa-maze-runner: - image: 855461928731.dkr.ecr.us-west-1.amazonaws.com/maze-runner-releases:latest-v5-cli + image: 855461928731.dkr.ecr.us-west-1.amazonaws.com/maze-runner-releases:latest-v6-cli environment: DEBUG: VERBOSE: diff --git a/features/breadcrumbs.feature b/features/breadcrumbs.feature index 06e78efd5..35634898a 100644 --- a/features/breadcrumbs.feature +++ b/features/breadcrumbs.feature @@ -60,17 +60,18 @@ Feature: Attaching a series of notable events leading up to errors And I run "NetworkBreadcrumbsScenario" Then I wait to receive an error And the event "breadcrumbs.0.timestamp" is a timestamp - And the event "breadcrumbs.0.name" equals "NSURLSession failed" + And the event "breadcrumbs.0.name" equals "NSURLSession request failed" And the event "breadcrumbs.0.type" equals "request" And the event "breadcrumbs.0.metaData.method" equals "GET" And the event "breadcrumbs.0.metaData.url" equals "http://bs-local.com:9340/reflect/" And the event "breadcrumbs.0.metaData.urlParams.status" equals "444" + And the event "breadcrumbs.0.metaData.urlParams.password" equals "[REDACTED]" And the event "breadcrumbs.0.metaData.status" equals 444 And the event "breadcrumbs.0.metaData.duration" is greater than 0 And the event "breadcrumbs.0.metaData.requestContentLength" is null And the event "breadcrumbs.0.metaData.responseContentLength" is greater than 0 And the event "breadcrumbs.1.timestamp" is a timestamp - And the event "breadcrumbs.1.name" equals "NSURLSession succeeded" + And the event "breadcrumbs.1.name" equals "NSURLSession request succeeded" And the event "breadcrumbs.1.type" equals "request" And the event "breadcrumbs.1.metaData.method" equals "GET" And the event "breadcrumbs.1.metaData.url" equals "http://bs-local.com:9340/reflect/" diff --git a/features/fixtures/shared/scenarios/NetworkBreadcrumbsScenario.swift b/features/fixtures/shared/scenarios/NetworkBreadcrumbsScenario.swift index 0c2f5c163..af88ca489 100644 --- a/features/fixtures/shared/scenarios/NetworkBreadcrumbsScenario.swift +++ b/features/fixtures/shared/scenarios/NetworkBreadcrumbsScenario.swift @@ -23,9 +23,8 @@ class NetworkBreadcrumbsScenario : Scenario { } override func run() { - // Make some network requests so that automatic network breadcrumbs are left - query(address: "http://bs-local.com:9340/reflect/?status=444") + query(address: "http://bs-local.com:9340/reflect/?status=444&password=T0p5ecr3t") query(address: "http://bs-local.com:9340/reflect/?delay_ms=3000") // Send a handled error diff --git a/features/steps/app_steps.rb b/features/steps/app_steps.rb new file mode 100644 index 000000000..33ca7b86d --- /dev/null +++ b/features/steps/app_steps.rb @@ -0,0 +1,72 @@ +When('I background the app for {int} seconds') do |duration| + Maze.driver.background_app(duration) +end + +When('I relaunch the app') do + case Maze.driver.capabilities['platformName'] + when 'Mac' + app = Maze.driver.capabilities['app'] + system("killall #{app} > /dev/null && sleep 1") + Maze.driver.get(app) + else + Maze.driver.launch_app + end +end + +When("I relaunch the app after a crash") do + # This step should only be used when the app has crashed, but the notifier needs a little + # time to write the crash report before being forced to reopen. From trials, 2s was not enough. + sleep(5) + case Maze.driver.capabilities['platformName'] + when 'Mac' + Maze.driver.get(Maze.driver.capabilities['app']) + else + Maze.driver.launch_app + end +end + +# +# https://appium.io/docs/en/commands/device/app/app-state/ +# +# 0: The current application state cannot be determined/is unknown +# 1: The application is not running +# 2: The application is running in the background and is suspended +# 3: The application is running in the background and is not suspended +# 4: The application is running in the foreground + +Then('the app is running in the foreground') do + wait_for_true do + Maze.driver.app_state('com.bugsnag.iOSTestApp') == :running_in_foreground + end +end + +Then('the app is running in the background') do + wait_for_true do + Maze.driver.app_state('com.bugsnag.iOSTestApp') == :running_in_background + end +end + +Then('the app is not running') do + wait_for_true do + case Maze.driver.capabilities['platformName'] + when 'iOS' + Maze.driver.app_state('com.bugsnag.iOSTestApp') == :not_running + when 'Mac' + `lsappinfo info -only pid -app com.bugsnag.macOSTestApp`.empty? + else + raise "Don't know how to query app state on this platform" + end + end +end + +When('I set the app to {string} mode') do |mode| + steps %( + Given the element "scenario_metadata" is present + When I send the keys "#{mode}" to the element "scenario_metadata" + And I close the keyboard + ) +end + +When('I send the app to the background') do + Maze.driver.background_app(-1) +end diff --git a/features/steps/ios_steps.rb b/features/steps/cocoa_steps.rb similarity index 64% rename from features/steps/ios_steps.rb rename to features/steps/cocoa_steps.rb index b0d99c1ab..9900f5359 100644 --- a/features/steps/ios_steps.rb +++ b/features/steps/cocoa_steps.rb @@ -7,14 +7,6 @@ ) end -When('I set the app to {string} mode') do |mode| - steps %( - Given the element "scenario_metadata" is present - When I send the keys "#{mode}" to the element "scenario_metadata" - And I close the keyboard - ) -end - When("I run {string} and relaunch the app") do |event_type| begin step("I run \"#{event_type}\"") @@ -74,74 +66,10 @@ def click_if_present(element) ) end -When('I send the app to the background') do - Maze.driver.background_app(-1) -end - -When('I background the app for {int} seconds') do |duration| - Maze.driver.background_app(duration) -end - -When('I relaunch the app') do - case Maze.driver.capabilities['platformName'] - when 'Mac' - app = Maze.driver.capabilities['app'] - system("killall #{app} > /dev/null && sleep 1") - Maze.driver.get(app) - else - Maze.driver.launch_app - end -end - -When("I relaunch the app after a crash") do - # This step should only be used when the app has crashed, but the notifier needs a little - # time to write the crash report before being forced to reopen. From trials, 2s was not enough. - sleep(5) - case Maze.driver.capabilities['platformName'] - when 'Mac' - Maze.driver.get(Maze.driver.capabilities['app']) - else - Maze.driver.launch_app - end -end - When('I clear the error queue') do Maze::Server.errors.clear end -# -# https://appium.io/docs/en/commands/device/app/app-state/ -# -# 0: The current application state cannot be determined/is unknown -# 1: The application is not running -# 2: The application is running in the background and is suspended -# 3: The application is running in the background and is not suspended -# 4: The application is running in the foreground - -Then('the app is running in the foreground') do - wait_for_true do - Maze.driver.app_state('com.bugsnag.iOSTestApp') == :running_in_foreground - end -end - -Then('the app is running in the background') do - wait_for_true do - Maze.driver.app_state('com.bugsnag.iOSTestApp') == :running_in_background - end -end - -Then('the app is not running') do - wait_for_true do - case Maze.driver.capabilities['platformName'] - when 'iOS' - Maze.driver.app_state('com.bugsnag.iOSTestApp') == :not_running - when 'Mac' - `lsappinfo info -only pid -app com.bugsnag.macOSTestApp`.empty? - else - raise "Don't know how to query app state on this platform" - end - end -end def request_matches_row(body, row) row.each do |key, expected_value| @@ -175,77 +103,6 @@ def request_fields_are_equal(key, index_a, index_b) val_a.eql? val_b end -Then('the event {string} equals one of:') do |field, possible_values| - value = Maze::Helper.read_key_path(Maze::Server.errors.current[:body], "events.0.#{field}") - assert_includes(possible_values.raw.flatten, value) -end - -Then('the event {string} is within {int} seconds of the current timestamp') do |field, threshold_secs| - value = Maze::Helper.read_key_path(Maze::Server.errors.current[:body], "events.0.#{field}") - assert_not_nil(value, 'Expected a timestamp') - now_secs = Time.now.to_i - then_secs = Time.parse(value).to_i - delta = now_secs - then_secs - assert_true(delta.abs < threshold_secs, "Expected current timestamp, but received #{value}") -end - -Then('the event {string} is between {float} and {float}') do |field, lower, upper| - value = Maze::Helper.read_key_path(Maze::Server.errors.current[:body], "events.0.#{field}") - assert_not_nil(value, 'Expected a value') - assert_true(lower <= value && value <= upper, "Expected a value between #{lower} and #{upper}, but received #{value}") -end - -Then('the event {string} is between {int} and {int}') do |field, lower, upper| - value = Maze::Helper.read_key_path(Maze::Server.errors.current[:body], "events.0.#{field}") - assert_not_nil(value, 'Expected a value') - assert_true(lower <= value && value <= upper, "Expected a value between #{lower} and #{upper}, but received #{value}") -end - -Then('the event {string} is less than the event {string}') do |field1, field2| - value1 = Maze::Helper.read_key_path(Maze::Server.errors.current[:body], "events.0.#{field1}") - assert_not_nil(value1, 'Expected a value') - value2 = Maze::Helper.read_key_path(Maze::Server.errors.current[:body], "events.0.#{field2}") - assert_not_nil(value2, 'Expected a value') - assert_true(value1 < value2, "Expected value to be less than #{value2}, but received #{value1}") -end - -Then('the event breadcrumbs contain {string} with type {string}') do |string, type| - crumbs = Maze::Helper.read_key_path(find_request(0)[:body], 'events.0.breadcrumbs') - assert_not_equal(0, crumbs.length, 'There are no breadcrumbs on this event') - match = crumbs.detect do |crumb| - crumb['name'] == string && crumb['type'] == type - end - assert_not_nil(match, 'No crumb matches the provided message and type') -end - -Then('the event breadcrumbs contain {string}') do |string| - crumbs = Maze::Helper.read_key_path(Maze::Server.errors.current[:body], 'events.0.breadcrumbs') - assert_not_equal(0, crumbs.length, 'There are no breadcrumbs on this event') - match = crumbs.detect do |crumb| - crumb['name'] == string - end - assert_not_nil(match, 'No crumb matches the provided message') -end - -Then('the stack trace is an array with {int} stack frames') do |expected_length| - stack_trace = Maze::Helper.read_key_path(Maze::Server.errors.current[:body], 'events.0.exceptions.0.stacktrace') - assert_equal(expected_length, stack_trace.length) -end - -Then('the {string} of stack frame {int} equals one of:') do |key, num, possible_values| - field = "events.0.exceptions.0.stacktrace.#{num}.#{key}" - value = Maze::Helper.read_key_path(Maze::Server.errors.current[:body], field) - assert_includes(possible_values.raw.flatten, value) -end - -Then('the stacktrace contains methods:') do |table| - stack_trace = Maze::Helper.read_key_path(Maze::Server.errors.current[:body], 'events.0.exceptions.0.stacktrace') - expected = table.raw.flatten - actual = stack_trace.map { |s| s['method'] } - contains = actual.each_cons(expected.length).to_a.include? expected - assert_true(contains, "Stacktrace methods #{actual} did not contain #{expected}") -end - def check_device_model(field, list) internal_names = { 'iPhone 6' => %w[iPhone7,2], diff --git a/features/steps/reusable_steps.rb b/features/steps/reusable_steps.rb new file mode 100644 index 000000000..75b9851d9 --- /dev/null +++ b/features/steps/reusable_steps.rb @@ -0,0 +1,72 @@ +# A collection of steps that could be added to Maze Runner + +Then('the event {string} equals one of:') do |field, possible_values| + value = Maze::Helper.read_key_path(Maze::Server.errors.current[:body], "events.0.#{field}") + assert_includes(possible_values.raw.flatten, value) +end + +Then('the event {string} is within {int} seconds of the current timestamp') do |field, threshold_secs| + value = Maze::Helper.read_key_path(Maze::Server.errors.current[:body], "events.0.#{field}") + assert_not_nil(value, 'Expected a timestamp') + now_secs = Time.now.to_i + then_secs = Time.parse(value).to_i + delta = now_secs - then_secs + assert_true(delta.abs < threshold_secs, "Expected current timestamp, but received #{value}") +end + +Then('the event {string} is between {float} and {float}') do |field, lower, upper| + value = Maze::Helper.read_key_path(Maze::Server.errors.current[:body], "events.0.#{field}") + assert_not_nil(value, 'Expected a value') + assert_true(lower <= value && value <= upper, "Expected a value between #{lower} and #{upper}, but received #{value}") +end + +Then('the event {string} is between {int} and {int}') do |field, lower, upper| + value = Maze::Helper.read_key_path(Maze::Server.errors.current[:body], "events.0.#{field}") + assert_not_nil(value, 'Expected a value') + assert_true(lower <= value && value <= upper, "Expected a value between #{lower} and #{upper}, but received #{value}") +end + +Then('the event {string} is less than the event {string}') do |field1, field2| + value1 = Maze::Helper.read_key_path(Maze::Server.errors.current[:body], "events.0.#{field1}") + assert_not_nil(value1, 'Expected a value') + value2 = Maze::Helper.read_key_path(Maze::Server.errors.current[:body], "events.0.#{field2}") + assert_not_nil(value2, 'Expected a value') + assert_true(value1 < value2, "Expected value to be less than #{value2}, but received #{value1}") +end + +Then('the event breadcrumbs contain {string} with type {string}') do |string, type| + crumbs = Maze::Helper.read_key_path(find_request(0)[:body], 'events.0.breadcrumbs') + assert_not_equal(0, crumbs.length, 'There are no breadcrumbs on this event') + match = crumbs.detect do |crumb| + crumb['name'] == string && crumb['type'] == type + end + assert_not_nil(match, 'No crumb matches the provided message and type') +end + +Then('the event breadcrumbs contain {string}') do |string| + crumbs = Maze::Helper.read_key_path(Maze::Server.errors.current[:body], 'events.0.breadcrumbs') + assert_not_equal(0, crumbs.length, 'There are no breadcrumbs on this event') + match = crumbs.detect do |crumb| + crumb['name'] == string + end + assert_not_nil(match, 'No crumb matches the provided message') +end + +Then('the stack trace is an array with {int} stack frames') do |expected_length| + stack_trace = Maze::Helper.read_key_path(Maze::Server.errors.current[:body], 'events.0.exceptions.0.stacktrace') + assert_equal(expected_length, stack_trace.length) +end + +Then('the {string} of stack frame {int} equals one of:') do |key, num, possible_values| + field = "events.0.exceptions.0.stacktrace.#{num}.#{key}" + value = Maze::Helper.read_key_path(Maze::Server.errors.current[:body], field) + assert_includes(possible_values.raw.flatten, value) +end + +Then('the stacktrace contains methods:') do |table| + stack_trace = Maze::Helper.read_key_path(Maze::Server.errors.current[:body], 'events.0.exceptions.0.stacktrace') + expected = table.raw.flatten + actual = stack_trace.map { |s| s['method'] } + contains = actual.each_cons(expected.length).to_a.include? expected + assert_true(contains, "Stacktrace methods #{actual} did not contain #{expected}") +end diff --git a/features/support/env.rb b/features/support/env.rb index 30294a067..b1f86ac63 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -1,13 +1,40 @@ require 'fileutils' -# Set this explicitly -$api_key = "12312312312312312312312312312312" - -AfterConfiguration do |_config| +BeforeAll do + $api_key = "12312312312312312312312312312312" Maze.config.receive_no_requests_wait = 15 # Setup a 3 minute timeout for receiving requests is STRESS_TEST env var is set Maze.config.receive_requests_wait = 180 unless ENV['STRESS_TEST'].nil? + + # Additional require MacOS configuration + if Maze.config.os == 'macos' + # The default macOS Crash Reporter "#{app_name} quit unexpectedly" alert grabs focus which can cause tests to flake. + # This option, which appears to have been introduced in macOS 10.11, displays a notification instead of the alert. + `defaults write com.apple.CrashReporter UseUNC 1` + + fixture_dir = 'features/fixtures/macos/output' + app_dir = '/Applications' + zip_name = "#{Maze.config.app}.zip" + app_name = "#{Maze.config.app}.app" + + # If built app file already exists, skip unzip + unless File.exist?("#{fixture_dir}/#{app_name}") + raise Exception, 'Test fixture build archive not found' unless File.file?("#{fixture_dir}/#{zip_name}") + `cd #{fixture_dir} && unzip #{zip_name}` + end + + FileUtils.mv("#{fixture_dir}/#{app_name}", "#{app_dir}/#{app_name}") + end +end + +AfterAll do + # Additional require MacOS configuration + if Maze.config.os == 'macos' + app_dir = '/Applications' + app_name = "#{Maze.config.app}.app" + FileUtils.rm_rf("#{app_dir}/#{app_name}") + end end def skip_below(os, version) @@ -30,27 +57,3 @@ def skip_below(os, version) Before('@stress_test') do |_scenario| skip_this_scenario('Skipping: Run is not configured for stress tests') if ENV['STRESS_TEST'].nil? end - -# Additional require MacOS configuration -if Maze.config.os == 'macos' - # The default macOS Crash Reporter "#{app_name} quit unexpectedly" alert grabs focus which can cause tests to flake. - # This option, which appears to have been introduced in macOS 10.11, displays a notification instead of the alert. - `defaults write com.apple.CrashReporter UseUNC 1` - - fixture_dir = 'features/fixtures/macos/output' - app_dir = '/Applications' - zip_name = "#{Maze.config.app}.zip" - app_name = "#{Maze.config.app}.app" - - # If built app file already exists, skip unzip - unless File.exist?("#{fixture_dir}/#{app_name}") - raise Exception, 'Test fixture build archive not found' unless File.file?("#{fixture_dir}/#{zip_name}") - `cd #{fixture_dir} && unzip #{zip_name}` - end - - FileUtils.mv("#{fixture_dir}/#{app_name}", "#{app_dir}/#{app_name}") - - at_exit do - FileUtils.rm_rf("#{app_dir}/#{app_name}") - end -end