From f1ed6f8149f6e293073a61191a2695ea9345c885 Mon Sep 17 00:00:00 2001
From: Matthew T <20070360+mdtro@users.noreply.github.com>
Date: Wed, 24 Jul 2024 13:01:46 -0500
Subject: [PATCH 01/63] Revert "ci: dependency review action (#4191)" (#4195)
This reverts commit bce565d784e7ee60f457e4f5b0131dd429c246d9.
---
.github/workflows/dependency-review.yml | 19 -------------------
1 file changed, 19 deletions(-)
delete mode 100644 .github/workflows/dependency-review.yml
diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml
deleted file mode 100644
index 24510de818e..00000000000
--- a/.github/workflows/dependency-review.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-name: 'Dependency Review'
-on:
- pull_request:
- branches: ['master']
-
-permissions:
- contents: read
-
-jobs:
- dependency-review:
- runs-on: ubuntu-latest
- steps:
- - name: 'Checkout Repository'
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- - name: Dependency Review
- uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4
- with:
- # Possible values: "critical", "high", "moderate", "low"
- fail-on-severity: high
From 84fdd2236a8f7a73fc178875158de52d02513420 Mon Sep 17 00:00:00 2001
From: Andrew McKnight
Date: Wed, 24 Jul 2024 10:32:56 -0800
Subject: [PATCH 02/63] test: add flag to allow skipping SDK start in sample
app (#4189)
helped to try reproducing a certain customer use case
---
.../xcshareddata/xcschemes/iOS-Swift.xcscheme | 4 ++++
Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 4 +++-
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme b/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme
index b4ea1e92fba..6a6bd7ebbf9 100644
--- a/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme
+++ b/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme
@@ -73,6 +73,10 @@
argument = "--disable-file-io-tracing"
isEnabled = "NO">
+
+
diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift
index a2d845bf219..703631ad8ec 100644
--- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift
+++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift
@@ -164,7 +164,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
if ProcessInfo.processInfo.arguments.contains("--io.sentry.wipe-data") {
removeAppData()
}
- AppDelegate.startSentry()
+ if !ProcessInfo.processInfo.arguments.contains("--skip-sentry-init") {
+ AppDelegate.startSentry()
+ }
randomDistributionTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in
let random = Double.random(in: 0..<1_000)
From 061c982c8616fa6ca7f0762998f39a8f4d8d2fc2 Mon Sep 17 00:00:00 2001
From: Andrew McKnight
Date: Wed, 24 Jul 2024 10:33:26 -0800
Subject: [PATCH 03/63] test: add env var override sample app session tracking
interval (#4193)
---
.../xcshareddata/xcschemes/iOS-Swift.xcscheme | 5 +++++
Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 4 +++-
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme b/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme
index 6a6bd7ebbf9..593e6f9b882 100644
--- a/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme
+++ b/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme
@@ -203,6 +203,11 @@
value = ""
isEnabled = "NO">
+
+
Date: Fri, 26 Jul 2024 13:08:27 +0200
Subject: [PATCH 04/63] ref: Remove double-checked lock for flush (#4198)
The SentryHttpTransport used a double-checked lock in flush, which isn't
required because it's doubtful that somebody calls flush in a tight loop
from multiple threads, and when they do, it's acceptable to block them
for a bit longer.
---
Sources/Sentry/SentryHttpTransport.m | 6 ------
Tests/SentryTests/Networking/SentryHttpTransportTests.swift | 4 ++--
2 files changed, 2 insertions(+), 8 deletions(-)
diff --git a/Sources/Sentry/SentryHttpTransport.m b/Sources/Sentry/SentryHttpTransport.m
index 612ec264ca0..7acc738b01d 100644
--- a/Sources/Sentry/SentryHttpTransport.m
+++ b/Sources/Sentry/SentryHttpTransport.m
@@ -184,12 +184,6 @@ - (SentryFlushResult)flush:(NSTimeInterval)timeout
dispatch_time_t delta = (int64_t)(timeout * (NSTimeInterval)NSEC_PER_SEC);
dispatch_time_t dispatchTimeout = dispatch_time(DISPATCH_TIME_NOW, delta);
- // Double-Checked Locking to avoid acquiring unnecessary locks.
- if (_isFlushing) {
- SENTRY_LOG_DEBUG(@"Already flushing.");
- return kSentryFlushResultAlreadyFlushing;
- }
-
@synchronized(self) {
if (_isFlushing) {
SENTRY_LOG_DEBUG(@"Already flushing.");
diff --git a/Tests/SentryTests/Networking/SentryHttpTransportTests.swift b/Tests/SentryTests/Networking/SentryHttpTransportTests.swift
index 86e9e254bc3..e3b21850464 100644
--- a/Tests/SentryTests/Networking/SentryHttpTransportTests.swift
+++ b/Tests/SentryTests/Networking/SentryHttpTransportTests.swift
@@ -906,14 +906,14 @@ class SentryHttpTransportTests: XCTestCase {
ensureFlushingGroup.waitWithTimeout()
// Now the transport should also have left the synchronized block, and the
- // double-checked lock, should return immediately.
+ // flush should return immediately.
let initiallyInactiveQueue = fixture.queue
for _ in 0..<2 {
allFlushCallsGroup.enter()
initiallyInactiveQueue.async {
for _ in 0..<10 {
- XCTAssertEqual(.alreadyFlushing, self.sut.flush(flushTimeout), "Double checked lock should have returned immediately")
+ XCTAssertEqual(.alreadyFlushing, self.sut.flush(flushTimeout), "Flush should have returned immediately")
}
allFlushCallsGroup.leave()
From b2fea10a55223488641de90397c42fefa4129518 Mon Sep 17 00:00:00 2001
From: Dhiogo Brustolin
Date: Fri, 26 Jul 2024 15:46:51 +0200
Subject: [PATCH 05/63] feat: Add reportAccessibilityIdentifier option (#4183)
Added an option to choose whether to report accessibilityIdentifier with the view hierarchy
---
CHANGELOG.md | 1 +
Sources/Sentry/Public/SentryOptions.h | 9 +++++++++
Sources/Sentry/SentryOptions.m | 4 ++++
Sources/Sentry/SentryViewHierarchy.m | 11 ++++++++++-
Sources/Sentry/SentryViewHierarchyIntegration.m | 2 ++
Sources/Sentry/include/SentryViewHierarchy.h | 5 +++++
.../SentryViewHierarchyIntegrationTests.swift | 17 +++++++++++++++++
Tests/SentryTests/SentryOptionsTest.m | 6 ++++++
.../SentryTests/SentryViewHierarchyTests.swift | 16 ++++++++++++++++
9 files changed, 70 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff84aa09227..4ea0d5ec4fc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
### Features
+- Add `reportAccessibilityIdentifier` option (#4183)
- Record dropped spans (#4172)
### Fixes
diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h
index d98da46d920..97032857c4b 100644
--- a/Sources/Sentry/Public/SentryOptions.h
+++ b/Sources/Sentry/Public/SentryOptions.h
@@ -278,6 +278,15 @@ NS_SWIFT_NAME(Options)
*/
@property (nonatomic, assign) BOOL attachViewHierarchy;
+/**
+ * @brief If enabled, view hierarchy attachment will contain view `accessibilityIdentifier`.
+ * Set it to @c NO if your project uses `accessibilityIdentifier` for PII.
+ * @warning This feature is not available in @c DebugWithoutUIKit and @c ReleaseWithoutUIKit
+ * configurations even when targeting iOS or tvOS platforms.
+ * @note Default value is @c YES.
+ */
+@property (nonatomic, assign) BOOL reportAccessibilityIdentifier;
+
/**
* When enabled, the SDK creates transactions for UI events like buttons clicks, switch toggles,
* and other ui elements that uses UIControl @c sendAction:to:forEvent:
diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m
index de2f29e288e..7b60e72013f 100644
--- a/Sources/Sentry/SentryOptions.m
+++ b/Sources/Sentry/SentryOptions.m
@@ -114,6 +114,7 @@ - (instancetype)init
self.enableUIViewControllerTracing = YES;
self.attachScreenshot = NO;
self.attachViewHierarchy = NO;
+ self.reportAccessibilityIdentifier = YES;
self.enableUserInteractionTracing = YES;
self.idleTimeout = SentryTracerDefaultTimeout;
self.enablePreWarmedAppStartTracing = NO;
@@ -416,6 +417,9 @@ - (BOOL)validateOptions:(NSDictionary *)options
[self setBool:options[@"attachViewHierarchy"]
block:^(BOOL value) { self->_attachViewHierarchy = value; }];
+ [self setBool:options[@"reportAccessibilityIdentifier"]
+ block:^(BOOL value) { self->_reportAccessibilityIdentifier = value; }];
+
[self setBool:options[@"enableUserInteractionTracing"]
block:^(BOOL value) { self->_enableUserInteractionTracing = value; }];
diff --git a/Sources/Sentry/SentryViewHierarchy.m b/Sources/Sentry/SentryViewHierarchy.m
index b984140267a..c7cc073d57e 100644
--- a/Sources/Sentry/SentryViewHierarchy.m
+++ b/Sources/Sentry/SentryViewHierarchy.m
@@ -29,6 +29,14 @@
@implementation SentryViewHierarchy
+- (instancetype)init
+{
+ if (self = [super init]) {
+ self.reportAccessibilityIdentifier = YES;
+ }
+ return self;
+}
+
- (BOOL)saveViewHierarchy:(NSString *)filePath
{
NSArray *windows = [SentryDependencyContainer.sharedInstance.application windows];
@@ -119,7 +127,8 @@ - (int)viewHierarchyFromView:(UIView *)view intoContext:(SentryCrashJSONEncodeCo
tryJson(sentrycrashjson_addStringElement(
context, "type", viewClassName, SentryCrashJSON_SIZE_AUTOMATIC));
- if (view.accessibilityIdentifier && view.accessibilityIdentifier.length != 0) {
+ if (self.reportAccessibilityIdentifier && view.accessibilityIdentifier
+ && view.accessibilityIdentifier.length != 0) {
tryJson(sentrycrashjson_addStringElement(context, "identifier",
view.accessibilityIdentifier.UTF8String, SentryCrashJSON_SIZE_AUTOMATIC));
}
diff --git a/Sources/Sentry/SentryViewHierarchyIntegration.m b/Sources/Sentry/SentryViewHierarchyIntegration.m
index 9f9bb5c0956..f9077507de2 100644
--- a/Sources/Sentry/SentryViewHierarchyIntegration.m
+++ b/Sources/Sentry/SentryViewHierarchyIntegration.m
@@ -40,6 +40,8 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options
sentrycrash_setSaveViewHierarchy(&saveViewHierarchy);
+ SentryDependencyContainer.sharedInstance.viewHierarchy.reportAccessibilityIdentifier
+ = options.reportAccessibilityIdentifier;
return YES;
}
diff --git a/Sources/Sentry/include/SentryViewHierarchy.h b/Sources/Sentry/include/SentryViewHierarchy.h
index 381a432d578..4a52a03500d 100644
--- a/Sources/Sentry/include/SentryViewHierarchy.h
+++ b/Sources/Sentry/include/SentryViewHierarchy.h
@@ -6,6 +6,11 @@ NS_ASSUME_NONNULL_BEGIN
@interface SentryViewHierarchy : NSObject
+/**
+ * Whether we should add `accessibilityIdentifier` to the view hierarchy.
+ */
+@property (nonatomic) BOOL reportAccessibilityIdentifier;
+
/**
Get the view hierarchy in a json format.
Always runs in the main thread.
diff --git a/Tests/SentryTests/Integrations/ViewHierarchy/SentryViewHierarchyIntegrationTests.swift b/Tests/SentryTests/Integrations/ViewHierarchy/SentryViewHierarchyIntegrationTests.swift
index 0c848712a8c..2fea19d2b2a 100644
--- a/Tests/SentryTests/Integrations/ViewHierarchy/SentryViewHierarchyIntegrationTests.swift
+++ b/Tests/SentryTests/Integrations/ViewHierarchy/SentryViewHierarchyIntegrationTests.swift
@@ -146,6 +146,23 @@ class SentryViewHierarchyIntegrationTests: XCTestCase {
wait(for: [ex], timeout: 1)
}
+
+ func testReportAccessibilityIdentifierTrue() {
+ SentrySDK.start {
+ $0.attachViewHierarchy = true
+ $0.setIntegrations([SentryViewHierarchyIntegration.self])
+ }
+ XCTAssertTrue(SentryDependencyContainer.sharedInstance().viewHierarchy.reportAccessibilityIdentifier)
+ }
+
+ func testReportAccessibilityIdentifierFalse() {
+ SentrySDK.start {
+ $0.attachViewHierarchy = true
+ $0.reportAccessibilityIdentifier = false
+ $0.setIntegrations([SentryViewHierarchyIntegration.self])
+ }
+ XCTAssertFalse(SentryDependencyContainer.sharedInstance().viewHierarchy.reportAccessibilityIdentifier)
+ }
}
#endif // os(iOS) || os(tvOS) || targetEnvironment(macCatalyst)
diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m
index f86dab500c3..6b5fa7e58f5 100644
--- a/Tests/SentryTests/SentryOptionsTest.m
+++ b/Tests/SentryTests/SentryOptionsTest.m
@@ -656,6 +656,7 @@ - (void)assertDefaultValues:(SentryOptions *)options
XCTAssertEqual(options.enableUserInteractionTracing, YES);
XCTAssertEqual(options.enablePreWarmedAppStartTracing, NO);
XCTAssertEqual(options.attachViewHierarchy, NO);
+ XCTAssertEqual(options.reportAccessibilityIdentifier, YES);
XCTAssertEqual(options.experimental.sessionReplay.errorSampleRate, 0);
XCTAssertEqual(options.experimental.sessionReplay.sessionSampleRate, 0);
#endif // SENTRY_HAS_UIKIT
@@ -808,6 +809,11 @@ - (void)testAttachScreenshot
[self testBooleanField:@"attachScreenshot" defaultValue:NO];
}
+- (void)testReportAccessibilityIdentifier
+{
+ [self testBooleanField:@"reportAccessibilityIdentifier" defaultValue:YES];
+}
+
- (void)testEnableUserInteractionTracing
{
[self testBooleanField:@"enableUserInteractionTracing" defaultValue:YES];
diff --git a/Tests/SentryTests/SentryViewHierarchyTests.swift b/Tests/SentryTests/SentryViewHierarchyTests.swift
index 5d2a7084d26..e4e727df96f 100644
--- a/Tests/SentryTests/SentryViewHierarchyTests.swift
+++ b/Tests/SentryTests/SentryViewHierarchyTests.swift
@@ -148,6 +148,22 @@ class SentryViewHierarchyTests: XCTestCase {
XCTAssertEqual(descriptions, "{\"rendering_system\":\"UIKIT\",\"windows\":[{\"type\":\"UIWindow\",\"identifier\":\"WindowId\",\"width\":10,\"height\":10,\"x\":0,\"y\":0,\"alpha\":1,\"visible\":false,\"children\":[]}]}")
}
+
+ func test_ViewHierarchy_save_noIdentifier() throws {
+ let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 10, height: 10))
+ window.accessibilityIdentifier = "WindowId"
+
+ fixture.uiApplication.windows = [window]
+
+ let path = FileManager.default.temporaryDirectory.appendingPathComponent("view.json").path
+ let sut = self.fixture.sut
+ sut.reportAccessibilityIdentifier = false
+ sut.save(path)
+
+ let descriptions = try XCTUnwrap(String(contentsOfFile: path))
+
+ XCTAssertEqual(descriptions, "{\"rendering_system\":\"UIKIT\",\"windows\":[{\"type\":\"UIWindow\",\"width\":10,\"height\":10,\"x\":0,\"y\":0,\"alpha\":1,\"visible\":false,\"children\":[]}]}")
+ }
func test_invalidFilePath() {
let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 10, height: 10))
From fb2bfe856e56e7447a876f7e8207a378377eddd6 Mon Sep 17 00:00:00 2001
From: Dhiogo Brustolin
Date: Mon, 29 Jul 2024 09:27:40 +0200
Subject: [PATCH 06/63] feat: Replay for crashes (#4171)
Send the replay for the final moments before a crash.
---
CHANGELOG.md | 6 +
.../xcshareddata/xcschemes/iOS-Swift.xcscheme | 4 +-
Sentry.xcodeproj/project.pbxproj | 8 +
SentryTestUtils/TestHub.swift | 7 +
Sources/Sentry/SentryClient.m | 11 +-
Sources/Sentry/SentryGlobalEventProcessor.m | 11 ++
Sources/Sentry/SentryHttpTransport.m | 2 +-
Sources/Sentry/SentryHub.m | 3 +-
Sources/Sentry/SentryNetworkTracker.m | 3 +-
Sources/Sentry/SentryOptions.m | 7 +-
Sources/Sentry/SentrySerialization.m | 6 +-
.../Sentry/SentrySessionReplayIntegration.m | 158 +++++++++++++++++-
Sources/Sentry/SentrySessionReplaySyncC.c | 97 +++++++++++
Sources/Sentry/SentryTraceContext.m | 3 +-
.../include/SentryGlobalEventProcessor.h | 2 +
Sources/Sentry/include/SentrySerialization.h | 2 +-
.../include/SentrySessionReplayIntegration.h | 2 +-
.../Sentry/include/SentrySessionReplaySyncC.h | 19 +++
Sources/Sentry/include/SentryTraceContext.h | 3 +-
Sources/SentryCrash/Recording/SentryCrashC.c | 2 +
.../SessionReplay/SentryOnDemandReplay.swift | 53 +++++-
.../SessionReplay/SentryReplayRecording.swift | 10 +-
.../SessionReplay/SentrySessionReplay.swift | 30 ++--
.../Network/SentryNetworkTrackerTests.swift | 4 +-
.../SentryOnDemandReplayTests.swift | 2 +-
.../SentrySessionReplayIntegrationTests.swift | 116 ++++++++++++-
.../SentrySessionReplayTests.swift | 4 +-
.../PrivateSentrySDKOnlyTests.swift | 3 +-
Tests/SentryTests/SentryClientTests.swift | 12 +-
Tests/SentryTests/SentryOptionsTest.m | 8 +-
.../SentryTests/SentryTests-Bridging-Header.h | 1 +
.../Transaction/SentryTraceStateTests.swift | 5 +-
32 files changed, 541 insertions(+), 63 deletions(-)
create mode 100644 Sources/Sentry/SentrySessionReplaySyncC.c
create mode 100644 Sources/Sentry/include/SentrySessionReplaySyncC.h
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4ea0d5ec4fc..8fa8238c585 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
# Changelog
+## Unreleased
+
+### Features
+
+- Replay for crashes (#4171)
+
## 8.32.0
### Features
diff --git a/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme b/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme
index 593e6f9b882..0173095e27f 100644
--- a/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme
+++ b/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme
@@ -50,8 +50,8 @@
Void)?
+ public var capturedReplayRecordingVideo = Invocations<(replay: SentryReplayEvent, recording: SentryReplayRecording, video: URL)>()
+ public override func capture(_ replayEvent: SentryReplayEvent, replayRecording: SentryReplayRecording, video videoURL: URL) {
+ capturedReplayRecordingVideo.record((replayEvent, replayRecording, videoURL))
+ onReplayCapture?()
+ }
}
diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m
index 4e6c60daf4a..03db338fe23 100644
--- a/Sources/Sentry/SentryClient.m
+++ b/Sources/Sentry/SentryClient.m
@@ -11,7 +11,7 @@
#import "SentryDsn.h"
#import "SentryEnvelope+Private.h"
#import "SentryEnvelopeItemType.h"
-#import "SentryEvent.h"
+#import "SentryEvent+Private.h"
#import "SentryException.h"
#import "SentryExtraContextProvider.h"
#import "SentryFileManager.h"
@@ -403,7 +403,8 @@ - (nullable SentryTraceContext *)getTraceStateWithEvent:(SentryEvent *)event
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
return [[SentryTraceContext alloc] initWithTraceId:scope.propagationContext.traceId
options:self.options
- userSegment:scope.userObject.segment];
+ userSegment:scope.userObject.segment
+ replayId:scope.replayId];
#pragma clang diagnostic pop
}
@@ -468,6 +469,12 @@ - (SentryId *)sendEvent:(SentryEvent *)event
}
}
+ if (event.isCrashEvent && event.context[@"replay"] &&
+ [event.context[@"replay"] isKindOfClass:NSDictionary.class]) {
+ NSDictionary *replay = event.context[@"replay"];
+ scope.replayId = replay[@"replay_id"];
+ }
+
SentryTraceContext *traceContext = [self getTraceStateWithEvent:event withScope:scope];
if (nil == session.releaseName || [session.releaseName length] == 0) {
diff --git a/Sources/Sentry/SentryGlobalEventProcessor.m b/Sources/Sentry/SentryGlobalEventProcessor.m
index a41a4995e9b..7963173630f 100644
--- a/Sources/Sentry/SentryGlobalEventProcessor.m
+++ b/Sources/Sentry/SentryGlobalEventProcessor.m
@@ -32,4 +32,15 @@ - (void)removeAllProcessors
[self.processors removeAllObjects];
}
+- (nullable SentryEvent *)reportAll:(SentryEvent *)event
+{
+ for (SentryEventProcessor proc in self.processors) {
+ event = proc(event);
+ if (event == nil) {
+ return nil;
+ }
+ }
+ return event;
+}
+
@end
diff --git a/Sources/Sentry/SentryHttpTransport.m b/Sources/Sentry/SentryHttpTransport.m
index 7acc738b01d..3aa91226dfb 100644
--- a/Sources/Sentry/SentryHttpTransport.m
+++ b/Sources/Sentry/SentryHttpTransport.m
@@ -402,7 +402,7 @@ - (void)recordLostSpans:(SentryEnvelopeItem *)envelopeItem reason:(SentryDiscard
{
if ([SentryEnvelopeItemTypeTransaction isEqualToString:envelopeItem.header.type]) {
NSDictionary *transactionJson =
- [SentrySerialization deserializeEventEnvelopeItem:envelopeItem.data];
+ [SentrySerialization deserializeDictionaryFromJsonData:envelopeItem.data];
if (transactionJson == nil) {
return;
}
diff --git a/Sources/Sentry/SentryHub.m b/Sources/Sentry/SentryHub.m
index e7a3c14baac..7c317152f81 100644
--- a/Sources/Sentry/SentryHub.m
+++ b/Sources/Sentry/SentryHub.m
@@ -705,7 +705,8 @@ - (BOOL)envelopeContainsEventWithErrorOrHigher:(NSArray *)
for (SentryEnvelopeItem *item in items) {
if ([item.header.type isEqualToString:SentryEnvelopeItemTypeEvent]) {
// If there is no level the default is error
- NSDictionary *eventJson = [SentrySerialization deserializeEventEnvelopeItem:item.data];
+ NSDictionary *eventJson =
+ [SentrySerialization deserializeDictionaryFromJsonData:item.data];
if (eventJson == nil) {
return NO;
}
diff --git a/Sources/Sentry/SentryNetworkTracker.m b/Sources/Sentry/SentryNetworkTracker.m
index a781d83958e..8e1e823b22d 100644
--- a/Sources/Sentry/SentryNetworkTracker.m
+++ b/Sources/Sentry/SentryNetworkTracker.m
@@ -239,7 +239,8 @@ - (void)addTraceWithoutTransactionToTask:(NSURLSessionTask *)sessionTask
SentryTraceContext *traceContext =
[[SentryTraceContext alloc] initWithTraceId:propagationContext.traceId
options:SentrySDK.currentHub.client.options
- userSegment:SentrySDK.currentHub.scope.userObject.segment];
+ userSegment:SentrySDK.currentHub.scope.userObject.segment
+ replayId:SentrySDK.currentHub.scope.replayId];
#pragma clang diagnostic pop
[self addBaggageHeader:[traceContext toBaggage]
diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m
index 7b60e72013f..84c09747e71 100644
--- a/Sources/Sentry/SentryOptions.m
+++ b/Sources/Sentry/SentryOptions.m
@@ -44,8 +44,12 @@ @implementation SentryOptions {
{
// The order of integrations here is important.
// SentryCrashIntegration needs to be initialized before SentryAutoSessionTrackingIntegration.
+ // And SentrySessionReplayIntegration before SentryCrashIntegration.
NSMutableArray *defaultIntegrations =
@[
+#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION
+ NSStringFromClass([SentrySessionReplayIntegration class]),
+#endif
NSStringFromClass([SentryCrashIntegration class]),
#if SENTRY_HAS_UIKIT
NSStringFromClass([SentryAppStartTrackingIntegration class]),
@@ -55,9 +59,6 @@ @implementation SentryOptions {
NSStringFromClass([SentryUIEventTrackingIntegration class]),
NSStringFromClass([SentryViewHierarchyIntegration class]),
NSStringFromClass([SentryWatchdogTerminationTrackingIntegration class]),
-# if !TARGET_OS_VISION
- NSStringFromClass([SentrySessionReplayIntegration class]),
-# endif
#endif // SENTRY_HAS_UIKIT
NSStringFromClass([SentryANRTrackingIntegration class]),
NSStringFromClass([SentryAutoBreadcrumbTrackingIntegration class]),
diff --git a/Sources/Sentry/SentrySerialization.m b/Sources/Sentry/SentrySerialization.m
index 418f101dbac..49198cec6ba 100644
--- a/Sources/Sentry/SentrySerialization.m
+++ b/Sources/Sentry/SentrySerialization.m
@@ -297,16 +297,16 @@ + (SentryAppState *_Nullable)appStateWithData:(NSData *)data
return [[SentryAppState alloc] initWithJSONObject:appSateDictionary];
}
-+ (NSDictionary *)deserializeEventEnvelopeItem:(NSData *)eventEnvelopeItemData
++ (NSDictionary *)deserializeDictionaryFromJsonData:(NSData *)data
{
NSError *error = nil;
- NSDictionary *eventDictionary = [NSJSONSerialization JSONObjectWithData:eventEnvelopeItemData
+ NSDictionary *eventDictionary = [NSJSONSerialization JSONObjectWithData:data
options:0
error:&error];
if (nil != error) {
[SentryLog
logWithMessage:[NSString
- stringWithFormat:@"Failed to deserialize envelope item data: %@",
+ stringWithFormat:@"Failed to deserialize json item dictionary: %@",
error]
andLevel:kSentryLevelError];
}
diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m
index 85e9c87f852..31034eab69e 100644
--- a/Sources/Sentry/SentrySessionReplayIntegration.m
+++ b/Sources/Sentry/SentrySessionReplayIntegration.m
@@ -5,14 +5,18 @@
# import "SentryClient+Private.h"
# import "SentryDependencyContainer.h"
# import "SentryDisplayLinkWrapper.h"
+# import "SentryEvent+Private.h"
# import "SentryFileManager.h"
# import "SentryGlobalEventProcessor.h"
# import "SentryHub+Private.h"
+# import "SentryLog.h"
# import "SentryNSNotificationCenterWrapper.h"
# import "SentryOptions.h"
# import "SentryRandom.h"
# import "SentrySDK+Private.h"
# import "SentryScope+Private.h"
+# import "SentrySerialization.h"
+# import "SentrySessionReplaySyncC.h"
# import "SentrySwift.h"
# import "SentrySwizzle.h"
# import "SentryUIApplication.h"
@@ -38,6 +42,7 @@ @implementation SentrySessionReplayIntegration {
BOOL _startedAsFullSession;
SentryReplayOptions *_replayOptions;
SentryNSNotificationCenterWrapper *_notificationCenter;
+ SentryOnDemandReplay *_resumeReplayMaker;
}
- (BOOL)installWithOptions:(nonnull SentryOptions *)options
@@ -61,14 +66,119 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options
[SentryGlobalEventProcessor.shared
addEventProcessor:^SentryEvent *_Nullable(SentryEvent *_Nonnull event) {
- [self.sessionReplay captureReplayForEvent:event];
-
+ if (event.isCrashEvent) {
+ [self resumePreviousSessionReplay:event];
+ } else {
+ [self.sessionReplay captureReplayForEvent:event];
+ }
return event;
}];
return YES;
}
+- (void)resumePreviousSessionReplay:(SentryEvent *)event
+{
+ NSURL *dir = [self replayDirectory];
+ NSData *lastReplay =
+ [NSData dataWithContentsOfURL:[dir URLByAppendingPathComponent:@"lastreplay"]];
+ if (lastReplay == nil) {
+ return;
+ }
+
+ NSDictionary *jsonObject =
+ [SentrySerialization deserializeDictionaryFromJsonData:lastReplay];
+ if (jsonObject == nil) {
+ return;
+ }
+
+ SentryId *replayId = jsonObject[@"replayId"]
+ ? [[SentryId alloc] initWithUUIDString:jsonObject[@"replayId"]]
+ : [[SentryId alloc] init];
+ NSURL *lastReplayURL = [dir URLByAppendingPathComponent:jsonObject[@"path"]];
+
+ SentryCrashReplay crashInfo = { 0 };
+ bool hasCrashInfo = sentrySessionReplaySync_readInfo(&crashInfo,
+ [[lastReplayURL URLByAppendingPathComponent:@"crashInfo"].path
+ cStringUsingEncoding:NSUTF8StringEncoding]);
+
+ SentryReplayType type = hasCrashInfo ? SentryReplayTypeSession : SentryReplayTypeBuffer;
+ NSTimeInterval duration
+ = hasCrashInfo ? _replayOptions.sessionSegmentDuration : _replayOptions.errorReplayDuration;
+ int segmentId = hasCrashInfo ? crashInfo.segmentId + 1 : 0;
+
+ if (type == SentryReplayTypeBuffer) {
+ float errorSampleRate = [jsonObject[@"errorSampleRate"] floatValue];
+ if ([SentryDependencyContainer.sharedInstance.random nextNumber] >= errorSampleRate) {
+ return;
+ }
+ }
+
+ _resumeReplayMaker = [[SentryOnDemandReplay alloc] initWithContentFrom:lastReplayURL.path];
+ _resumeReplayMaker.bitRate = _replayOptions.replayBitRate;
+ _resumeReplayMaker.videoScale = _replayOptions.sizeScale;
+
+ NSDate *beginning = hasCrashInfo
+ ? [NSDate dateWithTimeIntervalSinceReferenceDate:crashInfo.lastSegmentEnd]
+ : [_resumeReplayMaker oldestFrameDate];
+
+ if (beginning == nil) {
+ return; // no frames to send
+ }
+
+ NSError *error;
+ if (![_resumeReplayMaker
+ createVideoWithBeginning:beginning
+ end:[beginning dateByAddingTimeInterval:duration]
+ outputFileURL:[lastReplayURL URLByAppendingPathComponent:@"lastVideo.mp4"]
+ error:&error
+ completion:^(SentryVideoInfo *video, NSError *renderError) {
+ if (renderError != nil) {
+ SENTRY_LOG_ERROR(
+ @"Could not create replay video: %@", renderError);
+ } else {
+ [self captureVideo:video
+ replayId:replayId
+ segmentId:segmentId
+ type:type];
+ }
+ self->_resumeReplayMaker = nil;
+ }]) {
+ SENTRY_LOG_ERROR(@"Could not create replay video: %@", error);
+ return;
+ }
+
+ NSMutableDictionary *eventContext = event.context.mutableCopy;
+ eventContext[@"replay"] =
+ [NSDictionary dictionaryWithObjectsAndKeys:replayId.sentryIdString, @"replay_id", nil];
+ event.context = eventContext;
+}
+
+- (void)captureVideo:(SentryVideoInfo *)video
+ replayId:(SentryId *)replayId
+ segmentId:(int)segment
+ type:(SentryReplayType)type
+{
+ SentryReplayEvent *replayEvent = [[SentryReplayEvent alloc] initWithEventId:replayId
+ replayStartTimestamp:video.start
+ replayType:type
+ segmentId:segment];
+ replayEvent.timestamp = video.end;
+ SentryReplayRecording *recording = [[SentryReplayRecording alloc] initWithSegmentId:segment
+ video:video
+ extraEvents:@[]];
+
+ [SentrySDK.currentHub captureReplayEvent:replayEvent
+ replayRecording:recording
+ video:video.path];
+
+ NSError *error = nil;
+ if (![[NSFileManager defaultManager] removeItemAtURL:video.path error:&error]) {
+ NSLog(@"[SentrySessionReplay:%d] Could not delete replay segment from disk: %@", __LINE__,
+ error.localizedDescription);
+ }
+}
+
- (void)startSession
{
_startedAsFullSession = [self shouldReplayFullSession:_replayOptions.sessionSampleRate];
@@ -110,9 +220,7 @@ - (void)startWithOptions:(SentryReplayOptions *)replayOptions
breadcrumbConverter:(id)breadcrumbConverter
fullSession:(BOOL)shouldReplayFullSession
{
- NSURL *docs =
- [NSURL fileURLWithPath:[SentryDependencyContainer.sharedInstance.fileManager sentryPath]];
- docs = [docs URLByAppendingPathComponent:SENTRY_REPLAY_FOLDER];
+ NSURL *docs = [self replayDirectory];
NSString *currentSession = [NSUUID UUID].UUIDString;
docs = [docs URLByAppendingPathComponent:currentSession];
@@ -125,6 +233,7 @@ - (void)startWithOptions:(SentryReplayOptions *)replayOptions
SentryOnDemandReplay *replayMaker = [[SentryOnDemandReplay alloc] initWithOutputPath:docs.path];
replayMaker.bitRate = replayOptions.replayBitRate;
+ replayMaker.videoScale = replayOptions.sizeScale;
replayMaker.cacheMaxSize
= (NSInteger)(shouldReplayFullSession ? replayOptions.sessionSegmentDuration + 1
: replayOptions.errorReplayDuration + 1);
@@ -153,6 +262,38 @@ - (void)startWithOptions:(SentryReplayOptions *)replayOptions
selector:@selector(resume)
name:UIApplicationWillEnterForegroundNotification
object:nil];
+
+ [self saveCurrentSessionInfo:self.sessionReplay.sessionReplayId
+ path:docs.path
+ options:replayOptions];
+}
+
+- (NSURL *)replayDirectory
+{
+ NSURL *dir =
+ [NSURL fileURLWithPath:[SentryDependencyContainer.sharedInstance.fileManager sentryPath]];
+ return [dir URLByAppendingPathComponent:SENTRY_REPLAY_FOLDER];
+}
+
+- (void)saveCurrentSessionInfo:(SentryId *)sessionId
+ path:(NSString *)path
+ options:(SentryReplayOptions *)options
+{
+ NSDictionary *info = [[NSDictionary alloc] initWithObjectsAndKeys:sessionId.sentryIdString,
+ @"replayId", path.lastPathComponent, @"path",
+ @(options.errorSampleRate), @"errorSampleRate", nil];
+
+ NSData *data = [SentrySerialization dataWithJSONObject:info];
+
+ NSString *infoPath =
+ [[path stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"lastreplay"];
+ if ([NSFileManager.defaultManager fileExistsAtPath:infoPath]) {
+ [NSFileManager.defaultManager removeItemAtPath:infoPath error:nil];
+ }
+ [data writeToFile:infoPath atomically:YES];
+
+ sentrySessionReplaySync_start([[path stringByAppendingPathComponent:@"crashInfo"]
+ cStringUsingEncoding:NSUTF8StringEncoding]);
}
- (void)stop
@@ -185,9 +326,9 @@ - (void)sentrySessionStarted:(SentrySession *)session
[self startSession];
}
-- (void)captureReplay
+- (BOOL)captureReplay
{
- [self.sessionReplay captureReplay];
+ return [self.sessionReplay captureReplay];
}
- (void)configureReplayWith:(nullable id)breadcrumbConverter
@@ -291,6 +432,9 @@ - (void)sessionReplayNewSegmentWithReplayEvent:(SentryReplayEvent *)replayEvent
[SentrySDK.currentHub captureReplayEvent:replayEvent
replayRecording:replayRecording
video:videoUrl];
+
+ sentrySessionReplaySync_updateInfo(
+ (unsigned int)replayEvent.segmentId, replayEvent.timestamp.timeIntervalSinceReferenceDate);
}
- (void)sessionReplayStartedWithReplayId:(SentryId *)replayId
diff --git a/Sources/Sentry/SentrySessionReplaySyncC.c b/Sources/Sentry/SentrySessionReplaySyncC.c
new file mode 100644
index 00000000000..952a38f3b6b
--- /dev/null
+++ b/Sources/Sentry/SentrySessionReplaySyncC.c
@@ -0,0 +1,97 @@
+#include "SentrySessionReplaySyncC.h"
+#include "SentryAsyncSafeLog.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+static SentryCrashReplay crashReplay = { 0 };
+
+void
+sentrySessionReplaySync_start(const char *const path)
+{
+ crashReplay.lastSegmentEnd = 0;
+ crashReplay.segmentId = 0;
+
+ if (crashReplay.path != NULL) {
+ free(crashReplay.path);
+ }
+
+ crashReplay.path = malloc(strlen(path));
+ strcpy(crashReplay.path, path);
+}
+
+void
+sentrySessionReplaySync_updateInfo(unsigned int segmentId, double lastSegmentEnd)
+{
+ crashReplay.segmentId = segmentId;
+ crashReplay.lastSegmentEnd = lastSegmentEnd;
+}
+
+void
+sentrySessionReplaySync_writeInfo(void)
+{
+ int fd = open(crashReplay.path, O_RDWR | O_CREAT | O_TRUNC, 0644);
+
+ if (fd < 1) {
+ SENTRY_ASYNC_SAFE_LOG_ERROR(
+ "Could not open replay info crash for file %s: %s", crashReplay.path, strerror(errno));
+ return;
+ }
+
+ if (!sentrycrashfu_writeBytesToFD(
+ fd, (char *)&crashReplay.segmentId, sizeof(crashReplay.segmentId))) {
+ SENTRY_ASYNC_SAFE_LOG_ERROR("Error writing replay info for crash.");
+ close(fd);
+ return;
+ }
+
+ if (!sentrycrashfu_writeBytesToFD(
+ fd, (char *)&crashReplay.lastSegmentEnd, sizeof(crashReplay.lastSegmentEnd))) {
+ SENTRY_ASYNC_SAFE_LOG_ERROR("Error writing replay info for crash.");
+ close(fd);
+ return;
+ }
+
+ close(fd);
+}
+
+bool
+sentrySessionReplaySync_readInfo(SentryCrashReplay *output, const char *const path)
+{
+ int fd = open(path, O_RDONLY);
+ if (fd < 0) {
+ SENTRY_ASYNC_SAFE_LOG_ERROR(
+ "Could not open replay info crash file %s: %s", path, strerror(errno));
+ return false;
+ }
+
+ unsigned int segmentId = 0;
+ double lastSegmentEnd = 0;
+
+ if (!sentrycrashfu_readBytesFromFD(fd, (char *)&segmentId, sizeof(segmentId))) {
+ SENTRY_ASYNC_SAFE_LOG_ERROR("Error reading segmentId from replay info crash file.");
+ close(fd);
+ return false;
+ }
+
+ if (!sentrycrashfu_readBytesFromFD(fd, (char *)&lastSegmentEnd, sizeof(lastSegmentEnd))) {
+ SENTRY_ASYNC_SAFE_LOG_ERROR("Error reading lastSegmentEnd from replay info crash file.");
+ close(fd);
+ return false;
+ }
+
+ close(fd);
+
+ if (lastSegmentEnd == 0) {
+ return false;
+ }
+
+ output->segmentId = segmentId;
+ output->lastSegmentEnd = lastSegmentEnd;
+ return true;
+}
diff --git a/Sources/Sentry/SentryTraceContext.m b/Sources/Sentry/SentryTraceContext.m
index afe2a1b541f..c14486b23dd 100644
--- a/Sources/Sentry/SentryTraceContext.m
+++ b/Sources/Sentry/SentryTraceContext.m
@@ -92,6 +92,7 @@ - (nullable instancetype)initWithTracer:(SentryTracer *)tracer
- (instancetype)initWithTraceId:(SentryId *)traceId
options:(SentryOptions *)options
userSegment:(nullable NSString *)userSegment
+ replayId:(nullable NSString *)replayId;
{
return [[SentryTraceContext alloc] initWithTraceId:traceId
publicKey:options.parsedDsn.url.user
@@ -101,7 +102,7 @@ - (instancetype)initWithTraceId:(SentryId *)traceId
userSegment:userSegment
sampleRate:nil
sampled:nil
- replayId:nil];
+ replayId:replayId];
}
- (nullable instancetype)initWithDict:(NSDictionary *)dictionary
diff --git a/Sources/Sentry/include/SentryGlobalEventProcessor.h b/Sources/Sentry/include/SentryGlobalEventProcessor.h
index c2571f29f28..be540f80b8b 100644
--- a/Sources/Sentry/include/SentryGlobalEventProcessor.h
+++ b/Sources/Sentry/include/SentryGlobalEventProcessor.h
@@ -15,6 +15,8 @@ SENTRY_NO_INIT
- (void)addEventProcessor:(SentryEventProcessor)newProcessor;
+- (nullable SentryEvent *)reportAll:(SentryEvent *)event;
+
@end
NS_ASSUME_NONNULL_END
diff --git a/Sources/Sentry/include/SentrySerialization.h b/Sources/Sentry/include/SentrySerialization.h
index a5149e3ce64..32c4873fd43 100644
--- a/Sources/Sentry/include/SentrySerialization.h
+++ b/Sources/Sentry/include/SentrySerialization.h
@@ -24,7 +24,7 @@ NS_ASSUME_NONNULL_BEGIN
/**
* Retrieves the json object from an event envelope item data.
*/
-+ (NSDictionary *)deserializeEventEnvelopeItem:(NSData *)eventEnvelopeItemData;
++ (NSDictionary *)deserializeDictionaryFromJsonData:(NSData *)data;
/**
* Extract the level from data of an envelopte item containing an event. Default is the 'error'
diff --git a/Sources/Sentry/include/SentrySessionReplayIntegration.h b/Sources/Sentry/include/SentrySessionReplayIntegration.h
index 47237126b32..dcb2ffc121c 100644
--- a/Sources/Sentry/include/SentrySessionReplayIntegration.h
+++ b/Sources/Sentry/include/SentrySessionReplayIntegration.h
@@ -13,7 +13,7 @@ NS_ASSUME_NONNULL_BEGIN
/**
* Captures Replay. Used by the Hybrid SDKs.
*/
-- (void)captureReplay;
+- (BOOL)captureReplay;
/**
* Configure session replay with different breadcrumb converter
diff --git a/Sources/Sentry/include/SentrySessionReplaySyncC.h b/Sources/Sentry/include/SentrySessionReplaySyncC.h
new file mode 100644
index 00000000000..faadac62a50
--- /dev/null
+++ b/Sources/Sentry/include/SentrySessionReplaySyncC.h
@@ -0,0 +1,19 @@
+#ifndef SentrySessionReplaySyncC_h
+#define SentrySessionReplaySyncC_h
+#include
+
+typedef struct {
+ unsigned int segmentId;
+ double lastSegmentEnd;
+ char *path;
+} SentryCrashReplay;
+
+void sentrySessionReplaySync_start(const char *const path);
+
+void sentrySessionReplaySync_updateInfo(unsigned int segmentId, double lastSegmentEnd);
+
+void sentrySessionReplaySync_writeInfo(void);
+
+bool sentrySessionReplaySync_readInfo(SentryCrashReplay *output, const char *const path);
+
+#endif /* SentrySessionReplaySyncC_h */
diff --git a/Sources/Sentry/include/SentryTraceContext.h b/Sources/Sentry/include/SentryTraceContext.h
index 9ed6a67816a..9eed40b1f60 100644
--- a/Sources/Sentry/include/SentryTraceContext.h
+++ b/Sources/Sentry/include/SentryTraceContext.h
@@ -96,7 +96,8 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (instancetype)initWithTraceId:(SentryId *)traceId
options:(SentryOptions *)options
- userSegment:(nullable NSString *)userSegment;
+ userSegment:(nullable NSString *)userSegment
+ replayId:(nullable NSString *)replayId;
/**
* Create a SentryBaggage with the information of this SentryTraceContext.
diff --git a/Sources/SentryCrash/Recording/SentryCrashC.c b/Sources/SentryCrash/Recording/SentryCrashC.c
index c201a944379..1225728bf65 100644
--- a/Sources/SentryCrash/Recording/SentryCrashC.c
+++ b/Sources/SentryCrash/Recording/SentryCrashC.c
@@ -41,6 +41,7 @@
#include "SentryAsyncSafeLog.h"
+#include "SentrySessionReplaySyncC.h"
#include
#include
#include
@@ -83,6 +84,7 @@ onCrash(struct SentryCrash_MonitorContext *monitorContext)
sentrycrashcrs_getNextCrashReportPath(crashReportFilePath);
strncpy(g_lastCrashReportFilePath, crashReportFilePath, sizeof(g_lastCrashReportFilePath));
sentrycrashreport_writeStandardReport(monitorContext, crashReportFilePath);
+ sentrySessionReplaySync_writeInfo();
}
// Report is saved to disk, now we try to take screenshots
diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift
index 3f094959f57..dd08caa4e28 100644
--- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift
+++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift
@@ -27,6 +27,7 @@ enum SentryOnDemandReplayError: Error {
@objcMembers
class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker {
+
private let _outputPath: String
private var _currentPixelBuffer: SentryPixelBuffer?
private var _totalFrames = 0
@@ -44,20 +45,50 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker {
var videoWidth = 200
var videoHeight = 434
+ var videoScale: Float = 1
var bitRate = 20_000
var frameRate = 1
var cacheMaxSize = UInt.max
+ private var actualWidth: Int { Int(Float(videoWidth) * videoScale) }
+ private var actualHeight: Int { Int(Float(videoHeight) * videoScale) }
+
+ init(outputPath: String, workingQueue: SentryDispatchQueueWrapper, dateProvider: SentryCurrentDateProvider) {
+ self._outputPath = outputPath
+ self.dateProvider = dateProvider
+ self.workingQueue = workingQueue
+ }
+
+ convenience init(withContentFrom outputPath: String, workingQueue: SentryDispatchQueueWrapper, dateProvider: SentryCurrentDateProvider) {
+ self.init(outputPath: outputPath, workingQueue: workingQueue, dateProvider: dateProvider)
+
+ do {
+ let content = try FileManager.default.contentsOfDirectory(atPath: outputPath)
+ _frames = content.compactMap {
+ guard $0.hasSuffix(".png") else { return SentryReplayFrame?.none }
+ guard let time = Double($0.dropLast(4)) else { return nil }
+ return SentryReplayFrame(imagePath: "\(outputPath)/\($0)", time: Date(timeIntervalSinceReferenceDate: time), screenName: nil)
+ }.sorted { $0.time < $1.time }
+ } catch {
+ print("[SentryOnDemandReplay:\(#line)] Could not list frames from replay: \(error.localizedDescription)")
+ return
+ }
+ }
+
convenience init(outputPath: String) {
self.init(outputPath: outputPath,
workingQueue: SentryDispatchQueueWrapper(name: "io.sentry.onDemandReplay", attributes: nil),
dateProvider: SentryCurrentDateProvider())
}
- init(outputPath: String, workingQueue: SentryDispatchQueueWrapper, dateProvider: SentryCurrentDateProvider) {
- self._outputPath = outputPath
- self.dateProvider = dateProvider
- self.workingQueue = workingQueue
+ convenience init(withContentFrom outputPath: String) {
+ self.init(withContentFrom: outputPath,
+ workingQueue: SentryDispatchQueueWrapper(name: "io.sentry.onDemandReplay", attributes: nil),
+ dateProvider: SentryCurrentDateProvider())
+
+ guard let last = _frames.last, let image = UIImage(contentsOfFile: last.imagePath) else { return }
+ videoWidth = Int(image.size.width)
+ videoHeight = Int(image.size.height)
}
func addFrameAsync(image: UIImage, forScreen: String?) {
@@ -70,7 +101,7 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker {
guard let data = rescaleImage(image)?.pngData() else { return }
let date = dateProvider.date()
- let imagePath = (_outputPath as NSString).appendingPathComponent("\(_totalFrames).png")
+ let imagePath = (_outputPath as NSString).appendingPathComponent("\(date.timeIntervalSinceReferenceDate).png")
do {
try data.write(to: URL(fileURLWithPath: imagePath))
} catch {
@@ -105,6 +136,10 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker {
})
}
+ var oldestFrameDate: Date? {
+ return _frames.first?.time
+ }
+
func createVideoWith(beginning: Date, end: Date, outputFileURL: URL, completion: @escaping (SentryVideoInfo?, Error?) -> Void) throws {
var frameCount = 0
let videoFrames = filterFrames(beginning: beginning, end: end)
@@ -113,7 +148,7 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker {
let videoWriter = try AVAssetWriter(url: outputFileURL, fileType: .mp4)
let videoWriterInput = AVAssetWriterInput(mediaType: .video, outputSettings: createVideoSettings())
- _currentPixelBuffer = SentryPixelBuffer(size: CGSize(width: videoWidth, height: videoHeight), videoWriterInput: videoWriterInput)
+ _currentPixelBuffer = SentryPixelBuffer(size: CGSize(width: actualWidth, height: actualHeight), videoWriterInput: videoWriterInput)
if _currentPixelBuffer == nil { return }
videoWriter.add(videoWriterInput)
@@ -151,7 +186,7 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker {
completion(nil, SentryOnDemandReplayError.cantReadVideoSize)
return
}
- videoInfo = SentryVideoInfo(path: outputFileURL, height: self.videoHeight, width: self.videoWidth, duration: TimeInterval(videoFrames.framesPaths.count / self.frameRate), frameCount: videoFrames.framesPaths.count, frameRate: self.frameRate, start: videoFrames.start, end: videoFrames.end, fileSize: fileSize, screens: videoFrames.screens)
+ videoInfo = SentryVideoInfo(path: outputFileURL, height: self.actualHeight, width: self.actualWidth, duration: TimeInterval(videoFrames.framesPaths.count / self.frameRate), frameCount: videoFrames.framesPaths.count, frameRate: self.frameRate, start: videoFrames.start, end: videoFrames.end, fileSize: fileSize, screens: videoFrames.screens)
} catch {
completion(nil, error)
}
@@ -189,8 +224,8 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker {
private func createVideoSettings() -> [String: Any] {
return [
AVVideoCodecKey: AVVideoCodecType.h264,
- AVVideoWidthKey: videoWidth,
- AVVideoHeightKey: videoHeight,
+ AVVideoWidthKey: actualWidth,
+ AVVideoHeightKey: actualHeight,
AVVideoCompressionPropertiesKey: [
AVVideoAverageBitRateKey: bitRate,
AVVideoProfileLevelKey: AVVideoProfileLevelH264BaselineAutoLevel
diff --git a/Sources/Swift/Integrations/SessionReplay/SentryReplayRecording.swift b/Sources/Swift/Integrations/SessionReplay/SentryReplayRecording.swift
index de45c5a4d1d..10f7fc4e332 100644
--- a/Sources/Swift/Integrations/SessionReplay/SentryReplayRecording.swift
+++ b/Sources/Swift/Integrations/SessionReplay/SentryReplayRecording.swift
@@ -9,11 +9,19 @@ class SentryReplayRecording: NSObject {
static let SentryReplayFrameRateType = "constant"
let segmentId: Int
-
let events: [any SentryRRWebEventProtocol]
+ let height: Int
+ let width: Int
+
+ convenience init(segmentId: Int, video: SentryVideoInfo, extraEvents: [any SentryRRWebEventProtocol]) {
+ self.init(segmentId: segmentId, size: video.fileSize, start: video.start, duration: video.duration, frameCount: video.frameCount, frameRate: video.frameRate, height: video.height, width: video.width, extraEvents: extraEvents)
+ }
+
init(segmentId: Int, size: Int, start: Date, duration: TimeInterval, frameCount: Int, frameRate: Int, height: Int, width: Int, extraEvents: [any SentryRRWebEventProtocol]?) {
self.segmentId = segmentId
+ self.width = width
+ self.height = height
let meta = SentryRRWebMetaEvent(timestamp: start, height: height, width: width)
let video = SentryRRWebVideoEvent(timestamp: start, segmentId: segmentId, size: size, duration: duration, encoding: SentryReplayRecording.SentryReplayEncoding, container: SentryReplayRecording.SentryReplayContainer, height: height, width: width, frameCount: frameCount, frameRateType: SentryReplayRecording.SentryReplayFrameRateType, frameRate: frameRate, left: 0, top: 0)
diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift
index dfcb8a46ac4..a72aeddfa70 100644
--- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift
+++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift
@@ -3,6 +3,11 @@ import Foundation
@_implementationOnly import _SentryPrivate
import UIKit
+enum SessionReplayError: Error {
+ case cantCreateReplayDirectory
+ case noFramesAvailable
+}
+
@objc
protocol SentrySessionReplayDelegate: NSObjectProtocol {
func sessionReplayIsFullSession() -> Bool
@@ -77,8 +82,8 @@ class SentrySessionReplay: NSObject {
videoSegmentStart = nil
currentSegmentId = 0
sessionReplayId = SentryId()
- replayMaker.videoWidth = Int(Float(rootView.frame.size.width) * replayOptions.sizeScale)
- replayMaker.videoHeight = Int(Float(rootView.frame.size.height) * replayOptions.sizeScale)
+ replayMaker.videoWidth = Int(rootView.frame.size.width)
+ replayMaker.videoHeight = Int(rootView.frame.size.height)
imageCollection = []
if fullSession {
@@ -250,7 +255,7 @@ class SentrySessionReplay: NSObject {
touchTracker.flushFinishedEvents()
}
- let recording = SentryReplayRecording(segmentId: replayEvent.segmentId, size: video.fileSize, start: video.start, duration: video.duration, frameCount: video.frameCount, frameRate: video.frameRate, height: video.height, width: video.width, extraEvents: events)
+ let recording = SentryReplayRecording(segmentId: segment, video: video, extraEvents: events)
delegate?.sessionReplayNewSegment(replayEvent: replayEvent, replayRecording: recording, videoUrl: video.path)
@@ -268,14 +273,17 @@ class SentrySessionReplay: NSObject {
}
.compactMap(breadcrumbConverter.convert(from:))
}
-
+
private func takeScreenshot() {
guard let rootView = rootView, !processingScreenshot else { return }
-
- lock.synchronized {
- guard !processingScreenshot else { return }
- processingScreenshot = true
+
+ lock.lock()
+ guard !processingScreenshot else {
+ lock.unlock()
+ return
}
+ processingScreenshot = true
+ lock.unlock()
let screenName = delegate?.currentScreenNameForSessionReplay()
@@ -285,8 +293,10 @@ class SentrySessionReplay: NSObject {
}
private func newImage(image: UIImage, forScreen screen: String?) {
- processingScreenshot = false
- replayMaker.addFrameAsync(image: image, forScreen: screen)
+ lock.synchronized {
+ processingScreenshot = false
+ replayMaker.addFrameAsync(image: image, forScreen: screen)
+ }
}
}
diff --git a/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift
index b1353ea5741..9509afa696a 100644
--- a/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift
+++ b/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift
@@ -715,7 +715,7 @@ class SentryNetworkTrackerTests: XCTestCase {
sut.urlSessionTaskResume(task)
let expectedTraceHeader = SentrySDK.currentHub().scope.propagationContext.traceHeader.value()
- let traceContext = SentryTraceContext(trace: SentrySDK.currentHub().scope.propagationContext.traceId, options: self.fixture.options, userSegment: self.fixture.scope.userObject?.segment)
+ let traceContext = SentryTraceContext(trace: SentrySDK.currentHub().scope.propagationContext.traceId, options: self.fixture.options, userSegment: self.fixture.scope.userObject?.segment, replayId: nil)
let expectedBaggageHeader = traceContext.toBaggage().toHTTPHeader(withOriginalBaggage: nil)
XCTAssertEqual(task.currentRequest?.allHTTPHeaderFields?["baggage"] ?? "", expectedBaggageHeader)
XCTAssertEqual(task.currentRequest?.allHTTPHeaderFields?["sentry-trace"] ?? "", expectedTraceHeader)
@@ -728,7 +728,7 @@ class SentryNetworkTrackerTests: XCTestCase {
sut.urlSessionTaskResume(task)
let expectedTraceHeader = SentrySDK.currentHub().scope.propagationContext.traceHeader.value()
- let traceContext = SentryTraceContext(trace: SentrySDK.currentHub().scope.propagationContext.traceId, options: self.fixture.options, userSegment: self.fixture.scope.userObject?.segment)
+ let traceContext = SentryTraceContext(trace: SentrySDK.currentHub().scope.propagationContext.traceId, options: self.fixture.options, userSegment: self.fixture.scope.userObject?.segment, replayId: nil)
let expectedBaggageHeader = traceContext.toBaggage().toHTTPHeader(withOriginalBaggage: nil)
XCTAssertEqual(task.currentRequest?.allHTTPHeaderFields?["baggage"] ?? "", expectedBaggageHeader)
XCTAssertEqual(task.currentRequest?.allHTTPHeaderFields?["sentry-trace"] ?? "", expectedTraceHeader)
diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift
index d9a0e813a2e..07b166bf893 100644
--- a/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift
+++ b/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift
@@ -110,7 +110,7 @@ class SentryOnDemandReplayTests: XCTestCase {
group.wait()
queue.queue.sync {} //Wait for all enqueued operation to finish
- XCTAssertEqual(sut.frames.map({ ($0.imagePath as NSString).lastPathComponent }), (0..<10).map { "\($0).png" })
+ XCTAssertEqual(sut.frames.map({ ($0.imagePath as NSString).lastPathComponent }), (0..<10).map { "\($0).0.png" })
}
func testReleaseIsThreadSafe() {
diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift
index 5aacd3ed010..d330daa7e58 100644
--- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift
+++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift
@@ -43,15 +43,14 @@ class SentrySessionReplayIntegrationTests: XCTestCase {
}
private func startSDK(sessionSampleRate: Float, errorSampleRate: Float, enableSwizzling: Bool = true) {
- if #available(iOS 16.0, tvOS 16.0, *) {
- SentrySDK.start {
- $0.dsn = "https://user@test.com/test"
- $0.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: sessionSampleRate, errorSampleRate: errorSampleRate)
- $0.setIntegrations([SentrySessionReplayIntegration.self])
- $0.enableSwizzling = enableSwizzling
- }
- SentrySDK.currentHub().startSession()
+ SentrySDK.start {
+ $0.dsn = "https://user@test.com/test"
+ $0.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: sessionSampleRate, errorSampleRate: errorSampleRate)
+ $0.setIntegrations([SentrySessionReplayIntegration.self])
+ $0.enableSwizzling = enableSwizzling
+ $0.cacheDirectoryPath = FileManager.default.temporaryDirectory.path
}
+ SentrySDK.currentHub().startSession()
}
func testNoInstall() {
@@ -184,6 +183,107 @@ class SentrySessionReplayIntegrationTests: XCTestCase {
XCTAssertEqual(sut.currentScreenNameForSessionReplay(), "Scope Screen")
}
+ func testSessionReplayForCrash() throws {
+ startSDK(sessionSampleRate: 1, errorSampleRate: 1)
+
+ let client = SentryClient(options: try XCTUnwrap(SentrySDK.options))
+ let scope = Scope()
+ let hub = TestHub(client: client, andScope: scope)
+ SentrySDK.setCurrentHub(hub)
+ let expectation = expectation(description: "Replay to be capture")
+ hub.onReplayCapture = {
+ expectation.fulfill()
+ }
+
+ try createLastSessionReplay()
+ let crash = Event(error: NSError(domain: "Error", code: 1))
+ crash.context = [:]
+ crash.isCrashEvent = true
+ SentryGlobalEventProcessor.shared().reportAll(crash)
+
+ wait(for: [expectation], timeout: 1)
+ XCTAssertEqual(hub.capturedReplayRecordingVideo.count, 1)
+
+ let replayInfo = try XCTUnwrap(hub.capturedReplayRecordingVideo.first)
+ XCTAssertEqual(replayInfo.replay.replayType, SentryReplayType.session)
+ XCTAssertEqual(replayInfo.recording.segmentId, 2)
+ XCTAssertEqual(replayInfo.replay.replayStartTimestamp, Date(timeIntervalSinceReferenceDate: 5))
+ }
+
+ func testBufferReplayForCrash() throws {
+ startSDK(sessionSampleRate: 1, errorSampleRate: 1)
+
+ let client = SentryClient(options: try XCTUnwrap(SentrySDK.options))
+ let scope = Scope()
+ let hub = TestHub(client: client, andScope: scope)
+ SentrySDK.setCurrentHub(hub)
+ let expectation = expectation(description: "Replay to be capture")
+ hub.onReplayCapture = {
+ expectation.fulfill()
+ }
+
+ try createLastSessionReplay(writeSessionInfo: false)
+ let crash = Event(error: NSError(domain: "Error", code: 1))
+ crash.context = [:]
+ crash.isCrashEvent = true
+ SentryGlobalEventProcessor.shared().reportAll(crash)
+
+ wait(for: [expectation], timeout: 1)
+ XCTAssertEqual(hub.capturedReplayRecordingVideo.count, 1)
+
+ let replayInfo = try XCTUnwrap(hub.capturedReplayRecordingVideo.first)
+ XCTAssertEqual(replayInfo.replay.replayType, SentryReplayType.buffer)
+ XCTAssertEqual(replayInfo.recording.segmentId, 0)
+ XCTAssertEqual(replayInfo.replay.replayStartTimestamp, Date(timeIntervalSinceReferenceDate: 5))
+ }
+
+ func testBufferReplayIgnoredBecauseSampleRateForCrash() throws {
+ startSDK(sessionSampleRate: 1, errorSampleRate: 1)
+
+ let client = SentryClient(options: try XCTUnwrap(SentrySDK.options))
+ let scope = Scope()
+ let hub = TestHub(client: client, andScope: scope)
+ SentrySDK.setCurrentHub(hub)
+ let expectation = expectation(description: "Replay to be capture")
+ expectation.isInverted = true
+ hub.onReplayCapture = {
+ expectation.fulfill()
+ }
+
+ try createLastSessionReplay(writeSessionInfo: false, errorSampleRate: 0)
+ let crash = Event(error: NSError(domain: "Error", code: 1))
+ crash.context = [:]
+ crash.isCrashEvent = true
+ SentryGlobalEventProcessor.shared().reportAll(crash)
+
+ wait(for: [expectation], timeout: 1)
+ XCTAssertEqual(hub.capturedReplayRecordingVideo.count, 0)
+ }
+
+ func createLastSessionReplay(writeSessionInfo: Bool = true, errorSampleRate: Double = 1) throws {
+ let replayFolder = SentryDependencyContainer.sharedInstance().fileManager.sentryPath + "/replay"
+ let jsonPath = replayFolder + "/lastreplay"
+ var sessionFolder = UUID().uuidString
+ let info: [String: Any] = ["replayId": SentryId().sentryIdString,
+ "path": sessionFolder,
+ "errorSampleRate": errorSampleRate]
+ let data = SentrySerialization.data(withJSONObject: info)
+ try data?.write(to: URL(fileURLWithPath: jsonPath))
+
+ sessionFolder = "\(replayFolder)/\(sessionFolder)"
+ try FileManager.default.createDirectory(atPath: sessionFolder, withIntermediateDirectories: true)
+
+ for i in 5...9 {
+ let image = UIImage.add.jpegData(compressionQuality: 1)
+ try image?.write(to: URL(fileURLWithPath: "\(sessionFolder)/\(i).png") )
+ }
+
+ if writeSessionInfo {
+ sentrySessionReplaySync_start("\(sessionFolder)/crashInfo")
+ sentrySessionReplaySync_updateInfo(1, Double(4))
+ sentrySessionReplaySync_writeInfo()
+ }
+ }
}
#endif
diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift
index 4e9b7e6d468..adbf7d9d482 100644
--- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift
+++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift
@@ -136,8 +136,8 @@ class SentrySessionReplayTests: XCTestCase {
view.frame = CGRect(x: 0, y: 0, width: 320, height: 900)
sut.start(rootView: fixture.rootView, fullSession: true)
- XCTAssertEqual(Int(320 * options.sizeScale), fixture.replayMaker.videoWidth)
- XCTAssertEqual(Int(900 * options.sizeScale), fixture.replayMaker.videoHeight)
+ XCTAssertEqual(320, fixture.replayMaker.videoWidth)
+ XCTAssertEqual(900, fixture.replayMaker.videoHeight)
}
func testSentReplay_FullSession() {
diff --git a/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift b/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift
index 8e4b84773dc..691734878ed 100644
--- a/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift
+++ b/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift
@@ -373,8 +373,9 @@ class PrivateSentrySDKOnlyTests: XCTestCase {
return true
}
- override func captureReplay() {
+ override func captureReplay() -> Bool {
TestSentrySessionReplayIntegration.captureReplayCalledTimes += 1
+ return true
}
static func captureReplayShouldBeCalledAtLeastOnce() -> Bool {
diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift
index b0b632721bf..de775c16117 100644
--- a/Tests/SentryTests/SentryClientTests.swift
+++ b/Tests/SentryTests/SentryClientTests.swift
@@ -687,7 +687,7 @@ class SentryClientTest: XCTestCase {
try assertValidErrorEvent(eventWithSessionArguments.event, error)
XCTAssertEqual(fixture.session, eventWithSessionArguments.session)
- let expectedTraceContext = SentryTraceContext(trace: scope.propagationContext.traceId, options: Options(), userSegment: "segment")
+ let expectedTraceContext = SentryTraceContext(trace: scope.propagationContext.traceId, options: Options(), userSegment: "segment", replayId: nil)
XCTAssertEqual(eventWithSessionArguments.traceContext?.traceId,
expectedTraceContext.traceId)
}
@@ -1895,6 +1895,16 @@ class SentryClientTest: XCTestCase {
XCTAssertNil(replayEvent.threads)
XCTAssertNil(replayEvent.debugMeta)
}
+
+ func testCaptureCrashEventSetReplayInScope() {
+ let sut = fixture.getSut()
+ let event = Event()
+ event.isCrashEvent = true
+ let scope = Scope()
+ event.context = ["replay": ["replay_id": "someReplay"]]
+ sut.captureCrash(event, with: SentrySession(releaseName: "", distinctId: ""), with: scope)
+ XCTAssertEqual(scope.replayId, "someReplay")
+ }
}
private extension SentryClientTest {
diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m
index 6b5fa7e58f5..5f26a695c92 100644
--- a/Tests/SentryTests/SentryOptionsTest.m
+++ b/Tests/SentryTests/SentryOptionsTest.m
@@ -459,11 +459,15 @@ - (void)testDefaultIntegrations
@"Default integrations are not set correctly");
}
-- (void)testSentryCrashIntegrationIsFirst
+#if SENTRY_HAS_UIKIT
+- (void)testIntegrationOrder
{
XCTAssertEqualObjects(SentryOptions.defaultIntegrations.firstObject,
- NSStringFromClass([SentryCrashIntegration class]));
+ NSStringFromClass([SentrySessionReplayIntegration class]));
+ XCTAssertEqualObjects(
+ SentryOptions.defaultIntegrations[1], NSStringFromClass([SentryCrashIntegration class]));
}
+#endif
- (void)testSampleRateWithDict
{
diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h
index ae34d5580fc..b187a747604 100644
--- a/Tests/SentryTests/SentryTests-Bridging-Header.h
+++ b/Tests/SentryTests/SentryTests-Bridging-Header.h
@@ -243,3 +243,4 @@
#import "SentryCrashCachedData.h"
#import "SentryCrashInstallation+Private.h"
#import "SentryCrashMonitor_MachException.h"
+#import "SentrySessionReplaySyncC.h"
diff --git a/Tests/SentryTests/Transaction/SentryTraceStateTests.swift b/Tests/SentryTests/Transaction/SentryTraceStateTests.swift
index 7b549815040..3d5233b4c08 100644
--- a/Tests/SentryTests/Transaction/SentryTraceStateTests.swift
+++ b/Tests/SentryTests/Transaction/SentryTraceStateTests.swift
@@ -99,7 +99,7 @@ class SentryTraceContextTests: XCTestCase {
options.dsn = TestConstants.realDSN
let traceId = SentryId()
- let traceContext = SentryTraceContext(trace: traceId, options: options, userSegment: "segment")
+ let traceContext = SentryTraceContext(trace: traceId, options: options, userSegment: "segment", replayId: "replayId")
XCTAssertEqual(options.parsedDsn?.url.user, traceContext.publicKey)
XCTAssertEqual(traceId, traceContext.traceId)
@@ -107,6 +107,7 @@ class SentryTraceContextTests: XCTestCase {
XCTAssertEqual(options.environment, traceContext.environment)
XCTAssertNil(traceContext.transaction)
XCTAssertEqual("segment", traceContext.userSegment)
+ XCTAssertEqual(traceContext.replayId, "replayId")
XCTAssertNil(traceContext.sampleRate)
XCTAssertNil(traceContext.sampled)
}
@@ -116,7 +117,7 @@ class SentryTraceContextTests: XCTestCase {
options.dsn = TestConstants.realDSN
let traceId = SentryId()
- let traceContext = SentryTraceContext(trace: traceId, options: options, userSegment: nil)
+ let traceContext = SentryTraceContext(trace: traceId, options: options, userSegment: nil, replayId: nil)
XCTAssertEqual(options.parsedDsn?.url.user, traceContext.publicKey)
XCTAssertEqual(traceId, traceContext.traceId)
From b9214551e33436a71df1098dd1bcaab7a06200e0 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 30 Jul 2024 08:49:40 +0200
Subject: [PATCH 07/63] chore(deps): bump slather from 2.8.2 to 2.8.3 (#4205)
Bumps [slather](https://github.com/SlatherOrg/slather) from 2.8.2 to 2.8.3.
- [Release notes](https://github.com/SlatherOrg/slather/releases)
- [Changelog](https://github.com/SlatherOrg/slather/blob/master/CHANGELOG.md)
- [Commits](https://github.com/SlatherOrg/slather/compare/v2.8.2...v2.8.3)
---
updated-dependencies:
- dependency-name: slather
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Gemfile.lock | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index 4dc6e731ecf..f43cd42c5e0 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -226,7 +226,7 @@ GEM
mini_magick (4.13.1)
mini_mime (1.1.5)
mini_portile2 (2.8.7)
- minitest (5.24.0)
+ minitest (5.24.1)
molinillo (0.8.0)
multi_json (1.15.0)
multipart-post (2.4.1)
@@ -236,7 +236,7 @@ GEM
naturally (2.2.1)
netrc (0.11.0)
nkf (0.2.0)
- nokogiri (1.16.6)
+ nokogiri (1.16.7)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
optparse (0.5.0)
@@ -270,7 +270,7 @@ GEM
simctl (1.6.10)
CFPropertyList
naturally
- slather (2.8.2)
+ slather (2.8.3)
CFPropertyList (>= 2.2, < 4)
activesupport
clamp (~> 1.3)
From 6a82cac46b472fe5c408c1f224301b7ca4586ab2 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 30 Jul 2024 08:50:09 +0200
Subject: [PATCH 08/63] chore(deps): bump fastlane from 2.221.1 to 2.222.0
(#4204)
Bumps [fastlane](https://github.com/fastlane/fastlane) from 2.221.1 to 2.222.0.
- [Release notes](https://github.com/fastlane/fastlane/releases)
- [Changelog](https://github.com/fastlane/fastlane/blob/master/CHANGELOG.latest.md)
- [Commits](https://github.com/fastlane/fastlane/compare/fastlane/2.221.1...fastlane/2.222.0)
---
updated-dependencies:
- dependency-name: fastlane
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Gemfile.lock | 26 +++++++++++++-------------
1 file changed, 13 insertions(+), 13 deletions(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index f43cd42c5e0..aae5fc862c4 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -23,20 +23,20 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
- aws-partitions (1.946.0)
- aws-sdk-core (3.197.2)
+ aws-partitions (1.959.0)
+ aws-sdk-core (3.201.3)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1)
- aws-sdk-kms (1.85.0)
- aws-sdk-core (~> 3, >= 3.197.0)
- aws-sigv4 (~> 1.1)
- aws-sdk-s3 (1.152.3)
- aws-sdk-core (~> 3, >= 3.197.0)
+ aws-sdk-kms (1.88.0)
+ aws-sdk-core (~> 3, >= 3.201.0)
+ aws-sigv4 (~> 1.5)
+ aws-sdk-s3 (1.156.0)
+ aws-sdk-core (~> 3, >= 3.201.0)
aws-sdk-kms (~> 1)
- aws-sigv4 (~> 1.8)
- aws-sigv4 (1.8.0)
+ aws-sigv4 (~> 1.5)
+ aws-sigv4 (1.9.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
@@ -96,7 +96,7 @@ GEM
escape (0.0.4)
ethon (0.16.0)
ffi (>= 1.15.0)
- excon (0.110.0)
+ excon (0.111.0)
faraday (1.10.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
@@ -118,7 +118,7 @@ GEM
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
- faraday-net_http (1.0.1)
+ faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
@@ -126,7 +126,7 @@ GEM
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.3.1)
- fastlane (2.221.1)
+ fastlane (2.222.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -223,7 +223,7 @@ GEM
mime-types (3.5.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2023.1003)
- mini_magick (4.13.1)
+ mini_magick (4.13.2)
mini_mime (1.1.5)
mini_portile2 (2.8.7)
minitest (5.24.1)
From dd63bbb2cf98cb8b170088cc28473774eec3a759 Mon Sep 17 00:00:00 2001
From: Dhiogo Brustolin
Date: Tue, 30 Jul 2024 13:27:08 +0200
Subject: [PATCH 09/63] Feat: Redact web view from replay (#4203)
Redact web view from replay
---
CHANGELOG.md | 1 +
.../iOS-Swift.xcodeproj/project.pbxproj | 6 +++
.../iOS-Swift/Base.lproj/Main.storyboard | 47 +++++++++++--------
.../iOS-Swift/ExtraViewController.swift | 4 ++
.../ViewControllers/WebViewController.swift | 18 +++++++
Sources/Swift/Tools/UIRedactBuilder.swift | 7 ++-
Tests/SentryTests/UIRedactBuilderTests.swift | 21 +++++++++
7 files changed, 84 insertions(+), 20 deletions(-)
create mode 100644 Samples/iOS-Swift/iOS-Swift/ViewControllers/WebViewController.swift
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8fa8238c585..c72ccbccc80 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
### Features
- Replay for crashes (#4171)
+- Redact web view from replay (#4203)
## 8.32.0
diff --git a/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj b/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj
index 767fc108731..a1bed4c746e 100644
--- a/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj
+++ b/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj
@@ -89,6 +89,8 @@
D8832B1F2AF535B200C522B0 /* PageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8832B1D2AF52D0500C522B0 /* PageViewController.swift */; };
D890CD3C26CEE2FA001246CF /* NibViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D890CD3B26CEE2FA001246CF /* NibViewController.xib */; };
D890CD3F26CEE31B001246CF /* NibViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D890CD3E26CEE31B001246CF /* NibViewController.swift */; };
+ D8AE48C92C57DC2F0092A2A6 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AE48C82C57DC2F0092A2A6 /* WebViewController.swift */; };
+ D8AE48CF2C57E0BD0092A2A6 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AE48C82C57DC2F0092A2A6 /* WebViewController.swift */; };
D8B56CF0273A8D97004DF238 /* Sentry.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 630853322440C44F00DDE4CE /* Sentry.framework */; };
D8B56CF1273A8D97004DF238 /* Sentry.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 630853322440C44F00DDE4CE /* Sentry.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
D8C33E1F29FBB1F70071B75A /* UIEventBreadcrumbsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8C33E1E29FBB1F70071B75A /* UIEventBreadcrumbsController.swift */; };
@@ -346,6 +348,7 @@
D88E666D28732B6700153425 /* iOS13-Swift-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "iOS13-Swift-Bridging-Header.h"; sourceTree = ""; };
D890CD3B26CEE2FA001246CF /* NibViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NibViewController.xib; sourceTree = ""; };
D890CD3E26CEE31B001246CF /* NibViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NibViewController.swift; sourceTree = ""; };
+ D8AE48C82C57DC2F0092A2A6 /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = ""; };
D8C33E1E29FBB1F70071B75A /* UIEventBreadcrumbsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIEventBreadcrumbsController.swift; sourceTree = ""; };
D8C33E2529FBB8D90071B75A /* UIEventBreadcrumbTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIEventBreadcrumbTests.swift; sourceTree = ""; };
D8D7BB492750067900044146 /* UIAssert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAssert.swift; sourceTree = ""; };
@@ -584,6 +587,7 @@
D8F01DE92A1376B5008F4996 /* InfoForBreadcrumbController.swift */,
D8832B1D2AF52D0500C522B0 /* PageViewController.swift */,
B70038842BB33E7700065A38 /* ReplaceContentViewController.swift */,
+ D8AE48C82C57DC2F0092A2A6 /* WebViewController.swift */,
);
path = ViewControllers;
sourceTree = "";
@@ -951,6 +955,7 @@
D8D7BB4A2750067900044146 /* UIAssert.swift in Sources */,
D8F3D057274E574200B56F8C /* LoremIpsumViewController.swift in Sources */,
629EC8AD2B0B537400858855 /* ANRs.swift in Sources */,
+ D8AE48C92C57DC2F0092A2A6 /* WebViewController.swift in Sources */,
D8DBDA78274D5FC400007380 /* SplitViewController.swift in Sources */,
84ACC43C2A73CB5900932A18 /* ProfilingNetworkScanner.swift in Sources */,
D80D021A29EE936F0084393D /* ExtraViewController.swift in Sources */,
@@ -987,6 +992,7 @@
buildActionMask = 2147483647;
files = (
D8832B1A2AF5000F00C522B0 /* TopViewControllerInspector.swift in Sources */,
+ D8AE48CF2C57E0BD0092A2A6 /* WebViewController.swift in Sources */,
D8444E56275F79590042F4DE /* UIViewExtension.swift in Sources */,
D8269A57274C0FA100BD5BD5 /* NibViewController.swift in Sources */,
D8269A4E274C09A400BD5BD5 /* SwiftUIViewController.swift in Sources */,
diff --git a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard
index 016a19ef88c..e3fe04cb43d 100644
--- a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard
+++ b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard
@@ -856,10 +856,10 @@
-
+
-
+
-
-
+
@@ -943,7 +952,7 @@
-
+
@@ -951,7 +960,7 @@
-
+
@@ -959,7 +968,7 @@
-
+
@@ -968,7 +977,7 @@
-
+
@@ -977,7 +986,7 @@
-
+
@@ -986,7 +995,7 @@
-
+
@@ -996,7 +1005,7 @@
-
+
@@ -1010,22 +1019,22 @@
-
+
-
+
@@ -1033,21 +1042,21 @@
-
+
+
_Bad software is everywhere, and we're tired of it. Sentry is on a mission to help developers write better software faster, so we can get back to enjoying technology. If you want to join us [**Check out our open positions**](https://sentry.io/careers/)_
+# Official Sentry SDK for iOS / tvOS / macOS / watchOS (1)
+
[](https://github.com/getsentry/sentry-cocoa/actions/workflows/build.yml?query=branch%3Amain)
[](https://codebeat.co/projects/github-com-getsentry-sentry-cocoa-master)
[](https://codecov.io/gh/getsentry/sentry-cocoa)
From 2911760829e5f46f9451281504a9b40b2142d7b3 Mon Sep 17 00:00:00 2001
From: Dhiogo Brustolin
Date: Tue, 6 Aug 2024 16:23:19 +0200
Subject: [PATCH 30/63] fix build crash after merge (#4237)
Fix framework build
---
Sources/Sentry/Public/SentrySDK.h | 2 +-
Sources/Sentry/SentrySDK.m | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/Sources/Sentry/Public/SentrySDK.h b/Sources/Sentry/Public/SentrySDK.h
index ec9d48f29ec..73b3432dd94 100644
--- a/Sources/Sentry/Public/SentrySDK.h
+++ b/Sources/Sentry/Public/SentrySDK.h
@@ -334,7 +334,7 @@ SENTRY_NO_INIT
*/
+ (void)close;
-#if SENTRY_HAS_UIKIT
+#if SENTRY_TARGET_REPLAY_SUPPORTED
/**
* @warning This is an experimental feature and may still have bugs.
diff --git a/Sources/Sentry/SentrySDK.m b/Sources/Sentry/SentrySDK.m
index b6b5e35d6ed..412b9b69d30 100644
--- a/Sources/Sentry/SentrySDK.m
+++ b/Sources/Sentry/SentrySDK.m
@@ -571,7 +571,7 @@ + (void)stopProfiler
}
#endif // SENTRY_TARGET_PROFILING_SUPPORTED
-#if SENTRY_HAS_UIKIT
+#if SENTRY_TARGET_REPLAY_SUPPORTED
+ (void)replayRedactView:(UIView *)view
{
[SentryRedactViewHelper redactView:view];
From c42dfa31f76463e483db09972c9fe284b631f572 Mon Sep 17 00:00:00 2001
From: Philipp Hofmann
Date: Wed, 7 Aug 2024 11:18:39 +0200
Subject: [PATCH 31/63] impr: Skip enriching scope when nil (#4243)
Skip enriching the scope in the hub initializer when it is nil, as
enrich scope expects a non nil scope.
---
CHANGELOG.md | 1 +
Sources/Sentry/SentryHub.m | 35 ++++++++++---------
Sources/Swift/Tools/UIRedactBuilder.swift | 2 +-
.../SentryCrash/TestSentryCrashWrapper.h | 2 ++
.../SentryCrash/TestSentryCrashWrapper.m | 7 ++++
Tests/SentryTests/SentryHubTests.swift | 26 +++++++++++---
6 files changed, 51 insertions(+), 22 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3827ca8eafc..eed8e8336ba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@
### Improvements
- Reduce memory usage of storing envelopes (#4219)
+- Skip enriching scope when nil (#4243)
## 8.32.0
diff --git a/Sources/Sentry/SentryHub.m b/Sources/Sentry/SentryHub.m
index 7c317152f81..57186a346d7 100644
--- a/Sources/Sentry/SentryHub.m
+++ b/Sources/Sentry/SentryHub.m
@@ -56,10 +56,24 @@ @implementation SentryHub {
- (instancetype)initWithClient:(nullable SentryClient *)client
andScope:(nullable SentryScope *)scope
{
+ return [self initWithClient:client
+ andScope:scope
+ andCrashWrapper:SentryDependencyContainer.sharedInstance.crashWrapper
+ andDispatchQueue:SentryDependencyContainer.sharedInstance.dispatchQueueWrapper];
+}
+
+/** Internal constructor for testing */
+- (instancetype)initWithClient:(nullable SentryClient *)client
+ andScope:(nullable SentryScope *)scope
+ andCrashWrapper:(SentryCrashWrapper *)crashWrapper
+ andDispatchQueue:(SentryDispatchQueueWrapper *)dispatchQueue
+{
+
if (self = [super init]) {
_client = client;
_scope = scope;
- _dispatchQueue = SentryDependencyContainer.sharedInstance.dispatchQueueWrapper;
+ _crashWrapper = crashWrapper;
+ _dispatchQueue = dispatchQueue;
SentryStatsdClient *statsdClient = [[SentryStatsdClient alloc] initWithClient:client];
SentryMetricsClient *metricsClient =
[[SentryMetricsClient alloc] initWithClient:statsdClient];
@@ -76,23 +90,12 @@ - (instancetype)initWithClient:(nullable SentryClient *)client
_integrationsLock = [[NSObject alloc] init];
_installedIntegrations = [[NSMutableArray alloc] init];
_installedIntegrationNames = [[NSMutableSet alloc] init];
- _crashWrapper = [SentryCrashWrapper sharedInstance];
_errorsBeforeSession = 0;
- [SentryDependencyContainer.sharedInstance.crashWrapper enrichScope:scope];
+ if (_scope) {
+ [_crashWrapper enrichScope:_scope];
+ }
}
- return self;
-}
-
-/** Internal constructor for testing */
-- (instancetype)initWithClient:(nullable SentryClient *)client
- andScope:(nullable SentryScope *)scope
- andCrashWrapper:(SentryCrashWrapper *)crashWrapper
- andDispatchQueue:(SentryDispatchQueueWrapper *)dispatchQueue
-{
- self = [self initWithClient:client andScope:scope];
- _crashWrapper = crashWrapper;
- _dispatchQueue = dispatchQueue;
return self;
}
@@ -538,7 +541,7 @@ - (SentryScope *)scope
_scope = [[SentryScope alloc] init];
}
- [SentryDependencyContainer.sharedInstance.crashWrapper enrichScope:_scope];
+ [_crashWrapper enrichScope:_scope];
}
return _scope;
}
diff --git a/Sources/Swift/Tools/UIRedactBuilder.swift b/Sources/Swift/Tools/UIRedactBuilder.swift
index bab12681531..a8efef15903 100644
--- a/Sources/Swift/Tools/UIRedactBuilder.swift
+++ b/Sources/Swift/Tools/UIRedactBuilder.swift
@@ -73,7 +73,7 @@ class UIRedactBuilder {
#else
ignoreClassesIdentifiers = []
#endif
- redactClassesIdentifiers = Set(redactClasses.map( { ObjectIdentifier($0) }))
+ redactClassesIdentifiers = Set(redactClasses.map({ ObjectIdentifier($0) }))
}
func containsIgnoreClass(_ ignoreClass: AnyClass) -> Bool {
diff --git a/Tests/SentryTests/SentryCrash/TestSentryCrashWrapper.h b/Tests/SentryTests/SentryCrash/TestSentryCrashWrapper.h
index 6599019fc75..499ba9822ea 100644
--- a/Tests/SentryTests/SentryCrash/TestSentryCrashWrapper.h
+++ b/Tests/SentryTests/SentryCrash/TestSentryCrashWrapper.h
@@ -35,6 +35,8 @@ SENTRY_NO_INIT
@property (nonatomic) BOOL binaryCacheStopped;
+@property (nonatomic) BOOL enrichScopeCalled;
+
@end
NS_ASSUME_NONNULL_END
diff --git a/Tests/SentryTests/SentryCrash/TestSentryCrashWrapper.m b/Tests/SentryTests/SentryCrash/TestSentryCrashWrapper.m
index 3c0b6f875d7..a8a1db92188 100644
--- a/Tests/SentryTests/SentryCrash/TestSentryCrashWrapper.m
+++ b/Tests/SentryTests/SentryCrash/TestSentryCrashWrapper.m
@@ -17,6 +17,7 @@ + (instancetype)sharedInstance
instance.uninstallAsyncHooksCalled = NO;
instance.internalFreeMemorySize = 0;
instance.internalAppMemorySize = 0;
+ instance.enrichScopeCalled = NO;
return instance;
}
@@ -87,4 +88,10 @@ - (bytes)appMemorySize
return self.internalAppMemorySize;
}
+- (void)enrichScope:(SentryScope *)scope
+{
+ self.enrichScopeCalled = YES;
+ [super enrichScope:scope];
+}
+
@end
diff --git a/Tests/SentryTests/SentryHubTests.swift b/Tests/SentryTests/SentryHubTests.swift
index a9b78a30fd3..60ce83c389d 100644
--- a/Tests/SentryTests/SentryHubTests.swift
+++ b/Tests/SentryTests/SentryHubTests.swift
@@ -17,7 +17,7 @@ class SentryHubTests: XCTestCase {
let message = "some message"
let event: Event
let currentDateProvider = TestCurrentDateProvider()
- let sentryCrash = TestSentryCrashWrapper.sharedInstance()
+ let sentryCrashWrapper = TestSentryCrashWrapper.sharedInstance()
let fileManager: SentryFileManager
let crashedSession: SentrySession
let transactionName = "Some Transaction"
@@ -52,7 +52,7 @@ class SentryHubTests: XCTestCase {
}
func getSut(_ options: Options, _ scope: Scope? = nil) -> SentryHub {
- let hub = SentryHub(client: client, andScope: scope, andCrashWrapper: sentryCrash, andDispatchQueue: self.dispatchQueueWrapper)
+ let hub = SentryHub(client: client, andScope: scope, andCrashWrapper: sentryCrashWrapper, andDispatchQueue: dispatchQueueWrapper)
hub.bindClient(client)
return hub
}
@@ -178,14 +178,30 @@ class SentryHubTests: XCTestCase {
XCTAssertNil(hub.scope.serialize()["breadcrumbs"])
}
- func testScopeEnriched() {
- let hub = fixture.getSut(fixture.options, Scope())
+ func testScopeEnriched_WithInitializer() {
+ let hub = SentryHub(client: nil, andScope: Scope())
XCTAssertFalse(hub.scope.contextDictionary.allValues.isEmpty)
XCTAssertNotNil(hub.scope.contextDictionary["os"])
XCTAssertNotNil(hub.scope.contextDictionary["device"])
XCTAssertNotNil(hub.scope.contextDictionary["app"])
}
+ func testScopeNotEnriched_WhenScopeIsNil() {
+ _ = fixture.getSut()
+
+ XCTAssertFalse(fixture.sentryCrashWrapper.enrichScopeCalled)
+ }
+
+ func testScopeEnriched_WhenCreatingDefaultScope() {
+ let hub = SentryHub(client: nil, andScope: nil)
+
+ let scope = hub.scope
+ XCTAssertFalse(scope.contextDictionary.allValues.isEmpty)
+ XCTAssertNotNil(scope.contextDictionary["os"])
+ XCTAssertNotNil(scope.contextDictionary["device"])
+ XCTAssertNotNil(scope.contextDictionary["app"])
+ }
+
func testAddBreadcrumb_WithCallbackModifies() {
let crumbMessage = "modified"
let options = fixture.options
@@ -1161,7 +1177,7 @@ class SentryHubTests: XCTestCase {
}
private func givenCrashedSession() {
- fixture.sentryCrash.internalCrashedLastLaunch = true
+ fixture.sentryCrashWrapper.internalCrashedLastLaunch = true
fixture.fileManager.storeCrashedSession(fixture.crashedSession)
sut.closeCachedSession(withTimestamp: fixture.currentDateProvider.date())
sut.startSession()
From bb7da60b9b1d1f5efbbaab2a2ed608ea5a04dc99 Mon Sep 17 00:00:00 2001
From: Philipp Hofmann
Date: Wed, 7 Aug 2024 11:26:27 +0200
Subject: [PATCH 32/63] test: Disable logs for verbose tracer tests (#4241)
Disable logs for
testAddingSpansOnDifferentThread_WhileFinishing_DoesNotCrash
because it can logs around 5K log messages.
---
.../Transaction/SentryTracerTests.swift | 190 +++++++++---------
1 file changed, 98 insertions(+), 92 deletions(-)
diff --git a/Tests/SentryTests/Transaction/SentryTracerTests.swift b/Tests/SentryTests/Transaction/SentryTracerTests.swift
index b0fa42f69cf..9c53feac95d 100644
--- a/Tests/SentryTests/Transaction/SentryTracerTests.swift
+++ b/Tests/SentryTests/Transaction/SentryTracerTests.swift
@@ -207,38 +207,39 @@ class SentryTracerTests: XCTestCase {
/// to a crash when spans keep finishing while finishInternal is executed because
/// shouldIgnoreWaitForChildrenCallback could be then nil in hasUnfinishedChildSpansToWaitFor.
func testFinish_ShouldIgnoreWaitForChildrenCallback_DoesNotCrash() throws {
-
- for _ in 0..<5 {
- let sut = fixture.getSut()
-
- let dispatchQueue = DispatchQueue(label: "test", attributes: [.concurrent, .initiallyInactive])
-
- let expectation = expectation(description: "call everything")
- expectation.expectedFulfillmentCount = 11
-
- sut.shouldIgnoreWaitForChildrenCallback = { _ in
- return true
- }
-
- for _ in 0..<1_000 {
- let child = sut.startChild(operation: self.fixture.transactionOperation)
- child.finish()
- }
-
- dispatchQueue.async {
- for _ in 0..<10 {
+ SentryLog.withOutLogs {
+ for _ in 0..<5 {
+ let sut = fixture.getSut()
+
+ let dispatchQueue = DispatchQueue(label: "test", attributes: [.concurrent, .initiallyInactive])
+
+ let expectation = expectation(description: "call everything")
+ expectation.expectedFulfillmentCount = 11
+
+ sut.shouldIgnoreWaitForChildrenCallback = { _ in
+ return true
+ }
+
+ for _ in 0..<1_000 {
let child = sut.startChild(operation: self.fixture.transactionOperation)
child.finish()
+ }
+
+ dispatchQueue.async {
+ for _ in 0..<10 {
+ let child = sut.startChild(operation: self.fixture.transactionOperation)
+ child.finish()
+ expectation.fulfill()
+ }
+ }
+ dispatchQueue.async {
+ sut.finish()
expectation.fulfill()
}
+
+ dispatchQueue.activate()
+ wait(for: [expectation], timeout: 1.0)
}
- dispatchQueue.async {
- sut.finish()
- expectation.fulfill()
- }
-
- dispatchQueue.activate()
- wait(for: [expectation], timeout: 1.0)
}
}
@@ -1083,39 +1084,41 @@ class SentryTracerTests: XCTestCase {
}
func testFinishAsync() throws {
- let sut = fixture.getSut()
- let child = sut.startChild(operation: fixture.transactionOperation)
- sut.finish()
-
- let queue = DispatchQueue(label: "SentryTracerTests", attributes: [.concurrent, .initiallyInactive])
- let group = DispatchGroup()
-
- let children = 5
- let grandchildren = 10
- for _ in 0 ..< children {
- group.enter()
- queue.async {
- let grandChild = child.startChild(operation: self.fixture.transactionOperation)
- for _ in 0 ..< grandchildren {
- let grandGrandChild = grandChild.startChild(operation: self.fixture.transactionOperation)
- grandGrandChild.finish()
+ try SentryLog.withOutLogs {
+ let sut = fixture.getSut()
+ let child = sut.startChild(operation: fixture.transactionOperation)
+ sut.finish()
+
+ let queue = DispatchQueue(label: "SentryTracerTests", attributes: [.concurrent, .initiallyInactive])
+ let group = DispatchGroup()
+
+ let children = 5
+ let grandchildren = 10
+ for _ in 0 ..< children {
+ group.enter()
+ queue.async {
+ let grandChild = child.startChild(operation: self.fixture.transactionOperation)
+ for _ in 0 ..< grandchildren {
+ let grandGrandChild = grandChild.startChild(operation: self.fixture.transactionOperation)
+ grandGrandChild.finish()
+ }
+
+ grandChild.finish()
+ self.assertTransactionNotCaptured(sut)
+ group.leave()
}
-
- grandChild.finish()
- self.assertTransactionNotCaptured(sut)
- group.leave()
}
+
+ queue.activate()
+ group.wait()
+
+ child.finish()
+
+ assertOneTransactionCaptured(sut)
+
+ let spans = try XCTUnwrap(try getSerializedTransaction()["spans"]! as? [[String: Any]])
+ XCTAssertEqual(spans.count, children * (grandchildren + 1) + 1)
}
-
- queue.activate()
- group.wait()
-
- child.finish()
-
- assertOneTransactionCaptured(sut)
-
- let spans = try XCTUnwrap(try getSerializedTransaction()["spans"]! as? [[String: Any]])
- XCTAssertEqual(spans.count, children * (grandchildren + 1) + 1)
}
#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst)
@@ -1152,44 +1155,47 @@ class SentryTracerTests: XCTestCase {
#endif // os(iOS) || os(tvOS) || targetEnvironment(macCatalyst)
func testAddingSpansOnDifferentThread_WhileFinishing_DoesNotCrash() throws {
- let sut = fixture.getSut(waitForChildren: false)
-
- let children = 1_000
- for _ in 0..
Date: Wed, 7 Aug 2024 20:33:25 +1000
Subject: [PATCH 33/63] Add include for missing ucontext64_t type (#4244)
* Add include for missing ucontext64_t type
* Fix changelog
---------
Co-authored-by: Dhiogo Brustolin
---
CHANGELOG.md | 1 +
Sources/SentryCrash/Recording/Tools/SentryCrashMachineContext.c | 1 +
2 files changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index eed8e8336ba..7a4ea41cb85 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@
- Missing mach info for crash reports (#4230)
- Crash reports not generated on visionOS (#4229)
- Don’t force cast to `NSComparisonPredicate` in TERNARY operator (#4232)
+- Missing '#include ' (#4244)
### Improvements
diff --git a/Sources/SentryCrash/Recording/Tools/SentryCrashMachineContext.c b/Sources/SentryCrash/Recording/Tools/SentryCrashMachineContext.c
index 222aa41bc3f..566c9eea1d6 100644
--- a/Sources/SentryCrash/Recording/Tools/SentryCrashMachineContext.c
+++ b/Sources/SentryCrash/Recording/Tools/SentryCrashMachineContext.c
@@ -38,6 +38,7 @@
#include "SentryAsyncSafeLog.h"
#ifdef __arm64__
+#include
# define UC_MCONTEXT uc_mcontext64
typedef ucontext64_t SignalUserContext;
#else
From 4b1113e1728cb62d69c8bf45b61afbb8cc66c070 Mon Sep 17 00:00:00 2001
From: Philipp Hofmann
Date: Wed, 7 Aug 2024 15:00:49 +0200
Subject: [PATCH 34/63] chore: Add primer for important bug fix (#4246)
Add a primer to update to 8.33.0 for an important bug fix for release
health.
Co-authored-by: Karl Heinz Struggl
---
CHANGELOG.md | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7a4ea41cb85..9153c683430 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,11 @@
## Unreleased
+This release fixes an essential bug (#4230) for release health and unhandled/crash events that we introduced with a refactoring (#4101) released in [8.30.1](https://github.com/getsentry/sentry-cocoa/releases/tag/8.30.1).
+This bug caused unhandled/crash events to have the unhandled property and mach info missing, which is required for release health to show events in the unhandled tab.
+This release fixes an essential bug (#4230) for release health and unhandled/crash events that we introduced with a refactoring (#4101) released in [8.30.0](https://github.com/getsentry/sentry-cocoa/releases/tag/8.30.0).
+This bug caused unhandled/crash events to have the unhandled property and mach info missing, which is required for release health to show events in the unhandled tab. It's essential to mention that this bug doesn't impact release health statistics, such as crash-free session or user rates.
+
### Features
- Support orientation change for session replay (#4194)
From 6a6a5b2ab76e4e6b6b1f40805ec743d9fb26f226 Mon Sep 17 00:00:00 2001
From: Philipp Hofmann
Date: Wed, 7 Aug 2024 15:18:43 +0200
Subject: [PATCH 35/63] fix: EXC_BAD_ACCESS in SentryMetricProfiler (#4242)
Add missing synchronize block around the clear method.
---
CHANGELOG.md | 1 +
Sources/Sentry/SentryMetricProfiler.mm | 10 +++++++++-
.../Recording/Tools/SentryCrashMachineContext.c | 2 +-
3 files changed, 11 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9153c683430..0daf2eb7f77 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,7 @@ This bug caused unhandled/crash events to have the unhandled property and mach i
- Missing mach info for crash reports (#4230)
- Crash reports not generated on visionOS (#4229)
- Don’t force cast to `NSComparisonPredicate` in TERNARY operator (#4232)
+- EXC_BAD_ACCESS in SentryMetricProfiler (#4242)
- Missing '#include ' (#4244)
### Improvements
diff --git a/Sources/Sentry/SentryMetricProfiler.mm b/Sources/Sentry/SentryMetricProfiler.mm
index 2ac24c1c5cb..64fdade4d24 100644
--- a/Sources/Sentry/SentryMetricProfiler.mm
+++ b/Sources/Sentry/SentryMetricProfiler.mm
@@ -113,7 +113,8 @@ @implementation SentryMetricProfiler {
- (instancetype)initWithMode:(SentryProfilerMode)mode
{
if (self = [super init]) {
- [self clear];
+ // It doesn't make sense to acquire a lock in the init.
+ [self clearNotThreadSafe];
_mode = mode;
}
return self;
@@ -210,6 +211,13 @@ - (void)stop
}
- (void)clear
+{
+ @synchronized(self) {
+ [self clearNotThreadSafe];
+ }
+}
+
+- (void)clearNotThreadSafe
{
_cpuUsage = [NSMutableArray array];
_memoryFootprint = [NSMutableArray array];
diff --git a/Sources/SentryCrash/Recording/Tools/SentryCrashMachineContext.c b/Sources/SentryCrash/Recording/Tools/SentryCrashMachineContext.c
index 566c9eea1d6..f22f0b0536f 100644
--- a/Sources/SentryCrash/Recording/Tools/SentryCrashMachineContext.c
+++ b/Sources/SentryCrash/Recording/Tools/SentryCrashMachineContext.c
@@ -38,7 +38,7 @@
#include "SentryAsyncSafeLog.h"
#ifdef __arm64__
-#include
+# include
# define UC_MCONTEXT uc_mcontext64
typedef ucontext64_t SignalUserContext;
#else
From 46467d08c03a17625050d538c7b6d845b69f74c8 Mon Sep 17 00:00:00 2001
From: Karl Heinz Struggl
Date: Wed, 7 Aug 2024 06:25:22 -0700
Subject: [PATCH 36/63] Update CHANGELOG.md (#4250)
---
CHANGELOG.md | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0daf2eb7f77..f7be562ef5a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,10 +2,8 @@
## Unreleased
-This release fixes an essential bug (#4230) for release health and unhandled/crash events that we introduced with a refactoring (#4101) released in [8.30.1](https://github.com/getsentry/sentry-cocoa/releases/tag/8.30.1).
-This bug caused unhandled/crash events to have the unhandled property and mach info missing, which is required for release health to show events in the unhandled tab.
-This release fixes an essential bug (#4230) for release health and unhandled/crash events that we introduced with a refactoring (#4101) released in [8.30.0](https://github.com/getsentry/sentry-cocoa/releases/tag/8.30.0).
-This bug caused unhandled/crash events to have the unhandled property and mach info missing, which is required for release health to show events in the unhandled tab. It's essential to mention that this bug doesn't impact release health statistics, such as crash-free session or user rates.
+This release fixes a bug (#4230) that we introduced with a refactoring (#4101) released in [8.30.1](https://github.com/getsentry/sentry-cocoa/releases/tag/8.30.1).
+This bug caused unhandled/crash events to have the unhandled property and mach info missing, which is required for release health to show events in the unhandled tab. It's essential to mention that this bug **doesn't impact** release health statistics, such as crash-free session or user rates.
### Features
From 676fe227fbfbe641a1754c448a356801cbd24553 Mon Sep 17 00:00:00 2001
From: Philipp Hofmann
Date: Wed, 7 Aug 2024 16:44:36 +0200
Subject: [PATCH 37/63] test: Stop init tracer for all HttpTransportTests
(#4252)
Only init the transaction including the transaction for the transaction
envelope for the tests that use it, because the tracer initializes the
SentryFramesTracker and might have other side effects.
---
.../Networking/SentryHttpTransportTests.swift | 50 +++++++++----------
1 file changed, 25 insertions(+), 25 deletions(-)
diff --git a/Tests/SentryTests/Networking/SentryHttpTransportTests.swift b/Tests/SentryTests/Networking/SentryHttpTransportTests.swift
index 282a56d5b07..527a1fd92f1 100644
--- a/Tests/SentryTests/Networking/SentryHttpTransportTests.swift
+++ b/Tests/SentryTests/Networking/SentryHttpTransportTests.swift
@@ -11,11 +11,7 @@ class SentryHttpTransportTests: XCTestCase {
let event: Event
let eventEnvelope: SentryEnvelope
let eventRequest: SentryNSURLRequest
- let transaction: Transaction
- let transactionEnvelope: SentryEnvelope
- let transactionRequest: SentryNSURLRequest
let attachmentEnvelopeItem: SentryEnvelopeItem
- let transactionEnvelopeItem: SentryEnvelopeItem
let eventWithAttachmentRequest: SentryNSURLRequest
let eventWithSessionEnvelope: SentryEnvelope
let eventWithSessionRequest: SentryNSURLRequest
@@ -57,32 +53,16 @@ class SentryHttpTransportTests: XCTestCase {
event = Event()
event.message = SentryMessage(formatted: "Some message")
-
- let tracer = SentryTracer(transactionContext: TransactionContext(name: "SomeTransaction", operation: "SomeOperation"), hub: nil)
- transaction = Transaction(
- trace: tracer,
- children: [
- tracer.startChild(operation: "child1"),
- tracer.startChild(operation: "child2"),
- tracer.startChild(operation: "child3")
- ]
- )
eventRequest = buildRequest(SentryEnvelope(event: event))
- transactionRequest = buildRequest(SentryEnvelope(event: transaction))
attachmentEnvelopeItem = SentryEnvelopeItem(attachment: TestData.dataAttachment, maxAttachmentSize: 5 * 1_024 * 1_024)!
- transactionEnvelopeItem = SentryEnvelopeItem(event: transaction)
eventEnvelope = SentryEnvelope(id: event.eventId, items: [SentryEnvelopeItem(event: event), attachmentEnvelopeItem])
// We are comparing byte data and the `sentAt` header is also set in the transport, so we also need them here in the expected envelope.
eventEnvelope.header.sentAt = SentryDependencyContainer.sharedInstance().dateProvider.date()
eventWithAttachmentRequest = buildRequest(eventEnvelope)
- transactionEnvelope = SentryEnvelope(id: transaction.eventId, items: [SentryEnvelopeItem(event: transaction), attachmentEnvelopeItem])
- // We are comparing byte data and the `sentAt` header is also set in the transport, so we also need them here in the expected envelope.
- transactionEnvelope.header.sentAt = SentryDependencyContainer.sharedInstance().dateProvider.date()
-
session = SentrySession(releaseName: "2.0.1", distinctId: "some-id")
sessionEnvelope = SentryEnvelope(id: nil, singleItem: SentryEnvelopeItem(session: session))
sessionEnvelope.header.sentAt = SentryDependencyContainer.sharedInstance().dateProvider.date()
@@ -126,6 +106,24 @@ class SentryHttpTransportTests: XCTestCase {
clientReportEnvelope.header.sentAt = SentryDependencyContainer.sharedInstance().dateProvider.date()
clientReportRequest = buildRequest(clientReportEnvelope)
}
+
+ func getTransactionEnvelope() -> SentryEnvelope {
+ let tracer = SentryTracer(transactionContext: TransactionContext(name: "SomeTransaction", operation: "SomeOperation"), hub: nil)
+ let transaction = Transaction(
+ trace: tracer,
+ children: [
+ tracer.startChild(operation: "child1"),
+ tracer.startChild(operation: "child2"),
+ tracer.startChild(operation: "child3")
+ ]
+ )
+
+ let transactionEnvelope = SentryEnvelope(id: transaction.eventId, items: [SentryEnvelopeItem(event: transaction), attachmentEnvelopeItem])
+ // We are comparing byte data and the `sentAt` header is also set in the transport, so we also need them here in the expected envelope.
+ transactionEnvelope.header.sentAt = currentDateProvider.date()
+
+ return transactionEnvelope
+ }
func getSut(dispatchQueueWrapper: SentryDispatchQueueWrapper? = nil) -> SentryHttpTransport {
@@ -565,16 +563,18 @@ class SentryHttpTransportTests: XCTestCase {
SentryEnvelopeItem(clientReport: clientReport)
]
- let clientReportEnvelope = SentryEnvelope(id: fixture.transaction.eventId, items: clientReportEnvelopeItems)
+ let transactionEnvelope = fixture.getTransactionEnvelope()
+
+ let clientReportEnvelope = SentryEnvelope(id: transactionEnvelope.header.eventId, items: clientReportEnvelopeItems)
clientReportEnvelope.header.sentAt = SentryDependencyContainer.sharedInstance().dateProvider.date()
let clientReportRequest = SentryHttpTransportTests.buildRequest(clientReportEnvelope)
givenRateLimitResponse(forCategory: "transaction")
- sut.send(envelope: fixture.transactionEnvelope)
+ sut.send(envelope: transactionEnvelope)
waitForAllRequests()
- sut.send(envelope: fixture.transactionEnvelope)
+ sut.send(envelope: transactionEnvelope)
waitForAllRequests()
let actualEventRequest = fixture.requestManager.requests.last
@@ -602,7 +602,7 @@ class SentryHttpTransportTests: XCTestCase {
func testCacheFull_RecordsLostSpans() {
givenNoInternetConnection()
for _ in 0...fixture.options.maxCacheItems {
- sut.send(envelope: fixture.transactionEnvelope)
+ sut.send(envelope: fixture.getTransactionEnvelope())
}
waitForAllRequests()
@@ -1032,7 +1032,7 @@ class SentryHttpTransportTests: XCTestCase {
}
private func sendTransactionAsync() {
- sut.send(envelope: fixture.transactionEnvelope)
+ sut.send(envelope: fixture.getTransactionEnvelope())
}
private func sendEnvelope(envelope: SentryEnvelope = TestConstants.envelope) {
From 5d6bf0e1c495b9ae5d80a978f653893d4efe5f38 Mon Sep 17 00:00:00 2001
From: Philipp Hofmann
Date: Wed, 7 Aug 2024 16:45:30 +0200
Subject: [PATCH 38/63] test: Fix testSkipCrumbs_WhenSenderOrTargetIsNil
(#4253)
The testSkipCrumbs_WhenSenderOrTargetIsNil failed, because a
connectivity crumb popped up during the test. This is fixed now by
only validating that the crumbs don't contain any touch crumbs.
---
.../Breadcrumbs/SentryBreadcrumbTrackerTests.swift | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/Tests/SentryTests/Integrations/Breadcrumbs/SentryBreadcrumbTrackerTests.swift b/Tests/SentryTests/Integrations/Breadcrumbs/SentryBreadcrumbTrackerTests.swift
index 0825086c031..369cb0ce133 100644
--- a/Tests/SentryTests/Integrations/Breadcrumbs/SentryBreadcrumbTrackerTests.swift
+++ b/Tests/SentryTests/Integrations/Breadcrumbs/SentryBreadcrumbTrackerTests.swift
@@ -195,8 +195,10 @@ class SentryBreadcrumbTrackerTests: XCTestCase {
swizzlingWrapper.execute(action: "methodPressed:", target: nil, sender: self, event: nil)
swizzlingWrapper.execute(action: "methodPressed:", target: self, sender: nil, event: nil)
- // The tracker adds 1 enabled crumb when being started
- XCTAssertEqual(1, delegate.addCrumbInvocations.invocations.count)
+ let touchCrumbs = delegate.addCrumbInvocations.invocations.filter { crumb in
+ return crumb.category == "touch"
+ }
+ XCTAssertEqual(0, touchCrumbs.count)
}
func testTouchBreadcrumbForSessionReplay() throws {
From ebeb68cb0fb0d32e292c194a1e74316a40f30465 Mon Sep 17 00:00:00 2001
From: Andrew McKnight
Date: Wed, 7 Aug 2024 15:36:46 -0800
Subject: [PATCH 39/63] impr(profiling): add sdk name and version to profile
chunks (#4215)
---
Sources/Sentry/Profiling/SentryProfilerSerialization.mm | 6 ++++++
.../SentryProfilerTests/SentryContinuousProfilerTests.swift | 4 ++++
2 files changed, 10 insertions(+)
diff --git a/Sources/Sentry/Profiling/SentryProfilerSerialization.mm b/Sources/Sentry/Profiling/SentryProfilerSerialization.mm
index 24f38cbe15f..eff964d19f9 100644
--- a/Sources/Sentry/Profiling/SentryProfilerSerialization.mm
+++ b/Sources/Sentry/Profiling/SentryProfilerSerialization.mm
@@ -16,6 +16,7 @@
# import "SentryHub.h"
# import "SentryInternalDefines.h"
# import "SentryLog.h"
+# import "SentryMeta.h"
# import "SentryMetricProfiler.h"
# import "SentryOptions.h"
# import "SentryProfileTimeseries.h"
@@ -260,6 +261,11 @@
payload[@"release"] = hub.getClient.options.releaseName;
payload[@"platform"] = SentryPlatformName;
+ const auto clientInfo = [NSMutableDictionary dictionary];
+ clientInfo[@"name"] = SentryMeta.sdkName;
+ clientInfo[@"version"] = SentryMeta.versionString;
+ payload[@"client_sdk"] = clientInfo;
+
// add the gathered metrics
auto metrics = serializedMetrics;
diff --git a/Tests/SentryProfilerTests/SentryContinuousProfilerTests.swift b/Tests/SentryProfilerTests/SentryContinuousProfilerTests.swift
index 98f5af5a836..98e2e6b3c01 100644
--- a/Tests/SentryProfilerTests/SentryContinuousProfilerTests.swift
+++ b/Tests/SentryProfilerTests/SentryContinuousProfilerTests.swift
@@ -184,6 +184,10 @@ private extension SentryContinuousProfilerTests {
XCTAssertFalse(try XCTUnwrap(firstImage["image_addr"] as? String).isEmpty)
XCTAssertGreaterThan(try XCTUnwrap(firstImage["image_size"] as? Int), 0)
XCTAssertEqual(try XCTUnwrap(firstImage["type"] as? String), "macho")
+
+ let clientInfo = try XCTUnwrap(profile["client_sdk"] as? [String: String])
+ XCTAssertEqual(try XCTUnwrap(clientInfo["name"]), SentryMeta.sdkName)
+ XCTAssertEqual(try XCTUnwrap(clientInfo["version"]), SentryMeta.versionString)
let sampledProfile = try XCTUnwrap(profile["profile"] as? [String: Any])
let threadMetadata = try XCTUnwrap(sampledProfile["thread_metadata"] as? [String: [String: Any]])
From 7219f3bd868c6adff01372c1e6e47623e28f31d7 Mon Sep 17 00:00:00 2001
From: Andrew McKnight
Date: Wed, 7 Aug 2024 16:07:28 -0800
Subject: [PATCH 40/63] chore(SentryNSNotificationCenterWrapper): add block
observation (#4234)
---
.../TestNSNotificationCenterWrapper.swift | 16 ++++++++--
.../SentryNSNotificationCenterWrapper.m | 13 ++++++++-
.../SentryNSNotificationCenterWrapper.h | 16 ++++++++--
...ntryNSNotificationCenterWrapperTests.swift | 29 ++++++++++++++++---
4 files changed, 64 insertions(+), 10 deletions(-)
diff --git a/SentryTestUtils/TestNSNotificationCenterWrapper.swift b/SentryTestUtils/TestNSNotificationCenterWrapper.swift
index 8a03b2c709e..90f8a336820 100644
--- a/SentryTestUtils/TestNSNotificationCenterWrapper.swift
+++ b/SentryTestUtils/TestNSNotificationCenterWrapper.swift
@@ -16,12 +16,19 @@ import Sentry
addObserverInvocations.record((WeakReference(value: observer), aSelector, aName))
}
}
+
+ public var addObserverWithBlockInvocations = Invocations<(observer: WeakReference, name: NSNotification.Name?, block: (Notification) -> Void)>()
+ public override func addObserver(forName name: NSNotification.Name?, object obj: Any?, queue: OperationQueue?, using block: @escaping (Notification) -> Void) -> any NSObjectProtocol {
+ let observer = NSObject()
+ addObserverWithBlockInvocations.record((WeakReference(value: observer), name, block))
+ return observer
+ }
public var removeObserverWithNameInvocations = Invocations< NSNotification.Name>()
public var removeObserverWithNameInvocationsCount: Int {
return removeObserverWithNameInvocations.count
}
- public override func removeObserver(_ observer: NSObject, name aName: NSNotification.Name) {
+ public override func removeObserver(_ observer: any NSObjectProtocol, name aName: NSNotification.Name) {
removeObserverWithNameInvocations.record(aName)
}
@@ -32,7 +39,7 @@ import Sentry
public var removeObserverInvocationsCount: Int {
return removeObserverInvocations.count
}
- public override func removeObserver(_ observer: NSObject) {
+ public override func removeObserver(_ observer: any NSObjectProtocol) {
if ignoreRemoveObserver == false {
removeObserverInvocations.record(Void())
}
@@ -44,5 +51,10 @@ import Sentry
.forEach { observer, selector, _ in
_ = observer.value?.perform(selector, with: nil)
}
+ addObserverWithBlockInvocations.invocations.forEach { _, name, block in
+ if let name = name {
+ block(Notification(name: name))
+ }
+ }
}
}
diff --git a/Sources/Sentry/SentryNSNotificationCenterWrapper.m b/Sources/Sentry/SentryNSNotificationCenterWrapper.m
index 3ab71f31923..00ea6675b16 100644
--- a/Sources/Sentry/SentryNSNotificationCenterWrapper.m
+++ b/Sources/Sentry/SentryNSNotificationCenterWrapper.m
@@ -66,6 +66,17 @@ - (void)addObserver:(NSObject *)observer selector:(SEL)aSelector name:(NSNotific
object:nil];
}
+- (id)addObserverForName:(nullable NSNotificationName)name
+ object:(nullable id)obj
+ queue:(nullable NSOperationQueue *)queue
+ usingBlock:(void (^)(NSNotification *notification))block
+{
+ return [NSNotificationCenter.defaultCenter addObserverForName:name
+ object:obj
+ queue:queue
+ usingBlock:block];
+}
+
- (void)removeObserver:(NSObject *)observer name:(NSNotificationName)aName
{
[NSNotificationCenter.defaultCenter removeObserver:observer name:aName object:nil];
@@ -78,7 +89,7 @@ - (void)removeObserver:(NSObject *)observer
[NSNotificationCenter.defaultCenter removeObserver:observer name:aName object:anObject];
}
-- (void)removeObserver:(NSObject *)observer
+- (void)removeObserver:(id)observer
{
[NSNotificationCenter.defaultCenter removeObserver:observer];
}
diff --git a/Sources/Sentry/include/SentryNSNotificationCenterWrapper.h b/Sources/Sentry/include/SentryNSNotificationCenterWrapper.h
index 6dfe416cfa1..72e3e3c3697 100644
--- a/Sources/Sentry/include/SentryNSNotificationCenterWrapper.h
+++ b/Sources/Sentry/include/SentryNSNotificationCenterWrapper.h
@@ -25,13 +25,23 @@ NS_ASSUME_NONNULL_BEGIN
- (void)addObserver:(NSObject *)observer selector:(SEL)aSelector name:(NSNotificationName)aName;
-- (void)removeObserver:(NSObject *)observer
+/**
+ * @note Per NSNotificationCenter's docs: The return value is retained by the system, and should be
+ * held onto by the caller in order to remove the observer with removeObserver: later, to stop
+ * observation.
+ */
+- (id)addObserverForName:(nullable NSNotificationName)name
+ object:(nullable id)obj
+ queue:(nullable NSOperationQueue *)queue
+ usingBlock:(void (^)(NSNotification *notification))block;
+
+- (void)removeObserver:(id)observer
name:(NSNotificationName)aName
object:(nullable id)anObject;
-- (void)removeObserver:(NSObject *)observer name:(NSNotificationName)aName;
+- (void)removeObserver:(id)observer name:(NSNotificationName)aName;
-- (void)removeObserver:(NSObject *)observer;
+- (void)removeObserver:(id)observer;
- (void)postNotification:(NSNotification *)notification;
diff --git a/Tests/SentryTests/Helper/SentryNSNotificationCenterWrapperTests.swift b/Tests/SentryTests/Helper/SentryNSNotificationCenterWrapperTests.swift
index 3133d4d250a..ec505fc8b2d 100644
--- a/Tests/SentryTests/Helper/SentryNSNotificationCenterWrapperTests.swift
+++ b/Tests/SentryTests/Helper/SentryNSNotificationCenterWrapperTests.swift
@@ -15,10 +15,6 @@ class SentryNSNotificationCenterWrapperTests: XCTestCase {
super.setUp()
sut = SentryNSNotificationCenterWrapper()
-
- didBecomeActiveExpectation = expectation(description: "didBecomeActive")
- willResignActiveExpectation = expectation(description: "willResignActive")
- willResignActiveExpectation.isInverted = true
}
override func tearDown() {
@@ -28,6 +24,7 @@ class SentryNSNotificationCenterWrapperTests: XCTestCase {
}
func testAddObserver() {
+ addDefaultExpectations()
sut.addObserver(self, selector: #selector(didBecomeActive), name: didBecomeActiveNotification)
NotificationCenter.default.post(Notification(name: didBecomeActiveNotification))
@@ -35,7 +32,23 @@ class SentryNSNotificationCenterWrapperTests: XCTestCase {
wait(for: [didBecomeActiveExpectation, willResignActiveExpectation], timeout: 0.5)
}
+ func testAddObserverWithBlock() {
+ let exp = expectation(description: "received notification block callback")
+ var observer: NSObject?
+ observer = sut.addObserver(forName: didBecomeActiveNotification, object: nil, queue: nil) { _ in
+ do {
+ self.sut.removeObserver(try XCTUnwrap(observer))
+ } catch {
+ XCTFail("notification observer was not correctly retained")
+ }
+ exp.fulfill()
+ } as? NSObject
+ NotificationCenter.default.post(.init(name: didBecomeActiveNotification))
+ wait(for: [exp], timeout: 0.5)
+ }
+
func testRemoveSpecificObserver() {
+ addDefaultExpectations()
sut.addObserver(self, selector: #selector(didBecomeActive), name: didBecomeActiveNotification)
sut.addObserver(self, selector: #selector(willResignActive), name: willResignActiveNotification)
@@ -46,6 +59,7 @@ class SentryNSNotificationCenterWrapperTests: XCTestCase {
}
func testRemoveObserver() {
+ addDefaultExpectations()
didBecomeActiveExpectation.isInverted = true
sut.addObserver(self, selector: #selector(didBecomeActive), name: didBecomeActiveNotification)
@@ -58,6 +72,7 @@ class SentryNSNotificationCenterWrapperTests: XCTestCase {
}
func testPostNotificationsOnMock() {
+ addDefaultExpectations()
let sut = TestNSNotificationCenterWrapper()
sut.addObserver(self, selector: #selector(didBecomeActive), name: didBecomeActiveNotification)
sut.post(Notification(name: didBecomeActiveNotification))
@@ -73,4 +88,10 @@ class SentryNSNotificationCenterWrapperTests: XCTestCase {
func willResignActive() {
willResignActiveExpectation.fulfill()
}
+
+ func addDefaultExpectations() {
+ didBecomeActiveExpectation = expectation(description: "didBecomeActive")
+ willResignActiveExpectation = expectation(description: "willResignActive")
+ willResignActiveExpectation.isInverted = true
+ }
}
From 65e8a5b36f7aec8a6b8bdfdb1c7c4b04214201f1 Mon Sep 17 00:00:00 2001
From: Andrew McKnight
Date: Wed, 7 Aug 2024 17:06:24 -0800
Subject: [PATCH 41/63] fix(continuous profiling): full chunk stoppage (#4214)
---
SentryTestUtils/ClearTestState.swift | 5 +-
.../SentryTestUtils-ObjC-BridgingHeader.h | 1 +
.../Profiling/SentryContinuousProfiler.mm | 71 +++++++++++++++----
Sources/Sentry/SentryProfiler.mm | 10 +--
Sources/Sentry/include/SentrySDK+Private.h | 4 +-
.../SentryContinuousProfilerTests.swift | 35 ++++++++-
.../SentryContinuousProfiler+Test.h | 1 +
7 files changed, 108 insertions(+), 19 deletions(-)
diff --git a/SentryTestUtils/ClearTestState.swift b/SentryTestUtils/ClearTestState.swift
index 5f324cc5591..c6d346f34ee 100644
--- a/SentryTestUtils/ClearTestState.swift
+++ b/SentryTestUtils/ClearTestState.swift
@@ -43,9 +43,12 @@ class TestCleanup: NSObject {
_sentry_threadUnsafe_traceProfileTimeoutTimer = nil
SentryTraceProfiler.getCurrentProfiler()?.stop(for: SentryProfilerTruncationReason.normal)
SentryTraceProfiler.resetConcurrencyTracking()
- SentryContinuousProfiler.stop()
removeAppLaunchProfilingConfigFile()
sentry_stopAndDiscardLaunchProfileTracer()
+
+ if SentryContinuousProfiler.isCurrentlyProfiling() {
+ SentryContinuousProfiler.stopTimerAndCleanup()
+ }
#endif // os(iOS) || os(macOS) || targetEnvironment(macCatalyst)
#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst)
diff --git a/SentryTestUtils/SentryTestUtils-ObjC-BridgingHeader.h b/SentryTestUtils/SentryTestUtils-ObjC-BridgingHeader.h
index 152c899767d..5696b8d04af 100644
--- a/SentryTestUtils/SentryTestUtils-ObjC-BridgingHeader.h
+++ b/SentryTestUtils/SentryTestUtils-ObjC-BridgingHeader.h
@@ -17,6 +17,7 @@
#import "SentryProfilingConditionals.h"
#if SENTRY_TARGET_PROFILING_SUPPORTED
+# import "SentryContinuousProfiler+Test.h"
# import "SentryContinuousProfiler.h"
# import "SentryLaunchProfiling.h"
# import "SentryProfiler+Private.h"
diff --git a/Sources/Sentry/Profiling/SentryContinuousProfiler.mm b/Sources/Sentry/Profiling/SentryContinuousProfiler.mm
index e3cacd4e933..30a2d41a7fd 100644
--- a/Sources/Sentry/Profiling/SentryContinuousProfiler.mm
+++ b/Sources/Sentry/Profiling/SentryContinuousProfiler.mm
@@ -19,6 +19,7 @@
# if SENTRY_HAS_UIKIT
# import "SentryFramesTracker.h"
# import "SentryScreenFrames.h"
+# import
# endif // SENTRY_HAS_UIKIT
# pragma mark - Private
@@ -35,6 +36,16 @@
/** @note: The session ID is reused for any profile sessions started in the same app session. */
SentryId *_profileSessionID;
+/**
+ * To avoid sending small chunks at the end of profiles, we let the current chunk run to the full
+ * time after the call to stop the profiler is received.
+ * */
+BOOL _stopCalled;
+
+# if SENTRY_HAS_UIKIT
+NSObject *_observerToken;
+# endif // SENTRY_HAS_UIKIT
+
void
disableTimer()
{
@@ -90,6 +101,8 @@ + (void)start
return;
}
+ _stopCalled = NO;
+
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{ _profileSessionID = [[SentryId alloc] init]; });
_threadUnsafe_gContinuousCurrentProfiler.profilerId = _profileSessionID;
@@ -101,6 +114,18 @@ + (void)start
object:nil
userInfo:nil]];
[self scheduleTimer];
+
+# if SENTRY_HAS_UIKIT
+ _observerToken = [SentryDependencyContainer.sharedInstance.notificationCenterWrapper
+ addObserverForName:UIApplicationWillResignActiveNotification
+ object:nil
+ queue:nil
+ usingBlock:^(NSNotification *_Nonnull notification) {
+ [SentryDependencyContainer.sharedInstance.notificationCenterWrapper
+ removeObserver:_observerToken];
+ [self stopTimerAndCleanup];
+ }];
+# endif // SENTRY_HAS_UIKIT
}
+ (BOOL)isCurrentlyProfiling
@@ -119,12 +144,7 @@ + (void)stop
return;
}
- _sentry_threadUnsafe_transmitChunkEnvelope();
- disableTimer();
-
- [_threadUnsafe_gContinuousCurrentProfiler
- stopForReason:SentryProfilerTruncationReasonNormal];
- _threadUnsafe_gContinuousCurrentProfiler = nil;
+ _stopCalled = YES;
}
}
@@ -145,6 +165,8 @@ + (void)scheduleTimer
[SentryDependencyContainer.sharedInstance.dispatchQueueWrapper dispatchAsyncOnMainQueue:^{
std::lock_guard l(_threadUnsafe_gContinuousProfilerLock);
if (_chunkTimer != nil) {
+ SENTRY_LOG_WARN(@"There was already a timer in flight, but this codepath shouldn't be "
+ @"taken if there is no profiler running.");
return;
}
@@ -159,14 +181,39 @@ + (void)scheduleTimer
+ (void)timerExpired
{
- std::lock_guard l(_threadUnsafe_gContinuousProfilerLock);
- if (![_threadUnsafe_gContinuousCurrentProfiler isRunning]) {
- SENTRY_LOG_WARN(@"Current profiler is not running. Sending whatever data it has left "
- @"and disabling the timer from running again.");
- disableTimer();
+ {
+ std::lock_guard l(_threadUnsafe_gContinuousProfilerLock);
+ if (![_threadUnsafe_gContinuousCurrentProfiler isRunning]) {
+ SENTRY_LOG_WARN(@"Current profiler is not running. Sending whatever data it has left "
+ @"and disabling the timer from running again.");
+ disableTimer();
+ }
+
+ _sentry_threadUnsafe_transmitChunkEnvelope();
+
+ if (!_stopCalled) {
+ return;
+ }
+ }
+
+# if SENTRY_HAS_UIKIT
+ if (_observerToken != nil) {
+ [SentryDependencyContainer.sharedInstance.notificationCenterWrapper
+ removeObserver:_observerToken];
}
+# endif // SENTRY_HAS_UIKIT
+
+ [self stopTimerAndCleanup];
+}
+
++ (void)stopTimerAndCleanup
+{
+ std::lock_guard l(_threadUnsafe_gContinuousProfilerLock);
+
+ disableTimer();
- _sentry_threadUnsafe_transmitChunkEnvelope();
+ [_threadUnsafe_gContinuousCurrentProfiler stopForReason:SentryProfilerTruncationReasonNormal];
+ _threadUnsafe_gContinuousCurrentProfiler = nil;
}
# pragma mark - Testing
diff --git a/Sources/Sentry/SentryProfiler.mm b/Sources/Sentry/SentryProfiler.mm
index fbfb4e639c3..a46f1a73373 100644
--- a/Sources/Sentry/SentryProfiler.mm
+++ b/Sources/Sentry/SentryProfiler.mm
@@ -89,10 +89,12 @@ - (instancetype)initWithMode:(SentryProfilerMode)mode
[self.metricProfiler start];
# if SENTRY_HAS_UIKIT
- [SentryDependencyContainer.sharedInstance.notificationCenterWrapper
- addObserver:self
- selector:@selector(backgroundAbort)
- name:UIApplicationWillResignActiveNotification];
+ if (mode == SentryProfilerModeTrace) {
+ [SentryDependencyContainer.sharedInstance.notificationCenterWrapper
+ addObserver:self
+ selector:@selector(backgroundAbort)
+ name:UIApplicationWillResignActiveNotification];
+ }
# endif // SENTRY_HAS_UIKIT
return self;
diff --git a/Sources/Sentry/include/SentrySDK+Private.h b/Sources/Sentry/include/SentrySDK+Private.h
index ae1344d16b7..5dcca1a1347 100644
--- a/Sources/Sentry/include/SentrySDK+Private.h
+++ b/Sources/Sentry/include/SentrySDK+Private.h
@@ -60,7 +60,9 @@ SentrySDK ()
+ (void)startProfiler;
/**
- * Stop a continuous profiling session if there is one ongoing.
+ * Stop a continuous profiling session if there is one ongoing. This doesn't immediately
+ * stop the profiler, rather, the current profiling chunk timer will be allowed to expire and will
+ * not be renewed afterwards.
* @seealso https://docs.sentry.io/platforms/apple/profiling/
*/
+ (void)stopProfiler;
diff --git a/Tests/SentryProfilerTests/SentryContinuousProfilerTests.swift b/Tests/SentryProfilerTests/SentryContinuousProfilerTests.swift
index 98e2e6b3c01..2a14874b5e7 100644
--- a/Tests/SentryProfilerTests/SentryContinuousProfilerTests.swift
+++ b/Tests/SentryProfilerTests/SentryContinuousProfilerTests.swift
@@ -99,7 +99,7 @@ final class SentryContinuousProfilerTests: XCTestCase {
SentryContinuousProfiler.start()
XCTAssert(SentryContinuousProfiler.isCurrentlyProfiling())
SentrySDK.close()
- XCTAssertFalse(SentryContinuousProfiler.isCurrentlyProfiling())
+ assertContinuousProfileStoppage()
}
func testStartingAPerformanceTransactionDoesNotStartProfiler() throws {
@@ -110,6 +110,32 @@ final class SentryContinuousProfilerTests: XCTestCase {
manualSpan.finish()
automaticSpan.finish()
}
+
+ // test that when stop is called, the profiler runs to the end of the last
+ // chunk and transmits that before stopping
+ func testStoppingProfilerTransmitsLastFullChunk() throws {
+ SentryContinuousProfiler.start()
+ XCTAssert(SentryContinuousProfiler.isCurrentlyProfiling())
+
+ // assert that the first chunk was sent
+ fixture.currentDateProvider.advanceBy(interval: kSentryProfilerChunkExpirationInterval)
+ fixture.timeoutTimerFactory.fire()
+ let envelope = try XCTUnwrap(self.fixture.client?.captureEnvelopeInvocations.last)
+ let profileItem = try XCTUnwrap(envelope.items.first)
+ XCTAssertEqual("profile_chunk", profileItem.header.type)
+
+ // assert that the profiler doesn't stop until after the next timer period elapses
+ SentryContinuousProfiler.stop()
+ assertContinuousProfileStoppage()
+
+ // check that the last full chunk was sent
+ let lastEnvelope = try XCTUnwrap(self.fixture.client?.captureEnvelopeInvocations.last)
+ let lastProfileItem = try XCTUnwrap(lastEnvelope.items.first)
+ XCTAssertEqual("profile_chunk", lastProfileItem.header.type)
+
+ // check that two chunks were sent in total
+ XCTAssertEqual(2, self.fixture.client?.captureEnvelopeInvocations.count)
+ }
}
private extension SentryContinuousProfilerTests {
@@ -148,6 +174,13 @@ private extension SentryContinuousProfilerTests {
XCTAssert(SentryContinuousProfiler.isCurrentlyProfiling())
SentryContinuousProfiler.stop()
+ assertContinuousProfileStoppage()
+ }
+
+ func assertContinuousProfileStoppage() {
+ XCTAssert(SentryContinuousProfiler.isCurrentlyProfiling())
+ fixture.currentDateProvider.advance(by: kSentryProfilerTimeoutInterval)
+ fixture.timeoutTimerFactory.fire()
XCTAssertFalse(SentryContinuousProfiler.isCurrentlyProfiling())
}
diff --git a/Tests/SentryTests/SentryContinuousProfiler+Test.h b/Tests/SentryTests/SentryContinuousProfiler+Test.h
index caf450e2d7b..14e45ecb2c0 100644
--- a/Tests/SentryTests/SentryContinuousProfiler+Test.h
+++ b/Tests/SentryTests/SentryContinuousProfiler+Test.h
@@ -9,6 +9,7 @@
@interface
SentryContinuousProfiler ()
++ (void)stopTimerAndCleanup;
+ (nullable SentryProfiler *)profiler;
@end
From ae1fe61868a8970e5ac145067a5b48128da4d50f Mon Sep 17 00:00:00 2001
From: Andrew McKnight
Date: Wed, 7 Aug 2024 23:40:21 -0800
Subject: [PATCH 42/63] ref: remove isRunning state var and inspect displayLink
directly (#4192)
* ref: remove isRunning state var and inspect displayLink directly
* add check back
---
SentryTestUtils/TestDisplayLinkWrapper.swift | 8 ++++++++
.../Sentry/include/SentryDisplayLinkWrapper.h | 2 ++
.../Sentry/include/SentryDisplayLinkWrapper.m | 6 ++++++
.../SessionReplay/SentrySessionReplay.swift | 20 ++++---------------
.../SentrySessionReplayTests.swift | 4 ++--
5 files changed, 22 insertions(+), 18 deletions(-)
diff --git a/SentryTestUtils/TestDisplayLinkWrapper.swift b/SentryTestUtils/TestDisplayLinkWrapper.swift
index e0528bb4843..246201310d5 100644
--- a/SentryTestUtils/TestDisplayLinkWrapper.swift
+++ b/SentryTestUtils/TestDisplayLinkWrapper.swift
@@ -29,6 +29,8 @@ public class TestDisplayLinkWrapper: SentryDisplayLinkWrapper {
public var dateProvider: TestCurrentDateProvider
/// The smallest magnitude of time that is significant to how frames are classified as normal/slow/frozen.
public let timeEpsilon = 0.001
+
+ public var _isRunning: Bool = false
public init(dateProvider: TestCurrentDateProvider? = nil) {
self.dateProvider = dateProvider ?? TestCurrentDateProvider()
@@ -46,12 +48,17 @@ public class TestDisplayLinkWrapper: SentryDisplayLinkWrapper {
linkInvocations.record(Void())
self.target = target as AnyObject
self.selector = sel
+ _isRunning = true
}
}
public override var timestamp: CFTimeInterval {
return dateProvider.systemTime().toTimeInterval()
}
+
+ public override func isRunning() -> Bool {
+ _isRunning
+ }
public override var targetTimestamp: CFTimeInterval {
return dateProvider.systemTime().toTimeInterval() + currentFrameRate.tickDuration
@@ -61,6 +68,7 @@ public class TestDisplayLinkWrapper: SentryDisplayLinkWrapper {
public override func invalidate() {
target = nil
selector = nil
+ _isRunning = false
invalidateInvocations.record(Void())
}
diff --git a/Sources/Sentry/include/SentryDisplayLinkWrapper.h b/Sources/Sentry/include/SentryDisplayLinkWrapper.h
index eeb63204d52..7ddb0173d90 100644
--- a/Sources/Sentry/include/SentryDisplayLinkWrapper.h
+++ b/Sources/Sentry/include/SentryDisplayLinkWrapper.h
@@ -17,6 +17,8 @@ NS_ASSUME_NONNULL_BEGIN
- (void)invalidate;
+- (BOOL)isRunning;
+
@end
NS_ASSUME_NONNULL_END
diff --git a/Sources/Sentry/include/SentryDisplayLinkWrapper.m b/Sources/Sentry/include/SentryDisplayLinkWrapper.m
index dc7a2bd8469..29eab1a8f6e 100644
--- a/Sources/Sentry/include/SentryDisplayLinkWrapper.m
+++ b/Sources/Sentry/include/SentryDisplayLinkWrapper.m
@@ -27,6 +27,12 @@ - (void)linkWithTarget:(id)target selector:(SEL)sel
- (void)invalidate
{
[displayLink invalidate];
+ displayLink = nil;
+}
+
+- (BOOL)isRunning
+{
+ return displayLink != nil && !displayLink.isPaused;
}
@end
diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift
index e56291c1543..020e94d6bd7 100644
--- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift
+++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift
@@ -19,7 +19,6 @@ protocol SentrySessionReplayDelegate: NSObjectProtocol {
@objcMembers
class SentrySessionReplay: NSObject {
- private(set) var isRunning = false
private(set) var isFullSession = false
private(set) var sessionReplayId: SentryId?
@@ -42,6 +41,10 @@ class SentrySessionReplay: NSObject {
private let dispatchQueue: SentryDispatchQueueWrapper
private let lock = NSLock()
+ var isRunning: Bool {
+ displayLink.isRunning()
+ }
+
var screenshotProvider: SentryViewScreenshotProvider
var breadcrumbConverter: SentryReplayBreadcrumbConverter
@@ -70,16 +73,7 @@ class SentrySessionReplay: NSObject {
func start(rootView: UIView, fullSession: Bool) {
guard !isRunning else { return }
-
- lock.lock()
- guard !isRunning else {
- lock.unlock()
- return
- }
displayLink.link(withTarget: self, selector: #selector(newFrame(_:)))
- isRunning = true
- lock.unlock()
-
self.rootView = rootView
lastScreenShot = dateProvider.date()
videoSegmentStart = nil
@@ -100,12 +94,7 @@ class SentrySessionReplay: NSObject {
}
func stop() {
- lock.lock()
- defer { lock.unlock() }
- guard isRunning else { return }
-
displayLink.invalidate()
- isRunning = false
prepareSegmentUntil(date: dateProvider.date())
}
@@ -118,7 +107,6 @@ class SentrySessionReplay: NSObject {
videoSegmentStart = nil
displayLink.link(withTarget: self, selector: #selector(newFrame(_:)))
- isRunning = true
}
deinit {
diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift
index 375c3f0156c..a36f3a1abbd 100644
--- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift
+++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift
@@ -233,11 +233,11 @@ class SentrySessionReplayTests: XCTestCase {
Dynamic(sut).newFrame(nil)
fixture.dateProvider.advance(by: 5)
Dynamic(sut).newFrame(nil)
- XCTAssertTrue(sut.isRunning)
+ XCTAssertTrue(fixture.displayLink.isRunning())
fixture.dateProvider.advance(by: 3_600)
Dynamic(sut).newFrame(nil)
- XCTAssertFalse(sut.isRunning)
+ XCTAssertFalse(fixture.displayLink.isRunning())
}
func testSaveScreenShotInBufferMode() {
From 3603565b75ac96530a873ca2ddc94f8b30022e9c Mon Sep 17 00:00:00 2001
From: Philipp Hofmann
Date: Thu, 8 Aug 2024 09:45:38 +0200
Subject: [PATCH 43/63] fix: Accessing UI API on bg thread in enrichScope
(#4245)
Fix accessing UIDevice in enrich scope, which could be called on a
background thread by caching the system version in the
SentryUIDeviceWrapper.
---
CHANGELOG.md | 1 +
Sources/Sentry/SentryCrashWrapper.m | 4 +++-
Sources/Sentry/SentryDependencyContainer.m | 12 +++--------
Sources/Sentry/SentrySDK.m | 21 ++++++++++++-------
Sources/Sentry/SentryUIDeviceWrapper.m | 19 +++++++++++++++--
.../HybridPublic/SentryDependencyContainer.h | 4 ++--
.../Sentry/include/SentryUIDeviceWrapper.h | 11 ++++++----
.../SentryCrashIntegrationTests.swift | 3 +++
Tests/SentryTests/SentrySDKTests.swift | 19 +++++++++++++++++
9 files changed, 68 insertions(+), 26 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f7be562ef5a..cf4f521bb2e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,6 +22,7 @@ This bug caused unhandled/crash events to have the unhandled property and mach i
- Missing mach info for crash reports (#4230)
- Crash reports not generated on visionOS (#4229)
- Don’t force cast to `NSComparisonPredicate` in TERNARY operator (#4232)
+- Fix accessing UI API on bg thread in enrichScope (#4245)
- EXC_BAD_ACCESS in SentryMetricProfiler (#4242)
- Missing '#include ' (#4244)
diff --git a/Sources/Sentry/SentryCrashWrapper.m b/Sources/Sentry/SentryCrashWrapper.m
index b0e9bba43ab..0a3cd1c48a2 100644
--- a/Sources/Sentry/SentryCrashWrapper.m
+++ b/Sources/Sentry/SentryCrashWrapper.m
@@ -5,6 +5,7 @@
#import "SentryCrashMonitor_AppState.h"
#import "SentryCrashMonitor_System.h"
#import "SentryScope.h"
+#import "SentryUIDeviceWrapper.h"
#import
#import
#import
@@ -126,7 +127,8 @@ - (void)enrichScope:(SentryScope *)scope
// For MacCatalyst the UIDevice returns the current version of MacCatalyst and not the
// macOSVersion. Therefore we have to use NSProcessInfo.
#if SENTRY_HAS_UIKIT && !TARGET_OS_MACCATALYST
- [osData setValue:[UIDevice currentDevice].systemVersion forKey:@"version"];
+ [osData setValue:[SentryDependencyContainer.sharedInstance.uiDeviceWrapper getSystemVersion]
+ forKey:@"version"];
#else
NSOperatingSystemVersion version = [NSProcessInfo processInfo].operatingSystemVersion;
NSString *systemVersion = [NSString stringWithFormat:@"%d.%d.%d", (int)version.majorVersion,
diff --git a/Sources/Sentry/SentryDependencyContainer.m b/Sources/Sentry/SentryDependencyContainer.m
index 1ec1c9ac0a0..5a53361c081 100644
--- a/Sources/Sentry/SentryDependencyContainer.m
+++ b/Sources/Sentry/SentryDependencyContainer.m
@@ -198,11 +198,10 @@ - (SentryNSNotificationCenterWrapper *)notificationCenterWrapper
}
}
-#if TARGET_OS_IOS
+#if SENTRY_HAS_UIKIT
- (SentryUIDeviceWrapper *)uiDeviceWrapper SENTRY_DISABLE_THREAD_SANITIZER(
"double-checked lock produce false alarms")
{
-# if SENTRY_HAS_UIKIT
if (_uiDeviceWrapper == nil) {
@synchronized(sentryDependencyContainerLock) {
if (_uiDeviceWrapper == nil) {
@@ -211,14 +210,9 @@ - (SentryUIDeviceWrapper *)uiDeviceWrapper SENTRY_DISABLE_THREAD_SANITIZER(
}
}
return _uiDeviceWrapper;
-# else
- SENTRY_LOG_DEBUG(
- @"SentryDependencyContainer.uiDeviceWrapper only works with UIKit enabled. Ensure you're "
- @"using the right configuration of Sentry that links UIKit.");
- return nil;
-# endif // SENTRY_HAS_UIKIT
}
-#endif // TARGET_OS_IOS
+
+#endif // SENTRY_HAS_UIKIT
#if SENTRY_UIKIT_AVAILABLE
- (SentryScreenshot *)screenshot SENTRY_DISABLE_THREAD_SANITIZER(
diff --git a/Sources/Sentry/SentrySDK.m b/Sources/Sentry/SentrySDK.m
index 412b9b69d30..a8257d8dfa7 100644
--- a/Sources/Sentry/SentrySDK.m
+++ b/Sources/Sentry/SentrySDK.m
@@ -218,28 +218,33 @@ + (void)startWithOptions:(SentryOptions *)options
SentryScope *scope
= options.initialScope([[SentryScope alloc] initWithMaxBreadcrumbs:options.maxBreadcrumbs]);
- // The Hub needs to be initialized with a client so that closing a session
- // can happen.
- SentryHub *hub = [[SentryHub alloc] initWithClient:newClient andScope:scope];
- [SentrySDK setCurrentHub:hub];
- SENTRY_LOG_DEBUG(@"SDK initialized! Version: %@", SentryMeta.versionString);
SENTRY_LOG_DEBUG(@"Dispatching init work required to run on main thread.");
[SentryDependencyContainer.sharedInstance.dispatchQueueWrapper dispatchAsyncOnMainQueue:^{
SENTRY_LOG_DEBUG(@"SDK main thread init started...");
+ // The UIDeviceWrapper needs to start before the Hub, because the Hub
+ // enriches the scope, which calls the UIDeviceWrapper.
+#if SENTRY_HAS_UIKIT
+ [SentryDependencyContainer.sharedInstance.uiDeviceWrapper start];
+#endif // TARGET_OS_IOS && SENTRY_HAS_UIKIT
+
+ // The Hub needs to be initialized with a client so that closing a session
+ // can happen.
+ SentryHub *hub = [[SentryHub alloc] initWithClient:newClient andScope:scope];
+ [SentrySDK setCurrentHub:hub];
+
[SentryCrashWrapper.sharedInstance startBinaryImageCache];
[SentryDependencyContainer.sharedInstance.binaryImageCache start];
[SentrySDK installIntegrations];
-#if TARGET_OS_IOS && SENTRY_HAS_UIKIT
- [SentryDependencyContainer.sharedInstance.uiDeviceWrapper start];
-#endif // TARGET_OS_IOS && SENTRY_HAS_UIKIT
#if SENTRY_TARGET_PROFILING_SUPPORTED
sentry_manageTraceProfilerOnStartSDK(options, hub);
#endif // SENTRY_TARGET_PROFILING_SUPPORTED
}];
+
+ SENTRY_LOG_DEBUG(@"SDK initialized! Version: %@", SentryMeta.versionString);
}
+ (void)startWithConfigureOptions:(void (^)(SentryOptions *options))configureOptions
diff --git a/Sources/Sentry/SentryUIDeviceWrapper.m b/Sources/Sentry/SentryUIDeviceWrapper.m
index f82a018360b..22854e6ce11 100644
--- a/Sources/Sentry/SentryUIDeviceWrapper.m
+++ b/Sources/Sentry/SentryUIDeviceWrapper.m
@@ -2,7 +2,7 @@
#import "SentryDependencyContainer.h"
#import "SentryDispatchQueueWrapper.h"
-#if TARGET_OS_IOS && SENTRY_HAS_UIKIT
+#if SENTRY_HAS_UIKIT
NS_ASSUME_NONNULL_BEGIN
@@ -10,6 +10,7 @@
SentryUIDeviceWrapper ()
@property (nonatomic) BOOL cleanupDeviceOrientationNotifications;
@property (nonatomic) BOOL cleanupBatteryMonitoring;
+@property (nonatomic, copy) NSString *systemVersion;
@end
@implementation SentryUIDeviceWrapper
@@ -17,6 +18,8 @@ @implementation SentryUIDeviceWrapper
- (void)start
{
[SentryDependencyContainer.sharedInstance.dispatchQueueWrapper dispatchAsyncOnMainQueue:^{
+
+# if TARGET_OS_IOS
if (!UIDevice.currentDevice.isGeneratingDeviceOrientationNotifications) {
self.cleanupDeviceOrientationNotifications = YES;
[UIDevice.currentDevice beginGeneratingDeviceOrientationNotifications];
@@ -27,11 +30,15 @@ - (void)start
self.cleanupBatteryMonitoring = YES;
UIDevice.currentDevice.batteryMonitoringEnabled = YES;
}
+# endif
+
+ self.systemVersion = [UIDevice currentDevice].systemVersion;
}];
}
- (void)stop
{
+# if TARGET_OS_IOS
BOOL needsCleanUp = self.cleanupDeviceOrientationNotifications;
BOOL needsDisablingBattery = self.cleanupBatteryMonitoring;
UIDevice *device = [UIDevice currentDevice];
@@ -43,6 +50,7 @@ - (void)stop
device.batteryMonitoringEnabled = NO;
}
}];
+# endif // TARGET_OS_IOS
}
- (void)dealloc
@@ -50,6 +58,7 @@ - (void)dealloc
[self stop];
}
+# if TARGET_OS_IOS
- (UIDeviceOrientation)orientation
{
return (UIDeviceOrientation)[UIDevice currentDevice].orientation;
@@ -69,9 +78,15 @@ - (float)batteryLevel
{
return [UIDevice currentDevice].batteryLevel;
}
+# endif // TARGET_OS_IOS
+
+- (NSString *)getSystemVersion
+{
+ return self.systemVersion;
+}
@end
NS_ASSUME_NONNULL_END
-#endif // TARGET_OS_IOS && SENTRY_HAS_UIKIT
+#endif // SENTRY_HAS_UIKIT
diff --git a/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h b/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h
index e3c5a03b593..390644cc5c8 100644
--- a/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h
+++ b/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h
@@ -32,7 +32,7 @@
@class SentryViewHierarchy;
#endif // SENTRY_UIKIT_AVAILABLE
-#if TARGET_OS_IOS
+#if SENTRY_HAS_UIKIT
@class SentryUIDeviceWrapper;
#endif // TARGET_OS_IOS
@@ -80,7 +80,7 @@ SENTRY_NO_INIT
@property (nonatomic, strong) SentryUIApplication *application;
#endif // SENTRY_UIKIT_AVAILABLE
-#if TARGET_OS_IOS
+#if SENTRY_HAS_UIKIT
@property (nonatomic, strong) SentryUIDeviceWrapper *uiDeviceWrapper;
#endif // TARGET_OS_IOS
diff --git a/Sources/Sentry/include/SentryUIDeviceWrapper.h b/Sources/Sentry/include/SentryUIDeviceWrapper.h
index c6661b5947a..5dca34d5587 100644
--- a/Sources/Sentry/include/SentryUIDeviceWrapper.h
+++ b/Sources/Sentry/include/SentryUIDeviceWrapper.h
@@ -1,24 +1,27 @@
#import "SentryDefines.h"
-#if TARGET_OS_IOS && SENTRY_HAS_UIKIT
+#if SENTRY_HAS_UIKIT
# import
NS_ASSUME_NONNULL_BEGIN
-@class SentryDispatchQueueWrapper;
-
@interface SentryUIDeviceWrapper : NSObject
- (void)start;
- (void)stop;
+
+# if TARGET_OS_IOS
- (UIDeviceOrientation)orientation;
- (BOOL)isBatteryMonitoringEnabled;
- (UIDeviceBatteryState)batteryState;
- (float)batteryLevel;
+# endif // TARGET_OS_IOS
+
+- (NSString *)getSystemVersion;
@end
NS_ASSUME_NONNULL_END
-#endif // TARGET_OS_IOS && SENTRY_HAS_UIKIT
+#endif // SENTRY_HAS_UIKIT
diff --git a/Tests/SentryTests/Integrations/SentryCrash/SentryCrashIntegrationTests.swift b/Tests/SentryTests/Integrations/SentryCrash/SentryCrashIntegrationTests.swift
index d839d00d504..f41d21abcfc 100644
--- a/Tests/SentryTests/Integrations/SentryCrash/SentryCrashIntegrationTests.swift
+++ b/Tests/SentryTests/Integrations/SentryCrash/SentryCrashIntegrationTests.swift
@@ -296,6 +296,9 @@ class SentryCrashIntegrationTests: NotificationCenterTestCase {
}
private func givenSutWithGlobalHubAndCrashWrapper() -> (SentryCrashIntegration, SentryHub) {
+#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst)
+ SentryDependencyContainer.sharedInstance().uiDeviceWrapper.start()
+#endif
let sut = fixture.getSut(crashWrapper: SentryCrashWrapper.sharedInstance())
let hub = fixture.hub
SentrySDK.setCurrentHub(hub)
diff --git a/Tests/SentryTests/SentrySDKTests.swift b/Tests/SentryTests/SentrySDKTests.swift
index f928dc31b25..b108dd13339 100644
--- a/Tests/SentryTests/SentrySDKTests.swift
+++ b/Tests/SentryTests/SentrySDKTests.swift
@@ -691,6 +691,25 @@ class SentrySDKTests: XCTestCase {
SentrySDK.close()
XCTAssertFalse(deviceWrapper.started)
}
+
+ /// Ensure to start the UIDeviceWrapper before initializing the hub, so enrich scope sets the correct OS version.
+ func testStartSDK_ScopeContextContainsOSVersion() throws {
+ let expectation = expectation(description: "MainThreadTestIntegration install called")
+ MainThreadTestIntegration.expectation = expectation
+
+ DispatchQueue.global(qos: .background).async {
+ SentrySDK.start { options in
+ options.integrations = [ NSStringFromClass(MainThreadTestIntegration.self) ]
+ }
+ }
+
+ wait(for: [expectation], timeout: 1.0)
+
+ let os = try XCTUnwrap (SentrySDK.currentHub().scope.contextDictionary["os"] as? [String: Any])
+#if !targetEnvironment(macCatalyst)
+ XCTAssertEqual(UIDevice.current.systemVersion, os["version"] as? String)
+#endif
+ }
#endif
func testResumeAndPauseAppHangTracking() {
From 92bc771862851c796e82b7eafb6d139c39a1c32d Mon Sep 17 00:00:00 2001
From: Philipp Hofmann
Date: Thu, 8 Aug 2024 09:46:07 +0200
Subject: [PATCH 44/63] ci: Bump Xcode 15.2 to 15.4 (#4255)
Bump Xcode 15.2 to 15.4 on for jobs running on macos-14.
---
.github/workflows/test.yml | 10 +++++-----
.github/workflows/ui-tests.yml | 2 +-
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 825727629ff..c37e13985fc 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -94,7 +94,7 @@ jobs:
# iOS 17
- runs-on: macos-14
platform: "iOS"
- xcode: "15.2"
+ xcode: "15.4"
test-destination-os: "17.2"
device: "iPhone 15"
@@ -113,7 +113,7 @@ jobs:
# macOS 14
- runs-on: macos-14
platform: "macOS"
- xcode: "15.2"
+ xcode: "15.4"
test-destination-os: "latest"
# Catalyst. We only test the latest version, as
@@ -121,7 +121,7 @@ jobs:
# on an older iOS or macOS version is low.
- runs-on: macos-14
platform: "Catalyst"
- xcode: "15.2"
+ xcode: "15.4"
test-destination-os: "latest"
# tvOS 15
@@ -133,13 +133,13 @@ jobs:
# tvOS 16
- runs-on: macos-13
platform: "tvOS"
- xcode: "14.3"
+ xcode: "14.3.1"
test-destination-os: "17.2"
# tvOS 17
- runs-on: macos-14
platform: "tvOS"
- xcode: "15.2"
+ xcode: "15.4"
test-destination-os: "17.5"
steps:
diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml
index c12911bb0e0..e7b918a95af 100644
--- a/.github/workflows/ui-tests.yml
+++ b/.github/workflows/ui-tests.yml
@@ -122,7 +122,7 @@ jobs:
device: "iPhone 14 (16.4)"
- runs-on: macos-14
- xcode: "15.2"
+ xcode: "15.4"
device: "iPhone 15 (17.2)"
steps:
From 188f381b458be0f87bb8c29ad19d3f5d149f3b7a Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 8 Aug 2024 10:05:29 +0200
Subject: [PATCH 45/63] chore(deps): bump rexml from 3.2.9 to 3.3.3 (#4258)
Bumps [rexml](https://github.com/ruby/rexml) from 3.2.9 to 3.3.3.
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.2.9...v3.3.3)
---
updated-dependencies:
- dependency-name: rexml
dependency-type: indirect
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Gemfile.lock | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index aae5fc862c4..546e7300010 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -255,7 +255,7 @@ GEM
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
retriable (3.1.2)
- rexml (3.2.9)
+ rexml (3.3.3)
strscan
rouge (2.0.7)
ruby-macho (2.5.1)
@@ -292,13 +292,13 @@ GEM
uber (0.1.0)
unicode-display_width (2.5.0)
word_wrap (1.0.0)
- xcodeproj (1.24.0)
+ xcodeproj (1.25.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
- rexml (~> 3.2.4)
+ rexml (>= 3.3.2, < 4.0)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)
From 466eb2b5a941443f3fd6f1ab81e44d9f642a0fe4 Mon Sep 17 00:00:00 2001
From: Philipp Hofmann
Date: Thu, 8 Aug 2024 10:44:34 +0200
Subject: [PATCH 46/63] ci: Remove unit tests Xcode 14 for tvOS and macOS
(#4256)
Both tvOS and macOS were using Xcode 14.3 on macos-13, which hasn't been
available for a while now. So, the tests were actually running on Xcode
15.2, which uses tvOS 17.2 instead of tvOS 16 and macOS 14.2 instead of
macOS 13. We never noticed this. Furthermore, I think it's not worth the
computation time to run tests on these OS versions compared to the small
risk of finding bugs. We run the tests on tvOS 15 and macOS 12, which
should be sufficient.
---
.github/workflows/test.yml | 14 ++++----------
1 file changed, 4 insertions(+), 10 deletions(-)
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index c37e13985fc..32e9ce3ff89 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -104,11 +104,8 @@ jobs:
xcode: "13.4.1"
test-destination-os: "latest"
- # macOS 13
- - runs-on: macos-13
- platform: "macOS"
- xcode: "14.3"
- test-destination-os: "latest"
+ # We don't run the unit tests on macOS 13 cause we run them on all on GH actions available iOS versions.
+ # The chance of missing a bug solely on tvOS 16 that doesn't occur on iOS, macOS 12 or macOS 14 is minimal.
# macOS 14
- runs-on: macos-14
@@ -130,11 +127,8 @@ jobs:
xcode: "13.4.1"
test-destination-os: "16.1"
- # tvOS 16
- - runs-on: macos-13
- platform: "tvOS"
- xcode: "14.3.1"
- test-destination-os: "17.2"
+ # We don't run the unit tests on tvOS 16 cause we run them on all on GH actions available iOS versions.
+ # The chance of missing a bug solely on tvOS 16 that doesn't occur on iOS, tvOS 15 or tvOS 16 is minimal.
# tvOS 17
- runs-on: macos-14
From 5bcb070f6499c3403d567d965d9f04dffce8d479 Mon Sep 17 00:00:00 2001
From: Philipp Hofmann
Date: Thu, 8 Aug 2024 10:44:56 +0200
Subject: [PATCH 47/63] ci: Remove UI Address Sanitizer Tests (#4259)
The tests keep failing in CI for no good reason. We don't act on the
failures, so it's better to disable them. It's better to have a test
suite that we can trust so we can take a step back when a CI job fails
instead of continuing to ignore these failures.
---
.github/workflows/ui-tests.yml | 23 -----------------------
1 file changed, 23 deletions(-)
diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml
index e7b918a95af..91f74bd7102 100644
--- a/.github/workflows/ui-tests.yml
+++ b/.github/workflows/ui-tests.yml
@@ -82,29 +82,6 @@ jobs:
path: |
~/Library/Logs/scan/*.log
./fastlane/test_output/**
-
- ui-tests-address-sanitizer:
- name: UI Tests with Address Sanitizer
- runs-on: macos-13
-
- steps:
- - uses: actions/checkout@v4
- - run: ./scripts/ci-select-xcode.sh 15.2
-
- # GitHub Actions sometimes fail to launch the UI tests. Therefore we retry
- - name: Run Fastlane
- run: for i in {1..2}; do fastlane ui_tests_ios_swift device:"iPhone 14 (17.2)" address_sanitizer:true && break ; done
- shell: sh
-
- - name: Archiving Raw Test Logs
- uses: actions/upload-artifact@v4
- if: ${{ failure() || cancelled() }}
- with:
- name: raw-uitest-output-asan
- path: |
- ~/Library/Logs/scan/*.log
- ./fastlane/test_output/**
-
ios-swift-ui-tests:
name: iOS-Swift UI Tests ${{matrix.device}}
From 9067467d2f280933e43aadb67fd46163f51ee40a Mon Sep 17 00:00:00 2001
From: Philipp Hofmann
Date: Thu, 8 Aug 2024 10:45:22 +0200
Subject: [PATCH 48/63] test: Remove metrics API from the iOS-Swift sample
(#4249)
We plan on deprecating the existing metrics API. Therefore, we can
already remove the usages in the iOS-Swift sample app to minimize side
effects for UI tests.
---
Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 7 -------
Samples/iOS-Swift/iOS-Swift/ErrorsViewController.swift | 6 ------
2 files changed, 13 deletions(-)
diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift
index cc63313fd29..76e21d8eaca 100644
--- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift
+++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift
@@ -155,8 +155,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
return scope
}
})
-
- SentrySDK.metrics.increment(key: "app.start", value: 1.0, tags: ["view": "app-delegate"])
}
//swiftlint:enable function_body_length cyclomatic_complexity
@@ -173,11 +171,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
AppDelegate.startSentry()
}
- randomDistributionTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in
- let random = Double.random(in: 0..<1_000)
- SentrySDK.metrics.distribution(key: "random.distribution", value: random)
- }
-
if #available(iOS 15.0, *) {
metricKit.receiveReports()
}
diff --git a/Samples/iOS-Swift/iOS-Swift/ErrorsViewController.swift b/Samples/iOS-Swift/iOS-Swift/ErrorsViewController.swift
index 87ec76487aa..6f31c018e34 100644
--- a/Samples/iOS-Swift/iOS-Swift/ErrorsViewController.swift
+++ b/Samples/iOS-Swift/iOS-Swift/ErrorsViewController.swift
@@ -11,12 +11,6 @@ class ErrorsViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
SentrySDK.reportFullyDisplayed()
-
- SentrySDK.metrics.increment(key: "load.errors.view.controller")
-
- SentrySDK.metrics.timing(key: "timing.some.delayed") {
- Thread.sleep(forTimeInterval: 0.01)
- }
}
@IBAction func useAfterFree(_ sender: UIButton) {
From 6610ef1f5ca61c625c6ff549daaff1dc6f000e95 Mon Sep 17 00:00:00 2001
From: Philipp Hofmann
Date: Thu, 8 Aug 2024 12:25:30 +0200
Subject: [PATCH 49/63] fix: Rare flush timeout when called in tight loop
(#4257)
Fix an edge case that flush could keep timing out when called in a tight
loop. The test case testFlush_WhenNoInternet_BlocksAndFinishes does this
and sometimes fails with 'Flush should not time out'. This is fixed now,
by removing the duplicate setting of isFlushing to NO in two different
synchronize blocks.
---
CHANGELOG.md | 1 +
Sources/Sentry/SentryHttpTransport.m | 4 ----
2 files changed, 1 insertion(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cf4f521bb2e..63f2bb2ef8c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,7 @@ This bug caused unhandled/crash events to have the unhandled property and mach i
- Fix accessing UI API on bg thread in enrichScope (#4245)
- EXC_BAD_ACCESS in SentryMetricProfiler (#4242)
- Missing '#include ' (#4244)
+- Rare flush timeout when called in tight loop (#4257)
### Improvements
diff --git a/Sources/Sentry/SentryHttpTransport.m b/Sources/Sentry/SentryHttpTransport.m
index 3df7a3fb689..12dac0f557a 100644
--- a/Sources/Sentry/SentryHttpTransport.m
+++ b/Sources/Sentry/SentryHttpTransport.m
@@ -209,10 +209,6 @@ - (SentryFlushResult)flush:(NSTimeInterval)timeout
intptr_t result = dispatch_group_wait(self.dispatchGroup, dispatchTimeout);
- @synchronized(self) {
- self.isFlushing = NO;
- }
-
if (result == 0) {
SENTRY_LOG_DEBUG(@"Finished flushing.");
return kSentryFlushResultSuccess;
From f8d9bc344a052a26f51c8cecec93724b871ef36b Mon Sep 17 00:00:00 2001
From: getsentry-bot
Date: Thu, 8 Aug 2024 11:24:25 +0000
Subject: [PATCH 50/63] release: 8.33.0
---
.github/last-release-runid | 2 +-
CHANGELOG.md | 2 +-
Package.swift | 8 ++++----
Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj | 8 ++++----
Sentry.podspec | 2 +-
SentryPrivate.podspec | 2 +-
SentrySwiftUI.podspec | 4 ++--
Sources/Configuration/SDK.xcconfig | 2 +-
Sources/Sentry/SentryMeta.m | 2 +-
Tests/HybridSDKTest/HybridPod.podspec | 2 +-
10 files changed, 17 insertions(+), 17 deletions(-)
diff --git a/.github/last-release-runid b/.github/last-release-runid
index 1db3fcf1845..fb533541322 100644
--- a/.github/last-release-runid
+++ b/.github/last-release-runid
@@ -1 +1 @@
-10076101832
+10300958982
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 63f2bb2ef8c..2ab51d1422f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,6 @@
# Changelog
-## Unreleased
+## 8.33.0
This release fixes a bug (#4230) that we introduced with a refactoring (#4101) released in [8.30.1](https://github.com/getsentry/sentry-cocoa/releases/tag/8.30.1).
This bug caused unhandled/crash events to have the unhandled property and mach info missing, which is required for release health to show events in the unhandled tab. It's essential to mention that this bug **doesn't impact** release health statistics, such as crash-free session or user rates.
diff --git a/Package.swift b/Package.swift
index b29e9118fcc..336c6839d1a 100644
--- a/Package.swift
+++ b/Package.swift
@@ -12,13 +12,13 @@ let package = Package(
targets: [
.binaryTarget(
name: "Sentry",
- url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.32.0/Sentry.xcframework.zip",
- checksum: "780558374b95d370e8b604097f9ccb2cac328fdd18c4b8542a58ece83d2548d2" //Sentry-Static
+ url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.33.0/Sentry.xcframework.zip",
+ checksum: "5a0794d298dc863e295309a34710170701d191e5445aed7e415befd218714606" //Sentry-Static
),
.binaryTarget(
name: "Sentry-Dynamic",
- url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.32.0/Sentry-Dynamic.xcframework.zip",
- checksum: "207a09fd95caa9a9731d16dfd04844759ae7b8f8682ea0193ad79e66b257595d" //Sentry-Dynamic
+ url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.33.0/Sentry-Dynamic.xcframework.zip",
+ checksum: "4ea23fbc25edb599802f41bb00b4c9da38d1137a75ff0f79d72897d46dd451b9" //Sentry-Dynamic
),
.target ( name: "SentrySwiftUI",
dependencies: ["Sentry", "SentryInternal"],
diff --git a/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj b/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj
index a1bed4c746e..abcfd2e2ee1 100644
--- a/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj
+++ b/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj
@@ -1257,7 +1257,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 8.32.0;
+ MARKETING_VERSION = 8.33.0;
PRODUCT_BUNDLE_IDENTIFIER = "io.sentry.sample.iOS-Swift";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match Development io.sentry.sample.iOS-Swift";
@@ -1286,7 +1286,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 8.32.0;
+ MARKETING_VERSION = 8.33.0;
PRODUCT_BUNDLE_IDENTIFIER = "io.sentry.sample.iOS-Swift";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AppStore io.sentry.sample.iOS-Swift";
@@ -1935,7 +1935,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 8.32.0;
+ MARKETING_VERSION = 8.33.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";
@@ -1970,7 +1970,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 8.32.0;
+ MARKETING_VERSION = 8.33.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 d8b1abec355..a982c1b261d 100644
--- a/Sentry.podspec
+++ b/Sentry.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "Sentry"
- s.version = "8.32.0"
+ s.version = "8.33.0"
s.summary = "Sentry client for cocoa"
s.homepage = "https://github.com/getsentry/sentry-cocoa"
s.license = "mit"
diff --git a/SentryPrivate.podspec b/SentryPrivate.podspec
index 3bf0bf7ce7b..c0927b7e208 100644
--- a/SentryPrivate.podspec
+++ b/SentryPrivate.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "SentryPrivate"
- s.version = "8.32.0"
+ s.version = "8.33.0"
s.summary = "Sentry Private Library."
s.homepage = "https://github.com/getsentry/sentry-cocoa"
s.license = "mit"
diff --git a/SentrySwiftUI.podspec b/SentrySwiftUI.podspec
index 3ae7fdd9039..f0dcae83d5f 100644
--- a/SentrySwiftUI.podspec
+++ b/SentrySwiftUI.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "SentrySwiftUI"
- s.version = "8.32.0"
+ s.version = "8.33.0"
s.summary = "Sentry client for SwiftUI"
s.homepage = "https://github.com/getsentry/sentry-cocoa"
s.license = "mit"
@@ -19,5 +19,5 @@ Pod::Spec.new do |s|
s.watchos.framework = 'WatchKit'
s.source_files = "Sources/SentrySwiftUI/**/*.{swift,h,m}"
- s.dependency 'Sentry', "8.32.0"
+ s.dependency 'Sentry', "8.33.0"
end
diff --git a/Sources/Configuration/SDK.xcconfig b/Sources/Configuration/SDK.xcconfig
index a67a568b140..1dc6307c682 100644
--- a/Sources/Configuration/SDK.xcconfig
+++ b/Sources/Configuration/SDK.xcconfig
@@ -10,7 +10,7 @@ DYLIB_INSTALL_NAME_BASE = @rpath
MACH_O_TYPE = mh_dylib
FRAMEWORK_VERSION = A
-CURRENT_PROJECT_VERSION = 8.32.0
+CURRENT_PROJECT_VERSION = 8.33.0
ALWAYS_SEARCH_USER_PATHS = NO
CLANG_ENABLE_OBJC_ARC = YES
diff --git a/Sources/Sentry/SentryMeta.m b/Sources/Sentry/SentryMeta.m
index d77b29f6f8d..4111d237e08 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 *versionString = @"8.32.0";
+static NSString *versionString = @"8.33.0";
static NSString *sdkName = @"sentry.cocoa";
+ (NSString *)versionString
diff --git a/Tests/HybridSDKTest/HybridPod.podspec b/Tests/HybridSDKTest/HybridPod.podspec
index f9005095cd2..129766d89e2 100644
--- a/Tests/HybridSDKTest/HybridPod.podspec
+++ b/Tests/HybridSDKTest/HybridPod.podspec
@@ -13,6 +13,6 @@ Pod::Spec.new do |s|
s.requires_arc = true
s.frameworks = 'Foundation'
s.swift_versions = "5.5"
- s.dependency "Sentry/HybridSDK", "8.32.0"
+ s.dependency "Sentry/HybridSDK", "8.33.0"
s.source_files = "HybridTest.swift"
end
From 706c41fb426060a15fdb72e7d361ade877db77e5 Mon Sep 17 00:00:00 2001
From: Andrew McKnight
Date: Thu, 8 Aug 2024 14:20:03 -0800
Subject: [PATCH 51/63] chore: add shellcheck precommit hook (#4200)
---
.github/workflows/lint.yml | 8 ++++++++
.pre-commit-config.yaml | 5 +++++
scripts/build-xcframework.sh | 14 +++++++-------
scripts/bump.sh | 7 +++----
scripts/check-uikit-linkage.sh | 12 ++++++------
scripts/ci-select-xcode.sh | 2 +-
scripts/commit-formatted-code.sh | 4 ++--
scripts/create-simulator.sh | 2 +-
scripts/test-alamofire.sh | 2 +-
scripts/test-homekit.sh | 2 +-
scripts/update-package-sha.sh | 2 +-
scripts/upload-dsyms-with-xcode-build-phase.sh | 2 +-
scripts/xcode-slowest-tests.sh | 2 +-
scripts/xcode-test.sh | 16 ++++++++--------
14 files changed, 46 insertions(+), 34 deletions(-)
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 4cc034ef0e7..fc65669dd1d 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -74,3 +74,11 @@ jobs:
- run: pod repo update
- name: Validate HybridPod Podspec
run: pod lib lint ./Tests/HybridSDKTest/HybridPod.podspec --allow-warnings --verbose --platforms=ios "--include-podspecs={Sentry.podspec}"
+
+ shellcheck:
+ name: Run Shellcheck
+ runs-on: macos-13
+ steps:
+ - uses: actions/checkout@v4
+ - run: brew install shellcheck
+ - run: shellcheck **/*.sh
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 384f9fcb076..f78f886f7fb 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -19,6 +19,11 @@ repos:
- id: check-github-actions
- id: check-github-workflows
args: [--verbose]
+
+ - repo: https://github.com/shellcheck-py/shellcheck-py
+ rev: v0.10.0.1
+ hooks:
+ - id: shellcheck
- repo: local
hooks:
diff --git a/scripts/build-xcframework.sh b/scripts/build-xcframework.sh
index 7c1716eb486..6259b7595c8 100755
--- a/scripts/build-xcframework.sh
+++ b/scripts/build-xcframework.sh
@@ -28,9 +28,9 @@ generate_xcframework() {
rm -rf Carthage/DerivedData
for sdk in "${sdks[@]}"; do
- if [[ -n "$(grep "${sdk}" <<< "$ALL_SDKS")" ]]; then
+ if grep -q "${sdk}" <<< "$ALL_SDKS"; then
- xcodebuild archive -project Sentry.xcodeproj/ -scheme "$scheme" -configuration "$resolved_configuration" -sdk "$sdk" -archivePath ./Carthage/archive/${scheme}${suffix}/${sdk}.xcarchive CODE_SIGNING_REQUIRED=NO SKIP_INSTALL=NO CODE_SIGN_IDENTITY= CARTHAGE=YES MACH_O_TYPE=$MACH_O_TYPE ENABLE_CODE_COVERAGE=NO GCC_GENERATE_DEBUGGING_SYMBOLS="$GCC_GENERATE_DEBUGGING_SYMBOLS"
+ xcodebuild archive -project Sentry.xcodeproj/ -scheme "$scheme" -configuration "$resolved_configuration" -sdk "$sdk" -archivePath "./Carthage/archive/${scheme}${suffix}/${sdk}.xcarchive" CODE_SIGNING_REQUIRED=NO SKIP_INSTALL=NO CODE_SIGN_IDENTITY= CARTHAGE=YES MACH_O_TYPE="$MACH_O_TYPE" ENABLE_CODE_COVERAGE=NO GCC_GENERATE_DEBUGGING_SYMBOLS="$GCC_GENERATE_DEBUGGING_SYMBOLS"
createxcframework+="-framework Carthage/archive/${scheme}${suffix}/${sdk}.xcarchive/Products/Library/Frameworks/${resolved_product_name}.framework "
@@ -56,16 +56,16 @@ generate_xcframework() {
done
#Create framework for mac catalyst
- xcodebuild -project Sentry.xcodeproj/ -scheme "$scheme" -configuration "$resolved_configuration" -sdk iphoneos -destination 'platform=macOS,variant=Mac Catalyst' -derivedDataPath ./Carthage/DerivedData CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY= CARTHAGE=YES MACH_O_TYPE=$MACH_O_TYPE SUPPORTS_MACCATALYST=YES ENABLE_CODE_COVERAGE=NO GCC_GENERATE_DEBUGGING_SYMBOLS="$GCC_GENERATE_DEBUGGING_SYMBOLS"
+ xcodebuild -project Sentry.xcodeproj/ -scheme "$scheme" -configuration "$resolved_configuration" -sdk iphoneos -destination 'platform=macOS,variant=Mac Catalyst' -derivedDataPath ./Carthage/DerivedData CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY= CARTHAGE=YES MACH_O_TYPE="$MACH_O_TYPE" SUPPORTS_MACCATALYST=YES ENABLE_CODE_COVERAGE=NO GCC_GENERATE_DEBUGGING_SYMBOLS="$GCC_GENERATE_DEBUGGING_SYMBOLS"
if [ "$MACH_O_TYPE" = "staticlib" ]; then
- local infoPlist="Carthage/DerivedData/Build/Products/"$resolved_configuration"-maccatalyst/${scheme}.framework/Resources/Info.plist"
+ local infoPlist="Carthage/DerivedData/Build/Products/$resolved_configuration-maccatalyst/${scheme}.framework/Resources/Info.plist"
plutil -replace "MinimumOSVersion" -string "100.0" "$infoPlist"
fi
- createxcframework+="-framework Carthage/DerivedData/Build/Products/"$resolved_configuration"-maccatalyst/${resolved_product_name}.framework "
- if [ -d "Carthage/DerivedData/Build/Products/"$resolved_configuration"-maccatalyst/${resolved_product_name}.framework.dSYM" ]; then
- createxcframework+="-debug-symbols $(pwd -P)/Carthage/DerivedData/Build/Products/"$resolved_configuration"-maccatalyst/${resolved_product_name}.framework.dSYM "
+ createxcframework+="-framework Carthage/DerivedData/Build/Products/$resolved_configuration-maccatalyst/${resolved_product_name}.framework "
+ if [ -d "Carthage/DerivedData/Build/Products/$resolved_configuration-maccatalyst/${resolved_product_name}.framework.dSYM" ]; then
+ createxcframework+="-debug-symbols $(pwd -P)/Carthage/DerivedData/Build/Products/$resolved_configuration-maccatalyst/${resolved_product_name}.framework.dSYM "
fi
createxcframework+="-output Carthage/${scheme}${suffix}.xcframework"
diff --git a/scripts/bump.sh b/scripts/bump.sh
index 5e223ebed68..be11d2dbb78 100755
--- a/scripts/bump.sh
+++ b/scripts/bump.sh
@@ -1,16 +1,15 @@
#!/bin/bash
set -eux
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
-cd $SCRIPT_DIR/..
+cd "$SCRIPT_DIR/.."
OLD_VERSION="${1}"
NEW_VERSION="${2}"
echo "--> Clean VersionBump"
cd Utils/VersionBump && swift build
-cd $SCRIPT_DIR/..
+cd "$SCRIPT_DIR/.."
echo "--> Bumping version to ${OLD_VERSION} ${NEW_VERSION}"
-./Utils/VersionBump/.build/debug/VersionBump ${NEW_VERSION}
+./Utils/VersionBump/.build/debug/VersionBump "${NEW_VERSION}"
./scripts/update-package-sha.sh
-
diff --git a/scripts/check-uikit-linkage.sh b/scripts/check-uikit-linkage.sh
index 25355b99a9a..e0170c91e29 100755
--- a/scripts/check-uikit-linkage.sh
+++ b/scripts/check-uikit-linkage.sh
@@ -1,7 +1,7 @@
-# We ship a version of Sentry that does not link UIKit. This script runs in CI to ensure we don't accidentally add any code to that configuration that re-adds it again.
-
#!/bin/bash
+# We ship a version of Sentry that does not link UIKit. This script runs in CI to ensure we don't accidentally add any code to that configuration that re-adds it again.
+
set -eou pipefail
CONFIGURATION="${1}"
@@ -11,20 +11,20 @@ MODULE_NAME="${4}"
SENTRY_BUILD_PRODUCT_PATH="$DERIVED_DATA_PATH/Build/Products/$CONFIGURATION-iphonesimulator/$MODULE_NAME.framework/$MODULE_NAME"
-stat $SENTRY_BUILD_PRODUCT_PATH
+stat "$SENTRY_BUILD_PRODUCT_PATH"
-MATCHES=$(otool -L $SENTRY_BUILD_PRODUCT_PATH | grep -c -e "UIKit.framework/UIKit" -e "libswiftUIKit.dylib" ||:)
+MATCHES=$(otool -L "$SENTRY_BUILD_PRODUCT_PATH" | grep -c -e "UIKit.framework/UIKit" -e "libswiftUIKit.dylib" ||:)
case "$LINKAGE_TEST" in
"linked")
- if [ $MATCHES == 0 ]; then
+ if [ "$MATCHES" == 0 ]; then
echo "UIKit.framework linkage not found."
exit 1
fi
echo "Success! UIKit.framework linked."
;;
"unlinked")
- if [ $MATCHES != 0 ]; then
+ if [ "$MATCHES" != 0 ]; then
echo "UIKit.framework linkage found."
exit 1
fi
diff --git a/scripts/ci-select-xcode.sh b/scripts/ci-select-xcode.sh
index f47b442b6b8..7f9eef3b9d0 100755
--- a/scripts/ci-select-xcode.sh
+++ b/scripts/ci-select-xcode.sh
@@ -10,5 +10,5 @@ set -euo pipefail
# 14.3 is the default
XCODE_VERSION="${1:-14.3}"
-sudo xcode-select -s /Applications/Xcode_${XCODE_VERSION}.app/Contents/Developer
+sudo xcode-select -s "/Applications/Xcode_${XCODE_VERSION}.app/Contents/Developer"
swiftc --version
diff --git a/scripts/commit-formatted-code.sh b/scripts/commit-formatted-code.sh
index 24bba6f6b6c..ad7d65e3513 100755
--- a/scripts/commit-formatted-code.sh
+++ b/scripts/commit-formatted-code.sh
@@ -10,7 +10,7 @@ else
git config --global user.name 'Sentry Github Bot'
git config --global user.email 'bot+github-bot@sentry.io'
git fetch
- git checkout ${GITHUB_BRANCH}
+ git checkout "${GITHUB_BRANCH}"
git commit -am "Format code"
- git push --set-upstream origin ${GITHUB_BRANCH}
+ git push --set-upstream origin "${GITHUB_BRANCH}"
fi
diff --git a/scripts/create-simulator.sh b/scripts/create-simulator.sh
index 3a0724f045f..c612068f1ba 100755
--- a/scripts/create-simulator.sh
+++ b/scripts/create-simulator.sh
@@ -8,5 +8,5 @@ SIM_RUNTIME="${2}"
SIM_RUNTIME_WITH_DASH="${3}"
sudo mkdir -p /Library/Developer/CoreSimulator/Profiles/Runtimes
-sudo ln -s /Applications/Xcode_${XCODE_VERSION}.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ ${SIM_RUNTIME}.simruntime
+sudo ln -s "/Applications/Xcode_${XCODE_VERSION}.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime" "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS ${SIM_RUNTIME}.simruntime"
xcrun simctl create custom-test-device "iPhone 8" "com.apple.CoreSimulator.SimRuntime.iOS-${SIM_RUNTIME_WITH_DASH}"
diff --git a/scripts/test-alamofire.sh b/scripts/test-alamofire.sh
index daad2d410a2..3004a7cc8c8 100755
--- a/scripts/test-alamofire.sh
+++ b/scripts/test-alamofire.sh
@@ -12,7 +12,7 @@ set -euo pipefail
echo "### Integration test - Alamofire ###"
current_dir=$(pwd)
-scripts_path=$(dirname $(readlink -f "$0"))
+scripts_path=$(dirname "$(readlink -f "$0")")
trap 'cd "$current_dir"' ERR
diff --git a/scripts/test-homekit.sh b/scripts/test-homekit.sh
index c6e507880ad..2bbeb046d27 100755
--- a/scripts/test-homekit.sh
+++ b/scripts/test-homekit.sh
@@ -11,7 +11,7 @@ set -euo pipefail
echo "### Integration test - Home Assistant ###"
current_dir=$(pwd)
-scripts_path=$(dirname $(readlink -f "$0"))
+scripts_path=$(dirname "$(readlink -f "$0")")
trap 'cd "$current_dir"' ERR
diff --git a/scripts/update-package-sha.sh b/scripts/update-package-sha.sh
index ee06d488d2a..22043cca6b8 100755
--- a/scripts/update-package-sha.sh
+++ b/scripts/update-package-sha.sh
@@ -20,4 +20,4 @@ else
sed -i "" "s/checksum: \".*\" \/\/Sentry-Dynamic/checksum: \"$NEW_CHECKSUM_DYNAMIC\" \/\/Sentry-Dynamic/" Package.swift
fi
-echo $GITHUB_RUN_ID > .github/last-release-runid
+echo "$GITHUB_RUN_ID" > .github/last-release-runid
diff --git a/scripts/upload-dsyms-with-xcode-build-phase.sh b/scripts/upload-dsyms-with-xcode-build-phase.sh
index 72b8b9c0594..ed33ba6fa21 100755
--- a/scripts/upload-dsyms-with-xcode-build-phase.sh
+++ b/scripts/upload-dsyms-with-xcode-build-phase.sh
@@ -9,6 +9,6 @@ set -euxo pipefail
SENTRY_AUTH_TOKEN="${1}"
REPLACE="s/YOUR_AUTH_TOKEN/${SENTRY_AUTH_TOKEN}/g"
-sed -i '' $REPLACE ./scripts/upload-dsyms-with-xcode-build-phase.patch
+sed -i '' "$REPLACE" ./scripts/upload-dsyms-with-xcode-build-phase.patch
git apply ./scripts/upload-dsyms-with-xcode-build-phase.patch
diff --git a/scripts/xcode-slowest-tests.sh b/scripts/xcode-slowest-tests.sh
index c5f2a6ebe40..21e7390a75c 100755
--- a/scripts/xcode-slowest-tests.sh
+++ b/scripts/xcode-slowest-tests.sh
@@ -8,4 +8,4 @@ RAW_TEST_OUTPUT_LOG=${1:-raw-test-output.log}
NUMBER_OF_SLOWEST_TEST="${2:-20}"
echo "The $NUMBER_OF_SLOWEST_TEST slowest test cases:"
-cat $RAW_TEST_OUTPUT_LOG | grep 'Test\ Case.*seconds' | awk -F '[()]' '{print $2 " -> " $1}' | sort -rn | head -n $NUMBER_OF_SLOWEST_TEST
+grep 'Test\ Case.*seconds' "$RAW_TEST_OUTPUT_LOG" | awk -F '[()]' '{print $2 " -> " $1}' | sort -rn | head -n "$NUMBER_OF_SLOWEST_TEST"
diff --git a/scripts/xcode-test.sh b/scripts/xcode-test.sh
index cf1d5928b0d..d65cad9d9b1 100755
--- a/scripts/xcode-test.sh
+++ b/scripts/xcode-test.sh
@@ -41,8 +41,8 @@ case $PLATFORM in
;;
esac
-if [ -n $CONFIGURATION_OVERRIDE ]; then
- CONFIGURATION=$CONFIGURATION_OVERRIDE
+if [ -n "$CONFIGURATION_OVERRIDE" ]; then
+ CONFIGURATION="$CONFIGURATION_OVERRIDE"
else
case $REF_NAME in
"main")
@@ -91,9 +91,9 @@ if [ $RUN_BUILD == true ]; then
env NSUnbufferedIO=YES xcodebuild \
-workspace Sentry.xcworkspace \
-scheme Sentry \
- -configuration $CONFIGURATION \
+ -configuration "$CONFIGURATION" \
-destination "$DESTINATION" \
- -derivedDataPath $DERIVED_DATA_PATH \
+ -derivedDataPath "$DERIVED_DATA_PATH" \
-quiet \
build
fi
@@ -102,7 +102,7 @@ if [ $RUN_BUILD_FOR_TESTING == true ]; then
env NSUnbufferedIO=YES xcodebuild \
-workspace Sentry.xcworkspace \
-scheme Sentry \
- -configuration $CONFIGURATION \
+ -configuration "$CONFIGURATION" \
-destination "$DESTINATION" -quiet \
build-for-testing
fi
@@ -111,11 +111,11 @@ if [ $RUN_TEST_WITHOUT_BUILDING == true ]; then
env NSUnbufferedIO=YES xcodebuild \
-workspace Sentry.xcworkspace \
-scheme Sentry \
- -configuration $CONFIGURATION \
+ -configuration "$CONFIGURATION" \
-destination "$DESTINATION" \
test-without-building |
tee raw-test-output.log |
$RUBY_ENV_ARGS xcpretty -t &&
- slather coverage --configuration $CONFIGURATION &&
- exit ${PIPESTATUS[0]}
+ slather coverage --configuration "$CONFIGURATION" &&
+ exit "${PIPESTATUS[0]}"
fi
From d8cc6ae6e5e841f2a263d37ca8747cdf47f4922a Mon Sep 17 00:00:00 2001
From: Philipp Hofmann
Date: Fri, 9 Aug 2024 06:40:09 +0200
Subject: [PATCH 52/63] test: Improve trigger non fully blocking AppHang
(#4265)
Make triggering a non-fully blocking app hang where some frames are
rendered, but the UI still seems stuck to the user more realistic by
continuously blocking the main thread for 0.5 seconds and then quickly
rendering a few frames and then blocking it again.
Co-authored-by: Andrew McKnight
---
.../iOS-Swift-UITests/ProfilingUITests.swift | 2 +-
.../iOS-Swift.xcodeproj/project.pbxproj | 12 +++---
.../iOS-Swift/Base.lproj/Main.storyboard | 10 ++---
.../iOS-Swift/ExtraViewController.swift | 2 +-
Samples/iOS-Swift/iOS-Swift/Tools/ANRs.swift | 37 -------------------
.../iOS-Swift/Tools/TriggerAppHang.swift | 34 +++++++++++++++++
.../TransactionsViewController.swift | 6 +--
7 files changed, 50 insertions(+), 53 deletions(-)
delete mode 100644 Samples/iOS-Swift/iOS-Swift/Tools/ANRs.swift
create mode 100644 Samples/iOS-Swift/iOS-Swift/Tools/TriggerAppHang.swift
diff --git a/Samples/iOS-Swift/iOS-Swift-UITests/ProfilingUITests.swift b/Samples/iOS-Swift/iOS-Swift-UITests/ProfilingUITests.swift
index bff042faee2..6415e337080 100644
--- a/Samples/iOS-Swift/iOS-Swift-UITests/ProfilingUITests.swift
+++ b/Samples/iOS-Swift/iOS-Swift-UITests/ProfilingUITests.swift
@@ -48,7 +48,7 @@ class ProfilingUITests: BaseUITest {
goToTransactions()
startTransaction()
- app.buttons["anrFillingRunLoop"].afterWaitingForExistence("Couldn't find button to ANR").tap()
+ app.buttons["appHangFullyBlocking"].afterWaitingForExistence("Couldn't find button to trigger fully blocking AppHang.").tap()
stopTransaction()
goToProfiling()
diff --git a/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj b/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj
index abcfd2e2ee1..932ea9beef1 100644
--- a/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj
+++ b/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj
@@ -9,8 +9,8 @@
/* Begin PBXBuildFile section */
0AAAB8572887F7C60011845C /* PermissionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AABE2E928855FF80057ED69 /* PermissionsViewController.swift */; };
0AABE2EA28855FF80057ED69 /* PermissionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AABE2E928855FF80057ED69 /* PermissionsViewController.swift */; };
- 629EC8AD2B0B537400858855 /* ANRs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 629EC8AC2B0B537400858855 /* ANRs.swift */; };
- 629EC8BB2B0B5BAE00858855 /* ANRs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 629EC8AC2B0B537400858855 /* ANRs.swift */; };
+ 629EC8AD2B0B537400858855 /* TriggerAppHang.swift in Sources */ = {isa = PBXBuildFile; fileRef = 629EC8AC2B0B537400858855 /* TriggerAppHang.swift */; };
+ 629EC8BB2B0B5BAE00858855 /* TriggerAppHang.swift in Sources */ = {isa = PBXBuildFile; fileRef = 629EC8AC2B0B537400858855 /* TriggerAppHang.swift */; };
62C07D5C2AF3E3F500894688 /* BaseUITest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C07D5B2AF3E3F500894688 /* BaseUITest.swift */; };
630853532440C60F00DDE4CE /* Sentry.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 630853322440C44F00DDE4CE /* Sentry.framework */; };
637AFDAA243B02760034958B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 637AFDA9243B02760034958B /* AppDelegate.swift */; };
@@ -279,7 +279,7 @@
/* Begin PBXFileReference section */
0AABE2E928855FF80057ED69 /* PermissionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionsViewController.swift; sourceTree = ""; };
- 629EC8AC2B0B537400858855 /* ANRs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ANRs.swift; sourceTree = ""; };
+ 629EC8AC2B0B537400858855 /* TriggerAppHang.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TriggerAppHang.swift; sourceTree = ""; };
62C07D5B2AF3E3F500894688 /* BaseUITest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseUITest.swift; sourceTree = ""; };
6308532C2440C44F00DDE4CE /* Sentry.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Sentry.xcodeproj; path = ../../Sentry.xcodeproj; sourceTree = ""; };
637AFDA6243B02760034958B /* iOS-Swift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "iOS-Swift.app"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -607,7 +607,7 @@
7B5525B22938B5B5006A2932 /* DiskWriteException.swift */,
D8F01DF02A1377D0008F4996 /* SentryExposure.h */,
D8832B192AF4FE2000C522B0 /* SentryUIApplication.h */,
- 629EC8AC2B0B537400858855 /* ANRs.swift */,
+ 629EC8AC2B0B537400858855 /* TriggerAppHang.swift */,
);
path = Tools;
sourceTree = "";
@@ -954,7 +954,7 @@
7B79000429028C7300A7F467 /* MetricKitManager.swift in Sources */,
D8D7BB4A2750067900044146 /* UIAssert.swift in Sources */,
D8F3D057274E574200B56F8C /* LoremIpsumViewController.swift in Sources */,
- 629EC8AD2B0B537400858855 /* ANRs.swift in Sources */,
+ 629EC8AD2B0B537400858855 /* TriggerAppHang.swift in Sources */,
D8AE48C92C57DC2F0092A2A6 /* WebViewController.swift in Sources */,
D8DBDA78274D5FC400007380 /* SplitViewController.swift in Sources */,
84ACC43C2A73CB5900932A18 /* ProfilingNetworkScanner.swift in Sources */,
@@ -1011,7 +1011,7 @@
D8F3D055274E572F00B56F8C /* RandomErrors.swift in Sources */,
0AAAB8572887F7C60011845C /* PermissionsViewController.swift in Sources */,
D8269A3C274C095E00BD5BD5 /* AppDelegate.swift in Sources */,
- 629EC8BB2B0B5BAE00858855 /* ANRs.swift in Sources */,
+ 629EC8BB2B0B5BAE00858855 /* TriggerAppHang.swift in Sources */,
D80D021B29EE9E3D0084393D /* ErrorsViewController.swift in Sources */,
D8444E53275F792A0042F4DE /* UIAssert.swift in Sources */,
D8269A3E274C095E00BD5BD5 /* SceneDelegate.swift in Sources */,
diff --git a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard
index e3fe04cb43d..c3745fcac53 100644
--- a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard
+++ b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard
@@ -63,13 +63,13 @@
-
+
-
+
-
+
-
+
@@ -167,7 +167,7 @@
-
+
diff --git a/Samples/iOS-Swift/iOS-Swift/ExtraViewController.swift b/Samples/iOS-Swift/iOS-Swift/ExtraViewController.swift
index d2e50622782..efe9668402c 100644
--- a/Samples/iOS-Swift/iOS-Swift/ExtraViewController.swift
+++ b/Samples/iOS-Swift/iOS-Swift/ExtraViewController.swift
@@ -102,7 +102,7 @@ class ExtraViewController: UIViewController {
@IBAction func anrFillingRunLoop(_ sender: UIButton) {
highlightButton(sender)
- triggerANRFillingRunLoop(button: self.anrFillingRunLoopButton)
+ triggerNonFullyBlockingAppHang()
}
@IBAction func getPasteBoardString(_ sender: Any) {
diff --git a/Samples/iOS-Swift/iOS-Swift/Tools/ANRs.swift b/Samples/iOS-Swift/iOS-Swift/Tools/ANRs.swift
deleted file mode 100644
index b0be6d62538..00000000000
--- a/Samples/iOS-Swift/iOS-Swift/Tools/ANRs.swift
+++ /dev/null
@@ -1,37 +0,0 @@
-import Foundation
-import UIKit
-
-func triggerANRFillingRunLoop(button: UIButton) {
- let dispatchQueue = DispatchQueue(label: "ANR")
-
- let buttonTitle = button.currentTitle
- var i = 0
-
- func sleep(timeout: Double) {
- let group = DispatchGroup()
- group.enter()
- let queue = DispatchQueue(label: "delay", qos: .background, attributes: [])
-
- queue.asyncAfter(deadline: .now() + timeout) {
- group.leave()
- }
-
- group.wait()
- }
-
- dispatchQueue.async {
- for _ in 0...30 {
- i += Int.random(in: 0...10)
- i -= 1
-
- DispatchQueue.main.async {
- sleep(timeout: 0.1)
- button.setTitle("Title \(i)", for: .normal)
- }
- }
-
- DispatchQueue.main.sync {
- button.setTitle(buttonTitle, for: .normal)
- }
- }
-}
diff --git a/Samples/iOS-Swift/iOS-Swift/Tools/TriggerAppHang.swift b/Samples/iOS-Swift/iOS-Swift/Tools/TriggerAppHang.swift
new file mode 100644
index 00000000000..8b6e2b6bc0e
--- /dev/null
+++ b/Samples/iOS-Swift/iOS-Swift/Tools/TriggerAppHang.swift
@@ -0,0 +1,34 @@
+import Foundation
+import UIKit
+
+/// Triggers a non-fully blocking app hang by blocking the app for 0.5 seconds,
+/// then allowing it to draw a couple of frames, and blocking it again. While the app
+/// is not fully blocked because it renders a few frames, it still seems blocked to the
+/// user and should be considered an app hang.
+func triggerNonFullyBlockingAppHang() {
+
+ DispatchQueue.global().async {
+ for _ in 0...10 {
+ Thread.sleep(forTimeInterval: 0.001)
+ DispatchQueue.main.sync {
+ Thread.sleep(forTimeInterval: 0.5)
+ }
+ }
+ }
+}
+
+/// Schedules heavy UI rendering work on the main thread in a tight loop, which causes
+/// a fully blocking app hang without frames being rendered.
+func triggerFullyBlockingAppHang(button: UIButton) {
+ let buttonTitle = button.currentTitle
+ var i = 0
+
+ for _ in 0...5_000_000 {
+ i += Int.random(in: 0...10)
+ i -= 1
+
+ button.setTitle("\(i)", for: .normal)
+ }
+
+ button.setTitle(buttonTitle, for: .normal)
+}
diff --git a/Samples/iOS-Swift/iOS-Swift/TransactionsViewController.swift b/Samples/iOS-Swift/iOS-Swift/TransactionsViewController.swift
index 9a2cb61fb68..6c294bf98ba 100644
--- a/Samples/iOS-Swift/iOS-Swift/TransactionsViewController.swift
+++ b/Samples/iOS-Swift/iOS-Swift/TransactionsViewController.swift
@@ -3,7 +3,7 @@ import UIKit
class TransactionsViewController: UIViewController {
- @IBOutlet weak var anrFillingRunLoopButton: UIButton!
+ @IBOutlet weak var appHangFullyBlockingButton: UIButton!
private let dispatchQueue = DispatchQueue(label: "ViewController", attributes: .concurrent)
private var timer: Timer?
@@ -127,8 +127,8 @@ class TransactionsViewController: UIViewController {
present(alert, animated: true)
}
- @IBAction func anrFillingRunLoop(_ sender: Any) {
- triggerANRFillingRunLoop(button: self.anrFillingRunLoopButton)
+ @IBAction func appHangFullyBlocking(_ sender: Any) {
+ triggerFullyBlockingAppHang(button: self.appHangFullyBlockingButton)
}
@IBAction func captureTransaction(_ sender: UIButton) {
From d26c4175572e885ceb2aa2278c3c83d00fdc5959 Mon Sep 17 00:00:00 2001
From: Andrew McKnight
Date: Fri, 9 Aug 2024 14:33:28 -0800
Subject: [PATCH 53/63] chore: debug logging (#4266)
---
.../SentryProfiledTracerConcurrency.mm | 3 +++
.../Sentry/Profiling/SentryProfilerState.mm | 18 +++++++++++++++---
Sources/Sentry/SentryFramesTracker.m | 1 +
Sources/Sentry/SentryHttpTransport.m | 7 +++++--
Sources/Sentry/SentryNSURLRequest.m | 2 ++
Sources/Sentry/SentryScreenFrames.m | 13 +++++++++++++
6 files changed, 39 insertions(+), 5 deletions(-)
diff --git a/Sources/Sentry/Profiling/SentryProfiledTracerConcurrency.mm b/Sources/Sentry/Profiling/SentryProfiledTracerConcurrency.mm
index bdb2de79260..f809cbd1f92 100644
--- a/Sources/Sentry/Profiling/SentryProfiledTracerConcurrency.mm
+++ b/Sources/Sentry/Profiling/SentryProfiledTracerConcurrency.mm
@@ -125,6 +125,9 @@
# if SENTRY_HAS_UIKIT
profiler.screenFrameData =
[SentryDependencyContainer.sharedInstance.framesTracker.currentFrames copy];
+ SENTRY_LOG_DEBUG(
+ @"Grabbing copy of frames tracker screen frames data to attach to profiler: %@.",
+ profiler.screenFrameData);
if (_gProfilersToTracers.count == 0) {
[SentryDependencyContainer.sharedInstance.framesTracker resetProfilingTimestamps];
}
diff --git a/Sources/Sentry/Profiling/SentryProfilerState.mm b/Sources/Sentry/Profiling/SentryProfilerState.mm
index 27e214fa72d..437c9f235cd 100644
--- a/Sources/Sentry/Profiling/SentryProfilerState.mm
+++ b/Sources/Sentry/Profiling/SentryProfilerState.mm
@@ -1,5 +1,6 @@
#import "SentryProfilerState.h"
#if SENTRY_TARGET_PROFILING_SUPPORTED
+# import "SentryAsyncSafeLog.h"
# import "SentryBacktrace.hpp"
# import "SentryDependencyContainer.h"
# import "SentryDispatchQueueWrapper.h"
@@ -121,7 +122,8 @@ - (void)appendBacktrace:(const Backtrace &)backtrace
const auto symbols
= backtrace_symbols(reinterpret_cast(backtrace.addresses.data()),
static_cast(backtrace.addresses.size()));
-# endif
+ const auto *backtraceFunctionNames = [NSMutableArray array];
+# endif // defined(DEBUG)
const auto stack = [NSMutableArray array];
for (std::vector::size_type backtraceAddressIdx = 0;
@@ -133,8 +135,10 @@ - (void)appendBacktrace:(const Backtrace &)backtrace
const auto frame = [NSMutableDictionary dictionary];
frame[@"instruction_addr"] = instructionAddress;
# if defined(DEBUG)
- frame[@"function"]
+ const auto functionName
= parseBacktraceSymbolsFunctionName(symbols[backtraceAddressIdx]);
+ frame[@"function"] = functionName;
+ [backtraceFunctionNames addObject:functionName];
# endif
const auto newFrameIndex = @(state.frames.count);
[stack addObject:newFrameIndex];
@@ -146,7 +150,7 @@ - (void)appendBacktrace:(const Backtrace &)backtrace
}
# if defined(DEBUG)
free(symbols);
-# endif
+# endif // defined(DEBUG)
const auto sample = [[SentrySample alloc] init];
sample.absoluteTimestamp = backtrace.absoluteTimestamp;
@@ -165,6 +169,14 @@ - (void)appendBacktrace:(const Backtrace &)backtrace
[state.stacks addObject:stack];
}
+# if defined(DEBUG)
+ if (backtraceFunctionNames.count > 0) {
+ SENTRY_ASYNC_SAFE_LOG_DEBUG("Recorded backtrace for thread %s at %llu: %s",
+ threadID.UTF8String, sample.absoluteTimestamp,
+ backtraceFunctionNames.description.UTF8String);
+ }
+# endif // defined(DEBUG)
+
[state.samples addObject:sample];
}];
}
diff --git a/Sources/Sentry/SentryFramesTracker.m b/Sources/Sentry/SentryFramesTracker.m
index 6c5908bf16c..44f1e911ef8 100644
--- a/Sources/Sentry/SentryFramesTracker.m
+++ b/Sources/Sentry/SentryFramesTracker.m
@@ -135,6 +135,7 @@ - (void)resetProfilingTimestamps
- (void)resetProfilingTimestampsInternal
{
+ SENTRY_LOG_DEBUG(@"Resetting profiling GPU timeseries data.");
self.frozenFrameTimestamps = [SentryMutableFrameInfoTimeSeries array];
self.slowFrameTimestamps = [SentryMutableFrameInfoTimeSeries array];
self.frameRateTimestamps = [SentryMutableFrameInfoTimeSeries array];
diff --git a/Sources/Sentry/SentryHttpTransport.m b/Sources/Sentry/SentryHttpTransport.m
index 12dac0f557a..df8bf38f533 100644
--- a/Sources/Sentry/SentryHttpTransport.m
+++ b/Sources/Sentry/SentryHttpTransport.m
@@ -290,12 +290,14 @@ - (void)sendAllCachedEnvelopes
SentryEnvelope *envelope = [SentrySerialization envelopeWithData:envelopeFileContents.contents];
if (nil == envelope) {
+ SENTRY_LOG_DEBUG(@"Envelope contained no deserializable data.");
[self deleteEnvelopeAndSendNext:envelopeFileContents.path];
return;
}
SentryEnvelope *rateLimitedEnvelope = [self.envelopeRateLimit removeRateLimitedItems:envelope];
if (rateLimitedEnvelope.items.count == 0) {
+ SENTRY_LOG_DEBUG(@"Envelope had no rate-limited items, nothing to send.");
[self deleteEnvelopeAndSendNext:envelopeFileContents.path];
return;
}
@@ -309,6 +311,7 @@ - (void)sendAllCachedEnvelopes
didFailWithError:&requestError];
if (nil != requestError) {
+ SENTRY_LOG_DEBUG(@"Failed to build request: %@.", requestError);
[self recordLostEventFor:rateLimitedEnvelope.items];
[self deleteEnvelopeAndSendNext:envelopeFileContents.path];
return;
@@ -350,12 +353,13 @@ - (void)sendEnvelope:(SentryEnvelope *)envelope
return;
}
- // If the response is not nil we had an internet connection.
if (error && response.statusCode != 429) {
+ SENTRY_LOG_DEBUG(@"Request error other than rate limit: %@", error);
[weakSelf recordLostEventFor:envelope.items];
}
if (nil != response) {
+ SENTRY_LOG_DEBUG(@"Envelope sent successfully!");
[weakSelf.rateLimits update:response];
[weakSelf deleteEnvelopeAndSendNext:envelopePath];
} else {
@@ -367,7 +371,6 @@ - (void)sendEnvelope:(SentryEnvelope *)envelope
- (void)finishedSending
{
- SENTRY_LOG_DEBUG(@"Finished sending.");
@synchronized(self) {
self.isSending = NO;
if (self.isFlushing) {
diff --git a/Sources/Sentry/SentryNSURLRequest.m b/Sources/Sentry/SentryNSURLRequest.m
index 22b840c5700..7d9b79ccae1 100644
--- a/Sources/Sentry/SentryNSURLRequest.m
+++ b/Sources/Sentry/SentryNSURLRequest.m
@@ -95,6 +95,8 @@ - (instancetype)initEnvelopeRequestWithURL:(NSURL *)url
forHTTPHeaderField:@"User-Agent"];
[self setValue:@"gzip" forHTTPHeaderField:@"Content-Encoding"];
self.HTTPBody = sentry_gzippedWithCompressionLevel(data, -1, error);
+
+ SENTRY_LOG_DEBUG(@"Constructed request: %@", self);
}
return self;
diff --git a/Sources/Sentry/SentryScreenFrames.m b/Sources/Sentry/SentryScreenFrames.m
index 2bf64957e62..6b2ce14d64a 100644
--- a/Sources/Sentry/SentryScreenFrames.m
+++ b/Sources/Sentry/SentryScreenFrames.m
@@ -78,6 +78,19 @@ - (nonnull id)copyWithZone:(nullable NSZone *)zone
# endif // SENTRY_TARGET_PROFILING_SUPPORTED
+- (NSString *)description
+{
+ NSMutableString *result = [NSMutableString
+ stringWithFormat:@"Total frames: %lu; slow frames: %lu; frozen frames: %lu",
+ (unsigned long)_total, (unsigned long)_slow, (unsigned long)_frozen];
+# if SENTRY_TARGET_PROFILING_SUPPORTED
+ [result appendFormat:
+ @"\nslowFrameTimestamps: %@\nfrozenFrameTimestamps: %@\nframeRateTimestamps: %@",
+ _slowFrameTimestamps, _frozenFrameTimestamps, _frameRateTimestamps];
+# endif // SENTRY_TARGET_PROFILING_SUPPORTED
+ return result;
+}
+
@end
#endif // SENTRY_UIKIT_AVAILABLE
From 594c2e693f9498c4acd10f6e64970f1f48adf13b Mon Sep 17 00:00:00 2001
From: Dhiogo Brustolin
Date: Mon, 12 Aug 2024 13:29:58 +0200
Subject: [PATCH 54/63] feat: Add a replay quality option for Objc (#4267)
Quality options was not exposed to Objc.
---
CHANGELOG.md | 6 ++++++
Samples/iOS-ObjectiveC/iOS-ObjectiveC/AppDelegate.m | 6 ++++++
.../Integrations/SessionReplay/SentryReplayOptions.swift | 1 +
3 files changed, 13 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2ab51d1422f..e18ffbb7c67 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
# Changelog
+## Unreleased
+
+### Features
+
+- Add replay quality option for Objective-C (#4267)
+
## 8.33.0
This release fixes a bug (#4230) that we introduced with a refactoring (#4101) released in [8.30.1](https://github.com/getsentry/sentry-cocoa/releases/tag/8.30.1).
diff --git a/Samples/iOS-ObjectiveC/iOS-ObjectiveC/AppDelegate.m b/Samples/iOS-ObjectiveC/iOS-ObjectiveC/AppDelegate.m
index a4c8e5d6fcf..ddaca944199 100644
--- a/Samples/iOS-ObjectiveC/iOS-ObjectiveC/AppDelegate.m
+++ b/Samples/iOS-ObjectiveC/iOS-ObjectiveC/AppDelegate.m
@@ -26,6 +26,12 @@ - (BOOL)application:(UIApplication *)application
[[SentryHttpStatusCodeRange alloc] initWithMin:400 max:599];
options.failedRequestStatusCodes = @[ httpStatusCodeRange ];
+ options.experimental.sessionReplay.quality = SentryReplayQualityMedium;
+ options.experimental.sessionReplay.redactAllText = true;
+ options.experimental.sessionReplay.redactAllImages = true;
+ options.experimental.sessionReplay.sessionSampleRate = 0;
+ options.experimental.sessionReplay.onErrorSampleRate = 1;
+
options.initialScope = ^(SentryScope *scope) {
[scope setTagValue:@"" forKey:@""];
return scope;
diff --git a/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift
index bd5cca99a61..4c9ef876d98 100644
--- a/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift
+++ b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift
@@ -6,6 +6,7 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
/**
* Enum to define the quality of the session replay.
*/
+ @objc
public enum SentryReplayQuality: Int {
/**
* Video Scale: 80%
From e0abb7e49c3133b01a2ad0307415661289c2d99e Mon Sep 17 00:00:00 2001
From: Philipp Hofmann
Date: Mon, 12 Aug 2024 15:30:24 +0200
Subject: [PATCH 55/63] chore: Duplicate ANRTracker classes (#4262)
This is the first step for #3492, which is part of the EPIC AppHang improvements #4261.
---
Sentry.xcodeproj/project.pbxproj | 24 ++
Sources/Sentry/SentryANRTrackerV2.m | 210 +++++++++++++++
.../Sentry/SentryANRTrackingIntegrationV2.m | 130 ++++++++++
Sources/Sentry/SentryBaseIntegration.m | 12 +
Sources/Sentry/SentryDependencyContainer.m | 19 ++
Sources/Sentry/SentryOptions.m | 4 +
.../HybridPublic/SentryDependencyContainer.h | 3 +
Sources/Sentry/include/SentryANRTrackerV2.h | 34 +++
.../include/SentryANRTrackingIntegrationV2.h | 18 ++
.../Sentry/include/SentryBaseIntegration.h | 1 +
.../Sentry/include/SentryOptions+Private.h | 2 +
.../Helper/SentryTestThreadWrapper.swift | 2 +
.../ANR/SentryANRTrackerV2Tests.swift | 239 ++++++++++++++++++
.../SentryANRTrackingIntegrationV2Tests.swift | 229 +++++++++++++++++
Tests/SentryTests/SentryOptionsTest.m | 5 +
.../SentryTests/SentryTests-Bridging-Header.h | 2 +
16 files changed, 934 insertions(+)
create mode 100644 Sources/Sentry/SentryANRTrackerV2.m
create mode 100644 Sources/Sentry/SentryANRTrackingIntegrationV2.m
create mode 100644 Sources/Sentry/include/SentryANRTrackerV2.h
create mode 100644 Sources/Sentry/include/SentryANRTrackingIntegrationV2.h
create mode 100644 Tests/SentryTests/Integrations/ANR/SentryANRTrackerV2Tests.swift
create mode 100644 Tests/SentryTests/Integrations/ANR/SentryANRTrackingIntegrationV2Tests.swift
diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj
index 8fca179e12b..d19c857a7a9 100644
--- a/Sentry.xcodeproj/project.pbxproj
+++ b/Sentry.xcodeproj/project.pbxproj
@@ -76,6 +76,11 @@
620203B22C59025E0008317C /* SentryFileContents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 620203B12C59025E0008317C /* SentryFileContents.swift */; };
620379DB2AFE1415005AC0C1 /* SentryBuildAppStartSpans.h in Headers */ = {isa = PBXBuildFile; fileRef = 620379DA2AFE1415005AC0C1 /* SentryBuildAppStartSpans.h */; };
620379DD2AFE1432005AC0C1 /* SentryBuildAppStartSpans.m in Sources */ = {isa = PBXBuildFile; fileRef = 620379DC2AFE1432005AC0C1 /* SentryBuildAppStartSpans.m */; };
+ 621A9D5A2C64F18900B5D7D6 /* SentryANRTrackingIntegrationV2.h in Headers */ = {isa = PBXBuildFile; fileRef = 621A9D592C64F18900B5D7D6 /* SentryANRTrackingIntegrationV2.h */; };
+ 621A9D5C2C64F1AE00B5D7D6 /* SentryANRTrackingIntegrationV2.m in Sources */ = {isa = PBXBuildFile; fileRef = 621A9D5B2C64F1AE00B5D7D6 /* SentryANRTrackingIntegrationV2.m */; };
+ 621A9D5F2C64F3BC00B5D7D6 /* SentryANRTrackingIntegrationV2Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621A9D5D2C64F3B700B5D7D6 /* SentryANRTrackingIntegrationV2Tests.swift */; };
+ 621AE74B2C626C230012E730 /* SentryANRTrackerV2.h in Headers */ = {isa = PBXBuildFile; fileRef = 621AE74A2C626C230012E730 /* SentryANRTrackerV2.h */; };
+ 621AE74D2C626C510012E730 /* SentryANRTrackerV2.m in Sources */ = {isa = PBXBuildFile; fileRef = 621AE74C2C626C510012E730 /* SentryANRTrackerV2.m */; };
621D9F2F2B9B0320003D94DE /* SentryCurrentDateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621D9F2E2B9B0320003D94DE /* SentryCurrentDateProvider.swift */; };
621F61F12BEA073A005E654F /* SentryEnabledFeaturesBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621F61F02BEA073A005E654F /* SentryEnabledFeaturesBuilder.swift */; };
62262B862BA1C46D004DA3DD /* SentryStatsdClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 62262B852BA1C46D004DA3DD /* SentryStatsdClient.h */; };
@@ -130,6 +135,7 @@
62E081AB29ED4322000F69FC /* SentryBreadcrumbTestDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E081AA29ED4322000F69FC /* SentryBreadcrumbTestDelegate.swift */; };
62E146D02BAAE47600ED34FD /* LocalMetricsAggregator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E146CF2BAAE47600ED34FD /* LocalMetricsAggregator.swift */; };
62E146D22BAAF55B00ED34FD /* LocalMetricsAggregatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E146D12BAAF55B00ED34FD /* LocalMetricsAggregatorTests.swift */; };
+ 62EF86A12C626D39004E058B /* SentryANRTrackerV2Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621AE74E2C626CF70012E730 /* SentryANRTrackerV2Tests.swift */; };
62F05D2B2C0DB1F100916E3F /* SentryLogTestHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 62F05D2A2C0DB1F100916E3F /* SentryLogTestHelper.m */; };
62F226B729A37C120038080D /* SentryBooleanSerialization.m in Sources */ = {isa = PBXBuildFile; fileRef = 62F226B629A37C120038080D /* SentryBooleanSerialization.m */; };
62F4DDA12C04CB9700588890 /* SentryBaggageSerializationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62F4DDA02C04CB9700588890 /* SentryBaggageSerializationTests.swift */; };
@@ -1051,6 +1057,12 @@
620203B12C59025E0008317C /* SentryFileContents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryFileContents.swift; sourceTree = ""; };
620379DA2AFE1415005AC0C1 /* SentryBuildAppStartSpans.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryBuildAppStartSpans.h; path = include/SentryBuildAppStartSpans.h; sourceTree = ""; };
620379DC2AFE1432005AC0C1 /* SentryBuildAppStartSpans.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryBuildAppStartSpans.m; sourceTree = ""; };
+ 621A9D592C64F18900B5D7D6 /* SentryANRTrackingIntegrationV2.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryANRTrackingIntegrationV2.h; path = include/SentryANRTrackingIntegrationV2.h; sourceTree = ""; };
+ 621A9D5B2C64F1AE00B5D7D6 /* SentryANRTrackingIntegrationV2.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryANRTrackingIntegrationV2.m; sourceTree = ""; };
+ 621A9D5D2C64F3B700B5D7D6 /* SentryANRTrackingIntegrationV2Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryANRTrackingIntegrationV2Tests.swift; sourceTree = ""; };
+ 621AE74A2C626C230012E730 /* SentryANRTrackerV2.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryANRTrackerV2.h; path = include/SentryANRTrackerV2.h; sourceTree = ""; };
+ 621AE74C2C626C510012E730 /* SentryANRTrackerV2.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryANRTrackerV2.m; sourceTree = ""; };
+ 621AE74E2C626CF70012E730 /* SentryANRTrackerV2Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryANRTrackerV2Tests.swift; sourceTree = ""; };
621D9F2E2B9B0320003D94DE /* SentryCurrentDateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCurrentDateProvider.swift; sourceTree = ""; };
621F61F02BEA073A005E654F /* SentryEnabledFeaturesBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryEnabledFeaturesBuilder.swift; sourceTree = ""; };
62262B852BA1C46D004DA3DD /* SentryStatsdClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryStatsdClient.h; path = include/SentryStatsdClient.h; sourceTree = ""; };
@@ -2834,6 +2846,10 @@
7B127B0E27CF6F4700A71ED2 /* SentryANRTrackingIntegration.m */,
7BCFA71427D0BAB7008C662C /* SentryANRTracker.h */,
7BCFA71527D0BB50008C662C /* SentryANRTracker.m */,
+ 621A9D592C64F18900B5D7D6 /* SentryANRTrackingIntegrationV2.h */,
+ 621A9D5B2C64F1AE00B5D7D6 /* SentryANRTrackingIntegrationV2.m */,
+ 621AE74A2C626C230012E730 /* SentryANRTrackerV2.h */,
+ 621AE74C2C626C510012E730 /* SentryANRTrackerV2.m */,
);
name = ANR;
sourceTree = "";
@@ -2842,7 +2858,9 @@
isa = PBXGroup;
children = (
7B2A70D727D5F07F008B0D15 /* SentryANRTrackerTests.swift */,
+ 621AE74E2C626CF70012E730 /* SentryANRTrackerV2Tests.swift */,
7BFA69F527E0840400233199 /* SentryANRTrackingIntegrationTests.swift */,
+ 621A9D5D2C64F3B700B5D7D6 /* SentryANRTrackingIntegrationV2Tests.swift */,
);
path = ANR;
sourceTree = "";
@@ -3987,6 +4005,7 @@
03F84D2727DD414C008FE43F /* SentryMachLogging.hpp in Headers */,
63295AF51EF3C7DB002D4490 /* SentryNSDictionarySanitize.h in Headers */,
D8739D172BEEA33F007D2F66 /* SentryLevelHelper.h in Headers */,
+ 621A9D5A2C64F18900B5D7D6 /* SentryANRTrackingIntegrationV2.h in Headers */,
8E4A037825F6F52100000D77 /* SentrySampleDecision.h in Headers */,
63FE717920DA4C1100CDBAE8 /* SentryCrashReportStore.h in Headers */,
0AAE202128ED9BCC00D0CD80 /* SentryReachability.h in Headers */,
@@ -4085,6 +4104,7 @@
7BC852332458802C005A70F0 /* SentryDataCategoryMapper.h in Headers */,
7BDB03B7251364F800BAE198 /* SentryDispatchQueueWrapper.h in Headers */,
7BF9EF842722D07B00B5BBEF /* SentryObjCRuntimeWrapper.h in Headers */,
+ 621AE74B2C626C230012E730 /* SentryANRTrackerV2.h in Headers */,
639889B71EDECFA800EA7442 /* SentryBreadcrumbTracker.h in Headers */,
632331F9240506DF008D91D6 /* SentryScope+Private.h in Headers */,
D8603DD8284F894C000E1227 /* SentryBaggage.h in Headers */,
@@ -4493,6 +4513,7 @@
7B7D873624864C9D00D2ECFF /* SentryCrashDefaultMachineContextWrapper.m in Sources */,
63FE712F20DA4C1100CDBAE8 /* SentryCrashSysCtl.c in Sources */,
7B3B473825D6CC7E00D01640 /* SentryNSError.m in Sources */,
+ 621AE74D2C626C510012E730 /* SentryANRTrackerV2.m in Sources */,
D8ACE3C82762187200F5A213 /* SentryNSDataTracker.m in Sources */,
7BE3C77D2446112C00A38442 /* SentryRateLimitParser.m in Sources */,
51B15F7E2BE88A7C0026A2F2 /* URLSessionTaskHelper.swift in Sources */,
@@ -4547,6 +4568,7 @@
8ECC674725C23A20000E2BF6 /* SentrySpanContext.m in Sources */,
7B18DE4228D9F794004845C6 /* SentryNSNotificationCenterWrapper.m in Sources */,
639FCFA91EBC80CC00778193 /* SentryFrame.m in Sources */,
+ 621A9D5C2C64F1AE00B5D7D6 /* SentryANRTrackingIntegrationV2.m in Sources */,
D858FA672A29EAB3002A3503 /* SentryBinaryImageCache.m in Sources */,
D8AFC0572BDA895400118BE1 /* UIRedactBuilder.swift in Sources */,
8E564AEA267AF22600FE117D /* SentryNetworkTracker.m in Sources */,
@@ -4912,6 +4934,7 @@
D855AD62286ED6A4002573E1 /* SentryCrashTests.m in Sources */,
D8AFC0012BD252B900118BE1 /* SentryOnDemandReplayTests.swift in Sources */,
0A9415BA28F96CAC006A5DD1 /* TestSentryReachability.swift in Sources */,
+ 62EF86A12C626D39004E058B /* SentryANRTrackerV2Tests.swift in Sources */,
D880E3A728573E87008A90DB /* SentryBaggageTests.swift in Sources */,
7B16FD022654F86B008177D3 /* SentrySysctlTests.swift in Sources */,
7BAF3DB5243C743E008A5414 /* SentryClientTests.swift in Sources */,
@@ -4995,6 +5018,7 @@
7BB7E7C729267A28004BF96B /* EmptyIntegration.swift in Sources */,
7B965728268321CD00C66E25 /* SentryCrashScopeObserverTests.swift in Sources */,
626866742BA89683006995EA /* BucketMetricsAggregatorTests.swift in Sources */,
+ 621A9D5F2C64F3BC00B5D7D6 /* SentryANRTrackingIntegrationV2Tests.swift in Sources */,
7BD86ECB264A6DB5005439DB /* TestSysctl.swift in Sources */,
D861301C2BB5A267004C0F5E /* SentrySessionReplayTests.swift in Sources */,
7B0DC73428869BF40039995F /* NSMutableDictionarySentryTests.swift in Sources */,
diff --git a/Sources/Sentry/SentryANRTrackerV2.m b/Sources/Sentry/SentryANRTrackerV2.m
new file mode 100644
index 00000000000..10a6c75bdac
--- /dev/null
+++ b/Sources/Sentry/SentryANRTrackerV2.m
@@ -0,0 +1,210 @@
+#import "SentryANRTrackerV2.h"
+#import "SentryCrashWrapper.h"
+#import "SentryDependencyContainer.h"
+#import "SentryDispatchQueueWrapper.h"
+#import "SentryLog.h"
+#import "SentrySwift.h"
+#import "SentryThreadWrapper.h"
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+typedef NS_ENUM(NSInteger, SentryANRTrackerState) {
+ kSentryANRTrackerNotRunning = 1,
+ kSentryANRTrackerRunning,
+ kSentryANRTrackerStarting,
+ kSentryANRTrackerStopping
+};
+
+@interface
+SentryANRTrackerV2 ()
+
+@property (nonatomic, strong) SentryCrashWrapper *crashWrapper;
+@property (nonatomic, strong) SentryDispatchQueueWrapper *dispatchQueueWrapper;
+@property (nonatomic, strong) SentryThreadWrapper *threadWrapper;
+@property (nonatomic, strong) NSHashTable> *listeners;
+@property (nonatomic, assign) NSTimeInterval timeoutInterval;
+
+@end
+
+@implementation SentryANRTrackerV2 {
+ NSObject *threadLock;
+ SentryANRTrackerState state;
+}
+
+- (instancetype)initWithTimeoutInterval:(NSTimeInterval)timeoutInterval
+ crashWrapper:(SentryCrashWrapper *)crashWrapper
+ dispatchQueueWrapper:(SentryDispatchQueueWrapper *)dispatchQueueWrapper
+ threadWrapper:(SentryThreadWrapper *)threadWrapper
+{
+ if (self = [super init]) {
+ self.timeoutInterval = timeoutInterval;
+ self.crashWrapper = crashWrapper;
+ self.dispatchQueueWrapper = dispatchQueueWrapper;
+ self.threadWrapper = threadWrapper;
+ self.listeners = [NSHashTable weakObjectsHashTable];
+ threadLock = [[NSObject alloc] init];
+ state = kSentryANRTrackerNotRunning;
+ }
+ return self;
+}
+
+- (void)detectANRs
+{
+ NSUUID *threadID = [NSUUID UUID];
+
+ @synchronized(threadLock) {
+ [self.threadWrapper threadStarted:threadID];
+
+ if (state != kSentryANRTrackerStarting) {
+ [self.threadWrapper threadFinished:threadID];
+ return;
+ }
+
+ NSThread.currentThread.name = @"io.sentry.app-hang-tracker";
+ state = kSentryANRTrackerRunning;
+ }
+
+ __block atomic_int ticksSinceUiUpdate = 0;
+ __block BOOL reported = NO;
+
+ NSInteger reportThreshold = 5;
+ NSTimeInterval sleepInterval = self.timeoutInterval / reportThreshold;
+
+ SentryCurrentDateProvider *dateProvider = SentryDependencyContainer.sharedInstance.dateProvider;
+
+ // Canceling the thread can take up to sleepInterval.
+ while (YES) {
+ @synchronized(threadLock) {
+ if (state != kSentryANRTrackerRunning) {
+ break;
+ }
+ }
+
+ NSDate *blockDeadline = [[dateProvider date] dateByAddingTimeInterval:self.timeoutInterval];
+
+ atomic_fetch_add_explicit(&ticksSinceUiUpdate, 1, memory_order_relaxed);
+
+ [self.dispatchQueueWrapper dispatchAsyncOnMainQueue:^{
+ atomic_store_explicit(&ticksSinceUiUpdate, 0, memory_order_relaxed);
+
+ if (reported) {
+ SENTRY_LOG_WARN(@"ANR stopped.");
+
+ // The ANR stopped, don't block the main thread with calling ANRStopped listeners.
+ // While the ANR code reports an ANR and collects the stack trace, the ANR might
+ // stop simultaneously. In that case, the ANRs stack trace would contain the
+ // following code running on the main thread. To avoid this, we offload work to a
+ // background thread.
+ [self.dispatchQueueWrapper dispatchAsyncWithBlock:^{ [self ANRStopped]; }];
+ }
+
+ reported = NO;
+ }];
+
+ [self.threadWrapper sleepForTimeInterval:sleepInterval];
+
+ // The blockDeadline should be roughly executed after the timeoutInterval even if there is
+ // an ANR. If the app gets suspended this thread could sleep and wake up again. To avoid
+ // false positives, we don't report ANRs if the delta is too big.
+ NSTimeInterval deltaFromNowToBlockDeadline =
+ [[dateProvider date] timeIntervalSinceDate:blockDeadline];
+
+ if (deltaFromNowToBlockDeadline >= self.timeoutInterval) {
+ SENTRY_LOG_DEBUG(
+ @"Ignoring ANR because the delta is too big: %f.", deltaFromNowToBlockDeadline);
+ continue;
+ }
+
+ if (atomic_load_explicit(&ticksSinceUiUpdate, memory_order_relaxed) >= reportThreshold
+ && !reported) {
+ reported = YES;
+
+ if (![self.crashWrapper isApplicationInForeground]) {
+ SENTRY_LOG_DEBUG(@"Ignoring ANR because the app is in the background");
+ continue;
+ }
+
+ SENTRY_LOG_WARN(@"ANR detected.");
+ [self ANRDetected];
+ }
+ }
+
+ @synchronized(threadLock) {
+ state = kSentryANRTrackerNotRunning;
+ [self.threadWrapper threadFinished:threadID];
+ }
+}
+
+- (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];
+
+ @synchronized(threadLock) {
+ if (self.listeners.count > 0 && state == kSentryANRTrackerNotRunning) {
+ if (state == kSentryANRTrackerNotRunning) {
+ state = kSentryANRTrackerStarting;
+ [NSThread detachNewThreadSelector:@selector(detectANRs)
+ toTarget:self
+ withObject:nil];
+ }
+ }
+ }
+ }
+}
+
+- (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)stop
+{
+ @synchronized(threadLock) {
+ SENTRY_LOG_INFO(@"Stopping ANR detection");
+ state = kSentryANRTrackerStopping;
+ }
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Sources/Sentry/SentryANRTrackingIntegrationV2.m b/Sources/Sentry/SentryANRTrackingIntegrationV2.m
new file mode 100644
index 00000000000..f738729e328
--- /dev/null
+++ b/Sources/Sentry/SentryANRTrackingIntegrationV2.m
@@ -0,0 +1,130 @@
+#import "SentryANRTrackingIntegrationV2.h"
+#import "SentryANRTrackerV2.h"
+#import "SentryClient+Private.h"
+#import "SentryCrashMachineContext.h"
+#import "SentryCrashWrapper.h"
+#import "SentryDependencyContainer.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 "SentryStacktrace.h"
+#import "SentryThread.h"
+#import "SentryThreadInspector.h"
+#import "SentryThreadWrapper.h"
+#import "SentryUIApplication.h"
+#import
+
+#if SENTRY_HAS_UIKIT
+# import
+#endif
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface
+SentryANRTrackingIntegrationV2 ()
+
+@property (nonatomic, strong) SentryANRTrackerV2 *tracker;
+@property (nonatomic, strong) SentryOptions *options;
+@property (atomic, assign) BOOL reportAppHangs;
+
+@end
+
+@implementation SentryANRTrackingIntegrationV2
+
+- (BOOL)installWithOptions:(SentryOptions *)options
+{
+ if (![super installWithOptions:options]) {
+ return NO;
+ }
+
+ self.tracker =
+ [SentryDependencyContainer.sharedInstance getANRTrackerV2:options.appHangTimeoutInterval];
+
+ [self.tracker addListener:self];
+ self.options = options;
+ self.reportAppHangs = YES;
+
+ return YES;
+}
+
+- (SentryIntegrationOption)integrationOptions
+{
+ return kIntegrationOptionEnableAppHangTrackingV2 | kIntegrationOptionDebuggerNotAttached;
+}
+
+- (void)pauseAppHangTracking
+{
+ self.reportAppHangs = NO;
+}
+
+- (void)resumeAppHangTracking
+{
+ self.reportAppHangs = YES;
+}
+
+- (void)uninstall
+{
+ [self.tracker removeListener:self];
+}
+
+- (void)dealloc
+{
+ [self uninstall];
+}
+
+- (void)anrDetected
+{
+ if (self.reportAppHangs == NO) {
+ SENTRY_LOG_DEBUG(@"AppHangTracking paused. Ignoring reported app hang.")
+ return;
+ }
+
+#if SENTRY_HAS_UIKIT
+ // If the app is not active, the main thread may be blocked or too busy.
+ // Since there is no UI for the user to interact, there is no need to report app hang.
+ if (SentryDependencyContainer.sharedInstance.application.applicationState
+ != UIApplicationStateActive) {
+ return;
+ }
+#endif
+ SentryThreadInspector *threadInspector = SentrySDK.currentHub.getClient.threadInspector;
+
+ NSArray *threads = [threadInspector getCurrentThreadsWithStackTrace];
+
+ if (threads.count == 0) {
+ SENTRY_LOG_WARN(@"Getting current thread returned an empty list. Can't create AppHang "
+ @"event without a stacktrace.");
+ return;
+ }
+
+ NSString *message = [NSString stringWithFormat:@"App hanging for at least %li ms.",
+ (long)(self.options.appHangTimeoutInterval * 1000)];
+ SentryEvent *event = [[SentryEvent alloc] initWithLevel:kSentryLevelError];
+ SentryException *sentryException =
+ [[SentryException alloc] initWithValue:message type:SentryANRExceptionTypeV2];
+
+ sentryException.mechanism = [[SentryMechanism alloc] initWithType:@"AppHang"];
+ sentryException.stacktrace = [threads[0] stacktrace];
+ sentryException.stacktrace.snapshot = @(YES);
+
+ [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
+{
+ // We dont report when an ANR ends.
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Sources/Sentry/SentryBaseIntegration.m b/Sources/Sentry/SentryBaseIntegration.m
index f37baf66e42..f6f7f6698d2 100644
--- a/Sources/Sentry/SentryBaseIntegration.m
+++ b/Sources/Sentry/SentryBaseIntegration.m
@@ -89,6 +89,18 @@ - (BOOL)shouldBeEnabledWithOptions:(SentryOptions *)options
}
}
+ if (integrationOptions & kIntegrationOptionEnableAppHangTrackingV2) {
+ if (!options.enableAppHangTrackingV2) {
+ [self logWithOptionName:@"enableAppHangTrackingV2"];
+ return NO;
+ }
+
+ if (options.appHangTimeoutInterval == 0) {
+ [self logWithReason:@"because appHangTimeoutInterval is 0"];
+ return NO;
+ }
+ }
+
if ((integrationOptions & kIntegrationOptionEnableNetworkTracking)
&& !options.enableNetworkTracking) {
[self logWithOptionName:@"enableNetworkTracking"];
diff --git a/Sources/Sentry/SentryDependencyContainer.m b/Sources/Sentry/SentryDependencyContainer.m
index 5a53361c081..337d3236533 100644
--- a/Sources/Sentry/SentryDependencyContainer.m
+++ b/Sources/Sentry/SentryDependencyContainer.m
@@ -1,4 +1,5 @@
#import "SentryANRTracker.h"
+#import "SentryANRTrackerV2.h"
#import "SentryBinaryImageCache.h"
#import "SentryDispatchFactory.h"
#import "SentryDispatchQueueWrapper.h"
@@ -339,6 +340,24 @@ - (SentryANRTracker *)getANRTracker:(NSTimeInterval)timeout
return _anrTracker;
}
+- (SentryANRTrackerV2 *)getANRTrackerV2:(NSTimeInterval)timeout
+ SENTRY_DISABLE_THREAD_SANITIZER("double-checked lock produce false alarms")
+{
+ if (_anrTrackerV2 == nil) {
+ @synchronized(sentryDependencyContainerLock) {
+ if (_anrTrackerV2 == nil) {
+ _anrTrackerV2 =
+ [[SentryANRTrackerV2 alloc] initWithTimeoutInterval:timeout
+ crashWrapper:self.crashWrapper
+ dispatchQueueWrapper:self.dispatchQueueWrapper
+ threadWrapper:self.threadWrapper];
+ }
+ }
+ }
+
+ return _anrTrackerV2;
+}
+
- (SentryNSProcessInfoWrapper *)processInfoWrapper SENTRY_DISABLE_THREAD_SANITIZER(
"double-checked lock produce false alarms")
{
diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m
index 2a1a995b971..707094ccfab 100644
--- a/Sources/Sentry/SentryOptions.m
+++ b/Sources/Sentry/SentryOptions.m
@@ -122,6 +122,7 @@ - (instancetype)init
#endif // SENTRY_HAS_UIKIT
self.enableAppHangTracking = YES;
self.appHangTimeoutInterval = 2.0;
+ self.enableAppHangTrackingV2 = NO;
self.enableAutoBreadcrumbTracking = YES;
self.enableNetworkTracking = YES;
self.enableFileIOTracing = YES;
@@ -444,6 +445,9 @@ - (BOOL)validateOptions:(NSDictionary *)options
self.appHangTimeoutInterval = [options[@"appHangTimeoutInterval"] doubleValue];
}
+ [self setBool:options[@"enableAppHangTrackingV2"]
+ block:^(BOOL value) { self->_enableAppHangTrackingV2 = value; }];
+
[self setBool:options[@"enableNetworkTracking"]
block:^(BOOL value) { self->_enableNetworkTracking = value; }];
diff --git a/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h b/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h
index 390644cc5c8..db68e110ca7 100644
--- a/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h
+++ b/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h
@@ -1,6 +1,7 @@
#import "SentryDefines.h"
@class SentryANRTracker;
+@class SentryANRTrackerV2;
@class SentryAppStateManager;
@class SentryBinaryImageCache;
@class SentryCrash;
@@ -63,6 +64,7 @@ SENTRY_NO_INIT
@property (nonatomic, strong) SentryNSNotificationCenterWrapper *notificationCenterWrapper;
@property (nonatomic, strong) SentryDebugImageProvider *debugImageProvider;
@property (nonatomic, strong) SentryANRTracker *anrTracker;
+@property (nonatomic, strong) SentryANRTrackerV2 *anrTrackerV2;
@property (nonatomic, strong) SentryNSProcessInfoWrapper *processInfoWrapper;
@property (nonatomic, strong) SentrySystemWrapper *systemWrapper;
@property (nonatomic, strong) SentryDispatchFactory *dispatchFactory;
@@ -89,6 +91,7 @@ SENTRY_NO_INIT
#endif // !TARGET_OS_WATCH
- (SentryANRTracker *)getANRTracker:(NSTimeInterval)timeout;
+- (SentryANRTrackerV2 *)getANRTrackerV2:(NSTimeInterval)timeout;
#if SENTRY_HAS_METRIC_KIT
@property (nonatomic, strong) SentryMXManager *metricKitManager API_AVAILABLE(
diff --git a/Sources/Sentry/include/SentryANRTrackerV2.h b/Sources/Sentry/include/SentryANRTrackerV2.h
new file mode 100644
index 00000000000..b7bb95dfe57
--- /dev/null
+++ b/Sources/Sentry/include/SentryANRTrackerV2.h
@@ -0,0 +1,34 @@
+#import "SentryDefines.h"
+
+@class SentryOptions, SentryCrashWrapper, SentryDispatchQueueWrapper, SentryThreadWrapper;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol SentryANRTrackerV2Delegate;
+
+@interface SentryANRTrackerV2 : NSObject
+SENTRY_NO_INIT
+
+- (instancetype)initWithTimeoutInterval:(NSTimeInterval)timeoutInterval
+ crashWrapper:(SentryCrashWrapper *)crashWrapper
+ dispatchQueueWrapper:(SentryDispatchQueueWrapper *)dispatchQueueWrapper
+ threadWrapper:(SentryThreadWrapper *)threadWrapper;
+
+- (void)addListener:(id)listener;
+
+- (void)removeListener:(id)listener;
+
+// Function used for tests
+- (void)clear;
+
+@end
+
+@protocol SentryANRTrackerV2Delegate
+
+- (void)anrDetected;
+
+- (void)anrStopped;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Sources/Sentry/include/SentryANRTrackingIntegrationV2.h b/Sources/Sentry/include/SentryANRTrackingIntegrationV2.h
new file mode 100644
index 00000000000..88a27b741ea
--- /dev/null
+++ b/Sources/Sentry/include/SentryANRTrackingIntegrationV2.h
@@ -0,0 +1,18 @@
+#import "SentryANRTrackerV2.h"
+#import "SentryBaseIntegration.h"
+#import "SentrySwift.h"
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+static NSString *const SentryANRExceptionTypeV2 = @"App Hanging";
+
+@interface SentryANRTrackingIntegrationV2
+ : SentryBaseIntegration
+
+- (void)pauseAppHangTracking;
+- (void)resumeAppHangTracking;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Sources/Sentry/include/SentryBaseIntegration.h b/Sources/Sentry/include/SentryBaseIntegration.h
index c8adad48fc9..fa803291fc3 100644
--- a/Sources/Sentry/include/SentryBaseIntegration.h
+++ b/Sources/Sentry/include/SentryBaseIntegration.h
@@ -23,6 +23,7 @@ typedef NS_OPTIONS(NSUInteger, SentryIntegrationOption) {
kIntegrationOptionEnableCrashHandler = 1 << 16,
kIntegrationOptionEnableMetricKit = 1 << 17,
kIntegrationOptionEnableReplay = 1 << 18,
+ kIntegrationOptionEnableAppHangTrackingV2 = 1 << 19,
};
@class SentryOptions;
diff --git a/Sources/Sentry/include/SentryOptions+Private.h b/Sources/Sentry/include/SentryOptions+Private.h
index 80570f9dcb5..839ca7e6c4f 100644
--- a/Sources/Sentry/include/SentryOptions+Private.h
+++ b/Sources/Sentry/include/SentryOptions+Private.h
@@ -17,6 +17,8 @@ SentryOptions ()
SENTRY_EXTERN BOOL sentry_isValidSampleRate(NSNumber *sampleRate);
+@property (nonatomic, assign) BOOL enableAppHangTrackingV2;
+
@end
NS_ASSUME_NONNULL_END
diff --git a/Tests/SentryTests/Helper/SentryTestThreadWrapper.swift b/Tests/SentryTests/Helper/SentryTestThreadWrapper.swift
index c4ce81c7300..6bc8de5a722 100644
--- a/Tests/SentryTests/Helper/SentryTestThreadWrapper.swift
+++ b/Tests/SentryTests/Helper/SentryTestThreadWrapper.swift
@@ -9,8 +9,10 @@ class SentryTestThreadWrapper: SentryThreadWrapper {
var threadStartedInvocations = Invocations()
var threadFinishedInvocations = Invocations()
+ public var blockWhenSleeping: () -> Void = {}
override func sleep(forTimeInterval timeInterval: TimeInterval) {
// Don't sleep. Do nothing.
+ blockWhenSleeping()
}
override func threadStarted(_ threadID: UUID) {
diff --git a/Tests/SentryTests/Integrations/ANR/SentryANRTrackerV2Tests.swift b/Tests/SentryTests/Integrations/ANR/SentryANRTrackerV2Tests.swift
new file mode 100644
index 00000000000..57c35133329
--- /dev/null
+++ b/Tests/SentryTests/Integrations/ANR/SentryANRTrackerV2Tests.swift
@@ -0,0 +1,239 @@
+@testable import Sentry
+import SentryTestUtils
+import XCTest
+
+#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst)
+class SentryANRTrackerV2Tests: XCTestCase, SentryANRTrackerV2Delegate {
+
+ private var sut: SentryANRTrackerV2!
+ private var fixture: Fixture!
+ private var anrDetectedExpectation: XCTestExpectation!
+ private var anrStoppedExpectation: XCTestExpectation!
+ private let waitTimeout: TimeInterval = 1.0
+
+ private class Fixture {
+ let timeoutInterval: TimeInterval = 5
+ let currentDate = TestCurrentDateProvider()
+ let crashWrapper: TestSentryCrashWrapper
+ let dispatchQueue = TestSentryDispatchQueueWrapper()
+ let threadWrapper = SentryTestThreadWrapper()
+
+ init() {
+ crashWrapper = TestSentryCrashWrapper.sharedInstance()
+ SentryDependencyContainer.sharedInstance().dateProvider = currentDate
+ }
+ }
+
+ override func setUp() {
+ super.setUp()
+
+ anrDetectedExpectation = expectation(description: "ANR Detection")
+ anrStoppedExpectation = expectation(description: "ANR Stopped")
+ anrStoppedExpectation.isInverted = true
+
+ fixture = Fixture()
+
+ sut = SentryANRTrackerV2(
+ timeoutInterval: fixture.timeoutInterval,
+ crashWrapper: fixture.crashWrapper,
+ dispatchQueueWrapper: fixture.dispatchQueue,
+ threadWrapper: fixture.threadWrapper)
+ }
+
+ override func tearDown() {
+ super.tearDown()
+ sut.clear()
+
+ wait(for: [fixture.threadWrapper.threadFinishedExpectation], timeout: 5)
+ XCTAssertEqual(0, fixture.threadWrapper.threads.count)
+ clearTestState()
+ }
+
+ func start() {
+ sut.addListener(self)
+ }
+
+ func testContinuousANR_OneReported() {
+ fixture.dispatchQueue.blockBeforeMainBlock = {
+ self.advanceTime(bySeconds: self.fixture.timeoutInterval)
+ return false
+ }
+ start()
+
+ wait(for: [anrDetectedExpectation, anrStoppedExpectation], timeout: waitTimeout)
+ }
+
+ func testMultipleListeners() {
+ fixture.dispatchQueue.blockBeforeMainBlock = {
+ self.advanceTime(bySeconds: self.fixture.timeoutInterval)
+ return false
+ }
+
+ let secondListener = SentryANRTrackerV2TestDelegate()
+ sut.addListener(secondListener)
+
+ start()
+
+ wait(for: [anrDetectedExpectation, anrStoppedExpectation, secondListener.anrStoppedExpectation, secondListener.anrDetectedExpectation], timeout: waitTimeout)
+ }
+
+ func testANRButAppInBackground_NoANR() {
+ anrDetectedExpectation.isInverted = true
+ fixture.crashWrapper.internalIsApplicationInForeground = false
+
+ fixture.dispatchQueue.blockBeforeMainBlock = {
+ self.advanceTime(bySeconds: self.fixture.timeoutInterval)
+ return false
+ }
+ start()
+
+ wait(for: [anrDetectedExpectation, anrStoppedExpectation], timeout: waitTimeout)
+ }
+
+ func testMultipleANRs_MultipleReported() {
+ anrDetectedExpectation.expectedFulfillmentCount = 3
+ let expectedANRStoppedInvocations = 2
+ anrStoppedExpectation.isInverted = false
+ anrStoppedExpectation.expectedFulfillmentCount = expectedANRStoppedInvocations
+
+ fixture.dispatchQueue.blockBeforeMainBlock = {
+ self.advanceTime(bySeconds: self.fixture.timeoutInterval)
+ let invocations = self.fixture.dispatchQueue.blockOnMainInvocations.count
+ if [0, 10, 15, 25].contains(invocations) {
+ return true
+ }
+
+ return false
+ }
+ start()
+
+ wait(for: [anrDetectedExpectation, anrStoppedExpectation], timeout: waitTimeout)
+ XCTAssertEqual(expectedANRStoppedInvocations, fixture.dispatchQueue.dispatchAsyncInvocations.count)
+ }
+
+ func testAppSuspended_NoANR() {
+ // To avoid spamming the test logs
+ SentryLog.configure(true, diagnosticLevel: .error)
+
+ anrDetectedExpectation.isInverted = true
+ fixture.dispatchQueue.blockBeforeMainBlock = {
+ let delta = self.fixture.timeoutInterval * 2
+ self.advanceTime(bySeconds: delta)
+ return false
+ }
+ start()
+
+ wait(for: [anrDetectedExpectation, anrStoppedExpectation], timeout: waitTimeout)
+
+ SentryLog.setTestDefaultLogLevel()
+ }
+
+ func testRemoveListener_StopsReportingANRs() {
+ anrDetectedExpectation.isInverted = true
+
+ let mainBlockExpectation = expectation(description: "Main Block")
+
+ fixture.dispatchQueue.blockBeforeMainBlock = {
+ self.sut.removeListener(self)
+ mainBlockExpectation.fulfill()
+ return true
+ }
+
+ start()
+
+ wait(for: [anrDetectedExpectation, anrStoppedExpectation, mainBlockExpectation], timeout: waitTimeout)
+ }
+
+ func testClear_StopsReportingANRs() {
+ let secondListener = SentryANRTrackerV2TestDelegate()
+ secondListener.anrDetectedExpectation.isInverted = true
+ 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()
+ return true
+ }
+
+ sut.addListener(secondListener)
+ start()
+ wait(for: [anrDetectedExpectation, anrStoppedExpectation, mainBlockExpectation, secondListener.anrStoppedExpectation, secondListener.anrDetectedExpectation], timeout: waitTimeout)
+
+ }
+
+ func testNotRemovingDeallocatedListener_DoesNotRetainListener_AndStopsTracking() {
+ anrDetectedExpectation.isInverted = true
+ anrStoppedExpectation.isInverted = true
+
+ // So ARC deallocates SentryANRTrackerTestDelegate
+ let addListenersCount = 10
+ func addListeners() {
+ for _ in 0..
+
+ XCTAssertGreaterThan(addListenersCount, listeners?.count ?? addListenersCount)
+
+ wait(for: [anrDetectedExpectation, anrStoppedExpectation], timeout: 0.0)
+ }
+
+ func testClearDirectlyAfterStart() {
+ anrDetectedExpectation.isInverted = true
+
+ let invocations = 10
+ for _ in 0..= 1
+ }.count
+
+ XCTAssertTrue(threadsWithFrames > 1, "Not enough threads with frames")
+ }
+ }
+
+ func testANRDetected_DetectingPaused_NoEventCaptured() {
+ givenInitializedTracker()
+ setUpThreadInspector()
+ sut.pauseAppHangTracking()
+
+ Dynamic(sut).anrDetected()
+
+ assertNoEventCaptured()
+ }
+
+ func testANRDetected_DetectingPausedResumed_EventCaptured() throws {
+ givenInitializedTracker()
+ setUpThreadInspector()
+ sut.pauseAppHangTracking()
+ sut.resumeAppHangTracking()
+
+ Dynamic(sut).anrDetected()
+
+ try assertEventWithScopeCaptured { event, _, _ in
+ XCTAssertNotNil(event)
+ guard let ex = event?.exceptions?.first else {
+ XCTFail("ANR Exception not found")
+ return
+ }
+
+ XCTAssertEqual(ex.mechanism?.type, "AppHang")
+ }
+ }
+
+ func testCallPauseResumeOnMultipleThreads_DoesNotCrash() {
+ givenInitializedTracker()
+
+ testConcurrentModifications(asyncWorkItems: 100, writeLoopCount: 10, writeWork: {_ in
+ self.sut.pauseAppHangTracking()
+ Dynamic(self.sut).anrDetected()
+ }, readWork: {
+ self.sut.resumeAppHangTracking()
+ Dynamic(self.sut).anrDetected()
+ })
+ }
+
+ func testANRDetected_ButNoThreads_EventNotCaptured() {
+ givenInitializedTracker()
+ setUpThreadInspector(addThreads: false)
+
+ Dynamic(sut).anrDetected()
+
+ assertNoEventCaptured()
+ }
+#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst)
+ func testANRDetected_ButBackground_EventNotCaptured() {
+
+ class BackgroundSentryUIApplication: SentryUIApplication {
+ override var applicationState: UIApplication.State { .background }
+ }
+
+ givenInitializedTracker()
+ setUpThreadInspector()
+ SentryDependencyContainer.sharedInstance().application = BackgroundSentryUIApplication()
+
+ Dynamic(sut).anrDetected()
+
+ assertNoEventCaptured()
+ }
+#endif
+
+ func testDealloc_CallsUninstall() {
+ givenInitializedTracker()
+
+ // // So ARC deallocates the SentryANRTrackingIntegration
+ func initIntegration() {
+ self.crashWrapper.internalIsBeingTraced = false
+ let sut = SentryANRTrackingIntegration()
+ sut.install(with: self.options)
+ }
+
+ initIntegration()
+
+ let tracker = SentryDependencyContainer.sharedInstance().getANRTrackerV2(self.options.appHangTimeoutInterval)
+
+ let listeners = Dynamic(tracker).listeners.asObject as? NSHashTable
+
+ XCTAssertEqual(1, listeners?.count ?? 2)
+ }
+
+ func testEventIsNotANR() {
+ XCTAssertFalse(Event().isAppHangEvent)
+ }
+
+ private func givenInitializedTracker(isBeingTraced: Bool = false) {
+ givenSdkWithHub()
+ self.crashWrapper.internalIsBeingTraced = isBeingTraced
+ sut = SentryANRTrackingIntegrationV2()
+ sut.install(with: self.options)
+ }
+
+ private func setUpThreadInspector(addThreads: Bool = true) {
+ let threadInspector = TestThreadInspector.instance
+
+ if addThreads {
+
+ let frame1 = Sentry.Frame()
+ frame1.function = "Second_frame_function"
+
+ let thread1 = SentryThread(threadId: 0)
+ thread1.stacktrace = SentryStacktrace(frames: [frame1], registers: [:])
+ thread1.current = true
+
+ let frame2 = Sentry.Frame()
+ frame2.function = "main"
+
+ let thread2 = SentryThread(threadId: 1)
+ thread2.stacktrace = SentryStacktrace(frames: [frame2], registers: [:])
+ thread2.current = false
+
+ threadInspector.allThreads = [
+ thread2,
+ thread1
+ ]
+ } else {
+ threadInspector.allThreads = []
+ }
+
+ SentrySDK.currentHub().getClient()?.threadInspector = threadInspector
+ }
+}
diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m
index 1b722f428d8..c0c25f03175 100644
--- a/Tests/SentryTests/SentryOptionsTest.m
+++ b/Tests/SentryTests/SentryOptionsTest.m
@@ -916,6 +916,11 @@ - (void)testEnableAppHangTracking
[self testBooleanField:@"enableAppHangTracking" defaultValue:YES];
}
+- (void)testEnableAppHangTrackingV2
+{
+ [self testBooleanField:@"enableAppHangTrackingV2" defaultValue:NO];
+}
+
- (void)testDefaultAppHangsTimeout
{
SentryOptions *options = [self getValidOptions:@{}];
diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h
index 30a4f05994e..b60da6ae8e0 100644
--- a/Tests/SentryTests/SentryTests-Bridging-Header.h
+++ b/Tests/SentryTests/SentryTests-Bridging-Header.h
@@ -45,7 +45,9 @@
#import "PrivateSentrySDKOnly.h"
#import "Sentry/Sentry-Swift.h"
#import "SentryANRTracker.h"
+#import "SentryANRTrackerV2.h"
#import "SentryANRTrackingIntegration.h"
+#import "SentryANRTrackingIntegrationV2.h"
#import "SentryAppStartMeasurement.h"
#import "SentryAppStartTracker.h"
#import "SentryAppStartTrackingIntegration.h"
From 80f2f3995a48176c62096302a4a87866695e11a8 Mon Sep 17 00:00:00 2001
From: Andrew McKnight
Date: Mon, 12 Aug 2024 08:47:12 -0800
Subject: [PATCH 56/63] chore(iOS-Swift): reenable ui tracing via scheme launch
arg (#4273)
---
.../xcshareddata/xcschemes/iOS-Swift.xcscheme | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme b/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme
index 0173095e27f..9faf745682d 100644
--- a/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme
+++ b/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-Swift.xcscheme
@@ -159,7 +159,7 @@
+ isEnabled = "NO">
From 5c23b776b6352a60698dbab26ba0d2202f6ba6d9 Mon Sep 17 00:00:00 2001
From: Dhiogo Brustolin
Date: Tue, 13 Aug 2024 13:27:10 +0200
Subject: [PATCH 57/63] feat: Pause replay in session mode when offline (#4264)
If the device has no connection we stop trying to send segments.
We do capture replay for errors,
---
CHANGELOG.md | 1 +
.../Sentry/SentrySessionReplayIntegration.m | 19 +++++-
.../SessionReplay/SentrySessionReplay.swift | 53 ++++++++++-----
.../SentrySessionReplayIntegrationTests.swift | 10 +++
.../SentrySessionReplayTests.swift | 65 ++++++++++++++++++-
5 files changed, 128 insertions(+), 20 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e18ffbb7c67..7fe6b8bff87 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
### Features
+- Pause replay in session mode when offline (#4264)
- Add replay quality option for Objective-C (#4267)
## 8.33.0
diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m
index be7439bdd5e..42537d21efc 100644
--- a/Sources/Sentry/SentrySessionReplayIntegration.m
+++ b/Sources/Sentry/SentrySessionReplayIntegration.m
@@ -14,6 +14,7 @@
# import "SentryNSNotificationCenterWrapper.h"
# import "SentryOptions.h"
# import "SentryRandom.h"
+# import "SentryReachability.h"
# import "SentrySDK+Private.h"
# import "SentryScope+Private.h"
# import "SentrySerialization.h"
@@ -22,6 +23,7 @@
# import "SentrySwizzle.h"
# import "SentryUIApplication.h"
# import
+
NS_ASSUME_NONNULL_BEGIN
static NSString *SENTRY_REPLAY_FOLDER = @"replay";
@@ -34,7 +36,7 @@
static SentryTouchTracker *_touchTracker;
@interface
-SentrySessionReplayIntegration ()
+SentrySessionReplayIntegration ()
- (void)newSceneActivate;
@end
@@ -74,6 +76,7 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options
return event;
}];
+ [SentryDependencyContainer.sharedInstance.reachability addObserver:self];
[SentryViewPhotographer.shared addIgnoreClasses:_replayOptions.ignoreRedactViewTypes];
[SentryViewPhotographer.shared addRedactClasses:_replayOptions.redactViewTypes];
@@ -425,7 +428,7 @@ - (SentryTouchTracker *)getTouchTracker
# pragma mark - SessionReplayDelegate
-- (BOOL)sessionReplayIsFullSession
+- (BOOL)sessionReplayShouldCaptureReplayForError
{
return SentryDependencyContainer.sharedInstance.random.nextNumber
<= _replayOptions.onErrorSampleRate;
@@ -464,6 +467,18 @@ - (nullable NSString *)currentScreenNameForSessionReplay
.firstObject;
}
+# pragma mark - SentryReachabilityObserver
+
+- (void)connectivityChanged:(BOOL)connected typeDescription:(nonnull NSString *)typeDescription
+{
+
+ if (connected) {
+ [_sessionReplay resume];
+ } else {
+ [_sessionReplay pause];
+ }
+}
+
@end
NS_ASSUME_NONNULL_END
diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift
index 020e94d6bd7..d413ea0d649 100644
--- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift
+++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift
@@ -10,7 +10,7 @@ enum SessionReplayError: Error {
@objc
protocol SentrySessionReplayDelegate: NSObjectProtocol {
- func sessionReplayIsFullSession() -> Bool
+ func sessionReplayShouldCaptureReplayForError() -> Bool
func sessionReplayNewSegment(replayEvent: SentryReplayEvent, replayRecording: SentryReplayRecording, videoUrl: URL)
func sessionReplayStarted(replayId: SentryId)
func breadcrumbsForSessionReplay() -> [Breadcrumb]
@@ -32,6 +32,7 @@ class SentrySessionReplay: NSObject {
private var currentSegmentId = 0
private var processingScreenshot = false
private var reachedMaximumDuration = false
+ private(set) var isSessionPaused = false
private let replayOptions: SentryReplayOptions
private let replayMaker: SentryReplayVideoMaker
@@ -70,6 +71,8 @@ class SentrySessionReplay: NSObject {
self.breadcrumbConverter = breadcrumbConverter
self.touchTracker = touchTracker
}
+
+ deinit { displayLink.invalidate() }
func start(rootView: UIView, fullSession: Bool) {
guard !isRunning else { return }
@@ -93,26 +96,41 @@ class SentrySessionReplay: NSObject {
delegate?.sessionReplayStarted(replayId: sessionReplayId)
}
+ func pause() {
+ lock.lock()
+ defer { lock.unlock() }
+
+ self.isSessionPaused = true
+ self.videoSegmentStart = nil
+ }
+
func stop() {
+ lock.lock()
+ defer { lock.unlock() }
+
displayLink.invalidate()
- prepareSegmentUntil(date: dateProvider.date())
+ if isFullSession {
+ prepareSegmentUntil(date: dateProvider.date())
+ }
+ isSessionPaused = false
}
func resume() {
- guard !reachedMaximumDuration else { return }
-
lock.lock()
defer { lock.unlock() }
+
+ if isSessionPaused {
+ isSessionPaused = false
+ return
+ }
+
+ guard !reachedMaximumDuration else { return }
guard !isRunning else { return }
videoSegmentStart = nil
displayLink.link(withTarget: self, selector: #selector(newFrame(_:)))
}
- deinit {
- displayLink.invalidate()
- }
-
func captureReplayFor(event: Event) {
guard isRunning else { return }
@@ -132,15 +150,14 @@ class SentrySessionReplay: NSObject {
guard isRunning else { return false }
guard !isFullSession else { return true }
- guard delegate?.sessionReplayIsFullSession() == true else {
+ guard delegate?.sessionReplayShouldCaptureReplayForError() == true else {
return false
}
startFullReplay()
let replayStart = dateProvider.date().addingTimeInterval(-replayOptions.errorReplayDuration - (Double(replayOptions.frameRate) / 2.0))
- createAndCapture(startedAt: replayStart)
-
+ createAndCapture(startedAt: replayStart, replayType: .buffer)
return true
}
@@ -160,7 +177,9 @@ class SentrySessionReplay: NSObject {
@objc
private func newFrame(_ sender: CADisplayLink) {
- guard let lastScreenShot = lastScreenShot, isRunning else { return }
+ guard let lastScreenShot = lastScreenShot, isRunning &&
+ !(isFullSession && isSessionPaused) //If replay is in session mode but it is paused we dont take screenshots
+ else { return }
let now = dateProvider.date()
@@ -199,10 +218,10 @@ class SentrySessionReplay: NSObject {
pathToSegment = pathToSegment.appendingPathComponent("\(currentSegmentId).mp4")
let segmentStart = videoSegmentStart ?? dateProvider.date().addingTimeInterval(-replayOptions.sessionSegmentDuration)
- createAndCapture(startedAt: segmentStart)
+ createAndCapture(startedAt: segmentStart, replayType: .session)
}
- private func createAndCapture(startedAt: Date) {
+ private func createAndCapture(startedAt: Date, replayType: SentryReplayType) {
//Creating a video is heavy and blocks the thread
//Since this function is always called in the main thread
//we dispatch it to a background thread.
@@ -210,7 +229,7 @@ class SentrySessionReplay: NSObject {
do {
let videos = try self.replayMaker.createVideoWith(beginning: startedAt, end: self.dateProvider.date())
for video in videos {
- self.newSegmentAvailable(videoInfo: video)
+ self.newSegmentAvailable(videoInfo: video, replayType: replayType)
}
} catch {
SentryLog.debug("Could not create replay video - \(error.localizedDescription)")
@@ -218,9 +237,9 @@ class SentrySessionReplay: NSObject {
}
}
- private func newSegmentAvailable(videoInfo: SentryVideoInfo) {
+ private func newSegmentAvailable(videoInfo: SentryVideoInfo, replayType: SentryReplayType) {
guard let sessionReplayId = sessionReplayId else { return }
- captureSegment(segment: currentSegmentId, video: videoInfo, replayId: sessionReplayId, replayType: .session)
+ captureSegment(segment: currentSegmentId, video: videoInfo, replayId: sessionReplayId, replayType: replayType)
replayMaker.releaseFramesUntil(videoInfo.end)
videoSegmentStart = videoInfo.end
currentSegmentId++
diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift
index 90ba6a2cd4d..890d8693611 100644
--- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift
+++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift
@@ -31,6 +31,7 @@ class SentrySessionReplayIntegrationTests: XCTestCase {
override func setUp() {
SentryDependencyContainer.sharedInstance().application = uiApplication
+ SentryDependencyContainer.sharedInstance().reachability = TestSentryReachability()
}
override func tearDown() {
@@ -273,6 +274,15 @@ class SentrySessionReplayIntegrationTests: XCTestCase {
XCTAssertEqual(hub.capturedReplayRecordingVideo.count, 0)
}
+ func testPauseSessionReplayWithReacheability() throws {
+ startSDK(sessionSampleRate: 1, errorSampleRate: 0)
+ let sut = try getSut()
+ (sut as? SentryReachabilityObserver)?.connectivityChanged(false, typeDescription: "")
+ XCTAssertTrue(sut.sessionReplay.isSessionPaused)
+ (sut as? SentryReachabilityObserver)?.connectivityChanged(true, typeDescription: "")
+ XCTAssertFalse(sut.sessionReplay.isSessionPaused)
+ }
+
func testMaskViewFromSDK() {
class AnotherLabel: UILabel {
}
diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift
index a36f3a1abbd..40312644a7d 100644
--- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift
+++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift
@@ -76,7 +76,7 @@ class SentrySessionReplayTests: XCTestCase {
displayLinkWrapper: displayLink)
}
- func sessionReplayIsFullSession() -> Bool {
+ func sessionReplayShouldCaptureReplayForError() -> Bool {
return isFullSession
}
@@ -251,6 +251,69 @@ class SentrySessionReplayTests: XCTestCase {
XCTAssertNotNil(fixture.screenshotProvider.lastImageCall)
}
+ func testPauseResume_FullSession() {
+ let fixture = Fixture()
+
+ let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1))
+ sut.start(rootView: fixture.rootView, fullSession: true)
+
+ fixture.dateProvider.advance(by: 1)
+ Dynamic(sut).newFrame(nil)
+ XCTAssertNotNil(fixture.screenshotProvider.lastImageCall)
+ sut.pause()
+ fixture.screenshotProvider.lastImageCall = nil
+
+ fixture.dateProvider.advance(by: 1)
+ Dynamic(sut).newFrame(nil)
+ XCTAssertNil(fixture.screenshotProvider.lastImageCall)
+
+ fixture.dateProvider.advance(by: 4)
+ Dynamic(sut).newFrame(nil)
+ XCTAssertNil(fixture.replayMaker.lastCallToCreateVideo)
+
+ sut.resume()
+
+ fixture.dateProvider.advance(by: 1)
+ Dynamic(sut).newFrame(nil)
+ XCTAssertNotNil(fixture.screenshotProvider.lastImageCall)
+
+ fixture.dateProvider.advance(by: 5)
+ Dynamic(sut).newFrame(nil)
+ XCTAssertNotNil(fixture.replayMaker.lastCallToCreateVideo)
+ }
+
+ func testPause_BufferSession() {
+ let fixture = Fixture()
+
+ let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 0, onErrorSampleRate: 1))
+ sut.start(rootView: fixture.rootView, fullSession: false)
+
+ fixture.dateProvider.advance(by: 1)
+
+ Dynamic(sut).newFrame(nil)
+ XCTAssertNotNil(fixture.screenshotProvider.lastImageCall)
+ sut.pause()
+ fixture.screenshotProvider.lastImageCall = nil
+
+ fixture.dateProvider.advance(by: 1)
+ Dynamic(sut).newFrame(nil)
+ XCTAssertNotNil(fixture.screenshotProvider.lastImageCall)
+
+ fixture.dateProvider.advance(by: 4)
+ Dynamic(sut).newFrame(nil)
+
+ let event = Event(error: NSError(domain: "Some error", code: 1))
+ sut.captureReplayFor(event: event)
+
+ XCTAssertNotNil(fixture.replayMaker.lastCallToCreateVideo)
+
+ //After changing to session mode the replay should pause
+ fixture.screenshotProvider.lastImageCall = nil
+ fixture.dateProvider.advance(by: 1)
+ Dynamic(sut).newFrame(nil)
+ XCTAssertNil(fixture.screenshotProvider.lastImageCall)
+ }
+
@available(iOS 16.0, tvOS 16, *)
func testDealloc_CallsStop() {
let fixture = Fixture()
From 02671d850aa5eb27ea7810311729a22911d2d3a1 Mon Sep 17 00:00:00 2001
From: Dhiogo Brustolin
Date: Tue, 13 Aug 2024 13:27:34 +0200
Subject: [PATCH 58/63] fix: Session replay not redacting buttons and other non
UILabel texts (#4277)
Subclasses of classes that should be redacted were not
---
CHANGELOG.md | 4 ++++
Sources/Swift/Tools/UIRedactBuilder.swift | 11 ++++++++--
Tests/SentryTests/UIRedactBuilderTests.swift | 21 ++++++++++++++------
3 files changed, 28 insertions(+), 8 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7fe6b8bff87..8b9252b1e91 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@
- Pause replay in session mode when offline (#4264)
- Add replay quality option for Objective-C (#4267)
+### Fixes
+
+- Session replay not redacting buttons and other non UILabel texts (#4277)
+
## 8.33.0
This release fixes a bug (#4230) that we introduced with a refactoring (#4101) released in [8.30.1](https://github.com/getsentry/sentry-cocoa/releases/tag/8.30.1).
diff --git a/Sources/Swift/Tools/UIRedactBuilder.swift b/Sources/Swift/Tools/UIRedactBuilder.swift
index a8efef15903..e74ff98e708 100644
--- a/Sources/Swift/Tools/UIRedactBuilder.swift
+++ b/Sources/Swift/Tools/UIRedactBuilder.swift
@@ -77,11 +77,18 @@ class UIRedactBuilder {
}
func containsIgnoreClass(_ ignoreClass: AnyClass) -> Bool {
- return ignoreClassesIdentifiers.contains(ObjectIdentifier(ignoreClass))
+ return ignoreClassesIdentifiers.contains(ObjectIdentifier(ignoreClass))
}
func containsRedactClass(_ redactClass: AnyClass) -> Bool {
- return redactClassesIdentifiers.contains(ObjectIdentifier(redactClass))
+ var currentClass: AnyClass? = redactClass
+ while currentClass != nil && currentClass != UIView.self {
+ if let currentClass = currentClass, redactClassesIdentifiers.contains(ObjectIdentifier(currentClass)) {
+ return true
+ }
+ currentClass = currentClass?.superclass()
+ }
+ return false
}
func addIgnoreClass(_ ignoreClass: AnyClass) {
diff --git a/Tests/SentryTests/UIRedactBuilderTests.swift b/Tests/SentryTests/UIRedactBuilderTests.swift
index 725a8698797..ca3812a64d0 100644
--- a/Tests/SentryTests/UIRedactBuilderTests.swift
+++ b/Tests/SentryTests/UIRedactBuilderTests.swift
@@ -146,18 +146,15 @@ class UIRedactBuilderTests: XCTestCase {
}
func testIgnoreClasses() {
- class AnotherLabel: UILabel {
- }
-
let sut = UIRedactBuilder()
- sut.addIgnoreClass(AnotherLabel.self)
- rootView.addSubview(AnotherLabel(frame: CGRect(x: 20, y: 20, width: 40, height: 40)))
+ sut.addIgnoreClass(UILabel.self)
+ rootView.addSubview(UILabel(frame: CGRect(x: 20, y: 20, width: 40, height: 40)))
let result = sut.redactRegionsFor(view: rootView, options: RedactOptions())
XCTAssertEqual(result.count, 0)
}
- func testRedactlasses() {
+ func testRedactClasses() {
class AnotherView: UIView {
}
@@ -170,6 +167,18 @@ class UIRedactBuilderTests: XCTestCase {
XCTAssertEqual(result.count, 1)
}
+ func testRedactSubClass() {
+ class AnotherView: UILabel {
+ }
+
+ let sut = UIRedactBuilder()
+ let view = AnotherView(frame: CGRect(x: 20, y: 20, width: 40, height: 40))
+ rootView.addSubview(view)
+
+ let result = sut.redactRegionsFor(view: rootView, options: RedactOptions())
+ XCTAssertEqual(result.count, 1)
+ }
+
func testIgnoreView() {
class AnotherLabel: UILabel {
}
From 464117d76c03363a8a5699b3edd07c57398c9518 Mon Sep 17 00:00:00 2001
From: Karl Heinz Struggl
Date: Wed, 14 Aug 2024 00:13:49 -0700
Subject: [PATCH 59/63] fix: Crash deserializing empty envelope length>0
(#4281)
* fixes crash deserializing empty envelope length>0
* changelog
---
CHANGELOG.md | 2 ++
Sources/Sentry/SentrySerialization.m | 6 +++---
Tests/SentryTests/PrivateSentrySDKOnlyTests.swift | 5 +++++
3 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8b9252b1e91..74efc2cf41b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,8 @@
### Fixes
- Session replay not redacting buttons and other non UILabel texts (#4277)
+- Crash deserializing empty envelope length>0 (#4281]
+
## 8.33.0
diff --git a/Sources/Sentry/SentrySerialization.m b/Sources/Sentry/SentrySerialization.m
index 7f88e521d88..96fe605aeee 100644
--- a/Sources/Sentry/SentrySerialization.m
+++ b/Sources/Sentry/SentrySerialization.m
@@ -162,9 +162,6 @@ + (SentryEnvelope *_Nullable)envelopeWithData:(NSData *)data
NSUInteger endOfEnvelope = data.length - 1;
for (NSInteger i = itemHeaderStart; i <= endOfEnvelope; ++i) {
if (bytes[i] == '\n' || i == endOfEnvelope) {
- if (endOfEnvelope == i) {
- i++; // 0 byte attachment
- }
NSData *itemHeaderData =
[data subdataWithRange:NSMakeRange(itemHeaderStart, i - itemHeaderStart)];
@@ -222,6 +219,9 @@ + (SentryEnvelope *_Nullable)envelopeWithData:(NSData *)data
itemHeader = [[SentryEnvelopeItemHeader alloc] initWithType:type length:bodyLength];
}
+ if (endOfEnvelope == i) {
+ i++; // 0 byte attachment
+ }
NSData *itemBody = [data subdataWithRange:NSMakeRange(i + 1, bodyLength)];
#ifdef DEBUG
if ([SentryEnvelopeItemTypeEvent isEqual:type] ||
diff --git a/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift b/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift
index 691734878ed..3aa3fdf6d8d 100644
--- a/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift
+++ b/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift
@@ -114,6 +114,11 @@ class PrivateSentrySDKOnlyTests: XCTestCase {
let itemData = "{}\n{\"length\":0,\"type\":\"attachment\"}\n".data(using: .utf8)!
XCTAssertNotNil(PrivateSentrySDKOnly.envelope(with: itemData))
}
+
+ func testEnvelopeWithDataLengthGtZero() throws {
+ let itemData = "{}\n{\"length\":1,\"type\":\"attachment\"}\n".data(using: .utf8)!
+ XCTAssertNil(PrivateSentrySDKOnly.envelope(with: itemData))
+ }
func testGetDebugImages() {
let images = PrivateSentrySDKOnly.getDebugImages()
From a4ffa76e716daae7636910900683eea03ace9c00 Mon Sep 17 00:00:00 2001
From: Philipp Hofmann
Date: Wed, 14 Aug 2024 11:52:55 +0200
Subject: [PATCH 60/63] fix: Rarely reporting too long frame delays (#4278)
Fix a race condition in the SentryFramesTracker and
SentryDelayedFramesTracker that sometimes leads to frame delay durations
longer than the queried interval of start and end time. This is fixed by
moving the previousFrameSystemTimestamp down to the
SentryDelayedFramesTracker so we can adequately synchronize it without
acquiring extra locks on the main thread.
---
CHANGELOG.md | 2 +-
SentryTestUtils/TestDisplayLinkWrapper.swift | 5 ++
Sources/Sentry/SentryDelayedFramesTracker.m | 47 +++++++++++++++----
Sources/Sentry/SentryFramesTracker.m | 18 ++-----
.../include/SentryDelayedFramesTracker.h | 9 ++--
.../SentryFramesTrackerTests.swift | 35 ++++++++++++++
6 files changed, 88 insertions(+), 28 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 74efc2cf41b..c7909374f0d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,9 +10,9 @@
### Fixes
- Session replay not redacting buttons and other non UILabel texts (#4277)
+- Rarely reporting too long frame delays (#4278) by fixing a race condition in the frames tracking logic.
- Crash deserializing empty envelope length>0 (#4281]
-
## 8.33.0
This release fixes a bug (#4230) that we introduced with a refactoring (#4101) released in [8.30.1](https://github.com/getsentry/sentry-cocoa/releases/tag/8.30.1).
diff --git a/SentryTestUtils/TestDisplayLinkWrapper.swift b/SentryTestUtils/TestDisplayLinkWrapper.swift
index 246201310d5..d0bcc16f043 100644
--- a/SentryTestUtils/TestDisplayLinkWrapper.swift
+++ b/SentryTestUtils/TestDisplayLinkWrapper.swift
@@ -114,6 +114,11 @@ public class TestDisplayLinkWrapper: SentryDisplayLinkWrapper {
call()
return fastestFrozenFrameDuration
}
+
+ public func frameWith(delay: Double) {
+ dateProvider.advance(by: currentFrameRate.tickDuration + delay)
+ call()
+ }
/// There's no upper bound for a frozen frame, except maybe for the watchdog time limit.
/// - parameter extraTime: the additional time to add to the frozen frame threshold when simulating a frozen frame.
diff --git a/Sources/Sentry/SentryDelayedFramesTracker.m b/Sources/Sentry/SentryDelayedFramesTracker.m
index 5a515e5cd55..bf795f006c6 100644
--- a/Sources/Sentry/SentryDelayedFramesTracker.m
+++ b/Sources/Sentry/SentryDelayedFramesTracker.m
@@ -3,6 +3,7 @@
#if SENTRY_HAS_UIKIT
# import "SentryDelayedFrame.h"
+# import "SentryInternalCDefines.h"
# import "SentryLog.h"
# import "SentrySwift.h"
# import "SentryTime.h"
@@ -15,6 +16,8 @@
@property (nonatomic, assign) CFTimeInterval keepDelayedFramesDuration;
@property (nonatomic, strong, readonly) SentryCurrentDateProvider *dateProvider;
@property (nonatomic, strong) NSMutableArray *delayedFrames;
+@property (nonatomic) uint64_t lastDelayedFrameSystemTimestamp;
+@property (nonatomic) uint64_t previousFrameSystemTimestamp;
@end
@@ -31,6 +34,20 @@ - (instancetype)initWithKeepDelayedFramesDuration:(CFTimeInterval)keepDelayedFra
return self;
}
+- (void)setPreviousFrameSystemTimestamp:(uint64_t)previousFrameSystemTimestamp
+ SENTRY_DISABLE_THREAD_SANITIZER("We don't want to synchronize the access to this property to "
+ "avoid slowing down the main thread.")
+{
+ _previousFrameSystemTimestamp = previousFrameSystemTimestamp;
+}
+
+- (uint64_t)getPreviousFrameSystemTimestamp SENTRY_DISABLE_THREAD_SANITIZER(
+ "We don't want to synchronize the access to this property to avoid slowing down the main "
+ "thread.")
+{
+ return _previousFrameSystemTimestamp;
+}
+
- (void)resetDelayedFramesTimeStamps
{
_delayedFrames = [NSMutableArray array];
@@ -42,9 +59,13 @@ - (void)resetDelayedFramesTimeStamps
}
- (void)recordDelayedFrame:(uint64_t)startSystemTimestamp
- expectedDuration:(CFTimeInterval)expectedDuration
- actualDuration:(CFTimeInterval)actualDuration
+ thisFrameSystemTimestamp:(uint64_t)thisFrameSystemTimestamp
+ expectedDuration:(CFTimeInterval)expectedDuration
+ actualDuration:(CFTimeInterval)actualDuration
{
+ // This @synchronized block only gets called for delayed frames.
+ // We accept the tradeoff of slowing down the main thread a bit to
+ // record the frame delay data.
@synchronized(self.delayedFrames) {
[self removeOldDelayedFrames];
@@ -53,6 +74,8 @@ - (void)recordDelayedFrame:(uint64_t)startSystemTimestamp
expectedDuration:expectedDuration
actualDuration:actualDuration];
[self.delayedFrames addObject:delayedFrame];
+ self.lastDelayedFrameSystemTimestamp = thisFrameSystemTimestamp;
+ self.previousFrameSystemTimestamp = thisFrameSystemTimestamp;
}
}
@@ -92,7 +115,6 @@ - (void)removeOldDelayedFrames
- (CFTimeInterval)getFramesDelay:(uint64_t)startSystemTimestamp
endSystemTimestamp:(uint64_t)endSystemTimestamp
isRunning:(BOOL)isRunning
- previousFrameSystemTimestamp:(uint64_t)previousFrameSystemTimestamp
slowFrameThreshold:(CFTimeInterval)slowFrameThreshold
{
CFTimeInterval cantCalculateFrameDelayReturnValue = -1.0;
@@ -114,13 +136,20 @@ - (CFTimeInterval)getFramesDelay:(uint64_t)startSystemTimestamp
return cantCalculateFrameDelayReturnValue;
}
- if (previousFrameSystemTimestamp == 0) {
- SENTRY_LOG_DEBUG(@"Not calculating frames delay because no frames yet recorded.");
- return cantCalculateFrameDelayReturnValue;
- }
+ // Make a local copy because this method can be called on a background thread and the value
+ // could change.
+ uint64_t localPreviousFrameSystemTimestamp;
NSMutableArray *frames;
@synchronized(self.delayedFrames) {
+
+ localPreviousFrameSystemTimestamp = self.previousFrameSystemTimestamp;
+
+ if (localPreviousFrameSystemTimestamp == 0) {
+ SENTRY_LOG_DEBUG(@"Not calculating frames delay because no frames yet recorded.");
+ return cantCalculateFrameDelayReturnValue;
+ }
+
uint64_t oldestDelayedFrameStartTimestamp = UINT64_MAX;
SentryDelayedFrame *oldestDelayedFrame = self.delayedFrames.firstObject;
if (oldestDelayedFrame != nil) {
@@ -139,10 +168,10 @@ - (CFTimeInterval)getFramesDelay:(uint64_t)startSystemTimestamp
// Add a delayed frame for a potentially ongoing but not recorded delayed frame
SentryDelayedFrame *currentFrameDelay = [[SentryDelayedFrame alloc]
- initWithStartTimestamp:previousFrameSystemTimestamp
+ initWithStartTimestamp:localPreviousFrameSystemTimestamp
expectedDuration:slowFrameThreshold
actualDuration:nanosecondsToTimeInterval(
- endSystemTimestamp - previousFrameSystemTimestamp)];
+ endSystemTimestamp - localPreviousFrameSystemTimestamp)];
[frames addObject:currentFrameDelay];
diff --git a/Sources/Sentry/SentryFramesTracker.m b/Sources/Sentry/SentryFramesTracker.m
index 44f1e911ef8..7edacf06554 100644
--- a/Sources/Sentry/SentryFramesTracker.m
+++ b/Sources/Sentry/SentryFramesTracker.m
@@ -96,19 +96,6 @@ - (void)setDisplayLinkWrapper:(SentryDisplayLinkWrapper *)displayLinkWrapper
{
_displayLinkWrapper = displayLinkWrapper;
}
-- (void)setPreviousFrameSystemTimestamp:(uint64_t)previousFrameSystemTimestamp
- SENTRY_DISABLE_THREAD_SANITIZER("As you can only disable the thread sanitizer for methods, we "
- "must manually create the setter here.")
-{
- _previousFrameSystemTimestamp = previousFrameSystemTimestamp;
-}
-
-- (uint64_t)getPreviousFrameSystemTimestamp SENTRY_DISABLE_THREAD_SANITIZER(
- "As you can only disable the thread sanitizer for methods, we must manually create the getter "
- "here.")
-{
- return _previousFrameSystemTimestamp;
-}
- (void)resetFrames
{
@@ -193,6 +180,7 @@ - (void)displayLinkCallback
if (self.previousFrameTimestamp == SentryPreviousFrameInitialValue) {
self.previousFrameTimestamp = thisFrameTimestamp;
self.previousFrameSystemTimestamp = thisFrameSystemTimestamp;
+ [self.delayedFramesTracker setPreviousFrameSystemTimestamp:thisFrameSystemTimestamp];
[self reportNewFrame];
return;
}
@@ -254,8 +242,11 @@ - (void)displayLinkCallback
if (frameDuration > slowFrameThreshold(_currentFrameRate)) {
[self.delayedFramesTracker recordDelayedFrame:self.previousFrameSystemTimestamp
+ thisFrameSystemTimestamp:thisFrameSystemTimestamp
expectedDuration:slowFrameThreshold(_currentFrameRate)
actualDuration:frameDuration];
+ } else {
+ [self.delayedFramesTracker setPreviousFrameSystemTimestamp:thisFrameSystemTimestamp];
}
_totalFrames++;
@@ -314,7 +305,6 @@ - (CFTimeInterval)getFramesDelay:(uint64_t)startSystemTimestamp
return [self.delayedFramesTracker getFramesDelay:startSystemTimestamp
endSystemTimestamp:endSystemTimestamp
isRunning:_isRunning
- previousFrameSystemTimestamp:self.previousFrameSystemTimestamp
slowFrameThreshold:slowFrameThreshold(_currentFrameRate)];
}
diff --git a/Sources/Sentry/include/SentryDelayedFramesTracker.h b/Sources/Sentry/include/SentryDelayedFramesTracker.h
index bb02b3bf7f6..17d76c34ac1 100644
--- a/Sources/Sentry/include/SentryDelayedFramesTracker.h
+++ b/Sources/Sentry/include/SentryDelayedFramesTracker.h
@@ -23,8 +23,11 @@ SENTRY_NO_INIT
- (void)resetDelayedFramesTimeStamps;
- (void)recordDelayedFrame:(uint64_t)startSystemTimestamp
- expectedDuration:(CFTimeInterval)expectedDuration
- actualDuration:(CFTimeInterval)actualDuration;
+ thisFrameSystemTimestamp:(uint64_t)thisFrameSystemTimestamp
+ expectedDuration:(CFTimeInterval)expectedDuration
+ actualDuration:(CFTimeInterval)actualDuration;
+
+- (void)setPreviousFrameSystemTimestamp:(uint64_t)previousFrameSystemTimestamp;
/**
* This method returns the duration of all delayed frames between startSystemTimestamp and
@@ -44,7 +47,6 @@ SENTRY_NO_INIT
* delay.
* @param endSystemTimestamp The end system time stamp for the time interval to query frames delay.
* @param isRunning Wether the frames tracker is running or not.
- * @param previousFrameSystemTimestamp The system timestamp of the previous frame.
* @param slowFrameThreshold The threshold for a slow frame. For 60 fps this is roughly 16.67 ms.
*
* @return the frames delay duration or -1 if it can't calculate the frames delay.
@@ -52,7 +54,6 @@ SENTRY_NO_INIT
- (CFTimeInterval)getFramesDelay:(uint64_t)startSystemTimestamp
endSystemTimestamp:(uint64_t)endSystemTimestamp
isRunning:(BOOL)isRunning
- previousFrameSystemTimestamp:(uint64_t)previousFrameSystemTimestamp
slowFrameThreshold:(CFTimeInterval)slowFrameThreshold;
@end
diff --git a/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift
index 2c7774fd9b1..790ae532a7a 100644
--- a/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift
+++ b/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift
@@ -534,6 +534,41 @@ class SentryFramesTrackerTests: XCTestCase {
wait(for: [expectation], timeout: 3.0)
}
+ func testGetFramesDelayOnTightLoop_WhileKeepAddingDelayedFrames() {
+ let displayLink = fixture.displayLinkWrapper
+ let dateProvider = fixture.dateProvider
+
+ let sut = fixture.sut
+ sut.start()
+
+ for _ in 0..<100 {
+ displayLink.normalFrame()
+ }
+
+ let expectation = expectation(description: "Get Frames Delays")
+
+ DispatchQueue.global().async {
+
+ for _ in 0..<1_000 {
+
+ let endSystemTimestamp = dateProvider.systemTime()
+ let startSystemTimestamp = endSystemTimestamp - timeIntervalToNanoseconds(1.0)
+
+ let frameDelay = sut.getFramesDelay(startSystemTimestamp, endSystemTimestamp: endSystemTimestamp)
+
+ XCTAssertLessThanOrEqual(frameDelay, 1.0)
+ }
+
+ expectation.fulfill()
+ }
+
+ for _ in 0..<1_000 {
+ displayLink.frameWith(delay: 1.0)
+ }
+
+ wait(for: [expectation], timeout: 3.0)
+ }
+
func testAddMultipleListeners_AllCalledWithSameDate() {
let sut = fixture.sut
let listener1 = FrameTrackerListener()
From df2835d9cbe51cae572a5c90204bfcf35b38162e Mon Sep 17 00:00:00 2001
From: Philipp Hofmann
Date: Wed, 14 Aug 2024 13:03:53 +0200
Subject: [PATCH 61/63] ref: Add FramesDelayResult (#4279)
Add SentryFramesDelayResult containing framesContributingToDelayCount,
which is the count for the frames that contributed to the frames delay
count. This is required for GH-3492.
---
Sentry.xcodeproj/project.pbxproj | 12 +++++
Sources/Sentry/SentryDelayedFramesTracker.m | 18 +++++---
Sources/Sentry/SentryFramesTracker.m | 5 ++-
Sources/Sentry/SentrySpan.m | 3 +-
Sources/Sentry/SentryTracer.m | 3 +-
.../HybridPublic/SentryFramesTracker.h | 9 ++--
.../include/SentryDelayedFramesTracker.h | 11 +++--
.../SentryFramesDelayResult.swift | 13 ++++++
.../SentryFramesTrackerTests.swift | 44 +++++++++++--------
9 files changed, 78 insertions(+), 40 deletions(-)
create mode 100644 Sources/Swift/Integrations/FramesTracking/SentryFramesDelayResult.swift
diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj
index d19c857a7a9..da34ccf245c 100644
--- a/Sentry.xcodeproj/project.pbxproj
+++ b/Sentry.xcodeproj/project.pbxproj
@@ -122,6 +122,7 @@
62B0C30D2BA9D39600648D59 /* CounterMetricTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62B0C30C2BA9D39600648D59 /* CounterMetricTests.swift */; };
62B0C30F2BA9D74800648D59 /* DistributionMetricTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62B0C30E2BA9D74800648D59 /* DistributionMetricTests.swift */; };
62B0C3112BA9D85C00648D59 /* SetMetricTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62B0C3102BA9D85C00648D59 /* SetMetricTests.swift */; };
+ 62B558B02C6B9C3C00C34FEC /* SentryFramesDelayResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62B558AF2C6B9C3C00C34FEC /* SentryFramesDelayResult.swift */; };
62B86CFC29F052BB008F3947 /* SentryTestLogConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 62B86CFB29F052BB008F3947 /* SentryTestLogConfig.m */; };
62BAD74E2BA1C58D00EBAAFC /* EncodeMetricTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62262B952BA1C564004DA3DD /* EncodeMetricTests.swift */; };
62BAD7502BA1C5AF00EBAAFC /* SentryMetricsClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62BAD74F2BA1C5AF00EBAAFC /* SentryMetricsClientTests.swift */; };
@@ -1106,6 +1107,7 @@
62B0C30C2BA9D39600648D59 /* CounterMetricTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterMetricTests.swift; sourceTree = ""; };
62B0C30E2BA9D74800648D59 /* DistributionMetricTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DistributionMetricTests.swift; sourceTree = ""; };
62B0C3102BA9D85C00648D59 /* SetMetricTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetMetricTests.swift; sourceTree = ""; };
+ 62B558AF2C6B9C3C00C34FEC /* SentryFramesDelayResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryFramesDelayResult.swift; sourceTree = ""; };
62B86CFB29F052BB008F3947 /* SentryTestLogConfig.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryTestLogConfig.m; sourceTree = ""; };
62BAD74F2BA1C5AF00EBAAFC /* SentryMetricsClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryMetricsClientTests.swift; sourceTree = ""; };
62BAD7552BA202C300EBAAFC /* SentryMetricsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryMetricsClient.swift; sourceTree = ""; };
@@ -2152,6 +2154,14 @@
path = Extensions;
sourceTree = "";
};
+ 62B558AE2C6B9C3000C34FEC /* FramesTracking */ = {
+ isa = PBXGroup;
+ children = (
+ 62B558AF2C6B9C3C00C34FEC /* SentryFramesDelayResult.swift */,
+ );
+ path = FramesTracking;
+ sourceTree = "";
+ };
630436001EBCB87500C4D3FA /* Networking */ = {
isa = PBXGroup;
children = (
@@ -3883,6 +3893,7 @@
D8CAC02D2BA0663E00E38F34 /* Integrations */ = {
isa = PBXGroup;
children = (
+ 62B558AE2C6B9C3000C34FEC /* FramesTracking */,
D8739CF72BECFF92007D2F66 /* Performance */,
D8CAC02C2BA0663E00E38F34 /* SessionReplay */,
);
@@ -4717,6 +4728,7 @@
9286059729A5098900F96038 /* SentryGeo.m in Sources */,
7B42C48227E08F4B009B58C2 /* SentryDependencyContainer.m in Sources */,
639FCFAD1EBC811400778193 /* SentryUser.m in Sources */,
+ 62B558B02C6B9C3C00C34FEC /* SentryFramesDelayResult.swift in Sources */,
7DAC589123D8B2E0001CF26B /* SentryGlobalEventProcessor.m in Sources */,
7BBD189E244EC8D200427C76 /* SentryRetryAfterHeaderParser.m in Sources */,
63FE711920DA4C1000CDBAE8 /* SentryCrashMachineContext.c in Sources */,
diff --git a/Sources/Sentry/SentryDelayedFramesTracker.m b/Sources/Sentry/SentryDelayedFramesTracker.m
index bf795f006c6..52a0a9b436f 100644
--- a/Sources/Sentry/SentryDelayedFramesTracker.m
+++ b/Sources/Sentry/SentryDelayedFramesTracker.m
@@ -112,12 +112,13 @@ - (void)removeOldDelayedFrames
[self.delayedFrames removeObjectsInRange:NSMakeRange(0, left)];
}
-- (CFTimeInterval)getFramesDelay:(uint64_t)startSystemTimestamp
- endSystemTimestamp:(uint64_t)endSystemTimestamp
- isRunning:(BOOL)isRunning
- slowFrameThreshold:(CFTimeInterval)slowFrameThreshold
+- (SentryFramesDelayResult *)getFramesDelay:(uint64_t)startSystemTimestamp
+ endSystemTimestamp:(uint64_t)endSystemTimestamp
+ isRunning:(BOOL)isRunning
+ slowFrameThreshold:(CFTimeInterval)slowFrameThreshold
{
- CFTimeInterval cantCalculateFrameDelayReturnValue = -1.0;
+ SentryFramesDelayResult *cantCalculateFrameDelayReturnValue =
+ [[SentryFramesDelayResult alloc] initWithDelayDuration:-1.0 framesCount:0];
if (isRunning == NO) {
SENTRY_LOG_DEBUG(@"Not calculating frames delay because frames tracker isn't running.");
@@ -189,6 +190,7 @@ - (CFTimeInterval)getFramesDelay:(uint64_t)startSystemTimestamp
endDate:endDate];
CFTimeInterval delay = 0.0;
+ NSUInteger framesCount = 0;
// Iterate in reverse order, as younger frame delays are more likely to match the queried
// period.
@@ -201,9 +203,13 @@ - (CFTimeInterval)getFramesDelay:(uint64_t)startSystemTimestamp
}
delay = delay + [self calculateDelay:frame queryDateInterval:queryDateInterval];
+ framesCount++;
}
- return delay;
+ SentryFramesDelayResult *data =
+ [[SentryFramesDelayResult alloc] initWithDelayDuration:delay framesCount:framesCount];
+
+ return data;
}
- (CFTimeInterval)calculateDelay:(SentryDelayedFrame *)delayedFrame
diff --git a/Sources/Sentry/SentryFramesTracker.m b/Sources/Sentry/SentryFramesTracker.m
index 7edacf06554..cbc5188fd7e 100644
--- a/Sources/Sentry/SentryFramesTracker.m
+++ b/Sources/Sentry/SentryFramesTracker.m
@@ -299,8 +299,9 @@ - (SentryScreenFrames *)currentFrames SENTRY_DISABLE_THREAD_SANITIZER()
# endif // SENTRY_TARGET_PROFILING_SUPPORTED
}
-- (CFTimeInterval)getFramesDelay:(uint64_t)startSystemTimestamp
- endSystemTimestamp:(uint64_t)endSystemTimestamp SENTRY_DISABLE_THREAD_SANITIZER()
+- (SentryFramesDelayResult *)getFramesDelay:(uint64_t)startSystemTimestamp
+ endSystemTimestamp:(uint64_t)endSystemTimestamp
+ SENTRY_DISABLE_THREAD_SANITIZER()
{
return [self.delayedFramesTracker getFramesDelay:startSystemTimestamp
endSystemTimestamp:endSystemTimestamp
diff --git a/Sources/Sentry/SentrySpan.m b/Sources/Sentry/SentrySpan.m
index d231b7d1e80..5c5aa770bcc 100644
--- a/Sources/Sentry/SentrySpan.m
+++ b/Sources/Sentry/SentrySpan.m
@@ -259,7 +259,8 @@ - (void)finishWithStatus:(SentrySpanStatus)status
CFTimeInterval framesDelay = [_framesTracker
getFramesDelay:_startSystemTime
- endSystemTimestamp:SentryDependencyContainer.sharedInstance.dateProvider.systemTime];
+ endSystemTimestamp:SentryDependencyContainer.sharedInstance.dateProvider.systemTime]
+ .delayDuration;
if (framesDelay >= 0) {
[self setDataValue:@(framesDelay) forKey:@"frames.delay"];
diff --git a/Sources/Sentry/SentryTracer.m b/Sources/Sentry/SentryTracer.m
index b06ddf7df55..98a310c3540 100644
--- a/Sources/Sentry/SentryTracer.m
+++ b/Sources/Sentry/SentryTracer.m
@@ -845,7 +845,8 @@ - (void)addFrameStatistics
if (framesTracker.isRunning) {
CFTimeInterval framesDelay = [framesTracker
getFramesDelay:self.startSystemTime
- endSystemTimestamp:SentryDependencyContainer.sharedInstance.dateProvider.systemTime];
+ endSystemTimestamp:SentryDependencyContainer.sharedInstance.dateProvider.systemTime]
+ .delayDuration;
if (framesDelay >= 0) {
[self setDataValue:@(framesDelay) forKey:@"frames.delay"];
diff --git a/Sources/Sentry/include/HybridPublic/SentryFramesTracker.h b/Sources/Sentry/include/HybridPublic/SentryFramesTracker.h
index 3e6dbee0a19..46c46e87da2 100644
--- a/Sources/Sentry/include/HybridPublic/SentryFramesTracker.h
+++ b/Sources/Sentry/include/HybridPublic/SentryFramesTracker.h
@@ -9,6 +9,7 @@
@class SentryDispatchQueueWrapper;
@class SentryNSNotificationCenterWrapper;
@class SentryScreenFrames;
+@class SentryFramesDelayResult;
NS_ASSUME_NONNULL_BEGIN
@@ -48,12 +49,8 @@ NS_ASSUME_NONNULL_BEGIN
- (void)start;
- (void)stop;
-/*
- * Returns the frames delay for the passed time period. If the method can't calculate the frames
- * delay, it returns -1.
- */
-- (CFTimeInterval)getFramesDelay:(uint64_t)startSystemTimestamp
- endSystemTimestamp:(uint64_t)endSystemTimestamp;
+- (SentryFramesDelayResult *)getFramesDelay:(uint64_t)startSystemTimestamp
+ endSystemTimestamp:(uint64_t)endSystemTimestamp;
- (void)addListener:(id)listener;
diff --git a/Sources/Sentry/include/SentryDelayedFramesTracker.h b/Sources/Sentry/include/SentryDelayedFramesTracker.h
index 17d76c34ac1..d936a259e4d 100644
--- a/Sources/Sentry/include/SentryDelayedFramesTracker.h
+++ b/Sources/Sentry/include/SentryDelayedFramesTracker.h
@@ -3,6 +3,7 @@
#if SENTRY_HAS_UIKIT
@class SentryCurrentDateProvider;
+@class SentryFramesDelayResult;
NS_ASSUME_NONNULL_BEGIN
@@ -48,13 +49,11 @@ SENTRY_NO_INIT
* @param endSystemTimestamp The end system time stamp for the time interval to query frames delay.
* @param isRunning Wether the frames tracker is running or not.
* @param slowFrameThreshold The threshold for a slow frame. For 60 fps this is roughly 16.67 ms.
- *
- * @return the frames delay duration or -1 if it can't calculate the frames delay.
*/
-- (CFTimeInterval)getFramesDelay:(uint64_t)startSystemTimestamp
- endSystemTimestamp:(uint64_t)endSystemTimestamp
- isRunning:(BOOL)isRunning
- slowFrameThreshold:(CFTimeInterval)slowFrameThreshold;
+- (SentryFramesDelayResult *)getFramesDelay:(uint64_t)startSystemTimestamp
+ endSystemTimestamp:(uint64_t)endSystemTimestamp
+ isRunning:(BOOL)isRunning
+ slowFrameThreshold:(CFTimeInterval)slowFrameThreshold;
@end
diff --git a/Sources/Swift/Integrations/FramesTracking/SentryFramesDelayResult.swift b/Sources/Swift/Integrations/FramesTracking/SentryFramesDelayResult.swift
new file mode 100644
index 00000000000..e4ace6cacdf
--- /dev/null
+++ b/Sources/Swift/Integrations/FramesTracking/SentryFramesDelayResult.swift
@@ -0,0 +1,13 @@
+import Foundation
+
+@objcMembers
+class SentryFramesDelayResult: NSObject {
+ /// The frames delay for the passed time period. If frame delay can't be calculated this is -1.
+ let delayDuration: CFTimeInterval
+ let framesContributingToDelayCount: UInt
+
+ init(delayDuration: CFTimeInterval, framesCount: UInt) {
+ self.delayDuration = delayDuration
+ self.framesContributingToDelayCount = framesCount
+ }
+}
diff --git a/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift
index 790ae532a7a..d1012cf4548 100644
--- a/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift
+++ b/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift
@@ -1,4 +1,5 @@
-import _SentryPrivate
+@testable import _SentryPrivate
+@testable import Sentry
import SentryTestUtils
import XCTest
@@ -198,7 +199,8 @@ class SentryFramesTrackerTests: XCTestCase {
let expectedDelay = displayLink.timeEpsilon + displayLink.slowestSlowFrameDuration - slowFrameThreshold(displayLink.currentFrameRate.rawValue)
let actualFrameDelay = sut.getFramesDelay(startSystemTime, endSystemTimestamp: endSystemTime)
- XCTAssertEqual(actualFrameDelay, expectedDelay, accuracy: 0.0001)
+ XCTAssertEqual(actualFrameDelay.delayDuration, expectedDelay, accuracy: 0.0001)
+ XCTAssertEqual(actualFrameDelay.framesContributingToDelayCount, 4)
}
/**
@@ -225,7 +227,8 @@ class SentryFramesTrackerTests: XCTestCase {
let expectedDelay = delayWithoutFrameRecord - slowFrameThreshold(displayLink.currentFrameRate.rawValue)
let actualFrameDelay = sut.getFramesDelay(startSystemTime, endSystemTimestamp: endSystemTime)
- XCTAssertEqual(actualFrameDelay, expectedDelay, accuracy: 0.0001)
+ XCTAssertEqual(actualFrameDelay.delayDuration, expectedDelay, accuracy: 0.0001)
+ XCTAssertEqual(actualFrameDelay.framesContributingToDelayCount, 2)
}
/**
@@ -293,7 +296,8 @@ class SentryFramesTrackerTests: XCTestCase {
let endSystemTime = fixture.dateProvider.systemTime()
let actualFrameDelay = sut.getFramesDelay(startSystemTime, endSystemTimestamp: endSystemTime)
- XCTAssertEqual(actualFrameDelay, -1)
+ XCTAssertEqual(actualFrameDelay.delayDuration, -1)
+ XCTAssertEqual(actualFrameDelay.framesContributingToDelayCount, 0)
}
func testDelayedFrames_NoRecordedDelayedFrames_ReturnsZero() {
@@ -312,7 +316,8 @@ class SentryFramesTrackerTests: XCTestCase {
let endSystemTime = fixture.dateProvider.systemTime()
let actualFrameDelay = sut.getFramesDelay(startSystemTime, endSystemTimestamp: endSystemTime)
- XCTAssertEqual(actualFrameDelay, 0.0, accuracy: 0.0001)
+ XCTAssertEqual(actualFrameDelay.delayDuration, 0.0, accuracy: 0.0001)
+ XCTAssertEqual(actualFrameDelay.framesContributingToDelayCount, 2)
}
func testDelayedFrames_NoRecordedDelayedFrames_ButFrameIsDelayed_ReturnsDelay() {
@@ -334,7 +339,8 @@ class SentryFramesTrackerTests: XCTestCase {
let expectedDelay = delay - slowFrameThreshold(fixture.displayLinkWrapper.currentFrameRate.rawValue)
let actualFrameDelay = sut.getFramesDelay(startSystemTime, endSystemTimestamp: endSystemTime)
- XCTAssertEqual(actualFrameDelay, expectedDelay, accuracy: 0.0001)
+ XCTAssertEqual(actualFrameDelay.delayDuration, expectedDelay, accuracy: 0.0001)
+ XCTAssertEqual(actualFrameDelay.framesContributingToDelayCount, 2)
}
func testDelayedFrames_FrameIsDelayedSmallerThanSlowFrameThreshold_ReturnsDelay() {
@@ -359,7 +365,9 @@ class SentryFramesTrackerTests: XCTestCase {
let expectedDelay = delay
let actualFrameDelay = sut.getFramesDelay(startSystemTime, endSystemTimestamp: endSystemTime)
- XCTAssertEqual(actualFrameDelay, expectedDelay, accuracy: 0.0001)
+ XCTAssertEqual(
+ actualFrameDelay.delayDuration, expectedDelay, accuracy: 0.0001)
+ XCTAssertEqual(actualFrameDelay.framesContributingToDelayCount, 1)
}
private func testFrameDelay(timeIntervalAfterFrameStart: TimeInterval = 0.0, timeIntervalBeforeFrameEnd: TimeInterval = 0.0, expectedDelay: TimeInterval) {
@@ -377,7 +385,7 @@ class SentryFramesTrackerTests: XCTestCase {
let startSystemTime = slowFrameStartSystemTime + timeIntervalToNanoseconds(timeIntervalAfterFrameStart)
let actualFrameDelay = sut.getFramesDelay(startSystemTime, endSystemTimestamp: endSystemTime)
- XCTAssertEqual(actualFrameDelay, expectedDelay, accuracy: 0.0001)
+ XCTAssertEqual(actualFrameDelay.delayDuration, expectedDelay, accuracy: 0.0001)
}
/**
@@ -405,7 +413,7 @@ class SentryFramesTrackerTests: XCTestCase {
let expectedDelay = displayLink.slowestSlowFrameDuration - slowFrameThreshold(displayLink.currentFrameRate.rawValue)
let actualFrameDelay = sut.getFramesDelay(startSystemTime, endSystemTimestamp: endSystemTime)
- XCTAssertEqual(actualFrameDelay, expectedDelay, accuracy: 0.0001)
+ XCTAssertEqual(actualFrameDelay.delayDuration, expectedDelay, accuracy: 0.0001)
}
func testFrameDelay_WithStartBeforeEnd_ReturnsMinusOne() {
@@ -417,7 +425,7 @@ class SentryFramesTrackerTests: XCTestCase {
_ = displayLink.slowestSlowFrame()
let actualFrameDelay = sut.getFramesDelay(1, endSystemTimestamp: 0)
- XCTAssertEqual(actualFrameDelay, -1.0)
+ XCTAssertEqual(actualFrameDelay.delayDuration, -1.0)
}
func testFrameDelay_LongestTimeStamp_ReturnsMinusOne() {
@@ -429,7 +437,7 @@ class SentryFramesTrackerTests: XCTestCase {
_ = displayLink.slowestSlowFrame()
let actualFrameDelay = sut.getFramesDelay(0, endSystemTimestamp: UInt64.max)
- XCTAssertEqual(actualFrameDelay, -1.0)
+ XCTAssertEqual(actualFrameDelay.delayDuration, -1.0)
}
func testFrameDelay_KeepAddingSlowFrames_OnlyTheMaxDurationFramesReturned() {
@@ -441,7 +449,7 @@ class SentryFramesTrackerTests: XCTestCase {
let endSystemTime = fixture.dateProvider.systemTime()
let actualFrameDelay = sut.getFramesDelay(startSystemTime, endSystemTimestamp: endSystemTime)
- XCTAssertEqual(actualFrameDelay, expectedDelay, accuracy: 0.0001)
+ XCTAssertEqual(actualFrameDelay.delayDuration, expectedDelay, accuracy: 0.0001)
}
func testFrameDelay_MoreThanMaxDuration_FrameInformationMissing_DelayReturned() {
@@ -459,7 +467,7 @@ class SentryFramesTrackerTests: XCTestCase {
let expectedDelay = slowFramesDelay + delayNotRecorded
let actualFrameDelay = sut.getFramesDelay(startSystemTime, endSystemTimestamp: endSystemTime)
- XCTAssertEqual(actualFrameDelay, expectedDelay, accuracy: 0.0001)
+ XCTAssertEqual(actualFrameDelay.delayDuration, expectedDelay, accuracy: 0.0001)
}
func testFrameDelay_MoreThanMaxDuration_StartTimeTooEarly_ReturnsMinusOne() {
@@ -471,7 +479,7 @@ class SentryFramesTrackerTests: XCTestCase {
let endSystemTime = fixture.dateProvider.systemTime()
let actualFrameDelay = sut.getFramesDelay(startSystemTime - 1, endSystemTimestamp: endSystemTime)
- XCTAssertEqual(actualFrameDelay, -1, accuracy: 0.0001, "startSystemTimeStamp starts one nanosecond before the oldest slow frame. Therefore the frame delay can't be calculated and should me 0.")
+ XCTAssertEqual(actualFrameDelay.delayDuration, -1, accuracy: 0.0001, "startSystemTimeStamp starts one nanosecond before the oldest slow frame. Therefore the frame delay can't be calculated and should me 0.")
}
func testFrameDelay_FramesTrackerNotRunning_ReturnsMinusOne() {
@@ -489,7 +497,7 @@ class SentryFramesTrackerTests: XCTestCase {
sut.stop()
let actualFrameDelay = sut.getFramesDelay(startSystemTime, endSystemTimestamp: endSystemTime)
- XCTAssertEqual(actualFrameDelay, -1.0)
+ XCTAssertEqual(actualFrameDelay.delayDuration, -1.0)
}
func testFrameDelay_RestartTracker_ReturnsMinusOne() {
@@ -504,7 +512,7 @@ class SentryFramesTrackerTests: XCTestCase {
let endSystemTime = fixture.dateProvider.systemTime()
let actualFrameDelay = sut.getFramesDelay(startSystemTime, endSystemTimestamp: endSystemTime)
- XCTAssertEqual(actualFrameDelay, -1.0)
+ XCTAssertEqual(actualFrameDelay.delayDuration, -1.0)
}
func testFrameDelay_GetInfoFromBackgroundThreadWhileAdding() {
@@ -523,7 +531,7 @@ class SentryFramesTrackerTests: XCTestCase {
let actualFrameDelay = sut.getFramesDelay(startSystemTime, endSystemTimestamp: endSystemTime)
- XCTAssertGreaterThanOrEqual(actualFrameDelay, -1)
+ XCTAssertGreaterThanOrEqual(actualFrameDelay.delayDuration, -1)
expectation.fulfill()
}
@@ -556,7 +564,7 @@ class SentryFramesTrackerTests: XCTestCase {
let frameDelay = sut.getFramesDelay(startSystemTimestamp, endSystemTimestamp: endSystemTimestamp)
- XCTAssertLessThanOrEqual(frameDelay, 1.0)
+ XCTAssertLessThanOrEqual(frameDelay.delayDuration, 1.0)
}
expectation.fulfill()
From 66b6cd4deacf2fa8ac99c461fa3d93ee2df30468 Mon Sep 17 00:00:00 2001
From: Indragie Karunaratne
Date: Wed, 14 Aug 2024 16:55:17 -0400
Subject: [PATCH 62/63] fix(SentryBacktrace): Guard stack frame pointer
dereference (#4268)
---
CHANGELOG.md | 1 +
Sources/Sentry/SentryBacktrace.cpp | 10 +++++++++-
2 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c7909374f0d..b76413da8d0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@
- Session replay not redacting buttons and other non UILabel texts (#4277)
- Rarely reporting too long frame delays (#4278) by fixing a race condition in the frames tracking logic.
- Crash deserializing empty envelope length>0 (#4281]
+- Guard dereferencing of stack frame pointer in SentryBacktrace ([#4268](https://github.com/getsentry/sentry-cocoa/pull/4268))
## 8.33.0
diff --git a/Sources/Sentry/SentryBacktrace.cpp b/Sources/Sentry/SentryBacktrace.cpp
index c5d5a7a324c..32cb54e27d2 100644
--- a/Sources/Sentry/SentryBacktrace.cpp
+++ b/Sources/Sentry/SentryBacktrace.cpp
@@ -11,7 +11,12 @@
# include "SentryThreadMetadataCache.hpp"
# include "SentryThreadState.hpp"
# include "SentryTime.h"
-
+extern "C" {
+# define restrict
+/** Allow importing C99 headers that use the restrict keyword, which isn't valid in C++ */
+# include "SentryCrashMemory.h"
+# undef restrict
+}
# include
# include
# include
@@ -81,6 +86,9 @@ namespace profiling {
bool reachedEndOfStack = false;
while (depth < maxDepth) {
const auto frame = reinterpret_cast(current);
+ if (!sentrycrashmem_isMemoryReadable(frame, sizeof(StackFrame))) {
+ break;
+ }
if (LIKELY(skip == 0)) {
addresses[depth++] = getPreviousInstructionAddress(frame->returnAddress);
} else {
From d2ced2d961b34573ebd2ea0567a2f1408e90f0ae Mon Sep 17 00:00:00 2001
From: getsentry-bot
Date: Wed, 14 Aug 2024 21:12:30 +0000
Subject: [PATCH 63/63] release: 8.34.0
---
.github/last-release-runid | 2 +-
CHANGELOG.md | 2 +-
Package.swift | 8 ++++----
Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj | 8 ++++----
Sentry.podspec | 2 +-
SentryPrivate.podspec | 2 +-
SentrySwiftUI.podspec | 4 ++--
Sources/Configuration/SDK.xcconfig | 2 +-
Sources/Sentry/SentryMeta.m | 2 +-
Tests/HybridSDKTest/HybridPod.podspec | 2 +-
10 files changed, 17 insertions(+), 17 deletions(-)
diff --git a/.github/last-release-runid b/.github/last-release-runid
index fb533541322..8e460b7d369 100644
--- a/.github/last-release-runid
+++ b/.github/last-release-runid
@@ -1 +1 @@
-10300958982
+10394898401
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b76413da8d0..1254f58f772 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,6 @@
# Changelog
-## Unreleased
+## 8.34.0
### Features
diff --git a/Package.swift b/Package.swift
index 336c6839d1a..9133e270127 100644
--- a/Package.swift
+++ b/Package.swift
@@ -12,13 +12,13 @@ let package = Package(
targets: [
.binaryTarget(
name: "Sentry",
- url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.33.0/Sentry.xcframework.zip",
- checksum: "5a0794d298dc863e295309a34710170701d191e5445aed7e415befd218714606" //Sentry-Static
+ url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.34.0/Sentry.xcframework.zip",
+ checksum: "f85df7251fb4cf4fac1ab93587313466cc6c463b517f1a4778487aeb69a5f2dd" //Sentry-Static
),
.binaryTarget(
name: "Sentry-Dynamic",
- url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.33.0/Sentry-Dynamic.xcframework.zip",
- checksum: "4ea23fbc25edb599802f41bb00b4c9da38d1137a75ff0f79d72897d46dd451b9" //Sentry-Dynamic
+ url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.34.0/Sentry-Dynamic.xcframework.zip",
+ checksum: "e0abb4ad4c01cd157c650fa7bb1c971ed451516faf5afde492470314f3f09d74" //Sentry-Dynamic
),
.target ( name: "SentrySwiftUI",
dependencies: ["Sentry", "SentryInternal"],
diff --git a/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj b/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj
index 932ea9beef1..b4d09fc5bc8 100644
--- a/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj
+++ b/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj
@@ -1257,7 +1257,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 8.33.0;
+ MARKETING_VERSION = 8.34.0;
PRODUCT_BUNDLE_IDENTIFIER = "io.sentry.sample.iOS-Swift";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match Development io.sentry.sample.iOS-Swift";
@@ -1286,7 +1286,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 8.33.0;
+ MARKETING_VERSION = 8.34.0;
PRODUCT_BUNDLE_IDENTIFIER = "io.sentry.sample.iOS-Swift";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AppStore io.sentry.sample.iOS-Swift";
@@ -1935,7 +1935,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 8.33.0;
+ MARKETING_VERSION = 8.34.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";
@@ -1970,7 +1970,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 8.33.0;
+ MARKETING_VERSION = 8.34.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 a982c1b261d..875b2992b5a 100644
--- a/Sentry.podspec
+++ b/Sentry.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "Sentry"
- s.version = "8.33.0"
+ s.version = "8.34.0"
s.summary = "Sentry client for cocoa"
s.homepage = "https://github.com/getsentry/sentry-cocoa"
s.license = "mit"
diff --git a/SentryPrivate.podspec b/SentryPrivate.podspec
index c0927b7e208..af41ba102dc 100644
--- a/SentryPrivate.podspec
+++ b/SentryPrivate.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "SentryPrivate"
- s.version = "8.33.0"
+ s.version = "8.34.0"
s.summary = "Sentry Private Library."
s.homepage = "https://github.com/getsentry/sentry-cocoa"
s.license = "mit"
diff --git a/SentrySwiftUI.podspec b/SentrySwiftUI.podspec
index f0dcae83d5f..e2e8901e2e5 100644
--- a/SentrySwiftUI.podspec
+++ b/SentrySwiftUI.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "SentrySwiftUI"
- s.version = "8.33.0"
+ s.version = "8.34.0"
s.summary = "Sentry client for SwiftUI"
s.homepage = "https://github.com/getsentry/sentry-cocoa"
s.license = "mit"
@@ -19,5 +19,5 @@ Pod::Spec.new do |s|
s.watchos.framework = 'WatchKit'
s.source_files = "Sources/SentrySwiftUI/**/*.{swift,h,m}"
- s.dependency 'Sentry', "8.33.0"
+ s.dependency 'Sentry', "8.34.0"
end
diff --git a/Sources/Configuration/SDK.xcconfig b/Sources/Configuration/SDK.xcconfig
index 1dc6307c682..9c7663b0c36 100644
--- a/Sources/Configuration/SDK.xcconfig
+++ b/Sources/Configuration/SDK.xcconfig
@@ -10,7 +10,7 @@ DYLIB_INSTALL_NAME_BASE = @rpath
MACH_O_TYPE = mh_dylib
FRAMEWORK_VERSION = A
-CURRENT_PROJECT_VERSION = 8.33.0
+CURRENT_PROJECT_VERSION = 8.34.0
ALWAYS_SEARCH_USER_PATHS = NO
CLANG_ENABLE_OBJC_ARC = YES
diff --git a/Sources/Sentry/SentryMeta.m b/Sources/Sentry/SentryMeta.m
index 4111d237e08..ed02370e8c3 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 *versionString = @"8.33.0";
+static NSString *versionString = @"8.34.0";
static NSString *sdkName = @"sentry.cocoa";
+ (NSString *)versionString
diff --git a/Tests/HybridSDKTest/HybridPod.podspec b/Tests/HybridSDKTest/HybridPod.podspec
index 129766d89e2..7bcb73b2123 100644
--- a/Tests/HybridSDKTest/HybridPod.podspec
+++ b/Tests/HybridSDKTest/HybridPod.podspec
@@ -13,6 +13,6 @@ Pod::Spec.new do |s|
s.requires_arc = true
s.frameworks = 'Foundation'
s.swift_versions = "5.5"
- s.dependency "Sentry/HybridSDK", "8.33.0"
+ s.dependency "Sentry/HybridSDK", "8.34.0"
s.source_files = "HybridTest.swift"
end