Skip to content

Commit a40ffe8

Browse files
authored
fix: Start session replay without a UIWindow (#3911)
If there is no UIWindow available during SDK initialization, wait for a UISceneDidActivateNotification
1 parent 38b36f5 commit a40ffe8

File tree

6 files changed

+90
-17
lines changed

6 files changed

+90
-17
lines changed

Sentry.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,7 @@
859859
D8ACE3CE2762187D00F5A213 /* SentryNSDataTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = D8ACE3CB2762187D00F5A213 /* SentryNSDataTracker.h */; };
860860
D8ACE3CF2762187D00F5A213 /* SentryFileIOTrackingIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D8ACE3CC2762187D00F5A213 /* SentryFileIOTrackingIntegration.h */; };
861861
D8AFC0012BD252B900118BE1 /* SentryOnDemandReplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC0002BD252B900118BE1 /* SentryOnDemandReplayTests.swift */; };
862+
D8AFC0622BDBEE4200118BE1 /* SentrySessionReplayIntegration+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = D8AFC0612BDBEDF100118BE1 /* SentrySessionReplayIntegration+Private.h */; };
862863
D8B0542E2A7D2C720056BAF6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D8B0542D2A7D2C720056BAF6 /* PrivacyInfo.xcprivacy */; };
863864
D8B088B629C9E3FF00213258 /* SentryTracerConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = D8B088B429C9E3FF00213258 /* SentryTracerConfiguration.h */; };
864865
D8B088B729C9E3FF00213258 /* SentryTracerConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = D8B088B529C9E3FF00213258 /* SentryTracerConfiguration.m */; };
@@ -1889,6 +1890,7 @@
18891890
D8ACE3CB2762187D00F5A213 /* SentryNSDataTracker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryNSDataTracker.h; path = include/SentryNSDataTracker.h; sourceTree = "<group>"; };
18901891
D8ACE3CC2762187D00F5A213 /* SentryFileIOTrackingIntegration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryFileIOTrackingIntegration.h; path = include/SentryFileIOTrackingIntegration.h; sourceTree = "<group>"; };
18911892
D8AFC0002BD252B900118BE1 /* SentryOnDemandReplayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryOnDemandReplayTests.swift; sourceTree = "<group>"; };
1893+
D8AFC0612BDBEDF100118BE1 /* SentrySessionReplayIntegration+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentrySessionReplayIntegration+Private.h"; path = "include/SentrySessionReplayIntegration+Private.h"; sourceTree = "<group>"; };
18921894
D8B0542D2A7D2C720056BAF6 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
18931895
D8B088B429C9E3FF00213258 /* SentryTracerConfiguration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryTracerConfiguration.h; path = include/SentryTracerConfiguration.h; sourceTree = "<group>"; };
18941896
D8B088B529C9E3FF00213258 /* SentryTracerConfiguration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryTracerConfiguration.m; sourceTree = "<group>"; };
@@ -3540,6 +3542,7 @@
35403542
D820CDB12BB1886100BA339D /* SentrySessionReplay.h */,
35413543
D820CDB22BB1886100BA339D /* SentrySessionReplay.m */,
35423544
D820CDB52BB1895F00BA339D /* SentrySessionReplayIntegration.h */,
3545+
D8AFC0612BDBEDF100118BE1 /* SentrySessionReplayIntegration+Private.h */,
35433546
D820CDB62BB1895F00BA339D /* SentrySessionReplayIntegration.m */,
35443547
D820CE112BB2F13C00BA339D /* SentryCoreGraphicsHelper.h */,
35453548
D820CE122BB2F13C00BA339D /* SentryCoreGraphicsHelper.m */,
@@ -3935,6 +3938,7 @@
39353938
6383953623ABA42C000C1594 /* SentryHttpTransport.h in Headers */,
39363939
84A8891C28DBD28900C51DFD /* SentryDevice.h in Headers */,
39373940
8E564AEF267AF24400FE117D /* SentryNetworkTracker.h in Headers */,
3941+
D8AFC0622BDBEE4200118BE1 /* SentrySessionReplayIntegration+Private.h in Headers */,
39383942
63FE715120DA4C1100CDBAE8 /* SentryCrashDebug.h in Headers */,
39393943
63FE70F520DA4C1000CDBAE8 /* SentryCrashMonitor_System.h in Headers */,
39403944
7B31C291277B04A000337126 /* SentryCrashPlatformSpecificDefines.h in Headers */,

Sources/Sentry/SentrySessionReplayIntegration.m

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#import "SentrySessionReplayIntegration.h"
1+
#import "SentrySessionReplayIntegration+Private.h"
22

