diff --git a/CHANGELOG.md b/CHANGELOG.md index 95f5c2b81aa..a7f20cc8f52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ - Added `thermal_state` to device context (#4305) +### Refactoring + +- Moved session replay API to `SentrySDK.replay` (#4326) +- Changed default session replay quality to `medium` (#4326) + ### Fixes - Resumes replay when the app becomes active (#4303) diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 0a6438ab07d..42204cc6ff4 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -867,6 +867,8 @@ D88817DA26D72AB800BF2251 /* SentryTraceContext.h in Headers */ = {isa = PBXBuildFile; fileRef = D88817D926D72AB800BF2251 /* SentryTraceContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; D88817DD26D72BA500BF2251 /* SentryTraceStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88817DB26D72B7B00BF2251 /* SentryTraceStateTests.swift */; }; D8918B222849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8918B212849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift */; }; + D8A3649C2C91AA3300AC569B /* SentryReplayApi.m in Sources */ = {isa = PBXBuildFile; fileRef = D8A3649B2C91AA3300AC569B /* SentryReplayApi.m */; }; + D8A3649D2C91AA3300AC569B /* SentryReplayApi.h in Headers */ = {isa = PBXBuildFile; fileRef = D8A3649A2C91AA3300AC569B /* SentryReplayApi.h */; settings = {ATTRIBUTES = (Public, ); }; }; D8AB40DB2806EC1900E5E9F7 /* SentryScreenshotIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D8AB40DA2806EC1900E5E9F7 /* SentryScreenshotIntegration.h */; }; D8ACE3C72762187200F5A213 /* SentryNSDataSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = D8ACE3C42762187200F5A213 /* SentryNSDataSwizzling.m */; }; D8ACE3C82762187200F5A213 /* SentryNSDataTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = D8ACE3C52762187200F5A213 /* SentryNSDataTracker.m */; }; @@ -1936,6 +1938,8 @@ D88817DB26D72B7B00BF2251 /* SentryTraceStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceStateTests.swift; sourceTree = ""; }; D88D25E92B8E0BAC0073C3D5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; D8918B212849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySDKIntegrationTestsBase.swift; sourceTree = ""; }; + D8A3649A2C91AA3300AC569B /* SentryReplayApi.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryReplayApi.h; path = Public/SentryReplayApi.h; sourceTree = ""; }; + D8A3649B2C91AA3300AC569B /* SentryReplayApi.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryReplayApi.m; sourceTree = ""; }; D8AB40DA2806EC1900E5E9F7 /* SentryScreenshotIntegration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryScreenshotIntegration.h; path = include/SentryScreenshotIntegration.h; sourceTree = ""; }; D8ACE3C42762187200F5A213 /* SentryNSDataSwizzling.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryNSDataSwizzling.m; sourceTree = ""; }; D8ACE3C52762187200F5A213 /* SentryNSDataTracker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryNSDataTracker.m; sourceTree = ""; }; @@ -3645,6 +3649,8 @@ D820CDB62BB1895F00BA339D /* SentrySessionReplayIntegration.m */, D82859412C3E753C009A28AA /* SentrySessionReplaySyncC.h */, D82859422C3E753C009A28AA /* SentrySessionReplaySyncC.c */, + D8A3649A2C91AA3300AC569B /* SentryReplayApi.h */, + D8A3649B2C91AA3300AC569B /* SentryReplayApi.m */, ); name = SessionReplay; sourceTree = ""; @@ -4209,6 +4215,7 @@ 63FE70ED20DA4C1000CDBAE8 /* SentryCrashMonitor_NSException.h in Headers */, 7BA61CB4247BC3EB00C130A8 /* SentryCrashBinaryImageProvider.h in Headers */, 63FE713D20DA4C1100CDBAE8 /* SentryAsyncSafeLog.h in Headers */, + D8A3649D2C91AA3300AC569B /* SentryReplayApi.h in Headers */, 15E0A8E1240C41CE00F044E3 /* SentryEnvelope.h in Headers */, 630435FE1EBCA9D900C4D3FA /* SentryNSURLRequest.h in Headers */, 7BC852392458830A005A70F0 /* SentryEnvelopeItemType.h in Headers */, @@ -4738,6 +4745,7 @@ D8F016B62B962548007B9AFB /* StringExtensions.swift in Sources */, 7B4E23C2251A2C2B00060D68 /* SentrySessionCrashedHandler.m in Sources */, 9286059729A5098900F96038 /* SentryGeo.m in Sources */, + D8A3649C2C91AA3300AC569B /* SentryReplayApi.m in Sources */, 7B42C48227E08F4B009B58C2 /* SentryDependencyContainer.m in Sources */, 639FCFAD1EBC811400778193 /* SentryUser.m in Sources */, 62B558B02C6B9C3C00C34FEC /* SentryFramesDelayResult.swift in Sources */, diff --git a/Sources/Sentry/Public/Sentry.h b/Sources/Sentry/Public/Sentry.h index 83d298589f4..52e725b022f 100644 --- a/Sources/Sentry/Public/Sentry.h +++ b/Sources/Sentry/Public/Sentry.h @@ -30,6 +30,7 @@ FOUNDATION_EXPORT const unsigned char SentryVersionString[]; # import # import # import +# import # import # import # import diff --git a/Sources/Sentry/Public/SentryReplayApi.h b/Sources/Sentry/Public/SentryReplayApi.h new file mode 100644 index 00000000000..ecbb58872a1 --- /dev/null +++ b/Sources/Sentry/Public/SentryReplayApi.h @@ -0,0 +1,50 @@ +#import + +#if __has_include() +# import +#else +# import +#endif + +#if SENTRY_TARGET_REPLAY_SUPPORTED + +@class UIView; + +NS_ASSUME_NONNULL_BEGIN + +@interface SentryReplayApi : NSObject + +/** + * Marks this view to be redacted during replays. + * + * @warning This is an experimental feature and may still have bugs. + */ +- (void)redactView:(UIView *)view NS_SWIFT_NAME(redactView(_:)); + +/** + * Marks this view to be ignored during redact step of session replay. + * All its content will be visible in the replay. + * + * @warning This is an experimental feature and may still have bugs. + */ +- (void)ignoreView:(UIView *)view NS_SWIFT_NAME(ignoreView(_:)); + +/** + * Pauses the replay. + * + * @warning This is an experimental feature and may still have bugs. + */ +- (void)pause; + +/** + * Resumes the ongoing replay. + * + * @warning This is an experimental feature and may still have bugs. + */ +- (void)resume; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/Sources/Sentry/Public/SentrySDK.h b/Sources/Sentry/Public/SentrySDK.h index 9f728c26380..e8cd70cb585 100644 --- a/Sources/Sentry/Public/SentrySDK.h +++ b/Sources/Sentry/Public/SentrySDK.h @@ -6,6 +6,7 @@ SentryUserFeedback, SentryTransactionContext; @class SentryMetricsAPI; @class UIView; +@class SentryReplayApi; NS_ASSUME_NONNULL_BEGIN @@ -28,6 +29,13 @@ SENTRY_NO_INIT @property (class, nonatomic, readonly) SentryMetricsAPI *metrics; +#if SENTRY_TARGET_REPLAY_SUPPORTED +/** + * API to control session replay + */ +@property (class, nonatomic, readonly) SentryReplayApi *replay; +#endif + /** * Inits and configures Sentry (SentryHub, SentryClient) and sets up all integrations. Make sure to * set a valid DSN. @@ -334,25 +342,6 @@ SENTRY_NO_INIT */ + (void)close; -#if SENTRY_TARGET_REPLAY_SUPPORTED - -/** - * @warning This is an experimental feature and may still have bugs. - * - * Marks this view to be redacted during replays. - */ -+ (void)replayRedactView:(UIView *)view; - -/** - * @warning This is an experimental feature and may still have bugs. - * - * Marks this view to be ignored during redact step - * of session replay. All its content will be visible in the replay. - */ -+ (void)replayIgnoreView:(UIView *)view; - -#endif - #if SENTRY_TARGET_PROFILING_SUPPORTED /** * Start a new continuous profiling session if one is not already running. diff --git a/Sources/Sentry/Public/SentryWithoutUIKit.h b/Sources/Sentry/Public/SentryWithoutUIKit.h index 82b500bf1ee..a3eba11f48e 100644 --- a/Sources/Sentry/Public/SentryWithoutUIKit.h +++ b/Sources/Sentry/Public/SentryWithoutUIKit.h @@ -31,6 +31,7 @@ FOUNDATION_EXPORT const unsigned char SentryVersionString[]; # import # import # import +# import # import # import # import diff --git a/Sources/Sentry/SentryReplayApi.m b/Sources/Sentry/SentryReplayApi.m new file mode 100644 index 00000000000..e9ce39b34a9 --- /dev/null +++ b/Sources/Sentry/SentryReplayApi.m @@ -0,0 +1,39 @@ +#import "SentryReplayApi.h" + +#if SENTRY_TARGET_REPLAY_SUPPORTED + +# import "SentryHub+Private.h" +# import "SentrySDK+Private.h" +# import "SentrySessionReplayIntegration.h" +# import "SentrySwift.h" +# import + +@implementation SentryReplayApi + +- (void)redactView:(UIView *)view +{ + [SentryRedactViewHelper redactView:view]; +} + +- (void)ignoreView:(UIView *)view +{ + [SentryRedactViewHelper ignoreView:view]; +} + +- (void)pause +{ + SentrySessionReplayIntegration *replayIntegration = + [SentrySDK.currentHub getInstalledIntegration:SentrySessionReplayIntegration.class]; + [replayIntegration pause]; +} + +- (void)resume +{ + SentrySessionReplayIntegration *replayIntegration = + [SentrySDK.currentHub getInstalledIntegration:SentrySessionReplayIntegration.class]; + [replayIntegration resume]; +} + +@end + +#endif diff --git a/Sources/Sentry/SentrySDK.m b/Sources/Sentry/SentrySDK.m index a8257d8dfa7..56dcefa32d7 100644 --- a/Sources/Sentry/SentrySDK.m +++ b/Sources/Sentry/SentrySDK.m @@ -17,6 +17,7 @@ #import "SentryMeta.h" #import "SentryOptions+Private.h" #import "SentryProfilingConditionals.h" +#import "SentryReplayApi.h" #import "SentrySamplingContext.h" #import "SentryScope.h" #import "SentrySerialization.h" @@ -94,7 +95,15 @@ + (nullable SentryOptions *)options return startOption; } } - +#if SENTRY_TARGET_REPLAY_SUPPORTED ++ (SentryReplayApi *)replay +{ + static SentryReplayApi *replay; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ replay = [[SentryReplayApi alloc] init]; }); + return replay; +} +#endif /** Internal, only needed for testing. */ + (void)setCurrentHub:(nullable SentryHub *)hub { @@ -576,18 +585,6 @@ + (void)stopProfiler } #endif // SENTRY_TARGET_PROFILING_SUPPORTED -#if SENTRY_TARGET_REPLAY_SUPPORTED -+ (void)replayRedactView:(UIView *)view -{ - [SentryRedactViewHelper redactView:view]; -} - -+ (void)replayIgnoreView:(UIView *)view -{ - [SentryRedactViewHelper ignoreView:view]; -} -#endif - @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 53a1064cb0e..2600b0fafb8 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -35,6 +35,8 @@ */ static SentryTouchTracker *_touchTracker; +static SentrySessionReplayIntegration *_installedInstance; + @interface SentrySessionReplayIntegration () - (void)newSceneActivate; @@ -47,6 +49,11 @@ @implementation SentrySessionReplayIntegration { SentryOnDemandReplay *_resumeReplayMaker; } ++ (nullable SentrySessionReplayIntegration *)installed +{ + return _installedInstance; +} + - (BOOL)installWithOptions:(nonnull SentryOptions *)options { if ([super installWithOptions:options] == NO) { @@ -80,6 +87,7 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options [SentryViewPhotographer.shared addIgnoreClasses:_replayOptions.ignoreRedactViewTypes]; [SentryViewPhotographer.shared addRedactClasses:_replayOptions.redactViewTypes]; + _installedInstance = self; return YES; } @@ -190,7 +198,7 @@ - (void)captureVideo:(SentryVideoInfo *)video - (void)startSession { - [self.sessionReplay stop]; + [self.sessionReplay pause]; _startedAsFullSession = [self shouldReplayFullSession:_replayOptions.sessionSampleRate]; @@ -266,7 +274,7 @@ - (void)startWithOptions:(SentryReplayOptions *)replayOptions fullSession:[self shouldReplayFullSession:replayOptions.sessionSampleRate]]; [_notificationCenter addObserver:self - selector:@selector(stop) + selector:@selector(pause) name:UIApplicationDidEnterBackgroundNotification object:nil]; @@ -308,9 +316,9 @@ - (void)saveCurrentSessionInfo:(SentryId *)sessionId cStringUsingEncoding:NSUTF8StringEncoding]); } -- (void)stop +- (void)pause { - [self.sessionReplay stop]; + [self.sessionReplay pause]; } - (void)resume @@ -320,7 +328,7 @@ - (void)resume - (void)sentrySessionEnded:(SentrySession *)session { - [self stop]; + [self pause]; [_notificationCenter removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil]; @@ -361,7 +369,11 @@ - (void)uninstall { [SentrySDK.currentHub unregisterSessionListener:self]; _touchTracker = nil; - [self stop]; + [self pause]; + + if (_installedInstance == self) { + _installedInstance = nil; + } } - (void)dealloc @@ -475,7 +487,7 @@ - (void)connectivityChanged:(BOOL)connected typeDescription:(nonnull NSString *) if (connected) { [_sessionReplay resume]; } else { - [_sessionReplay pause]; + [_sessionReplay pauseSessionMode]; } } diff --git a/Sources/Sentry/include/SentrySessionReplayIntegration.h b/Sources/Sentry/include/SentrySessionReplayIntegration.h index dcb2ffc121c..f5fd4183a52 100644 --- a/Sources/Sentry/include/SentrySessionReplayIntegration.h +++ b/Sources/Sentry/include/SentrySessionReplayIntegration.h @@ -10,6 +10,11 @@ NS_ASSUME_NONNULL_BEGIN @interface SentrySessionReplayIntegration : SentryBaseIntegration +/** + * The last instance of the installed integration + */ +@property (class, nonatomic, readonly, nullable) SentrySessionReplayIntegration *installed; + /** * Captures Replay. Used by the Hybrid SDKs. */ @@ -23,6 +28,10 @@ NS_ASSUME_NONNULL_BEGIN - (void)configureReplayWith:(nullable id)breadcrumbConverter screenshotProvider:(nullable id)screenshotProvider; +- (void)pause; + +- (void)resume; + @end #endif // SENTRY_TARGET_REPLAY_SUPPORTED NS_ASSUME_NONNULL_END diff --git a/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift index 4c9ef876d98..638d3935cc3 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift @@ -65,7 +65,7 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { * Indicates the quality of the replay. * The higher the quality, the higher the CPU and bandwidth usage. */ - public var quality = SentryReplayQuality.low + public var quality = SentryReplayQuality.medium /** * A list of custom UIView subclasses that need diff --git a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift index d413ea0d649..4fab0b3a52f 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift @@ -96,7 +96,7 @@ class SentrySessionReplay: NSObject { delegate?.sessionReplayStarted(replayId: sessionReplayId) } - func pause() { + func pauseSessionMode() { lock.lock() defer { lock.unlock() } @@ -104,7 +104,7 @@ class SentrySessionReplay: NSObject { self.videoSegmentStart = nil } - func stop() { + func pause() { lock.lock() defer { lock.unlock() } @@ -185,7 +185,7 @@ class SentrySessionReplay: NSObject { if let sessionStart = sessionStart, isFullSession && now.timeIntervalSince(sessionStart) > replayOptions.maximumDuration { reachedMaximumDuration = true - stop() + pause() return } diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayOptionsTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayOptionsTests.swift index 6dcc51fb987..98575ddecd1 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayOptionsTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayOptionsTests.swift @@ -6,8 +6,8 @@ class SentryReplayOptionsTests: XCTestCase { func testQualityLow() { let options = SentryReplayOptions() + options.quality = .low - XCTAssertEqual(options.quality, .low) XCTAssertEqual(options.replayBitRate, 20_000) XCTAssertEqual(options.sizeScale, 0.8) } diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift index 40312644a7d..b9b3e02e982 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift @@ -260,7 +260,7 @@ class SentrySessionReplayTests: XCTestCase { fixture.dateProvider.advance(by: 1) Dynamic(sut).newFrame(nil) XCTAssertNotNil(fixture.screenshotProvider.lastImageCall) - sut.pause() + sut.pauseSessionMode() fixture.screenshotProvider.lastImageCall = nil fixture.dateProvider.advance(by: 1) @@ -292,7 +292,7 @@ class SentrySessionReplayTests: XCTestCase { Dynamic(sut).newFrame(nil) XCTAssertNotNil(fixture.screenshotProvider.lastImageCall) - sut.pause() + sut.pauseSessionMode() fixture.screenshotProvider.lastImageCall = nil fixture.dateProvider.advance(by: 1) diff --git a/Tests/SentryTests/UIRedactBuilderTests.swift b/Tests/SentryTests/UIRedactBuilderTests.swift index ca3812a64d0..978aad0a3c1 100644 --- a/Tests/SentryTests/UIRedactBuilderTests.swift +++ b/Tests/SentryTests/UIRedactBuilderTests.swift @@ -185,7 +185,7 @@ class UIRedactBuilderTests: XCTestCase { let sut = UIRedactBuilder() let label = AnotherLabel(frame: CGRect(x: 20, y: 20, width: 40, height: 40)) - SentrySDK.replayIgnore(label) + SentrySDK.replay.ignoreView(label) rootView.addSubview(label) let result = sut.redactRegionsFor(view: rootView, options: RedactOptions()) @@ -198,7 +198,7 @@ class UIRedactBuilderTests: XCTestCase { let sut = UIRedactBuilder() let view = AnotherView(frame: CGRect(x: 20, y: 20, width: 40, height: 40)) - SentrySDK.replayRedactView(view) + SentrySDK.replay.redactView(view) rootView.addSubview(view) let result = sut.redactRegionsFor(view: rootView, options: RedactOptions())