Skip to content

Commit

Permalink
fix: Delete old session replay files (#4446)
Browse files Browse the repository at this point in the history
Clean up for old session replay files
  • Loading branch information
brustolin authored Oct 17, 2024
1 parent b048ba3 commit d605f55
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 12 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ via the option `swizzleClassNameExclude`.
- Finish TTID correctly when viewWillAppear is skipped (#4417)
- Swizzling RootUIViewController if ignored by `swizzleClassNameExclude` (#4407)
- Data race in SentrySwizzleInfo.originalCalled (#4434)
- Delete old session replay files (#4446)
- Thread running at user-initiated quality-of-service for session replay (#4439)


### Improvements

- Serializing profile on a BG Thread (#4377) to avoid potentially slightly blocking the main thread.
Expand Down
6 changes: 6 additions & 0 deletions Sources/Sentry/SentryFileManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,12 @@ - (void)deleteOldEnvelopesFromPath:(NSString *)envelopesPath
}
}

- (BOOL)isDirectory:(NSString *)path
{
BOOL isDir = NO;
return [NSFileManager.defaultManager fileExistsAtPath:path isDirectory:&isDir] && isDir;
}

- (void)deleteAllEnvelopes
{
[self removeFileAtPath:self.envelopesPath];
Expand Down
55 changes: 47 additions & 8 deletions Sources/Sentry/SentrySessionReplayIntegration.m
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ - (void)setupWith:(SentryReplayOptions *)replayOptions enableTouchTracker:(BOOL)
_notificationCenter = SentryDependencyContainer.sharedInstance.notificationCenterWrapper;

[self moveCurrentReplay];
[self cleanUp];

[SentrySDK.currentHub registerSessionListener:self];
[SentryGlobalEventProcessor.shared
addEventProcessor:^SentryEvent *_Nullable(SentryEvent *_Nonnull event) {
Expand All @@ -103,6 +105,19 @@ - (void)setupWith:(SentryReplayOptions *)replayOptions enableTouchTracker:(BOOL)
[SentryDependencyContainer.sharedInstance.reachability addObserver:self];
}

- (nullable NSDictionary<NSString *, id> *)lastReplayInfo
{
NSURL *dir = [self replayDirectory];
NSURL *lastReplayUrl = [dir URLByAppendingPathComponent:SENTRY_LAST_REPLAY];
NSData *lastReplay = [NSData dataWithContentsOfURL:lastReplayUrl];

if (lastReplay == nil) {
return nil;
}

return [SentrySerialization deserializeDictionaryFromJsonData:lastReplay];
}

/**
* Send the cached frames from a previous session that eventually crashed.
* This function is called when processing an event created by SentryCrashIntegration,
Expand All @@ -112,15 +127,8 @@ - (void)setupWith:(SentryReplayOptions *)replayOptions enableTouchTracker:(BOOL)
- (void)resumePreviousSessionReplay:(SentryEvent *)event
{
NSURL *dir = [self replayDirectory];
NSURL *lastReplayUrl = [dir URLByAppendingPathComponent:SENTRY_LAST_REPLAY];
NSData *lastReplay = [NSData dataWithContentsOfURL:lastReplayUrl];
NSDictionary<NSString *, id> *jsonObject = [self lastReplayInfo];

if (lastReplay == nil) {
return;
}

NSDictionary<NSString *, id> *jsonObject =
[SentrySerialization deserializeDictionaryFromJsonData:lastReplay];
if (jsonObject == nil) {
return;
}
Expand Down Expand Up @@ -365,6 +373,37 @@ - (void)moveCurrentReplay
}
}

- (void)cleanUp
{
NSURL *replayDir = [self replayDirectory];
NSDictionary<NSString *, id> *lastReplayInfo = [self lastReplayInfo];
NSString *lastReplayFolder = lastReplayInfo[@"path"];

SentryFileManager *fileManager = SentryDependencyContainer.sharedInstance.fileManager;
// Mapping replay folder here and not in dispatched queue to prevent a race condition between
// listing files and creating a new replay session.
NSArray *replayFiles = [fileManager allFilesInFolder:replayDir.path];
if (replayFiles.count == 0) {
return;
}

[SentryDependencyContainer.sharedInstance.dispatchQueueWrapper dispatchAsyncWithBlock:^{
for (NSString *file in replayFiles) {
// Skip the last replay folder.
if ([file isEqualToString:lastReplayFolder]) {
continue;
}

NSString *filePath = [replayDir.path stringByAppendingPathComponent:file];

// Check if the file is a directory before deleting it.
if ([fileManager isDirectory:filePath]) {
[fileManager removeFileAtPath:filePath];
}
}
}];
}

- (void)pause
{
[self.sessionReplay pause];
Expand Down
3 changes: 2 additions & 1 deletion Sources/Sentry/include/SentryFileManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ SENTRY_NO_INIT
- (NSNumber *_Nullable)readTimezoneOffset;
- (void)storeTimezoneOffset:(NSInteger)offset;
- (void)deleteTimezoneOffset;

- (NSArray<NSString *> *)allFilesInFolder:(NSString *)path;
- (BOOL)isDirectory:(NSString *)path;
BOOL createDirectoryIfNotExists(NSString *path, NSError **error);

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -358,12 +358,47 @@ class SentrySessionReplayIntegrationTests: XCTestCase {
XCTAssertTrue(sessionReplay.isFullSession)
}

func createLastSessionReplay(writeSessionInfo: Bool = true, errorSampleRate: Double = 1) throws {
func testCleanUp() throws {
// Create 3 old Sessions
try createLastSessionReplay()
try createLastSessionReplay()
try createLastSessionReplay()
SentryDependencyContainer.sharedInstance().dispatchQueueWrapper = TestSentryDispatchQueueWrapper()

// Start the integration with a configuration that will enable it
startSDK(sessionSampleRate: 0, errorSampleRate: 1)

// Check whether there is only one old session directory and the current session directory
let content = try FileManager.default.contentsOfDirectory(atPath: replayFolder()).filter { name in
!name.hasPrefix("replay") && !name.hasPrefix(".") //remove replay info files and system directories
}

XCTAssertEqual(content.count, 2)
}

func testCleanUpWithNoFiles() throws {
let options = Options()
options.dsn = "https://[email protected]/test"
options.cacheDirectoryPath = FileManager.default.temporaryDirectory.path

let replayFolder = options.cacheDirectoryPath + "/io.sentry/\(options.parsedDsn?.getHash() ?? "")/replay"
let dispatchQueue = TestSentryDispatchQueueWrapper()
SentryDependencyContainer.sharedInstance().dispatchQueueWrapper = dispatchQueue
SentryDependencyContainer.sharedInstance().fileManager = try SentryFileManager(options: options)

if FileManager.default.fileExists(atPath: replayFolder()) {
try FileManager.default.removeItem(atPath: replayFolder())
}

// We can't use SentrySDK.start because the dependency container dispatch queue is used for other tasks.
// Manually starting the integration and initializing it makes the test more controlled.
let integration = SentrySessionReplayIntegration()
integration.install(with: options)

XCTAssertEqual(dispatchQueue.dispatchAsyncCalled, 0)
}

func createLastSessionReplay(writeSessionInfo: Bool = true, errorSampleRate: Double = 1) throws {
let replayFolder = replayFolder()
let jsonPath = replayFolder + "/replay.current"
var sessionFolder = UUID().uuidString
let info: [String: Any] = ["replayId": SentryId().sentryIdString,
Expand All @@ -389,6 +424,13 @@ class SentrySessionReplayIntegrationTests: XCTestCase {
sentrySessionReplaySync_writeInfo()
}
}

func replayFolder() -> String {
let options = Options()
options.dsn = "https://[email protected]/test"
options.cacheDirectoryPath = FileManager.default.temporaryDirectory.path
return options.cacheDirectoryPath + "/io.sentry/\(options.parsedDsn?.getHash() ?? "")/replay"
}
}

#endif

0 comments on commit d605f55

Please sign in to comment.