33
#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION
44

@@ -8,12 +8,14 @@
88
# import "SentryFileManager.h"
99
# import "SentryGlobalEventProcessor.h"
1010
# import "SentryHub+Private.h"
11+
# import "SentryNSNotificationCenterWrapper.h"
1112
# import "SentryOptions.h"
1213
# import "SentryRandom.h"
1314
# import "SentrySDK+Private.h"
1415
# import "SentrySessionReplay.h"
1516
# import "SentrySwift.h"
1617
# import "SentryUIApplication.h"
18+
# import <UIKit/UIKit.h>
1719

1820
NS_ASSUME_NONNULL_BEGIN
1921

@@ -23,6 +25,7 @@
2325
@interface
2426
SentrySessionReplayIntegration ()
2527
@property (nonatomic, strong) SentrySessionReplay *sessionReplay;
28+
- (void)newSceneActivate;
2629
@end
2730

2831
API_AVAILABLE(ios(16.0), tvos(16.0))
@@ -33,9 +36,13 @@
3336
API_AVAILABLE(ios(16.0), tvos(16.0))
3437
@interface
3538
SentryOnDemandReplay (SentryReplayMaker) <SentryReplayMaker>
39+
3640
@end
3741

38-
@implementation SentrySessionReplayIntegration
42+
@implementation SentrySessionReplayIntegration {
43+
BOOL _startedAsFullSession;
44+
SentryReplayOptions *_replayOptions;
45+
}
3946

4047
- (BOOL)installWithOptions:(nonnull SentryOptions *)options
4148
{
@@ -44,15 +51,41 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options
4451
}
4552

4653
if (@available(iOS 16.0, tvOS 16.0, *)) {
47-
SentryReplayOptions *replayOptions = options.experimental.sessionReplay;
54+
_replayOptions = options.experimental.sessionReplay;
4855

49-
BOOL shouldReplayFullSession =
50-
[self shouldReplayFullSession:replayOptions.sessionSampleRate];
56+
_startedAsFullSession = [self shouldReplayFullSession:_replayOptions.sessionSampleRate];
5157

52-
if (!shouldReplayFullSession && replayOptions.errorSampleRate == 0) {
58+
if (!_startedAsFullSession && _replayOptions.errorSampleRate == 0) {
5359
return NO;
5460
}
5561

62+
if (SentryDependencyContainer.sharedInstance.application.windows.count > 0) {
63+
// If a window its already available start replay right away
64+
[self startWithOptions:_replayOptions fullSession:_startedAsFullSession];
65+
} else {
66+
// Wait for a scene to be available to started the replay
67+
[SentryDependencyContainer.sharedInstance.notificationCenterWrapper
68+
addObserver:self
69+
selector:@selector(newSceneActivate)
70+
name:UISceneDidActivateNotification];
71+
}
72+
73+
return YES;
74+
} else {
75+
return NO;
76+
}
77+
}
78+
79+
- (void)newSceneActivate
80+
{
81+
[SentryDependencyContainer.sharedInstance.notificationCenterWrapper removeObserver:self];
82+
[self startWithOptions:_replayOptions fullSession:_startedAsFullSession];
83+
}
84+
85+
- (void)startWithOptions:(SentryReplayOptions *)replayOptions
86+
fullSession:(BOOL)shouldReplayFullSession
87+
{
88+
if (@available(iOS 16.0, tvOS 16.0, *)) {
5689
NSURL *docs = [NSURL
5790
fileURLWithPath:[SentryDependencyContainer.sharedInstance.fileManager sentryPath]];
5891
docs = [docs URLByAppendingPathComponent:SENTRY_REPLAY_FOLDER];
@@ -97,10 +130,6 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options
97130
[self.sessionReplay captureReplayForEvent:event];
98131
return event;
99132
}];
100-
101-
return YES;
102-
} else {
103-
return NO;
104133
}
105134
}
106135

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#import "SentryBaseIntegration.h"
2+
#import "SentrySessionReplayIntegration.h"
3+
#import "SentrySwift.h"
4+
5+
#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION
6+
7+
@interface
8+
SentrySessionReplayIntegration () <SentryIntegrationProtocol>
9+
10+
@end
11+
12+
#endif

Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,25 @@ import XCTest
88

99
class SentrySessionReplayIntegrationTests: XCTestCase {
1010

11+
private class TestSentryUIApplication: SentryUIApplication {
12+
var windowsMock: [UIWindow]? = [UIWindow()]
13+
override var windows: [UIWindow]? {
14+
windowsMock
15+
}
16+
}
17+
1118
override func setUpWithError() throws {
1219
guard #available(iOS 16.0, tvOS 16.0, *) else {
1320
throw XCTSkip("iOS version not supported")
1421
}
1522
}
1623

