From 12f7fa0f89d8d2feeee926d85ccac282509d2b5b Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Tue, 21 Jun 2022 16:56:14 -0400 Subject: [PATCH 01/14] ref: fix some typos (#1909) * ref: rename to fix typo and align with interface header name * ref: fix typo in constant name --- Sentry.xcodeproj/project.pbxproj | 10 +++++----- .../{SentryHybridSKdsOnly.m => PrivateSentrySDKOnly.m} | 0 Sources/Sentry/SentryFramesTracker.m | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) rename Sources/Sentry/{SentryHybridSKdsOnly.m => PrivateSentrySDKOnly.m} (100%) diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index eca5a967651..19724aa0dce 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -335,7 +335,7 @@ 7B6C5F8726034395007F7DFF /* SentryOutOfMemoryLogic.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B6C5F8626034395007F7DFF /* SentryOutOfMemoryLogic.m */; }; 7B6CC50224EE5A42001816D7 /* SentryHubTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B6CC50124EE5A42001816D7 /* SentryHubTests.swift */; }; 7B6D125F265F778500C9BE4B /* PrivateSentrySDKOnly.h in Headers */ = {isa = PBXBuildFile; fileRef = 7B6D125E265F778500C9BE4B /* PrivateSentrySDKOnly.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 7B6D1261265F784000C9BE4B /* SentryHybridSKdsOnly.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B6D1260265F784000C9BE4B /* SentryHybridSKdsOnly.m */; }; + 7B6D1261265F784000C9BE4B /* PrivateSentrySDKOnly.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B6D1260265F784000C9BE4B /* PrivateSentrySDKOnly.m */; }; 7B6D1263265F7CC600C9BE4B /* PrivateSentrySDKOnlyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B6D1262265F7CC600C9BE4B /* PrivateSentrySDKOnlyTests.swift */; }; 7B6D135C27F4605D00331ED2 /* TestEnvelopeRateLimitDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B6D135B27F4605D00331ED2 /* TestEnvelopeRateLimitDelegate.swift */; }; 7B6D98E924C6D336005502FA /* SentrySdkInfo+Equality.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B6D98E824C6D336005502FA /* SentrySdkInfo+Equality.m */; }; @@ -1022,7 +1022,7 @@ 7B6C5F8626034395007F7DFF /* SentryOutOfMemoryLogic.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryOutOfMemoryLogic.m; sourceTree = ""; }; 7B6CC50124EE5A42001816D7 /* SentryHubTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryHubTests.swift; sourceTree = ""; }; 7B6D125E265F778500C9BE4B /* PrivateSentrySDKOnly.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = PrivateSentrySDKOnly.h; path = Public/PrivateSentrySDKOnly.h; sourceTree = ""; }; - 7B6D1260265F784000C9BE4B /* SentryHybridSKdsOnly.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryHybridSKdsOnly.m; sourceTree = ""; }; + 7B6D1260265F784000C9BE4B /* PrivateSentrySDKOnly.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PrivateSentrySDKOnly.m; sourceTree = ""; }; 7B6D1262265F7CC600C9BE4B /* PrivateSentrySDKOnlyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateSentrySDKOnlyTests.swift; sourceTree = ""; }; 7B6D135B27F4605D00331ED2 /* TestEnvelopeRateLimitDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestEnvelopeRateLimitDelegate.swift; sourceTree = ""; }; 7B6D98E724C6D336005502FA /* SentrySdkInfo+Equality.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SentrySdkInfo+Equality.h"; sourceTree = ""; }; @@ -1376,8 +1376,8 @@ D86F419727C8FEFA00490520 /* SentryCoreDataMiddleware+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryCoreDataMiddleware+Extension.swift"; sourceTree = ""; }; D8751FA4274743710032F4DE /* SentryNSURLSessionTaskSearchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryNSURLSessionTaskSearchTests.swift; sourceTree = ""; }; D875ED0A276CC84700422FAC /* SentryNSDataTrackerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryNSDataTrackerTests.swift; sourceTree = ""; }; - D880E3B02860A5A0008A90DB /* SentryEvent+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryEvent+Private.h"; path = "include/SentryEvent+Private.h"; sourceTree = ""; }; D880E3A628573E87008A90DB /* SentryBaggageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBaggageTests.swift; sourceTree = ""; }; + D880E3B02860A5A0008A90DB /* SentryEvent+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryEvent+Private.h"; path = "include/SentryEvent+Private.h"; sourceTree = ""; }; D884A20327C80F2700074664 /* SentryCoreDataTrackerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCoreDataTrackerTest.swift; sourceTree = ""; }; D885266327739D01001269FC /* SentryFileIOTrackingIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryFileIOTrackingIntegrationTests.swift; sourceTree = ""; }; D88817D626D7149100BF2251 /* SentryTraceContext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryTraceContext.m; sourceTree = ""; }; @@ -1802,7 +1802,7 @@ 8E25C94F25F836AB00DC215B /* Tools */, 63AA76931EB9C1C200D153DE /* Sentry.h */, 7B6D125E265F778500C9BE4B /* PrivateSentrySDKOnly.h */, - 7B6D1260265F784000C9BE4B /* SentryHybridSKdsOnly.m */, + 7B6D1260265F784000C9BE4B /* PrivateSentrySDKOnly.m */, 63AA76941EB9C1C200D153DE /* SentryClient.h */, 63AA75ED1EB8B3C400D153DE /* SentryClient.m */, 7B85DC1C24EFAFCD007D01D2 /* SentryClient+Private.h */, @@ -3202,7 +3202,7 @@ 8EE32518261FE27B00DC3FF2 /* SentryUIViewControllerSanitizer.m in Sources */, 7BE1E33424F7E3CB009D3AD0 /* SentryMigrateSessionInit.m in Sources */, 15E0A8F22411A45A00F044E3 /* SentrySession.m in Sources */, - 7B6D1261265F784000C9BE4B /* SentryHybridSKdsOnly.m in Sources */, + 7B6D1261265F784000C9BE4B /* PrivateSentrySDKOnly.m in Sources */, 63BE85711ECEC6DE00DC44F5 /* NSDate+SentryExtras.m in Sources */, 7BD4BD4927EB2A5D0071F4FF /* SentryDiscardedEvent.m in Sources */, 03F84D3827DD4191008FE43F /* SentryBacktrace.cpp in Sources */, diff --git a/Sources/Sentry/SentryHybridSKdsOnly.m b/Sources/Sentry/PrivateSentrySDKOnly.m similarity index 100% rename from Sources/Sentry/SentryHybridSKdsOnly.m rename to Sources/Sentry/PrivateSentrySDKOnly.m diff --git a/Sources/Sentry/SentryFramesTracker.m b/Sources/Sentry/SentryFramesTracker.m index 0ec3c00ba8b..b024d50dcad 100644 --- a/Sources/Sentry/SentryFramesTracker.m +++ b/Sources/Sentry/SentryFramesTracker.m @@ -7,7 +7,7 @@ # import static CFTimeInterval const SentryFrozenFrameThreshold = 0.7; -static CFTimeInterval const SentryPreviousFrameInitalValue = -1; +static CFTimeInterval const SentryPreviousFrameInitialValue = -1; /** * Relaxed memoring ordering is typical for incrementing counters. This operation only requires @@ -81,7 +81,7 @@ - (void)resetFrames atomic_store_explicit(&_frozenFrames, 0, SentryFramesMemoryOrder); atomic_store_explicit(&_slowFrames, 0, SentryFramesMemoryOrder); - self.previousFrameTimestamp = SentryPreviousFrameInitalValue; + self.previousFrameTimestamp = SentryPreviousFrameInitialValue; } - (void)start @@ -94,7 +94,7 @@ - (void)displayLinkCallback { CFTimeInterval lastFrameTimestamp = self.displayLinkWrapper.timestamp; - if (self.previousFrameTimestamp == SentryPreviousFrameInitalValue) { + if (self.previousFrameTimestamp == SentryPreviousFrameInitialValue) { self.previousFrameTimestamp = lastFrameTimestamp; return; } From 9d314768d1b1594183ca798599c21b4062ea31ce Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Wed, 22 Jun 2022 11:13:43 -0400 Subject: [PATCH 02/14] fix: disable automatic user interaction tracing when benchmarking (#1908) --- Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 6 +++++- .../iOS-SwiftUITests/SDKPerformanceBenchmarkTests.swift | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index b6132bc0387..45f75e1a132 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -27,7 +27,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { options.enableCoreDataTracking = true options.enableProfiling = true options.attachScreenshot = true - options.enableUserInteractionTracing = true + + // the benchmark test starts and stops a custom transaction using a UIButton, and automatic user interaction tracing stops the transaction that begins with that button press after the idle timeout elapses, stopping the profiler (only one profiler runs regardless of the number of concurrent transactions) + if !ProcessInfo.processInfo.arguments.contains("--io.sentry.test.benchmarking") { + options.enableUserInteractionTracing = true + } } return true diff --git a/Samples/iOS-Swift/iOS-SwiftUITests/SDKPerformanceBenchmarkTests.swift b/Samples/iOS-Swift/iOS-SwiftUITests/SDKPerformanceBenchmarkTests.swift index 58f50f404a7..26ac673684d 100644 --- a/Samples/iOS-Swift/iOS-SwiftUITests/SDKPerformanceBenchmarkTests.swift +++ b/Samples/iOS-Swift/iOS-SwiftUITests/SDKPerformanceBenchmarkTests.swift @@ -14,7 +14,7 @@ class SDKPerformanceBenchmarkTests: XCTestCase { var results = [Double]() for _ in 0..<5 { let app = XCUIApplication() - + app.launchArguments.append("--io.sentry.test.benchmarking") app.launch() app.buttons["Performance scenarios"].tap() From 840f876bf727512fde52effb6b654365e202f5f5 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Wed, 22 Jun 2022 17:01:23 -0400 Subject: [PATCH 03/14] ci: data generation workflow (#1874) * ci: generate fake data from a sample app * fix: set deployment target to support the test matrix * fix: try every saucelabs run 5 times * fix: only run for 13 and higher as that's Kingfisher dep's min supported version * fix: rerun data generation on fastlane updates * test: send events to new project created under sentry-sdks org * ci: rerun workflows on sauce config changes * ci: rerun workflow on changes to TrendingMovies sources * upload dsyms with direct invocation of sentry-cli * only run data generation on iphones, cut out lower iOS 13 jobs that always fail Co-authored-by: Sentry Github Bot Co-authored-by: Philipp Hofmann --- .github/workflows/profile-data-generator.yml | 74 ++++++++++ .github/workflows/saucelabs-UI-tests.yml | 1 + .sauce/profile-data-generator-config.yml | 69 +++++++++ .../ProfileDataGeneratorUITest.m | 124 ++++++++++++++++ .../TrendingMovies.xcodeproj/project.pbxproj | 135 +++++++++++++++++- .../xcschemes/TrendingMovies.xcscheme | 10 ++ .../TrendingMovies/Utilities/Tracer.swift | 3 +- fastlane/Fastfile | 24 ++++ 8 files changed, 436 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/profile-data-generator.yml create mode 100644 .sauce/profile-data-generator-config.yml create mode 100644 Samples/TrendingMovies/ProfileDataGeneratorUITest/ProfileDataGeneratorUITest.m diff --git a/.github/workflows/profile-data-generator.yml b/.github/workflows/profile-data-generator.yml new file mode 100644 index 00000000000..4d735ef4086 --- /dev/null +++ b/.github/workflows/profile-data-generator.yml @@ -0,0 +1,74 @@ +# Defines a workflow that generates large volumes of real-ish profiling data to work with for various development tasks. + +name: Generate Profiling Test Data +on: + schedule: + - cron: '0 7 * * *' # every day at 0700 UTC (midnight SF, 0300 NYC, 0900 Paris) + push: + branches: + - master + pull_request: + paths: + - '.github/workflows/profile-data-generator.yml' + - 'fastlane/**' + - 'Samples/TrendingMovies/**' + - '.sauce/profile-data-generator-config.yml' + +jobs: + build-profile-data-generator-targets: + name: Build app and UI test targets + runs-on: macos-12 + steps: + - uses: actions/checkout@v3 + - run: ./scripts/ci-select-xcode.sh 13.4.1 + - name: Install SentryCli + run: brew install getsentry/tools/sentry-cli + - name: Cache Carthage dependencies + id: trendingmovies-carthage-cache + uses: actions/cache@v3 + with: + path: ./Samples/TrendingMovies/Carthage/Build + key: trendingmovies-carthage-cache-key-${{ hashFiles('Samples/TrendingMovies/Cartfile.resolved') }} + - name: Install Carthage deps + if: steps.trendingmovies-carthage-cache.cache-hit != 'true' + run: cd Samples/TrendingMovies && carthage update --use-xcframeworks + - run: fastlane build_profile_data_generator_ui_test + env: + APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} + APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} + APP_STORE_CONNECT_KEY: ${{ secrets.APP_STORE_CONNECT_KEY }} + FASTLANE_KEYCHAIN_PASSWORD: ${{ secrets.FASTLANE_KEYCHAIN_PASSWORD }} + MATCH_GIT_PRIVATE_KEY: ${{ secrets.MATCH_GIT_PRIVATE_KEY }} + MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} + MATCH_USERNAME: ${{ secrets.MATCH_USERNAME }} + - name: Upload dSYMs + run: | + sentry-cli --auth-token ${{ secrets.SENTRY_AUTH_TOKEN }} upload-dif --org sentry-sdks --project trending-movies DerivedData/Build/Products/Debug-iphoneos/ProfileDataGeneratorUITest-Runner.app/PlugIns/ProfileDataGeneratorUITest.xctest.dSYM + sentry-cli --auth-token ${{ secrets.SENTRY_AUTH_TOKEN }} upload-dif --org sentry-sdks --project trending-movies DerivedData/Build/Products/Debug-iphoneos/TrendingMovies.app.dSYM + - name: Archiving DerivedData + uses: actions/upload-artifact@v3 + with: + name: data-generator-build-products + path: | + **/Debug-iphoneos/TrendingMovies.app + **/Debug-iphoneos/ProfileDataGeneratorUITest-Runner.app + + run-profile-data-generator: + name: Run on Sauce Labs + runs-on: ubuntu-latest + needs: build-profile-data-generator-targets + strategy: + fail-fast: false + matrix: + iOS: [15.5, 15.4, 14.8, 14.7, 13.7] + steps: + - uses: actions/checkout@v3 + - uses: actions/download-artifact@v3 + with: + name: data-generator-build-products + - run: npm install -g saucectl@0.99.4 + - name: Run Tests in Sauce Labs + env: + SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} + SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + run: for i in {1..5}; do saucectl run --select-suite iOS-${{ matrix.iOS }} --config .sauce/profile-data-generator-config.yml && break ; done diff --git a/.github/workflows/saucelabs-UI-tests.yml b/.github/workflows/saucelabs-UI-tests.yml index 92266724c64..3389fb5b9ea 100644 --- a/.github/workflows/saucelabs-UI-tests.yml +++ b/.github/workflows/saucelabs-UI-tests.yml @@ -14,6 +14,7 @@ on: - 'Tests/**' - '.github/workflows/saucelabs-UI-tests.yml' - 'fastlane/**' + - '.sauce/config.yml' jobs: build-ui-tests: diff --git a/.sauce/profile-data-generator-config.yml b/.sauce/profile-data-generator-config.yml new file mode 100644 index 00000000000..0eab9fddae5 --- /dev/null +++ b/.sauce/profile-data-generator-config.yml @@ -0,0 +1,69 @@ +apiVersion: v1alpha +kind: xcuitest +sauce: + region: us-west-1 + concurrency: 2 + +defaults: + timeout: 20m + +xcuitest: + app: ./DerivedData/Build/Products/Debug-iphoneos/TrendingMovies.app + testApp: ./DerivedData/Build/Products/Debug-iphoneos/ProfileDataGeneratorUITest-Runner.app + +suites: + +# iPhone 11 +# iPhone 11 Pro Max +# iPhone 12 +# iPhone 12 mini +# iPhone 13 +# iPhone 13 Pro +# iPhone 13 Pro Max +# iPhone 13 mini +# iPhone XR + - name: "iOS-15.5" + devices: + - name: "iPhone.*" + platformVersion: "15.5" + +# iPad 10.2 2020 +# iPad Air 2022 5th Gen +# iPad Mini 2021 6th Gen +# iPhone SE 2022 +# iPhone XS + - name: "iOS-15.4" + devices: + - name: "iPhone.*" + platformVersion: "15.4" + +# iPad Air 3 (2019) +# iPhone 11 +# iPhone 11 Pro +# iPhone 12 +# iPhone 12 Pro +# iPhone 12 Pro Max +# iPhone 7 Plus +# iPhone 8 +# iPhone SE 2020 +# iPhone X + - name: "iOS-14.8" + devices: + - name: "iPhone.*" + platformVersion: "14.8" + +# iPad Pro 12.9 2020 +# iPad Pro 12.9 2021 + - name: "iOS-14.7" + devices: + - name: "iPhone.*" + platformVersion: "14.7" + +#iPad Pro 11 2018 +#iPad Pro 12.9 2018 +#iPhone SE 2020 +#iPhone X + - name: "iOS-13.7" + devices: + - name: "iPhone.*" + platformVersion: "13.7" diff --git a/Samples/TrendingMovies/ProfileDataGeneratorUITest/ProfileDataGeneratorUITest.m b/Samples/TrendingMovies/ProfileDataGeneratorUITest/ProfileDataGeneratorUITest.m new file mode 100644 index 00000000000..b6ed51fa28d --- /dev/null +++ b/Samples/TrendingMovies/ProfileDataGeneratorUITest/ProfileDataGeneratorUITest.m @@ -0,0 +1,124 @@ +#import + +static NSUInteger const kMaxScrollCount = 10; +static NSUInteger const kMaxConsecutiveFindCellFailures = 3; +static NSTimeInterval const kWaitForAppStateTimeout = 10.0; +static NSTimeInterval const kWaitForElementTimeout = 5.0; + +@interface ProfileDataGeneratorUITest : XCTestCase +@end + +@implementation ProfileDataGeneratorUITest + +- (void)setUp +{ + [super setUp]; + self.continueAfterFailure = NO; +} + +- (void)testGenerateProfileData +{ + CFTimeInterval const startTime = CACurrentMediaTime(); + CFTimeInterval const runDuration_seconds = 3.0 * 60.0; + generateProfileData(5 /* nCellsPerTab */, YES /* clearState */); + while (true) { + if ((CACurrentMediaTime() - startTime) >= runDuration_seconds) { + break; + } + if (!generateProfileData(5 /* nCellsPerTab */, NO /* clearState */)) { + break; + } + } +} + +/** + * Generates profile data by interacting with UI elements in the TrendingMovies app while running a + * Sentry transaction with profiling enabled. + * @param nCellsPerTab The number of cells to tap on, per tab. + * @param clearState Whether to clear filesystem state when the app starts. + * @return Whether the operation was successful or not. + */ +BOOL +generateProfileData(NSUInteger nCellsPerTab, BOOL clearState) +{ + XCUIApplication *app = [[XCUIApplication alloc] init]; + if (clearState) { + app.launchArguments = @[ @"--clear" ]; + } + [app launch]; + if (![app waitForState:XCUIApplicationStateRunningForeground timeout:kWaitForAppStateTimeout]) { + XCTFail("App failed to transition to Foreground state"); + return NO; + } + + XCUIElementQuery *const tabBarButtons = app.tabBars.firstMatch.buttons; + NSUInteger consecutiveFindCellFailureCount = 0; + for (NSUInteger t = 0; t < tabBarButtons.count; t++) { + XCUIElement *const tabBarButton = [tabBarButtons elementBoundByIndex:t]; + if (![tabBarButton waitForExistenceWithTimeout:kWaitForElementTimeout]) { + XCTFail("Failed to find tab bar button %llu", (unsigned long long)t); + return NO; + } + [tabBarButton doubleTap]; + + for (NSUInteger i = 0; i < nCellsPerTab; i++) { + XCUIElement *const cellElement + = app.collectionViews + .cells[[NSString stringWithFormat:@"movie %llu", (unsigned long long)i]]; + + NSUInteger scrollCount = 0; + BOOL retriedOnce = NO; + while (!cellElement.hittable) { + [app swipeUpWithVelocity:XCUIGestureVelocitySlow]; + scrollCount++; + + if (scrollCount >= kMaxScrollCount) { + if (!retriedOnce) { + // We might have overshot the cell, so scroll back up to the top and + // try again. + for (NSUInteger i = 0; i < kMaxScrollCount; i++) { + [app swipeDownWithVelocity:XCUIGestureVelocityFast]; + } + scrollCount = 0; + retriedOnce = YES; + } else { + // Something's wrong, bail out. + break; + } + } + } + if (![cellElement waitForExistenceWithTimeout:kWaitForElementTimeout]) { + consecutiveFindCellFailureCount++; + break; + } + consecutiveFindCellFailureCount = 0; + [cellElement tap]; + [NSThread sleepForTimeInterval:1.0]; + XCUIElement *const backButton = [app.navigationBars.buttons elementBoundByIndex:0]; + if (![backButton waitForExistenceWithTimeout:kWaitForElementTimeout]) { + XCTFail("Failed to find back button"); + return NO; + } + [backButton tap]; + } + + if (consecutiveFindCellFailureCount >= kMaxConsecutiveFindCellFailures) { + XCTFail("Failed to find a cell %llu times", + (unsigned long long)consecutiveFindCellFailureCount); + break; + } + } + + [XCUIDevice.sharedDevice pressButton:XCUIDeviceButtonHome]; + // Allow some time for the data to be uploaded before the app is killed. + [NSThread sleepForTimeInterval:5.0]; + + [app terminate]; + if (![app waitForState:XCUIApplicationStateNotRunning timeout:kWaitForAppStateTimeout]) { + XCTFail("App failed to transition to NotRunning state"); + return NO; + } + return YES; +} + +@end diff --git a/Samples/TrendingMovies/TrendingMovies.xcodeproj/project.pbxproj b/Samples/TrendingMovies/TrendingMovies.xcodeproj/project.pbxproj index e2b4503653d..e02acd3d182 100644 --- a/Samples/TrendingMovies/TrendingMovies.xcodeproj/project.pbxproj +++ b/Samples/TrendingMovies/TrendingMovies.xcodeproj/project.pbxproj @@ -66,6 +66,7 @@ 844A357B282B4B6500C6D1DF /* KingfisherSwiftUI.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 844A3573282B4B6500C6D1DF /* KingfisherSwiftUI.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 844A359C282DAA6100C6D1DF /* Sentry.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 844A356B282B3E4500C6D1DF /* Sentry.framework */; }; 844A359D282DAA6100C6D1DF /* Sentry.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 844A356B282B3E4500C6D1DF /* Sentry.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 847EA5542852F7E800F65FE4 /* ProfileDataGeneratorUITest.m in Sources */ = {isa = PBXBuildFile; fileRef = 847EA5532852F7E800F65FE4 /* ProfileDataGeneratorUITest.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -83,6 +84,13 @@ remoteGlobalIDString = 63AA76651EB8CB2F00D153DE; remoteInfo = SentryTests; }; + 847EA5492852F7CD00F65FE4 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 844A34CD282B2B6100C6D1DF /* Project object */; + proxyType = 1; + remoteGlobalIDString = 844A34D4282B2B6100C6D1DF; + remoteInfo = TrendingMovies; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -162,6 +170,8 @@ 844A3571282B4B6500C6D1DF /* FaceAware.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = FaceAware.xcframework; path = Carthage/Build/FaceAware.xcframework; sourceTree = ""; }; 844A3572282B4B6500C6D1DF /* Kingfisher.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Kingfisher.xcframework; path = Carthage/Build/Kingfisher.xcframework; sourceTree = ""; }; 844A3573282B4B6500C6D1DF /* KingfisherSwiftUI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = KingfisherSwiftUI.xcframework; path = Carthage/Build/KingfisherSwiftUI.xcframework; sourceTree = ""; }; + 847EA5432852F7CD00F65FE4 /* ProfileDataGeneratorUITest.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ProfileDataGeneratorUITest.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 847EA5532852F7E800F65FE4 /* ProfileDataGeneratorUITest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ProfileDataGeneratorUITest.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -177,6 +187,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 847EA5402852F7CD00F65FE4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -201,6 +218,7 @@ 844A352B282B2B9B00C6D1DF /* Videos */, 844A3511282B2B9B00C6D1DF /* ViewControllers */, 844A3525282B2B9B00C6D1DF /* YouTube */, + 847EA5442852F7CD00F65FE4 /* ProfileDataGeneratorUITest */, 844A34D6282B2B6100C6D1DF /* Products */, 844A34E7282B2B9A00C6D1DF /* TrendingMovies-Bridging-Header.h */, 844A356F282B4B6500C6D1DF /* Frameworks */, @@ -211,6 +229,7 @@ isa = PBXGroup; children = ( 844A34D5282B2B6100C6D1DF /* TrendingMovies.app */, + 847EA5432852F7CD00F65FE4 /* ProfileDataGeneratorUITest.xctest */, ); name = Products; sourceTree = ""; @@ -386,6 +405,14 @@ name = Frameworks; sourceTree = ""; }; + 847EA5442852F7CD00F65FE4 /* ProfileDataGeneratorUITest */ = { + isa = PBXGroup; + children = ( + 847EA5532852F7E800F65FE4 /* ProfileDataGeneratorUITest.m */, + ); + path = ProfileDataGeneratorUITest; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -407,6 +434,24 @@ productReference = 844A34D5282B2B6100C6D1DF /* TrendingMovies.app */; productType = "com.apple.product-type.application"; }; + 847EA5422852F7CD00F65FE4 /* ProfileDataGeneratorUITest */ = { + isa = PBXNativeTarget; + buildConfigurationList = 847EA54F2852F7CD00F65FE4 /* Build configuration list for PBXNativeTarget "ProfileDataGeneratorUITest" */; + buildPhases = ( + 847EA53F2852F7CD00F65FE4 /* Sources */, + 847EA5402852F7CD00F65FE4 /* Frameworks */, + 847EA5412852F7CD00F65FE4 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 847EA54A2852F7CD00F65FE4 /* PBXTargetDependency */, + ); + name = ProfileDataGeneratorUITest; + productName = DataGeneratorUITest; + productReference = 847EA5432852F7CD00F65FE4 /* ProfileDataGeneratorUITest.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -421,6 +466,10 @@ CreatedOnToolsVersion = 13.2; LastSwiftMigration = 1320; }; + 847EA5422852F7CD00F65FE4 = { + CreatedOnToolsVersion = 13.2; + TestTargetID = 844A34D4282B2B6100C6D1DF; + }; }; }; buildConfigurationList = 844A34D0282B2B6100C6D1DF /* Build configuration list for PBXProject "TrendingMovies" */; @@ -443,6 +492,7 @@ projectRoot = ""; targets = ( 844A34D4282B2B6100C6D1DF /* TrendingMovies */, + 847EA5422852F7CD00F65FE4 /* ProfileDataGeneratorUITest */, ); }; /* End PBXProject section */ @@ -474,6 +524,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 847EA5412852F7CD00F65FE4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -531,8 +588,24 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 847EA53F2852F7CD00F65FE4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 847EA5542852F7E800F65FE4 /* ProfileDataGeneratorUITest.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 847EA54A2852F7CD00F65FE4 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 844A34D4282B2B6100C6D1DF /* TrendingMovies */; + targetProxy = 847EA5492852F7CD00F65FE4 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 844A3522282B2B9B00C6D1DF /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; @@ -579,7 +652,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -666,12 +739,13 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 97JCY7859U; FRAMEWORK_SEARCH_PATHS = "\"$(SRCROOT)/Carthage/Build\""; INFOPLIST_FILE = "$(SRCROOT)/TrendingMovies/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", @@ -680,6 +754,7 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = io.sentry.sample.TrendingMovies; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match Development io.sentry.sample.TrendingMovies"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "TrendingMovies-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -693,12 +768,13 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 97JCY7859U; FRAMEWORK_SEARCH_PATHS = "\"$(SRCROOT)/Carthage/Build\""; INFOPLIST_FILE = "$(SRCROOT)/TrendingMovies/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", @@ -707,12 +783,56 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = io.sentry.sample.TrendingMovies; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "TrendingMovies-Bridging-Header.h"; SWIFT_VERSION = 5.0; }; name = Release; }; + 847EA54B2852F7CD00F65FE4 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 97JCY7859U; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.sentry.sample.movies.ProfileDataGeneratorUITest; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match Development io.sentry.sample.movies.ProfileDataGeneratorUITest.xctrunner"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = TrendingMovies; + }; + name = Debug; + }; + 847EA54C2852F7CD00F65FE4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 97JCY7859U; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.sentry.sample.movies.ProfileDataGeneratorUITest; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = TrendingMovies; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -734,6 +854,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 847EA54F2852F7CD00F65FE4 /* Build configuration list for PBXNativeTarget "ProfileDataGeneratorUITest" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 847EA54B2852F7CD00F65FE4 /* Debug */, + 847EA54C2852F7CD00F65FE4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 844A34CD282B2B6100C6D1DF /* Project object */; diff --git a/Samples/TrendingMovies/TrendingMovies.xcodeproj/xcshareddata/xcschemes/TrendingMovies.xcscheme b/Samples/TrendingMovies/TrendingMovies.xcodeproj/xcshareddata/xcschemes/TrendingMovies.xcscheme index 58b5fb45ee3..67c14f5de77 100644 --- a/Samples/TrendingMovies/TrendingMovies.xcodeproj/xcshareddata/xcschemes/TrendingMovies.xcscheme +++ b/Samples/TrendingMovies/TrendingMovies.xcodeproj/xcshareddata/xcschemes/TrendingMovies.xcscheme @@ -28,6 +28,16 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> + + + + Date: Fri, 24 Jun 2022 15:39:16 +0200 Subject: [PATCH 04/14] Fix high percentage of slow frames (#1915) The SentryFramesTracker didn't handle fps changes from the CADisplayLink. This is fixed now by calculating the actual frame rate as pointed out by the docs. Fixes GH-1881 --- .github/workflows/saucelabs-UI-tests.yml | 16 +++--- .sauce/config.yml | 5 ++ CHANGELOG.md | 6 +++ .../iOS-Swift/Base.lproj/Main.storyboard | 20 ++++--- .../iOS-Swift/iOS-Swift/ViewController.swift | 11 ++++ .../iOS-SwiftUITests/LaunchUITests.swift | 20 +++++++ Sources/Sentry/SentryFramesTracker.m | 32 ++++++----- .../Sentry/include/SentryDisplayLinkWrapper.h | 2 + .../Sentry/include/SentryDisplayLinkWrapper.m | 5 ++ .../SentryFramesTrackerTests.swift | 31 +++++------ .../TestDisplayLinkWrapper.swift | 53 ++++++++++++++----- Tests/SentryTests/Protocol/TestData.swift | 16 ------ 12 files changed, 145 insertions(+), 72 deletions(-) diff --git a/.github/workflows/saucelabs-UI-tests.yml b/.github/workflows/saucelabs-UI-tests.yml index 3389fb5b9ea..6232a4ace95 100644 --- a/.github/workflows/saucelabs-UI-tests.yml +++ b/.github/workflows/saucelabs-UI-tests.yml @@ -49,7 +49,7 @@ jobs: run-ui-tests-with-sauce: - name: Run UI Tests for iOS ${{ matrix.iOS }} on Sauce Labs + name: Run UI Tests for iOS ${{ matrix.suite }} on Sauce Labs runs-on: ubuntu-latest needs: build-ui-tests strategy: @@ -57,19 +57,23 @@ jobs: matrix: include: - xcode: "13.2" - iOS: 15 + suite: "iOS-15" + # We want to test the frame tracker at 120 fps - xcode: "13.2" - iOS: 14 + suite: "iPhone-Pro" - xcode: "13.2" - iOS: 13 + suite: "iOS-14" + + - xcode: "13.2" + suite: "iOS-13" # iOS 12 has a failing test that we need to fix https://github.com/getsentry/sentry-cocoa/issues/1566 # iOS 11 keeps timing out and we don't know how to fix it. - xcode: "12.5.1" - iOS: 10 + suite: "iOS-10" steps: - uses: actions/checkout@v3 @@ -85,4 +89,4 @@ jobs: env: SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} - run: for i in {1..5}; do saucectl run --select-suite iOS-${{ matrix.iOS }} && break ; done + run: for i in {1..5}; do saucectl run --select-suite ${{ matrix.suite }} && break ; done diff --git a/.sauce/config.yml b/.sauce/config.yml index a0aad50d9f5..97816c9a15d 100644 --- a/.sauce/config.yml +++ b/.sauce/config.yml @@ -17,6 +17,11 @@ suites: devices: - name: "iPhone.*" platformVersion: "15.4" + + - name: "iPhone-Pro" + devices: + - name: "iPhone 13 Pro.*" + platformVersion: "15.5" - name: "iOS-14" devices: diff --git a/CHANGELOG.md b/CHANGELOG.md index 34d24c6328e..9f480999a2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixes + +- Fix high percentage of slow frames (#1915) + ## 7.18.0 ### Features diff --git a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard index cf07b3a6977..0bd9427c9dc 100644 --- a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard +++ b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -18,7 +18,7 @@ - + @@ -185,7 +185,7 @@ - + - + @@ -201,13 +201,20 @@ - + @@ -227,6 +234,7 @@ + diff --git a/Samples/iOS-Swift/iOS-Swift/ViewController.swift b/Samples/iOS-Swift/iOS-Swift/ViewController.swift index b95168f78ed..beea55ded64 100644 --- a/Samples/iOS-Swift/iOS-Swift/ViewController.swift +++ b/Samples/iOS-Swift/iOS-Swift/ViewController.swift @@ -6,6 +6,7 @@ class ViewController: UIViewController { @IBOutlet weak var dsnTextField: UITextField! @IBOutlet weak var anrFullyBlockingButton: UIButton! @IBOutlet weak var anrFillingRunLoopButton: UIButton! + @IBOutlet weak var framesLabel: UILabel! private let dispatchQueue = DispatchQueue(label: "ViewController") @@ -45,6 +46,16 @@ class ViewController: UIViewController { } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if #available(iOS 10.0, *) { + Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in + self.framesLabel.text = "Frames Total:\(PrivateSentrySDKOnly.currentScreenFrames.total) Slow:\(PrivateSentrySDKOnly.currentScreenFrames.slow) Frozen:\(PrivateSentrySDKOnly.currentScreenFrames.frozen)" + } + } + } + @IBAction func addBreadcrumb(_ sender: Any) { let crumb = Breadcrumb(level: SentryLevel.info, category: "Debug") crumb.message = "tapped addBreadcrumb" diff --git a/Samples/iOS-Swift/iOS-SwiftUITests/LaunchUITests.swift b/Samples/iOS-Swift/iOS-SwiftUITests/LaunchUITests.swift index 44317e6c7c0..20712d3649a 100644 --- a/Samples/iOS-Swift/iOS-SwiftUITests/LaunchUITests.swift +++ b/Samples/iOS-Swift/iOS-SwiftUITests/LaunchUITests.swift @@ -12,6 +12,7 @@ class LaunchUITests: XCTestCase { app.launch() waitForExistenseOfMainScreen() + checkSlowAndFrozenFrames() } override func tearDown() { @@ -70,6 +71,25 @@ class LaunchUITests: XCTestCase { XCTAssertTrue(app.buttons["captureMessageButton"].waitForExistence(), "Home Screen doesn't exist.") } + private func checkSlowAndFrozenFrames() { + let frameStatsLabel = app.staticTexts["framesStatsLabel"] + XCTAssertTrue(frameStatsLabel.waitForExistence(), "Frame statistics message not found.") + + let frameStatsAsStringArray = frameStatsLabel.label.components(separatedBy: CharacterSet.decimalDigits.inverted) + let frameStats = frameStatsAsStringArray.filter { $0 != "" }.map { Int($0) } + + XCTAssertEqual(3, frameStats.count) + guard let totalFrames = frameStats[0] else { XCTFail("No total frames found."); return } + guard let slowFrames = frameStats[1] else { XCTFail("No slow frames found."); return } + guard let frozenFrames = frameStats[1] else { XCTFail("No frozen frames found."); return } + + let slowFramesPercentage = Double(slowFrames) / Double(totalFrames) + XCTAssertTrue(0.5 > slowFramesPercentage, "Too many slow frames.") + + let frozenFramesPercentage = Double(frozenFrames) / Double(totalFrames) + XCTAssertTrue(0.5 > frozenFramesPercentage, "Too many frozen frames.") + } + private func assertApp() { let confirmation = app.staticTexts["ASSERT_MESSAGE"] let errorMessage = app.staticTexts["ASSERT_ERROR"] diff --git a/Sources/Sentry/SentryFramesTracker.m b/Sources/Sentry/SentryFramesTracker.m index b024d50dcad..54bd25c9395 100644 --- a/Sources/Sentry/SentryFramesTracker.m +++ b/Sources/Sentry/SentryFramesTracker.m @@ -1,5 +1,6 @@ #import "SentryFramesTracker.h" #import "SentryDisplayLinkWrapper.h" +#import #import #include @@ -19,7 +20,6 @@ SentryFramesTracker () @property (nonatomic, strong, readonly) SentryDisplayLinkWrapper *displayLinkWrapper; -@property (nonatomic, assign, readonly) CFTimeInterval slowFrameThreshold; @property (nonatomic, assign) CFTimeInterval previousFrameTimestamp; @end @@ -52,17 +52,6 @@ - (instancetype)initWithDisplayLinkWrapper:(SentryDisplayLinkWrapper *)displayLi if (self = [super init]) { _isRunning = NO; _displayLinkWrapper = displayLinkWrapper; - - // If we can't get the frame rate we assume it is 60. - double maximumFramesPerSecond = 60.0; - if (@available(iOS 10.3, tvOS 10.3, macCatalyst 13.0, *)) { - maximumFramesPerSecond = (double)UIScreen.mainScreen.maximumFramesPerSecond; - } - - // Most frames take just a few microseconds longer than the optimal caculated duration. - // Therefore we substract one, because otherwise almost all frames would be slow. - _slowFrameThreshold = 1 / (maximumFramesPerSecond - 1); - [self resetFrames]; } return self; @@ -99,10 +88,27 @@ - (void)displayLinkCallback return; } + // Calculate the actual frame rate as pointed out by the Apple docs: + // https://developer.apple.com/documentation/quartzcore/cadisplaylink?language=objc The actual + // frame rate can change at any time by setting preferredFramesPerSecond or due to ProMotion + // display, low power mode, critical thermal state, and accessibility settings. Therefore we + // need to check the frame rate for every callback. + // targetTimestamp is only available on iOS 10.0 and tvOS 10.0 and above. We use a fallback of + // 60 fps. + double actualFramesPerSecond = 60.0; + if (@available(iOS 10.0, tvOS 10.0, *)) { + actualFramesPerSecond + = 1 / (self.displayLinkWrapper.targetTimestamp - self.displayLinkWrapper.timestamp); + } + + // Most frames take just a few microseconds longer than the optimal caculated duration. + // Therefore we substract one, because otherwise almost all frames would be slow. + CFTimeInterval slowFrameThreshold = 1 / (actualFramesPerSecond - 1); + CFTimeInterval frameDuration = lastFrameTimestamp - self.previousFrameTimestamp; self.previousFrameTimestamp = lastFrameTimestamp; - if (frameDuration > self.slowFrameThreshold && frameDuration <= SentryFrozenFrameThreshold) { + if (frameDuration > slowFrameThreshold && frameDuration <= SentryFrozenFrameThreshold) { atomic_fetch_add_explicit(&_slowFrames, 1, SentryFramesMemoryOrder); } diff --git a/Sources/Sentry/include/SentryDisplayLinkWrapper.h b/Sources/Sentry/include/SentryDisplayLinkWrapper.h index f76de88e9f1..99e55f9bb13 100644 --- a/Sources/Sentry/include/SentryDisplayLinkWrapper.h +++ b/Sources/Sentry/include/SentryDisplayLinkWrapper.h @@ -11,6 +11,8 @@ NS_ASSUME_NONNULL_BEGIN @property (readonly, nonatomic) CFTimeInterval timestamp; +@property (readonly, nonatomic) CFTimeInterval targetTimestamp API_AVAILABLE(ios(10.0), tvos(10.0)); + - (void)linkWithTarget:(id)target selector:(SEL)sel; - (void)invalidate; diff --git a/Sources/Sentry/include/SentryDisplayLinkWrapper.m b/Sources/Sentry/include/SentryDisplayLinkWrapper.m index 35ddfd67617..48bdc3ea100 100644 --- a/Sources/Sentry/include/SentryDisplayLinkWrapper.m +++ b/Sources/Sentry/include/SentryDisplayLinkWrapper.m @@ -12,6 +12,11 @@ - (CFTimeInterval)timestamp return displayLink.timestamp; } +- (CFTimeInterval)targetTimestamp API_AVAILABLE(ios(10.0), tvos(10.0)) +{ + return displayLink.targetTimestamp; +} + - (void)linkWithTarget:(id)target selector:(SEL)sel { displayLink = [CADisplayLink displayLinkWithTarget:target selector:sel]; diff --git a/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift index 5fea9a1c859..88978d52c78 100644 --- a/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift @@ -47,12 +47,9 @@ class SentryFramesTrackerTests: XCTestCase { sut.start() fixture.displayLinkWrapper.call() - fixture.displayLinkWrapper.internalTimestamp += TestData.slowFrameThreshold + 0.001 - fixture.displayLinkWrapper.call() - fixture.displayLinkWrapper.internalTimestamp += TestData.slowFrameThreshold - fixture.displayLinkWrapper.call() - fixture.displayLinkWrapper.internalTimestamp += TestData.frozenFrameThreshold - fixture.displayLinkWrapper.call() + fixture.displayLinkWrapper.slowFrame() + fixture.displayLinkWrapper.normalFrame() + fixture.displayLinkWrapper.almostFrozenFrame() let currentFrames = sut.currentFrames XCTAssertEqual(2, currentFrames.slow) @@ -65,10 +62,8 @@ class SentryFramesTrackerTests: XCTestCase { sut.start() fixture.displayLinkWrapper.call() - fixture.displayLinkWrapper.internalTimestamp += TestData.frozenFrameThreshold + 0.001 - fixture.displayLinkWrapper.call() - fixture.displayLinkWrapper.internalTimestamp += TestData.frozenFrameThreshold - fixture.displayLinkWrapper.call() + fixture.displayLinkWrapper.slowFrame() + fixture.displayLinkWrapper.frozenFrame() let currentFrames = sut.currentFrames XCTAssertEqual(1, currentFrames.slow) @@ -91,16 +86,14 @@ class SentryFramesTrackerTests: XCTestCase { let frames: UInt = 600_000 for _ in 0 ..< frames { - fixture.displayLinkWrapper.internalTimestamp += TestData.slowFrameThreshold + 0.001 - fixture.displayLinkWrapper.call() - - fixture.displayLinkWrapper.internalTimestamp += TestData.frozenFrameThreshold + 0.001 - fixture.displayLinkWrapper.call() + fixture.displayLinkWrapper.normalFrame() + fixture.displayLinkWrapper.slowFrame() + fixture.displayLinkWrapper.frozenFrame() } group.wait() currentFrames = sut.currentFrames - XCTAssertEqual(2 * frames, currentFrames.total) + XCTAssertEqual(3 * frames, currentFrames.total) XCTAssertEqual(frames, currentFrames.slow) XCTAssertEqual(frames, currentFrames.frozen) } @@ -112,10 +105,12 @@ class SentryFramesTrackerTests: XCTestCase { let frames: UInt = 1_000 self.measure { for _ in 0 ..< frames { - fixture.displayLinkWrapper.call() - fixture.displayLinkWrapper.internalTimestamp += TestData.slowFrameThreshold + fixture.displayLinkWrapper.normalFrame() } } + + XCTAssertEqual(0, sut.currentFrames.slow) + XCTAssertEqual(0, sut.currentFrames.frozen) } private func assertPreviousCountBiggerThanCurrent(_ group: DispatchGroup, count: @escaping () -> UInt) { diff --git a/Tests/SentryTests/Integrations/Performance/FramesTracking/TestDisplayLinkWrapper.swift b/Tests/SentryTests/Integrations/Performance/FramesTracking/TestDisplayLinkWrapper.swift index f9eb3bb167a..a2808407509 100644 --- a/Tests/SentryTests/Integrations/Performance/FramesTracking/TestDisplayLinkWrapper.swift +++ b/Tests/SentryTests/Integrations/Performance/FramesTracking/TestDisplayLinkWrapper.swift @@ -5,6 +5,17 @@ class TestDiplayLinkWrapper: SentryDisplayLinkWrapper { var target: AnyObject! var selector: Selector! + var internalTimestamp = 0.0 + var internalActualFrameRate = 60.0 + let frozenFrameThreshold = 0.7 + + var frameDuration: Double { + return 1.0 / internalActualFrameRate + } + + private var slowFrameThreshold: CFTimeInterval { + return 1 / (Double(internalActualFrameRate) - 1.0) + } override func link(withTarget target: Any, selector sel: Selector) { self.target = target as AnyObject @@ -14,13 +25,35 @@ class TestDiplayLinkWrapper: SentryDisplayLinkWrapper { func call() { _ = target.perform(selector) } - - var internalTimestamp = 0.0 - + override var timestamp: CFTimeInterval { return internalTimestamp } + func normalFrame() { + internalTimestamp += frameDuration + call() + } + + func slowFrame() { + internalTimestamp += slowFrameThreshold + 0.001 + call() + } + + func almostFrozenFrame() { + internalTimestamp += frozenFrameThreshold + call() + } + + func frozenFrame() { + internalTimestamp += frozenFrameThreshold + 0.001 + call() + } + + override var targetTimestamp: CFTimeInterval { + return internalTimestamp + frameDuration + } + override func invalidate() { target = nil selector = nil @@ -29,22 +62,16 @@ class TestDiplayLinkWrapper: SentryDisplayLinkWrapper { func givenFrames(_ slow: Int, _ frozen: Int, _ normal: Int) { self.call() - // Slow frames for _ in 0.. SentryAppStartMeasurement { let appStartDuration = 0.5 let runtimeInit = appStartTimestamp.addingTimeInterval(0.2) From f906913c14eadb6fd31988db1ea9ff10aec9f198 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Fri, 24 Jun 2022 14:05:00 +0000 Subject: [PATCH 05/14] release: 7.18.1 --- CHANGELOG.md | 2 +- Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj | 8 ++++---- Sentry.podspec | 2 +- Sources/Configuration/Sentry.xcconfig | 2 +- Sources/Sentry/SentryMeta.m | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f480999a2e..2bc779c844e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 7.18.1 ### Fixes diff --git a/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj b/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj index af220010d10..db49b1acb75 100644 --- a/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj +++ b/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj @@ -966,7 +966,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 7.18.0; + MARKETING_VERSION = 7.18.1; PRODUCT_BUNDLE_IDENTIFIER = "io.sentry.sample.iOS-Swift"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match Development io.sentry.sample.iOS-Swift"; @@ -995,7 +995,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 7.18.0; + MARKETING_VERSION = 7.18.1; PRODUCT_BUNDLE_IDENTIFIER = "io.sentry.sample.iOS-Swift"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore io.sentry.sample.iOS-Swift"; @@ -1145,7 +1145,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 7.18.0; + MARKETING_VERSION = 7.18.1; PRODUCT_BUNDLE_IDENTIFIER = "io.sentry.sample.iOS-Swift.Clip"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match Development io.sentry.sample.iOS-Swift.Clip"; @@ -1181,7 +1181,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 7.18.0; + MARKETING_VERSION = 7.18.1; PRODUCT_BUNDLE_IDENTIFIER = "io.sentry.sample.iOS-Swift.Clip"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore io.sentry.sample.iOS-Swift.Clip"; diff --git a/Sentry.podspec b/Sentry.podspec index a0ff3e2e8bd..931e33389f1 100644 --- a/Sentry.podspec +++ b/Sentry.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "Sentry" - s.version = "7.18.0" + s.version = "7.18.1" s.summary = "Sentry client for cocoa" s.homepage = "https://github.com/getsentry/sentry-cocoa" s.license = "mit" diff --git a/Sources/Configuration/Sentry.xcconfig b/Sources/Configuration/Sentry.xcconfig index 24b151526e5..2dbc03a5705 100644 --- a/Sources/Configuration/Sentry.xcconfig +++ b/Sources/Configuration/Sentry.xcconfig @@ -29,7 +29,7 @@ MACH_O_TYPE = mh_dylib FRAMEWORK_VERSION = A PRODUCT_NAME = Sentry -CURRENT_PROJECT_VERSION = 7.18.0 +CURRENT_PROJECT_VERSION = 7.18.1 INFOPLIST_FILE = Sources/Sentry/Info.plist PRODUCT_BUNDLE_IDENTIFIER = io.sentry.Sentry ALWAYS_SEARCH_USER_PATHS = NO diff --git a/Sources/Sentry/SentryMeta.m b/Sources/Sentry/SentryMeta.m index f64465783ae..77d8f982d72 100644 --- a/Sources/Sentry/SentryMeta.m +++ b/Sources/Sentry/SentryMeta.m @@ -5,7 +5,7 @@ @implementation SentryMeta // Don't remove the static keyword. If you do the compiler adds the constant name to the global // symbol table and it might clash with other constants. When keeping the static keyword the // compiler replaces all occurrences with the value. -static NSString *const versionString = @"7.18.0"; +static NSString *const versionString = @"7.18.1"; static NSString *const sdkName = @"sentry.cocoa"; + (NSString *)versionString From 4e3946cdd9613da63edeef45ca70388fb68ceaed Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Mon, 27 Jun 2022 15:45:56 -0400 Subject: [PATCH 06/14] fix: remove webkit/optimization check (#1921) --- Sources/Sentry/include/SentryCompiler.h | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Sources/Sentry/include/SentryCompiler.h b/Sources/Sentry/include/SentryCompiler.h index 084addf988d..1d097c011ae 100644 --- a/Sources/Sentry/include/SentryCompiler.h +++ b/Sources/Sentry/include/SentryCompiler.h @@ -102,10 +102,6 @@ #endif /* COMPILER(GCC) */ -#if COMPILER(GCC_COMPATIBLE) && !defined(DEBUG) && !defined(__OPTIMIZE__) && !defined(RELEASE_WITHOUT_OPTIMIZATIONS) -#error "Building release without compiler optimizations: WebKit will be slow. Set -DRELEASE_WITHOUT_OPTIMIZATIONS if this is intended." -#endif - /* COMPILER(MINGW) - MinGW GCC */ #if defined(__MINGW32__) From 94de24e759ce565c3b9acbdc158b213cd7371a24 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Tue, 28 Jun 2022 09:33:59 +0200 Subject: [PATCH 07/14] Fix changelog (#1922) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bc779c844e..265d1e18360 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixes + +- Remove WebKit optimization check (#1921) + ## 7.18.1 ### Fixes From 02de834216db8870d340851f5aad219ceae3d167 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Wed, 29 Jun 2022 09:55:55 +0200 Subject: [PATCH 08/14] feat: Add App Hangs tracking (#1906) Added app hangs tracking Co-authored-by: Dhiogo Brustolin --- CHANGELOG.md | 6 +- Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 3 + Sentry.xcodeproj/project.pbxproj | 6 + .../xcshareddata/xcschemes/Sentry.xcscheme | 5 + Sources/Sentry/Public/SentryOptions.h | 14 ++ Sources/Sentry/SentryANRTracker.m | 114 ++++++++++++---- Sources/Sentry/SentryANRTrackingIntegration.m | 78 ++++++----- Sources/Sentry/SentryClient.m | 1 + .../SentryCrashDefaultMachineContextWrapper.m | 13 ++ Sources/Sentry/SentryCrashStackEntryMapper.m | 21 +-- Sources/Sentry/SentryDependencyContainer.m | 20 +++ Sources/Sentry/SentryOptions.m | 12 ++ .../SentryOutOfMemoryTrackingIntegration.m | 27 ++++ Sources/Sentry/SentryStacktraceBuilder.m | 55 +++++++- Sources/Sentry/SentryThreadInspector.m | 116 +++++++++++++++- Sources/Sentry/include/SentryANRTracker.h | 22 ++- .../include/SentryANRTrackingIntegration.h | 4 - Sources/Sentry/include/SentryClient+Private.h | 3 +- .../SentryCrashMachineContextWrapper.h | 2 + .../include/SentryCrashStackEntryMapper.h | 7 + .../include/SentryDependencyContainer.h | 7 +- .../SentryOutOfMemoryTrackingIntegration.h | 4 +- .../Sentry/include/SentryStacktraceBuilder.h | 14 ++ .../Sentry/include/SentryThreadInspector.h | 9 ++ .../Monitors/SentryCrashMonitor_Deadlock.m | 2 +- .../SentryCrashMonitor_MachException.c | 3 +- .../Monitors/SentryCrashMonitor_Signal.c | 2 +- .../Recording/Tools/SentryCrashStackCursor.h | 33 ++--- .../SentryCrashStackCursor_MachineContext.h | 2 + .../ANR/SentryANRTrackerTests.swift | 87 +++++++++--- .../SentryANRTrackingIntegrationTests.swift | 125 +++++++++++------- .../SentryOutOfMemoryIntegrationTests.swift | 76 +++++++++++ .../SentryThreadInspectorTests.swift | 89 ++++++++++++- .../SentryCrash/TestThreadInspector.swift | 8 +- Tests/SentryTests/SentryOptionsTest.m | 15 +++ .../SentrySDKIntegrationTestsBase.swift | 92 +++++++++++++ Tests/SentryTests/SentrySDKTests.swift | 73 ---------- .../SentryTests/SentryTests-Bridging-Header.h | 2 +- 38 files changed, 916 insertions(+), 256 deletions(-) create mode 100644 Tests/SentryTests/SentrySDKIntegrationTestsBase.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 265d1e18360..047a8d75234 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Add App Hangs tracking (#1906) + ### Fixes - Remove WebKit optimization check (#1921) @@ -15,7 +19,7 @@ ## 7.18.0 ### Features - + - Replace tracestate header with baggage (#1867) ### Fixes diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index 45f75e1a132..ba08a084282 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -27,6 +27,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { options.enableCoreDataTracking = true options.enableProfiling = true options.attachScreenshot = true + options.enableAppHangTracking = true + options.appHangTimeoutInterval = 2 + options.enableUserInteractionTracing = true // the benchmark test starts and stops a custom transaction using a UIButton, and automatic user interaction tracing stops the transaction that begins with that button press after the idle timeout elapses, stopping the profiler (only one profiler runs regardless of the number of concurrent transactions) if !ProcessInfo.processInfo.arguments.contains("--io.sentry.test.benchmarking") { diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 19724aa0dce..f4bd98802d5 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -646,9 +646,11 @@ D880E3A728573E87008A90DB /* SentryBaggageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D880E3A628573E87008A90DB /* SentryBaggageTests.swift */; }; D884A20527C80F6300074664 /* SentryCoreDataTrackerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D884A20327C80F2700074664 /* SentryCoreDataTrackerTest.swift */; }; D885266427739D01001269FC /* SentryFileIOTrackingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D885266327739D01001269FC /* SentryFileIOTrackingIntegrationTests.swift */; }; + D8853C842833EABC00700D64 /* SentryANRTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 7BCFA71427D0BAB7008C662C /* SentryANRTracker.h */; }; D88817D826D7149100BF2251 /* SentryTraceContext.m in Sources */ = {isa = PBXBuildFile; fileRef = D88817D626D7149100BF2251 /* SentryTraceContext.m */; }; D88817DA26D72AB800BF2251 /* SentryTraceContext.h in Headers */ = {isa = PBXBuildFile; fileRef = D88817D926D72AB800BF2251 /* SentryTraceContext.h */; }; D88817DD26D72BA500BF2251 /* SentryTraceStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88817DB26D72B7B00BF2251 /* SentryTraceStateTests.swift */; }; + D8918B222849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8918B212849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift */; }; D8AB40DB2806EC1900E5E9F7 /* SentryScreenshotIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D8AB40DA2806EC1900E5E9F7 /* SentryScreenshotIntegration.h */; }; D8ACE3C72762187200F5A213 /* SentryNSDataSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = D8ACE3C42762187200F5A213 /* SentryNSDataSwizzling.m */; }; D8ACE3C82762187200F5A213 /* SentryNSDataTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = D8ACE3C52762187200F5A213 /* SentryNSDataTracker.m */; }; @@ -1383,6 +1385,7 @@ D88817D626D7149100BF2251 /* SentryTraceContext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryTraceContext.m; sourceTree = ""; }; D88817D926D72AB800BF2251 /* SentryTraceContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryTraceContext.h; path = include/SentryTraceContext.h; sourceTree = ""; }; D88817DB26D72B7B00BF2251 /* SentryTraceStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceStateTests.swift; sourceTree = ""; }; + D8918B212849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySDKIntegrationTestsBase.swift; sourceTree = ""; }; D8AB40DA2806EC1900E5E9F7 /* SentryScreenshotIntegration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryScreenshotIntegration.h; path = include/SentryScreenshotIntegration.h; sourceTree = ""; }; D8ACE3C42762187200F5A213 /* SentryNSDataSwizzling.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryNSDataSwizzling.m; sourceTree = ""; }; D8ACE3C52762187200F5A213 /* SentryNSDataTracker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryNSDataTracker.m; sourceTree = ""; }; @@ -1774,6 +1777,7 @@ 7B4260332630315C00B36EDD /* SampleError.swift */, 7B6D1262265F7CC600C9BE4B /* PrivateSentrySDKOnlyTests.swift */, 8ED3D305264DFE700049393B /* SentryUIViewControllerSanitizerTests.swift */, + D8918B212849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift */, ); path = SentryTests; sourceTree = ""; @@ -2918,6 +2922,7 @@ 7BE1E32824F7AE08009D3AD0 /* SentrySession+Private.h in Headers */, 7B5CAF7127F5953400ED0DB6 /* SentryEnvelope+Private.h in Headers */, 63FE713320DA4C1100CDBAE8 /* SentryCrashCPU.h in Headers */, + D8853C842833EABC00700D64 /* SentryANRTracker.h in Headers */, 63FE715B20DA4C1100CDBAE8 /* SentryCrashSignalInfo.h in Headers */, 63FE70E520DA4C1000CDBAE8 /* SentryCrashMonitor_CPPException.h in Headers */, 7B127B0D27CF6F2300A71ED2 /* SentryANRTrackingIntegration.h in Headers */, @@ -3499,6 +3504,7 @@ 7B59398224AB47650003AAD2 /* SentrySessionTrackerTests.swift in Sources */, 7B05A61824A4D14A00EF211D /* SentrySessionGeneratorTests.swift in Sources */, 63FE720920DA66EC00CDBAE8 /* XCTestCase+SentryCrash.m in Sources */, + D8918B222849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift in Sources */, 7B85BD8E24C5C3A6000A4225 /* SentryFileManagerTestExtension.swift in Sources */, 7B0002342477F52D0035FEF1 /* SentrySessionTests.swift in Sources */, ); diff --git a/Sentry.xcodeproj/xcshareddata/xcschemes/Sentry.xcscheme b/Sentry.xcodeproj/xcshareddata/xcschemes/Sentry.xcscheme index 6a92a5ba9f4..8c6597a7afd 100644 --- a/Sentry.xcodeproj/xcshareddata/xcschemes/Sentry.xcscheme +++ b/Sentry.xcodeproj/xcshareddata/xcschemes/Sentry.xcscheme @@ -54,6 +54,11 @@ BlueprintName = "SentryTests" ReferencedContainer = "container:Sentry.xcodeproj"> + + + + diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index 86eba28f2a3..5a19012e205 100644 --- a/Sources/Sentry/Public/SentryOptions.h +++ b/Sources/Sentry/Public/SentryOptions.h @@ -328,6 +328,20 @@ NS_SWIFT_NAME(Options) */ @property (nonatomic, assign) BOOL sendClientReports; +/** + * When enabled, the SDK tracks when the application stops responding for a specific amount of + * time defined by the `appHangsTimeoutInterval` option. + */ +@property (nonatomic, assign) BOOL enableAppHangTracking; + +/** + * The minimum amount of time an app should be unresponsive to be classified as an App Hanging. + * The actual amount may be a little longer. + * Avoid using values lower than 100ms, which may cause a lot of app hangs events being transmitted. + * The default value is 2 seconds. + */ +@property (nonatomic, assign) NSTimeInterval appHangTimeoutInterval; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryANRTracker.m b/Sources/Sentry/SentryANRTracker.m index 34b20fdb229..8cc327d132b 100644 --- a/Sources/Sentry/SentryANRTracker.m +++ b/Sources/Sentry/SentryANRTracker.m @@ -6,50 +6,47 @@ NS_ASSUME_NONNULL_BEGIN -#if SENTRY_HAS_UIKIT - @interface SentryANRTracker () -@property (weak, nonatomic) id delegate; -@property (nonatomic, assign) NSTimeInterval timeoutInterval; @property (nonatomic, strong) id currentDate; @property (nonatomic, strong) SentryCrashWrapper *crashWrapper; @property (nonatomic, strong) SentryDispatchQueueWrapper *dispatchQueueWrapper; @property (nonatomic, strong) SentryThreadWrapper *threadWrapper; +@property (nonatomic, strong) NSMutableSet> *listeners; +@property (nonatomic, assign) NSTimeInterval timeoutInterval; @property (weak, nonatomic) NSThread *thread; @end -@implementation SentryANRTracker +@implementation SentryANRTracker { + NSObject *threadLock; + BOOL running; +} -- (instancetype)initWithDelegate:(id)delegate - timeoutIntervalMillis:(NSUInteger)timeoutIntervalMillis - currentDateProvider:(id)currentDateProvider - crashWrapper:(SentryCrashWrapper *)crashWrapper - dispatchQueueWrapper:(SentryDispatchQueueWrapper *)dispatchQueueWrapper - threadWrapper:(SentryThreadWrapper *)threadWrapper +- (instancetype)initWithTimeoutInterval:(NSTimeInterval)timeoutInterval + currentDateProvider:(id)currentDateProvider + crashWrapper:(SentryCrashWrapper *)crashWrapper + dispatchQueueWrapper:(SentryDispatchQueueWrapper *)dispatchQueueWrapper + threadWrapper:(SentryThreadWrapper *)threadWrapper { if (self = [super init]) { - self.delegate = delegate; - self.timeoutInterval = (double)timeoutIntervalMillis / 1000; + self.timeoutInterval = timeoutInterval; self.currentDate = currentDateProvider; self.crashWrapper = crashWrapper; self.dispatchQueueWrapper = dispatchQueueWrapper; self.threadWrapper = threadWrapper; + self.listeners = [NSMutableSet new]; + threadLock = [[NSObject alloc] init]; + running = false; } return self; } -- (void)start -{ - [NSThread detachNewThreadSelector:@selector(detectANRs) toTarget:self withObject:nil]; -} - - (void)detectANRs { - NSThread.currentThread.name = @"io.sentry.anr-tracker"; + NSThread.currentThread.name = @"io.sentry.app-hang-tracker"; self.thread = NSThread.currentThread; @@ -68,7 +65,7 @@ - (void)detectANRs if (blockExecutedOnMainThread) { if (wasPreviousANR) { [SentryLog logWithMessage:@"ANR stopped." andLevel:kSentryLevelWarning]; - [self.delegate anrStopped]; + [self ANRStopped]; } wasPreviousANR = NO; @@ -103,18 +100,85 @@ - (void)detectANRs wasPreviousANR = YES; [SentryLog logWithMessage:@"ANR detected." andLevel:kSentryLevelWarning]; - [self.delegate anrDetected]; + [self ANRDetected]; + } +} + +- (void)ANRDetected +{ + NSArray *localListeners; + @synchronized(self.listeners) { + localListeners = [self.listeners allObjects]; + } + + for (id target in localListeners) { + [target anrDetected]; + } +} + +- (void)ANRStopped +{ + NSArray *targets; + @synchronized(self.listeners) { + targets = [self.listeners allObjects]; + } + + for (id target in targets) { + [target anrStopped]; + } +} + +- (void)addListener:(id)listener +{ + @synchronized(self.listeners) { + [self.listeners addObject:listener]; + + if (self.listeners.count > 0 && !running) { + @synchronized(threadLock) { + if (!running) { + [self start]; + } + } + } + } +} + +- (void)removeListener:(id)listener +{ + @synchronized(self.listeners) { + [self.listeners removeObject:listener]; + + if (self.listeners.count == 0) { + [self stop]; + } + } +} + +- (void)clear +{ + @synchronized(self.listeners) { + [self.listeners removeAllObjects]; + [self stop]; + } +} + +- (void)start +{ + @synchronized(threadLock) { + [NSThread detachNewThreadSelector:@selector(detectANRs) toTarget:self withObject:nil]; + running = YES; } } - (void)stop { - [SentryLog logWithMessage:@"Stopping ANR detection" andLevel:kSentryLevelInfo]; - [self.thread cancel]; + @synchronized(threadLock) { + [SentryLog logWithMessage:@"Stopping ANR detection" andLevel:kSentryLevelInfo]; + [self.thread cancel]; + running = NO; + } } @end -#endif - NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryANRTrackingIntegration.m b/Sources/Sentry/SentryANRTrackingIntegration.m index dc07b11c9c7..f6d4f39c969 100644 --- a/Sources/Sentry/SentryANRTrackingIntegration.m +++ b/Sources/Sentry/SentryANRTrackingIntegration.m @@ -1,9 +1,18 @@ #import "SentryANRTrackingIntegration.h" #import "SentryANRTracker.h" +#import "SentryClient+Private.h" +#import "SentryCrashMachineContext.h" #import "SentryCrashWrapper.h" #import "SentryDefaultCurrentDateProvider.h" #import "SentryDispatchQueueWrapper.h" +#import "SentryEvent.h" +#import "SentryException.h" +#import "SentryHub+Private.h" #import "SentryLog.h" +#import "SentryMechanism.h" +#import "SentrySDK+Private.h" +#import "SentryThread.h" +#import "SentryThreadInspector.h" #import "SentryThreadWrapper.h" #import #import @@ -13,20 +22,11 @@ NS_ASSUME_NONNULL_BEGIN -#if SENTRY_HAS_UIKIT - -/** - * As we only use the ANR tracking integration for detecting falsely reported OOMs we can use a more - * defensive value, because we are not reporting any ANRs. - */ -static NSUInteger const SENTRY_ANR_TRACKER_TIMEOUT_MILLIS = 2000; - @interface SentryANRTrackingIntegration () @property (nonatomic, strong) SentryANRTracker *tracker; -@property (nonatomic, strong) SentryAppStateManager *appStateManager; -@property (nonatomic, strong) SentryCrashWrapper *crashWrapper; +@property (nonatomic, strong) SentryOptions *options; @end @@ -34,34 +34,36 @@ @implementation SentryANRTrackingIntegration - (void)installWithOptions:(SentryOptions *)options { - SentryDependencyContainer *dependencies = [SentryDependencyContainer sharedInstance]; - self.crashWrapper = dependencies.crashWrapper; - if ([self shouldBeDisabled:options]) { [options removeEnabledIntegration:NSStringFromClass([self class])]; return; } - self.appStateManager = dependencies.appStateManager; - self.tracker = - [[SentryANRTracker alloc] initWithDelegate:self - timeoutIntervalMillis:SENTRY_ANR_TRACKER_TIMEOUT_MILLIS - currentDateProvider:[SentryDefaultCurrentDateProvider sharedInstance] - crashWrapper:dependencies.crashWrapper - dispatchQueueWrapper:[[SentryDispatchQueueWrapper alloc] init] - threadWrapper:dependencies.threadWrapper]; - [self.tracker start]; + [SentryDependencyContainer.sharedInstance getANRTracker:options.appHangTimeoutInterval]; + + [self.tracker addListener:self]; + self.options = options; } - (BOOL)shouldBeDisabled:(SentryOptions *)options { - if (!options.enableOutOfMemoryTracking) { + if (!options.enableAppHangTracking) { + [SentryLog logWithMessage:@"Not going to enable App Hanging integration because " + @"enableAppHangsTracking is disabled." + andLevel:kSentryLevelDebug]; + return YES; + } + + if (options.appHangTimeoutInterval == 0) { + [SentryLog logWithMessage:@"Not going to enable App Hanging integration because " + @"appHangsTimeoutInterval is 0." + andLevel:kSentryLevelDebug]; return YES; } // In case the debugger is attached - if ([self.crashWrapper isBeingTraced]) { + if ([SentryDependencyContainer.sharedInstance.crashWrapper isBeingTraced]) { return YES; } @@ -70,23 +72,37 @@ - (BOOL)shouldBeDisabled:(SentryOptions *)options - (void)uninstall { - [self.tracker stop]; + [self.tracker removeListener:self]; } - (void)anrDetected { - [self.appStateManager - updateAppState:^(SentryAppState *appState) { appState.isANROngoing = YES; }]; + SentryThreadInspector *threadInspector = SentrySDK.currentHub.getClient.threadInspector; + + NSString *message = [NSString stringWithFormat:@"App hanging for at least %li ms.", + (long)(self.options.appHangTimeoutInterval * 1000)]; + + NSArray *threads = [threadInspector getCurrentThreadsWithStackTrace]; + + SentryEvent *event = [[SentryEvent alloc] initWithLevel:kSentryLevelError]; + SentryException *sentryException = [[SentryException alloc] initWithValue:message + type:@"App Hanging"]; + sentryException.mechanism = [[SentryMechanism alloc] initWithType:@"AppHang"]; + sentryException.stacktrace = [threads[0] stacktrace]; + [threads enumerateObjectsUsingBlock:^(SentryThread *_Nonnull obj, NSUInteger idx, + BOOL *_Nonnull stop) { obj.current = [NSNumber numberWithBool:idx == 0]; }]; + + event.exceptions = @[ sentryException ]; + event.threads = threads; + + [SentrySDK captureEvent:event]; } - (void)anrStopped { - [self.appStateManager - updateAppState:^(SentryAppState *appState) { appState.isANROngoing = NO; }]; + // We dont report when an ANR ends. } @end -#endif - NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 72686b50cbc..c9a42dba3ff 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -477,6 +477,7 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event || (nil != event.exceptions && [event.exceptions count] > 0); BOOL threadsNotAttached = !(nil != event.threads && event.threads.count > 0); + if (!isCrashEvent && shouldAttachStacktrace && threadsNotAttached) { event.threads = [self.threadInspector getCurrentThreads]; } diff --git a/Sources/Sentry/SentryCrashDefaultMachineContextWrapper.m b/Sources/Sentry/SentryCrashDefaultMachineContextWrapper.m index 77001bbb729..b2f04ccb676 100644 --- a/Sources/Sentry/SentryCrashDefaultMachineContextWrapper.m +++ b/Sources/Sentry/SentryCrashDefaultMachineContextWrapper.m @@ -12,11 +12,19 @@ #import "SentryThread.h" #import #include +#include NS_ASSUME_NONNULL_BEGIN +SentryCrashThread mainThreadID; + @implementation SentryCrashDefaultMachineContextWrapper ++ (void)initialize +{ + mainThreadID = pthread_mach_thread_np(pthread_self()); +} + - (void)fillContextForCurrentThread:(struct SentryCrashMachineContext *)context { sentrycrashmc_getContextForThread(sentrycrashthread_self(), context, true); @@ -40,6 +48,11 @@ - (void)getThreadName:(const SentryCrashThread)thread sentrycrashthread_getThreadName(thread, buffer, bufLength); } +- (BOOL)isMainThread:(SentryCrashThread)thread +{ + return thread == mainThreadID; +} + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryCrashStackEntryMapper.m b/Sources/Sentry/SentryCrashStackEntryMapper.m index c9ac5dda282..4937146fc83 100644 --- a/Sources/Sentry/SentryCrashStackEntryMapper.m +++ b/Sources/Sentry/SentryCrashStackEntryMapper.m @@ -23,26 +23,26 @@ - (instancetype)initWithInAppLogic:(SentryInAppLogic *)inAppLogic return self; } -- (SentryFrame *)mapStackEntryWithCursor:(SentryCrashStackCursor)stackCursor +- (SentryFrame *)sentryCrashStackEntryToSentryFrame:(SentryCrashStackEntry)stackEntry { SentryFrame *frame = [[SentryFrame alloc] init]; - NSNumber *symbolAddress = @(stackCursor.stackEntry.symbolAddress); + NSNumber *symbolAddress = @(stackEntry.symbolAddress); frame.symbolAddress = sentry_formatHexAddress(symbolAddress); - NSNumber *instructionAddress = @(stackCursor.stackEntry.address); + NSNumber *instructionAddress = @(stackEntry.address); frame.instructionAddress = sentry_formatHexAddress(instructionAddress); - NSNumber *imageAddress = @(stackCursor.stackEntry.imageAddress); + NSNumber *imageAddress = @(stackEntry.imageAddress); frame.imageAddress = sentry_formatHexAddress(imageAddress); - if (stackCursor.stackEntry.symbolName != NULL) { - frame.function = [NSString stringWithCString:stackCursor.stackEntry.symbolName + if (stackEntry.symbolName != NULL) { + frame.function = [NSString stringWithCString:stackEntry.symbolName encoding:NSUTF8StringEncoding]; } - if (stackCursor.stackEntry.imageName != NULL) { - NSString *imageName = [NSString stringWithCString:stackCursor.stackEntry.imageName + if (stackEntry.imageName != NULL) { + NSString *imageName = [NSString stringWithCString:stackEntry.imageName encoding:NSUTF8StringEncoding]; frame.package = imageName; frame.inApp = @([self.inAppLogic isInApp:imageName]); @@ -51,6 +51,11 @@ - (SentryFrame *)mapStackEntryWithCursor:(SentryCrashStackCursor)stackCursor return frame; } +- (SentryFrame *)mapStackEntryWithCursor:(SentryCrashStackCursor)stackCursor +{ + return [self sentryCrashStackEntryToSentryFrame:stackCursor.stackEntry]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryDependencyContainer.m b/Sources/Sentry/SentryDependencyContainer.m index 8254af846dd..014bacb2a33 100644 --- a/Sources/Sentry/SentryDependencyContainer.m +++ b/Sources/Sentry/SentryDependencyContainer.m @@ -1,3 +1,6 @@ +#import "SentryANRTracker.h" +#import "SentryDefaultCurrentDateProvider.h" +#import "SentryDispatchQueueWrapper.h" #import "SentryUIApplication.h" #import #import @@ -157,4 +160,21 @@ - (SentryDebugImageProvider *)debugImageProvider return _debugImageProvider; } +- (SentryANRTracker *)getANRTracker:(NSTimeInterval)timeout +{ + if (_anrTracker == nil) { + @synchronized(sentryDependencyContainerLock) { + if (_anrTracker == nil) { + _anrTracker = [[SentryANRTracker alloc] + initWithTimeoutInterval:timeout + currentDateProvider:[SentryDefaultCurrentDateProvider sharedInstance] + crashWrapper:self.crashWrapper + dispatchQueueWrapper:[[SentryDispatchQueueWrapper alloc] init] + threadWrapper:self.threadWrapper]; + } + } + } + return _anrTracker; +} + @end diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index 9f64f77fe2a..0d98fa9e2a8 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -1,4 +1,5 @@ #import "SentryOptions.h" +#import "SentryANRTracker.h" #import "SentryDsn.h" #import "SentryLog.h" #import "SentryMeta.h" @@ -58,6 +59,9 @@ - (instancetype)init self.enableUserInteractionTracing = NO; self.idleTimeout = 3.0; #endif + self.enableAppHangTracking = NO; + self.appHangTimeoutInterval = 2.0; + self.enableNetworkTracking = YES; self.enableFileIOTracking = NO; self.enableNetworkBreadcrumbs = YES; @@ -243,8 +247,16 @@ - (BOOL)validateOptions:(NSDictionary *)options if ([options[@"idleTimeout"] isKindOfClass:[NSNumber class]]) { self.idleTimeout = [options[@"idleTimeout"] doubleValue]; } + #endif + [self setBool:options[@"enableAppHangTracking"] + block:^(BOOL value) { self->_enableAppHangTracking = value; }]; + + if ([options[@"appHangTimeoutInterval"] isKindOfClass:[NSNumber class]]) { + self.appHangTimeoutInterval = [options[@"appHangTimeoutInterval"] doubleValue]; + } + [self setBool:options[@"enableNetworkTracking"] block:^(BOOL value) { self->_enableNetworkTracking = value; }]; diff --git a/Sources/Sentry/SentryOutOfMemoryTrackingIntegration.m b/Sources/Sentry/SentryOutOfMemoryTrackingIntegration.m index a0d440b2cca..84bb2209712 100644 --- a/Sources/Sentry/SentryOutOfMemoryTrackingIntegration.m +++ b/Sources/Sentry/SentryOutOfMemoryTrackingIntegration.m @@ -1,4 +1,6 @@ +#import "SentryDefines.h" #import +#import #import #import #import @@ -19,7 +21,9 @@ SentryOutOfMemoryTrackingIntegration () @property (nonatomic, strong) SentryOutOfMemoryTracker *tracker; +@property (nonatomic, strong) SentryANRTracker *anrTracker; @property (nullable, nonatomic, copy) NSString *testConfigurationFilePath; +@property (nonatomic, strong) SentryAppStateManager *appStateManager; @end @@ -62,6 +66,12 @@ - (void)installWithOptions:(SentryOptions *)options dispatchQueueWrapper:dispatchQueueWrapper fileManager:fileManager]; [self.tracker start]; + + self.anrTracker = + [SentryDependencyContainer.sharedInstance getANRTracker:options.appHangTimeoutInterval]; + [self.anrTracker addListener:self]; + + self.appStateManager = appStateManager; } - (BOOL)shouldBeDisabled:(SentryOptions *)options @@ -86,6 +96,23 @@ - (void)uninstall if (nil != self.tracker) { [self.tracker stop]; } + [self.anrTracker removeListener:self]; +} + +- (void)anrDetected +{ +#if SENTRY_HAS_UIKIT + [self.appStateManager + updateAppState:^(SentryAppState *appState) { appState.isANROngoing = YES; }]; +#endif +} + +- (void)anrStopped +{ +#if SENTRY_HAS_UIKIT + [self.appStateManager + updateAppState:^(SentryAppState *appState) { appState.isANROngoing = NO; }]; +#endif } @end diff --git a/Sources/Sentry/SentryStacktraceBuilder.m b/Sources/Sentry/SentryStacktraceBuilder.m index fa0ead21ba1..1631ca68e18 100644 --- a/Sources/Sentry/SentryStacktraceBuilder.m +++ b/Sources/Sentry/SentryStacktraceBuilder.m @@ -1,5 +1,6 @@ #import "SentryStacktraceBuilder.h" #import "SentryCrashStackCursor.h" +#import "SentryCrashStackCursor_MachineContext.h" #import "SentryCrashStackCursor_SelfThread.h" #import "SentryCrashStackEntryMapper.h" #import "SentryFrame.h" @@ -25,15 +26,9 @@ - (id)initWithCrashStackEntryMapper:(SentryCrashStackEntryMapper *)crashStackEnt return self; } -- (SentryStacktrace *)buildStacktraceForCurrentThread +- (SentryStacktrace *)retrieveStacktraceFromCursor:(SentryCrashStackCursor)stackCursor { NSMutableArray *frames = [NSMutableArray new]; - - SentryCrashStackCursor stackCursor; - // We don't need to skip any frames, because we filter out non sentry frames below. - NSInteger framesToSkip = 0; - sentrycrashsc_initSelfThread(&stackCursor, (int)framesToSkip); - SentryFrame *frame = nil; while (stackCursor.advanceCursor(&stackCursor)) { if (stackCursor.symbolicate(&stackCursor)) { @@ -61,6 +56,52 @@ - (SentryStacktrace *)buildStacktraceForCurrentThread return stacktrace; } +- (SentryStacktrace *)buildStackTraceFromStackEntries:(SentryCrashStackEntry *)entries + amount:(unsigned int)amount +{ + NSMutableArray *frames = [[NSMutableArray alloc] initWithCapacity:amount]; + SentryFrame *frame = nil; + for (int i = 0; i < amount; i++) { + SentryCrashStackEntry stackEntry = entries[i]; + if (stackEntry.address == SentryCrashSC_ASYNC_MARKER) { + if (frame != nil) { + frame.stackStart = @(YES); + } + // skip the marker frame + continue; + } + frame = [self.crashStackEntryMapper sentryCrashStackEntryToSentryFrame:stackEntry]; + [frames addObject:frame]; + } + + NSArray *framesCleared = [SentryFrameRemover removeNonSdkFrames:frames]; + + // The frames must be ordered from caller to callee, or oldest to youngest + NSArray *framesReversed = [[framesCleared reverseObjectEnumerator] allObjects]; + + return [[SentryStacktrace alloc] initWithFrames:framesReversed registers:@{}]; +} + +- (SentryStacktrace *)buildStacktraceForThread:(SentryCrashThread)thread + context:(struct SentryCrashMachineContext *)context +{ + sentrycrashmc_getContextForThread(thread, context, false); + SentryCrashStackCursor stackCursor; + sentrycrashsc_initWithMachineContext(&stackCursor, MAX_STACKTRACE_LENGTH, context); + + return [self retrieveStacktraceFromCursor:stackCursor]; +} + +- (SentryStacktrace *)buildStacktraceForCurrentThread +{ + SentryCrashStackCursor stackCursor; + // We don't need to skip any frames, because we filter out non sentry frames below. + NSInteger framesToSkip = 0; + sentrycrashsc_initSelfThread(&stackCursor, (int)framesToSkip); + + return [self retrieveStacktraceFromCursor:stackCursor]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryThreadInspector.m b/Sources/Sentry/SentryThreadInspector.m index e338b226415..e4cf1372b29 100644 --- a/Sources/Sentry/SentryThreadInspector.m +++ b/Sources/Sentry/SentryThreadInspector.m @@ -1,8 +1,12 @@ #import "SentryThreadInspector.h" +#import "SentryCrashStackCursor.h" +#include "SentryCrashStackCursor_MachineContext.h" +#include "SentryCrashSymbolicator.h" #import "SentryFrame.h" #import "SentryStacktrace.h" #import "SentryStacktraceBuilder.h" #import "SentryThread.h" +#include @interface SentryThreadInspector () @@ -12,6 +16,36 @@ @end +typedef struct { + SentryCrashThread thread; + SentryCrashStackEntry stackEntries[MAX_STACKTRACE_LENGTH]; + int stackLength; +} SentryThreadInfo; + +// We need a C function to retrieve information from the stack trace in order to avoid +// calling into not async-signal-safe code while there are suspended threads. +unsigned int +getStackEntriesFromThread(SentryCrashThread thread, struct SentryCrashMachineContext *context, + SentryCrashStackEntry *buffer, unsigned int maxEntries) +{ + sentrycrashmc_getContextForThread(thread, context, false); + SentryCrashStackCursor stackCursor; + sentrycrashsc_initWithMachineContext(&stackCursor, MAX_STACKTRACE_LENGTH, context); + + unsigned int entries = 0; + while (stackCursor.advanceCursor(&stackCursor)) { + if (entries == maxEntries) + break; + if (stackCursor.symbolicate(&stackCursor)) { + buffer[entries] = stackCursor.stackEntry; + entries++; + } + } + sentrycrash_async_backtrace_decref(stackCursor.async_caller); + + return entries; +} + @implementation SentryThreadInspector - (id)initWithStacktraceBuilder:(SentryStacktraceBuilder *)stacktraceBuilder @@ -29,8 +63,9 @@ - (id)initWithStacktraceBuilder:(SentryStacktraceBuilder *)stacktraceBuilder NSMutableArray *threads = [NSMutableArray new]; SentryCrashMC_NEW_CONTEXT(context); - [self.machineContextWrapper fillContextForCurrentThread:context]; + SentryCrashThread currentThread = sentrycrashthread_self(); + [self.machineContextWrapper fillContextForCurrentThread:context]; int threadCount = [self.machineContextWrapper getThreadCount:context]; for (int i = 0; i < threadCount; i++) { @@ -40,15 +75,88 @@ - (id)initWithStacktraceBuilder:(SentryStacktraceBuilder *)stacktraceBuilder sentryThread.name = [self getThreadName:thread]; sentryThread.crashed = @NO; - bool isCurrent = thread == sentrycrashthread_self(); + bool isCurrent = thread == currentThread; sentryThread.current = @(isCurrent); - // For now we can only retrieve the stack trace of the current thread. if (isCurrent) { sentryThread.stacktrace = [self.stacktraceBuilder buildStacktraceForCurrentThread]; } - [threads addObject:sentryThread]; + // We need to make sure the main thread is always the first thread in the result + if ([self.machineContextWrapper isMainThread:thread]) + [threads insertObject:sentryThread atIndex:0]; + else + [threads addObject:sentryThread]; + } + + return threads; +} + +/** + * We are not sharing code with 'getCurrentThreads' because both methods use different approaches. + * This method retrieves thread information from the suspend method + * while the other retrieves information from the machine context. + * Having both approaches in the same method can lead to inconsistency between the number of + * threads, and while there is suspended threads we can't call into obj-c, so the previous approach + * wont work for retrieving stacktrace information for every thread. + */ +- (NSArray *)getCurrentThreadsWithStackTrace +{ + NSMutableArray *threads = [NSMutableArray new]; + + @synchronized(self) { + SentryCrashMC_NEW_CONTEXT(context); + SentryCrashThread currentThread = sentrycrashthread_self(); + + thread_act_array_t suspendedThreads = NULL; + mach_msg_type_number_t numSuspendedThreads = 0; + + sentrycrashmc_suspendEnvironment(&suspendedThreads, &numSuspendedThreads); + // DANGER: Do not try to allocate memory in the heap or call Objective-C code in this + // section Doing so when the threads are suspended may lead to deadlocks or crashes. + + SentryThreadInfo threadsInfos[numSuspendedThreads]; + + for (int i = 0; i < numSuspendedThreads; i++) { + if (suspendedThreads[i] != currentThread) { + int numberOfEntries = getStackEntriesFromThread(suspendedThreads[i], context, + threadsInfos[i].stackEntries, MAX_STACKTRACE_LENGTH); + threadsInfos[i].stackLength = numberOfEntries; + } else { + // We can't use 'getStackEntriesFromThread' to retrieve stack frames from the + // current thread. We are using the stackTraceBuilder to retrieve this information + // later. + threadsInfos[i].stackLength = 0; + } + threadsInfos[i].thread = suspendedThreads[i]; + } + + sentrycrashmc_resumeEnvironment(suspendedThreads, numSuspendedThreads); + // DANGER END: You may call Objective-C code again or allocate memory. + + for (int i = 0; i < numSuspendedThreads; i++) { + SentryThread *sentryThread = [[SentryThread alloc] initWithThreadId:@(i)]; + + sentryThread.name = [self getThreadName:threadsInfos[i].thread]; + + sentryThread.crashed = @NO; + bool isCurrent = threadsInfos[i].thread == currentThread; + sentryThread.current = @(isCurrent); + + if (isCurrent) { + sentryThread.stacktrace = [self.stacktraceBuilder buildStacktraceForCurrentThread]; + } else { + sentryThread.stacktrace = [self.stacktraceBuilder + buildStackTraceFromStackEntries:threadsInfos[i].stackEntries + amount:threadsInfos[i].stackLength]; + } + + // We need to make sure the main thread is always the first thread in the result + if ([self.machineContextWrapper isMainThread:threadsInfos[i].thread]) + [threads insertObject:sentryThread atIndex:0]; + else + [threads addObject:sentryThread]; + } } return threads; diff --git a/Sources/Sentry/include/SentryANRTracker.h b/Sources/Sentry/include/SentryANRTracker.h index e43fa8b3def..2f2779cad23 100644 --- a/Sources/Sentry/include/SentryANRTracker.h +++ b/Sources/Sentry/include/SentryANRTracker.h @@ -5,8 +5,6 @@ NS_ASSUME_NONNULL_BEGIN -#if SENTRY_HAS_UIKIT - @protocol SentryANRTrackerDelegate; /** @@ -26,16 +24,18 @@ NS_ASSUME_NONNULL_BEGIN @interface SentryANRTracker : NSObject SENTRY_NO_INIT -- (instancetype)initWithDelegate:(id)delegate - timeoutIntervalMillis:(NSUInteger)timeoutIntervalMillis - currentDateProvider:(id)currentDateProvider - crashWrapper:(SentryCrashWrapper *)crashWrapper - dispatchQueueWrapper:(SentryDispatchQueueWrapper *)dispatchQueueWrapper - threadWrapper:(SentryThreadWrapper *)threadWrapper; +- (instancetype)initWithTimeoutInterval:(NSTimeInterval)timeoutInterval + currentDateProvider:(id)currentDateProvider + crashWrapper:(SentryCrashWrapper *)crashWrapper + dispatchQueueWrapper:(SentryDispatchQueueWrapper *)dispatchQueueWrapper + threadWrapper:(SentryThreadWrapper *)threadWrapper; + +- (void)addListener:(id)listener; -- (void)start; +- (void)removeListener:(id)listener; -- (void)stop; +// Function used for tests +- (void)clear; @end @@ -47,6 +47,4 @@ SENTRY_NO_INIT @end -#endif - NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryANRTrackingIntegration.h b/Sources/Sentry/include/SentryANRTrackingIntegration.h index 6d4cce6baa6..803963e1b6a 100644 --- a/Sources/Sentry/include/SentryANRTrackingIntegration.h +++ b/Sources/Sentry/include/SentryANRTrackingIntegration.h @@ -4,13 +4,9 @@ NS_ASSUME_NONNULL_BEGIN -#if SENTRY_HAS_UIKIT - @interface SentryANRTrackingIntegration : NSObject @end -#endif - NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryClient+Private.h b/Sources/Sentry/include/SentryClient+Private.h index aa65ee48a1d..c6a85a98b49 100644 --- a/Sources/Sentry/include/SentryClient+Private.h +++ b/Sources/Sentry/include/SentryClient+Private.h @@ -3,7 +3,7 @@ #import "SentryDiscardReason.h" #import -@class SentryEnvelopeItem, SentryId, SentryAttachment; +@class SentryEnvelopeItem, SentryId, SentryAttachment, SentryThreadInspector; NS_ASSUME_NONNULL_BEGIN @@ -19,6 +19,7 @@ NS_ASSUME_NONNULL_BEGIN SentryClient (Private) @property (nonatomic, weak) id attachmentProcessor; +@property (nonatomic, strong) SentryThreadInspector *threadInspector; - (SentryFileManager *)fileManager; diff --git a/Sources/Sentry/include/SentryCrashMachineContextWrapper.h b/Sources/Sentry/include/SentryCrashMachineContextWrapper.h index 96f8e5adf17..96138f9fb32 100644 --- a/Sources/Sentry/include/SentryCrashMachineContextWrapper.h +++ b/Sources/Sentry/include/SentryCrashMachineContextWrapper.h @@ -18,6 +18,8 @@ NS_ASSUME_NONNULL_BEGIN andBuffer:(char *const)buffer andBufLength:(int)bufLength; +- (BOOL)isMainThread:(SentryCrashThread)thread; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryCrashStackEntryMapper.h b/Sources/Sentry/include/SentryCrashStackEntryMapper.h index cac9b3a657b..4dec4fc3cc5 100644 --- a/Sources/Sentry/include/SentryCrashStackEntryMapper.h +++ b/Sources/Sentry/include/SentryCrashStackEntryMapper.h @@ -20,6 +20,13 @@ SENTRY_NO_INIT */ - (SentryFrame *)mapStackEntryWithCursor:(SentryCrashStackCursor)stackCursor; +/** + * Maps a SentryCrashStackEntry to SentryFrame. + * + * @param stackEntry A stack entry retrieved from a thread. + */ +- (SentryFrame *)sentryCrashStackEntryToSentryFrame:(SentryCrashStackEntry)stackEntry; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryDependencyContainer.h b/Sources/Sentry/include/SentryDependencyContainer.h index 4f063925263..ad5778eb7dd 100644 --- a/Sources/Sentry/include/SentryDependencyContainer.h +++ b/Sources/Sentry/include/SentryDependencyContainer.h @@ -2,8 +2,8 @@ #import "SentryRandom.h" #import -@class SentryAppStateManager, SentryCrashWrapper, SentryThreadWrapper, SentryDispatchQueueWrapper, - SentrySwizzleWrapper, SentryDebugImageProvider; +@class SentryAppStateManager, SentryCrashWrapper, SentryThreadWrapper, SentrySwizzleWrapper, + SentryDispatchQueueWrapper, SentryDebugImageProvider, SentryANRTracker; #if SENTRY_HAS_UIKIT @class SentryScreenshot, SentryUIApplication; @@ -28,12 +28,15 @@ SENTRY_NO_INIT @property (nonatomic, strong) SentrySwizzleWrapper *swizzleWrapper; @property (nonatomic, strong) SentryDispatchQueueWrapper *dispatchQueueWrapper; @property (nonatomic, strong) SentryDebugImageProvider *debugImageProvider; +@property (nonatomic, strong) SentryANRTracker *anrTracker; #if SENTRY_HAS_UIKIT @property (nonatomic, strong) SentryScreenshot *screenshot; @property (nonatomic, strong) SentryUIApplication *application; #endif +- (SentryANRTracker *)getANRTracker:(NSTimeInterval)timeout; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryOutOfMemoryTrackingIntegration.h b/Sources/Sentry/include/SentryOutOfMemoryTrackingIntegration.h index f84e60f6e32..8940f647909 100644 --- a/Sources/Sentry/include/SentryOutOfMemoryTrackingIntegration.h +++ b/Sources/Sentry/include/SentryOutOfMemoryTrackingIntegration.h @@ -1,9 +1,11 @@ +#import "SentryANRTracker.h" #import "SentryIntegrationProtocol.h" #import NS_ASSUME_NONNULL_BEGIN -@interface SentryOutOfMemoryTrackingIntegration : NSObject +@interface SentryOutOfMemoryTrackingIntegration + : NSObject @end diff --git a/Sources/Sentry/include/SentryStacktraceBuilder.h b/Sources/Sentry/include/SentryStacktraceBuilder.h index 706ea50596d..dadfdbff145 100644 --- a/Sources/Sentry/include/SentryStacktraceBuilder.h +++ b/Sources/Sentry/include/SentryStacktraceBuilder.h @@ -1,3 +1,6 @@ +#import "SentryCrashMachineContext.h" +#import "SentryCrashStackCursor.h" +#include "SentryCrashThread.h" #import "SentryDefines.h" #import @@ -20,6 +23,17 @@ SENTRY_NO_INIT */ - (SentryStacktrace *)buildStacktraceForCurrentThread; +/** + * Builds the stacktrace for given thread removing frames from the SentrySDK until frames from + * a different package are found. When including Sentry via the Swift Package Manager the package is + * the same as the application that includes Sentry. In this case the full stacktrace is returned + * without skipping frames. + */ +- (SentryStacktrace *)buildStacktraceForThread:(SentryCrashThread)thread + context:(struct SentryCrashMachineContext *)context; + +- (SentryStacktrace *)buildStackTraceFromStackEntries:(SentryCrashStackEntry *)entries + amount:(unsigned int)amount; @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryThreadInspector.h b/Sources/Sentry/include/SentryThreadInspector.h index 258421a907a..5a3af1e1e30 100644 --- a/Sources/Sentry/include/SentryThreadInspector.h +++ b/Sources/Sentry/include/SentryThreadInspector.h @@ -15,9 +15,18 @@ SENTRY_NO_INIT /** * Gets current threads with the stacktrace only for the current thread. Frames from the SentrySDK * are not included. For more details checkout SentryStacktraceBuilder. + * The first thread in the result is always the main thread. */ - (NSArray *)getCurrentThreads; +/** + * Gets current threads with stacktrace, + * this will pause every thread in order to be possible to retrieve this information. + * Frames from the SentrySDK are not included. For more details checkout SentryStacktraceBuilder. + * The first thread in the result is always the main thread. + */ +- (NSArray *)getCurrentThreadsWithStackTrace; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Deadlock.m b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Deadlock.m index a8f9f088ce6..0c234c7a742 100644 --- a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Deadlock.m +++ b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Deadlock.m @@ -110,7 +110,7 @@ - (void)handleDeadlock SentryCrashMC_NEW_CONTEXT(machineContext); sentrycrashmc_getContextForThread(g_mainQueueThread, machineContext, false); SentryCrashStackCursor stackCursor; - sentrycrashsc_initWithMachineContext(&stackCursor, 100, machineContext); + sentrycrashsc_initWithMachineContext(&stackCursor, MAX_STACKTRACE_LENGTH, machineContext); char eventID[37]; sentrycrashid_generate(eventID); diff --git a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_MachException.c b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_MachException.c index f78d2b0fded..a18a4b81b0f 100644 --- a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_MachException.c +++ b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_MachException.c @@ -309,7 +309,8 @@ handleExceptions(void *const userData) crashContext->offendingMachineContext = machineContext; sentrycrashsc_initCursor(&g_stackCursor, NULL, NULL); if (sentrycrashmc_getContextForThread(exceptionMessage.thread.name, machineContext, true)) { - sentrycrashsc_initWithMachineContext(&g_stackCursor, 100, machineContext); + sentrycrashsc_initWithMachineContext( + &g_stackCursor, MAX_STACKTRACE_LENGTH, machineContext); SentryCrashLOG_TRACE("Fault address 0x%x, instruction address 0x%x", sentrycrashcpu_faultAddress(machineContext), sentrycrashcpu_instructionAddress(machineContext)); diff --git a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c index 2867f47abc0..adb56a472e8 100644 --- a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c +++ b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c @@ -91,7 +91,7 @@ handleSignal(int sigNum, siginfo_t *signalInfo, void *userContext) SentryCrashLOG_DEBUG("Filling out context."); SentryCrashMC_NEW_CONTEXT(machineContext); sentrycrashmc_getContextForSignal(userContext, machineContext); - sentrycrashsc_initWithMachineContext(&g_stackCursor, 100, machineContext); + sentrycrashsc_initWithMachineContext(&g_stackCursor, MAX_STACKTRACE_LENGTH, machineContext); SentryCrash_MonitorContext *crashContext = &g_monitorContext; memset(crashContext, 0, sizeof(*crashContext)); diff --git a/Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor.h b/Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor.h index 2f5a237b4b4..3a05a64ad62 100644 --- a/Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor.h +++ b/Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor.h @@ -44,25 +44,28 @@ extern "C" { /** A special marker frame being yielded as `address` to denote a chained async stacktrace. */ #define SentryCrashSC_ASYNC_MARKER (UINTPTR_MAX - 1234) -typedef struct SentryCrashStackCursor { - struct { - /** Current address in the stack trace. */ - uintptr_t address; +typedef struct { + /** Current address in the stack trace. */ + uintptr_t address; + + /** The name (if any) of the binary image the current address falls + * inside. */ + const char *imageName; - /** The name (if any) of the binary image the current address falls - * inside. */ - const char *imageName; + /** The starting address of the binary image the current address falls + * inside. */ + uintptr_t imageAddress; - /** The starting address of the binary image the current address falls - * inside. */ - uintptr_t imageAddress; + /** The name (if any) of the closest symbol to the current address. */ + const char *symbolName; - /** The name (if any) of the closest symbol to the current address. */ - const char *symbolName; + /** The address of the closest symbol to the current address. */ + uintptr_t symbolAddress; +} SentryCrashStackEntry; + +typedef struct SentryCrashStackCursor { + SentryCrashStackEntry stackEntry; - /** The address of the closest symbol to the current address. */ - uintptr_t symbolAddress; - } stackEntry; struct { /** Current depth as we walk the stack (1-based). */ int currentDepth; diff --git a/Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor_MachineContext.h b/Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor_MachineContext.h index 8ebbce2ea44..abd597c55d7 100644 --- a/Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor_MachineContext.h +++ b/Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor_MachineContext.h @@ -31,6 +31,8 @@ extern "C" { #include "SentryCrashStackCursor.h" +#define MAX_STACKTRACE_LENGTH 100 + /** Initialize a stack cursor for a machine context. * * @param cursor The stack cursor to initialize. diff --git a/Tests/SentryTests/Integrations/ANR/SentryANRTrackerTests.swift b/Tests/SentryTests/Integrations/ANR/SentryANRTrackerTests.swift index 0e5faeb9755..f23098b0753 100644 --- a/Tests/SentryTests/Integrations/ANR/SentryANRTrackerTests.swift +++ b/Tests/SentryTests/Integrations/ANR/SentryANRTrackerTests.swift @@ -7,7 +7,7 @@ class SentryANRTrackerTests: XCTestCase, SentryANRTrackerDelegate { private var fixture: Fixture! private var anrDetectedExpectation: XCTestExpectation! private var anrStoppedExpectation: XCTestExpectation! - private let waitTimeout: TimeInterval = 0.05 + private let waitTimeout: TimeInterval = 0.1 private class Fixture { let timeoutInterval: TimeInterval = 5 @@ -30,17 +30,21 @@ class SentryANRTrackerTests: XCTestCase, SentryANRTrackerDelegate { fixture = Fixture() - sut = SentryANRTracker(delegate: self, - timeoutIntervalMillis: UInt(fixture.timeoutInterval) * 1_000, - currentDateProvider: fixture.currentDate, - crashWrapper: fixture.crashWrapper, - dispatchQueueWrapper: fixture.dispatchQueue, - threadWrapper: fixture.threadWrapper) + sut = SentryANRTracker( + timeoutInterval: fixture.timeoutInterval, + currentDateProvider: fixture.currentDate, + crashWrapper: fixture.crashWrapper, + dispatchQueueWrapper: fixture.dispatchQueue, + threadWrapper: fixture.threadWrapper) } override func tearDown() { super.tearDown() - sut.stop() + sut.clear() + } + + func start() { + sut.addListener(self) } func testContinousANR_OneReported() { @@ -48,11 +52,25 @@ class SentryANRTrackerTests: XCTestCase, SentryANRTrackerDelegate { self.advanceTime(bySeconds: self.fixture.timeoutInterval) return false } - sut.start() + start() wait(for: [anrDetectedExpectation, anrStoppedExpectation], timeout: waitTimeout) } + func testMultipleListeners() { + fixture.dispatchQueue.blockBeforeMainBlock = { + self.advanceTime(bySeconds: self.fixture.timeoutInterval) + return false + } + + let secondListener = SentryANRTrackerTestDelegate() + sut.addListener(secondListener) + + start() + + wait(for: [anrDetectedExpectation, anrStoppedExpectation, secondListener.anrStoppedExpectation, secondListener.anrDetectedExpectation], timeout: waitTimeout) + } + func testANRButAppInBackground_NoANR() { anrDetectedExpectation.isInverted = true fixture.crashWrapper.internalIsApplicationInForeground = false @@ -61,7 +79,7 @@ class SentryANRTrackerTests: XCTestCase, SentryANRTrackerDelegate { self.advanceTime(bySeconds: self.fixture.timeoutInterval) return false } - sut.start() + start() wait(for: [anrDetectedExpectation, anrStoppedExpectation], timeout: waitTimeout) } @@ -80,7 +98,7 @@ class SentryANRTrackerTests: XCTestCase, SentryANRTrackerDelegate { return false } - sut.start() + start() wait(for: [anrDetectedExpectation, anrStoppedExpectation], timeout: waitTimeout) } @@ -92,26 +110,46 @@ class SentryANRTrackerTests: XCTestCase, SentryANRTrackerDelegate { self.advanceTime(bySeconds: delta) return false } - sut.start() + start() wait(for: [anrDetectedExpectation, anrStoppedExpectation], timeout: waitTimeout) } - func testStop_StopsReportingANRs() { + func testRemoveListener_StopsReportingANRs() { anrDetectedExpectation.isInverted = true let mainBlockExpectation = expectation(description: "Main Block") + fixture.dispatchQueue.blockBeforeMainBlock = { - self.sut.stop() + self.sut.removeListener(self) mainBlockExpectation.fulfill() return true } - sut.start() + start() wait(for: [anrDetectedExpectation, anrStoppedExpectation, mainBlockExpectation], timeout: waitTimeout) } + func testClear_StopsReportingANRs() { + let secondListener = SentryANRTrackerTestDelegate() + secondListener.anrDetectedExpectation.isInverted = true + anrDetectedExpectation.isInverted = true + + let mainBlockExpectation = expectation(description: "Main Block") + + fixture.dispatchQueue.blockBeforeMainBlock = { + self.sut.clear() + mainBlockExpectation.fulfill() + return true + } + + sut.addListener(secondListener) + start() + + wait(for: [anrDetectedExpectation, anrStoppedExpectation, mainBlockExpectation, secondListener.anrStoppedExpectation, secondListener.anrDetectedExpectation], timeout: waitTimeout) + } + func anrDetected() { anrDetectedExpectation.fulfill() } @@ -124,4 +162,23 @@ class SentryANRTrackerTests: XCTestCase, SentryANRTrackerDelegate { fixture.currentDate.setDate(date: fixture.currentDate.date().addingTimeInterval(bySeconds)) } } + +class SentryANRTrackerTestDelegate: NSObject, SentryANRTrackerDelegate { + + let anrDetectedExpectation = XCTestExpectation(description: "Test Delegate ANR Detection") + let anrStoppedExpectation = XCTestExpectation(description: "Test Delegate ANR Stopped") + + override init() { + anrStoppedExpectation.isInverted = true + } + + func anrStopped() { + anrStoppedExpectation.fulfill() + } + + func anrDetected() { + anrDetectedExpectation.fulfill() + } +} + #endif diff --git a/Tests/SentryTests/Integrations/ANR/SentryANRTrackingIntegrationTests.swift b/Tests/SentryTests/Integrations/ANR/SentryANRTrackingIntegrationTests.swift index 643d5d5ae5a..eb85b7609ce 100644 --- a/Tests/SentryTests/Integrations/ANR/SentryANRTrackingIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/ANR/SentryANRTrackingIntegrationTests.swift @@ -1,65 +1,50 @@ import XCTest -#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) -class SentryANRTrackingIntegrationTests: XCTestCase { - - private static let dsn = TestConstants.dsnAsString(username: "SentryANRTrackingIntegrationTests") +class SentryANRTrackingIntegrationTests: SentrySDKIntegrationTestsBase { private class Fixture { let options: Options - let client: TestClient! - let crashWrapper: TestSentryCrashWrapper - let currentDate = TestCurrentDateProvider() - let fileManager: SentryFileManager + let currentDate = TestCurrentDateProvider() + init() { options = Options() - options.dsn = SentryANRTrackingIntegrationTests.dsn - - client = TestClient(options: options) - - crashWrapper = TestSentryCrashWrapper.sharedInstance() - SentryDependencyContainer.sharedInstance().crashWrapper = crashWrapper - - let hub = SentryHub(client: client, andScope: nil, andCrashWrapper: crashWrapper, andCurrentDateProvider: currentDate) - SentrySDK.setCurrentHub(hub) - - fileManager = try! SentryFileManager(options: options, andCurrentDateProvider: currentDate) + options.enableAppHangTracking = true + options.appHangTimeoutInterval = 4.5 } } private var fixture: Fixture! private var sut: SentryANRTrackingIntegration! + override var options: Options { + self.fixture.options + } + override func setUp() { super.setUp() - fixture = Fixture() - fixture.fileManager.store(TestData.appState) } override func tearDown() { - super.tearDown() sut.uninstall() - fixture.fileManager.deleteAllFolders() clearTestState() + super.tearDown() } func testWhenBeingTraced_TrackerNotInitialized() { givenInitializedTracker(isBeingTraced: true) - XCTAssertNil(Dynamic(sut).tracker.asAnyObject) } func testWhenNoDebuggerAttached_TrackerInitialized() { givenInitializedTracker() - XCTAssertNotNil(Dynamic(sut).tracker.asAnyObject) } - func test_OOMDisabled_RemovesEnabledIntegration() { + func test_enableAppHangsTracking_Disabled_RemovesEnabledIntegration() { let options = Options() - options.enableOutOfMemoryTracking = false + options.enableAppHangTracking = false sut = SentryANRTrackingIntegration() sut.install(with: options) @@ -68,38 +53,82 @@ class SentryANRTrackingIntegrationTests: XCTestCase { assertArrayEquals(expected: expexted, actual: Array(options.enabledIntegrations)) } - func testANRDetected_UpdatesAppStateToTrue() { - givenInitializedTracker() + func test_appHangsTimeoutInterval_Zero_RemovesEnabledIntegration() { + let options = Options() + options.enableAppHangTracking = true + options.appHangTimeoutInterval = 0 - Dynamic(sut).anrDetected() + sut = SentryANRTrackingIntegration() + sut.install(with: options) - guard let appState = fixture.fileManager.readAppState() else { - XCTFail("appState must not be nil") - return - } - - XCTAssertTrue(appState.isANROngoing) + let expexted = Options.defaultIntegrations().filter { !$0.contains("ANRTracking") } + assertArrayEquals(expected: expexted, actual: Array(options.enabledIntegrations)) } - func testANRStopped_UpdatesAppStateToFalse() { + func testANRDetected_EventCaptured() { givenInitializedTracker() + setUpThreadInspector() - Dynamic(sut).anrStopped() + Dynamic(sut).anrDetected() - guard let appState = fixture.fileManager.readAppState() else { - XCTFail("appState must not be nil") - return + assertEventWithScopeCaptured { event, _, _ in + XCTAssertNotNil(event) + guard let ex = event?.exceptions?.first else { + XCTFail("ANR Exception not found") + return + } + + XCTAssertEqual(ex.mechanism?.type, "AppHang") + XCTAssertEqual(ex.type, "App Hanging") + XCTAssertEqual(ex.value, "App hanging for at least 4500 ms.") + XCTAssertNotNil(ex.stacktrace) + XCTAssertEqual(ex.stacktrace?.frames.first?.function, "main") + XCTAssertTrue(event?.threads?[0].current?.boolValue ?? false) + + guard let threads = event?.threads else { + XCTFail("ANR Exception not found") + return + } + + // Sometimes during tests its possible to have one thread without frames + // We just need to make sure we retrieve frame information for at least one other thread than the main thread + let threadsWithFrames = threads.filter { + ($0.stacktrace?.frames.count ?? 0) >= 1 + }.count + + XCTAssertTrue(threadsWithFrames > 1, "Not enough threads with frames") } - XCTAssertFalse(appState.isANROngoing) } private func givenInitializedTracker(isBeingTraced: Bool = false) { - fixture.crashWrapper.internalIsBeingTraced = isBeingTraced + givenSdkWithHub() + self.crashWrapper.internalIsBeingTraced = isBeingTraced sut = SentryANRTrackingIntegration() - let options = Options() - Dynamic(sut).setTestConfigurationFilePath(nil) - sut.install(with: options) + sut.install(with: self.options) + } + + private func setUpThreadInspector() { + let threadInspector = TestThreadInspector.instance + + let frame1 = Sentry.Frame() + frame1.function = "Second_frame_function" + + let thread1 = Sentry.Thread(threadId: 0) + thread1.stacktrace = Stacktrace(frames: [frame1], registers: [:]) + thread1.current = true + + let frame2 = Sentry.Frame() + frame2.function = "main" + + let thread2 = Sentry.Thread(threadId: 1) + thread2.stacktrace = Stacktrace(frames: [frame2], registers: [:]) + thread2.current = false + + threadInspector.allThreads = [ + thread2, + thread1 + ] + + SentrySDK.currentHub().getClient()?.threadInspector = threadInspector } } - -#endif diff --git a/Tests/SentryTests/Integrations/OutOfMemory/SentryOutOfMemoryIntegrationTests.swift b/Tests/SentryTests/Integrations/OutOfMemory/SentryOutOfMemoryIntegrationTests.swift index adbce9511ed..6216e053f37 100644 --- a/Tests/SentryTests/Integrations/OutOfMemory/SentryOutOfMemoryIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/OutOfMemory/SentryOutOfMemoryIntegrationTests.swift @@ -2,6 +2,45 @@ import XCTest class SentryOutOfMemoryIntegrationTests: XCTestCase { + private class Fixture { + let options: Options + let client: TestClient! + let crashWrapper: TestSentryCrashWrapper + let currentDate = TestCurrentDateProvider() + let fileManager: SentryFileManager + + init() { + options = Options() + + client = TestClient(options: options) + + crashWrapper = TestSentryCrashWrapper.sharedInstance() + SentryDependencyContainer.sharedInstance().crashWrapper = crashWrapper + + let hub = SentryHub(client: client, andScope: nil, andCrashWrapper: crashWrapper, andCurrentDateProvider: currentDate) + SentrySDK.setCurrentHub(hub) + + fileManager = try! SentryFileManager(options: options, andCurrentDateProvider: currentDate) + } + } + + private var fixture: Fixture! + private var sut: SentryOutOfMemoryTrackingIntegration! + + override func setUp() { + super.setUp() + + fixture = Fixture() + fixture.fileManager.store(TestData.appState) + } + + override func tearDown() { + sut?.uninstall() + fixture.fileManager.deleteAllFolders() + clearTestState() + super.tearDown() + } + func testWhenUnitTests_TrackerNotInitialized() { let sut = SentryOutOfMemoryTrackingIntegration() sut.install(with: Options()) @@ -23,7 +62,35 @@ class SentryOutOfMemoryIntegrationTests: XCTestCase { XCTAssertEqual(path, ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"]) } +#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) + func testANRDetected_UpdatesAppStateToTrue() { + givenInitializedTracker() + + Dynamic(sut).anrDetected() + + guard let appState = fixture.fileManager.readAppState() else { + XCTFail("appState must not be nil") + return + } + + XCTAssertTrue(appState.isANROngoing) + } +#endif + + func testANRStopped_UpdatesAppStateToFalse() { + givenInitializedTracker() + + Dynamic(sut).anrStopped() + + guard let appState = fixture.fileManager.readAppState() else { + XCTFail("appState must not be nil") + return + } + XCTAssertFalse(appState.isANROngoing) + } + func test_OOMDisabled_RemovesEnabledIntegration() { + givenInitializedTracker() let options = Options() options.enableOutOfMemoryTracking = false @@ -33,4 +100,13 @@ class SentryOutOfMemoryIntegrationTests: XCTestCase { let expexted = Options.defaultIntegrations().filter { !$0.contains("OutOfMemory") } assertArrayEquals(expected: expexted, actual: Array(options.enabledIntegrations)) } + + private func givenInitializedTracker(isBeingTraced: Bool = false) { + fixture.crashWrapper.internalIsBeingTraced = isBeingTraced + sut = SentryOutOfMemoryTrackingIntegration() + let options = Options() + Dynamic(sut).setTestConfigurationFilePath(nil) + sut.install(with: options) + } + } diff --git a/Tests/SentryTests/SentryCrash/SentryThreadInspectorTests.swift b/Tests/SentryTests/SentryCrash/SentryThreadInspectorTests.swift index 8a92c09ccbb..16ca06dc5ac 100644 --- a/Tests/SentryTests/SentryCrash/SentryThreadInspectorTests.swift +++ b/Tests/SentryTests/SentryCrash/SentryThreadInspectorTests.swift @@ -1,17 +1,18 @@ -@testable import Sentry import XCTest class SentryThreadInspectorTests: XCTestCase { private class Fixture { var testMachineContextWrapper = TestMachineContextWrapper() + var stacktraceBuilder = TestSentryStacktraceBuilder(crashStackEntryMapper: SentryCrashStackEntryMapper(inAppLogic: SentryInAppLogic(inAppIncludes: [], inAppExcludes: []))) func getSut(testWithRealMachineConextWrapper: Bool = false) -> SentryThreadInspector { let machineContextWrapper = testWithRealMachineConextWrapper ? SentryCrashDefaultMachineContextWrapper() : testMachineContextWrapper as SentryCrashMachineContextWrapper + let stacktraceBuilder = testWithRealMachineConextWrapper ? SentryStacktraceBuilder(crashStackEntryMapper: SentryCrashStackEntryMapper(inAppLogic: SentryInAppLogic(inAppIncludes: [], inAppExcludes: []))) : self.stacktraceBuilder return SentryThreadInspector( - stacktraceBuilder: SentryStacktraceBuilder(crashStackEntryMapper: SentryCrashStackEntryMapper(inAppLogic: SentryInAppLogic(inAppIncludes: [], inAppExcludes: []))), + stacktraceBuilder: stacktraceBuilder, andMachineContextWrapper: machineContextWrapper ) } @@ -37,6 +38,51 @@ class SentryThreadInspectorTests: XCTestCase { XCTAssertTrue(30 < stacktrace?.frames.count ?? 0, "Not enough stacktrace frames.") } + @available(macOS 10.12, iOS 10.0, tvOS 10.0, *) + func testStacktraceHasFrames_forEveryThread_withStitchAsyncOn() { + SentrySDK.start { $0.stitchAsyncCode = true } + assertStackForEveryThread() + } + + @available(macOS 10.12, iOS 10.0, tvOS 10.0, *) + func testStacktraceHasFrames_forEveryThread() { + assertStackForEveryThread() + } + + @available(macOS 10.12, iOS 10.0, tvOS 10.0, *) + func assertStackForEveryThread() { + + let queue = DispatchQueue(label: "defaultphil", attributes: [.concurrent, .initiallyInactive]) + + let expect = expectation(description: "Read every thread") + expect.expectedFulfillmentCount = 10 + + let sut = self.fixture.getSut(testWithRealMachineConextWrapper: true) + for _ in 0..<10 { + + queue.async { + let actual = sut.getCurrentThreadsWithStackTrace() + + // Sometimes during tests its possible to have one thread without frames + // We just need to make sure we retrieve frame information for at least one other thread than the main thread + var threadsWithFrames = 0 + + for thr in actual { + if (thr.stacktrace?.frames.count ?? 0) >= 1 { + threadsWithFrames += 1 + } + } + + XCTAssertTrue(threadsWithFrames > 1, "Not enough threads with frames") + + expect.fulfill() + } + } + + queue.activate() + wait(for: [expect], timeout: 10) + } + func testOnlyCurrentThreadHasStacktrace() { let actual = fixture.getSut(testWithRealMachineConextWrapper: true).getCurrentThreads() XCTAssertEqual(true, actual[0].current) @@ -111,9 +157,36 @@ class SentryThreadInspectorTests: XCTestCase { let thread = actual[0] XCTAssertEqual(threadName, thread.name) } + + func testMainThreadAsFirstThread() { + fixture.testMachineContextWrapper.mockThreads = [ ThreadInfo(threadId: 2, name: "Second Thread"), ThreadInfo(threadId: 1, name: "main") ] + fixture.testMachineContextWrapper.mainThread = 1 + fixture.testMachineContextWrapper.threadCount = 2 + + let sut = fixture.getSut() + let threads = sut.getCurrentThreads() + + XCTAssertEqual(threads[0].name, "main") + XCTAssertEqual(threads[1].name, "Second Thread") + } +} + +private class TestSentryStacktraceBuilder: SentryStacktraceBuilder { + + var stackTraces = [SentryCrashThread: Stacktrace]() + override func buildStacktrace(forThread thread: SentryCrashThread, context: OpaquePointer) -> Stacktrace { + return stackTraces[thread] ?? Stacktrace(frames: [], registers: [:]) + } + +} + +private struct ThreadInfo { + var threadId: SentryCrashThread + var name: String } private class TestMachineContextWrapper: NSObject, SentryCrashMachineContextWrapper { + func fillContext(forCurrentThread context: OpaquePointer) { // Do nothing } @@ -123,13 +196,16 @@ private class TestMachineContextWrapper: NSObject, SentryCrashMachineContextWrap threadCount } + var mockThreads: [ThreadInfo]? func getThread(_ context: OpaquePointer, with index: Int32) -> SentryCrashThread { - 0 + mockThreads?[Int(index)].threadId ?? 0 } var threadName: String? = "" func getThreadName(_ thread: SentryCrashThread, andBuffer buffer: UnsafeMutablePointer, andBufLength bufLength: Int32) { - if threadName != nil { + if let mocks = mockThreads, let index = mocks.firstIndex(where: { $0.threadId == thread }) { + strcpy(buffer, mocks[index].name) + } else if threadName != nil { strcpy(buffer, threadName) } else { _ = Array(repeating: 0, count: Int(bufLength)).withUnsafeBufferPointer { bufferPointer in @@ -137,4 +213,9 @@ private class TestMachineContextWrapper: NSObject, SentryCrashMachineContextWrap } } } + + var mainThread: SentryCrashThread? + func isMainThread(_ thread: SentryCrashThread) -> Bool { + return thread == mainThread + } } diff --git a/Tests/SentryTests/SentryCrash/TestThreadInspector.swift b/Tests/SentryTests/SentryCrash/TestThreadInspector.swift index a796e5e3290..6dc623e7091 100644 --- a/Tests/SentryTests/SentryCrash/TestThreadInspector.swift +++ b/Tests/SentryTests/SentryCrash/TestThreadInspector.swift @@ -2,6 +2,8 @@ import Foundation class TestThreadInspector: SentryThreadInspector { + var allThreads: [Sentry.Thread]? + static var instance: TestThreadInspector { // We need something to pass to the super initializer, because the empty initializer has been marked unavailable. let inAppLogic = SentryInAppLogic(inAppIncludes: [], inAppExcludes: []) @@ -11,7 +13,11 @@ class TestThreadInspector: SentryThreadInspector { } override func getCurrentThreads() -> [Sentry.Thread] { - return [TestData.thread] + return allThreads ?? [TestData.thread] + } + + override func getCurrentThreadsWithStackTrace() -> [Sentry.Thread] { + return allThreads ?? [TestData.thread] } } diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index 5ddfe69e1a5..02b771b832b 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -495,6 +495,8 @@ - (void)testNSNull_SetsDefaultValue @"enableUIViewControllerTracking" : [NSNull null], @"attachScreenshot" : [NSNull null], #endif + @"enableAppHangTracking" : [NSNull null], + @"appHangTimeoutInterval" : [NSNull null], @"enableNetworkTracking" : [NSNull null], @"tracesSampleRate" : [NSNull null], @"tracesSampler" : [NSNull null], @@ -541,6 +543,8 @@ - (void)assertDefaultValues:(SentryOptions *)options XCTAssertFalse(options.attachScreenshot); XCTAssertEqual(3.0, options.idleTimeout); #endif + XCTAssertFalse(options.enableAppHangTracking); + XCTAssertEqual(options.appHangTimeoutInterval, 2); XCTAssertEqual(YES, options.enableNetworkTracking); XCTAssertNil(options.tracesSampleRate); XCTAssertNil(options.tracesSampler); @@ -692,6 +696,17 @@ - (void)testIdleTimeout #endif +- (void)testEnableAppHangTracking +{ + [self testBooleanField:@"enableAppHangTracking" defaultValue:NO]; +} + +- (void)testDefaultAppHangsTimeout +{ + SentryOptions *options = [self getValidOptions:@{}]; + XCTAssertEqual(2, options.appHangTimeoutInterval); +} + - (void)testEnableNetworkTracking { [self testBooleanField:@"enableNetworkTracking"]; diff --git a/Tests/SentryTests/SentrySDKIntegrationTestsBase.swift b/Tests/SentryTests/SentrySDKIntegrationTestsBase.swift new file mode 100644 index 00000000000..6b3ecb8497e --- /dev/null +++ b/Tests/SentryTests/SentrySDKIntegrationTestsBase.swift @@ -0,0 +1,92 @@ +import Foundation +import XCTest + +class SentrySDKIntegrationTestsBase: XCTestCase { + + var currentDate = TestCurrentDateProvider() + var crashWrapper: TestSentryCrashWrapper! + + var options: Options { + Options() + } + + override func setUp() { + super.setUp() + crashWrapper = TestSentryCrashWrapper.sharedInstance() + SentryDependencyContainer.sharedInstance().crashWrapper = crashWrapper + currentDate = TestCurrentDateProvider() + } + + override func tearDown() { + super.tearDown() + clearTestState() + } + + func givenSdkWithHub(_ options: Options? = nil) { + let client = TestClient(options: options ?? self.options)! + let hub = SentryHub(client: client, andScope: Scope(), andCrashWrapper: TestSentryCrashWrapper.sharedInstance(), andCurrentDateProvider: currentDate) + + SentrySDK.setCurrentHub(hub) + } + + func givenSdkWithHubButNoClient() { + SentrySDK.setCurrentHub(SentryHub(client: nil, andScope: nil)) + } + + func assertEventCaptured(_ callback: (Event?) -> Void) { + guard let client = SentrySDK.currentHub().getClient() as? TestClient else { + XCTFail("Hub Client is not a `TestClient`") + return + } + XCTAssertEqual(1, client.captureEventInvocations.count, "More than one `Event` captured.") + callback(client.captureEventInvocations.first) + } + + func assertEventWithScopeCaptured(_ callback: (Event?, Scope?, [SentryEnvelopeItem]?) -> Void) { + guard let client = SentrySDK.currentHub().getClient() as? TestClient else { + XCTFail("Hub Client is not a `TestClient`") + return + } + + XCTAssertEqual(1, client.captureEventWithScopeInvocations.count, "More than one `Event` captured.") + let capture = client.captureEventWithScopeInvocations.first + callback(capture?.event, capture?.scope, capture?.additionalEnvelopeItems) + } + + func lastErrorWithScopeCaptured(_ callback: (Error?, Scope?) -> Void) { + guard let client = SentrySDK.currentHub().getClient() as? TestClient else { + XCTFail("Hub Client is not a `TestClient`") + return + } + + XCTAssertEqual(1, client.captureErrorWithScopeInvocations.count, "More than one `Error` captured.") + let capture = client.captureErrorWithScopeInvocations.first + callback(capture?.error, capture?.scope) + } + + func assertExceptionWithScopeCaptured(_ callback: (NSException?, Scope?) -> Void) { + guard let client = SentrySDK.currentHub().getClient() as? TestClient else { + XCTFail("Hub Client is not a `TestClient`") + return + } + + XCTAssertEqual(1, client.captureExceptionWithScopeInvocations.count, "More than one `Exception` captured.") + let capture = client.captureExceptionWithScopeInvocations.first + callback(capture?.exception, capture?.scope) + } + + func assertMessageWithScopeCaptured(_ callback: (String?, Scope?) -> Void) { + guard let client = SentrySDK.currentHub().getClient() as? TestClient else { + XCTFail("Hub Client is not a `TestClient`") + return + } + + XCTAssertEqual(1, client.captureMessageWithScopeInvocations.count, "More than one `Exception` captured.") + let capture = client.captureMessageWithScopeInvocations.first + callback(capture?.message, capture?.scope) + } + + func advanceTime(bySeconds: TimeInterval) { + currentDate.setDate(date: currentDate.date().addingTimeInterval(bySeconds)) + } +} diff --git a/Tests/SentryTests/SentrySDKTests.swift b/Tests/SentryTests/SentrySDKTests.swift index e2d3cdb61d6..e52ce9cae99 100644 --- a/Tests/SentryTests/SentrySDKTests.swift +++ b/Tests/SentryTests/SentrySDKTests.swift @@ -340,38 +340,6 @@ class SentrySDKTests: XCTestCase { XCTAssert(span === newSpan) } - func testPerformanceOfConfigureScope() { - func buildCrumb(_ i: Int) -> Breadcrumb { - let crumb = Breadcrumb() - crumb.message = String(repeating: String(i), count: 100) - crumb.data = ["some": String(repeating: String(i), count: 1_000)] - crumb.category = String(i) - return crumb - } - - SentrySDK.start(options: ["dsn": SentrySDKTests.dsnAsString]) - - SentrySDK.configureScope { scope in - let user = User() - user.email = "someone@gmail.com" - scope.setUser(user) - } - - for i in 0...100 { - SentrySDK.configureScope { scope in - scope.add(buildCrumb(i)) - } - } - - self.measure { - for i in 0...10 { - SentrySDK.configureScope { scope in - scope.add(buildCrumb(i)) - } - } - } - } - func testInstallIntegrations() { let options = Options() options.dsn = "mine" @@ -396,47 +364,6 @@ class SentrySDKTests: XCTestCase { assertIntegrationsInstalled(integrations: []) } - @available(tvOS 13.0, *) - @available(OSX 10.15, *) - @available(iOS 13.0, *) - func testMemoryFootprintOfAddingBreadcrumbs() { - SentrySDK.start { options in - options.dsn = SentrySDKTests.dsnAsString - options.debug = true - options.diagnosticLevel = SentryLevel.debug - options.attachStacktrace = true - } - - self.measure(metrics: [XCTMemoryMetric()]) { - for i in 0...1_000 { - let crumb = TestData.crumb - crumb.message = "\(i)" - SentrySDK.addBreadcrumb(crumb: crumb) - } - } - } - - @available(tvOS 13.0, *) - @available(OSX 10.15, *) - @available(iOS 13.0, *) - func testMemoryFootprintOfTransactions() { - SentrySDK.start { options in - options.dsn = SentrySDKTests.dsnAsString - } - - self.measure(metrics: [XCTMemoryMetric()]) { - for _ in 0...1_000 { - let trans = SentrySDK.startTransaction(name: "no leak", operation: "") - - for _ in 0...10 { - let span = trans.startChild(operation: "ui.load") - span.finish() - } - trans.finish() - } - } - } - func testStartSession() { givenSdkWithHub() diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index 7bb05557750..1b7f1c26471 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -8,6 +8,7 @@ #import "NSURLProtocolSwizzle.h" #import "PrivateSentrySDKOnly.h" #import "SentryANRTracker.h" +#import "SentryANRTrackingIntegration.h" #import "SentryAppStartMeasurement.h" #import "SentryAppStartTracker.h" #import "SentryAppStartTrackingIntegration.h" @@ -162,7 +163,6 @@ #import "URLSessionTaskMock.h" #if SENTRY_HAS_UIKIT -# import "SentryANRTrackingIntegration.h" # import "SentryUIEventTracker.h" # import "SentryUIEventTrackingIntegration.h" #endif From 95d16371ea45f22fd9428b5ae97fc55df8c70203 Mon Sep 17 00:00:00 2001 From: Indragie Karunaratne Date: Wed, 29 Jun 2022 15:32:03 -0700 Subject: [PATCH 09/14] feat: Add main thread ID to profiling payload (#1918) Add a is_main_thread key to the thread metadata in the profiling payload that identifies the main thread of the application. --- CHANGELOG.md | 1 + Sources/Sentry/SentryProfiler.mm | 13 ++++++++++++- .../SentryTests/Profiling/SentryProfilerTests.mm | 16 ++++++++++++++++ Tests/SentryTests/SentryHubTests.swift | 5 +++-- 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 047a8d75234..73c343ef6a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- Add main thread ID to profiling payload (#1918) - Add App Hangs tracking (#1906) ### Fixes diff --git a/Sources/Sentry/SentryProfiler.mm b/Sources/Sentry/SentryProfiler.mm index b29b5aa41af..f37d992fe42 100644 --- a/Sources/Sentry/SentryProfiler.mm +++ b/Sources/Sentry/SentryProfiler.mm @@ -93,12 +93,19 @@ @implementation SentryProfiler { uint64_t _startTimestamp; std::shared_ptr _profiler; SentryDebugImageProvider *_debugImageProvider; + thread::TIDType _mainThreadID; } - (instancetype)init { + if (![NSThread isMainThread]) { + [SentryLog logWithMessage:@"SentryProfiler must be initialized on the main thread" + andLevel:kSentryLevelError]; + return nil; + } if (self = [super init]) { _debugImageProvider = [SentryDependencyContainer sharedInstance].debugImageProvider; + _mainThreadID = ThreadHandle::current()->tid(); } return self; } @@ -132,7 +139,8 @@ - (void)start __weak const auto weakSelf = self; _profiler = std::make_shared( - [weakSelf, threadMetadata, queueMetadata, samples](auto &backtrace) { + [weakSelf, threadMetadata, queueMetadata, samples, mainThreadID = _mainThreadID]( + auto &backtrace) { const auto strongSelf = weakSelf; if (strongSelf == nil) { return; @@ -149,6 +157,9 @@ - (void)start [NSString stringWithUTF8String:backtrace.threadMetadata.name.c_str()]; } metadata[@"priority"] = @(backtrace.threadMetadata.priority); + if (backtrace.threadMetadata.threadID == mainThreadID) { + metadata[@"is_main_thread"] = @YES; + } threadMetadata[threadID] = metadata; } if (queueAddress != nil && queueMetadata[queueAddress] == nil diff --git a/Tests/SentryTests/Profiling/SentryProfilerTests.mm b/Tests/SentryTests/Profiling/SentryProfilerTests.mm index 7b2f6daebd2..8fdbdf9c1d6 100644 --- a/Tests/SentryTests/Profiling/SentryProfilerTests.mm +++ b/Tests/SentryTests/Profiling/SentryProfilerTests.mm @@ -33,6 +33,22 @@ - (void)testParseFunctionNameWithBacktraceSymbolsInput @"-[SentryProfilerTests testParseFunctionNameWithBacktraceSymbolsInput]"); } +- (void)testProfilerCanBeInitializedOnMainThread +{ + XCTAssertNotNil([[SentryProfiler alloc] init]); +} + +- (void)testProfilerCannotBeInitializedOffMainThread +{ + const auto expectation = [self expectationWithDescription:@"background initializing profiler"]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{ + XCTAssertNil([[SentryProfiler alloc] init]); + [expectation fulfill]; + }); + [self waitForExpectationsWithTimeout:1.0 + handler:^(NSError *_Nullable error) { NSLog(@"%@", error); }]; +} + @end #endif diff --git a/Tests/SentryTests/SentryHubTests.swift b/Tests/SentryTests/SentryHubTests.swift index 77716dc3ebb..e4005aed6e3 100644 --- a/Tests/SentryTests/SentryHubTests.swift +++ b/Tests/SentryTests/SentryHubTests.swift @@ -811,11 +811,12 @@ class SentryHubTests: XCTestCase { XCTAssertEqual(firstImage["type"] as! String, "macho") let sampledProfile = profile["sampled_profile"] as! [String: Any] - let threadMetadata = sampledProfile["thread_metadata"] as! [String: Any] + let threadMetadata = sampledProfile["thread_metadata"] as! [String: [String: Any]] let queueMetadata = sampledProfile["queue_metadata"] as! [String: Any] XCTAssertFalse(threadMetadata.isEmpty) - XCTAssertGreaterThan((threadMetadata.first?.value as! [String: Any])["priority"] as! Int, 0) + XCTAssertGreaterThan(threadMetadata.first?.value["priority"] as! Int, 0) + XCTAssertFalse(threadMetadata.values.filter { $0["is_main_thread"] as? Bool == true }.isEmpty) XCTAssertFalse(queueMetadata.isEmpty) XCTAssertFalse(((queueMetadata.first?.value as! [String: Any])["label"] as! String).isEmpty) From b30fb64b9149cdd137bd1bb2cdc3b1feb2dcfc61 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 30 Jun 2022 08:31:49 -0300 Subject: [PATCH 10/14] fix: Flake app hang test (#1924) Fixed a flake test for app hangs. --- .../Integrations/ANR/SentryANRTrackerTests.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Tests/SentryTests/Integrations/ANR/SentryANRTrackerTests.swift b/Tests/SentryTests/Integrations/ANR/SentryANRTrackerTests.swift index f23098b0753..84c02a64e14 100644 --- a/Tests/SentryTests/Integrations/ANR/SentryANRTrackerTests.swift +++ b/Tests/SentryTests/Integrations/ANR/SentryANRTrackerTests.swift @@ -137,7 +137,10 @@ class SentryANRTrackerTests: XCTestCase, SentryANRTrackerDelegate { anrDetectedExpectation.isInverted = true let mainBlockExpectation = expectation(description: "Main Block") - + + //Having a second Listener may cause the tracker to execute more than once before the end of the test + mainBlockExpectation.assertForOverFulfill = false + fixture.dispatchQueue.blockBeforeMainBlock = { self.sut.clear() mainBlockExpectation.fulfill() @@ -146,8 +149,8 @@ class SentryANRTrackerTests: XCTestCase, SentryANRTrackerDelegate { sut.addListener(secondListener) start() - wait(for: [anrDetectedExpectation, anrStoppedExpectation, mainBlockExpectation, secondListener.anrStoppedExpectation, secondListener.anrDetectedExpectation], timeout: waitTimeout) + } func anrDetected() { From ed975db82050eb202e289e375f947c947ad0b55b Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Thu, 30 Jun 2022 13:38:49 +0200 Subject: [PATCH 11/14] ref: Use NO for BOOL in SentryANRTracker (#1925) --- Sources/Sentry/SentryANRTracker.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Sentry/SentryANRTracker.m b/Sources/Sentry/SentryANRTracker.m index 8cc327d132b..4bc662b77be 100644 --- a/Sources/Sentry/SentryANRTracker.m +++ b/Sources/Sentry/SentryANRTracker.m @@ -39,7 +39,7 @@ - (instancetype)initWithTimeoutInterval:(NSTimeInterval)timeoutInterval self.threadWrapper = threadWrapper; self.listeners = [NSMutableSet new]; threadLock = [[NSObject alloc] init]; - running = false; + running = NO; } return self; } From 9667ce818fedf26619a6be69756440a62ba60f04 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Fri, 1 Jul 2022 11:53:39 +0200 Subject: [PATCH 12/14] Detect prewarmed starts with env variable (#1927) Stop reporting pre-warmed app starts based on the ActivePrewarm env variable. Increase the SENTRY_APP_START_MAX_DURATION to 180. Co-authored-by: Dhiogo Brustolin --- CHANGELOG.md | 1 + Sources/Sentry/SentryAppStartTracker.m | 43 ++++++++++++++++--- Tests/SentryTests/ClearTestState.swift | 3 ++ .../SentryAppStartTrackerTests.swift | 34 +++++++++++++-- 4 files changed, 72 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73c343ef6a5..078f5266d3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Fixes - Remove WebKit optimization check (#1921) +- Detect prewarmed starts with env variable (#1927) ## 7.18.1 diff --git a/Sources/Sentry/SentryAppStartTracker.m b/Sources/Sentry/SentryAppStartTracker.m index badd19f9aaa..8c3b6469a3d 100644 --- a/Sources/Sentry/SentryAppStartTracker.m +++ b/Sources/Sentry/SentryAppStartTracker.m @@ -16,12 +16,13 @@ # import static NSDate *runtimeInit = nil; +static BOOL isActivePrewarm = NO; /** - * The watchdog usually kicks in after an app hanging 10 to 20 seconds. As the app could hang in + * The watchdog usually kicks in after an app hanging for 30 seconds. As the app could hang in * multiple stages during the launch we pick a higher threshold. */ -static const NSTimeInterval SENTRY_APP_START_MAX_DURATION = 60.0; +static const NSTimeInterval SENTRY_APP_START_MAX_DURATION = 180.0; @interface SentryAppStartTracker () @@ -42,6 +43,11 @@ + (void)load { // Invoked whenever this class is added to the Objective-C runtime. runtimeInit = [NSDate date]; + + // The OS sets this environment variable if the app start is pre warmed. There are no official + // docs for this. Found at https://eisel.me/startup. Investigations show that this variable is + // deleted after UIApplicationDidFinishLaunchingNotification, so we have to check it here. + isActivePrewarm = [[NSProcessInfo processInfo].environment[@"ActivePrewarm"] isEqual:@"1"]; } - (instancetype)initWithCurrentDateProvider:(id)currentDateProvider @@ -61,6 +67,21 @@ - (instancetype)initWithCurrentDateProvider:(id)curre return self; } +- (BOOL)isActivePrewarmAvailable +{ +# if TARGET_OS_IOS + // Customer data suggest that app starts are also prewarmed on iOS 14 although this contradicts + // with Apple docs. + if (@available(iOS 14, *)) { + return YES; + } else { + return NO; + } +# else + return NO; +# endif +} + - (void)start { // It can happen that the OS posts the didFinishLaunching notification before we register for it @@ -94,6 +115,15 @@ - (void)buildAppStartMeasurement void (^block)(void) = ^(void) { [self stop]; + // Don't (yet) report pre warmed app starts. + // Check if prewarm is available. Just to be safe to not drop app start data on earlier OS + // verions. + if ([self isActivePrewarmAvailable] && isActivePrewarm) { + [SentryLog logWithMessage:@"The app was prewarmed. Not measuring app start." + andLevel:kSentryLevelInfo]; + return; + } + SentryAppStartType appStartType = [self getStartType]; if (appStartType == SentryAppStartTypeUnknown) { @@ -113,9 +143,9 @@ - (void)buildAppStartMeasurement // According to a talk at WWDC about optimizing app launch // (https://devstreaming-cdn.apple.com/videos/wwdc/2019/423lzf3qsjedrzivc7/423/423_optimizing_app_launch.pdf?dl=1 // slide 17) no process exists for cold and warm launches. Since iOS 15, though, the system - // might decide to pre-warm your app before the user tries to open it. Therefore we use the - // process start timestamp only if it's not too long ago. The process start time returned - // valid values when testing with real devices before iOS 15. See: + // might decide to pre-warm your app before the user tries to open it. The process start + // time returned valid values when testing with real devices if the app start is not + // prewarmed. See: // https://developer.apple.com/documentation/uikit/app_and_environment/responding_to_the_launch_of_your_app/about_the_app_launch_sequence#3894431 // https://developer.apple.com/documentation/metrickit/mxapplaunchmetric, // https://twitter.com/steipete/status/1466013492180312068, @@ -124,11 +154,12 @@ - (void)buildAppStartMeasurement NSTimeInterval appStartDuration = [[self.currentDate date] timeIntervalSinceDate:self.sysctl.processStartTimestamp]; + // Safety check to not report app starts that are completely off. if (appStartDuration >= SENTRY_APP_START_MAX_DURATION) { NSString *message = [NSString stringWithFormat: @"The app start exceeded the max duration of %f seconds. Not measuring app " - @"start.\nThis could be because the OS prewarmed the app's process.", + @"start.", SENTRY_APP_START_MAX_DURATION]; [SentryLog logWithMessage:message andLevel:kSentryLevelInfo]; return; diff --git a/Tests/SentryTests/ClearTestState.swift b/Tests/SentryTests/ClearTestState.swift index 978f22dbcdb..02db84e1294 100644 --- a/Tests/SentryTests/ClearTestState.swift +++ b/Tests/SentryTests/ClearTestState.swift @@ -17,6 +17,9 @@ func clearTestState() { let framesTracker = SentryFramesTracker.sharedInstance() framesTracker.stop() framesTracker.resetFrames() + + setenv("ActivePrewarm", "0", 1) + SentryAppStartTracker.load() #endif SentryDependencyContainer.reset() diff --git a/Tests/SentryTests/Integrations/Performance/AppStartTracking/SentryAppStartTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/AppStartTracking/SentryAppStartTrackerTests.swift index b6a153ffe3c..20a471ac555 100644 --- a/Tests/SentryTests/Integrations/Performance/AppStartTracking/SentryAppStartTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/AppStartTracking/SentryAppStartTrackerTests.swift @@ -128,17 +128,45 @@ class SentryAppStartTrackerTests: XCTestCase { } func testAppLaunches_OSPrewarmedProcess_NoAppStartUp() { - let processStartTime = fixture.currentDate.date().addingTimeInterval(-60) + setenv("ActivePrewarm", "1", 1) + SentryAppStartTracker.load() + givenSystemNotRebooted() + + startApp() + + #if os(iOS) + if #available(iOS 14.0, *) { + assertNoAppStartUp() + } else { + assertValidStart(type: .warm) + } + #else + assertValidStart(type: .warm) + #endif + } + + func testAppLaunches_WrongEnvValue_AppStartUp() { + setenv("ActivePrewarm", "0", 1) + SentryAppStartTracker.load() + givenSystemNotRebooted() + + startApp() + + assertValidStart(type: .warm) + } + + func testAppLaunches_MaximumAppStartDuration_NoAppStart() { + let processStartTime = fixture.currentDate.date().addingTimeInterval(-180) startApp(processStartTimeStamp: processStartTime) assertNoAppStartUp() } func testAppLaunches_OSAlmostPrewarmedProcess_AppStartUp() { - let processStartTime = fixture.currentDate.date().addingTimeInterval(-59) + let processStartTime = fixture.currentDate.date().addingTimeInterval(-179) startApp(processStartTimeStamp: processStartTime) - assertValidStart(type: .cold, expectedDuration: 59.4) + assertValidStart(type: .cold, expectedDuration: 179.4) } func testAppLaunchesBackgroundTask_NoAppStartUp() { From 673aaa416b0eee6c439140a4a7bf791fb0565e0f Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Fri, 1 Jul 2022 07:00:56 -0300 Subject: [PATCH 13/14] Update SentryCrashDefaultMachineContextWrapper.m (#1928) We were using `+initialize` method from `SentryCrashDefaultMachineContextWrapper` to save the main thread id. `+initialize` is called with the first message sent to the class, and this can occur in any thread. '+load' is called when the library is load and this always occurs on the main thread, therefore a much safer place to save main thread id. --- Sources/Sentry/SentryCrashDefaultMachineContextWrapper.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Sentry/SentryCrashDefaultMachineContextWrapper.m b/Sources/Sentry/SentryCrashDefaultMachineContextWrapper.m index b2f04ccb676..3c0cb6e7183 100644 --- a/Sources/Sentry/SentryCrashDefaultMachineContextWrapper.m +++ b/Sources/Sentry/SentryCrashDefaultMachineContextWrapper.m @@ -20,7 +20,7 @@ @implementation SentryCrashDefaultMachineContextWrapper -+ (void)initialize ++ (void)load { mainThreadID = pthread_mach_thread_np(pthread_self()); } From d312243b14225623d04cda5fd9d6564d4110d36c Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Mon, 4 Jul 2022 09:06:47 +0000 Subject: [PATCH 14/14] release: 7.19.0 --- CHANGELOG.md | 2 +- Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj | 8 ++++---- Sentry.podspec | 2 +- Sources/Configuration/Sentry.xcconfig | 2 +- Sources/Sentry/SentryMeta.m | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 078f5266d3c..426183cfe7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 7.19.0 ### Features diff --git a/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj b/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj index db49b1acb75..87e81898b64 100644 --- a/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj +++ b/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj @@ -966,7 +966,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 7.18.1; + MARKETING_VERSION = 7.19.0; PRODUCT_BUNDLE_IDENTIFIER = "io.sentry.sample.iOS-Swift"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match Development io.sentry.sample.iOS-Swift"; @@ -995,7 +995,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 7.18.1; + MARKETING_VERSION = 7.19.0; PRODUCT_BUNDLE_IDENTIFIER = "io.sentry.sample.iOS-Swift"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore io.sentry.sample.iOS-Swift"; @@ -1145,7 +1145,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 7.18.1; + MARKETING_VERSION = 7.19.0; PRODUCT_BUNDLE_IDENTIFIER = "io.sentry.sample.iOS-Swift.Clip"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match Development io.sentry.sample.iOS-Swift.Clip"; @@ -1181,7 +1181,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 7.18.1; + MARKETING_VERSION = 7.19.0; PRODUCT_BUNDLE_IDENTIFIER = "io.sentry.sample.iOS-Swift.Clip"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore io.sentry.sample.iOS-Swift.Clip"; diff --git a/Sentry.podspec b/Sentry.podspec index 931e33389f1..6f1552a673e 100644 --- a/Sentry.podspec +++ b/Sentry.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "Sentry" - s.version = "7.18.1" + s.version = "7.19.0" s.summary = "Sentry client for cocoa" s.homepage = "https://github.com/getsentry/sentry-cocoa" s.license = "mit" diff --git a/Sources/Configuration/Sentry.xcconfig b/Sources/Configuration/Sentry.xcconfig index 2dbc03a5705..484de6789b8 100644 --- a/Sources/Configuration/Sentry.xcconfig +++ b/Sources/Configuration/Sentry.xcconfig @@ -29,7 +29,7 @@ MACH_O_TYPE = mh_dylib FRAMEWORK_VERSION = A PRODUCT_NAME = Sentry -CURRENT_PROJECT_VERSION = 7.18.1 +CURRENT_PROJECT_VERSION = 7.19.0 INFOPLIST_FILE = Sources/Sentry/Info.plist PRODUCT_BUNDLE_IDENTIFIER = io.sentry.Sentry ALWAYS_SEARCH_USER_PATHS = NO diff --git a/Sources/Sentry/SentryMeta.m b/Sources/Sentry/SentryMeta.m index 77d8f982d72..df75986a472 100644 --- a/Sources/Sentry/SentryMeta.m +++ b/Sources/Sentry/SentryMeta.m @@ -5,7 +5,7 @@ @implementation SentryMeta // Don't remove the static keyword. If you do the compiler adds the constant name to the global // symbol table and it might clash with other constants. When keeping the static keyword the // compiler replaces all occurrences with the value. -static NSString *const versionString = @"7.18.1"; +static NSString *const versionString = @"7.19.0"; static NSString *const sdkName = @"sentry.cocoa"; + (NSString *)versionString