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 @@ - + - + - + - +