24+
private var uiApplication = TestSentryUIApplication()
25+
26+
override func setUp() {
27+
SentryDependencyContainer.sharedInstance().application = uiApplication
28+
}
29+
1730
override func tearDown() {
1831
super.tearDown()
1932
clearTestState()
@@ -68,6 +81,21 @@ class SentrySessionReplayIntegrationTests: XCTestCase {
6881
expect(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count) == 1
6982
expect(SentryGlobalEventProcessor.shared().processors.count) == 1
7083
}
84+
85+
func testWaitForNotificationWithNoWindow() {
86+
uiApplication.windowsMock = nil
87+
startSDK(sessionSampleRate: 1, errorSampleRate: 0)
88+
89+
guard let sut = SentrySDK.currentHub().installedIntegrations().first as? SentrySessionReplayIntegration else {
90+
fail("Did not installed replay integration")
91+
return
92+
}
93+
94+
expect(Dynamic(sut).sessionReplay.asObject) == nil
95+
uiApplication.windowsMock = [UIWindow()]
96+
NotificationCenter.default.post(name: UIScene.didActivateNotification, object: nil)
97+
expect(Dynamic(sut).sessionReplay.asObject) != nil
98+
}
7199
}
72100

73101
#endif

Tests/SentryTests/PrivateSentrySDKOnlyTests.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -222,12 +222,12 @@ class PrivateSentrySDKOnlyTests: XCTestCase {
222222
guard #available(iOS 16.0, tvOS 16.0, *) else { return }
223223

224224
let options = Options()
225-
options.setIntegrations([SentrySessionReplayIntegrationTest.self])
225+
options.setIntegrations([TestSentrySessionReplayIntegration.self])
226226
SentrySDK.start(options: options)
227227

228228
PrivateSentrySDKOnly.captureReplay()
229229

230-
XCTAssertTrue(SentrySessionReplayIntegrationTest.captureReplayShouldBeCalledAtLeastOnce())
230+
XCTAssertTrue(TestSentrySessionReplayIntegration.captureReplayShouldBeCalledAtLeastOnce())
231231
}
232232

233233
func testGetReplayIdShouldBeNil() {
@@ -245,31 +245,31 @@ class PrivateSentrySDKOnlyTests: XCTestCase {
245245

246246
func testAddReplayIgnoreClassesShouldNotFailWhenReplayIsAvailable() {
247247
let options = Options()
248-
options.setIntegrations([SentrySessionReplayIntegrationTest.self])
248+
options.setIntegrations([TestSentrySessionReplayIntegration.self])
249249
SentrySDK.start(options: options)
250250

251251
PrivateSentrySDKOnly.addReplayIgnoreClasses([UILabel.self])
252252
}
253253

254254
func testAddReplayRedactShouldNotFailWhenReplayIsAvailable() {
255255
let options = Options()
256-
options.setIntegrations([SentrySessionReplayIntegrationTest.self])
256+
options.setIntegrations([TestSentrySessionReplayIntegration.self])
257257
SentrySDK.start(options: options)
258258

259259
PrivateSentrySDKOnly.addReplayRedactClasses([UILabel.self])
260260
}
261261

262262
let VALID_REPLAY_ID = "0eac7ab503354dd5819b03e263627a29"
263263

264-
final class SentrySessionReplayIntegrationTest: SentrySessionReplayIntegration {
264+
private class TestSentrySessionReplayIntegration: SentrySessionReplayIntegration {
265265
static var captureReplayCalledTimes = 0
266266

267267
override func install(with options: Options) -> Bool {
268268
return true
269269
}
270270

271271
override func captureReplay() {
272-
SentrySessionReplayIntegrationTest.captureReplayCalledTimes += 1
272+
TestSentrySessionReplayIntegration.captureReplayCalledTimes += 1
273273
}
274274

275275
static func captureReplayShouldBeCalledAtLeastOnce() -> Bool {

Tests/SentryTests/SentryTests-Bridging-Header.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# import "MockUIScene.h"
1414
# import "SentryFramesTracker+TestInit.h"
1515
# import "SentrySessionReplay.h"
16-
# import "SentrySessionReplayIntegration.h"
16+
# import "SentrySessionReplayIntegration+Private.h"
1717
# import "SentryUIApplication+Private.h"
1818
# import "SentryUIApplication.h"
1919
# import "SentryUIDeviceWrapper.h"

0 commit comments

Comments
 (0)