Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
### Fixes

- Apply `screenshot` masking options to Android error and feedback screenshots ([#6930](https://github.com/getsentry/sentry-react-native/pull/6930))
- Persist fatal JS error envelopes to disk on Android before the process exits ([#6937](https://github.com/getsentry/sentry-react-native/pull/6937))
- The hard-crash envelope was only queued on the transport thread, so the event was lost when React Native terminated the process right after. It is now written to the offline cache on the calling thread and sent on the next launch, matching iOS.

### Internal

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import android.app.Activity;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
Expand All @@ -30,8 +31,10 @@
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeArray;
import com.facebook.react.bridge.WritableNativeMap;
import io.sentry.Hint;
import io.sentry.ILogger;
import io.sentry.IScope;
import io.sentry.IScopes;
import io.sentry.ISentryExecutorService;
import io.sentry.ISerializer;
import io.sentry.PerformanceCollectionData;
Expand All @@ -41,9 +44,12 @@
import io.sentry.SentryAttributes;
import io.sentry.SentryDate;
import io.sentry.SentryDateProvider;
import io.sentry.SentryEnvelope;
import io.sentry.SentryEnvelopeItem;
import io.sentry.SentryExecutorService;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.Session;
import io.sentry.android.core.AndroidProfiler;
import io.sentry.android.core.BuildInfoProvider;
import io.sentry.android.core.InternalSentrySdk;
Expand All @@ -55,12 +61,15 @@
import io.sentry.android.core.internal.debugmeta.AssetsDebugMetaLoader;
import io.sentry.android.core.internal.util.SentryFrameMetricsCollector;
import io.sentry.android.core.performance.AppStartMetrics;
import io.sentry.cache.EnvelopeCache;
import io.sentry.cache.IEnvelopeCache;
import io.sentry.profilemeasurements.ProfileMeasurement;
import io.sentry.profilemeasurements.ProfileMeasurementValue;
import io.sentry.protocol.SdkVersion;
import io.sentry.protocol.SentryId;
import io.sentry.protocol.User;
import io.sentry.protocol.ViewHierarchy;
import io.sentry.transport.NoOpEnvelopeCache;
import io.sentry.util.DebugMetaPropertiesApplier;
import io.sentry.util.FileUtils;
import io.sentry.util.JsonSerializationUtils;
Expand All @@ -69,6 +78,7 @@
import io.sentry.vendor.Base64;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
Expand Down Expand Up @@ -628,17 +638,91 @@ public void registerReplayTraceId(String traceId) {

public void captureEnvelope(String rawBytes, ReadableMap options, Promise promise) {
byte[] bytes = Base64.decode(rawBytes, Base64.DEFAULT);
final boolean hardCrashed = options.hasKey("hardCrashed") && options.getBoolean("hardCrashed");

try {
InternalSentrySdk.captureEnvelope(
bytes, !options.hasKey("hardCrashed") || !options.getBoolean("hardCrashed"));
if (!hardCrashed || isAppDebuggable() || !storeHardCrashEnvelope(bytes)) {
InternalSentrySdk.captureEnvelope(bytes, !hardCrashed);
}
} catch (Throwable e) { // NOPMD - We don't want to crash in any case
logger.log(SentryLevel.ERROR, "Error while capturing envelope");
promise.resolve(false);
}
promise.resolve(true);
}

/** Mirrors the iOS {@code #if DEBUG} gate: debug builds keep the direct capture path. */
private boolean isAppDebuggable() {
final @Nullable ApplicationInfo appInfo = reactApplicationContext.getApplicationInfo();
return appInfo != null && (appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
}

/**
* Writes a hard-crash envelope to the offline cache on the calling thread, like iOS {@code
* store}. {@code InternalSentrySdk.captureEnvelope} only queues the envelope on the transport
* executor; React Native then rethrows the error as {@code JavascriptException}, which is an
* ignored exception type, so the process dies before the executor writes the envelope to disk.
* The next launch sends the cached file. Returns false without touching anything when there is no
* disk cache, so the caller falls back to the queued path.
*/
private boolean storeHardCrashEnvelope(final @NotNull byte[] bytes) throws IOException {
final @NotNull IScopes scopes = Sentry.getCurrentScopes();
final @NotNull SentryOptions sentryOptions = scopes.getOptions();
final @NotNull IEnvelopeCache cache = sentryOptions.getEnvelopeDiskCache();
final @Nullable String cacheDirPath = sentryOptions.getCacheDirPath();
if (!sentryOptions.isEnabled() || cacheDirPath == null || cache instanceof NoOpEnvelopeCache) {
return false;
}

final @Nullable SentryEnvelope envelope;
try (InputStream in = new ByteArrayInputStream(bytes)) {
envelope = sentryOptions.getEnvelopeReader().read(in);
} catch (IOException | IllegalArgumentException e) {
logger.log(SentryLevel.ERROR, "Failed to read the hard crash envelope.", e);
return false;
}
if (envelope == null) {
return false;
}

final @NotNull List<SentryEnvelopeItem> items = new ArrayList<>();
for (final SentryEnvelopeItem item : envelope.getItems()) {
items.add(item);
}

// Same session handling as InternalSentrySdk.captureEnvelope for a crash: end the session as
// crashed and remove it from the scope so it is not sent twice.
final @NotNull AtomicReference<Session> crashedSession = new AtomicReference<>();
scopes.configureScope(
scope ->
scope.withSession(
session -> {
if (session != null && session.update(Session.State.Crashed, null, true, null)) {
session.end();
scope.clearSession();
crashedSession.set(session.clone());
}
}));
final @Nullable Session session = crashedSession.get();
if (session != null) {
items.add(SentryEnvelopeItem.fromSession(sentryOptions.getSerializer(), session));
}

final @NotNull SentryEnvelope repackaged = new SentryEnvelope(envelope.getHeader(), items);
if (!cache.storeEnvelope(repackaged, new Hint())) {
logger.log(SentryLevel.ERROR, "Failed to store the hard crash envelope, queueing it.");
scopes.captureEnvelope(repackaged);
return true;
}

// The crashed session is now on disk, so the persisted current-session file can go. If the
// store failed, the file stays so the next launch still reports the session as abnormal.
if (session != null && !EnvelopeCache.getCurrentSessionFile(cacheDirPath).delete()) {
logger.log(SentryLevel.WARNING, "Failed to delete the current session file.");
}
return true;
}

public void captureScreenshot(Promise promise) {

final Activity activity = getCurrentActivity();
Expand Down
Loading
Loading