Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

### Fixes

- Backfill release, environment, distribution, and app version/build for ANR and native crash events that occurred before SDK initialization, provided the app has not since been updated ([#5762](https://github.com/getsentry/sentry-java/pull/5762))
- Session Replay: Fix first recording segment missing for replays in `buffer` mode ([#5753](https://github.com/getsentry/sentry-java/pull/5753))
- Session Replay: Fix error-to-replay linkage in `buffer` mode ([#5754](https://github.com/getsentry/sentry-java/pull/5754))
- Prevent logs and metrics from remaining queued after a flush scheduling race ([#5756](https://github.com/getsentry/sentry-java/pull/5756))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@ static void initializeIntegrationsAndProcessors(
if (options.getCacheDirPath() != null) {
options.addScopeObserver(new PersistingScopeObserver(options));
options.addOptionsObserver(new PersistingOptionsObserver(options));
final PackageInfo packageInfo = ContextUtils.getPackageInfo(context, buildInfoProvider);
if (packageInfo != null && packageInfo.lastUpdateTime > 0) {
options.addOptionsObserver(
new PersistingOptionsCacheGenerationObserver(options, packageInfo.lastUpdateTime));
}
}

options.addEventProcessor(new DeduplicateMultithreadedEventProcessor(options));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import io.sentry.exception.ExceptionMechanismException;
import io.sentry.hints.AbnormalExit;
import io.sentry.hints.Backfillable;
import io.sentry.hints.NativeCrashExit;
import io.sentry.protocol.App;
import io.sentry.protocol.Contexts;
import io.sentry.protocol.DebugImage;
Expand Down Expand Up @@ -161,7 +162,13 @@ public ApplicationExitInfoEventProcessor(
mergeOS(event);
setDevice(event);

final OptionsSource optionsSource = getOptionsSource(backfillable);

if (!backfillable.shouldEnrich()) {
setRelease(event, optionsSource);
setEnvironment(event, optionsSource);
setDist(event, optionsSource);
setAppVersionAndBuild(event);
options
.getLogger()
.log(
Expand All @@ -170,9 +177,9 @@ public ApplicationExitInfoEventProcessor(
return event;
}

backfillScope(event);
backfillScope(event, optionsSource);

backfillOptions(event);
backfillOptions(event, optionsSource);

setStaticValues(event);

Expand All @@ -184,7 +191,8 @@ public ApplicationExitInfoEventProcessor(
}

// region scope persisted values
private void backfillScope(final @NotNull SentryEvent event) {
private void backfillScope(
final @NotNull SentryEvent event, final @NotNull OptionsSource optionsSource) {
setRequest(event);
setUser(event);
setScopeTags(event);
Expand All @@ -195,19 +203,25 @@ private void backfillScope(final @NotNull SentryEvent event) {
setFingerprints(event);
setLevel(event);
setTrace(event);
setReplayId(event);
setReplayId(event, optionsSource);
}

private boolean sampleReplay(final @NotNull SentryEvent event) {
private boolean sampleReplay(
final @NotNull SentryEvent event, final @NotNull OptionsSource optionsSource) {
final @Nullable Double currentSampleRate = options.getSessionReplay().getOnErrorSampleRate();
final @Nullable String replayErrorSampleRate =
PersistingOptionsObserver.read(options, REPLAY_ERROR_SAMPLE_RATE_FILENAME, String.class);
getLaunchOption(
REPLAY_ERROR_SAMPLE_RATE_FILENAME,
String.class,
currentSampleRate == null ? null : currentSampleRate.toString(),
optionsSource);

if (replayErrorSampleRate == null) {
return false;
}

try {
// we have to sample here with the old sample rate, because it may change between app launches
// Sample with the rate from the relevant launch because it may change between launches.
final double replayErrorSampleRateDouble = Double.parseDouble(replayErrorSampleRate);
if (replayErrorSampleRateDouble < SentryRandom.current().nextDouble()) {
options
Expand All @@ -226,15 +240,16 @@ private boolean sampleReplay(final @NotNull SentryEvent event) {
return true;
}

private void setReplayId(final @NotNull SentryEvent event) {
private void setReplayId(
final @NotNull SentryEvent event, final @NotNull OptionsSource optionsSource) {
@Nullable String persistedReplayId = readFromDisk(options, REPLAY_FILENAME, String.class);
@Nullable String cacheDirPath = options.getCacheDirPath();
if (cacheDirPath == null) {
return;
}
final @NotNull File replayFolder = new File(cacheDirPath, "replay_" + persistedReplayId);
if (!replayFolder.exists()) {
if (!sampleReplay(event)) {
if (!sampleReplay(event, optionsSource)) {
return;
}
// if the replay folder does not exist (e.g. running in buffer mode), we need to find the
Expand Down Expand Up @@ -393,14 +408,15 @@ private void setRequest(final @NotNull SentryBaseEvent event) {
// endregion

// region options persisted values
private void backfillOptions(final @NotNull SentryEvent event) {
setRelease(event);
setEnvironment(event);
setDist(event);
setDebugMeta(event);
setSdk(event);
private void backfillOptions(
final @NotNull SentryEvent event, final @NotNull OptionsSource optionsSource) {
setRelease(event, optionsSource);
setEnvironment(event, optionsSource);
setDist(event, optionsSource);
setDebugMeta(event, optionsSource);
setSdk(event, optionsSource);
setApp(event);
setOptionsTags(event);
setOptionsTags(event, optionsSource);
}

private void setApp(final @NotNull SentryBaseEvent event) {
Expand All @@ -415,25 +431,6 @@ private void setApp(final @NotNull SentryBaseEvent event) {
app.setAppIdentifier(packageInfo.packageName);
}

// backfill versionName and versionCode from the persisted release string
final String release =
event.getRelease() != null
? event.getRelease()
: PersistingOptionsObserver.read(options, RELEASE_FILENAME, String.class);
if (release != null) {
try {
final String versionName =
release.substring(release.indexOf('@') + 1, release.indexOf('+'));
final String versionCode = release.substring(release.indexOf('+') + 1);
app.setAppVersion(versionName);
app.setAppBuild(versionCode);
} catch (Throwable e) {
options
.getLogger()
.log(SentryLevel.WARNING, "Failed to parse release from scope cache: %s", release);
}
}

try {
final ContextUtils.SplitApksInfo splitApksInfo =
DeviceInfoUtil.getInstance(context, options).getSplitApksInfo();
Expand All @@ -448,25 +445,50 @@ private void setApp(final @NotNull SentryBaseEvent event) {
}

event.getContexts().setApp(app);
setAppVersionAndBuild(event);
}

private void setAppVersionAndBuild(final @NotNull SentryBaseEvent event) {
final String release = event.getRelease();
if (release != null) {
try {
@Nullable App app = event.getContexts().getApp();
if (app == null) {
app = new App();
}
final String versionName =
release.substring(release.indexOf('@') + 1, release.indexOf('+'));
final String versionCode = release.substring(release.indexOf('+') + 1);
app.setAppVersion(versionName);
app.setAppBuild(versionCode);
event.getContexts().setApp(app);
} catch (Throwable e) {
options
.getLogger()
.log(SentryLevel.WARNING, "Failed to parse release from scope cache: %s", release);
}
}
}

private void setRelease(final @NotNull SentryBaseEvent event) {
private void setRelease(
final @NotNull SentryBaseEvent event, final @NotNull OptionsSource optionsSource) {
if (event.getRelease() == null) {
final String release =
PersistingOptionsObserver.read(options, RELEASE_FILENAME, String.class);
event.setRelease(release);
event.setRelease(
getLaunchOption(RELEASE_FILENAME, String.class, options.getRelease(), optionsSource));
}
}

private void setEnvironment(final @NotNull SentryBaseEvent event) {
private void setEnvironment(
final @NotNull SentryBaseEvent event, final @NotNull OptionsSource optionsSource) {
if (event.getEnvironment() == null) {
final String environment =
PersistingOptionsObserver.read(options, ENVIRONMENT_FILENAME, String.class);
event.setEnvironment(environment != null ? environment : options.getEnvironment());
event.setEnvironment(
getLaunchOption(
ENVIRONMENT_FILENAME, String.class, options.getEnvironment(), optionsSource));
}
}

private void setDebugMeta(final @NotNull SentryBaseEvent event) {
private void setDebugMeta(
final @NotNull SentryBaseEvent event, final @NotNull OptionsSource optionsSource) {
DebugMeta debugMeta = event.getDebugMeta();

if (debugMeta == null) {
Expand All @@ -478,7 +500,8 @@ private void setDebugMeta(final @NotNull SentryBaseEvent event) {
List<DebugImage> images = debugMeta.getImages();
if (images != null) {
final String proguardUuid =
PersistingOptionsObserver.read(options, PROGUARD_UUID_FILENAME, String.class);
getBuildOption(
PROGUARD_UUID_FILENAME, String.class, options.getProguardUuid(), optionsSource);

if (proguardUuid != null) {
final DebugImage debugImage = new DebugImage();
Expand All @@ -490,15 +513,14 @@ private void setDebugMeta(final @NotNull SentryBaseEvent event) {
}
}

private void setDist(final @NotNull SentryBaseEvent event) {
private void setDist(
final @NotNull SentryBaseEvent event, final @NotNull OptionsSource optionsSource) {
if (event.getDist() == null) {
final String dist = PersistingOptionsObserver.read(options, DIST_FILENAME, String.class);
event.setDist(dist);
event.setDist(getLaunchOption(DIST_FILENAME, String.class, options.getDist(), optionsSource));
}
// if there's no user-set dist, fall back to versionCode from the persisted release string
// if there's no user-set dist, fall back to versionCode from the release string
if (event.getDist() == null) {
final String release =
PersistingOptionsObserver.read(options, RELEASE_FILENAME, String.class);
final String release = event.getRelease();
if (release != null) {
try {
final String versionCode = release.substring(release.indexOf('+') + 1);
Expand All @@ -512,20 +534,85 @@ private void setDist(final @NotNull SentryBaseEvent event) {
}
}

private void setSdk(final @NotNull SentryBaseEvent event) {
private <T> @Nullable T getLaunchOption(
final @NotNull String fileName,
final @NotNull Class<T> clazz,
final @Nullable T currentValue,
final @NotNull OptionsSource optionsSource) {
if (optionsSource == OptionsSource.CURRENT) {
return currentValue;
} else if (optionsSource == OptionsSource.NONE) {
return null;
}

final T persistedValue = PersistingOptionsObserver.read(options, fileName, clazz);
return persistedValue != null || optionsSource == OptionsSource.PERSISTED
? persistedValue
: currentValue;
}

private <T> @Nullable T getBuildOption(
final @NotNull String fileName,
final @NotNull Class<T> clazz,
final @Nullable T currentValue,
final @NotNull OptionsSource optionsSource) {
if (optionsSource == OptionsSource.CURRENT
|| optionsSource == OptionsSource.PERSISTED_WITH_CURRENT_FALLBACK) {
return currentValue;
} else if (optionsSource == OptionsSource.NONE) {
return null;
}
return PersistingOptionsObserver.read(options, fileName, clazz);
}

private @NotNull OptionsSource getOptionsSource(final @NotNull Backfillable hint) {
final @Nullable Long timestamp;
if (hint instanceof AbnormalExit) {
timestamp = ((AbnormalExit) hint).timestamp();
} else if (hint instanceof NativeCrashExit) {
timestamp = ((NativeCrashExit) hint).timestamp();
} else {
timestamp = null;
}
final Long cachedLastUpdateTime = PersistingOptionsCacheGenerationObserver.read(options);
final PackageInfo packageInfo = ContextUtils.getPackageInfo(context, buildInfoProvider);
final long currentLastUpdateTime = packageInfo == null ? 0 : packageInfo.lastUpdateTime;

if (timestamp != null && currentLastUpdateTime > 0 && currentLastUpdateTime <= timestamp) {
return cachedLastUpdateTime != null && cachedLastUpdateTime == currentLastUpdateTime
? OptionsSource.PERSISTED_WITH_CURRENT_FALLBACK
: OptionsSource.CURRENT;
}
if (cachedLastUpdateTime == null) {
return OptionsSource.PERSISTED;
}
// A cache generation created after the exit cannot describe that exit.
if (timestamp != null && cachedLastUpdateTime > 0 && cachedLastUpdateTime <= timestamp) {
return OptionsSource.PERSISTED;
}
return OptionsSource.NONE;
}

Comment thread
cursor[bot] marked this conversation as resolved.
private void setSdk(
final @NotNull SentryBaseEvent event, final @NotNull OptionsSource optionsSource) {
if (event.getSdk() == null) {
final SdkVersion sdkVersion =
PersistingOptionsObserver.read(options, SDK_VERSION_FILENAME, SdkVersion.class);
getBuildOption(
SDK_VERSION_FILENAME, SdkVersion.class, options.getSdkVersion(), optionsSource);
event.setSdk(sdkVersion);
}
}

@SuppressWarnings("unchecked")
private void setOptionsTags(final @NotNull SentryBaseEvent event) {
private void setOptionsTags(
final @NotNull SentryBaseEvent event, final @NotNull OptionsSource optionsSource) {
final Map<String, String> tags =
(Map<String, String>)
PersistingOptionsObserver.read(
options, PersistingOptionsObserver.TAGS_FILENAME, Map.class);
getLaunchOption(
PersistingOptionsObserver.TAGS_FILENAME,
Map.class,
options.getTags(),
optionsSource);
if (tags == null) {
return;
}
Expand All @@ -542,6 +629,13 @@ private void setOptionsTags(final @NotNull SentryBaseEvent event) {

// endregion

private enum OptionsSource {
CURRENT,
PERSISTED,
PERSISTED_WITH_CURRENT_FALLBACK,
NONE
}

@Override
public @Nullable Long getOrder() {
return 12000L;
Expand Down
Loading
Loading