Skip to content

Commit d605f55

Browse files
authored
fix: Delete old session replay files (#4446)
Clean up for old session replay files
1 parent b048ba3 commit d605f55

File tree

5 files changed

+100
-12
lines changed

5 files changed

+100
-12
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ via the option `swizzleClassNameExclude`.
2020
- Finish TTID correctly when viewWillAppear is skipped (#4417)
2121
- Swizzling RootUIViewController if ignored by `swizzleClassNameExclude` (#4407)
2222
- Data race in SentrySwizzleInfo.originalCalled (#4434)
23+
- Delete old session replay files (#4446)
2324
- Thread running at user-initiated quality-of-service for session replay (#4439)
2425

25-
2626
### Improvements
2727

2828
- Serializing profile on a BG Thread (#4377) to avoid potentially slightly blocking the main thread.

Sources/Sentry/SentryFileManager.m

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,12 @@ - (void)deleteOldEnvelopesFromPath:(NSString *)envelopesPath
270270
}
271271
}
272272

273+
- (BOOL)isDirectory:(NSString *)path
274+
{
275+
BOOL isDir = NO;
276+
return [NSFileManager.defaultManager fileExistsAtPath:path isDirectory:&isDir] && isDir;
277+
}
278+
273279
- (void)deleteAllEnvelopes
274280
{
275281
[self removeFileAtPath:self.envelopesPath];

Sources/Sentry/SentrySessionReplayIntegration.m

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ - (void)setupWith:(SentryReplayOptions *)replayOptions enableTouchTracker:(BOOL)
8989
_notificationCenter = SentryDependencyContainer.sharedInstance.notificationCenterWrapper;
9090

9191
[self moveCurrentReplay];
92+
[self cleanUp];
93+
9294
[SentrySDK.currentHub registerSessionListener:self];
9395
[SentryGlobalEventProcessor.shared
9496
addEventProcessor:^SentryEvent *_Nullable(SentryEvent *_Nonnull event) {
@@ -103,6 +105,19 @@ - (void)setupWith:(SentryReplayOptions *)replayOptions enableTouchTracker:(BOOL)
103105
[SentryDependencyContainer.sharedInstance.reachability addObserver:self];
104106
}
105107

108+
- (nullable NSDictionary<NSString *, id> *)lastReplayInfo
109+
{
110+
NSURL *dir = [self replayDirectory];
111+
NSURL *lastReplayUrl = [dir URLByAppendingPathComponent:SENTRY_LAST_REPLAY];
112+
NSData *lastReplay = [NSData dataWithContentsOfURL:lastReplayUrl];
113+
114+
if (lastReplay == nil) {
115+
return nil;
116+
}
117+
118+
return [SentrySerialization deserializeDictionaryFromJsonData:lastReplay];
119+
}
120+
106121
/**
107122
* Send the cached frames from a previous session that eventually crashed.
108123
* This function is called when processing an event created by SentryCrashIntegration,
@@ -112,15 +127,8 @@ - (void)setupWith:(SentryReplayOptions *)replayOptions enableTouchTracker:(BOOL)
112127
- (void)resumePreviousSessionReplay:(SentryEvent *)event
113128
{
114129
NSURL *dir = [self replayDirectory];
115-
NSURL *lastReplayUrl = [dir URLByAppendingPathComponent:SENTRY_LAST_REPLAY];
116-
NSData *lastReplay = [NSData dataWithContentsOfURL:lastReplayUrl];
130+
NSDictionary<NSString *, id> *jsonObject = [self lastReplayInfo];
117131

118-
if (lastReplay == nil) {
119-
return;
120-
}
121-
122-
NSDictionary<NSString *, id> *jsonObject =
123-
[SentrySerialization deserializeDictionaryFromJsonData:lastReplay];
124132
if (jsonObject == nil) {
125133
return;
126134
}
@@ -365,6 +373,37 @@ - (void)moveCurrentReplay
365373
}
366374
}
367375

376+
- (void)cleanUp
377+
{
378+
NSURL *replayDir = [self replayDirectory];
379+
NSDictionary<NSString *, id> *lastReplayInfo = [self lastReplayInfo];
380+
NSString *lastReplayFolder = lastReplayInfo[@"path"];
381+
382+
SentryFileManager *fileManager = SentryDependencyContainer.sharedInstance.fileManager;
383+
// Mapping replay folder here and not in dispatched queue to prevent a race condition between
384+
// listing files and creating a new replay session.
385+
NSArray *replayFiles = [fileManager allFilesInFolder:replayDir.path];
386+
if (replayFiles.count == 0) {
387+
return;
388+
}
389+
390+
[SentryDependencyContainer.sharedInstance.dispatchQueueWrapper dispatchAsyncWithBlock:^{
391+
for (NSString *file in replayFiles) {
392+
// Skip the last replay folder.
393+
if ([file isEqualToString:lastReplayFolder]) {
394+
continue;
395+
}
396+
397+
NSString *filePath = [replayDir.path stringByAppendingPathComponent:file];
398+
399+
// Check if the file is a directory before deleting it.
400+
if ([fileManager isDirectory:filePath]) {
401+
[fileManager removeFileAtPath:filePath];
402+
}
403+
}
404+
}];
405+
}
406+
368407
- (void)pause
369408
{
370409
[self.sessionReplay pause];

Sources/Sentry/include/SentryFileManager.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,8 @@ SENTRY_NO_INIT
8989
- (NSNumber *_Nullable)readTimezoneOffset;
9090
- (void)storeTimezoneOffset:(NSInteger)offset;
9191
- (void)deleteTimezoneOffset;
92-
92+
- (NSArray<NSString *> *)allFilesInFolder:(NSString *)path;
93+
- (BOOL)isDirectory:(NSString *)path;
9394
BOOL createDirectoryIfNotExists(NSString *path, NSError **error);
9495

9596
/**

Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -358,12 +358,47 @@ class SentrySessionReplayIntegrationTests: XCTestCase {
358358
XCTAssertTrue(sessionReplay.isFullSession)
359359
}
360360

361-
func createLastSessionReplay(writeSessionInfo: Bool = true, errorSampleRate: Double = 1) throws {
361+
func testCleanUp() throws {
362+
// Create 3 old Sessions
363+
try createLastSessionReplay()
364+
try createLastSessionReplay()
365+
try createLastSessionReplay()
366+
SentryDependencyContainer.sharedInstance().dispatchQueueWrapper = TestSentryDispatchQueueWrapper()
367+
368+
// Start the integration with a configuration that will enable it
369+
startSDK(sessionSampleRate: 0, errorSampleRate: 1)
370+
371+
// Check whether there is only one old session directory and the current session directory
372+
let content = try FileManager.default.contentsOfDirectory(atPath: replayFolder()).filter { name in
373+
!name.hasPrefix("replay") && !name.hasPrefix(".") //remove replay info files and system directories
374+
}
375+
376+
XCTAssertEqual(content.count, 2)
377+
}
378+
379+
func testCleanUpWithNoFiles() throws {
362380
let options = Options()
363381
options.dsn = "https://[email protected]/test"
364382
options.cacheDirectoryPath = FileManager.default.temporaryDirectory.path
365383

366-
let replayFolder = options.cacheDirectoryPath + "/io.sentry/\(options.parsedDsn?.getHash() ?? "")/replay"
384+
let dispatchQueue = TestSentryDispatchQueueWrapper()
385+
SentryDependencyContainer.sharedInstance().dispatchQueueWrapper = dispatchQueue
386+
SentryDependencyContainer.sharedInstance().fileManager = try SentryFileManager(options: options)
387+
388+
if FileManager.default.fileExists(atPath: replayFolder()) {
389+
try FileManager.default.removeItem(atPath: replayFolder())
390+
}
391+
392+
// We can't use SentrySDK.start because the dependency container dispatch queue is used for other tasks.
393+
// Manually starting the integration and initializing it makes the test more controlled.
394+
let integration = SentrySessionReplayIntegration()
395+
integration.install(with: options)
396+
397+
XCTAssertEqual(dispatchQueue.dispatchAsyncCalled, 0)
398+
}
399+
400+
func createLastSessionReplay(writeSessionInfo: Bool = true, errorSampleRate: Double = 1) throws {
401+
let replayFolder = replayFolder()
367402
let jsonPath = replayFolder + "/replay.current"
368403
var sessionFolder = UUID().uuidString
369404
let info: [String: Any] = ["replayId": SentryId().sentryIdString,
@@ -389,6 +424,13 @@ class SentrySessionReplayIntegrationTests: XCTestCase {
389424
sentrySessionReplaySync_writeInfo()
390425
}
391426
}
427+
428+
func replayFolder() -> String {
429+
let options = Options()
430+
options.dsn = "https://[email protected]/test"
431+
options.cacheDirectoryPath = FileManager.default.temporaryDirectory.path
432+
return options.cacheDirectoryPath + "/io.sentry/\(options.parsedDsn?.getHash() ?? "")/replay"
433+
}
392434
}
393435

394436
#endif

0 commit comments

Comments
 (0)