Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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, tags, and app version/build—and use the matching replay-on-error sample rate—for `ApplicationExitInfo` ANR and native crash events captured before SDK initialization, without reusing options cached by a later app update ([#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