Skip to content

Commit 4c6184a

Browse files
fix(core): Make session cache writes session-id aware
SessionEnd previously deleted session.json unconditionally and SessionStart always rotated it. A delayed end or start could therefore drop a newer session snapshot. Both paths now compare session ids and start times before deleting or rotating, and a new persistCurrentSession lets callers flush the active session to disk. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 1ae2b46 commit 4c6184a

3 files changed

Lines changed: 337 additions & 12 deletions

File tree

sentry/api/sentry.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4862,6 +4862,7 @@ public class io/sentry/cache/EnvelopeCache : io/sentry/cache/IEnvelopeCache {
48624862
public static fun getPreviousSessionFile (Ljava/lang/String;)Ljava/io/File;
48634863
public fun iterator ()Ljava/util/Iterator;
48644864
public fun movePreviousSession (Ljava/io/File;Ljava/io/File;)V
4865+
public fun persistCurrentSession (Lio/sentry/Session;)V
48654866
public fun store (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V
48664867
public fun storeEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Z
48674868
public fun waitPreviousSessionFlush ()Z

sentry/src/main/java/io/sentry/cache/EnvelopeCache.java

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ public boolean storeEnvelope(final @NotNull SentryEnvelope envelope, final @NotN
106106
return storeInternal(envelope, hint);
107107
}
108108

109+
@SuppressWarnings("JavaUtilDate")
109110
private boolean storeInternal(final @NotNull SentryEnvelope envelope, final @NotNull Hint hint) {
110111
Objects.requireNonNull(envelope, "Envelope is required.");
111112

@@ -118,8 +119,22 @@ private boolean storeInternal(final @NotNull SentryEnvelope envelope, final @Not
118119
final File previousSessionFile = getPreviousSessionFile(directoryPath);
119120

120121
if (HintUtils.hasType(hint, SessionEnd.class)) {
121-
if (!currentSessionFile.delete()) {
122-
options.getLogger().log(WARNING, "Current envelope doesn't exist.");
122+
try (final @NotNull ISentryLifecycleToken ignored = sessionLock.acquire()) {
123+
final @Nullable Session endingSession = readSessionFromEnvelope(envelope);
124+
final @Nullable Session currentSession = readSessionFromDisk(currentSessionFile);
125+
final boolean preservePendingSession =
126+
endingSession != null
127+
&& currentSession != null
128+
&& currentSession.isPendingUnhandled()
129+
&& endingSession.getSessionId() != null
130+
&& currentSession.getSessionId() != null
131+
&& !Objects.equals(endingSession.getSessionId(), currentSession.getSessionId())
132+
&& endingSession.getStarted() != null
133+
&& currentSession.getStarted() != null
134+
&& currentSession.getStarted().after(endingSession.getStarted());
135+
if (!preservePendingSession && !currentSessionFile.delete()) {
136+
options.getLogger().log(WARNING, "Current envelope doesn't exist.");
137+
}
123138
}
124139
}
125140

@@ -129,8 +144,22 @@ private boolean storeInternal(final @NotNull SentryEnvelope envelope, final @Not
129144
}
130145

131146
if (HintUtils.hasType(hint, SessionStart.class)) {
132-
movePreviousSession(currentSessionFile, previousSessionFile);
133-
updateCurrentSession(currentSessionFile, envelope);
147+
try (final @NotNull ISentryLifecycleToken ignored = sessionLock.acquire()) {
148+
final @Nullable Session startingSession = readSessionFromEnvelope(envelope);
149+
if (startingSession != null) {
150+
final @Nullable Session currentSession = readSessionFromDisk(currentSessionFile);
151+
if (currentSession != null && hasSameSessionId(currentSession, startingSession)) {
152+
if (!isNewerPendingOrErrorSnapshot(currentSession, startingSession)) {
153+
writeSessionToDisk(currentSessionFile, startingSession);
154+
}
155+
} else {
156+
movePreviousSession(currentSessionFile, previousSessionFile);
157+
writeSessionToDisk(currentSessionFile, startingSession);
158+
}
159+
} else {
160+
movePreviousSession(currentSessionFile, previousSessionFile);
161+
}
162+
}
134163

135164
boolean crashedLastRun = false;
136165
final File crashMarkerFile = new File(options.getCacheDirPath(), NATIVE_CRASH_MARKER_FILE);
@@ -274,8 +303,7 @@ private void writeCrashMarkerFile() {
274303
}
275304
}
276305

277-
private void updateCurrentSession(
278-
final @NotNull File currentSessionFile, final @NotNull SentryEnvelope envelope) {
306+
private @Nullable Session readSessionFromEnvelope(final @NotNull SentryEnvelope envelope) {
279307
final Iterable<SentryEnvelopeItem> items = envelope.getItems();
280308

281309
// we know that an envelope with a SessionStart hint has a single item inside
@@ -295,7 +323,7 @@ private void updateCurrentSession(
295323
"Item of type %s returned null by the parser.",
296324
item.getHeader().getType());
297325
} else {
298-
writeSessionToDisk(currentSessionFile, session);
326+
return session;
299327
}
300328
} catch (Throwable e) {
301329
options.getLogger().log(ERROR, "Item failed to process.", e);
@@ -309,10 +337,35 @@ private void updateCurrentSession(
309337
item.getHeader().getType());
310338
}
311339
} else {
312-
options
313-
.getLogger()
314-
.log(INFO, "Current envelope %s is empty", currentSessionFile.getAbsolutePath());
340+
options.getLogger().log(INFO, "Current envelope is empty.");
315341
}
342+
return null;
343+
}
344+
345+
private @Nullable Session readSessionFromDisk(final @NotNull File sessionFile) {
346+
if (!sessionFile.exists()) {
347+
return null;
348+
}
349+
try (final Reader reader =
350+
new BufferedReader(new InputStreamReader(new FileInputStream(sessionFile), UTF_8))) {
351+
return serializer.getValue().deserialize(reader, Session.class);
352+
} catch (Exception e) {
353+
options.getLogger().log(ERROR, "Failed to read session from disk.", e);
354+
return null;
355+
}
356+
}
357+
358+
private boolean isNewerPendingOrErrorSnapshot(
359+
final @NotNull Session currentSession, final @NotNull Session startingSession) {
360+
return (currentSession.isPendingUnhandled() && !startingSession.isPendingUnhandled())
361+
|| currentSession.errorCount() > startingSession.errorCount();
362+
}
363+
364+
private boolean hasSameSessionId(
365+
final @NotNull Session firstSession, final @NotNull Session secondSession) {
366+
return firstSession.getSessionId() != null
367+
&& secondSession.getSessionId() != null
368+
&& Objects.equals(firstSession.getSessionId(), secondSession.getSessionId());
316369
}
317370

318371
private boolean writeEnvelopeToDisk(
@@ -352,6 +405,13 @@ private void writeSessionToDisk(final @NotNull File file, final @NotNull Session
352405
}
353406
}
354407

408+
@ApiStatus.Internal
409+
public void persistCurrentSession(final @NotNull Session session) {
410+
try (final @NotNull ISentryLifecycleToken ignored = sessionLock.acquire()) {
411+
writeSessionToDisk(getCurrentSessionFile(directory.getOrCreate().getAbsolutePath()), session);
412+
}
413+
}
414+
355415
@Override
356416
public void discard(final @NotNull SentryEnvelope envelope) {
357417
Objects.requireNonNull(envelope, "Envelope is required.");

0 commit comments

Comments
 (0)