getsentry / getsentry/sentry-java

Android Replay: BufferCaptureStrategy.stop() can persist replay state after deleting replay cache dir

Aberta
#6,094 1 comentário 0 reações 0 responsáveis Ver no GitHub
Android Bug Platform: Java Replays
Linguagem predominante
Kotlin
Estrelas
1.4k
Forks
478
Merge médio
3d 4h
PRs com merge (30d)
72

Descrição

### Problem Statement

Android Session Replay can throw an internal error during replay teardown:

```text
Failed to execute task CaptureStrategy.runInBackground
java.io.IOException: No such file or directory
...
at io.sentry.android.replay.ReplayCache.persistSegmentValues
...
at io.sentry.android.replay.capture.BufferCaptureStrategy.stop
```

This appears to happen when stopping a buffering replay.

### Likely root cause

`BufferCaptureStrategy.stop()` currently deletes the replay cache directory and then updates replay state in the same task:

- delete replay cache dir
- set `currentSegment = -1`

The `currentSegment` setter persists state through `ReplayCache.persistSegmentValues(...)`.

On the replay worker thread, that persistence runs inline, so it can try to recreate/write `.ongoing_segment` after the replay directory has already been deleted.

That leads to:

```text
java.io.IOException: No such file or directory
at java.io.File.createNewFile
at io.sentry.android.replay.ReplayCache.persistSegmentValues
```

### Why this matters

This is at least noisy internal error logging, but it may also contribute to brittle replay lifecycle behavior in manual/custom flows that use combinations of:

- `flush()`
- `pause()`
- `stop()`
- `startBuffering()`

In our investigation, this showed up while testing a custom User Feedback + Replay workaround, where replay behavior was already timing-sensitive.

### Relevant code path

Conceptually, the flow is:

- `BufferCaptureStrategy.stop()`
- `FileUtils.deleteRecursively(replayCacheDir)`
- `currentSegment = -1`
- `persistSegmentValues(...)`
- `ongoingSegmentFile?.createNewFile()`
- parent folder no longer exists

### Expected behavior

Stopping replay should not try to persist replay state into a directory that has already been deleted.

### Actual behavior

Replay teardown logs `No such file or directory` from `ReplayCache.persistSegmentValues`.

### Suggested fix direction

Any of these would likely resolve it:

- clear/persist mutable state before deleting the replay directory
- mark the cache closed before state mutation can persist
- guard `persistSegmentValues` against a missing/deleted parent directory during teardown
- avoid persisting teardown sentinel values like `currentSegment = -1` once shutdown has begun

### Customer-visible context

This was observed during a real customer reproduction involving Android Session Replay plus custom in-app User Feedback, where the workaround flow exercised replay stop/restart behavior repeatedly.

### Logs

```text
Failed to execute task CaptureStrategy.runInBackground
java.io.IOException: No such file or directory
at java.io.UnixFileSystem.createFileExclusively0(Native Method)
at java.io.UnixFileSystem.createFileExclusively(UnixFileSystem.java:373)
at java.io.File.createNewFile(File.java:1022)
at io.sentry.android.replay.ReplayCache.persistSegmentValues$sentry_android_replay_release(ReplayCache.kt:290)
at io.sentry.android.replay.capture.BaseCaptureStrategy$special$$inlined$persistableAtomic$default$2$2.invoke(BaseCaptureStrategy.kt:240)
at io.sentry.android.replay.capture.BaseCaptureStrategy$special$$inlined$persistableAtomic$default$2.runInBackground(BaseCaptureStrategy.kt:227)
at io.sentry.android.replay.capture.BaseCaptureStrategy$special$$inlined$persistableAtomic$default$2.setValue(BaseCaptureStrategy.kt:239)
at io.sentry.android.replay.capture.BaseCaptureStrategy.setCurrentSegment(BaseCaptureStrategy.kt:88)
at io.sentry.android.replay.capture.BufferCaptureStrategy.stop$lambda$0(BufferCaptureStrategy.kt:84)
```

Guia de contribuição

Abrir o guia de contribuição

Direção de pesquisa

Comece com BufferCaptureStrategy.kt e ReplayCache.kt, acompanhando BufferCaptureStrategy.stop() por meio do setter de currentSegment e de persistSegmentValues(). Reproduza o caminho de teardown do replay em buffer, especialmente após excluir o diretório de cache do replay, e verifique se interromper o replay não registra mais o erro No such file or directory.

Escrita pelo modelo de indexação a partir do texto da issue.

Avaliação

Stack de tecnologia
android, kotlin
Domínio
mobile-dev
Tipo de issue
Bug
Dificuldade
3/5
Tempo estimado
1-2 dias
Status de atividade
Ativa
Clareza
Razoavelmente clara
Facilidade para iniciantes
70/100

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.