From f346b2691a1f10f62c87ce2d0eef5c92a8f6a3a4 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 14 Mar 2023 13:39:58 +0100 Subject: [PATCH 01/23] Initial implementation of ANRv2 (#2549) --- sentry-android-core/.gitignore | 3 + .../api/sentry-android-core.api | 24 + .../core/AndroidOptionsInitializer.java | 10 +- .../android/core/AnrIntegrationFactory.java | 21 + .../android/core/AnrV2EventProcessor.java | 563 ++++++++++++++++++ .../sentry/android/core/AnrV2Integration.java | 276 +++++++++ .../io/sentry/android/core/ContextUtils.java | 173 ++++++ .../core/DefaultAndroidEventProcessor.java | 191 +----- .../core/cache/AndroidEnvelopeCache.java | 70 ++- .../core/AndroidOptionsInitializerTest.kt | 55 +- .../android/core/AnrV2EventProcessorTest.kt | 434 ++++++++++++++ .../android/core/AnrV2IntegrationTest.kt | 334 +++++++++++ .../android/core/ContextUtilsUnitTests.kt | 104 ++++ .../sentry/android/core/SentryAndroidTest.kt | 148 ++++- .../core/cache/AndroidEnvelopeCacheTest.kt | 134 ++++- .../sentry/samples/android/MainActivity.java | 38 +- .../api/sentry-test-support.api | 8 + .../src/main/kotlin/io/sentry/test/Mocks.kt | 18 + sentry/api/sentry.api | 130 +++- .../io/sentry/BackfillingEventProcessor.java | 8 + .../src/main/java/io/sentry/Breadcrumb.java | 19 + .../main/java/io/sentry/IOptionsObserver.java | 25 + .../main/java/io/sentry/IScopeObserver.java | 39 +- .../src/main/java/io/sentry/ISerializer.java | 6 + .../main/java/io/sentry/IpAddressUtils.java | 2 +- .../main/java/io/sentry/JsonSerializer.java | 35 ++ .../main/java/io/sentry/NoOpSerializer.java | 8 + sentry/src/main/java/io/sentry/Scope.java | 82 ++- sentry/src/main/java/io/sentry/Sentry.java | 27 + .../main/java/io/sentry/SentryBaseEvent.java | 2 +- .../src/main/java/io/sentry/SentryClient.java | 18 +- .../src/main/java/io/sentry/SentryEvent.java | 4 + .../io/sentry/SentryExceptionFactory.java | 6 +- .../main/java/io/sentry/SentryOptions.java | 25 +- .../java/io/sentry/SentryThreadFactory.java | 4 +- .../src/main/java/io/sentry/SpanContext.java | 18 + .../UncaughtExceptionHandlerIntegration.java | 40 +- .../main/java/io/sentry/cache/CacheUtils.java | 115 ++++ .../java/io/sentry/cache/EnvelopeCache.java | 4 +- .../cache/PersistingOptionsObserver.java | 133 +++++ .../sentry/cache/PersistingScopeObserver.java | 164 +++++ .../java/io/sentry/hints/Backfillable.java | 9 + .../io/sentry/hints/BlockingFlushHint.java | 39 ++ .../src/main/java/io/sentry/protocol/App.java | 21 + .../main/java/io/sentry/protocol/Browser.java | 14 + .../main/java/io/sentry/protocol/Device.java | 77 +++ .../src/main/java/io/sentry/protocol/Gpu.java | 31 + .../io/sentry/protocol/OperatingSystem.java | 19 + .../main/java/io/sentry/protocol/Request.java | 47 +- .../java/io/sentry/protocol/SdkVersion.java | 13 + .../main/java/io/sentry/protocol/User.java | 18 + .../sentry/transport/AsyncHttpTransport.java | 4 +- .../main/java/io/sentry/util/HintUtils.java | 4 +- .../test/java/io/sentry/JsonSerializerTest.kt | 58 ++ sentry/src/test/java/io/sentry/ScopeTest.kt | 189 ++++-- .../test/java/io/sentry/SentryClientTest.kt | 55 +- .../test/java/io/sentry/SentryOptionsTest.kt | 10 + sentry/src/test/java/io/sentry/SentryTest.kt | 114 +++- .../java/io/sentry/cache/CacheUtilsTest.kt | 87 +++ .../java/io/sentry/cache/EnvelopeCacheTest.kt | 11 +- .../cache/PersistingOptionsObserverTest.kt | 143 +++++ .../cache/PersistingScopeObserverTest.kt | 284 +++++++++ .../sentry/clientreport/ClientReportTest.kt | 10 +- .../AsyncHttpTransportClientReportTest.kt | 12 +- .../test/java/io/sentry/util/HintUtilsTest.kt | 7 + 65 files changed, 4400 insertions(+), 394 deletions(-) create mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegrationFactory.java create mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java create mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java create mode 100644 sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt create mode 100644 sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt create mode 100644 sentry-test-support/src/main/kotlin/io/sentry/test/Mocks.kt create mode 100644 sentry/src/main/java/io/sentry/BackfillingEventProcessor.java create mode 100644 sentry/src/main/java/io/sentry/IOptionsObserver.java create mode 100644 sentry/src/main/java/io/sentry/cache/CacheUtils.java create mode 100644 sentry/src/main/java/io/sentry/cache/PersistingOptionsObserver.java create mode 100644 sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java create mode 100644 sentry/src/main/java/io/sentry/hints/Backfillable.java create mode 100644 sentry/src/main/java/io/sentry/hints/BlockingFlushHint.java create mode 100644 sentry/src/test/java/io/sentry/cache/CacheUtilsTest.kt create mode 100644 sentry/src/test/java/io/sentry/cache/PersistingOptionsObserverTest.kt create mode 100644 sentry/src/test/java/io/sentry/cache/PersistingScopeObserverTest.kt diff --git a/sentry-android-core/.gitignore b/sentry-android-core/.gitignore index b91faf9e76..4e4a850448 100644 --- a/sentry-android-core/.gitignore +++ b/sentry-android-core/.gitignore @@ -1 +1,4 @@ INSTALLATION +last_crash +.options-cache/ +.scope-cache/ diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 8aa2350d0d..9a8c074679 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -55,6 +55,28 @@ public final class io/sentry/android/core/AnrIntegration : io/sentry/Integration public final fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V } +public final class io/sentry/android/core/AnrIntegrationFactory { + public fun ()V + public static fun create (Landroid/content/Context;Lio/sentry/android/core/BuildInfoProvider;)Lio/sentry/Integration; +} + +public final class io/sentry/android/core/AnrV2EventProcessor : io/sentry/BackfillingEventProcessor { + public fun (Landroid/content/Context;Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/BuildInfoProvider;)V + public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; +} + +public class io/sentry/android/core/AnrV2Integration : io/sentry/Integration, java/io/Closeable { + public fun (Landroid/content/Context;)V + public fun close ()V + public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V +} + +public final class io/sentry/android/core/AnrV2Integration$AnrV2Hint : io/sentry/hints/BlockingFlushHint, io/sentry/hints/Backfillable { + public fun (JLio/sentry/ILogger;JZ)V + public fun shouldEnrich ()Z + public fun timestamp ()J +} + public final class io/sentry/android/core/AppComponentsBreadcrumbsIntegration : android/content/ComponentCallbacks2, io/sentry/Integration, java/io/Closeable { public fun (Landroid/content/Context;)V public fun close ()V @@ -271,9 +293,11 @@ public final class io/sentry/android/core/ViewHierarchyEventProcessor : io/sentr } public final class io/sentry/android/core/cache/AndroidEnvelopeCache : io/sentry/cache/EnvelopeCache { + public static final field LAST_ANR_REPORT Ljava/lang/String; public fun (Lio/sentry/android/core/SentryAndroidOptions;)V public fun getDirectory ()Ljava/io/File; public static fun hasStartupCrashMarker (Lio/sentry/SentryOptions;)Z + public static fun lastReportedAnr (Lio/sentry/SentryOptions;)J public fun store (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index 16465c8b78..c3a1578bc4 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -19,6 +19,8 @@ import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.android.fragment.FragmentLifecycleIntegration; import io.sentry.android.timber.SentryTimberIntegration; +import io.sentry.cache.PersistingOptionsObserver; +import io.sentry.cache.PersistingScopeObserver; import io.sentry.compose.gestures.ComposeGestureTargetLocator; import io.sentry.internal.gestures.GestureTargetLocator; import io.sentry.transport.NoOpEnvelopeCache; @@ -139,6 +141,7 @@ static void initializeIntegrationsAndProcessors( options.addEventProcessor(new PerformanceAndroidEventProcessor(options, activityFramesTracker)); options.addEventProcessor(new ScreenshotEventProcessor(options, buildInfoProvider)); options.addEventProcessor(new ViewHierarchyEventProcessor(options)); + options.addEventProcessor(new AnrV2EventProcessor(context, options, buildInfoProvider)); options.setTransportGate(new AndroidTransportGate(context, options.getLogger())); final SentryFrameMetricsCollector frameMetricsCollector = new SentryFrameMetricsCollector(context, options, buildInfoProvider); @@ -170,6 +173,11 @@ static void initializeIntegrationsAndProcessors( options.addCollector(new AndroidCpuCollector(options.getLogger(), buildInfoProvider)); } options.setTransactionPerformanceCollector(new DefaultTransactionPerformanceCollector(options)); + + if (options.getCacheDirPath() != null) { + options.addScopeObserver(new PersistingScopeObserver(options)); + options.addOptionsObserver(new PersistingOptionsObserver(options)); + } } private static void installDefaultIntegrations( @@ -213,7 +221,7 @@ private static void installDefaultIntegrations( // AppLifecycleIntegration has to be installed before AnrIntegration, because AnrIntegration // relies on AppState set by it options.addIntegration(new AppLifecycleIntegration()); - options.addIntegration(new AnrIntegration(context)); + options.addIntegration(AnrIntegrationFactory.create(context, buildInfoProvider)); // registerActivityLifecycleCallbacks is only available if Context is an AppContext if (context instanceof Application) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegrationFactory.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegrationFactory.java new file mode 100644 index 0000000000..cff8f72cd3 --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegrationFactory.java @@ -0,0 +1,21 @@ +package io.sentry.android.core; + +import android.content.Context; +import android.os.Build; +import io.sentry.Integration; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public final class AnrIntegrationFactory { + + @NotNull + public static Integration create( + final @NotNull Context context, final @NotNull BuildInfoProvider buildInfoProvider) { + if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.R) { + return new AnrV2Integration(context); + } else { + return new AnrIntegration(context); + } + } +} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java new file mode 100644 index 0000000000..80d8faaf9d --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java @@ -0,0 +1,563 @@ +package io.sentry.android.core; + +import static io.sentry.cache.PersistingOptionsObserver.DIST_FILENAME; +import static io.sentry.cache.PersistingOptionsObserver.ENVIRONMENT_FILENAME; +import static io.sentry.cache.PersistingOptionsObserver.PROGUARD_UUID_FILENAME; +import static io.sentry.cache.PersistingOptionsObserver.RELEASE_FILENAME; +import static io.sentry.cache.PersistingOptionsObserver.SDK_VERSION_FILENAME; +import static io.sentry.cache.PersistingScopeObserver.BREADCRUMBS_FILENAME; +import static io.sentry.cache.PersistingScopeObserver.CONTEXTS_FILENAME; +import static io.sentry.cache.PersistingScopeObserver.EXTRAS_FILENAME; +import static io.sentry.cache.PersistingScopeObserver.FINGERPRINT_FILENAME; +import static io.sentry.cache.PersistingScopeObserver.LEVEL_FILENAME; +import static io.sentry.cache.PersistingScopeObserver.REQUEST_FILENAME; +import static io.sentry.cache.PersistingScopeObserver.TRACE_FILENAME; +import static io.sentry.cache.PersistingScopeObserver.TRANSACTION_FILENAME; +import static io.sentry.cache.PersistingScopeObserver.USER_FILENAME; + +import android.annotation.SuppressLint; +import android.app.ActivityManager; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.os.Build; +import android.util.DisplayMetrics; +import androidx.annotation.WorkerThread; +import io.sentry.BackfillingEventProcessor; +import io.sentry.Breadcrumb; +import io.sentry.Hint; +import io.sentry.IpAddressUtils; +import io.sentry.SentryBaseEvent; +import io.sentry.SentryEvent; +import io.sentry.SentryExceptionFactory; +import io.sentry.SentryLevel; +import io.sentry.SentryOptions; +import io.sentry.SentryStackTraceFactory; +import io.sentry.SpanContext; +import io.sentry.cache.PersistingOptionsObserver; +import io.sentry.cache.PersistingScopeObserver; +import io.sentry.hints.Backfillable; +import io.sentry.protocol.App; +import io.sentry.protocol.Contexts; +import io.sentry.protocol.DebugImage; +import io.sentry.protocol.DebugMeta; +import io.sentry.protocol.Device; +import io.sentry.protocol.OperatingSystem; +import io.sentry.protocol.Request; +import io.sentry.protocol.SdkVersion; +import io.sentry.protocol.User; +import io.sentry.util.HintUtils; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * AnrV2Integration processes events on a background thread, hence the event processors will also be + * invoked on the same background thread, so we can safely read data from disk synchronously. + */ +@ApiStatus.Internal +@WorkerThread +public final class AnrV2EventProcessor implements BackfillingEventProcessor { + + /** + * Default value for {@link SentryEvent#getEnvironment()} set when both event and {@link + * SentryOptions} do not have the environment field set. + */ + static final String DEFAULT_ENVIRONMENT = "production"; + + private final @NotNull Context context; + + private final @NotNull SentryAndroidOptions options; + + private final @NotNull BuildInfoProvider buildInfoProvider; + + private final @NotNull SentryExceptionFactory sentryExceptionFactory; + + public AnrV2EventProcessor( + final @NotNull Context context, + final @NotNull SentryAndroidOptions options, + final @NotNull BuildInfoProvider buildInfoProvider) { + this.context = context; + this.options = options; + this.buildInfoProvider = buildInfoProvider; + + final SentryStackTraceFactory sentryStackTraceFactory = + new SentryStackTraceFactory( + this.options.getInAppExcludes(), this.options.getInAppIncludes()); + + sentryExceptionFactory = new SentryExceptionFactory(sentryStackTraceFactory); + } + + @Override + public @Nullable SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) { + final Object unwrappedHint = HintUtils.getSentrySdkHint(hint); + if (!(unwrappedHint instanceof Backfillable)) { + options + .getLogger() + .log( + SentryLevel.WARNING, + "The event is not Backfillable, but has been passed to BackfillingEventProcessor, skipping."); + return event; + } + + // we always set exception values, platform, os and device even if the ANR is not enrich-able + // even though the OS context may change in the meantime (OS update), we consider this an + // edge-case + setExceptions(event); + setPlatform(event); + mergeOS(event); + setDevice(event); + + if (!((Backfillable) unwrappedHint).shouldEnrich()) { + options + .getLogger() + .log( + SentryLevel.DEBUG, + "The event is Backfillable, but should not be enriched, skipping."); + return event; + } + + backfillScope(event); + + backfillOptions(event); + + setStaticValues(event); + + return event; + } + + // region scope persisted values + private void backfillScope(final @NotNull SentryEvent event) { + setRequest(event); + setUser(event); + setScopeTags(event); + setBreadcrumbs(event); + setExtras(event); + setContexts(event); + setTransaction(event); + setFingerprints(event); + setLevel(event); + setTrace(event); + } + + private void setTrace(final @NotNull SentryEvent event) { + final SpanContext spanContext = + PersistingScopeObserver.read(options, TRACE_FILENAME, SpanContext.class); + if (event.getContexts().getTrace() == null && spanContext != null) { + event.getContexts().setTrace(spanContext); + } + } + + private void setLevel(final @NotNull SentryEvent event) { + final SentryLevel level = + PersistingScopeObserver.read(options, LEVEL_FILENAME, SentryLevel.class); + if (event.getLevel() == null) { + event.setLevel(level); + } + } + + @SuppressWarnings("unchecked") + private void setFingerprints(final @NotNull SentryEvent event) { + final List fingerprint = + (List) PersistingScopeObserver.read(options, FINGERPRINT_FILENAME, List.class); + if (event.getFingerprints() == null) { + event.setFingerprints(fingerprint); + } + } + + private void setTransaction(final @NotNull SentryEvent event) { + final String transaction = + PersistingScopeObserver.read(options, TRANSACTION_FILENAME, String.class); + if (event.getTransaction() == null) { + event.setTransaction(transaction); + } + } + + private void setContexts(final @NotNull SentryBaseEvent event) { + final Contexts persistedContexts = + PersistingScopeObserver.read(options, CONTEXTS_FILENAME, Contexts.class); + if (persistedContexts == null) { + return; + } + final Contexts eventContexts = event.getContexts(); + for (Map.Entry entry : new Contexts(persistedContexts).entrySet()) { + if (!eventContexts.containsKey(entry.getKey())) { + eventContexts.put(entry.getKey(), entry.getValue()); + } + } + } + + @SuppressWarnings("unchecked") + private void setExtras(final @NotNull SentryBaseEvent event) { + final Map extras = + (Map) PersistingScopeObserver.read(options, EXTRAS_FILENAME, Map.class); + if (extras == null) { + return; + } + if (event.getExtras() == null) { + event.setExtras(new HashMap<>(extras)); + } else { + for (Map.Entry item : extras.entrySet()) { + if (!event.getExtras().containsKey(item.getKey())) { + event.getExtras().put(item.getKey(), item.getValue()); + } + } + } + } + + @SuppressWarnings("unchecked") + private void setBreadcrumbs(final @NotNull SentryBaseEvent event) { + final List breadcrumbs = + (List) + PersistingScopeObserver.read( + options, BREADCRUMBS_FILENAME, List.class, new Breadcrumb.Deserializer()); + if (breadcrumbs == null) { + return; + } + if (event.getBreadcrumbs() == null) { + event.setBreadcrumbs(new ArrayList<>(breadcrumbs)); + } else { + event.getBreadcrumbs().addAll(breadcrumbs); + } + } + + @SuppressWarnings("unchecked") + private void setScopeTags(final @NotNull SentryBaseEvent event) { + final Map tags = + (Map) + PersistingScopeObserver.read(options, PersistingScopeObserver.TAGS_FILENAME, Map.class); + if (tags == null) { + return; + } + if (event.getTags() == null) { + event.setTags(new HashMap<>(tags)); + } else { + for (Map.Entry item : tags.entrySet()) { + if (!event.getTags().containsKey(item.getKey())) { + event.setTag(item.getKey(), item.getValue()); + } + } + } + } + + private void setUser(final @NotNull SentryBaseEvent event) { + if (event.getUser() == null) { + final User user = PersistingScopeObserver.read(options, USER_FILENAME, User.class); + event.setUser(user); + } + } + + private void setRequest(final @NotNull SentryBaseEvent event) { + if (event.getRequest() == null) { + final Request request = + PersistingScopeObserver.read(options, REQUEST_FILENAME, Request.class); + event.setRequest(request); + } + } + + // endregion + + // region options persisted values + private void backfillOptions(final @NotNull SentryEvent event) { + setRelease(event); + setEnvironment(event); + setDist(event); + setDebugMeta(event); + setSdk(event); + setApp(event); + setOptionsTags(event); + } + + private void setApp(final @NotNull SentryBaseEvent event) { + App app = event.getContexts().getApp(); + if (app == null) { + app = new App(); + } + app.setAppName(ContextUtils.getApplicationName(context, options.getLogger())); + + final PackageInfo packageInfo = + ContextUtils.getPackageInfo(context, options.getLogger(), buildInfoProvider); + if (packageInfo != null) { + 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.ERROR, "Failed to parse release from scope cache: %s", release); + } + } + + event.getContexts().setApp(app); + } + + private void setRelease(final @NotNull SentryBaseEvent event) { + if (event.getRelease() == null) { + final String release = + PersistingOptionsObserver.read(options, RELEASE_FILENAME, String.class); + event.setRelease(release); + } + } + + private void setEnvironment(final @NotNull SentryBaseEvent event) { + if (event.getEnvironment() == null) { + final String environment = + PersistingOptionsObserver.read(options, ENVIRONMENT_FILENAME, String.class); + event.setEnvironment(environment != null ? environment : DEFAULT_ENVIRONMENT); + } + } + + private void setDebugMeta(final @NotNull SentryBaseEvent event) { + DebugMeta debugMeta = event.getDebugMeta(); + + if (debugMeta == null) { + debugMeta = new DebugMeta(); + } + if (debugMeta.getImages() == null) { + debugMeta.setImages(new ArrayList<>()); + } + List images = debugMeta.getImages(); + if (images != null) { + final String proguardUuid = + PersistingOptionsObserver.read(options, PROGUARD_UUID_FILENAME, String.class); + + final DebugImage debugImage = new DebugImage(); + debugImage.setType(DebugImage.PROGUARD); + debugImage.setUuid(proguardUuid); + images.add(debugImage); + event.setDebugMeta(debugMeta); + } + } + + private void setDist(final @NotNull SentryBaseEvent event) { + if (event.getDist() == null) { + final String dist = PersistingOptionsObserver.read(options, DIST_FILENAME, String.class); + event.setDist(dist); + } + // if there's no user-set dist, fall back to versionCode from the persisted release string + if (event.getDist() == null) { + final String release = + PersistingOptionsObserver.read(options, RELEASE_FILENAME, String.class); + if (release != null) { + try { + final String versionCode = release.substring(release.indexOf('+') + 1); + event.setDist(versionCode); + } catch (Throwable e) { + options + .getLogger() + .log(SentryLevel.ERROR, "Failed to parse release from scope cache: %s", release); + } + } + } + } + + private void setSdk(final @NotNull SentryBaseEvent event) { + if (event.getSdk() == null) { + final SdkVersion sdkVersion = + PersistingOptionsObserver.read(options, SDK_VERSION_FILENAME, SdkVersion.class); + event.setSdk(sdkVersion); + } + } + + @SuppressWarnings("unchecked") + private void setOptionsTags(final @NotNull SentryBaseEvent event) { + final Map tags = + (Map) + PersistingOptionsObserver.read( + options, PersistingOptionsObserver.TAGS_FILENAME, Map.class); + if (tags == null) { + return; + } + if (event.getTags() == null) { + event.setTags(new HashMap<>(tags)); + } else { + for (Map.Entry item : tags.entrySet()) { + if (!event.getTags().containsKey(item.getKey())) { + event.setTag(item.getKey(), item.getValue()); + } + } + } + } + // endregion + + // region static values + private void setStaticValues(final @NotNull SentryEvent event) { + mergeUser(event); + setSideLoadedInfo(event); + } + + private void setPlatform(final @NotNull SentryBaseEvent event) { + if (event.getPlatform() == null) { + // this actually means JVM related. + event.setPlatform(SentryBaseEvent.DEFAULT_PLATFORM); + } + } + + private void setExceptions(final @NotNull SentryEvent event) { + final Throwable throwable = event.getThrowableMechanism(); + if (throwable != null) { + event.setExceptions(sentryExceptionFactory.getSentryExceptions(throwable)); + } + } + + private void mergeUser(final @NotNull SentryBaseEvent event) { + if (options.isSendDefaultPii()) { + if (event.getUser() == null) { + final User user = new User(); + user.setIpAddress(IpAddressUtils.DEFAULT_IP_ADDRESS); + event.setUser(user); + } else if (event.getUser().getIpAddress() == null) { + event.getUser().setIpAddress(IpAddressUtils.DEFAULT_IP_ADDRESS); + } + } + + // userId should be set even if event is Cached as the userId is static and won't change anyway. + final User user = event.getUser(); + if (user == null) { + event.setUser(getDefaultUser()); + } else if (user.getId() == null) { + user.setId(getDeviceId()); + } + } + + /** + * Sets the default user which contains only the userId. + * + * @return the User object + */ + private @NotNull User getDefaultUser() { + User user = new User(); + user.setId(getDeviceId()); + + return user; + } + + private @Nullable String getDeviceId() { + try { + return Installation.id(context); + } catch (Throwable e) { + options.getLogger().log(SentryLevel.ERROR, "Error getting installationId.", e); + } + return null; + } + + private void setSideLoadedInfo(final @NotNull SentryBaseEvent event) { + try { + final Map sideLoadedInfo = + ContextUtils.getSideLoadedInfo(context, options.getLogger(), buildInfoProvider); + + if (sideLoadedInfo != null) { + for (final Map.Entry entry : sideLoadedInfo.entrySet()) { + event.setTag(entry.getKey(), entry.getValue()); + } + } + } catch (Throwable e) { + options.getLogger().log(SentryLevel.ERROR, "Error getting side loaded info.", e); + } + } + + private void setDevice(final @NotNull SentryBaseEvent event) { + if (event.getContexts().getDevice() == null) { + event.getContexts().setDevice(getDevice()); + } + } + + // only use static data that does not change between app launches (e.g. timezone, boottime, + // battery level will change) + @SuppressLint("NewApi") + private @NotNull Device getDevice() { + Device device = new Device(); + if (options.isSendDefaultPii()) { + device.setName(ContextUtils.getDeviceName(context, buildInfoProvider)); + } + device.setManufacturer(Build.MANUFACTURER); + device.setBrand(Build.BRAND); + device.setFamily(ContextUtils.getFamily(options.getLogger())); + device.setModel(Build.MODEL); + device.setModelId(Build.ID); + device.setArchs(ContextUtils.getArchitectures(buildInfoProvider)); + + final ActivityManager.MemoryInfo memInfo = + ContextUtils.getMemInfo(context, options.getLogger()); + if (memInfo != null) { + // in bytes + device.setMemorySize(getMemorySize(memInfo)); + } + + device.setSimulator(buildInfoProvider.isEmulator()); + + DisplayMetrics displayMetrics = ContextUtils.getDisplayMetrics(context, options.getLogger()); + if (displayMetrics != null) { + device.setScreenWidthPixels(displayMetrics.widthPixels); + device.setScreenHeightPixels(displayMetrics.heightPixels); + device.setScreenDensity(displayMetrics.density); + device.setScreenDpi(displayMetrics.densityDpi); + } + + if (device.getId() == null) { + device.setId(getDeviceId()); + } + + return device; + } + + @SuppressLint("NewApi") + private @NotNull Long getMemorySize(final @NotNull ActivityManager.MemoryInfo memInfo) { + if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN) { + return memInfo.totalMem; + } + // using Runtime as a fallback + return java.lang.Runtime.getRuntime().totalMemory(); // JVM in bytes too + } + + private void mergeOS(final @NotNull SentryBaseEvent event) { + final OperatingSystem currentOS = event.getContexts().getOperatingSystem(); + final OperatingSystem androidOS = getOperatingSystem(); + + // make Android OS the main OS using the 'os' key + event.getContexts().setOperatingSystem(androidOS); + + if (currentOS != null) { + // add additional OS which was already part of the SentryEvent (eg Linux read from NDK) + String osNameKey = currentOS.getName(); + if (osNameKey != null && !osNameKey.isEmpty()) { + osNameKey = "os_" + osNameKey.trim().toLowerCase(Locale.ROOT); + } else { + osNameKey = "os_1"; + } + event.getContexts().put(osNameKey, currentOS); + } + } + + private @NotNull OperatingSystem getOperatingSystem() { + OperatingSystem os = new OperatingSystem(); + os.setName("Android"); + os.setVersion(Build.VERSION.RELEASE); + os.setBuild(Build.DISPLAY); + + try { + os.setKernelVersion(ContextUtils.getKernelVersion(options.getLogger())); + } catch (Throwable e) { + options.getLogger().log(SentryLevel.ERROR, "Error getting OperatingSystem.", e); + } + + return os; + } + // endregion +} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java new file mode 100644 index 0000000000..e62f4088b3 --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java @@ -0,0 +1,276 @@ +package io.sentry.android.core; + +import android.annotation.SuppressLint; +import android.app.ActivityManager; +import android.app.ApplicationExitInfo; +import android.content.Context; +import android.os.Looper; +import io.sentry.DateUtils; +import io.sentry.Hint; +import io.sentry.IHub; +import io.sentry.ILogger; +import io.sentry.Integration; +import io.sentry.SentryEvent; +import io.sentry.SentryLevel; +import io.sentry.SentryOptions; +import io.sentry.android.core.cache.AndroidEnvelopeCache; +import io.sentry.exception.ExceptionMechanismException; +import io.sentry.hints.Backfillable; +import io.sentry.hints.BlockingFlushHint; +import io.sentry.protocol.Mechanism; +import io.sentry.protocol.SentryId; +import io.sentry.transport.CurrentDateProvider; +import io.sentry.transport.ICurrentDateProvider; +import io.sentry.util.HintUtils; +import io.sentry.util.Objects; +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@SuppressLint("NewApi") // we check this in AnrIntegrationFactory +public class AnrV2Integration implements Integration, Closeable { + + // using 91 to avoid timezone change hassle, 90 days is how long Sentry keeps the events + static final long NINETY_DAYS_THRESHOLD = TimeUnit.DAYS.toMillis(91); + + private final @NotNull Context context; + private final @NotNull ICurrentDateProvider dateProvider; + private @Nullable SentryAndroidOptions options; + + public AnrV2Integration(final @NotNull Context context) { + // using CurrentDateProvider instead of AndroidCurrentDateProvider as AppExitInfo uses + // System.currentTimeMillis + this(context, CurrentDateProvider.getInstance()); + } + + AnrV2Integration( + final @NotNull Context context, final @NotNull ICurrentDateProvider dateProvider) { + this.context = context; + this.dateProvider = dateProvider; + } + + @SuppressLint("NewApi") // we do the check in the AnrIntegrationFactory + @Override + public void register(@NotNull IHub hub, @NotNull SentryOptions options) { + this.options = + Objects.requireNonNull( + (options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null, + "SentryAndroidOptions is required"); + + this.options + .getLogger() + .log(SentryLevel.DEBUG, "AnrIntegration enabled: %s", this.options.isAnrEnabled()); + + if (this.options.getCacheDirPath() == null) { + this.options + .getLogger() + .log(SentryLevel.INFO, "Cache dir is not set, unable to process ANRs"); + return; + } + + if (this.options.isAnrEnabled()) { + final ActivityManager activityManager = + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + + List applicationExitInfoList = + activityManager.getHistoricalProcessExitReasons(null, 0, 0); + + if (applicationExitInfoList.size() != 0) { + options + .getExecutorService() + .submit( + new AnrProcessor( + new ArrayList<>( + applicationExitInfoList), // just making a deep copy to be safe, as we're + // modifying the list + hub, + this.options, + dateProvider)); + } else { + options.getLogger().log(SentryLevel.DEBUG, "No records in historical exit reasons."); + } + options.getLogger().log(SentryLevel.DEBUG, "AnrV2Integration installed."); + addIntegrationToSdkVersion(); + } + } + + @Override + public void close() throws IOException { + if (options != null) { + options.getLogger().log(SentryLevel.DEBUG, "AnrV2Integration removed."); + } + } + + static class AnrProcessor implements Runnable { + + final @NotNull List exitInfos; + private final @NotNull IHub hub; + private final @NotNull SentryAndroidOptions options; + private final long threshold; + + AnrProcessor( + final @NotNull List exitInfos, + final @NotNull IHub hub, + final @NotNull SentryAndroidOptions options, + final @NotNull ICurrentDateProvider dateProvider) { + this.exitInfos = exitInfos; + this.hub = hub; + this.options = options; + this.threshold = dateProvider.getCurrentTimeMillis() - NINETY_DAYS_THRESHOLD; + } + + @SuppressLint("NewApi") // we check this in AnrIntegrationFactory + @Override + public void run() { + final long lastReportedAnrTimestamp = AndroidEnvelopeCache.lastReportedAnr(options); + + // search for the latest ANR to report it separately as we're gonna enrich it. The latest + // ANR will be first in the list, as it's filled last-to-first in order of appearance + ApplicationExitInfo latestAnr = null; + for (ApplicationExitInfo applicationExitInfo : exitInfos) { + if (applicationExitInfo.getReason() == ApplicationExitInfo.REASON_ANR) { + latestAnr = applicationExitInfo; + // remove it, so it's not reported twice + exitInfos.remove(applicationExitInfo); + break; + } + } + + if (latestAnr == null) { + options + .getLogger() + .log(SentryLevel.DEBUG, "No ANRs have been found in the historical exit reasons list."); + return; + } + + if (latestAnr.getTimestamp() < threshold) { + options + .getLogger() + .log(SentryLevel.DEBUG, "Latest ANR happened too long ago, returning early."); + return; + } + + if (latestAnr.getTimestamp() <= lastReportedAnrTimestamp) { + options + .getLogger() + .log(SentryLevel.DEBUG, "Latest ANR has already been reported, returning early."); + return; + } + + // report the remainder without enriching + reportNonEnrichedHistoricalAnrs(exitInfos, lastReportedAnrTimestamp); + + // report the latest ANR with enriching, if contexts are available, otherwise report it + // non-enriched + reportAsSentryEvent(latestAnr, true); + } + + private void reportNonEnrichedHistoricalAnrs( + final @NotNull List exitInfos, final long lastReportedAnr) { + // we reverse the list, because the OS puts errors in order of appearance, last-to-first + // and we want to write a marker file after each ANR has been processed, so in case the app + // gets killed meanwhile, we can proceed from the last reported ANR and not process the entire + // list again + Collections.reverse(exitInfos); + for (ApplicationExitInfo applicationExitInfo : exitInfos) { + if (applicationExitInfo.getReason() == ApplicationExitInfo.REASON_ANR) { + if (applicationExitInfo.getTimestamp() < threshold) { + options + .getLogger() + .log(SentryLevel.DEBUG, "ANR happened too long ago %s.", applicationExitInfo); + continue; + } + + if (applicationExitInfo.getTimestamp() <= lastReportedAnr) { + options + .getLogger() + .log(SentryLevel.DEBUG, "ANR has already been reported %s.", applicationExitInfo); + continue; + } + + reportAsSentryEvent(applicationExitInfo, false); // do not enrich past events + } + } + } + + private void reportAsSentryEvent( + final @NotNull ApplicationExitInfo exitInfo, final boolean shouldEnrich) { + final long anrTimestamp = exitInfo.getTimestamp(); + final Throwable anrThrowable = buildAnrThrowable(exitInfo); + final AnrV2Hint anrHint = + new AnrV2Hint( + options.getFlushTimeoutMillis(), options.getLogger(), anrTimestamp, shouldEnrich); + + final Hint hint = HintUtils.createWithTypeCheckHint(anrHint); + + final SentryEvent event = new SentryEvent(anrThrowable); + event.setTimestamp(DateUtils.getDateTime(anrTimestamp)); + event.setLevel(SentryLevel.FATAL); + + final @NotNull SentryId sentryId = hub.captureEvent(event, hint); + final boolean isEventDropped = sentryId.equals(SentryId.EMPTY_ID); + if (!isEventDropped) { + // Block until the event is flushed to disk and the last_reported_anr marker is updated + if (!anrHint.waitFlush()) { + options + .getLogger() + .log( + SentryLevel.WARNING, + "Timed out waiting to flush ANR event to disk. Event: %s", + event.getEventId()); + } + } + } + + private @NotNull Throwable buildAnrThrowable(final @NotNull ApplicationExitInfo exitInfo) { + final boolean isBackground = + exitInfo.getImportance() != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; + + String message = "ANR"; + if (isBackground) { + message = "Background " + message; + } + + // TODO: here we should actually parse the trace file and extract the thread dump from there + // and then we could properly get the main thread stracktrace and construct a proper exception + final ApplicationNotResponding error = + new ApplicationNotResponding(message, Looper.getMainLooper().getThread()); + final Mechanism mechanism = new Mechanism(); + mechanism.setType("ANRv2"); + return new ExceptionMechanismException(mechanism, error, error.getThread(), true); + } + } + + @ApiStatus.Internal + public static final class AnrV2Hint extends BlockingFlushHint implements Backfillable { + + private final long timestamp; + + private final boolean shouldEnrich; + + public AnrV2Hint( + final long flushTimeoutMillis, + final @NotNull ILogger logger, + final long timestamp, + final boolean shouldEnrich) { + super(flushTimeoutMillis, logger); + this.timestamp = timestamp; + this.shouldEnrich = shouldEnrich; + } + + public long timestamp() { + return timestamp; + } + + @Override + public boolean shouldEnrich() { + return shouldEnrich; + } + } +} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java b/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java index d75b384165..d43dde9471 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java @@ -1,6 +1,7 @@ package io.sentry.android.core; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; +import static android.content.Context.ACTIVITY_SERVICE; import android.annotation.SuppressLint; import android.app.ActivityManager; @@ -10,9 +11,17 @@ import android.content.pm.PackageManager; import android.os.Build; import android.os.Process; +import android.provider.Settings; +import android.util.DisplayMetrics; import io.sentry.ILogger; import io.sentry.SentryLevel; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -151,4 +160,168 @@ static boolean isForegroundImportance(final @NotNull Context context) { } return false; } + + /** + * Get the device's current kernel version, as a string. Attempts to read /proc/version, and falls + * back to the 'os.version' System Property. + * + * @return the device's current kernel version, as a string + */ + @SuppressWarnings("DefaultCharset") + static @Nullable String getKernelVersion(final @NotNull ILogger logger) { + // its possible to try to execute 'uname' and parse it or also another unix commands or even + // looking for well known root installed apps + final String errorMsg = "Exception while attempting to read kernel information"; + final String defaultVersion = System.getProperty("os.version"); + + final File file = new File("/proc/version"); + if (!file.canRead()) { + return defaultVersion; + } + try (BufferedReader br = new BufferedReader(new FileReader(file))) { + return br.readLine(); + } catch (IOException e) { + logger.log(SentryLevel.ERROR, errorMsg, e); + } + + return defaultVersion; + } + + @SuppressWarnings("deprecation") + static @Nullable Map getSideLoadedInfo( + final @NotNull Context context, + final @NotNull ILogger logger, + final @NotNull BuildInfoProvider buildInfoProvider) { + String packageName = null; + try { + final PackageInfo packageInfo = getPackageInfo(context, logger, buildInfoProvider); + final PackageManager packageManager = context.getPackageManager(); + + if (packageInfo != null && packageManager != null) { + packageName = packageInfo.packageName; + + // getInstallSourceInfo requires INSTALL_PACKAGES permission which is only given to system + // apps. + final String installerPackageName = packageManager.getInstallerPackageName(packageName); + + final Map sideLoadedInfo = new HashMap<>(); + + if (installerPackageName != null) { + sideLoadedInfo.put("isSideLoaded", "false"); + // could be amazon, google play etc + sideLoadedInfo.put("installerStore", installerPackageName); + } else { + // if it's installed via adb, system apps or untrusted sources + sideLoadedInfo.put("isSideLoaded", "true"); + } + + return sideLoadedInfo; + } + } catch (IllegalArgumentException e) { + // it'll never be thrown as we are querying its own App's package. + logger.log(SentryLevel.DEBUG, "%s package isn't installed.", packageName); + } + + return null; + } + + /** + * Get the human-facing Application name. + * + * @return Application name + */ + static @Nullable String getApplicationName( + final @NotNull Context context, final @NotNull ILogger logger) { + try { + final ApplicationInfo applicationInfo = context.getApplicationInfo(); + final int stringId = applicationInfo.labelRes; + if (stringId == 0) { + if (applicationInfo.nonLocalizedLabel != null) { + return applicationInfo.nonLocalizedLabel.toString(); + } + return context.getPackageManager().getApplicationLabel(applicationInfo).toString(); + } else { + return context.getString(stringId); + } + } catch (Throwable e) { + logger.log(SentryLevel.ERROR, "Error getting application name.", e); + } + + return null; + } + + /** + * Get the DisplayMetrics object for the current application. + * + * @return the DisplayMetrics object for the current application + */ + static @Nullable DisplayMetrics getDisplayMetrics( + final @NotNull Context context, final @NotNull ILogger logger) { + try { + return context.getResources().getDisplayMetrics(); + } catch (Throwable e) { + logger.log(SentryLevel.ERROR, "Error getting DisplayMetrics.", e); + return null; + } + } + + /** + * Fake the device family by using the first word in the Build.MODEL. Works well in most cases... + * "Nexus 6P" -> "Nexus", "Galaxy S7" -> "Galaxy". + * + * @return family name of the device, as best we can tell + */ + static @Nullable String getFamily(final @NotNull ILogger logger) { + try { + return Build.MODEL.split(" ", -1)[0]; + } catch (Throwable e) { + logger.log(SentryLevel.ERROR, "Error getting device family.", e); + return null; + } + } + + @SuppressLint("NewApi") // we're wrapping into if-check with sdk version + static @Nullable String getDeviceName( + final @NotNull Context context, final @NotNull BuildInfoProvider buildInfoProvider) { + if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + return Settings.Global.getString(context.getContentResolver(), "device_name"); + } else { + return null; + } + } + + @SuppressWarnings("deprecation") + @SuppressLint("NewApi") // we're wrapping into if-check with sdk version + static @NotNull String[] getArchitectures(final @NotNull BuildInfoProvider buildInfoProvider) { + final String[] supportedAbis; + if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.LOLLIPOP) { + supportedAbis = Build.SUPPORTED_ABIS; + } else { + supportedAbis = new String[] {Build.CPU_ABI, Build.CPU_ABI2}; + } + return supportedAbis; + } + + /** + * Get MemoryInfo object representing the memory state of the application. + * + * @return MemoryInfo object representing the memory state of the application + */ + static @Nullable ActivityManager.MemoryInfo getMemInfo( + final @NotNull Context context, final @NotNull ILogger logger) { + try { + final ActivityManager actManager = + (ActivityManager) context.getSystemService(ACTIVITY_SERVICE); + final ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo(); + if (actManager != null) { + actManager.getMemoryInfo(memInfo); + return memInfo; + } + logger.log(SentryLevel.INFO, "Error getting MemoryInfo."); + return null; + } catch (Throwable e) { + logger.log(SentryLevel.ERROR, "Error getting MemoryInfo.", e); + return null; + } + } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java index 6a50b66577..e4fc09a88e 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java @@ -1,6 +1,5 @@ package io.sentry.android.core; -import static android.content.Context.ACTIVITY_SERVICE; import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED; import static android.os.BatteryManager.EXTRA_TEMPERATURE; @@ -9,7 +8,6 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.BatteryManager; @@ -18,7 +16,6 @@ import android.os.LocaleList; import android.os.StatFs; import android.os.SystemClock; -import android.provider.Settings; import android.util.DisplayMetrics; import io.sentry.DateUtils; import io.sentry.EventProcessor; @@ -38,10 +35,7 @@ import io.sentry.protocol.User; import io.sentry.util.HintUtils; import io.sentry.util.Objects; -import java.io.BufferedReader; import java.io.File; -import java.io.FileReader; -import java.io.IOException; import java.util.Calendar; import java.util.Date; import java.util.HashMap; @@ -105,7 +99,7 @@ public DefaultAndroidEventProcessor( map.put(ROOTED, rootChecker.isDeviceRooted()); - String kernelVersion = getKernelVersion(); + final String kernelVersion = ContextUtils.getKernelVersion(options.getLogger()); if (kernelVersion != null) { map.put(KERNEL_VERSION, kernelVersion); } @@ -113,7 +107,8 @@ public DefaultAndroidEventProcessor( // its not IO, but it has been cached in the old version as well map.put(EMULATOR, buildInfoProvider.isEmulator()); - final Map sideLoadedInfo = getSideLoadedInfo(); + final Map sideLoadedInfo = + ContextUtils.getSideLoadedInfo(context, options.getLogger(), buildInfoProvider); if (sideLoadedInfo != null) { map.put(SIDE_LOADED, sideLoadedInfo); } @@ -253,7 +248,7 @@ private void setDist(final @NotNull SentryBaseEvent event, final @NotNull String } private void setAppExtras(final @NotNull App app, final @NotNull Hint hint) { - app.setAppName(getApplicationName()); + app.setAppName(ContextUtils.getApplicationName(context, options.getLogger())); app.setAppStartTime(DateUtils.toUtilDate(AppStartState.getInstance().getAppStartTime())); // This should not be set by Hybrid SDKs since they have their own app's lifecycle @@ -267,28 +262,6 @@ private void setAppExtras(final @NotNull App app, final @NotNull Hint hint) { } } - @SuppressWarnings("deprecation") - private @NotNull String getAbi() { - return Build.CPU_ABI; - } - - @SuppressWarnings("deprecation") - private @NotNull String getAbi2() { - return Build.CPU_ABI2; - } - - @SuppressWarnings({"ObsoleteSdkInt", "deprecation", "NewApi"}) - private void setArchitectures(final @NotNull Device device) { - final String[] supportedAbis; - if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.LOLLIPOP) { - supportedAbis = Build.SUPPORTED_ABIS; - } else { - supportedAbis = new String[] {getAbi(), getAbi2()}; - // we were not checking CPU_ABI2, but I've added to the list now - } - device.setArchs(supportedAbis); - } - @SuppressWarnings({"ObsoleteSdkInt", "NewApi"}) private @NotNull Long getMemorySize(final @NotNull ActivityManager.MemoryInfo memInfo) { if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN) { @@ -305,14 +278,14 @@ private void setArchitectures(final @NotNull Device device) { Device device = new Device(); if (options.isSendDefaultPii()) { - device.setName(getDeviceName()); + device.setName(ContextUtils.getDeviceName(context, buildInfoProvider)); } device.setManufacturer(Build.MANUFACTURER); device.setBrand(Build.BRAND); - device.setFamily(getFamily()); + device.setFamily(ContextUtils.getFamily(options.getLogger())); device.setModel(Build.MODEL); device.setModelId(Build.ID); - setArchitectures(device); + device.setArchs(ContextUtils.getArchitectures(buildInfoProvider)); // setting such values require IO hence we don't run for transactions if (errorEvent) { @@ -332,7 +305,7 @@ private void setArchitectures(final @NotNull Device device) { options.getLogger().log(SentryLevel.ERROR, "Error getting emulator.", e); } - DisplayMetrics displayMetrics = getDisplayMetrics(); + DisplayMetrics displayMetrics = ContextUtils.getDisplayMetrics(context, options.getLogger()); if (displayMetrics != null) { device.setScreenWidthPixels(displayMetrics.widthPixels); device.setScreenHeightPixels(displayMetrics.heightPixels); @@ -379,7 +352,8 @@ private void setDeviceIO(final @NotNull Device device, final boolean applyScopeD } device.setOnline(connected); - final ActivityManager.MemoryInfo memInfo = getMemInfo(); + final ActivityManager.MemoryInfo memInfo = + ContextUtils.getMemInfo(context, options.getLogger()); if (memInfo != null) { // in bytes device.setMemorySize(getMemorySize(memInfo)); @@ -414,15 +388,6 @@ private void setDeviceIO(final @NotNull Device device, final boolean applyScopeD } } - @SuppressWarnings({"ObsoleteSdkInt", "NewApi"}) - private @Nullable String getDeviceName() { - if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - return Settings.Global.getString(context.getContentResolver(), "device_name"); - } else { - return null; - } - } - @SuppressWarnings("NewApi") private TimeZone getTimeZone() { if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.N) { @@ -447,46 +412,10 @@ private TimeZone getTimeZone() { return null; } - /** - * Get MemoryInfo object representing the memory state of the application. - * - * @return MemoryInfo object representing the memory state of the application - */ - private @Nullable ActivityManager.MemoryInfo getMemInfo() { - try { - ActivityManager actManager = (ActivityManager) context.getSystemService(ACTIVITY_SERVICE); - ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo(); - if (actManager != null) { - actManager.getMemoryInfo(memInfo); - return memInfo; - } - options.getLogger().log(SentryLevel.INFO, "Error getting MemoryInfo."); - return null; - } catch (Throwable e) { - options.getLogger().log(SentryLevel.ERROR, "Error getting MemoryInfo.", e); - return null; - } - } - private @Nullable Intent getBatteryIntent() { return context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); } - /** - * Fake the device family by using the first word in the Build.MODEL. Works well in most cases... - * "Nexus 6P" -> "Nexus", "Galaxy S7" -> "Galaxy". - * - * @return family name of the device, as best we can tell - */ - private @Nullable String getFamily() { - try { - return Build.MODEL.split(" ", -1)[0]; - } catch (Throwable e) { - options.getLogger().log(SentryLevel.ERROR, "Error getting device family.", e); - return null; - } - } - /** * Get the device's current battery level (as a percentage of total). * @@ -734,20 +663,6 @@ private boolean isExternalStorageMounted() { } } - /** - * Get the DisplayMetrics object for the current application. - * - * @return the DisplayMetrics object for the current application - */ - private @Nullable DisplayMetrics getDisplayMetrics() { - try { - return context.getResources().getDisplayMetrics(); - } catch (Throwable e) { - options.getLogger().log(SentryLevel.ERROR, "Error getting DisplayMetrics.", e); - return null; - } - } - private @NotNull OperatingSystem getOperatingSystem() { OperatingSystem os = new OperatingSystem(); os.setName("Android"); @@ -800,56 +715,6 @@ private void setAppPackageInfo(final @NotNull App app, final @NotNull PackageInf } } - /** - * Get the device's current kernel version, as a string. Attempts to read /proc/version, and falls - * back to the 'os.version' System Property. - * - * @return the device's current kernel version, as a string - */ - @SuppressWarnings("DefaultCharset") - private @Nullable String getKernelVersion() { - // its possible to try to execute 'uname' and parse it or also another unix commands or even - // looking for well known root installed apps - String errorMsg = "Exception while attempting to read kernel information"; - String defaultVersion = System.getProperty("os.version"); - - File file = new File("/proc/version"); - if (!file.canRead()) { - return defaultVersion; - } - try (BufferedReader br = new BufferedReader(new FileReader(file))) { - return br.readLine(); - } catch (IOException e) { - options.getLogger().log(SentryLevel.ERROR, errorMsg, e); - } - - return defaultVersion; - } - - /** - * Get the human-facing Application name. - * - * @return Application name - */ - private @Nullable String getApplicationName() { - try { - ApplicationInfo applicationInfo = context.getApplicationInfo(); - int stringId = applicationInfo.labelRes; - if (stringId == 0) { - if (applicationInfo.nonLocalizedLabel != null) { - return applicationInfo.nonLocalizedLabel.toString(); - } - return context.getPackageManager().getApplicationLabel(applicationInfo).toString(); - } else { - return context.getString(stringId); - } - } catch (Throwable e) { - options.getLogger().log(SentryLevel.ERROR, "Error getting application name.", e); - } - - return null; - } - /** * Sets the default user which contains only the userId. * @@ -871,42 +736,6 @@ private void setAppPackageInfo(final @NotNull App app, final @NotNull PackageInf return null; } - @SuppressWarnings("deprecation") - private @Nullable Map getSideLoadedInfo() { - String packageName = null; - try { - final PackageInfo packageInfo = - ContextUtils.getPackageInfo(context, options.getLogger(), buildInfoProvider); - final PackageManager packageManager = context.getPackageManager(); - - if (packageInfo != null && packageManager != null) { - packageName = packageInfo.packageName; - - // getInstallSourceInfo requires INSTALL_PACKAGES permission which is only given to system - // apps. - final String installerPackageName = packageManager.getInstallerPackageName(packageName); - - final Map sideLoadedInfo = new HashMap<>(); - - if (installerPackageName != null) { - sideLoadedInfo.put("isSideLoaded", "false"); - // could be amazon, google play etc - sideLoadedInfo.put("installerStore", installerPackageName); - } else { - // if it's installed via adb, system apps or untrusted sources - sideLoadedInfo.put("isSideLoaded", "true"); - } - - return sideLoadedInfo; - } - } catch (IllegalArgumentException e) { - // it'll never be thrown as we are querying its own App's package. - options.getLogger().log(SentryLevel.DEBUG, "%s package isn't installed.", packageName); - } - - return null; - } - @SuppressWarnings("unchecked") private void setSideLoadedInfo(final @NotNull SentryBaseEvent event) { try { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java b/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java index 1104adadd4..18f8bd05fa 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java @@ -5,16 +5,21 @@ import io.sentry.Hint; import io.sentry.SentryEnvelope; +import io.sentry.SentryLevel; import io.sentry.SentryOptions; +import io.sentry.UncaughtExceptionHandlerIntegration; +import io.sentry.android.core.AnrV2Integration; import io.sentry.android.core.AppStartState; import io.sentry.android.core.SentryAndroidOptions; import io.sentry.android.core.internal.util.AndroidCurrentDateProvider; import io.sentry.cache.EnvelopeCache; -import io.sentry.hints.DiskFlushNotification; import io.sentry.transport.ICurrentDateProvider; +import io.sentry.util.FileUtils; import io.sentry.util.HintUtils; import io.sentry.util.Objects; import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.TestOnly; @@ -22,6 +27,8 @@ @ApiStatus.Internal public final class AndroidEnvelopeCache extends EnvelopeCache { + public static final String LAST_ANR_REPORT = "last_anr_report"; + private final @NotNull ICurrentDateProvider currentDateProvider; public AndroidEnvelopeCache(final @NotNull SentryAndroidOptions options) { @@ -45,7 +52,8 @@ public void store(@NotNull SentryEnvelope envelope, @NotNull Hint hint) { final SentryAndroidOptions options = (SentryAndroidOptions) this.options; final Long appStartTime = AppStartState.getInstance().getAppStartMillis(); - if (HintUtils.hasType(hint, DiskFlushNotification.class) && appStartTime != null) { + if (HintUtils.hasType(hint, UncaughtExceptionHandlerIntegration.UncaughtExceptionHint.class) + && appStartTime != null) { long timeSinceSdkInit = currentDateProvider.getCurrentTimeMillis() - appStartTime; if (timeSinceSdkInit <= options.getStartupCrashDurationThresholdMillis()) { options @@ -57,6 +65,21 @@ public void store(@NotNull SentryEnvelope envelope, @NotNull Hint hint) { writeStartupCrashMarkerFile(); } } + + HintUtils.runIfHasType( + hint, + AnrV2Integration.AnrV2Hint.class, + (anrHint) -> { + final long timestamp = anrHint.timestamp(); + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Writing last reported ANR marker with timestamp %d", + timestamp); + + writeLastReportedAnrMarker(anrHint.timestamp()); + }); } @TestOnly @@ -74,7 +97,7 @@ private void writeStartupCrashMarkerFile() { .log(DEBUG, "Outbox path is null, the startup crash marker file will not be written"); return; } - final File crashMarkerFile = new File(options.getOutboxPath(), STARTUP_CRASH_MARKER_FILE); + final File crashMarkerFile = new File(outboxPath, STARTUP_CRASH_MARKER_FILE); try { crashMarkerFile.createNewFile(); } catch (Throwable e) { @@ -91,7 +114,7 @@ public static boolean hasStartupCrashMarker(final @NotNull SentryOptions options return false; } - final File crashMarkerFile = new File(options.getOutboxPath(), STARTUP_CRASH_MARKER_FILE); + final File crashMarkerFile = new File(outboxPath, STARTUP_CRASH_MARKER_FILE); try { final boolean exists = crashMarkerFile.exists(); if (exists) { @@ -112,4 +135,43 @@ public static boolean hasStartupCrashMarker(final @NotNull SentryOptions options } return false; } + + public static long lastReportedAnr(final @NotNull SentryOptions options) { + final String cacheDirPath = + Objects.requireNonNull( + options.getCacheDirPath(), "Cache dir path should be set for getting ANRs reported"); + + final File lastAnrMarker = new File(cacheDirPath, LAST_ANR_REPORT); + try { + if (lastAnrMarker.exists() && lastAnrMarker.canRead()) { + final String content = FileUtils.readText(lastAnrMarker); + // we wrapped into try-catch already + //noinspection ConstantConditions + return Long.parseLong(content.trim()); + } else { + options + .getLogger() + .log(DEBUG, "Last ANR marker does not exist. %s.", lastAnrMarker.getAbsolutePath()); + } + } catch (Throwable e) { + options.getLogger().log(ERROR, "Error reading last ANR marker", e); + } + return 0L; + } + + private void writeLastReportedAnrMarker(final long timestamp) { + final String cacheDirPath = options.getCacheDirPath(); + if (cacheDirPath == null) { + options.getLogger().log(DEBUG, "Cache dir path is null, the ANR marker will not be written"); + return; + } + + final File anrMarker = new File(cacheDirPath, LAST_ANR_REPORT); + try (final OutputStream outputStream = new FileOutputStream(anrMarker)) { + outputStream.write(String.valueOf(timestamp).getBytes(UTF_8)); + outputStream.flush(); + } catch (Throwable e) { + options.getLogger().log(ERROR, "Error writing the ANR marker to the disk", e); + } + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index 114646985a..762777ce68 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -14,12 +14,15 @@ import io.sentry.android.core.internal.modules.AssetsModulesLoader import io.sentry.android.core.internal.util.AndroidMainThreadChecker import io.sentry.android.fragment.FragmentLifecycleIntegration import io.sentry.android.timber.SentryTimberIntegration +import io.sentry.cache.PersistingOptionsObserver +import io.sentry.cache.PersistingScopeObserver import io.sentry.compose.gestures.ComposeGestureTargetLocator import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.whenever +import org.robolectric.annotation.Config import java.io.File import kotlin.test.BeforeTest import kotlin.test.Test @@ -141,7 +144,7 @@ class AndroidOptionsInitializerTest { fun `AndroidEventProcessor added to processors list`() { fixture.initSut() val actual = - fixture.sentryOptions.eventProcessors.any { it is DefaultAndroidEventProcessor } + fixture.sentryOptions.eventProcessors.firstOrNull { it is DefaultAndroidEventProcessor } assertNotNull(actual) } @@ -149,7 +152,7 @@ class AndroidOptionsInitializerTest { fun `PerformanceAndroidEventProcessor added to processors list`() { fixture.initSut() val actual = - fixture.sentryOptions.eventProcessors.any { it is PerformanceAndroidEventProcessor } + fixture.sentryOptions.eventProcessors.firstOrNull { it is PerformanceAndroidEventProcessor } assertNotNull(actual) } @@ -164,7 +167,7 @@ class AndroidOptionsInitializerTest { fun `ScreenshotEventProcessor added to processors list`() { fixture.initSut() val actual = - fixture.sentryOptions.eventProcessors.any { it is ScreenshotEventProcessor } + fixture.sentryOptions.eventProcessors.firstOrNull { it is ScreenshotEventProcessor } assertNotNull(actual) } @@ -172,7 +175,15 @@ class AndroidOptionsInitializerTest { fun `ViewHierarchyEventProcessor added to processors list`() { fixture.initSut() val actual = - fixture.sentryOptions.eventProcessors.any { it is ViewHierarchyEventProcessor } + fixture.sentryOptions.eventProcessors.firstOrNull { it is ViewHierarchyEventProcessor } + assertNotNull(actual) + } + + @Test + fun `AnrV2EventProcessor added to processors list`() { + fixture.initSut() + val actual = + fixture.sentryOptions.eventProcessors.firstOrNull { it is AnrV2EventProcessor } assertNotNull(actual) } @@ -534,4 +545,40 @@ class AndroidOptionsInitializerTest { assertIs(fixture.sentryOptions.transactionPerformanceCollector) } + + @Test + fun `PersistingScopeObserver is set to options`() { + fixture.initSut() + + assertTrue { fixture.sentryOptions.scopeObservers.any { it is PersistingScopeObserver } } + } + + @Test + fun `PersistingOptionsObserver is set to options`() { + fixture.initSut() + + assertTrue { fixture.sentryOptions.optionsObservers.any { it is PersistingOptionsObserver } } + } + + @Test + fun `when cacheDir is not set, persisting observers are not set to options`() { + fixture.initSut(configureOptions = { cacheDirPath = null }) + + assertFalse(fixture.sentryOptions.optionsObservers.any { it is PersistingOptionsObserver }) + assertFalse(fixture.sentryOptions.scopeObservers.any { it is PersistingScopeObserver }) + } + + @Config(sdk = [30]) + @Test + fun `AnrV2Integration added to integrations list for API 30 and above`() { + fixture.initSut(useRealContext = true) + + val anrv2Integration = + fixture.sentryOptions.integrations.firstOrNull { it is AnrV2Integration } + assertNotNull(anrv2Integration) + + val anrv1Integration = + fixture.sentryOptions.integrations.firstOrNull { it is AnrIntegration } + assertNull(anrv1Integration) + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt new file mode 100644 index 0000000000..96cbba43fe --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt @@ -0,0 +1,434 @@ +package io.sentry.android.core + +import android.app.ActivityManager +import android.app.ActivityManager.MemoryInfo +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.sentry.Breadcrumb +import io.sentry.Hint +import io.sentry.IpAddressUtils +import io.sentry.NoOpLogger +import io.sentry.SentryBaseEvent +import io.sentry.SentryEvent +import io.sentry.SentryLevel +import io.sentry.SentryLevel.DEBUG +import io.sentry.SpanContext +import io.sentry.cache.PersistingOptionsObserver +import io.sentry.cache.PersistingOptionsObserver.DIST_FILENAME +import io.sentry.cache.PersistingOptionsObserver.ENVIRONMENT_FILENAME +import io.sentry.cache.PersistingOptionsObserver.OPTIONS_CACHE +import io.sentry.cache.PersistingOptionsObserver.PROGUARD_UUID_FILENAME +import io.sentry.cache.PersistingOptionsObserver.RELEASE_FILENAME +import io.sentry.cache.PersistingOptionsObserver.SDK_VERSION_FILENAME +import io.sentry.cache.PersistingScopeObserver.BREADCRUMBS_FILENAME +import io.sentry.cache.PersistingScopeObserver.CONTEXTS_FILENAME +import io.sentry.cache.PersistingScopeObserver.EXTRAS_FILENAME +import io.sentry.cache.PersistingScopeObserver.FINGERPRINT_FILENAME +import io.sentry.cache.PersistingScopeObserver.LEVEL_FILENAME +import io.sentry.cache.PersistingScopeObserver.REQUEST_FILENAME +import io.sentry.cache.PersistingScopeObserver.SCOPE_CACHE +import io.sentry.cache.PersistingScopeObserver.TAGS_FILENAME +import io.sentry.cache.PersistingScopeObserver.TRACE_FILENAME +import io.sentry.cache.PersistingScopeObserver.TRANSACTION_FILENAME +import io.sentry.cache.PersistingScopeObserver.USER_FILENAME +import io.sentry.hints.Backfillable +import io.sentry.protocol.Browser +import io.sentry.protocol.Contexts +import io.sentry.protocol.DebugImage +import io.sentry.protocol.DebugMeta +import io.sentry.protocol.Device +import io.sentry.protocol.OperatingSystem +import io.sentry.protocol.Request +import io.sentry.protocol.Response +import io.sentry.protocol.SdkVersion +import io.sentry.protocol.User +import io.sentry.util.HintUtils +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.robolectric.annotation.Config +import org.robolectric.shadow.api.Shadow +import org.robolectric.shadows.ShadowActivityManager +import org.robolectric.shadows.ShadowBuild +import java.io.File +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertSame + +@RunWith(AndroidJUnit4::class) +class AnrV2EventProcessorTest { + + @get:Rule + val tmpDir = TemporaryFolder() + + class Fixture { + + val buildInfo = mock() + lateinit var context: Context + val options = SentryAndroidOptions().apply { + setLogger(NoOpLogger.getInstance()) + isSendDefaultPii = true + } + + fun getSut( + dir: TemporaryFolder, + currentSdk: Int = 21, + populateScopeCache: Boolean = false, + populateOptionsCache: Boolean = false + ): AnrV2EventProcessor { + options.cacheDirPath = dir.newFolder().absolutePath + whenever(buildInfo.sdkInfoVersion).thenReturn(currentSdk) + whenever(buildInfo.isEmulator).thenReturn(true) + + if (populateScopeCache) { + persistScope(TRACE_FILENAME, SpanContext("ui.load")) + persistScope(USER_FILENAME, User().apply { username = "bot"; id = "bot@me.com" }) + persistScope(TAGS_FILENAME, mapOf("one" to "two")) + persistScope( + BREADCRUMBS_FILENAME, + listOf(Breadcrumb.debug("test"), Breadcrumb.navigation("from", "to")) + ) + persistScope(EXTRAS_FILENAME, mapOf("key" to 123)) + persistScope(TRANSACTION_FILENAME, "TestActivity") + persistScope(FINGERPRINT_FILENAME, listOf("finger", "print")) + persistScope(LEVEL_FILENAME, SentryLevel.INFO) + persistScope( + CONTEXTS_FILENAME, + Contexts().apply { + setResponse(Response().apply { bodySize = 1024 }) + setBrowser(Browser().apply { name = "Google Chrome" }) + } + ) + persistScope( + REQUEST_FILENAME, + Request().apply { url = "google.com"; method = "GET" } + ) + } + + if (populateOptionsCache) { + persistOptions(RELEASE_FILENAME, "io.sentry.samples@1.2.0+232") + persistOptions(PROGUARD_UUID_FILENAME, "uuid") + persistOptions(SDK_VERSION_FILENAME, SdkVersion("sentry.java.android", "6.15.0")) + persistOptions(DIST_FILENAME, "232") + persistOptions(ENVIRONMENT_FILENAME, "debug") + persistOptions(PersistingOptionsObserver.TAGS_FILENAME, mapOf("option" to "tag")) + } + + return AnrV2EventProcessor(context, options, buildInfo) + } + + fun persistScope(filename: String, entity: T) { + val dir = File(options.cacheDirPath, SCOPE_CACHE).also { it.mkdirs() } + val file = File(dir, filename) + options.serializer.serialize(entity, file.writer()) + } + + fun persistOptions(filename: String, entity: T) { + val dir = File(options.cacheDirPath, OPTIONS_CACHE).also { it.mkdirs() } + val file = File(dir, filename) + options.serializer.serialize(entity, file.writer()) + } + + fun mockOutDeviceInfo() { + ShadowBuild.setManufacturer("Google") + ShadowBuild.setBrand("Pixel") + ShadowBuild.setModel("Pixel 3XL") + + val activityManager = + context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val shadowActivityManager = Shadow.extract(activityManager) + shadowActivityManager.setMemoryInfo(MemoryInfo().apply { totalMem = 2048 }) + } + } + + private val fixture = Fixture() + + @BeforeTest + fun `set up`() { + fixture.context = ApplicationProvider.getApplicationContext() + } + + @Test + fun `when event is not backfillable, does not enrich`() { + val processed = processEvent(Hint()) + + assertNull(processed.platform) + assertNull(processed.exceptions) + assertEquals(emptyMap(), processed.contexts) + } + + @Test + fun `when backfillable event is not enrichable, sets platform`() { + val hint = HintUtils.createWithTypeCheckHint(BackfillableHint(shouldEnrich = false)) + + val processed = processEvent(hint) + + assertEquals(SentryBaseEvent.DEFAULT_PLATFORM, processed.platform) + } + + @Test + fun `when backfillable event is not enrichable, sets OS`() { + val hint = HintUtils.createWithTypeCheckHint(BackfillableHint(shouldEnrich = false)) + + ShadowBuild.setVersionRelease("7.8.123") + val processed = processEvent(hint) + + assertEquals("7.8.123", processed.contexts.operatingSystem!!.version) + assertEquals("Android", processed.contexts.operatingSystem!!.name) + } + + @Test + fun `when backfillable event already has OS, sets Android as main OS and existing as secondary`() { + val hint = HintUtils.createWithTypeCheckHint(BackfillableHint(shouldEnrich = false)) + + val linuxOs = OperatingSystem().apply { name = " Linux " } + val processed = processEvent(hint) { + contexts.setOperatingSystem(linuxOs) + } + + assertSame(linuxOs, processed.contexts["os_linux"]) + assertEquals("Android", processed.contexts.operatingSystem!!.name) + } + + @Test + fun `when backfillable event already has OS without name, sets Android as main OS and existing with generated name`() { + val hint = HintUtils.createWithTypeCheckHint(BackfillableHint(shouldEnrich = false)) + + val osNoName = OperatingSystem().apply { version = "1.0" } + val processed = processEvent(hint) { + contexts.setOperatingSystem(osNoName) + } + + assertSame(osNoName, processed.contexts["os_1"]) + assertEquals("Android", processed.contexts.operatingSystem!!.name) + } + + @Test + @Config(qualifiers = "w360dp-h640dp-xxhdpi") + fun `when backfillable event is not enrichable, sets device`() { + val hint = HintUtils.createWithTypeCheckHint(BackfillableHint(shouldEnrich = false)) + + fixture.mockOutDeviceInfo() + + val processed = processEvent(hint) + + val device = processed.contexts.device!! + assertEquals("Google", device.manufacturer) + assertEquals("Pixel", device.brand) + assertEquals("Pixel", device.family) + assertEquals("Pixel 3XL", device.model) + assertEquals(true, device.isSimulator) + assertEquals(2048, device.memorySize) + assertEquals(1080, device.screenWidthPixels) + assertEquals(1920, device.screenHeightPixels) + assertEquals(3.0f, device.screenDensity) + assertEquals(480, device.screenDpi) + } + + @Test + fun `when backfillable event is enrichable, still sets static data`() { + val hint = HintUtils.createWithTypeCheckHint(BackfillableHint()) + + val processed = processEvent(hint) + + assertNotNull(processed.platform) + assertFalse(processed.contexts.isEmpty()) + } + + @Test + fun `when backfillable event is enrichable, backfills serialized scope data`() { + val hint = HintUtils.createWithTypeCheckHint(BackfillableHint()) + + val processed = processEvent(hint, populateScopeCache = true) + + // user + assertEquals("bot", processed.user!!.username) + assertEquals("bot@me.com", processed.user!!.id) + // trace + assertEquals("ui.load", processed.contexts.trace!!.operation) + // tags + assertEquals("two", processed.tags!!["one"]) + // breadcrumbs + assertEquals("test", processed.breadcrumbs!![0].message) + assertEquals("debug", processed.breadcrumbs!![0].type) + assertEquals("navigation", processed.breadcrumbs!![1].type) + assertEquals("to", processed.breadcrumbs!![1].data["to"]) + assertEquals("from", processed.breadcrumbs!![1].data["from"]) + // extras + assertEquals(123, processed.extras!!["key"]) + // transaction + assertEquals("TestActivity", processed.transaction) + // fingerprint + assertEquals(listOf("finger", "print"), processed.fingerprints) + // level + assertEquals(SentryLevel.INFO, processed.level) + // request + assertEquals("google.com", processed.request!!.url) + assertEquals("GET", processed.request!!.method) + // contexts + assertEquals(1024, processed.contexts.response!!.bodySize) + assertEquals("Google Chrome", processed.contexts.browser!!.name) + } + + @Test + fun `when backfillable event is enrichable, backfills serialized options data`() { + val hint = HintUtils.createWithTypeCheckHint(BackfillableHint()) + + val processed = processEvent(hint, populateOptionsCache = true) + + // release + assertEquals("io.sentry.samples@1.2.0+232", processed.release) + // proguard uuid + assertEquals(DebugImage.PROGUARD, processed.debugMeta!!.images!![0].type) + assertEquals("uuid", processed.debugMeta!!.images!![0].uuid) + // sdk version + assertEquals("sentry.java.android", processed.sdk!!.name) + assertEquals("6.15.0", processed.sdk!!.version) + // dist + assertEquals("232", processed.dist) + // environment + assertEquals("debug", processed.environment) + // app + // robolectric defaults + assertEquals("io.sentry.android.core.test", processed.contexts.app!!.appIdentifier) + assertEquals("io.sentry.android.core.test", processed.contexts.app!!.appName) + assertEquals("1.2.0", processed.contexts.app!!.appVersion) + assertEquals("232", processed.contexts.app!!.appBuild) + // tags + assertEquals("tag", processed.tags!!["option"]) + } + + @Test + fun `if release is in wrong format, does not crash and leaves app version and build empty`() { + val hint = HintUtils.createWithTypeCheckHint(BackfillableHint()) + val original = SentryEvent() + + val processor = fixture.getSut(tmpDir) + fixture.persistOptions(RELEASE_FILENAME, "io.sentry.samples") + + val processed = processor.process(original, hint) + + assertEquals("io.sentry.samples", processed!!.release) + assertNull(processed!!.contexts.app!!.appVersion) + assertNull(processed!!.contexts.app!!.appBuild) + } + + @Test + fun `if environment is not persisted, uses default`() { + val hint = HintUtils.createWithTypeCheckHint(BackfillableHint()) + + val processed = processEvent(hint) + + assertEquals(AnrV2EventProcessor.DEFAULT_ENVIRONMENT, processed.environment) + } + + @Test + fun `if dist is not persisted, backfills it from release`() { + val hint = HintUtils.createWithTypeCheckHint(BackfillableHint()) + val original = SentryEvent() + + val processor = fixture.getSut(tmpDir) + fixture.persistOptions(RELEASE_FILENAME, "io.sentry.samples@1.2.0+232") + + val processed = processor.process(original, hint) + + assertEquals("232", processed!!.dist) + } + + @Test + fun `merges user`() { + val hint = HintUtils.createWithTypeCheckHint(BackfillableHint()) + + val processed = processEvent(hint, populateScopeCache = true) + + assertEquals("bot@me.com", processed.user!!.id) + assertEquals("bot", processed.user!!.username) + assertEquals(IpAddressUtils.DEFAULT_IP_ADDRESS, processed.user!!.ipAddress) + } + + @Test + fun `uses installation id for user, if it has no id`() { + val hint = HintUtils.createWithTypeCheckHint(BackfillableHint()) + val original = SentryEvent() + + val processor = fixture.getSut(tmpDir) + fixture.persistOptions(USER_FILENAME, User()) + + val processed = processor.process(original, hint) + + assertEquals(Installation.deviceId, processed!!.user!!.id) + } + + @Test + fun `when event has some fields set, does not override them`() { + val hint = HintUtils.createWithTypeCheckHint(BackfillableHint()) + + val processed = processEvent(hint, populateScopeCache = true, populateOptionsCache = true) { + contexts.setDevice( + Device().apply { + brand = "Pixel" + model = "3XL" + memorySize = 4096 + } + ) + platform = "NotAndroid" + + transaction = "MainActivity" + level = DEBUG + breadcrumbs = listOf(Breadcrumb.debug("test")) + + environment = "debug" + release = "io.sentry.samples@1.1.0+220" + debugMeta = DebugMeta().apply { + images = listOf(DebugImage().apply { type = DebugImage.PROGUARD; uuid = "uuid1" }) + } + } + + assertEquals("NotAndroid", processed.platform) + assertEquals("Pixel", processed.contexts.device!!.brand) + assertEquals("3XL", processed.contexts.device!!.model) + assertEquals(4096, processed.contexts.device!!.memorySize) + + assertEquals("MainActivity", processed.transaction) + assertEquals(DEBUG, processed.level) + assertEquals(3, processed.breadcrumbs!!.size) + assertEquals("debug", processed.breadcrumbs!![0].type) + assertEquals("debug", processed.breadcrumbs!![1].type) + assertEquals("navigation", processed.breadcrumbs!![2].type) + + assertEquals("debug", processed.environment) + assertEquals("io.sentry.samples@1.1.0+220", processed.release) + assertEquals("220", processed.contexts.app!!.appBuild) + assertEquals("1.1.0", processed.contexts.app!!.appVersion) + assertEquals(2, processed.debugMeta!!.images!!.size) + assertEquals("uuid1", processed.debugMeta!!.images!![0].uuid) + assertEquals("uuid", processed.debugMeta!!.images!![1].uuid) + } + + private fun processEvent( + hint: Hint, + populateScopeCache: Boolean = false, + populateOptionsCache: Boolean = false, + configureEvent: SentryEvent.() -> Unit = {} + ): SentryEvent { + val original = SentryEvent().apply(configureEvent) + + val processor = fixture.getSut( + tmpDir, + populateScopeCache = populateScopeCache, + populateOptionsCache = populateOptionsCache + ) + return processor.process(original, hint)!! + } + + internal class BackfillableHint(private val shouldEnrich: Boolean = true) : Backfillable { + override fun shouldEnrich(): Boolean = shouldEnrich + } +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt new file mode 100644 index 0000000000..6a8474ba9c --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt @@ -0,0 +1,334 @@ +package io.sentry.android.core + +import android.app.ActivityManager +import android.app.ApplicationExitInfo +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.sentry.Hint +import io.sentry.IHub +import io.sentry.ILogger +import io.sentry.SentryLevel +import io.sentry.android.core.AnrV2Integration.AnrV2Hint +import io.sentry.android.core.cache.AndroidEnvelopeCache +import io.sentry.exception.ExceptionMechanismException +import io.sentry.hints.DiskFlushNotification +import io.sentry.protocol.SentryId +import io.sentry.test.ImmediateExecutorService +import io.sentry.util.HintUtils +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argThat +import org.mockito.kotlin.check +import org.mockito.kotlin.inOrder +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.robolectric.annotation.Config +import org.robolectric.shadow.api.Shadow +import org.robolectric.shadows.ShadowActivityManager +import org.robolectric.shadows.ShadowActivityManager.ApplicationExitInfoBuilder +import java.io.File +import java.util.concurrent.TimeUnit +import kotlin.concurrent.thread +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +@Config(sdk = [30]) +class AnrV2IntegrationTest { + + @get:Rule + val tmpDir = TemporaryFolder() + + class Fixture { + lateinit var context: Context + lateinit var shadowActivityManager: ShadowActivityManager + lateinit var lastReportedAnrFile: File + + val options = SentryAndroidOptions() + val hub = mock() + val logger = mock() + + fun getSut( + dir: TemporaryFolder?, + useImmediateExecutorService: Boolean = true, + isAnrEnabled: Boolean = true, + flushTimeoutMillis: Long = 0L, + lastReportedAnrTimestamp: Long? = null, + lastEventId: SentryId = SentryId() + ): AnrV2Integration { + options.run { + setLogger(this@Fixture.logger) + isDebug = true + cacheDirPath = dir?.newFolder()?.absolutePath + executorService = + if (useImmediateExecutorService) ImmediateExecutorService() else mock() + this.isAnrEnabled = isAnrEnabled + this.flushTimeoutMillis = flushTimeoutMillis + } + options.cacheDirPath?.let { cacheDir -> + lastReportedAnrFile = File(cacheDir, AndroidEnvelopeCache.LAST_ANR_REPORT) + lastReportedAnrFile.writeText(lastReportedAnrTimestamp.toString()) + } + whenever(hub.captureEvent(any(), anyOrNull())).thenReturn(lastEventId) + + return AnrV2Integration(context) + } + + fun addAppExitInfo( + reason: Int? = ApplicationExitInfo.REASON_ANR, + timestamp: Long? = null, + importance: Int? = null + ) { + val builder = ApplicationExitInfoBuilder.newBuilder() + if (reason != null) { + builder.setReason(reason) + } + if (timestamp != null) { + builder.setTimestamp(timestamp) + } + if (importance != null) { + builder.setImportance(importance) + } + shadowActivityManager.addApplicationExitInfo(builder.build()) + } + } + + private val fixture = Fixture() + private val oldTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(10) + private val newTimestamp = oldTimestamp + TimeUnit.DAYS.toMillis(5) + + @BeforeTest + fun `set up`() { + fixture.context = ApplicationProvider.getApplicationContext() + val activityManager = + fixture.context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager? + fixture.shadowActivityManager = Shadow.extract(activityManager) + } + + @Test + fun `when cacheDir is not set, does not process historical exits`() { + val integration = fixture.getSut(null, useImmediateExecutorService = false) + + integration.register(fixture.hub, fixture.options) + + verify(fixture.options.executorService, never()).submit(any()) + } + + @Test + fun `when anr tracking is not enabled, does not process historical exits`() { + val integration = + fixture.getSut(tmpDir, isAnrEnabled = false, useImmediateExecutorService = false) + + integration.register(fixture.hub, fixture.options) + + verify(fixture.options.executorService, never()).submit(any()) + } + + @Test + fun `when historical exit list is empty, does not process historical exits`() { + val integration = fixture.getSut(tmpDir, useImmediateExecutorService = false) + + integration.register(fixture.hub, fixture.options) + + verify(fixture.options.executorService, never()).submit(any()) + } + + @Test + fun `when there are no ANRs in historical exits, does not capture events`() { + val integration = fixture.getSut(tmpDir) + fixture.addAppExitInfo(reason = null) + + integration.register(fixture.hub, fixture.options) + + verify(fixture.hub, never()).captureEvent(any(), anyOrNull()) + } + + @Test + fun `when latest ANR is older than 90 days, does not capture events`() { + val oldTimestamp = System.currentTimeMillis() - + AnrV2Integration.NINETY_DAYS_THRESHOLD - + TimeUnit.DAYS.toMillis(2) + val integration = fixture.getSut(tmpDir) + fixture.addAppExitInfo(timestamp = oldTimestamp) + + integration.register(fixture.hub, fixture.options) + + verify(fixture.hub, never()).captureEvent(any(), anyOrNull()) + } + + @Test + fun `when latest ANR has already been reported, does not capture events`() { + val integration = fixture.getSut(tmpDir, lastReportedAnrTimestamp = oldTimestamp) + fixture.addAppExitInfo(timestamp = oldTimestamp) + + integration.register(fixture.hub, fixture.options) + + verify(fixture.hub, never()).captureEvent(any(), anyOrNull()) + } + + @Test + fun `when latest ANR has not been reported, captures event with enriching`() { + val integration = fixture.getSut(tmpDir, lastReportedAnrTimestamp = oldTimestamp) + fixture.addAppExitInfo(timestamp = newTimestamp) + + integration.register(fixture.hub, fixture.options) + + verify(fixture.hub).captureEvent( + check { + assertEquals(newTimestamp, it.timestamp.time) + assertEquals(SentryLevel.FATAL, it.level) + assertTrue { + it.throwable is ApplicationNotResponding && + it.throwable!!.message == "Background ANR" + } + assertTrue { + (it.throwableMechanism as ExceptionMechanismException).exceptionMechanism.type == "ANRv2" + } + }, + argThat { + val hint = HintUtils.getSentrySdkHint(this) + (hint as AnrV2Hint).shouldEnrich() + } + ) + } + + @Test + fun `when latest ANR has foreground importance, does not add Background to the name`() { + val integration = fixture.getSut(tmpDir, lastReportedAnrTimestamp = oldTimestamp) + fixture.addAppExitInfo( + timestamp = newTimestamp, + importance = ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND + ) + + integration.register(fixture.hub, fixture.options) + + verify(fixture.hub).captureEvent( + argThat { + throwable is ApplicationNotResponding && throwable!!.message == "ANR" + }, + anyOrNull() + ) + } + + @Test + fun `waits for ANR events to be flushed on disk`() { + val integration = fixture.getSut( + tmpDir, + lastReportedAnrTimestamp = oldTimestamp, + flushTimeoutMillis = 3000L + ) + fixture.addAppExitInfo(timestamp = newTimestamp) + + whenever(fixture.hub.captureEvent(any(), any())).thenAnswer { invocation -> + val hint = HintUtils.getSentrySdkHint(invocation.getArgument(1)) + as DiskFlushNotification + thread { + Thread.sleep(1000L) + hint.markFlushed() + } + SentryId() + } + + integration.register(fixture.hub, fixture.options) + + verify(fixture.hub).captureEvent(any(), anyOrNull()) + // shouldn't fall into timed out state, because we marked event as flushed on another thread + verify(fixture.logger, never()).log( + any(), + argThat { startsWith("Timed out") }, + any() + ) + } + + @Test + fun `when latest ANR event was dropped, does not block flushing`() { + val integration = fixture.getSut( + tmpDir, + lastReportedAnrTimestamp = oldTimestamp, + lastEventId = SentryId.EMPTY_ID + ) + fixture.addAppExitInfo(timestamp = newTimestamp) + + integration.register(fixture.hub, fixture.options) + + verify(fixture.hub).captureEvent(any(), anyOrNull()) + // we do not call markFlushed, hence it should time out waiting for flush, but because + // we drop the event, it should not even come to this if-check + verify(fixture.logger, never()).log( + any(), + argThat { startsWith("Timed out") }, + any() + ) + } + + @Test + fun `historical ANRs are reported non-enriched`() { + val integration = fixture.getSut(tmpDir, lastReportedAnrTimestamp = oldTimestamp) + fixture.addAppExitInfo(timestamp = newTimestamp - 2 * 60 * 1000) + fixture.addAppExitInfo(timestamp = newTimestamp - 1 * 60 * 1000) + fixture.addAppExitInfo(timestamp = newTimestamp) + + integration.register(fixture.hub, fixture.options) + + verify(fixture.hub, times(2)).captureEvent( + any(), + argThat { + val hint = HintUtils.getSentrySdkHint(this) + !(hint as AnrV2Hint).shouldEnrich() + } + ) + } + + @Test + fun `historical ANRs are reported in reverse order to keep track of last reported ANR in a marker file`() { + val integration = fixture.getSut(tmpDir, lastReportedAnrTimestamp = oldTimestamp) + // robolectric uses addFirst when adding exit infos, so the last one here will be the first on the list + fixture.addAppExitInfo(timestamp = newTimestamp - TimeUnit.DAYS.toMillis(2)) + fixture.addAppExitInfo(timestamp = newTimestamp - TimeUnit.DAYS.toMillis(1)) + fixture.addAppExitInfo(timestamp = newTimestamp) + + integration.register(fixture.hub, fixture.options) + + // the order is reverse here, so the oldest ANR will be reported first to keep track of + // last reported ANR in a marker file + inOrder(fixture.hub) { + verify(fixture.hub).captureEvent( + argThat { timestamp.time == newTimestamp - TimeUnit.DAYS.toMillis(2) }, + anyOrNull() + ) + verify(fixture.hub).captureEvent( + argThat { timestamp.time == newTimestamp - TimeUnit.DAYS.toMillis(1) }, + anyOrNull() + ) + verify(fixture.hub).captureEvent( + argThat { timestamp.time == newTimestamp }, + anyOrNull() + ) + } + } + + @Test + fun `ANR timestamp is passed with the hint`() { + val integration = fixture.getSut(tmpDir, lastReportedAnrTimestamp = oldTimestamp) + fixture.addAppExitInfo(timestamp = newTimestamp) + + integration.register(fixture.hub, fixture.options) + + verify(fixture.hub).captureEvent( + any(), + argThat { + val hint = HintUtils.getSentrySdkHint(this) + (hint as AnrV2Hint).timestamp() == newTimestamp + } + ) + } +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsUnitTests.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsUnitTests.kt index 23eafc7571..8f8846b954 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsUnitTests.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsUnitTests.kt @@ -1,17 +1,27 @@ package io.sentry.android.core +import android.app.ActivityManager +import android.app.ActivityManager.MemoryInfo import android.content.Context import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.ILogger import io.sentry.NoOpLogger import org.junit.runner.RunWith +import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.spy +import org.mockito.kotlin.whenever +import org.robolectric.annotation.Config +import org.robolectric.shadow.api.Shadow +import org.robolectric.shadows.ShadowActivityManager +import org.robolectric.shadows.ShadowBuild import kotlin.test.BeforeTest import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull +import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) class ContextUtilsUnitTests { @@ -23,6 +33,7 @@ class ContextUtilsUnitTests { fun `set up`() { context = ApplicationProvider.getApplicationContext() logger = NoOpLogger.getInstance() + ShadowBuild.reset() } @Test @@ -56,4 +67,97 @@ class ContextUtilsUnitTests { val mockedVersionName = ContextUtils.getVersionName(mockedPackageInfo) assertNotNull(mockedVersionName) } + + @Test + fun `when context is valid, getApplicationName returns application name`() { + val appName = ContextUtils.getApplicationName(context, logger) + assertEquals("io.sentry.android.core.test", appName) + } + + @Test + fun `when context is invalid, getApplicationName returns null`() { + val appName = ContextUtils.getApplicationName(mock(), logger) + assertNull(appName) + } + + @Test + fun `isSideLoaded returns true for test context`() { + val sideLoadedInfo = + ContextUtils.getSideLoadedInfo(context, logger, BuildInfoProvider(logger)) + assertEquals("true", sideLoadedInfo?.get("isSideLoaded")) + } + + @Test + fun `when installerPackageName is not null, sideLoadedInfo returns false and installerStore`() { + val mockedContext = spy(context) { + val mockedPackageManager = spy(mock.packageManager) { + whenever(mock.getInstallerPackageName(any())).thenReturn("play.google.com") + } + whenever(mock.packageManager).thenReturn(mockedPackageManager) + } + val sideLoadedInfo = + ContextUtils.getSideLoadedInfo(mockedContext, logger, BuildInfoProvider(logger)) + assertEquals("false", sideLoadedInfo?.get("isSideLoaded")) + assertEquals("play.google.com", sideLoadedInfo?.get("installerStore")) + } + + @Test + @Config(qualifiers = "w360dp-h640dp-xxhdpi") + fun `when display metrics specified, getDisplayMetrics returns correct values`() { + val displayMetrics = ContextUtils.getDisplayMetrics(context, logger) + assertEquals(1080, displayMetrics!!.widthPixels) + assertEquals(1920, displayMetrics.heightPixels) + assertEquals(3.0f, displayMetrics.density) + assertEquals(480, displayMetrics.densityDpi) + } + + @Test + fun `when display metrics are not specified, getDisplayMetrics returns null`() { + val displayMetrics = ContextUtils.getDisplayMetrics(mock(), logger) + assertNull(displayMetrics) + } + + @Test + fun `when Build MODEL specified, getFamily returns correct value`() { + ShadowBuild.setModel("Pixel 3XL") + val family = ContextUtils.getFamily(logger) + assertEquals("Pixel", family) + } + + @Test + fun `when Build MODEL is not specified, getFamily returns null`() { + ShadowBuild.setModel(null) + val family = ContextUtils.getFamily(logger) + assertNull(family) + } + + @Test + fun `when supported abis is specified, getArchitectures returns correct values`() { + val architectures = ContextUtils.getArchitectures(BuildInfoProvider(logger)) + assertEquals("armeabi-v7a", architectures[0]) + } + + @Test + fun `when memory info is specified, returns correct values`() { + val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val shadowActivityManager = Shadow.extract(activityManager) + + shadowActivityManager.setMemoryInfo( + MemoryInfo().apply { + availMem = 128 + totalMem = 2048 + lowMemory = true + } + ) + val memInfo = ContextUtils.getMemInfo(context, logger) + assertEquals(128, memInfo!!.availMem) + assertEquals(2048, memInfo.totalMem) + assertTrue(memInfo.lowMemory) + } + + @Test + fun `when memory info is not specified, returns null`() { + val memInfo = ContextUtils.getMemInfo(mock(), logger) + assertNull(memInfo) + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt index 5070cc00bf..5ec657c21c 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt @@ -1,8 +1,12 @@ package io.sentry.android.core +import android.app.ActivityManager +import android.app.ApplicationExitInfo +import android.content.Context import android.os.Bundle import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.sentry.Breadcrumb import io.sentry.Hint import io.sentry.ILogger import io.sentry.Sentry @@ -11,13 +15,26 @@ import io.sentry.SentryLevel import io.sentry.SentryLevel.DEBUG import io.sentry.SentryLevel.FATAL import io.sentry.SentryOptions +import io.sentry.SentryOptions.BeforeSendCallback import io.sentry.Session import io.sentry.android.core.cache.AndroidEnvelopeCache import io.sentry.android.fragment.FragmentLifecycleIntegration import io.sentry.android.timber.SentryTimberIntegration import io.sentry.cache.IEnvelopeCache +import io.sentry.cache.PersistingOptionsObserver +import io.sentry.cache.PersistingOptionsObserver.ENVIRONMENT_FILENAME +import io.sentry.cache.PersistingOptionsObserver.OPTIONS_CACHE +import io.sentry.cache.PersistingOptionsObserver.RELEASE_FILENAME +import io.sentry.cache.PersistingScopeObserver +import io.sentry.cache.PersistingScopeObserver.BREADCRUMBS_FILENAME +import io.sentry.cache.PersistingScopeObserver.SCOPE_CACHE +import io.sentry.cache.PersistingScopeObserver.TRANSACTION_FILENAME import io.sentry.transport.NoOpEnvelopeCache import io.sentry.util.StringUtils +import org.awaitility.kotlin.await +import org.awaitility.kotlin.withAlias +import org.junit.Rule +import org.junit.rules.TemporaryFolder import org.junit.runner.RunWith import org.mockito.Mockito import org.mockito.kotlin.any @@ -25,7 +42,14 @@ import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify +import org.robolectric.annotation.Config +import org.robolectric.shadow.api.Shadow +import org.robolectric.shadows.ShadowActivityManager +import org.robolectric.shadows.ShadowActivityManager.ApplicationExitInfoBuilder +import java.io.File import java.nio.file.Files +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean import kotlin.io.path.absolutePathString import kotlin.test.BeforeTest import kotlin.test.Test @@ -38,9 +62,14 @@ import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) class SentryAndroidTest { + @get:Rule + val tmpDir = TemporaryFolder() + class Fixture { + lateinit var shadowActivityManager: ShadowActivityManager fun initSut( + context: Context? = null, autoInit: Boolean = false, logger: ILogger? = null, options: Sentry.OptionsConfiguration? = null @@ -49,21 +78,43 @@ class SentryAndroidTest { putString(ManifestMetadataReader.DSN, "https://key@sentry.io/123") putBoolean(ManifestMetadataReader.AUTO_INIT, autoInit) } - val mockContext = ContextUtilsTest.mockMetaData(metaData = metadata) + val mockContext = context ?: ContextUtilsTest.mockMetaData(metaData = metadata) when { logger != null -> SentryAndroid.init(mockContext, logger) options != null -> SentryAndroid.init(mockContext, options) else -> SentryAndroid.init(mockContext) } } + + fun addAppExitInfo( + reason: Int? = ApplicationExitInfo.REASON_ANR, + timestamp: Long? = null, + importance: Int? = null + ) { + val builder = ApplicationExitInfoBuilder.newBuilder() + if (reason != null) { + builder.setReason(reason) + } + if (timestamp != null) { + builder.setTimestamp(timestamp) + } + if (importance != null) { + builder.setImportance(importance) + } + shadowActivityManager.addApplicationExitInfo(builder.build()) + } } private val fixture = Fixture() + private lateinit var context: Context @BeforeTest fun `set up`() { Sentry.close() AppStartState.getInstance().resetInstance() + context = ApplicationProvider.getApplicationContext() + val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager? + fixture.shadowActivityManager = Shadow.extract(activityManager) } @Test @@ -141,7 +192,10 @@ class SentryAndroidTest { // fragment integration is not auto-installed in the test, since the context is not Application // but we just verify here that the single integration is preserved - assertEquals(refOptions!!.integrations.filterIsInstance().size, 1) + assertEquals( + refOptions!!.integrations.filterIsInstance().size, + 1 + ) } @Test @@ -186,7 +240,10 @@ class SentryAndroidTest { val dsnHash = StringUtils.calculateStringHash(options!!.dsn, options!!.logger) val expectedCacheDir = "$cacheDirPath/$dsnHash" assertEquals(expectedCacheDir, options!!.cacheDirPath) - assertEquals(expectedCacheDir, (options!!.envelopeDiskCache as AndroidEnvelopeCache).directory.absolutePath) + assertEquals( + expectedCacheDir, + (options!!.envelopeDiskCache as AndroidEnvelopeCache).directory.absolutePath + ) } @Test @@ -203,7 +260,10 @@ class SentryAndroidTest { } } - private fun initSentryWithForegroundImportance(inForeground: Boolean, callback: (session: Session?) -> Unit) { + private fun initSentryWithForegroundImportance( + inForeground: Boolean, + callback: (session: Session?) -> Unit + ) { val context = ContextUtilsTest.createMockContext() Mockito.mockStatic(ContextUtils::class.java).use { mockedContextUtils -> @@ -233,6 +293,86 @@ class SentryAndroidTest { } } + @Test + @Config(sdk = [30]) + fun `AnrV2 events get enriched with previously persisted scope and options data, the new data gets persisted after that`() { + val cacheDir = tmpDir.newFolder().absolutePath + fixture.addAppExitInfo(timestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)) + val asserted = AtomicBoolean(false) + lateinit var options: SentryOptions + + fixture.initSut(context) { + it.dsn = "https://key@sentry.io/123" + it.cacheDirPath = cacheDir + // beforeSend is called after event processors are applied, so we can assert here + // against the enriched ANR event + it.beforeSend = BeforeSendCallback { event, hint -> + assertEquals("MainActivity", event.transaction) + assertEquals("Debug!", event.breadcrumbs!![0].message) + assertEquals("staging", event.environment) + assertEquals("io.sentry.sample@2.0.0", event.release) + asserted.set(true) + null + } + + // have to do it after the cacheDir is set to options, because it adds a dsn hash after + prefillOptionsCache(it.cacheDirPath!!) + prefillScopeCache(it.cacheDirPath!!) + + it.release = "io.sentry.sample@1.1.0+220" + it.environment = "debug" + // this is necessary to delay the AnrV2Integration processing to execute the configure + // scope block below (otherwise it won't be possible as hub is no-op before .init) + it.executorService.submit { + Thread.sleep(2000L) + Sentry.configureScope { scope -> + // make sure the scope values changed to test that we're still using previously + // persisted values for the old ANR events + assertEquals("TestActivity", scope.transactionName) + } + } + options = it + } + Sentry.configureScope { + it.setTransaction("TestActivity") + it.addBreadcrumb(Breadcrumb.error("Error!")) + } + await.withAlias("Failed because of BeforeSend callback above, but we swallow BeforeSend exceptions, hence the timeout") + .untilTrue(asserted) + + // assert that persisted values have changed + options.executorService.close(1000L) // finalizes all enqueued persisting tasks + assertEquals( + "TestActivity", + PersistingScopeObserver.read(options, TRANSACTION_FILENAME, String::class.java) + ) + assertEquals( + "io.sentry.sample@1.1.0+220", + PersistingOptionsObserver.read(options, RELEASE_FILENAME, String::class.java) + ) + } + + private fun prefillScopeCache(cacheDir: String) { + val scopeDir = File(cacheDir, SCOPE_CACHE).also { it.mkdirs() } + File(scopeDir, BREADCRUMBS_FILENAME).writeText( + """ + [{ + "timestamp": "2009-11-16T01:08:47.000Z", + "message": "Debug!", + "type": "debug", + "level": "debug" + }] + """.trimIndent() + ) + File(scopeDir, TRANSACTION_FILENAME).writeText("\"MainActivity\"") + } + + private fun prefillOptionsCache(cacheDir: String) { + val optionsDir = File(cacheDir, OPTIONS_CACHE).also { it.mkdirs() } + File(optionsDir, RELEASE_FILENAME).writeText("\"io.sentry.sample@2.0.0\"") + File(optionsDir, ENVIRONMENT_FILENAME).writeText("\"staging\"") + } + private class CustomEnvelopCache : IEnvelopeCache { override fun iterator(): MutableIterator = TODO() override fun store(envelope: SentryEnvelope, hint: Hint) = Unit diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt index a5d1bb1f24..3eee455d98 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt @@ -1,41 +1,51 @@ package io.sentry.android.core.cache +import io.sentry.NoOpLogger import io.sentry.SentryEnvelope +import io.sentry.UncaughtExceptionHandlerIntegration.UncaughtExceptionHint +import io.sentry.android.core.AnrV2Integration.AnrV2Hint import io.sentry.android.core.AppStartState import io.sentry.android.core.SentryAndroidOptions import io.sentry.cache.EnvelopeCache -import io.sentry.hints.DiskFlushNotification import io.sentry.transport.ICurrentDateProvider import io.sentry.util.HintUtils +import org.junit.Rule +import org.junit.rules.TemporaryFolder import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import java.io.File -import java.nio.file.Files -import java.nio.file.Path +import java.lang.IllegalArgumentException import kotlin.test.BeforeTest import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue class AndroidEnvelopeCacheTest { + + @get:Rule + val tmpDir = TemporaryFolder() + private class Fixture { - private val dir: Path = Files.createTempDirectory("sentry-cache") val envelope = mock { whenever(it.header).thenReturn(mock()) } val options = SentryAndroidOptions() val dateProvider = mock() - lateinit var markerFile: File + lateinit var startupCrashMarkerFile: File + lateinit var lastReportedAnrFile: File fun getSut( + dir: TemporaryFolder, appStartMillis: Long? = null, currentTimeMillis: Long? = null ): AndroidEnvelopeCache { - options.cacheDirPath = dir.toAbsolutePath().toFile().absolutePath + options.cacheDirPath = dir.newFolder("sentry-cache").absolutePath val outboxDir = File(options.outboxPath!!) outboxDir.mkdirs() - markerFile = File(outboxDir, EnvelopeCache.STARTUP_CRASH_MARKER_FILE) + startupCrashMarkerFile = File(outboxDir, EnvelopeCache.STARTUP_CRASH_MARKER_FILE) + lastReportedAnrFile = File(options.cacheDirPath!!, AndroidEnvelopeCache.LAST_ANR_REPORT) if (appStartMillis != null) { AppStartState.getInstance().setAppStartMillis(appStartMillis) @@ -56,57 +66,133 @@ class AndroidEnvelopeCacheTest { } @Test - fun `when no flush hint exists, does not write startup crash file`() { - val cache = fixture.getSut() + fun `when no uncaught hint exists, does not write startup crash file`() { + val cache = fixture.getSut(tmpDir) cache.store(fixture.envelope) - assertFalse(fixture.markerFile.exists()) + assertFalse(fixture.startupCrashMarkerFile.exists()) } @Test fun `when startup time is null, does not write startup crash file`() { - val cache = fixture.getSut() + val cache = fixture.getSut(tmpDir) - val hints = HintUtils.createWithTypeCheckHint(DiskFlushHint()) + val hints = HintUtils.createWithTypeCheckHint(UncaughtHint()) cache.store(fixture.envelope, hints) - assertFalse(fixture.markerFile.exists()) + assertFalse(fixture.startupCrashMarkerFile.exists()) } @Test fun `when time since sdk init is more than duration threshold, does not write startup crash file`() { - val cache = fixture.getSut(appStartMillis = 1000L, currentTimeMillis = 5000L) + val cache = fixture.getSut(dir = tmpDir, appStartMillis = 1000L, currentTimeMillis = 5000L) - val hints = HintUtils.createWithTypeCheckHint(DiskFlushHint()) + val hints = HintUtils.createWithTypeCheckHint(UncaughtHint()) cache.store(fixture.envelope, hints) - assertFalse(fixture.markerFile.exists()) + assertFalse(fixture.startupCrashMarkerFile.exists()) } @Test fun `when outbox dir is not set, does not write startup crash file`() { - val cache = fixture.getSut(appStartMillis = 1000L, currentTimeMillis = 2000L) + val cache = fixture.getSut(dir = tmpDir, appStartMillis = 1000L, currentTimeMillis = 2000L) fixture.options.cacheDirPath = null - val hints = HintUtils.createWithTypeCheckHint(DiskFlushHint()) + val hints = HintUtils.createWithTypeCheckHint(UncaughtHint()) cache.store(fixture.envelope, hints) - assertFalse(fixture.markerFile.exists()) + assertFalse(fixture.startupCrashMarkerFile.exists()) } @Test fun `when time since sdk init is less than duration threshold, writes startup crash file`() { - val cache = fixture.getSut(appStartMillis = 1000L, currentTimeMillis = 2000L) + val cache = fixture.getSut(dir = tmpDir, appStartMillis = 1000L, currentTimeMillis = 2000L) - val hints = HintUtils.createWithTypeCheckHint(DiskFlushHint()) + val hints = HintUtils.createWithTypeCheckHint(UncaughtHint()) cache.store(fixture.envelope, hints) - assertTrue(fixture.markerFile.exists()) + assertTrue(fixture.startupCrashMarkerFile.exists()) + } + + @Test + fun `when no AnrV2 hint exists, does not write last anr report file`() { + val cache = fixture.getSut(tmpDir) + + cache.store(fixture.envelope) + + assertFalse(fixture.lastReportedAnrFile.exists()) } - internal class DiskFlushHint : DiskFlushNotification { - override fun markFlushed() {} + @Test + fun `when cache dir is not set, does not write last anr report file`() { + val cache = fixture.getSut(tmpDir) + + fixture.options.cacheDirPath = null + + val hints = HintUtils.createWithTypeCheckHint( + AnrV2Hint( + 0, + NoOpLogger.getInstance(), + 12345678L, + false + ) + ) + cache.store(fixture.envelope, hints) + + assertFalse(fixture.lastReportedAnrFile.exists()) + } + + @Test + fun `when AnrV2 hint exists, writes last anr report timestamp into file`() { + val cache = fixture.getSut(tmpDir) + + val hints = HintUtils.createWithTypeCheckHint( + AnrV2Hint( + 0, + NoOpLogger.getInstance(), + 12345678L, + false + ) + ) + cache.store(fixture.envelope, hints) + + assertTrue(fixture.lastReportedAnrFile.exists()) + assertEquals("12345678", fixture.lastReportedAnrFile.readText()) } + + @Test + fun `when cache dir is not set, throws upon reading last reported anr file`() { + fixture.getSut(tmpDir) + + fixture.options.cacheDirPath = null + + try { + AndroidEnvelopeCache.lastReportedAnr(fixture.options) + } catch (e: Throwable) { + assertTrue { e is IllegalArgumentException } + } + } + + @Test + fun `when last reported anr file does not exist, returns 0 upon reading`() { + fixture.getSut(tmpDir) + + val lastReportedAnr = AndroidEnvelopeCache.lastReportedAnr(fixture.options) + + assertEquals(0L, lastReportedAnr) + } + + @Test + fun `when last reported anr file exists, returns timestamp from the file upon reading`() { + fixture.getSut(tmpDir) + fixture.lastReportedAnrFile.writeText("87654321") + + val lastReportedAnr = AndroidEnvelopeCache.lastReportedAnr(fixture.options) + + assertEquals(87654321L, lastReportedAnr) + } + + internal class UncaughtHint : UncaughtExceptionHint(0, NoOpLogger.getInstance()) } diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java index 01f3d4d392..fc91a505d7 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java @@ -2,6 +2,7 @@ import android.content.Intent; import android.os.Bundle; +import android.os.Handler; import androidx.appcompat.app.AppCompatActivity; import io.sentry.Attachment; import io.sentry.ISpan; @@ -30,7 +31,10 @@ public class MainActivity extends AppCompatActivity { private int crashCount = 0; private int screenLoadCount = 0; + final Object mutex = new Object(); + @Override + @SuppressWarnings("deprecation") protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -148,11 +152,35 @@ protected void onCreate(Bundle savedInstanceState) { // Sentry. // NOTE: By default it doesn't raise if the debugger is attached. That can also be // configured. - try { - Thread.sleep(10000); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } + new Thread( + new Runnable() { + @Override + public void run() { + synchronized (mutex) { + while (true) { + try { + Thread.sleep(10000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + } + }) + .start(); + + new Handler() + .postDelayed( + new Runnable() { + @Override + public void run() { + synchronized (mutex) { + // Shouldn't happen + throw new IllegalStateException(); + } + } + }, + 1000); }); binding.openSecondActivity.setOnClickListener( diff --git a/sentry-test-support/api/sentry-test-support.api b/sentry-test-support/api/sentry-test-support.api index 53079bdb7c..1432f2414f 100644 --- a/sentry-test-support/api/sentry-test-support.api +++ b/sentry-test-support/api/sentry-test-support.api @@ -7,6 +7,14 @@ public final class io/sentry/SkipError : java/lang/Error { public fun (Ljava/lang/String;)V } +public final class io/sentry/test/ImmediateExecutorService : io/sentry/ISentryExecutorService { + public fun ()V + public fun close (J)V + public fun schedule (Ljava/lang/Runnable;J)Ljava/util/concurrent/Future; + public fun submit (Ljava/lang/Runnable;)Ljava/util/concurrent/Future; + public fun submit (Ljava/util/concurrent/Callable;)Ljava/util/concurrent/Future; +} + public final class io/sentry/test/ReflectionKt { public static final fun containsMethod (Ljava/lang/Class;Ljava/lang/String;Ljava/lang/Class;)Z public static final fun containsMethod (Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/Class;)Z diff --git a/sentry-test-support/src/main/kotlin/io/sentry/test/Mocks.kt b/sentry-test-support/src/main/kotlin/io/sentry/test/Mocks.kt new file mode 100644 index 0000000000..9966aab7e2 --- /dev/null +++ b/sentry-test-support/src/main/kotlin/io/sentry/test/Mocks.kt @@ -0,0 +1,18 @@ +// ktlint-disable filename +package io.sentry.test + +import io.sentry.ISentryExecutorService +import org.mockito.kotlin.mock +import java.util.concurrent.Callable +import java.util.concurrent.Future + +class ImmediateExecutorService : ISentryExecutorService { + override fun submit(runnable: Runnable): Future<*> { + runnable.run() + return mock() + } + + override fun submit(callable: Callable): Future = mock() + override fun schedule(runnable: Runnable, delayMillis: Long): Future<*> = mock() + override fun close(timeoutMillis: Long) {} +} diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index e0958376cb..4060b76e64 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -25,6 +25,9 @@ public final class io/sentry/Attachment { public fun getSerializable ()Lio/sentry/JsonSerializable; } +public abstract interface class io/sentry/BackfillingEventProcessor : io/sentry/EventProcessor { +} + public final class io/sentry/Baggage { public fun (Lio/sentry/ILogger;)V public fun (Ljava/util/Map;Ljava/lang/String;ZLio/sentry/ILogger;)V @@ -86,6 +89,7 @@ public final class io/sentry/Breadcrumb : io/sentry/JsonSerializable, io/sentry/ public fun (Ljava/lang/String;)V public fun (Ljava/util/Date;)V public static fun debug (Ljava/lang/String;)Lio/sentry/Breadcrumb; + public fun equals (Ljava/lang/Object;)Z public static fun error (Ljava/lang/String;)Lio/sentry/Breadcrumb; public fun getCategory ()Ljava/lang/String; public fun getData ()Ljava/util/Map; @@ -95,6 +99,7 @@ public final class io/sentry/Breadcrumb : io/sentry/JsonSerializable, io/sentry/ public fun getTimestamp ()Ljava/util/Date; public fun getType ()Ljava/lang/String; public fun getUnknown ()Ljava/util/Map; + public fun hashCode ()I public static fun http (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/Breadcrumb; public static fun http (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)Lio/sentry/Breadcrumb; public static fun info (Ljava/lang/String;)Lio/sentry/Breadcrumb; @@ -491,13 +496,31 @@ public abstract interface class io/sentry/IMemoryCollector { public abstract fun collect ()Lio/sentry/MemoryCollectionData; } +public abstract interface class io/sentry/IOptionsObserver { + public fun setDist (Ljava/lang/String;)V + public fun setEnvironment (Ljava/lang/String;)V + public fun setProguardUuid (Ljava/lang/String;)V + public fun setRelease (Ljava/lang/String;)V + public fun setSdkVersion (Lio/sentry/protocol/SdkVersion;)V + public fun setTags (Ljava/util/Map;)V +} + public abstract interface class io/sentry/IScopeObserver { - public abstract fun addBreadcrumb (Lio/sentry/Breadcrumb;)V - public abstract fun removeExtra (Ljava/lang/String;)V - public abstract fun removeTag (Ljava/lang/String;)V - public abstract fun setExtra (Ljava/lang/String;Ljava/lang/String;)V - public abstract fun setTag (Ljava/lang/String;Ljava/lang/String;)V - public abstract fun setUser (Lio/sentry/protocol/User;)V + public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V + public fun removeExtra (Ljava/lang/String;)V + public fun removeTag (Ljava/lang/String;)V + public fun setBreadcrumbs (Ljava/util/Collection;)V + public fun setContexts (Lio/sentry/protocol/Contexts;)V + public fun setExtra (Ljava/lang/String;Ljava/lang/String;)V + public fun setExtras (Ljava/util/Map;)V + public fun setFingerprint (Ljava/util/Collection;)V + public fun setLevel (Lio/sentry/SentryLevel;)V + public fun setRequest (Lio/sentry/protocol/Request;)V + public fun setTag (Ljava/lang/String;Ljava/lang/String;)V + public fun setTags (Ljava/util/Map;)V + public fun setTrace (Lio/sentry/SpanContext;)V + public fun setTransaction (Ljava/lang/String;)V + public fun setUser (Lio/sentry/protocol/User;)V } public abstract interface class io/sentry/ISentryClient { @@ -535,6 +558,7 @@ public abstract interface class io/sentry/ISentryExecutorService { public abstract interface class io/sentry/ISerializer { public abstract fun deserialize (Ljava/io/Reader;Ljava/lang/Class;)Ljava/lang/Object; + public abstract fun deserializeCollection (Ljava/io/Reader;Ljava/lang/Class;Lio/sentry/JsonDeserializer;)Ljava/lang/Object; public abstract fun deserializeEnvelope (Ljava/io/InputStream;)Lio/sentry/SentryEnvelope; public abstract fun serialize (Lio/sentry/SentryEnvelope;Ljava/io/OutputStream;)V public abstract fun serialize (Ljava/lang/Object;Ljava/io/Writer;)V @@ -613,6 +637,7 @@ public abstract interface class io/sentry/IntegrationName { } public final class io/sentry/IpAddressUtils { + public static final field DEFAULT_IP_ADDRESS Ljava/lang/String; public static fun isDefault (Ljava/lang/String;)Z } @@ -675,6 +700,7 @@ public abstract interface class io/sentry/JsonSerializable { public final class io/sentry/JsonSerializer : io/sentry/ISerializer { public fun (Lio/sentry/SentryOptions;)V public fun deserialize (Ljava/io/Reader;Ljava/lang/Class;)Ljava/lang/Object; + public fun deserializeCollection (Ljava/io/Reader;Ljava/lang/Class;Lio/sentry/JsonDeserializer;)Ljava/lang/Object; public fun deserializeEnvelope (Ljava/io/InputStream;)Lio/sentry/SentryEnvelope; public fun serialize (Lio/sentry/SentryEnvelope;Ljava/io/OutputStream;)V public fun serialize (Ljava/lang/Object;Ljava/io/Writer;)V @@ -1231,6 +1257,7 @@ public abstract class io/sentry/SentryBaseEvent { public fun getEnvironment ()Ljava/lang/String; public fun getEventId ()Lio/sentry/protocol/SentryId; public fun getExtra (Ljava/lang/String;)Ljava/lang/Object; + public fun getExtras ()Ljava/util/Map; public fun getPlatform ()Ljava/lang/String; public fun getRelease ()Ljava/lang/String; public fun getRequest ()Lio/sentry/protocol/Request; @@ -1424,6 +1451,7 @@ public final class io/sentry/SentryEvent : io/sentry/SentryBaseEvent, io/sentry/ public fun setModule (Ljava/lang/String;Ljava/lang/String;)V public fun setModules (Ljava/util/Map;)V public fun setThreads (Ljava/util/List;)V + public fun setTimestamp (Ljava/util/Date;)V public fun setTransaction (Ljava/lang/String;)V public fun setUnknown (Ljava/util/Map;)V } @@ -1447,6 +1475,11 @@ public final class io/sentry/SentryEvent$JsonKeys { public fun ()V } +public final class io/sentry/SentryExceptionFactory { + public fun (Lio/sentry/SentryStackTraceFactory;)V + public fun getSentryExceptions (Ljava/lang/Throwable;)Ljava/util/List; +} + public final class io/sentry/SentryInstantDate : io/sentry/SentryDate { public fun ()V public fun (Ljava/time/Instant;)V @@ -1526,6 +1559,7 @@ public class io/sentry/SentryOptions { public fun addInAppExclude (Ljava/lang/String;)V public fun addInAppInclude (Ljava/lang/String;)V public fun addIntegration (Lio/sentry/Integration;)V + public fun addOptionsObserver (Lio/sentry/IOptionsObserver;)V public fun addScopeObserver (Lio/sentry/IScopeObserver;)V public fun addTracingOrigin (Ljava/lang/String;)V public fun getBeforeBreadcrumb ()Lio/sentry/SentryOptions$BeforeBreadcrumbCallback; @@ -1568,6 +1602,7 @@ public class io/sentry/SentryOptions { public fun getMaxSpans ()I public fun getMaxTraceFileSize ()J public fun getModulesLoader ()Lio/sentry/internal/modules/IModulesLoader; + public fun getOptionsObservers ()Ljava/util/List; public fun getOutboxPath ()Ljava/lang/String; public fun getProfilesSampleRate ()Ljava/lang/Double; public fun getProfilesSampler ()Lio/sentry/SentryOptions$ProfilesSamplerCallback; @@ -1577,6 +1612,7 @@ public class io/sentry/SentryOptions { public fun getReadTimeoutMillis ()I public fun getRelease ()Ljava/lang/String; public fun getSampleRate ()Ljava/lang/Double; + public fun getScopeObservers ()Ljava/util/List; public fun getSdkVersion ()Lio/sentry/protocol/SdkVersion; public fun getSentryClientName ()Ljava/lang/String; public fun getSerializer ()Lio/sentry/ISerializer; @@ -1749,6 +1785,10 @@ public final class io/sentry/SentryStackTraceFactory { public fun getStackFrames ([Ljava/lang/StackTraceElement;)Ljava/util/List; } +public final class io/sentry/SentryThreadFactory { + public fun (Lio/sentry/SentryStackTraceFactory;Lio/sentry/SentryOptions;)V +} + public final class io/sentry/SentryTraceHeader { public static final field SENTRY_TRACE_HEADER Ljava/lang/String; public fun (Lio/sentry/protocol/SentryId;Lio/sentry/SpanId;Ljava/lang/Boolean;)V @@ -1931,6 +1971,7 @@ public class io/sentry/SpanContext : io/sentry/JsonSerializable, io/sentry/JsonU public fun (Lio/sentry/protocol/SentryId;Lio/sentry/SpanId;Ljava/lang/String;Lio/sentry/SpanId;Lio/sentry/TracesSamplingDecision;)V public fun (Ljava/lang/String;)V public fun (Ljava/lang/String;Lio/sentry/TracesSamplingDecision;)V + public fun equals (Ljava/lang/Object;)Z public fun getDescription ()Ljava/lang/String; public fun getOperation ()Ljava/lang/String; public fun getParentSpanId ()Lio/sentry/SpanId; @@ -1942,6 +1983,7 @@ public class io/sentry/SpanContext : io/sentry/JsonSerializable, io/sentry/JsonU public fun getTags ()Ljava/util/Map; public fun getTraceId ()Lio/sentry/protocol/SentryId; public fun getUnknown ()Ljava/util/Map; + public fun hashCode ()I public fun serialize (Lio/sentry/JsonObjectWriter;Lio/sentry/ILogger;)V public fun setDescription (Ljava/lang/String;)V public fun setOperation (Ljava/lang/String;)V @@ -2165,6 +2207,10 @@ public final class io/sentry/UncaughtExceptionHandlerIntegration : io/sentry/Int public fun uncaughtException (Ljava/lang/Thread;Ljava/lang/Throwable;)V } +public class io/sentry/UncaughtExceptionHandlerIntegration$UncaughtExceptionHint : io/sentry/hints/BlockingFlushHint, io/sentry/hints/SessionEnd { + public fun (JLio/sentry/ILogger;)V +} + public final class io/sentry/UserFeedback : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun (Lio/sentry/protocol/SentryId;)V public fun (Lio/sentry/protocol/SentryId;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V @@ -2215,6 +2261,52 @@ public abstract interface class io/sentry/cache/IEnvelopeCache : java/lang/Itera public abstract fun store (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V } +public final class io/sentry/cache/PersistingOptionsObserver : io/sentry/IOptionsObserver { + public static final field DIST_FILENAME Ljava/lang/String; + public static final field ENVIRONMENT_FILENAME Ljava/lang/String; + public static final field OPTIONS_CACHE Ljava/lang/String; + public static final field PROGUARD_UUID_FILENAME Ljava/lang/String; + public static final field RELEASE_FILENAME Ljava/lang/String; + public static final field SDK_VERSION_FILENAME Ljava/lang/String; + public static final field TAGS_FILENAME Ljava/lang/String; + public fun (Lio/sentry/SentryOptions;)V + public static fun read (Lio/sentry/SentryOptions;Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object; + public static fun read (Lio/sentry/SentryOptions;Ljava/lang/String;Ljava/lang/Class;Lio/sentry/JsonDeserializer;)Ljava/lang/Object; + public fun setDist (Ljava/lang/String;)V + public fun setEnvironment (Ljava/lang/String;)V + public fun setProguardUuid (Ljava/lang/String;)V + public fun setRelease (Ljava/lang/String;)V + public fun setSdkVersion (Lio/sentry/protocol/SdkVersion;)V + public fun setTags (Ljava/util/Map;)V +} + +public final class io/sentry/cache/PersistingScopeObserver : io/sentry/IScopeObserver { + public static final field BREADCRUMBS_FILENAME Ljava/lang/String; + public static final field CONTEXTS_FILENAME Ljava/lang/String; + public static final field EXTRAS_FILENAME Ljava/lang/String; + public static final field FINGERPRINT_FILENAME Ljava/lang/String; + public static final field LEVEL_FILENAME Ljava/lang/String; + public static final field REQUEST_FILENAME Ljava/lang/String; + public static final field SCOPE_CACHE Ljava/lang/String; + public static final field TAGS_FILENAME Ljava/lang/String; + public static final field TRACE_FILENAME Ljava/lang/String; + public static final field TRANSACTION_FILENAME Ljava/lang/String; + public static final field USER_FILENAME Ljava/lang/String; + public fun (Lio/sentry/SentryOptions;)V + public static fun read (Lio/sentry/SentryOptions;Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object; + public static fun read (Lio/sentry/SentryOptions;Ljava/lang/String;Ljava/lang/Class;Lio/sentry/JsonDeserializer;)Ljava/lang/Object; + public fun setBreadcrumbs (Ljava/util/Collection;)V + public fun setContexts (Lio/sentry/protocol/Contexts;)V + public fun setExtras (Ljava/util/Map;)V + public fun setFingerprint (Ljava/util/Collection;)V + public fun setLevel (Lio/sentry/SentryLevel;)V + public fun setRequest (Lio/sentry/protocol/Request;)V + public fun setTags (Ljava/util/Map;)V + public fun setTrace (Lio/sentry/SpanContext;)V + public fun setTransaction (Ljava/lang/String;)V + public fun setUser (Lio/sentry/protocol/User;)V +} + public final class io/sentry/clientreport/ClientReport : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun (Ljava/util/Date;Ljava/util/List;)V public fun getDiscardedEvents ()Ljava/util/List; @@ -2347,6 +2439,16 @@ public abstract interface class io/sentry/hints/AbnormalExit { public abstract interface class io/sentry/hints/ApplyScopeData { } +public abstract interface class io/sentry/hints/Backfillable { + public abstract fun shouldEnrich ()Z +} + +public abstract class io/sentry/hints/BlockingFlushHint : io/sentry/hints/DiskFlushNotification, io/sentry/hints/Flushable { + public fun (JLio/sentry/ILogger;)V + public fun markFlushed ()V + public fun waitFlush ()Z +} + public abstract interface class io/sentry/hints/Cached { } @@ -2555,6 +2657,7 @@ public final class io/sentry/profilemeasurements/ProfileMeasurementValue$JsonKey public final class io/sentry/protocol/App : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public static final field TYPE Ljava/lang/String; public fun ()V + public fun equals (Ljava/lang/Object;)Z public fun getAppBuild ()Ljava/lang/String; public fun getAppIdentifier ()Ljava/lang/String; public fun getAppName ()Ljava/lang/String; @@ -2565,6 +2668,7 @@ public final class io/sentry/protocol/App : io/sentry/JsonSerializable, io/sentr public fun getInForeground ()Ljava/lang/Boolean; public fun getPermissions ()Ljava/util/Map; public fun getUnknown ()Ljava/util/Map; + public fun hashCode ()I public fun serialize (Lio/sentry/JsonObjectWriter;Lio/sentry/ILogger;)V public fun setAppBuild (Ljava/lang/String;)V public fun setAppIdentifier (Ljava/lang/String;)V @@ -2600,9 +2704,11 @@ public final class io/sentry/protocol/App$JsonKeys { public final class io/sentry/protocol/Browser : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public static final field TYPE Ljava/lang/String; public fun ()V + public fun equals (Ljava/lang/Object;)Z public fun getName ()Ljava/lang/String; public fun getUnknown ()Ljava/util/Map; public fun getVersion ()Ljava/lang/String; + public fun hashCode ()I public fun serialize (Lio/sentry/JsonObjectWriter;Lio/sentry/ILogger;)V public fun setName (Ljava/lang/String;)V public fun setUnknown (Ljava/util/Map;)V @@ -2721,6 +2827,7 @@ public final class io/sentry/protocol/DebugMeta$JsonKeys { public final class io/sentry/protocol/Device : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public static final field TYPE Ljava/lang/String; public fun ()V + public fun equals (Ljava/lang/Object;)Z public fun getArchs ()[Ljava/lang/String; public fun getBatteryLevel ()Ljava/lang/Float; public fun getBatteryTemperature ()Ljava/lang/Float; @@ -2749,6 +2856,7 @@ public final class io/sentry/protocol/Device : io/sentry/JsonSerializable, io/se public fun getTimezone ()Ljava/util/TimeZone; public fun getUnknown ()Ljava/util/Map; public fun getUsableMemory ()Ljava/lang/Long; + public fun hashCode ()I public fun isCharging ()Ljava/lang/Boolean; public fun isLowMemory ()Ljava/lang/Boolean; public fun isOnline ()Ljava/lang/Boolean; @@ -2846,6 +2954,7 @@ public final class io/sentry/protocol/Device$JsonKeys { public final class io/sentry/protocol/Gpu : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public static final field TYPE Ljava/lang/String; public fun ()V + public fun equals (Ljava/lang/Object;)Z public fun getApiType ()Ljava/lang/String; public fun getId ()Ljava/lang/Integer; public fun getMemorySize ()Ljava/lang/Integer; @@ -2855,6 +2964,7 @@ public final class io/sentry/protocol/Gpu : io/sentry/JsonSerializable, io/sentr public fun getVendorId ()Ljava/lang/String; public fun getVendorName ()Ljava/lang/String; public fun getVersion ()Ljava/lang/String; + public fun hashCode ()I public fun isMultiThreadedRendering ()Ljava/lang/Boolean; public fun serialize (Lio/sentry/JsonObjectWriter;Lio/sentry/ILogger;)V public fun setApiType (Ljava/lang/String;)V @@ -2978,12 +3088,14 @@ public final class io/sentry/protocol/Message$JsonKeys { public final class io/sentry/protocol/OperatingSystem : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public static final field TYPE Ljava/lang/String; public fun ()V + public fun equals (Ljava/lang/Object;)Z public fun getBuild ()Ljava/lang/String; public fun getKernelVersion ()Ljava/lang/String; public fun getName ()Ljava/lang/String; public fun getRawDescription ()Ljava/lang/String; public fun getUnknown ()Ljava/util/Map; public fun getVersion ()Ljava/lang/String; + public fun hashCode ()I public fun isRooted ()Ljava/lang/Boolean; public fun serialize (Lio/sentry/JsonObjectWriter;Lio/sentry/ILogger;)V public fun setBuild (Ljava/lang/String;)V @@ -3014,6 +3126,7 @@ public final class io/sentry/protocol/OperatingSystem$JsonKeys { public final class io/sentry/protocol/Request : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun ()V public fun (Lio/sentry/protocol/Request;)V + public fun equals (Ljava/lang/Object;)Z public fun getBodySize ()Ljava/lang/Long; public fun getCookies ()Ljava/lang/String; public fun getData ()Ljava/lang/Object; @@ -3025,6 +3138,7 @@ public final class io/sentry/protocol/Request : io/sentry/JsonSerializable, io/s public fun getQueryString ()Ljava/lang/String; public fun getUnknown ()Ljava/util/Map; public fun getUrl ()Ljava/lang/String; + public fun hashCode ()I public fun serialize (Lio/sentry/JsonObjectWriter;Lio/sentry/ILogger;)V public fun setBodySize (Ljava/lang/Long;)V public fun setCookies (Ljava/lang/String;)V @@ -3123,6 +3237,7 @@ public final class io/sentry/protocol/SdkVersion : io/sentry/JsonSerializable, i public fun (Ljava/lang/String;Ljava/lang/String;)V public fun addIntegration (Ljava/lang/String;)V public fun addPackage (Ljava/lang/String;Ljava/lang/String;)V + public fun equals (Ljava/lang/Object;)Z public fun getIntegrationSet ()Ljava/util/Set; public fun getIntegrations ()Ljava/util/List; public fun getName ()Ljava/lang/String; @@ -3130,6 +3245,7 @@ public final class io/sentry/protocol/SdkVersion : io/sentry/JsonSerializable, i public fun getPackages ()Ljava/util/List; public fun getUnknown ()Ljava/util/Map; public fun getVersion ()Ljava/lang/String; + public fun hashCode ()I public fun serialize (Lio/sentry/JsonObjectWriter;Lio/sentry/ILogger;)V public fun setName (Ljava/lang/String;)V public fun setUnknown (Ljava/util/Map;)V @@ -3504,6 +3620,7 @@ public final class io/sentry/protocol/TransactionNameSource : java/lang/Enum { public final class io/sentry/protocol/User : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun ()V public fun (Lio/sentry/protocol/User;)V + public fun equals (Ljava/lang/Object;)Z public fun getData ()Ljava/util/Map; public fun getEmail ()Ljava/lang/String; public fun getId ()Ljava/lang/String; @@ -3512,6 +3629,7 @@ public final class io/sentry/protocol/User : io/sentry/JsonSerializable, io/sent public fun getSegment ()Ljava/lang/String; public fun getUnknown ()Ljava/util/Map; public fun getUsername ()Ljava/lang/String; + public fun hashCode ()I public fun serialize (Lio/sentry/JsonObjectWriter;Lio/sentry/ILogger;)V public fun setData (Ljava/util/Map;)V public fun setEmail (Ljava/lang/String;)V diff --git a/sentry/src/main/java/io/sentry/BackfillingEventProcessor.java b/sentry/src/main/java/io/sentry/BackfillingEventProcessor.java new file mode 100644 index 0000000000..2d8d7bc557 --- /dev/null +++ b/sentry/src/main/java/io/sentry/BackfillingEventProcessor.java @@ -0,0 +1,8 @@ +package io.sentry; + +/** + * Marker interface for event processors that process events that have to be backfilled, i.e. + * currently stored in-memory data (like Scope or SentryOptions) is irrelevant, because the event + * happened in the past. + */ +public interface BackfillingEventProcessor extends EventProcessor {} diff --git a/sentry/src/main/java/io/sentry/Breadcrumb.java b/sentry/src/main/java/io/sentry/Breadcrumb.java index 868360c664..07502d7bf9 100644 --- a/sentry/src/main/java/io/sentry/Breadcrumb.java +++ b/sentry/src/main/java/io/sentry/Breadcrumb.java @@ -1,6 +1,7 @@ package io.sentry; import io.sentry.util.CollectionUtils; +import io.sentry.util.Objects; import io.sentry.util.UrlUtils; import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; @@ -434,6 +435,24 @@ public void setLevel(@Nullable SentryLevel level) { this.level = level; } + @SuppressWarnings("JavaUtilDate") + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Breadcrumb that = (Breadcrumb) o; + return timestamp.getTime() == that.timestamp.getTime() + && Objects.equals(message, that.message) + && Objects.equals(type, that.type) + && Objects.equals(category, that.category) + && level == that.level; + } + + @Override + public int hashCode() { + return Objects.hash(timestamp, message, type, category, level); + } + // region json @Nullable diff --git a/sentry/src/main/java/io/sentry/IOptionsObserver.java b/sentry/src/main/java/io/sentry/IOptionsObserver.java new file mode 100644 index 0000000000..519e9222b5 --- /dev/null +++ b/sentry/src/main/java/io/sentry/IOptionsObserver.java @@ -0,0 +1,25 @@ +package io.sentry; + +import io.sentry.protocol.SdkVersion; +import java.util.Map; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * A SentryOptions observer that tracks changes of SentryOptions. All methods are "default", so + * implementors can subscribe to only those properties, that they are interested in. + */ +public interface IOptionsObserver { + + default void setRelease(@Nullable String release) {} + + default void setProguardUuid(@Nullable String proguardUuid) {} + + default void setSdkVersion(@Nullable SdkVersion sdkVersion) {} + + default void setEnvironment(@Nullable String environment) {} + + default void setDist(@Nullable String dist) {} + + default void setTags(@NotNull Map tags) {} +} diff --git a/sentry/src/main/java/io/sentry/IScopeObserver.java b/sentry/src/main/java/io/sentry/IScopeObserver.java index 1efd6d7cff..d8d8bc68e6 100644 --- a/sentry/src/main/java/io/sentry/IScopeObserver.java +++ b/sentry/src/main/java/io/sentry/IScopeObserver.java @@ -1,20 +1,45 @@ package io.sentry; +import io.sentry.protocol.Contexts; +import io.sentry.protocol.Request; import io.sentry.protocol.User; +import java.util.Collection; +import java.util.Map; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -/** Observer for the sync. of Scopes across SDKs */ +/** + * A Scope observer that tracks changes on Scope. All methods are "default", so implementors can + * subscribe to only those properties, that they are interested in. + */ public interface IScopeObserver { - void setUser(@Nullable User user); + default void setUser(@Nullable User user) {} - void addBreadcrumb(@NotNull Breadcrumb crumb); + default void addBreadcrumb(@NotNull Breadcrumb crumb) {} - void setTag(@NotNull String key, @NotNull String value); + default void setBreadcrumbs(@NotNull Collection breadcrumbs) {} - void removeTag(@NotNull String key); + default void setTag(@NotNull String key, @NotNull String value) {} - void setExtra(@NotNull String key, @NotNull String value); + default void removeTag(@NotNull String key) {} - void removeExtra(@NotNull String key); + default void setTags(@NotNull Map tags) {} + + default void setExtra(@NotNull String key, @NotNull String value) {} + + default void removeExtra(@NotNull String key) {} + + default void setExtras(@NotNull Map extras) {} + + default void setRequest(@Nullable Request request) {} + + default void setFingerprint(@NotNull Collection fingerprint) {} + + default void setLevel(@Nullable SentryLevel level) {} + + default void setContexts(@NotNull Contexts contexts) {} + + default void setTransaction(@Nullable String transaction) {} + + default void setTrace(@Nullable SpanContext spanContext) {} } diff --git a/sentry/src/main/java/io/sentry/ISerializer.java b/sentry/src/main/java/io/sentry/ISerializer.java index dd44d108cc..f74c9ab022 100644 --- a/sentry/src/main/java/io/sentry/ISerializer.java +++ b/sentry/src/main/java/io/sentry/ISerializer.java @@ -10,6 +10,12 @@ import org.jetbrains.annotations.Nullable; public interface ISerializer { + + @Nullable T deserializeCollection( + @NotNull Reader reader, + @NotNull Class clazz, + @Nullable JsonDeserializer elementDeserializer); + @Nullable T deserialize(@NotNull Reader reader, @NotNull Class clazz); @Nullable diff --git a/sentry/src/main/java/io/sentry/IpAddressUtils.java b/sentry/src/main/java/io/sentry/IpAddressUtils.java index 7562ddf7a6..5d18220e6e 100644 --- a/sentry/src/main/java/io/sentry/IpAddressUtils.java +++ b/sentry/src/main/java/io/sentry/IpAddressUtils.java @@ -7,7 +7,7 @@ @ApiStatus.Internal public final class IpAddressUtils { - static final String DEFAULT_IP_ADDRESS = "{{auto}}"; + public static final String DEFAULT_IP_ADDRESS = "{{auto}}"; private static final List DEFAULT_IP_ADDRESS_VALID_VALUES = Arrays.asList(DEFAULT_IP_ADDRESS, "{{ auto }}"); diff --git a/sentry/src/main/java/io/sentry/JsonSerializer.java b/sentry/src/main/java/io/sentry/JsonSerializer.java index e3de57e971..6923c54864 100644 --- a/sentry/src/main/java/io/sentry/JsonSerializer.java +++ b/sentry/src/main/java/io/sentry/JsonSerializer.java @@ -39,6 +39,7 @@ import java.io.StringWriter; import java.io.Writer; import java.nio.charset.Charset; +import java.util.Collection; import java.util.HashMap; import java.util.Map; import org.jetbrains.annotations.NotNull; @@ -116,6 +117,31 @@ public JsonSerializer(@NotNull SentryOptions options) { // Deserialize + @SuppressWarnings("unchecked") + @Override + public @Nullable T deserializeCollection( + @NotNull Reader reader, + @NotNull Class clazz, + @Nullable JsonDeserializer elementDeserializer) { + try { + JsonObjectReader jsonObjectReader = new JsonObjectReader(reader); + if (Collection.class.isAssignableFrom(clazz)) { + if (elementDeserializer == null) { + // if the object has no known deserializer we do best effort and deserialize it as map + return (T) jsonObjectReader.nextObjectOrNull(); + } + + return (T) jsonObjectReader.nextList(options.getLogger(), elementDeserializer); + } else { + return (T) jsonObjectReader.nextObjectOrNull(); + } + } catch (Throwable e) { + options.getLogger().log(SentryLevel.ERROR, "Error when deserializing", e); + return null; + } + } + + @SuppressWarnings("unchecked") @Override public @Nullable T deserialize(@NotNull Reader reader, @NotNull Class clazz) { try { @@ -124,6 +150,8 @@ public JsonSerializer(@NotNull SentryOptions options) { if (deserializer != null) { Object object = deserializer.deserialize(jsonObjectReader, options.getLogger()); return clazz.cast(object); + } else if (isKnownPrimitive(clazz)) { + return (T) jsonObjectReader.nextObjectOrNull(); } else { return null; // No way to deserialize objects we don't know about. } @@ -223,4 +251,11 @@ public void serialize(@NotNull SentryEnvelope envelope, @NotNull OutputStream ou jsonObjectWriter.value(options.getLogger(), object); return stringWriter.toString(); } + + private boolean isKnownPrimitive(final @NotNull Class clazz) { + return clazz.isArray() + || Collection.class.isAssignableFrom(clazz) + || String.class.isAssignableFrom(clazz) + || Map.class.isAssignableFrom(clazz); + } } diff --git a/sentry/src/main/java/io/sentry/NoOpSerializer.java b/sentry/src/main/java/io/sentry/NoOpSerializer.java index ad517e2e5f..503c39aec3 100644 --- a/sentry/src/main/java/io/sentry/NoOpSerializer.java +++ b/sentry/src/main/java/io/sentry/NoOpSerializer.java @@ -20,6 +20,14 @@ public static NoOpSerializer getInstance() { private NoOpSerializer() {} + @Override + public @Nullable T deserializeCollection( + @NotNull Reader reader, + @NotNull Class clazz, + @Nullable JsonDeserializer elementDeserializer) { + return null; + } + @Override public @Nullable T deserialize(@NotNull Reader reader, @NotNull Class clazz) { return null; diff --git a/sentry/src/main/java/io/sentry/Scope.java b/sentry/src/main/java/io/sentry/Scope.java index ac27668611..997f93c05d 100644 --- a/sentry/src/main/java/io/sentry/Scope.java +++ b/sentry/src/main/java/io/sentry/Scope.java @@ -40,7 +40,7 @@ public final class Scope { private @NotNull List fingerprint = new ArrayList<>(); /** Scope's breadcrumb queue */ - private @NotNull Queue breadcrumbs; + private final @NotNull Queue breadcrumbs; /** Scope's tags */ private @NotNull Map tags = new ConcurrentHashMap<>(); @@ -152,6 +152,10 @@ public Scope(final @NotNull SentryOptions options) { */ public void setLevel(final @Nullable SentryLevel level) { this.level = level; + + for (final IScopeObserver observer : options.getScopeObservers()) { + observer.setLevel(level); + } } /** @@ -176,6 +180,10 @@ public void setTransaction(final @NotNull String transaction) { tx.setName(transaction, TransactionNameSource.CUSTOM); } this.transactionName = transaction; + + for (final IScopeObserver observer : options.getScopeObservers()) { + observer.setTransaction(transaction); + } } else { options.getLogger().log(SentryLevel.WARNING, "Transaction cannot be null"); } @@ -207,6 +215,16 @@ public ISpan getSpan() { public void setTransaction(final @Nullable ITransaction transaction) { synchronized (transactionLock) { this.transaction = transaction; + + for (final IScopeObserver observer : options.getScopeObservers()) { + if (transaction != null) { + observer.setTransaction(transaction.getName()); + observer.setTrace(transaction.getSpanContext()); + } else { + observer.setTransaction(null); + observer.setTrace(null); + } + } } } @@ -227,10 +245,8 @@ public void setTransaction(final @Nullable ITransaction transaction) { public void setUser(final @Nullable User user) { this.user = user; - if (options.isEnableScopeSync()) { - for (final IScopeObserver observer : options.getScopeObservers()) { - observer.setUser(user); - } + for (final IScopeObserver observer : options.getScopeObservers()) { + observer.setUser(user); } } @@ -250,6 +266,10 @@ public void setUser(final @Nullable User user) { */ public void setRequest(final @Nullable Request request) { this.request = request; + + for (final IScopeObserver observer : options.getScopeObservers()) { + observer.setRequest(request); + } } /** @@ -272,6 +292,10 @@ public void setFingerprint(final @NotNull List fingerprint) { return; } this.fingerprint = new ArrayList<>(fingerprint); + + for (final IScopeObserver observer : options.getScopeObservers()) { + observer.setFingerprint(fingerprint); + } } /** @@ -335,10 +359,9 @@ public void addBreadcrumb(@NotNull Breadcrumb breadcrumb, @Nullable Hint hint) { if (breadcrumb != null) { this.breadcrumbs.add(breadcrumb); - if (options.isEnableScopeSync()) { - for (final IScopeObserver observer : options.getScopeObservers()) { - observer.addBreadcrumb(breadcrumb); - } + for (final IScopeObserver observer : options.getScopeObservers()) { + observer.addBreadcrumb(breadcrumb); + observer.setBreadcrumbs(breadcrumbs); } } else { options.getLogger().log(SentryLevel.INFO, "Breadcrumb was dropped by beforeBreadcrumb"); @@ -358,6 +381,10 @@ public void addBreadcrumb(final @NotNull Breadcrumb breadcrumb) { /** Clear all the breadcrumbs */ public void clearBreadcrumbs() { breadcrumbs.clear(); + + for (final IScopeObserver observer : options.getScopeObservers()) { + observer.setBreadcrumbs(breadcrumbs); + } } /** Clears the transaction. */ @@ -366,6 +393,11 @@ public void clearTransaction() { transaction = null; } transactionName = null; + + for (final IScopeObserver observer : options.getScopeObservers()) { + observer.setTransaction(null); + observer.setTrace(null); + } } /** @@ -412,10 +444,9 @@ public void clear() { public void setTag(final @NotNull String key, final @NotNull String value) { this.tags.put(key, value); - if (options.isEnableScopeSync()) { - for (final IScopeObserver observer : options.getScopeObservers()) { - observer.setTag(key, value); - } + for (final IScopeObserver observer : options.getScopeObservers()) { + observer.setTag(key, value); + observer.setTags(tags); } } @@ -427,10 +458,9 @@ public void setTag(final @NotNull String key, final @NotNull String value) { public void removeTag(final @NotNull String key) { this.tags.remove(key); - if (options.isEnableScopeSync()) { - for (final IScopeObserver observer : options.getScopeObservers()) { - observer.removeTag(key); - } + for (final IScopeObserver observer : options.getScopeObservers()) { + observer.removeTag(key); + observer.setTags(tags); } } @@ -453,10 +483,9 @@ Map getExtras() { public void setExtra(final @NotNull String key, final @NotNull String value) { this.extra.put(key, value); - if (options.isEnableScopeSync()) { - for (final IScopeObserver observer : options.getScopeObservers()) { - observer.setExtra(key, value); - } + for (final IScopeObserver observer : options.getScopeObservers()) { + observer.setExtra(key, value); + observer.setExtras(extra); } } @@ -468,10 +497,9 @@ public void setExtra(final @NotNull String key, final @NotNull String value) { public void removeExtra(final @NotNull String key) { this.extra.remove(key); - if (options.isEnableScopeSync()) { - for (final IScopeObserver observer : options.getScopeObservers()) { - observer.removeExtra(key); - } + for (final IScopeObserver observer : options.getScopeObservers()) { + observer.removeExtra(key); + observer.setExtras(extra); } } @@ -492,6 +520,10 @@ public void removeExtra(final @NotNull String key) { */ public void setContexts(final @NotNull String key, final @NotNull Object value) { this.contexts.put(key, value); + + for (final IScopeObserver observer : options.getScopeObservers()) { + observer.setContexts(contexts); + } } /** diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 4b420a19a7..27abd369fc 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -223,6 +223,33 @@ private static synchronized void init( for (final Integration integration : options.getIntegrations()) { integration.register(HubAdapter.getInstance(), options); } + + notifyOptionsObservers(options); + } + + @SuppressWarnings("FutureReturnValueIgnored") + private static void notifyOptionsObservers(final @NotNull SentryOptions options) { + // enqueue a task to trigger the static options change for the observers. Since the executor + // is single-threaded, this task will be enqueued sequentially after all integrations that rely + // on the observers have done their work, even if they do that async. + try { + options + .getExecutorService() + .submit( + () -> { + // for static things like sentry options we can immediately trigger observers + for (final IOptionsObserver observer : options.getOptionsObservers()) { + observer.setRelease(options.getRelease()); + observer.setProguardUuid(options.getProguardUuid()); + observer.setSdkVersion(options.getSdkVersion()); + observer.setDist(options.getDist()); + observer.setEnvironment(options.getEnvironment()); + observer.setTags(options.getTags()); + } + }); + } catch (Throwable e) { + options.getLogger().log(SentryLevel.DEBUG, "Failed to notify options observers.", e); + } } @SuppressWarnings("FutureReturnValueIgnored") diff --git a/sentry/src/main/java/io/sentry/SentryBaseEvent.java b/sentry/src/main/java/io/sentry/SentryBaseEvent.java index f29a92b147..ed0593f9a5 100644 --- a/sentry/src/main/java/io/sentry/SentryBaseEvent.java +++ b/sentry/src/main/java/io/sentry/SentryBaseEvent.java @@ -290,7 +290,7 @@ public void setDebugMeta(final @Nullable DebugMeta debugMeta) { } @Nullable - Map getExtras() { + public Map getExtras() { return extra; } diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index c5a5299bc1..a34af31801 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -3,7 +3,7 @@ import io.sentry.clientreport.DiscardReason; import io.sentry.exception.SentryEnvelopeException; import io.sentry.hints.AbnormalExit; -import io.sentry.hints.DiskFlushNotification; +import io.sentry.hints.Backfillable; import io.sentry.protocol.Contexts; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; @@ -304,7 +304,15 @@ private SentryEvent processEvent( final @NotNull List eventProcessors) { for (final EventProcessor processor : eventProcessors) { try { - event = processor.process(event, hint); + // only wire backfillable events through the backfilling processors, skip from others, and + // the other way around + final boolean isBackfillingProcessor = processor instanceof BackfillingEventProcessor; + final boolean isBackfillable = HintUtils.hasType(hint, Backfillable.class); + if (isBackfillable && isBackfillingProcessor) { + event = processor.process(event, hint); + } else if (!isBackfillable && !isBackfillingProcessor) { + event = processor.process(event, hint); + } } catch (Throwable e) { options .getLogger() @@ -448,9 +456,9 @@ Session updateSessionData( } if (session.update(status, userAgent, crashedOrErrored, abnormalMechanism)) { - // if hint is DiskFlushNotification, it means we have an uncaughtException - // and we can end the session. - if (HintUtils.hasType(hint, DiskFlushNotification.class)) { + // if we have an uncaughtExceptionHint we can end the session. + if (HintUtils.hasType( + hint, UncaughtExceptionHandlerIntegration.UncaughtExceptionHint.class)) { session.end(); } } diff --git a/sentry/src/main/java/io/sentry/SentryEvent.java b/sentry/src/main/java/io/sentry/SentryEvent.java index 86ff9bc880..8eeea44028 100644 --- a/sentry/src/main/java/io/sentry/SentryEvent.java +++ b/sentry/src/main/java/io/sentry/SentryEvent.java @@ -112,6 +112,10 @@ public Date getTimestamp() { return (Date) timestamp.clone(); } + public void setTimestamp(final @NotNull Date timestamp) { + this.timestamp = timestamp; + } + public @Nullable Message getMessage() { return message; } diff --git a/sentry/src/main/java/io/sentry/SentryExceptionFactory.java b/sentry/src/main/java/io/sentry/SentryExceptionFactory.java index 84755dcb26..49be910044 100644 --- a/sentry/src/main/java/io/sentry/SentryExceptionFactory.java +++ b/sentry/src/main/java/io/sentry/SentryExceptionFactory.java @@ -12,12 +12,14 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; /** class responsible for converting Java Throwable to SentryExceptions */ -final class SentryExceptionFactory { +@ApiStatus.Internal +public final class SentryExceptionFactory { /** the SentryStackTraceFactory */ private final @NotNull SentryStackTraceFactory sentryStackTraceFactory; @@ -38,7 +40,7 @@ public SentryExceptionFactory(final @NotNull SentryStackTraceFactory sentryStack * @param throwable the {@link Throwable} to build this instance from */ @NotNull - List getSentryExceptions(final @NotNull Throwable throwable) { + public List getSentryExceptions(final @NotNull Throwable throwable) { return getSentryExceptions(extractExceptionQueue(throwable)); } diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 0681ae6fa5..b90105fd8d 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -273,7 +273,9 @@ public class SentryOptions { private @Nullable SSLSocketFactory sslSocketFactory; /** list of scope observers */ - private final @NotNull List observers = new ArrayList<>(); + private final @NotNull List observers = new CopyOnWriteArrayList<>(); + + private final @NotNull List optionsObservers = new CopyOnWriteArrayList<>(); /** * Enable the Java to NDK Scope sync. The default value for sentry-java is disabled and enabled @@ -1338,10 +1340,29 @@ public void addScopeObserver(final @NotNull IScopeObserver observer) { * @return the Scope observer list */ @NotNull - List getScopeObservers() { + public List getScopeObservers() { return observers; } + /** + * Adds a SentryOptions observer + * + * @param observer the Observer + */ + public void addOptionsObserver(final @NotNull IOptionsObserver observer) { + optionsObservers.add(observer); + } + + /** + * Returns the list of SentryOptions observers + * + * @return the SentryOptions observer list + */ + @NotNull + public List getOptionsObservers() { + return optionsObservers; + } + /** * Returns if the Java to NDK Scope sync is enabled * diff --git a/sentry/src/main/java/io/sentry/SentryThreadFactory.java b/sentry/src/main/java/io/sentry/SentryThreadFactory.java index a93b04d9c3..9d27741d53 100644 --- a/sentry/src/main/java/io/sentry/SentryThreadFactory.java +++ b/sentry/src/main/java/io/sentry/SentryThreadFactory.java @@ -8,12 +8,14 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; /** class responsible for converting Java Threads to SentryThreads */ -final class SentryThreadFactory { +@ApiStatus.Internal +public final class SentryThreadFactory { /** the SentryStackTraceFactory */ private final @NotNull SentryStackTraceFactory sentryStackTraceFactory; diff --git a/sentry/src/main/java/io/sentry/SpanContext.java b/sentry/src/main/java/io/sentry/SpanContext.java index 6b7a23adb4..7b387a8fb2 100644 --- a/sentry/src/main/java/io/sentry/SpanContext.java +++ b/sentry/src/main/java/io/sentry/SpanContext.java @@ -200,6 +200,24 @@ public void setSamplingDecision(final @Nullable TracesSamplingDecision samplingD this.samplingDecision = samplingDecision; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SpanContext)) return false; + SpanContext that = (SpanContext) o; + return traceId.equals(that.traceId) + && spanId.equals(that.spanId) + && Objects.equals(parentSpanId, that.parentSpanId) + && op.equals(that.op) + && Objects.equals(description, that.description) + && status == that.status; + } + + @Override + public int hashCode() { + return Objects.hash(traceId, spanId, parentSpanId, op, description, status); + } + // region JsonSerializable public static final class JsonKeys { diff --git a/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java b/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java index 746be67971..6924d08fdb 100644 --- a/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java +++ b/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java @@ -1,18 +1,15 @@ package io.sentry; -import static io.sentry.SentryLevel.ERROR; - +import com.jakewharton.nopen.annotation.Open; import io.sentry.exception.ExceptionMechanismException; -import io.sentry.hints.DiskFlushNotification; -import io.sentry.hints.Flushable; +import io.sentry.hints.BlockingFlushHint; import io.sentry.hints.SessionEnd; import io.sentry.protocol.Mechanism; import io.sentry.protocol.SentryId; import io.sentry.util.HintUtils; import io.sentry.util.Objects; import java.io.Closeable; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; @@ -150,33 +147,12 @@ public void close() { } } - private static final class UncaughtExceptionHint - implements DiskFlushNotification, Flushable, SessionEnd { - - private final CountDownLatch latch; - private final long flushTimeoutMillis; - private final @NotNull ILogger logger; - - UncaughtExceptionHint(final long flushTimeoutMillis, final @NotNull ILogger logger) { - this.flushTimeoutMillis = flushTimeoutMillis; - latch = new CountDownLatch(1); - this.logger = logger; - } - - @Override - public boolean waitFlush() { - try { - return latch.await(flushTimeoutMillis, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - logger.log(ERROR, "Exception while awaiting for flush in UncaughtExceptionHint", e); - } - return false; - } + @Open // open for tests + @ApiStatus.Internal + public static class UncaughtExceptionHint extends BlockingFlushHint implements SessionEnd { - @Override - public void markFlushed() { - latch.countDown(); + public UncaughtExceptionHint(final long flushTimeoutMillis, final @NotNull ILogger logger) { + super(flushTimeoutMillis, logger); } } } diff --git a/sentry/src/main/java/io/sentry/cache/CacheUtils.java b/sentry/src/main/java/io/sentry/cache/CacheUtils.java new file mode 100644 index 0000000000..1eb5f7e19f --- /dev/null +++ b/sentry/src/main/java/io/sentry/cache/CacheUtils.java @@ -0,0 +1,115 @@ +package io.sentry.cache; + +import static io.sentry.SentryLevel.DEBUG; +import static io.sentry.SentryLevel.ERROR; +import static io.sentry.SentryLevel.INFO; + +import io.sentry.JsonDeserializer; +import io.sentry.SentryLevel; +import io.sentry.SentryOptions; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.nio.charset.Charset; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +final class CacheUtils { + + @SuppressWarnings("CharsetObjectCanBeUsed") + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + static void store( + final @NotNull SentryOptions options, + final @NotNull T entity, + final @NotNull String dirName, + final @NotNull String fileName) { + final File cacheDir = ensureCacheDir(options, dirName); + if (cacheDir == null) { + options.getLogger().log(INFO, "Cache dir is not set, cannot store in scope cache"); + return; + } + + final File file = new File(cacheDir, fileName); + if (file.exists()) { + options.getLogger().log(DEBUG, "Overwriting %s in scope cache", fileName); + if (!file.delete()) { + options.getLogger().log(SentryLevel.ERROR, "Failed to delete: %s", file.getAbsolutePath()); + } + } + + try (final OutputStream outputStream = new FileOutputStream(file); + final Writer writer = new BufferedWriter(new OutputStreamWriter(outputStream, UTF_8))) { + options.getSerializer().serialize(entity, writer); + } catch (Throwable e) { + options.getLogger().log(ERROR, e, "Error persisting entity: %s", fileName); + } + } + + static void delete( + final @NotNull SentryOptions options, + final @NotNull String dirName, + final @NotNull String fileName) { + final File cacheDir = ensureCacheDir(options, dirName); + if (cacheDir == null) { + options.getLogger().log(INFO, "Cache dir is not set, cannot delete from scope cache"); + return; + } + + final File file = new File(cacheDir, fileName); + if (file.exists()) { + options.getLogger().log(DEBUG, "Deleting %s from scope cache", fileName); + if (!file.delete()) { + options.getLogger().log(SentryLevel.ERROR, "Failed to delete: %s", file.getAbsolutePath()); + } + } + } + + static @Nullable T read( + final @NotNull SentryOptions options, + final @NotNull String dirName, + final @NotNull String fileName, + final @NotNull Class clazz, + final @Nullable JsonDeserializer elementDeserializer) { + final File cacheDir = ensureCacheDir(options, dirName); + if (cacheDir == null) { + options.getLogger().log(INFO, "Cache dir is not set, cannot read from scope cache"); + return null; + } + + final File file = new File(cacheDir, fileName); + if (file.exists()) { + try (final Reader reader = + new BufferedReader(new InputStreamReader(new FileInputStream(file), UTF_8))) { + if (elementDeserializer == null) { + return options.getSerializer().deserialize(reader, clazz); + } else { + return options.getSerializer().deserializeCollection(reader, clazz, elementDeserializer); + } + } catch (Throwable e) { + options.getLogger().log(ERROR, e, "Error reading entity from scope cache: %s", fileName); + } + } else { + options.getLogger().log(DEBUG, "No entry stored for %s", fileName); + } + return null; + } + + private static @Nullable File ensureCacheDir( + final @NotNull SentryOptions options, final @NotNull String cacheDirName) { + final String cacheDir = options.getCacheDirPath(); + if (cacheDir == null) { + return null; + } + final File dir = new File(cacheDir, cacheDirName); + dir.mkdirs(); + return dir; + } +} diff --git a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java index 973a978fcc..2e7f71f46d 100644 --- a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java +++ b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java @@ -16,7 +16,7 @@ import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.Session; -import io.sentry.hints.DiskFlushNotification; +import io.sentry.UncaughtExceptionHandlerIntegration; import io.sentry.hints.SessionEnd; import io.sentry.hints.SessionStart; import io.sentry.transport.NoOpEnvelopeCache; @@ -204,7 +204,7 @@ public void store(final @NotNull SentryEnvelope envelope, final @NotNull Hint hi writeEnvelopeToDisk(envelopeFile, envelope); // write file to the disk when its about to crash so crashedLastRun can be marked on restart - if (HintUtils.hasType(hint, DiskFlushNotification.class)) { + if (HintUtils.hasType(hint, UncaughtExceptionHandlerIntegration.UncaughtExceptionHint.class)) { writeCrashMarkerFile(); } } diff --git a/sentry/src/main/java/io/sentry/cache/PersistingOptionsObserver.java b/sentry/src/main/java/io/sentry/cache/PersistingOptionsObserver.java new file mode 100644 index 0000000000..296b602534 --- /dev/null +++ b/sentry/src/main/java/io/sentry/cache/PersistingOptionsObserver.java @@ -0,0 +1,133 @@ +package io.sentry.cache; + +import static io.sentry.SentryLevel.ERROR; + +import io.sentry.IOptionsObserver; +import io.sentry.JsonDeserializer; +import io.sentry.SentryOptions; +import io.sentry.protocol.SdkVersion; +import java.util.Map; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class PersistingOptionsObserver implements IOptionsObserver { + public static final String OPTIONS_CACHE = ".options-cache"; + public static final String RELEASE_FILENAME = "release.json"; + public static final String PROGUARD_UUID_FILENAME = "proguard-uuid.json"; + public static final String SDK_VERSION_FILENAME = "sdk-version.json"; + public static final String ENVIRONMENT_FILENAME = "environment.json"; + public static final String DIST_FILENAME = "dist.json"; + public static final String TAGS_FILENAME = "tags.json"; + + private final @NotNull SentryOptions options; + + public PersistingOptionsObserver(final @NotNull SentryOptions options) { + this.options = options; + } + + @SuppressWarnings("FutureReturnValueIgnored") + private void serializeToDisk(final @NotNull Runnable task) { + try { + options + .getExecutorService() + .submit( + () -> { + try { + task.run(); + } catch (Throwable e) { + options.getLogger().log(ERROR, "Serialization task failed", e); + } + }); + } catch (Throwable e) { + options.getLogger().log(ERROR, "Serialization task could not be scheduled", e); + } + } + + @Override + public void setRelease(@Nullable String release) { + serializeToDisk( + () -> { + if (release == null) { + delete(RELEASE_FILENAME); + } else { + store(release, RELEASE_FILENAME); + } + }); + } + + @Override + public void setProguardUuid(@Nullable String proguardUuid) { + serializeToDisk( + () -> { + if (proguardUuid == null) { + delete(PROGUARD_UUID_FILENAME); + } else { + store(proguardUuid, PROGUARD_UUID_FILENAME); + } + }); + } + + @Override + public void setSdkVersion(@Nullable SdkVersion sdkVersion) { + serializeToDisk( + () -> { + if (sdkVersion == null) { + delete(SDK_VERSION_FILENAME); + } else { + store(sdkVersion, SDK_VERSION_FILENAME); + } + }); + } + + @Override + public void setDist(@Nullable String dist) { + serializeToDisk( + () -> { + if (dist == null) { + delete(DIST_FILENAME); + } else { + store(dist, DIST_FILENAME); + } + }); + } + + @Override + public void setEnvironment(@Nullable String environment) { + serializeToDisk( + () -> { + if (environment == null) { + delete(ENVIRONMENT_FILENAME); + } else { + store(environment, ENVIRONMENT_FILENAME); + } + }); + } + + @Override + public void setTags(@NotNull Map tags) { + serializeToDisk(() -> store(tags, TAGS_FILENAME)); + } + + private void store(final @NotNull T entity, final @NotNull String fileName) { + CacheUtils.store(options, entity, OPTIONS_CACHE, fileName); + } + + private void delete(final @NotNull String fileName) { + CacheUtils.delete(options, OPTIONS_CACHE, fileName); + } + + public static @Nullable T read( + final @NotNull SentryOptions options, + final @NotNull String fileName, + final @NotNull Class clazz) { + return read(options, fileName, clazz, null); + } + + public static @Nullable T read( + final @NotNull SentryOptions options, + final @NotNull String fileName, + final @NotNull Class clazz, + final @Nullable JsonDeserializer elementDeserializer) { + return CacheUtils.read(options, OPTIONS_CACHE, fileName, clazz, elementDeserializer); + } +} diff --git a/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java b/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java new file mode 100644 index 0000000000..bfccc8d30f --- /dev/null +++ b/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java @@ -0,0 +1,164 @@ +package io.sentry.cache; + +import static io.sentry.SentryLevel.ERROR; + +import io.sentry.Breadcrumb; +import io.sentry.IScopeObserver; +import io.sentry.JsonDeserializer; +import io.sentry.SentryLevel; +import io.sentry.SentryOptions; +import io.sentry.SpanContext; +import io.sentry.protocol.Contexts; +import io.sentry.protocol.Request; +import io.sentry.protocol.User; +import java.util.Collection; +import java.util.Map; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class PersistingScopeObserver implements IScopeObserver { + + public static final String SCOPE_CACHE = ".scope-cache"; + public static final String USER_FILENAME = "user.json"; + public static final String BREADCRUMBS_FILENAME = "breadcrumbs.json"; + public static final String TAGS_FILENAME = "tags.json"; + public static final String EXTRAS_FILENAME = "extras.json"; + public static final String CONTEXTS_FILENAME = "contexts.json"; + public static final String REQUEST_FILENAME = "request.json"; + public static final String LEVEL_FILENAME = "level.json"; + public static final String FINGERPRINT_FILENAME = "fingerprint.json"; + public static final String TRANSACTION_FILENAME = "transaction.json"; + public static final String TRACE_FILENAME = "trace.json"; + + private final @NotNull SentryOptions options; + + public PersistingScopeObserver(final @NotNull SentryOptions options) { + this.options = options; + } + + @Override + public void setUser(final @Nullable User user) { + serializeToDisk( + () -> { + if (user == null) { + delete(USER_FILENAME); + } else { + store(user, USER_FILENAME); + } + }); + } + + @Override + public void setBreadcrumbs(@NotNull Collection breadcrumbs) { + serializeToDisk(() -> store(breadcrumbs, BREADCRUMBS_FILENAME)); + } + + @Override + public void setTags(@NotNull Map tags) { + serializeToDisk(() -> store(tags, TAGS_FILENAME)); + } + + @Override + public void setExtras(@NotNull Map extras) { + serializeToDisk(() -> store(extras, EXTRAS_FILENAME)); + } + + @Override + public void setRequest(@Nullable Request request) { + serializeToDisk( + () -> { + if (request == null) { + delete(REQUEST_FILENAME); + } else { + store(request, REQUEST_FILENAME); + } + }); + } + + @Override + public void setFingerprint(@NotNull Collection fingerprint) { + serializeToDisk(() -> store(fingerprint, FINGERPRINT_FILENAME)); + } + + @Override + public void setLevel(@Nullable SentryLevel level) { + serializeToDisk( + () -> { + if (level == null) { + delete(LEVEL_FILENAME); + } else { + store(level, LEVEL_FILENAME); + } + }); + } + + @Override + public void setTransaction(@Nullable String transaction) { + serializeToDisk( + () -> { + if (transaction == null) { + delete(TRANSACTION_FILENAME); + } else { + store(transaction, TRANSACTION_FILENAME); + } + }); + } + + @Override + public void setTrace(@Nullable SpanContext spanContext) { + serializeToDisk( + () -> { + if (spanContext == null) { + delete(TRACE_FILENAME); + } else { + store(spanContext, TRACE_FILENAME); + } + }); + } + + @Override + public void setContexts(@NotNull Contexts contexts) { + serializeToDisk(() -> store(contexts, CONTEXTS_FILENAME)); + } + + @SuppressWarnings("FutureReturnValueIgnored") + private void serializeToDisk(final @NotNull Runnable task) { + try { + options + .getExecutorService() + .submit( + () -> { + try { + task.run(); + } catch (Throwable e) { + options.getLogger().log(ERROR, "Serialization task failed", e); + } + }); + } catch (Throwable e) { + options.getLogger().log(ERROR, "Serialization task could not be scheduled", e); + } + } + + private void store(final @NotNull T entity, final @NotNull String fileName) { + CacheUtils.store(options, entity, SCOPE_CACHE, fileName); + } + + private void delete(final @NotNull String fileName) { + CacheUtils.delete(options, SCOPE_CACHE, fileName); + } + + public static @Nullable T read( + final @NotNull SentryOptions options, + final @NotNull String fileName, + final @NotNull Class clazz) { + return read(options, fileName, clazz, null); + } + + public static @Nullable T read( + final @NotNull SentryOptions options, + final @NotNull String fileName, + final @NotNull Class clazz, + final @Nullable JsonDeserializer elementDeserializer) { + return CacheUtils.read(options, SCOPE_CACHE, fileName, clazz, elementDeserializer); + } +} diff --git a/sentry/src/main/java/io/sentry/hints/Backfillable.java b/sentry/src/main/java/io/sentry/hints/Backfillable.java new file mode 100644 index 0000000000..26015f5d9b --- /dev/null +++ b/sentry/src/main/java/io/sentry/hints/Backfillable.java @@ -0,0 +1,9 @@ +package io.sentry.hints; + +/** + * Marker interface for events that have to be backfilled with the event data (contexts, tags, etc.) + * that is persisted on disk between application launches + */ +public interface Backfillable { + boolean shouldEnrich(); +} diff --git a/sentry/src/main/java/io/sentry/hints/BlockingFlushHint.java b/sentry/src/main/java/io/sentry/hints/BlockingFlushHint.java new file mode 100644 index 0000000000..476d6ce8bc --- /dev/null +++ b/sentry/src/main/java/io/sentry/hints/BlockingFlushHint.java @@ -0,0 +1,39 @@ +package io.sentry.hints; + +import static io.sentry.SentryLevel.ERROR; + +import io.sentry.ILogger; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public abstract class BlockingFlushHint implements DiskFlushNotification, Flushable { + + private final CountDownLatch latch; + private final long flushTimeoutMillis; + private final @NotNull ILogger logger; + + public BlockingFlushHint(final long flushTimeoutMillis, final @NotNull ILogger logger) { + this.flushTimeoutMillis = flushTimeoutMillis; + latch = new CountDownLatch(1); + this.logger = logger; + } + + @Override + public boolean waitFlush() { + try { + return latch.await(flushTimeoutMillis, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.log(ERROR, "Exception while awaiting for flush in BlockingFlushHint", e); + } + return false; + } + + @Override + public void markFlushed() { + latch.countDown(); + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/App.java b/sentry/src/main/java/io/sentry/protocol/App.java index aa76f567fc..4ace02b52e 100644 --- a/sentry/src/main/java/io/sentry/protocol/App.java +++ b/sentry/src/main/java/io/sentry/protocol/App.java @@ -7,6 +7,7 @@ import io.sentry.JsonSerializable; import io.sentry.JsonUnknown; import io.sentry.util.CollectionUtils; +import io.sentry.util.Objects; import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; import java.util.Date; @@ -137,6 +138,26 @@ public void setInForeground(final @Nullable Boolean inForeground) { this.inForeground = inForeground; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + App app = (App) o; + return Objects.equals(appIdentifier, app.appIdentifier) + && Objects.equals(appStartTime, app.appStartTime) + && Objects.equals(deviceAppHash, app.deviceAppHash) + && Objects.equals(buildType, app.buildType) + && Objects.equals(appName, app.appName) + && Objects.equals(appVersion, app.appVersion) + && Objects.equals(appBuild, app.appBuild); + } + + @Override + public int hashCode() { + return Objects.hash( + appIdentifier, appStartTime, deviceAppHash, buildType, appName, appVersion, appBuild); + } + // region json @Nullable diff --git a/sentry/src/main/java/io/sentry/protocol/Browser.java b/sentry/src/main/java/io/sentry/protocol/Browser.java index cd5d3df5e1..260ef1ec64 100644 --- a/sentry/src/main/java/io/sentry/protocol/Browser.java +++ b/sentry/src/main/java/io/sentry/protocol/Browser.java @@ -7,6 +7,7 @@ import io.sentry.JsonSerializable; import io.sentry.JsonUnknown; import io.sentry.util.CollectionUtils; +import io.sentry.util.Objects; import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; import java.util.Map; @@ -48,6 +49,19 @@ public void setVersion(final @Nullable String version) { this.version = version; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Browser browser = (Browser) o; + return Objects.equals(name, browser.name) && Objects.equals(version, browser.version); + } + + @Override + public int hashCode() { + return Objects.hash(name, version); + } + // region json @Nullable diff --git a/sentry/src/main/java/io/sentry/protocol/Device.java b/sentry/src/main/java/io/sentry/protocol/Device.java index 2ca24bc5f2..296b81d1af 100644 --- a/sentry/src/main/java/io/sentry/protocol/Device.java +++ b/sentry/src/main/java/io/sentry/protocol/Device.java @@ -7,8 +7,10 @@ import io.sentry.JsonSerializable; import io.sentry.JsonUnknown; import io.sentry.util.CollectionUtils; +import io.sentry.util.Objects; import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; +import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Locale; @@ -403,6 +405,81 @@ public void setBatteryTemperature(final @Nullable Float batteryTemperature) { this.batteryTemperature = batteryTemperature; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Device device = (Device) o; + return Objects.equals(name, device.name) + && Objects.equals(manufacturer, device.manufacturer) + && Objects.equals(brand, device.brand) + && Objects.equals(family, device.family) + && Objects.equals(model, device.model) + && Objects.equals(modelId, device.modelId) + && Arrays.equals(archs, device.archs) + && Objects.equals(batteryLevel, device.batteryLevel) + && Objects.equals(charging, device.charging) + && Objects.equals(online, device.online) + && orientation == device.orientation + && Objects.equals(simulator, device.simulator) + && Objects.equals(memorySize, device.memorySize) + && Objects.equals(freeMemory, device.freeMemory) + && Objects.equals(usableMemory, device.usableMemory) + && Objects.equals(lowMemory, device.lowMemory) + && Objects.equals(storageSize, device.storageSize) + && Objects.equals(freeStorage, device.freeStorage) + && Objects.equals(externalStorageSize, device.externalStorageSize) + && Objects.equals(externalFreeStorage, device.externalFreeStorage) + && Objects.equals(screenWidthPixels, device.screenWidthPixels) + && Objects.equals(screenHeightPixels, device.screenHeightPixels) + && Objects.equals(screenDensity, device.screenDensity) + && Objects.equals(screenDpi, device.screenDpi) + && Objects.equals(bootTime, device.bootTime) + && Objects.equals(id, device.id) + && Objects.equals(language, device.language) + && Objects.equals(locale, device.locale) + && Objects.equals(connectionType, device.connectionType) + && Objects.equals(batteryTemperature, device.batteryTemperature); + } + + @Override + public int hashCode() { + int result = + Objects.hash( + name, + manufacturer, + brand, + family, + model, + modelId, + batteryLevel, + charging, + online, + orientation, + simulator, + memorySize, + freeMemory, + usableMemory, + lowMemory, + storageSize, + freeStorage, + externalStorageSize, + externalFreeStorage, + screenWidthPixels, + screenHeightPixels, + screenDensity, + screenDpi, + bootTime, + timezone, + id, + language, + locale, + connectionType, + batteryTemperature); + result = 31 * result + Arrays.hashCode(archs); + return result; + } + public enum DeviceOrientation implements JsonSerializable { PORTRAIT, LANDSCAPE; diff --git a/sentry/src/main/java/io/sentry/protocol/Gpu.java b/sentry/src/main/java/io/sentry/protocol/Gpu.java index e00462fff6..b8a5a1e3bc 100644 --- a/sentry/src/main/java/io/sentry/protocol/Gpu.java +++ b/sentry/src/main/java/io/sentry/protocol/Gpu.java @@ -7,6 +7,7 @@ import io.sentry.JsonSerializable; import io.sentry.JsonUnknown; import io.sentry.util.CollectionUtils; +import io.sentry.util.Objects; import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; import java.util.Map; @@ -130,6 +131,36 @@ public void setNpotSupport(final @Nullable String npotSupport) { this.npotSupport = npotSupport; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Gpu gpu = (Gpu) o; + return Objects.equals(name, gpu.name) + && Objects.equals(id, gpu.id) + && Objects.equals(vendorId, gpu.vendorId) + && Objects.equals(vendorName, gpu.vendorName) + && Objects.equals(memorySize, gpu.memorySize) + && Objects.equals(apiType, gpu.apiType) + && Objects.equals(multiThreadedRendering, gpu.multiThreadedRendering) + && Objects.equals(version, gpu.version) + && Objects.equals(npotSupport, gpu.npotSupport); + } + + @Override + public int hashCode() { + return Objects.hash( + name, + id, + vendorId, + vendorName, + memorySize, + apiType, + multiThreadedRendering, + version, + npotSupport); + } + // region JsonSerializable public static final class JsonKeys { diff --git a/sentry/src/main/java/io/sentry/protocol/OperatingSystem.java b/sentry/src/main/java/io/sentry/protocol/OperatingSystem.java index a305a17ecc..6a8107aaa0 100644 --- a/sentry/src/main/java/io/sentry/protocol/OperatingSystem.java +++ b/sentry/src/main/java/io/sentry/protocol/OperatingSystem.java @@ -7,6 +7,7 @@ import io.sentry.JsonSerializable; import io.sentry.JsonUnknown; import io.sentry.util.CollectionUtils; +import io.sentry.util.Objects; import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; import java.util.Map; @@ -103,6 +104,24 @@ public void setRooted(final @Nullable Boolean rooted) { this.rooted = rooted; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OperatingSystem that = (OperatingSystem) o; + return Objects.equals(name, that.name) + && Objects.equals(version, that.version) + && Objects.equals(rawDescription, that.rawDescription) + && Objects.equals(build, that.build) + && Objects.equals(kernelVersion, that.kernelVersion) + && Objects.equals(rooted, that.rooted); + } + + @Override + public int hashCode() { + return Objects.hash(name, version, rawDescription, build, kernelVersion, rooted); + } + // JsonSerializable public static final class JsonKeys { diff --git a/sentry/src/main/java/io/sentry/protocol/Request.java b/sentry/src/main/java/io/sentry/protocol/Request.java index b25ad49f9f..2e51a23197 100644 --- a/sentry/src/main/java/io/sentry/protocol/Request.java +++ b/sentry/src/main/java/io/sentry/protocol/Request.java @@ -7,6 +7,7 @@ import io.sentry.JsonSerializable; import io.sentry.JsonUnknown; import io.sentry.util.CollectionUtils; +import io.sentry.util.Objects; import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; import java.util.Map; @@ -191,19 +192,6 @@ public void setOthers(final @Nullable Map other) { this.other = CollectionUtils.newConcurrentHashMap(other); } - // region json - - @Nullable - @Override - public Map getUnknown() { - return unknown; - } - - @Override - public void setUnknown(@Nullable Map unknown) { - this.unknown = unknown; - } - public @Nullable String getFragment() { return fragment; } @@ -220,6 +208,39 @@ public void setBodySize(final @Nullable Long bodySize) { this.bodySize = bodySize; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Request request = (Request) o; + return Objects.equals(url, request.url) + && Objects.equals(method, request.method) + && Objects.equals(queryString, request.queryString) + && Objects.equals(cookies, request.cookies) + && Objects.equals(headers, request.headers) + && Objects.equals(env, request.env) + && Objects.equals(bodySize, request.bodySize) + && Objects.equals(fragment, request.fragment); + } + + @Override + public int hashCode() { + return Objects.hash(url, method, queryString, cookies, headers, env, bodySize, fragment); + } + + // region json + + @Nullable + @Override + public Map getUnknown() { + return unknown; + } + + @Override + public void setUnknown(@Nullable Map unknown) { + this.unknown = unknown; + } + public static final class JsonKeys { public static final String URL = "url"; public static final String METHOD = "method"; diff --git a/sentry/src/main/java/io/sentry/protocol/SdkVersion.java b/sentry/src/main/java/io/sentry/protocol/SdkVersion.java index e8dda2e655..eaa0aa34c9 100644 --- a/sentry/src/main/java/io/sentry/protocol/SdkVersion.java +++ b/sentry/src/main/java/io/sentry/protocol/SdkVersion.java @@ -160,6 +160,19 @@ public void addIntegration(final @NotNull String integration) { return sdk; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SdkVersion that = (SdkVersion) o; + return name.equals(that.name) && version.equals(that.version); + } + + @Override + public int hashCode() { + return Objects.hash(name, version); + } + // JsonKeys public static final class JsonKeys { diff --git a/sentry/src/main/java/io/sentry/protocol/User.java b/sentry/src/main/java/io/sentry/protocol/User.java index 86a5992a06..b696e4d5b8 100644 --- a/sentry/src/main/java/io/sentry/protocol/User.java +++ b/sentry/src/main/java/io/sentry/protocol/User.java @@ -7,6 +7,7 @@ import io.sentry.JsonSerializable; import io.sentry.JsonUnknown; import io.sentry.util.CollectionUtils; +import io.sentry.util.Objects; import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; import java.util.Map; @@ -189,6 +190,23 @@ public void setData(final @Nullable Map data) { this.data = CollectionUtils.newConcurrentHashMap(data); } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + User user = (User) o; + return Objects.equals(email, user.email) + && Objects.equals(id, user.id) + && Objects.equals(username, user.username) + && Objects.equals(segment, user.segment) + && Objects.equals(ipAddress, user.ipAddress); + } + + @Override + public int hashCode() { + return Objects.hash(email, id, username, segment, ipAddress); + } + // region json @Nullable diff --git a/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java b/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java index 49ff077b11..4d4a3842e9 100644 --- a/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java +++ b/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java @@ -6,6 +6,7 @@ import io.sentry.SentryEnvelope; import io.sentry.SentryLevel; import io.sentry.SentryOptions; +import io.sentry.UncaughtExceptionHandlerIntegration; import io.sentry.cache.IEnvelopeCache; import io.sentry.clientreport.DiscardReason; import io.sentry.hints.Cached; @@ -84,7 +85,8 @@ public void send(final @NotNull SentryEnvelope envelope, final @NotNull Hint hin } } else { SentryEnvelope envelopeThatMayIncludeClientReport; - if (HintUtils.hasType(hint, DiskFlushNotification.class)) { + if (HintUtils.hasType( + hint, UncaughtExceptionHandlerIntegration.UncaughtExceptionHint.class)) { envelopeThatMayIncludeClientReport = options.getClientReportRecorder().attachReportToEnvelope(filteredEnvelope); } else { diff --git a/sentry/src/main/java/io/sentry/util/HintUtils.java b/sentry/src/main/java/io/sentry/util/HintUtils.java index fcf752ba56..60e519a7e8 100644 --- a/sentry/src/main/java/io/sentry/util/HintUtils.java +++ b/sentry/src/main/java/io/sentry/util/HintUtils.java @@ -9,6 +9,7 @@ import io.sentry.Hint; import io.sentry.ILogger; import io.sentry.hints.ApplyScopeData; +import io.sentry.hints.Backfillable; import io.sentry.hints.Cached; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -99,7 +100,8 @@ public static void runIfHasType( * @return true if it should apply scope's data or false otherwise */ public static boolean shouldApplyScopeData(@NotNull Hint hint) { - return !hasType(hint, Cached.class) || hasType(hint, ApplyScopeData.class); + return (!hasType(hint, Cached.class) && !hasType(hint, Backfillable.class)) + || hasType(hint, ApplyScopeData.class); } @FunctionalInterface diff --git a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt index cf54a1a749..0654a890a2 100644 --- a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt +++ b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt @@ -1132,6 +1132,64 @@ class JsonSerializerTest { verify(stream, never()).close() } + @Test + fun `known primitives can be deserialized`() { + val string = serializeToString("value") + val collection = serializeToString(listOf("hello", "hallo")) + val map = serializeToString(mapOf("one" to "two")) + + val deserializedString = fixture.serializer.deserialize(StringReader(string), String::class.java) + val deserializedCollection = fixture.serializer.deserialize(StringReader(collection), List::class.java) + val deserializedMap = fixture.serializer.deserialize(StringReader(map), Map::class.java) + + assertEquals("value", deserializedString) + assertEquals(listOf("hello", "hallo"), deserializedCollection) + assertEquals(mapOf("one" to "two"), deserializedMap) + } + + @Test + fun `collection with element deserializer can be deserialized`() { + val breadcrumb1 = Breadcrumb.debug("test") + val breadcrumb2 = Breadcrumb.navigation("one", "other") + val collection = serializeToString(listOf(breadcrumb1, breadcrumb2)) + + val deserializedCollection = fixture.serializer.deserializeCollection(StringReader(collection), List::class.java, Breadcrumb.Deserializer()) + + assertEquals(listOf(breadcrumb1, breadcrumb2), deserializedCollection) + } + + @Test + fun `collection without element deserializer can be deserialized as map`() { + val timestamp = Date(0) + val timestampSerialized = serializeToString(timestamp) + .removePrefix("\"") + .removeSuffix("\"") + val collection = serializeToString( + listOf( + Breadcrumb(timestamp).apply { message = "test" }, + Breadcrumb(timestamp).apply { category = "navigation" } + ) + ) + + val deserializedCollection = fixture.serializer.deserialize(StringReader(collection), List::class.java) + + assertEquals( + listOf( + mapOf( + "data" to emptyMap(), + "message" to "test", + "timestamp" to timestampSerialized + ), + mapOf( + "data" to emptyMap(), + "category" to "navigation", + "timestamp" to timestampSerialized + ) + ), + deserializedCollection + ) + } + private fun assertSessionData(expectedSession: Session?) { assertNotNull(expectedSession) assertEquals(UUID.fromString("c81d4e2e-bcf2-11e6-869b-7df92533d2db"), expectedSession.sessionId) diff --git a/sentry/src/test/java/io/sentry/ScopeTest.kt b/sentry/src/test/java/io/sentry/ScopeTest.kt index a6afd4e57a..628c15cad9 100644 --- a/sentry/src/test/java/io/sentry/ScopeTest.kt +++ b/sentry/src/test/java/io/sentry/ScopeTest.kt @@ -1,17 +1,20 @@ package io.sentry +import io.sentry.SentryLevel.WARNING import io.sentry.protocol.Request import io.sentry.protocol.User import io.sentry.test.callMethod import org.junit.Assert.assertArrayEquals -import org.mockito.kotlin.any +import org.mockito.kotlin.argThat import org.mockito.kotlin.eq import org.mockito.kotlin.mock -import org.mockito.kotlin.never +import org.mockito.kotlin.times import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import java.util.concurrent.CopyOnWriteArrayList import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertNotSame import kotlin.test.assertNull @@ -107,7 +110,8 @@ class ScopeTest { scope.setTag("tag", "tag") scope.setExtra("extra", "extra") - val transaction = SentryTracer(TransactionContext("transaction-name", "op"), NoOpHub.getInstance()) + val transaction = + SentryTracer(TransactionContext("transaction-name", "op"), NoOpHub.getInstance()) scope.transaction = transaction val attachment = Attachment("path/log.txt") @@ -177,7 +181,12 @@ class ScopeTest { user.id = "456" request.method = "post" - scope.setTransaction(SentryTracer(TransactionContext("newTransaction", "op"), NoOpHub.getInstance())) + scope.setTransaction( + SentryTracer( + TransactionContext("newTransaction", "op"), + NoOpHub.getInstance() + ) + ) // because you can only set a new list to scope val newFingerprints = mutableListOf("def", "ghf") @@ -564,10 +573,9 @@ class ScopeTest { } @Test - fun `Scope set user sync scopes if enabled`() { + fun `Scope set user sync scopes`() { val observer = mock() val options = SentryOptions().apply { - isEnableScopeSync = true addScopeObserver(observer) } val scope = Scope(options) @@ -578,141 +586,225 @@ class ScopeTest { } @Test - fun `Scope set user wont sync scopes if disabled`() { + fun `Scope add breadcrumb sync scopes`() { val observer = mock() val options = SentryOptions().apply { addScopeObserver(observer) } val scope = Scope(options) - scope.user = User() - verify(observer, never()).setUser(any()) + val breadcrumb = Breadcrumb() + scope.addBreadcrumb(breadcrumb) + verify(observer).addBreadcrumb(eq(breadcrumb)) + verify(observer).setBreadcrumbs(argThat { elementAt(0) == breadcrumb }) + + scope.addBreadcrumb(Breadcrumb.debug("test")) + verify(observer).addBreadcrumb(argThat { message == "test" }) + verify(observer, times(2)).setBreadcrumbs( + argThat { + elementAt(0) == breadcrumb && elementAt(1).message == "test" + } + ) } @Test - fun `Scope add breadcrumb sync scopes if enabled`() { + fun `Scope clear breadcrumbs sync scopes`() { val observer = mock() val options = SentryOptions().apply { - isEnableScopeSync = true addScopeObserver(observer) } val scope = Scope(options) - val breadrumb = Breadcrumb() - scope.addBreadcrumb(breadrumb) - verify(observer).addBreadcrumb(eq(breadrumb)) + val breadcrumb = Breadcrumb() + scope.addBreadcrumb(breadcrumb) + assertFalse(scope.breadcrumbs.isEmpty()) + + scope.clearBreadcrumbs() + verify(observer, times(2)).setBreadcrumbs(argThat { isEmpty() }) } @Test - fun `Scope add breadcrumb wont sync scopes if disabled`() { + fun `Scope set tag sync scopes`() { val observer = mock() val options = SentryOptions().apply { addScopeObserver(observer) } val scope = Scope(options) - scope.addBreadcrumb(Breadcrumb()) - verify(observer, never()).addBreadcrumb(any()) + scope.setTag("a", "b") + verify(observer).setTag(eq("a"), eq("b")) + verify(observer).setTags(argThat { get("a") == "b" }) + + scope.setTag("one", "two") + verify(observer).setTag(eq("one"), eq("two")) + verify(observer, times(2)).setTags(argThat { get("a") == "b" && get("one") == "two" }) } @Test - fun `Scope set tag sync scopes if enabled`() { + fun `Scope remove tag sync scopes`() { val observer = mock() val options = SentryOptions().apply { - isEnableScopeSync = true addScopeObserver(observer) } val scope = Scope(options) scope.setTag("a", "b") - verify(observer).setTag(eq("a"), eq("b")) + assertFalse(scope.tags.isEmpty()) + + scope.removeTag("a") + verify(observer).removeTag(eq("a")) + verify(observer, times(2)).setTags(emptyMap()) } @Test - fun `Scope set tag wont sync scopes if disabled`() { + fun `Scope set extra sync scopes`() { val observer = mock() val options = SentryOptions().apply { addScopeObserver(observer) } val scope = Scope(options) - scope.setTag("a", "b") - verify(observer, never()).setTag(any(), any()) + scope.setExtra("a", "b") + verify(observer).setExtra(eq("a"), eq("b")) + verify(observer).setExtras(argThat { get("a") == "b" }) + + scope.setExtra("one", "two") + verify(observer).setExtra(eq("one"), eq("two")) + verify(observer, times(2)).setExtras(argThat { get("a") == "b" && get("one") == "two" }) } @Test - fun `Scope remove tag sync scopes if enabled`() { + fun `Scope remove extra sync scopes`() { val observer = mock() val options = SentryOptions().apply { - isEnableScopeSync = true addScopeObserver(observer) } val scope = Scope(options) - scope.removeTag("a") - verify(observer).removeTag(eq("a")) + scope.setExtra("a", "b") + assertFalse(scope.extras.isEmpty()) + + scope.removeExtra("a") + verify(observer).removeExtra(eq("a")) + verify(observer, times(2)).setExtras(emptyMap()) } @Test - fun `Scope remove tag wont sync scopes if disabled`() { + fun `Scope set level sync scopes`() { val observer = mock() val options = SentryOptions().apply { addScopeObserver(observer) } val scope = Scope(options) - scope.removeTag("a") - verify(observer, never()).removeTag(any()) + scope.level = WARNING + verify(observer).setLevel(eq(WARNING)) } @Test - fun `Scope set extra sync scopes if enabled`() { + fun `Scope set transaction name sync scopes`() { val observer = mock() val options = SentryOptions().apply { - isEnableScopeSync = true addScopeObserver(observer) } val scope = Scope(options) - scope.setExtra("a", "b") - verify(observer).setExtra(eq("a"), eq("b")) + scope.setTransaction("main") + verify(observer).setTransaction(eq("main")) } @Test - fun `Scope set extra wont sync scopes if disabled`() { + fun `Scope set transaction sync scopes`() { val observer = mock() val options = SentryOptions().apply { addScopeObserver(observer) } val scope = Scope(options) - scope.setExtra("a", "b") - verify(observer, never()).setExtra(any(), any()) + scope.transaction = mock { + whenever(mock.name).thenReturn("main") + whenever(mock.spanContext).thenReturn(SpanContext("ui.load")) + } + verify(observer).setTransaction(eq("main")) + verify(observer).setTrace(argThat { operation == "ui.load" }) } @Test - fun `Scope remove extra sync scopes if enabled`() { + fun `Scope set transaction null sync scopes`() { val observer = mock() val options = SentryOptions().apply { - isEnableScopeSync = true addScopeObserver(observer) } val scope = Scope(options) - scope.removeExtra("a") - verify(observer).removeExtra(eq("a")) + scope.transaction = null + verify(observer).setTransaction(null) + verify(observer).setTrace(null) } @Test - fun `Scope remove extra wont sync scopes if enabled`() { + fun `Scope clear transaction sync scopes`() { val observer = mock() val options = SentryOptions().apply { addScopeObserver(observer) } val scope = Scope(options) - scope.removeExtra("a") - verify(observer, never()).removeExtra(any()) + scope.transaction = mock { + whenever(mock.name).thenReturn("main") + whenever(mock.spanContext).thenReturn(SpanContext("ui.load")) + } + verify(observer).setTransaction(eq("main")) + verify(observer).setTrace(argThat { operation == "ui.load" }) + + scope.clearTransaction() + verify(observer).setTransaction(null) + verify(observer).setTrace(null) + } + + @Test + fun `Scope set request sync scopes`() { + val observer = mock() + val options = SentryOptions().apply { + addScopeObserver(observer) + } + val scope = Scope(options) + + scope.request = Request().apply { url = "https://google.com" } + verify(observer).setRequest(argThat { url == "https://google.com" }) + } + + @Test + fun `Scope set fingerprint sync scopes`() { + val observer = mock() + val options = SentryOptions().apply { + addScopeObserver(observer) + } + val scope = Scope(options) + + scope.fingerprint = listOf("finger", "print") + verify(observer).setFingerprint( + argThat { + elementAt(0) == "finger" && elementAt(1) == "print" + } + ) + } + + @Test + fun `Scope set contexts sync scopes`() { + val observer = mock() + val options = SentryOptions().apply { + addScopeObserver(observer) + } + val scope = Scope(options) + + data class Obj(val stuff: Int) + scope.setContexts("test", Obj(3)) + verify(observer).setContexts( + argThat { + (get("test") as Obj).stuff == 3 + } + ) } @Test @@ -831,7 +923,8 @@ class ScopeTest { @Test fun `when transaction is started, sets transaction name on the transaction object`() { val scope = Scope(SentryOptions()) - val sentryTransaction = SentryTracer(TransactionContext("transaction-name", "op"), NoOpHub.getInstance()) + val sentryTransaction = + SentryTracer(TransactionContext("transaction-name", "op"), NoOpHub.getInstance()) scope.transaction = sentryTransaction assertEquals("transaction-name", scope.transactionName) scope.setTransaction("new-name") @@ -844,7 +937,8 @@ class ScopeTest { fun `when transaction is set after transaction name is set, clearing transaction does not bring back old transaction name`() { val scope = Scope(SentryOptions()) scope.setTransaction("transaction-a") - val sentryTransaction = SentryTracer(TransactionContext("transaction-name", "op"), NoOpHub.getInstance()) + val sentryTransaction = + SentryTracer(TransactionContext("transaction-name", "op"), NoOpHub.getInstance()) scope.setTransaction(sentryTransaction) assertEquals("transaction-name", scope.transactionName) scope.clearTransaction() @@ -854,7 +948,8 @@ class ScopeTest { @Test fun `withTransaction returns the current Transaction bound to the Scope`() { val scope = Scope(SentryOptions()) - val sentryTransaction = SentryTracer(TransactionContext("transaction-name", "op"), NoOpHub.getInstance()) + val sentryTransaction = + SentryTracer(TransactionContext("transaction-name", "op"), NoOpHub.getInstance()) scope.setTransaction(sentryTransaction) scope.withTransaction { diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index f786683211..c57c97f9f2 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -7,8 +7,8 @@ import io.sentry.clientreport.DropEverythingEventProcessor import io.sentry.exception.SentryEnvelopeException import io.sentry.hints.AbnormalExit import io.sentry.hints.ApplyScopeData +import io.sentry.hints.Backfillable import io.sentry.hints.Cached -import io.sentry.hints.DiskFlushNotification import io.sentry.protocol.Mechanism import io.sentry.protocol.Request import io.sentry.protocol.SdkVersion @@ -695,6 +695,51 @@ class SentryClientTest { ) } + @Test + fun `backfillable events are only wired through backfilling processors`() { + val backfillingProcessor = mock() + val nonBackfillingProcessor = mock() + fixture.sentryOptions.addEventProcessor(backfillingProcessor) + fixture.sentryOptions.addEventProcessor(nonBackfillingProcessor) + + val event = SentryEvent() + val hint = HintUtils.createWithTypeCheckHint(BackfillableHint()) + + fixture.getSut().captureEvent(event, hint) + + verify(backfillingProcessor).process(eq(event), eq(hint)) + verify(nonBackfillingProcessor, never()).process(any(), anyOrNull()) + } + + @Test + fun `scope is not applied to backfillable events`() { + val event = SentryEvent() + val hint = HintUtils.createWithTypeCheckHint(BackfillableHint()) + val scope = createScope() + + fixture.getSut().captureEvent(event, scope, hint) + + assertNull(event.user) + assertNull(event.level) + assertNull(event.breadcrumbs) + assertNull(event.request) + } + + @Test + fun `non-backfillable events are only wired through regular processors`() { + val backfillingProcessor = mock() + val nonBackfillingProcessor = mock() + fixture.sentryOptions.addEventProcessor(backfillingProcessor) + fixture.sentryOptions.addEventProcessor(nonBackfillingProcessor) + + val event = SentryEvent() + + fixture.getSut().captureEvent(event) + + verify(backfillingProcessor, never()).process(any(), anyOrNull()) + verify(nonBackfillingProcessor).process(eq(event), anyOrNull()) + } + @Test fun `transaction dropped by beforeSendTransaction is recorded`() { fixture.sentryOptions.setBeforeSendTransaction { transaction, hint -> @@ -2206,10 +2251,6 @@ class SentryClientTest { override fun mechanism(): String? = mechanism } - internal class DiskFlushNotificationHint : DiskFlushNotification { - override fun markFlushed() {} - } - private fun eventProcessorThrows(): EventProcessor { return object : EventProcessor { override fun process(event: SentryEvent, hint: Hint): SentryEvent? { @@ -2217,6 +2258,10 @@ class SentryClientTest { } } } + + private class BackfillableHint : Backfillable { + override fun shouldEnrich(): Boolean = false + } } class DropEverythingEventProcessor : EventProcessor { diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 2314fe55ae..85ca667436 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -320,6 +320,16 @@ class SentryOptionsTest { assertTrue(options.scopeObservers.contains(observer)) } + @Test + fun `when adds options observer, observer list has it`() { + val observer = mock() + val options = SentryOptions().apply { + addOptionsObserver(observer) + } + + assertTrue(options.optionsObservers.contains(observer)) + } + @Test fun `copies options from another SentryOptions instance`() { val externalOptions = ExternalOptions() diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index a84cf32dcc..b43d9c3491 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -4,9 +4,12 @@ import io.sentry.cache.EnvelopeCache import io.sentry.cache.IEnvelopeCache import io.sentry.internal.modules.CompositeModulesLoader import io.sentry.internal.modules.IModulesLoader +import io.sentry.protocol.SdkVersion import io.sentry.protocol.SentryId +import io.sentry.test.ImmediateExecutorService import io.sentry.util.thread.IMainThreadChecker import io.sentry.util.thread.MainThreadChecker +import org.awaitility.kotlin.await import org.junit.rules.TemporaryFolder import org.mockito.kotlin.any import org.mockito.kotlin.argThat @@ -16,6 +19,7 @@ import org.mockito.kotlin.verify import java.io.File import java.nio.file.Files import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicBoolean import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test @@ -112,7 +116,10 @@ class SentryTest { it.setDebug(true) it.setLogger(logger) } - verify(logger).log(eq(SentryLevel.WARNING), eq("Sentry has been already initialized. Previous configuration will be overwritten.")) + verify(logger).log( + eq(SentryLevel.WARNING), + eq("Sentry has been already initialized. Previous configuration will be overwritten.") + ) } @Test @@ -124,7 +131,10 @@ class SentryTest { it.setDebug(true) it.setLogger(logger) } - verify(logger).log(eq(SentryLevel.WARNING), eq("Sentry has been already initialized. Previous configuration will be overwritten.")) + verify(logger).log( + eq(SentryLevel.WARNING), + eq("Sentry has been already initialized. Previous configuration will be overwritten.") + ) } @Test @@ -500,6 +510,106 @@ class SentryTest { verify(hub).reportFullyDisplayed() } + @Test + fun `init notifies option observers`() { + val optionsObserver = InMemoryOptionsObserver() + + Sentry.init { + it.dsn = dsn + + it.executorService = ImmediateExecutorService() + + it.addOptionsObserver(optionsObserver) + + it.release = "io.sentry.sample@1.1.0+220" + it.proguardUuid = "uuid" + it.dist = "220" + it.sdkVersion = SdkVersion("sentry.java.android", "6.13.0") + it.environment = "debug" + it.setTag("one", "two") + } + + assertEquals("io.sentry.sample@1.1.0+220", optionsObserver.release) + assertEquals("debug", optionsObserver.environment) + assertEquals("220", optionsObserver.dist) + assertEquals("uuid", optionsObserver.proguardUuid) + assertEquals(mapOf("one" to "two"), optionsObserver.tags) + assertEquals(SdkVersion("sentry.java.android", "6.13.0"), optionsObserver.sdkVersion) + } + + @Test + fun `if there is work enqueued, init notifies options observers after that work is done`() { + val optionsObserver = InMemoryOptionsObserver().apply { + setRelease("io.sentry.sample@2.0.0") + setEnvironment("production") + } + val triggered = AtomicBoolean(false) + + Sentry.init { + it.dsn = dsn + + it.addOptionsObserver(optionsObserver) + + it.release = "io.sentry.sample@1.1.0+220" + it.environment = "debug" + + it.executorService.submit { + // here the values should be still old. Sentry.init will submit another runnable + // to notify the options observers, but because the executor is single-threaded, the + // work will be enqueued and the observers will be notified after current work is + // finished, ensuring that even if something is using the options observer from a + // different thread, it will still use the old values. + Thread.sleep(1000L) + assertEquals("io.sentry.sample@2.0.0", optionsObserver.release) + assertEquals("production", optionsObserver.environment) + triggered.set(true) + } + } + + await.untilTrue(triggered) + assertEquals("io.sentry.sample@1.1.0+220", optionsObserver.release) + assertEquals("debug", optionsObserver.environment) + } + + private class InMemoryOptionsObserver : IOptionsObserver { + var release: String? = null + private set + var environment: String? = null + private set + var proguardUuid: String? = null + private set + var sdkVersion: SdkVersion? = null + private set + var dist: String? = null + private set + var tags: Map = mapOf() + private set + + override fun setRelease(release: String?) { + this.release = release + } + + override fun setEnvironment(environment: String?) { + this.environment = environment + } + + override fun setProguardUuid(proguardUuid: String?) { + this.proguardUuid = proguardUuid + } + + override fun setSdkVersion(sdkVersion: SdkVersion?) { + this.sdkVersion = sdkVersion + } + + override fun setDist(dist: String?) { + this.dist = dist + } + + override fun setTags(tags: MutableMap) { + this.tags = tags + } + } + private class CustomMainThreadChecker : IMainThreadChecker { override fun isMainThread(threadId: Long): Boolean = false } diff --git a/sentry/src/test/java/io/sentry/cache/CacheUtilsTest.kt b/sentry/src/test/java/io/sentry/cache/CacheUtilsTest.kt new file mode 100644 index 0000000000..ba42810cd9 --- /dev/null +++ b/sentry/src/test/java/io/sentry/cache/CacheUtilsTest.kt @@ -0,0 +1,87 @@ +package io.sentry.cache + +import io.sentry.SentryOptions +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import java.io.File +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull + +internal class CacheUtilsTest { + + @get:Rule + val tmpDir = TemporaryFolder() + + @Test + fun `if cacheDir is not set, store does nothing`() { + CacheUtils.store(SentryOptions(), "Hallo!", "stuff", "test.json") + + val (_, file) = tmpDirAndFile() + assertFalse(file.exists()) + } + + @Test + fun `store stores data in the file`() { + val (cacheDir, file) = tmpDirAndFile() + + CacheUtils.store( + SentryOptions().apply { cacheDirPath = cacheDir }, + "Hallo!", + "stuff", + "test.json" + ) + + assertEquals("\"Hallo!\"", file.readText()) + } + + @Test + fun `if cacheDir is not set, read returns null`() { + val value = CacheUtils.read( + SentryOptions(), + "stuff", + "test.json", + String::class.java, + null + ) + + assertNull(value) + } + + @Test + fun `read reads data from the file`() { + val (cacheDir, file) = tmpDirAndFile() + file.writeText("\"Hallo!\"") + + val value = CacheUtils.read( + SentryOptions().apply { cacheDirPath = cacheDir }, + "stuff", + "test.json", + String::class.java, + null + ) + + assertEquals("Hallo!", value) + } + + @Test + fun `delete deletes the file`() { + val (cacheDir, file) = tmpDirAndFile() + file.writeText("Hallo!") + + assertEquals("Hallo!", file.readText()) + + val options = SentryOptions().apply { cacheDirPath = cacheDir } + + CacheUtils.delete(options, "stuff", "test.json") + + assertFalse(file.exists()) + } + + private fun tmpDirAndFile(): Pair { + val cacheDir = tmpDir.newFolder().absolutePath + val dir = File(cacheDir, "stuff").also { it.mkdirs() } + return cacheDir to File(dir, "test.json") + } +} diff --git a/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt b/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt index dfe7e9b0dd..2a4ee2dc40 100644 --- a/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt +++ b/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt @@ -2,15 +2,16 @@ package io.sentry.cache import io.sentry.ILogger import io.sentry.ISerializer +import io.sentry.NoOpLogger import io.sentry.SentryCrashLastRunState import io.sentry.SentryEnvelope import io.sentry.SentryEvent import io.sentry.SentryLevel import io.sentry.SentryOptions import io.sentry.Session +import io.sentry.UncaughtExceptionHandlerIntegration.UncaughtExceptionHint import io.sentry.cache.EnvelopeCache.PREFIX_CURRENT_SESSION_FILE import io.sentry.cache.EnvelopeCache.SUFFIX_CURRENT_SESSION_FILE -import io.sentry.hints.DiskFlushNotification import io.sentry.hints.SessionEndHint import io.sentry.hints.SessionStartHint import io.sentry.protocol.User @@ -250,7 +251,7 @@ class EnvelopeCacheTest { } @Test - fun `write java marker file to disk when disk flush hint`() { + fun `write java marker file to disk when uncaught exception hint`() { val cache = fixture.getSUT() val markerFile = File(fixture.options.cacheDirPath!!, EnvelopeCache.CRASH_MARKER_FILE) @@ -258,16 +259,12 @@ class EnvelopeCacheTest { val envelope = SentryEnvelope.from(fixture.serializer, SentryEvent(), null) - val hints = HintUtils.createWithTypeCheckHint(DiskFlushHint()) + val hints = HintUtils.createWithTypeCheckHint(UncaughtExceptionHint(0, NoOpLogger.getInstance())) cache.store(envelope, hints) assertTrue(markerFile.exists()) } - internal class DiskFlushHint : DiskFlushNotification { - override fun markFlushed() {} - } - private fun createSession(): Session { return Session("dis", User(), "env", "rel") } diff --git a/sentry/src/test/java/io/sentry/cache/PersistingOptionsObserverTest.kt b/sentry/src/test/java/io/sentry/cache/PersistingOptionsObserverTest.kt new file mode 100644 index 0000000000..ded3908f14 --- /dev/null +++ b/sentry/src/test/java/io/sentry/cache/PersistingOptionsObserverTest.kt @@ -0,0 +1,143 @@ +package io.sentry.cache + +import io.sentry.SentryOptions +import io.sentry.cache.PersistingOptionsObserver.DIST_FILENAME +import io.sentry.cache.PersistingOptionsObserver.ENVIRONMENT_FILENAME +import io.sentry.cache.PersistingOptionsObserver.PROGUARD_UUID_FILENAME +import io.sentry.cache.PersistingOptionsObserver.RELEASE_FILENAME +import io.sentry.cache.PersistingOptionsObserver.SDK_VERSION_FILENAME +import io.sentry.cache.PersistingOptionsObserver.TAGS_FILENAME +import io.sentry.protocol.SdkVersion +import io.sentry.test.ImmediateExecutorService +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import kotlin.test.Test +import kotlin.test.assertEquals + +class StoreOptionsValue(private val store: PersistingOptionsObserver.(T) -> Unit) { + operator fun invoke(value: T, observer: PersistingOptionsObserver) { + observer.store(value) + } +} + +class DeleteOptionsValue(private val delete: PersistingOptionsObserver.() -> Unit) { + operator fun invoke(observer: PersistingOptionsObserver) { + observer.delete() + } +} + +@RunWith(Parameterized::class) +class PersistingOptionsObserverTest( + private val entity: T, + private val store: StoreOptionsValue, + private val filename: String, + private val delete: DeleteOptionsValue, + private val deletedEntity: T? +) { + + @get:Rule + val tmpDir = TemporaryFolder() + + class Fixture { + + val options = SentryOptions() + + fun getSut(cacheDir: TemporaryFolder): PersistingOptionsObserver { + options.run { + executorService = ImmediateExecutorService() + cacheDirPath = cacheDir.newFolder().absolutePath + } + return PersistingOptionsObserver(options) + } + } + + private val fixture = Fixture() + + @Test + fun `store and delete scope value`() { + val sut = fixture.getSut(tmpDir) + store(entity, sut) + + val persisted = read() + assertEquals(entity, persisted) + + delete(sut) + val persistedAfterDeletion = read() + assertEquals(deletedEntity, persistedAfterDeletion) + } + + private fun read(): T? = PersistingOptionsObserver.read( + fixture.options, + filename, + entity!!::class.java + ) + + companion object { + + private fun release(): Array = arrayOf( + "io.sentry.sample@1.1.0+23", + StoreOptionsValue { setRelease(it) }, + RELEASE_FILENAME, + DeleteOptionsValue { setRelease(null) }, + null + ) + + private fun proguardUuid(): Array = arrayOf( + "8a258c81-641d-4e54-b06e-a0f56b1ee2ef", + StoreOptionsValue { setProguardUuid(it) }, + PROGUARD_UUID_FILENAME, + DeleteOptionsValue { setProguardUuid(null) }, + null + ) + + private fun sdkVersion(): Array = arrayOf( + SdkVersion("sentry.java.android", "6.13.0"), + StoreOptionsValue { setSdkVersion(it) }, + SDK_VERSION_FILENAME, + DeleteOptionsValue { setSdkVersion(null) }, + null + ) + + private fun dist(): Array = arrayOf( + "223", + StoreOptionsValue { setDist(it) }, + DIST_FILENAME, + DeleteOptionsValue { setDist(null) }, + null + ) + + private fun environment(): Array = arrayOf( + "debug", + StoreOptionsValue { setEnvironment(it) }, + ENVIRONMENT_FILENAME, + DeleteOptionsValue { setEnvironment(null) }, + null + ) + + private fun tags(): Array = arrayOf( + mapOf( + "one" to "two", + "tag" to "none" + ), + StoreOptionsValue> { setTags(it) }, + TAGS_FILENAME, + DeleteOptionsValue { setTags(emptyMap()) }, + emptyMap() + ) + + @JvmStatic + @Parameterized.Parameters(name = "{2}") + fun data(): Collection> { + return listOf( + release(), + proguardUuid(), + dist(), + environment(), + sdkVersion(), + tags() + ) + } + } +} diff --git a/sentry/src/test/java/io/sentry/cache/PersistingScopeObserverTest.kt b/sentry/src/test/java/io/sentry/cache/PersistingScopeObserverTest.kt new file mode 100644 index 0000000000..d31b7088cf --- /dev/null +++ b/sentry/src/test/java/io/sentry/cache/PersistingScopeObserverTest.kt @@ -0,0 +1,284 @@ +package io.sentry.cache + +import io.sentry.Breadcrumb +import io.sentry.DateUtils +import io.sentry.JsonDeserializer +import io.sentry.SentryLevel +import io.sentry.SentryOptions +import io.sentry.SpanContext +import io.sentry.SpanId +import io.sentry.cache.PersistingScopeObserver.BREADCRUMBS_FILENAME +import io.sentry.cache.PersistingScopeObserver.CONTEXTS_FILENAME +import io.sentry.cache.PersistingScopeObserver.EXTRAS_FILENAME +import io.sentry.cache.PersistingScopeObserver.FINGERPRINT_FILENAME +import io.sentry.cache.PersistingScopeObserver.LEVEL_FILENAME +import io.sentry.cache.PersistingScopeObserver.REQUEST_FILENAME +import io.sentry.cache.PersistingScopeObserver.TAGS_FILENAME +import io.sentry.cache.PersistingScopeObserver.TRACE_FILENAME +import io.sentry.cache.PersistingScopeObserver.TRANSACTION_FILENAME +import io.sentry.cache.PersistingScopeObserver.USER_FILENAME +import io.sentry.protocol.App +import io.sentry.protocol.Browser +import io.sentry.protocol.Contexts +import io.sentry.protocol.Device +import io.sentry.protocol.Device.DeviceOrientation.PORTRAIT +import io.sentry.protocol.Gpu +import io.sentry.protocol.OperatingSystem +import io.sentry.protocol.Request +import io.sentry.protocol.SentryId +import io.sentry.protocol.User +import io.sentry.test.ImmediateExecutorService +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import kotlin.test.Test +import kotlin.test.assertEquals + +class StoreScopeValue(private val store: PersistingScopeObserver.(T) -> Unit) { + operator fun invoke(value: T, observer: PersistingScopeObserver) { + observer.store(value) + } +} + +class DeleteScopeValue(private val delete: PersistingScopeObserver.() -> Unit) { + operator fun invoke(observer: PersistingScopeObserver) { + observer.delete() + } +} + +@RunWith(Parameterized::class) +class PersistingScopeObserverTest( + private val entity: T, + private val store: StoreScopeValue, + private val filename: String, + private val delete: DeleteScopeValue, + private val deletedEntity: T?, + private val elementDeserializer: JsonDeserializer? +) { + + @get:Rule + val tmpDir = TemporaryFolder() + + class Fixture { + + val options = SentryOptions() + + fun getSut(cacheDir: TemporaryFolder): PersistingScopeObserver { + options.run { + executorService = ImmediateExecutorService() + cacheDirPath = cacheDir.newFolder().absolutePath + } + return PersistingScopeObserver(options) + } + } + + private val fixture = Fixture() + + @Test + fun `store and delete scope value`() { + val sut = fixture.getSut(tmpDir) + store(entity, sut) + + val persisted = read() + assertEquals(entity, persisted) + + delete(sut) + val persistedAfterDeletion = read() + assertEquals(deletedEntity, persistedAfterDeletion) + } + + private fun read(): T? = PersistingScopeObserver.read( + fixture.options, + filename, + entity!!::class.java, + elementDeserializer + ) + + companion object { + + private fun user(): Array = arrayOf( + User().apply { + email = "user@user.com" + id = "c4d61c1b-c144-431e-868f-37a46be5e5f2" + ipAddress = "192.168.0.1" + }, + StoreScopeValue { setUser(it) }, + USER_FILENAME, + DeleteScopeValue { setUser(null) }, + null, + null + ) + + private fun breadcrumbs(): Array = arrayOf( + listOf( + Breadcrumb.navigation("one", "two"), + Breadcrumb.userInteraction("click", "viewId", "viewClass") + ), + StoreScopeValue> { setBreadcrumbs(it) }, + BREADCRUMBS_FILENAME, + DeleteScopeValue { setBreadcrumbs(emptyList()) }, + emptyList(), + Breadcrumb.Deserializer() + ) + + private fun tags(): Array = arrayOf( + mapOf( + "one" to "two", + "tag" to "none" + ), + StoreScopeValue> { setTags(it) }, + TAGS_FILENAME, + DeleteScopeValue { setTags(emptyMap()) }, + emptyMap(), + null + ) + + private fun extras(): Array = arrayOf( + mapOf( + "one" to listOf("thing1", "thing2"), + "two" to 2, + "three" to 3.2 + ), + StoreScopeValue> { setExtras(it) }, + EXTRAS_FILENAME, + DeleteScopeValue { setExtras(emptyMap()) }, + emptyMap(), + null + ) + + private fun request(): Array = arrayOf( + Request().apply { + url = "https://google.com" + method = "GET" + queryString = "search" + cookies = "d84f4cfc-5310-4818-ad4f-3f8d22ceaca8" + fragment = "fragment" + bodySize = 1000 + }, + StoreScopeValue { setRequest(it) }, + REQUEST_FILENAME, + DeleteScopeValue { setRequest(null) }, + null, + null + ) + + private fun fingerprint(): Array = arrayOf( + listOf("finger", "print"), + StoreScopeValue> { setFingerprint(it) }, + FINGERPRINT_FILENAME, + DeleteScopeValue { setFingerprint(emptyList()) }, + emptyList(), + null + ) + + private fun level(): Array = arrayOf( + SentryLevel.WARNING, + StoreScopeValue { setLevel(it) }, + LEVEL_FILENAME, + DeleteScopeValue { setLevel(null) }, + null, + null + ) + + private fun transaction(): Array = arrayOf( + "MainActivity", + StoreScopeValue { setTransaction(it) }, + TRANSACTION_FILENAME, + DeleteScopeValue { setTransaction(null) }, + null, + null + ) + + private fun trace(): Array = arrayOf( + SpanContext(SentryId(), SpanId(), "ui.load", null, null), + StoreScopeValue { setTrace(it) }, + TRACE_FILENAME, + DeleteScopeValue { setTrace(null) }, + null, + null + ) + + private fun contexts(): Array = arrayOf( + Contexts().apply { + setApp( + App().apply { + appBuild = "1" + appIdentifier = "io.sentry.sample" + appName = "sample" + appStartTime = DateUtils.getCurrentDateTime() + buildType = "debug" + appVersion = "2021" + } + ) + setBrowser( + Browser().apply { + name = "Chrome" + } + ) + setDevice( + Device().apply { + name = "Pixel 3XL" + manufacturer = "Google" + brand = "Pixel" + family = "Pixels" + model = "3XL" + isCharging = true + isOnline = true + orientation = PORTRAIT + isSimulator = false + memorySize = 4096 + freeMemory = 2048 + usableMemory = 1536 + isLowMemory = false + storageSize = 64000 + freeStorage = 32000 + screenWidthPixels = 1080 + screenHeightPixels = 1920 + screenDpi = 446 + connectionType = "wifi" + batteryTemperature = 37.0f + batteryLevel = 92.0f + locale = "en-US" + } + ) + setGpu( + Gpu().apply { + vendorName = "GeForce" + memorySize = 1000 + } + ) + setOperatingSystem( + OperatingSystem().apply { + isRooted = true + build = "2021.123_alpha" + name = "Android" + version = "12" + } + ) + }, + StoreScopeValue { setContexts(it) }, + CONTEXTS_FILENAME, + DeleteScopeValue { setContexts(Contexts()) }, + Contexts(), + null + ) + + @JvmStatic + @Parameterized.Parameters(name = "{2}") + fun data(): Collection> { + return listOf( + user(), + breadcrumbs(), + tags(), + extras(), + request(), + fingerprint(), + level(), + transaction(), + trace(), + contexts() + ) + } + } +} diff --git a/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt b/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt index 4bca606ad6..000f575ecc 100644 --- a/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt +++ b/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt @@ -13,9 +13,9 @@ import io.sentry.SentryEnvelopeItem import io.sentry.SentryEvent import io.sentry.SentryOptions import io.sentry.Session +import io.sentry.UncaughtExceptionHandlerIntegration.UncaughtExceptionHint import io.sentry.UserFeedback import io.sentry.dsnString -import io.sentry.hints.DiskFlushNotification import io.sentry.hints.Retryable import io.sentry.protocol.SentryId import io.sentry.protocol.SentryTransaction @@ -196,8 +196,8 @@ class ClientReportTestHelper(val options: SentryOptions) { companion object { fun retryableHint() = HintUtils.createWithTypeCheckHint(TestRetryable()) - fun diskFlushNotificationHint() = HintUtils.createWithTypeCheckHint(TestDiskFlushNotification()) - fun retryableDiskFlushNotificationHint() = HintUtils.createWithTypeCheckHint(TestRetryableDiskFlushNotification()) + fun uncaughtExceptionHint() = HintUtils.createWithTypeCheckHint(TestUncaughtExceptionHint()) + fun retryableUncaughtExceptionHint() = HintUtils.createWithTypeCheckHint(TestRetryableUncaughtException()) fun assertClientReport(clientReportRecorder: IClientReportRecorder, expectedEvents: List) { val recorder = clientReportRecorder as ClientReportRecorder @@ -229,7 +229,7 @@ class TestRetryable : Retryable { } } -class TestRetryableDiskFlushNotification : Retryable, DiskFlushNotification { +class TestRetryableUncaughtException : UncaughtExceptionHint(0, NoOpLogger.getInstance()), Retryable { private var retry = false var flushed = false @@ -246,7 +246,7 @@ class TestRetryableDiskFlushNotification : Retryable, DiskFlushNotification { } } -class TestDiskFlushNotification : DiskFlushNotification { +class TestUncaughtExceptionHint : UncaughtExceptionHint(0, NoOpLogger.getInstance()) { var flushed = false override fun markFlushed() { diff --git a/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportClientReportTest.kt b/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportClientReportTest.kt index 6cc9d4a306..61ba2403b6 100644 --- a/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportClientReportTest.kt +++ b/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportClientReportTest.kt @@ -4,9 +4,9 @@ import io.sentry.SentryEnvelope import io.sentry.SentryOptions import io.sentry.SentryOptionsManipulator import io.sentry.Session -import io.sentry.clientreport.ClientReportTestHelper.Companion.diskFlushNotificationHint -import io.sentry.clientreport.ClientReportTestHelper.Companion.retryableDiskFlushNotificationHint import io.sentry.clientreport.ClientReportTestHelper.Companion.retryableHint +import io.sentry.clientreport.ClientReportTestHelper.Companion.retryableUncaughtExceptionHint +import io.sentry.clientreport.ClientReportTestHelper.Companion.uncaughtExceptionHint import io.sentry.clientreport.DiscardReason import io.sentry.clientreport.IClientReportRecorder import io.sentry.dsnString @@ -160,12 +160,12 @@ class AsyncHttpTransportClientReportTest { } @Test - fun `attaches report and records lost envelope on full queue for non retryable disk flush notification`() { + fun `attaches report and records lost envelope on full queue for non retryable uncaught exception`() { // given givenSetup(cancel = true) // when - fixture.getSUT().send(fixture.envelopeBeforeAttachingClientReport, diskFlushNotificationHint()) + fixture.getSUT().send(fixture.envelopeBeforeAttachingClientReport, uncaughtExceptionHint()) // then verify(fixture.clientReportRecorder, times(1)).attachReportToEnvelope(same(fixture.envelopeBeforeAttachingClientReport)) @@ -174,12 +174,12 @@ class AsyncHttpTransportClientReportTest { } @Test - fun `attaches report and records lost envelope on full queue for retryable disk flush notification`() { + fun `attaches report and records lost envelope on full queue for retryable uncaught exception`() { // given givenSetup(cancel = true) // when - fixture.getSUT().send(fixture.envelopeBeforeAttachingClientReport, retryableDiskFlushNotificationHint()) + fixture.getSUT().send(fixture.envelopeBeforeAttachingClientReport, retryableUncaughtExceptionHint()) // then verify(fixture.clientReportRecorder, times(1)).attachReportToEnvelope(same(fixture.envelopeBeforeAttachingClientReport)) diff --git a/sentry/src/test/java/io/sentry/util/HintUtilsTest.kt b/sentry/src/test/java/io/sentry/util/HintUtilsTest.kt index 5fc37f16fc..598c1b2375 100644 --- a/sentry/src/test/java/io/sentry/util/HintUtilsTest.kt +++ b/sentry/src/test/java/io/sentry/util/HintUtilsTest.kt @@ -3,6 +3,7 @@ package io.sentry.util import io.sentry.CustomCachedApplyScopeDataHint import io.sentry.Hint import io.sentry.hints.ApplyScopeData +import io.sentry.hints.Backfillable import io.sentry.hints.Cached import org.mockito.kotlin.mock import kotlin.test.Test @@ -33,4 +34,10 @@ class HintUtilsTest { val hints = HintUtils.createWithTypeCheckHint(CustomCachedApplyScopeDataHint()) assertTrue(HintUtils.shouldApplyScopeData(hints)) } + + @Test + fun `if event is Backfillable, it should not apply scopes data`() { + val hints = HintUtils.createWithTypeCheckHint(mock()) + assertFalse(HintUtils.shouldApplyScopeData(hints)) + } } From 06f0d7e9090cfaa68a52701832f001183fa05319 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 23 Mar 2023 12:31:44 +0100 Subject: [PATCH 02/23] Mark previous session as abnormal if an ANR has happened --- .../sentry/android/core/AnrV2Integration.java | 102 +++++++++++------ .../core/cache/AndroidEnvelopeCache.java | 14 ++- .../core/cache/AndroidEnvelopeCacheTest.kt | 4 +- sentry/src/main/java/io/sentry/Hub.java | 3 +- .../src/main/java/io/sentry/OutboxSender.java | 2 + .../java/io/sentry/cache/EnvelopeCache.java | 107 +++++++++++++++--- .../java/io/sentry/hints/AbnormalExit.java | 6 + .../io/sentry/hints/AllSessionsEndHint.java | 5 + .../io/sentry/hints/PreviousSessionEnd.java | 5 + .../sentry/hints/PreviousSessionEndHint.java | 4 + .../java/io/sentry/cache/EnvelopeCacheTest.kt | 8 +- 11 files changed, 198 insertions(+), 62 deletions(-) create mode 100644 sentry/src/main/java/io/sentry/hints/AllSessionsEndHint.java create mode 100644 sentry/src/main/java/io/sentry/hints/PreviousSessionEnd.java create mode 100644 sentry/src/main/java/io/sentry/hints/PreviousSessionEndHint.java diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java index e62f4088b3..640b0756da 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java @@ -14,9 +14,14 @@ import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.android.core.cache.AndroidEnvelopeCache; +import io.sentry.cache.EnvelopeCache; +import io.sentry.cache.IEnvelopeCache; import io.sentry.exception.ExceptionMechanismException; +import io.sentry.hints.AbnormalExit; import io.sentry.hints.Backfillable; import io.sentry.hints.BlockingFlushHint; +import io.sentry.hints.PreviousSessionEnd; +import io.sentry.hints.PreviousSessionEndHint; import io.sentry.protocol.Mechanism; import io.sentry.protocol.SentryId; import io.sentry.transport.CurrentDateProvider; @@ -75,26 +80,14 @@ public void register(@NotNull IHub hub, @NotNull SentryOptions options) { } if (this.options.isAnrEnabled()) { - final ActivityManager activityManager = - (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - - List applicationExitInfoList = - activityManager.getHistoricalProcessExitReasons(null, 0, 0); - - if (applicationExitInfoList.size() != 0) { - options - .getExecutorService() - .submit( - new AnrProcessor( - new ArrayList<>( - applicationExitInfoList), // just making a deep copy to be safe, as we're - // modifying the list - hub, - this.options, - dateProvider)); - } else { - options.getLogger().log(SentryLevel.DEBUG, "No records in historical exit reasons."); - } + options + .getExecutorService() + .submit( + new AnrProcessor( + context, + hub, + this.options, + dateProvider)); options.getLogger().log(SentryLevel.DEBUG, "AnrV2Integration installed."); addIntegrationToSdkVersion(); } @@ -109,17 +102,17 @@ public void close() throws IOException { static class AnrProcessor implements Runnable { - final @NotNull List exitInfos; + private final @NotNull Context context; private final @NotNull IHub hub; private final @NotNull SentryAndroidOptions options; private final long threshold; AnrProcessor( - final @NotNull List exitInfos, + final @NotNull Context context, final @NotNull IHub hub, final @NotNull SentryAndroidOptions options, final @NotNull ICurrentDateProvider dateProvider) { - this.exitInfos = exitInfos; + this.context = context; this.hub = hub; this.options = options; this.threshold = dateProvider.getCurrentTimeMillis() - NINETY_DAYS_THRESHOLD; @@ -128,7 +121,20 @@ static class AnrProcessor implements Runnable { @SuppressLint("NewApi") // we check this in AnrIntegrationFactory @Override public void run() { - final long lastReportedAnrTimestamp = AndroidEnvelopeCache.lastReportedAnr(options); + final ActivityManager activityManager = + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + + List applicationExitInfoList = + activityManager.getHistoricalProcessExitReasons(null, 0, 0); + if (applicationExitInfoList.size() == 0) { + endPreviousSession(); + options.getLogger().log(SentryLevel.DEBUG, "No records in historical exit reasons."); + return; + } + + // making a deep copy as we're modifying the list + final List exitInfos = new ArrayList<>(applicationExitInfoList); + final @Nullable Long lastReportedAnrTimestamp = AndroidEnvelopeCache.lastReportedAnr(options); // search for the latest ANR to report it separately as we're gonna enrich it. The latest // ANR will be first in the list, as it's filled last-to-first in order of appearance @@ -143,6 +149,7 @@ public void run() { } if (latestAnr == null) { + endPreviousSession(); options .getLogger() .log(SentryLevel.DEBUG, "No ANRs have been found in the historical exit reasons list."); @@ -150,13 +157,15 @@ public void run() { } if (latestAnr.getTimestamp() < threshold) { + endPreviousSession(); options .getLogger() .log(SentryLevel.DEBUG, "Latest ANR happened too long ago, returning early."); return; } - if (latestAnr.getTimestamp() <= lastReportedAnrTimestamp) { + if (lastReportedAnrTimestamp != null && latestAnr.getTimestamp() <= lastReportedAnrTimestamp) { + endPreviousSession(); options .getLogger() .log(SentryLevel.DEBUG, "Latest ANR has already been reported, returning early."); @@ -172,7 +181,7 @@ public void run() { } private void reportNonEnrichedHistoricalAnrs( - final @NotNull List exitInfos, final long lastReportedAnr) { + final @NotNull List exitInfos, final @Nullable Long lastReportedAnr) { // we reverse the list, because the OS puts errors in order of appearance, last-to-first // and we want to write a marker file after each ANR has been processed, so in case the app // gets killed meanwhile, we can proceed from the last reported ANR and not process the entire @@ -187,7 +196,7 @@ private void reportNonEnrichedHistoricalAnrs( continue; } - if (applicationExitInfo.getTimestamp() <= lastReportedAnr) { + if (lastReportedAnr != null && applicationExitInfo.getTimestamp() <= lastReportedAnr) { options .getLogger() .log(SentryLevel.DEBUG, "ANR has already been reported %s.", applicationExitInfo); @@ -202,10 +211,12 @@ private void reportNonEnrichedHistoricalAnrs( private void reportAsSentryEvent( final @NotNull ApplicationExitInfo exitInfo, final boolean shouldEnrich) { final long anrTimestamp = exitInfo.getTimestamp(); - final Throwable anrThrowable = buildAnrThrowable(exitInfo); + final boolean isBackground = + exitInfo.getImportance() != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; + final Throwable anrThrowable = buildAnrThrowable(exitInfo, isBackground); final AnrV2Hint anrHint = new AnrV2Hint( - options.getFlushTimeoutMillis(), options.getLogger(), anrTimestamp, shouldEnrich); + options.getFlushTimeoutMillis(), options.getLogger(), anrTimestamp, shouldEnrich, isBackground); final Hint hint = HintUtils.createWithTypeCheckHint(anrHint); @@ -228,10 +239,10 @@ private void reportAsSentryEvent( } } - private @NotNull Throwable buildAnrThrowable(final @NotNull ApplicationExitInfo exitInfo) { - final boolean isBackground = - exitInfo.getImportance() != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; - + private @NotNull Throwable buildAnrThrowable( + final @NotNull ApplicationExitInfo exitInfo, + final boolean isBackground + ) { String message = "ANR"; if (isBackground) { message = "Background " + message; @@ -245,26 +256,40 @@ private void reportAsSentryEvent( mechanism.setType("ANRv2"); return new ExceptionMechanismException(mechanism, error, error.getThread(), true); } + + private void endPreviousSession() { + final IEnvelopeCache envelopeCache = options.getEnvelopeDiskCache(); + if (envelopeCache instanceof EnvelopeCache) { + final Hint hint = HintUtils.createWithTypeCheckHint(new PreviousSessionEndHint()); + ((EnvelopeCache) envelopeCache).endPreviousSession(hint); + } + } } @ApiStatus.Internal - public static final class AnrV2Hint extends BlockingFlushHint implements Backfillable { + public static final class AnrV2Hint extends BlockingFlushHint implements Backfillable, + AbnormalExit, PreviousSessionEnd { private final long timestamp; private final boolean shouldEnrich; + private final boolean isBackgroundAnr; + public AnrV2Hint( final long flushTimeoutMillis, final @NotNull ILogger logger, final long timestamp, - final boolean shouldEnrich) { + final boolean shouldEnrich, + final boolean isBackgroundAnr) { super(flushTimeoutMillis, logger); this.timestamp = timestamp; this.shouldEnrich = shouldEnrich; + this.isBackgroundAnr = isBackgroundAnr; } - public long timestamp() { + @Override + public Long timestamp() { return timestamp; } @@ -272,5 +297,10 @@ public long timestamp() { public boolean shouldEnrich() { return shouldEnrich; } + + @Override + public String mechanism() { + return isBackgroundAnr ? "anr_background" : "anr_foreground"; + } } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java b/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java index 18f8bd05fa..102d8f5688 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java @@ -13,6 +13,7 @@ import io.sentry.android.core.SentryAndroidOptions; import io.sentry.android.core.internal.util.AndroidCurrentDateProvider; import io.sentry.cache.EnvelopeCache; +import io.sentry.hints.AbnormalExit; import io.sentry.transport.ICurrentDateProvider; import io.sentry.util.FileUtils; import io.sentry.util.HintUtils; @@ -22,6 +23,7 @@ import java.io.OutputStream; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; @ApiStatus.Internal @@ -70,7 +72,7 @@ public void store(@NotNull SentryEnvelope envelope, @NotNull Hint hint) { hint, AnrV2Integration.AnrV2Hint.class, (anrHint) -> { - final long timestamp = anrHint.timestamp(); + final @Nullable Long timestamp = anrHint.timestamp(); options .getLogger() .log( @@ -78,7 +80,7 @@ public void store(@NotNull SentryEnvelope envelope, @NotNull Hint hint) { "Writing last reported ANR marker with timestamp %d", timestamp); - writeLastReportedAnrMarker(anrHint.timestamp()); + writeLastReportedAnrMarker(timestamp); }); } @@ -136,7 +138,7 @@ public static boolean hasStartupCrashMarker(final @NotNull SentryOptions options return false; } - public static long lastReportedAnr(final @NotNull SentryOptions options) { + public static @Nullable Long lastReportedAnr(final @NotNull SentryOptions options) { final String cacheDirPath = Objects.requireNonNull( options.getCacheDirPath(), "Cache dir path should be set for getting ANRs reported"); @@ -147,7 +149,7 @@ public static long lastReportedAnr(final @NotNull SentryOptions options) { final String content = FileUtils.readText(lastAnrMarker); // we wrapped into try-catch already //noinspection ConstantConditions - return Long.parseLong(content.trim()); + return content.equals("null") ? null : Long.parseLong(content.trim()); } else { options .getLogger() @@ -156,10 +158,10 @@ public static long lastReportedAnr(final @NotNull SentryOptions options) { } catch (Throwable e) { options.getLogger().log(ERROR, "Error reading last ANR marker", e); } - return 0L; + return null; } - private void writeLastReportedAnrMarker(final long timestamp) { + private void writeLastReportedAnrMarker(final @Nullable Long timestamp) { final String cacheDirPath = options.getCacheDirPath(); if (cacheDirPath == null) { options.getLogger().log(DEBUG, "Cache dir path is null, the ANR marker will not be written"); diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt index 3eee455d98..7eb8e0baab 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt @@ -136,7 +136,7 @@ class AndroidEnvelopeCacheTest { 0, NoOpLogger.getInstance(), 12345678L, - false + false, false ) ) cache.store(fixture.envelope, hints) @@ -153,7 +153,7 @@ class AndroidEnvelopeCacheTest { 0, NoOpLogger.getInstance(), 12345678L, - false + false, false ) ) cache.store(fixture.envelope, hints) diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index 6d485a6b15..4e44f5f115 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -2,6 +2,7 @@ import io.sentry.Stack.StackItem; import io.sentry.clientreport.DiscardReason; +import io.sentry.hints.AllSessionsEndHint; import io.sentry.hints.SessionEndHint; import io.sentry.hints.SessionStartHint; import io.sentry.protocol.SentryId; @@ -319,7 +320,7 @@ public void endSession() { final StackItem item = this.stack.peek(); final Session previousSession = item.getScope().endSession(); if (previousSession != null) { - final Hint hint = HintUtils.createWithTypeCheckHint(new SessionEndHint()); + final Hint hint = HintUtils.createWithTypeCheckHint(new AllSessionsEndHint()); item.getClient().captureSession(previousSession, hint); } diff --git a/sentry/src/main/java/io/sentry/OutboxSender.java b/sentry/src/main/java/io/sentry/OutboxSender.java index fcf21fb121..9e704f0a0e 100644 --- a/sentry/src/main/java/io/sentry/OutboxSender.java +++ b/sentry/src/main/java/io/sentry/OutboxSender.java @@ -2,6 +2,7 @@ import static io.sentry.SentryLevel.ERROR; import static io.sentry.cache.EnvelopeCache.PREFIX_CURRENT_SESSION_FILE; +import static io.sentry.cache.EnvelopeCache.PREFIX_PREVIOUS_SESSION_FILE; import io.sentry.cache.EnvelopeCache; import io.sentry.hints.Flushable; @@ -99,6 +100,7 @@ protected boolean isRelevantFileName(final @Nullable String fileName) { // ignore current.envelope return fileName != null && !fileName.startsWith(PREFIX_CURRENT_SESSION_FILE) + && !fileName.startsWith(PREFIX_PREVIOUS_SESSION_FILE) && !fileName.startsWith(EnvelopeCache.STARTUP_CRASH_MARKER_FILE); // TODO: Use an extension to filter out relevant files } diff --git a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java index 2e7f71f46d..e3e1318b0e 100644 --- a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java +++ b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java @@ -9,6 +9,7 @@ import com.jakewharton.nopen.annotation.Open; import io.sentry.DateUtils; import io.sentry.Hint; +import io.sentry.ISerializer; import io.sentry.SentryCrashLastRunState; import io.sentry.SentryEnvelope; import io.sentry.SentryEnvelopeItem; @@ -17,6 +18,8 @@ import io.sentry.SentryOptions; import io.sentry.Session; import io.sentry.UncaughtExceptionHandlerIntegration; +import io.sentry.hints.AbnormalExit; +import io.sentry.hints.PreviousSessionEnd; import io.sentry.hints.SessionEnd; import io.sentry.hints.SessionStart; import io.sentry.transport.NoOpEnvelopeCache; @@ -56,7 +59,9 @@ public class EnvelopeCache extends CacheStrategy implements IEnvelopeCache { public static final String SUFFIX_ENVELOPE_FILE = ".envelope"; public static final String PREFIX_CURRENT_SESSION_FILE = "session"; - static final String SUFFIX_CURRENT_SESSION_FILE = ".json"; + + public static final String PREFIX_PREVIOUS_SESSION_FILE = "previous_session"; + static final String SUFFIX_SESSION_FILE = ".json"; public static final String CRASH_MARKER_FILE = "last_crash"; public static final String NATIVE_CRASH_MARKER_FILE = ".sentry-native/" + CRASH_MARKER_FILE; @@ -96,6 +101,14 @@ public void store(final @NotNull SentryEnvelope envelope, final @NotNull Hint hi } } + endPreviousSession(hint); + + /** + * Common cases when previous session is not ended with a SessionStart hint for envelope: + * - The previous session experienced Abnormal exit (ANR, OS kills app, User kills app) + * - The previous session experienced native crash + * - The previous session hasn't been ended properly (e.g. session was started in .init() and then in onForeground() after sessionTrackingIntervalMillis) + */ if (HintUtils.hasType(hint, SessionStart.class)) { boolean crashedLastRun = false; @@ -119,13 +132,12 @@ public void store(final @NotNull SentryEnvelope envelope, final @NotNull Hint hi } else { final File crashMarkerFile = new File(options.getCacheDirPath(), NATIVE_CRASH_MARKER_FILE); - Date timestamp = null; if (crashMarkerFile.exists()) { options .getLogger() .log(INFO, "Crash marker file exists, last Session is gonna be Crashed."); - timestamp = getTimestampFromCrashMarkerFile(crashMarkerFile); + final Date timestamp = getTimestampFromCrashMarkerFile(crashMarkerFile); crashedLastRun = true; if (!crashMarkerFile.delete()) { @@ -137,22 +149,24 @@ public void store(final @NotNull SentryEnvelope envelope, final @NotNull Hint hi crashMarkerFile.getAbsolutePath()); } session.update(Session.State.Crashed, null, true); + session.end(timestamp); + // if it was a native crash, we are safe to send session in an envelope, meaning + // there was no abnormal exit + saveSessionToEnvelope(session); + } else { + // if there was no native crash, the session has potentially experienced Abnormal exit + // so we end it with the current timestamp, but do not send it yet, as other envelopes + // may come later and change its attributes (status, etc.). We just save it as previous_session.json + session.end(); + writeSessionToDisk(getPreviousSessionFile(), session); } - - session.end(timestamp); - // if the App. has been upgraded and there's a new version of the SDK running, - // SdkVersion will be outdated. - final SentryEnvelope fromSession = - SentryEnvelope.from(serializer, session, options.getSdkVersion()); - final File fileFromSession = getEnvelopeFile(fromSession); - writeEnvelopeToDisk(fileFromSession, fromSession); } } catch (Throwable e) { options.getLogger().log(SentryLevel.ERROR, "Error processing session.", e); } // at this point the leftover session and its current session file already became a new - // envelope file to be sent + // envelope file to be sent or became a previous_session file // so deleting it as the new session will take place. if (!currentSessionFile.delete()) { options.getLogger().log(WARNING, "Failed to delete the current session file."); @@ -209,6 +223,68 @@ public void store(final @NotNull SentryEnvelope envelope, final @NotNull Hint hi } } + /** + * Attempts to end previous session, relying on PreviousSessionEnd hint. If the hint is also + * AbnormalExit, marks session as abnormal with abnormal mechanism and takes its timestamp. + *

+ * If there was no abnormal exit, the previous session will be captured with the current session + * at latest, preserving the original end timestamp. + *

+ * Otherwise, callers might also call it directly when necessary. + * + * @param hint a hint coming with the envelope + */ + public void endPreviousSession(final @NotNull Hint hint) { + if (HintUtils.hasType(hint, PreviousSessionEnd.class)) { + final File previousSessionFile = getPreviousSessionFile(); + if (previousSessionFile.exists()) { + options.getLogger().log(WARNING, "Previous session is not ended, we'd need to end it."); + + final ISerializer serializer = options.getSerializer(); + try (final Reader reader = + new BufferedReader( + new InputStreamReader(new FileInputStream(previousSessionFile), UTF_8))) { + final Session previousSession = serializer.deserialize(reader, Session.class); + if (previousSession != null) { + final Object sdkHint = HintUtils.getSentrySdkHint(hint); + if (sdkHint instanceof AbnormalExit) { + final Long abnormalExitTimestamp = ((AbnormalExit) sdkHint).timestamp(); + if (abnormalExitTimestamp != null) { + final Date timestamp = DateUtils.getDateTime(abnormalExitTimestamp); + // sanity check if the abnormal exit actually happened when the session was alive + final Date sessionStart = previousSession.getStarted(); + if (sessionStart == null || timestamp.before(sessionStart)) { + options.getLogger().log(WARNING, "Abnormal exit happened before previous session start, not ending the session."); + return; + } + } + final String abnormalMechanism = ((AbnormalExit) sdkHint).mechanism(); + previousSession.update(Session.State.Abnormal, null, true, abnormalMechanism); + } + saveSessionToEnvelope(previousSession); + + // at this point the previous session and its file already became a new envelope, so + // it's safe to delete it + if (!previousSessionFile.delete()) { + options.getLogger().log(WARNING, "Failed to delete the previous session file."); + } + } + } catch (Throwable e) { + options.getLogger().log(SentryLevel.ERROR, "Error processing previous session.", e); + } + } + } + } + + private void saveSessionToEnvelope(final @NotNull Session session) throws IOException { + // if the App. has been upgraded and there's a new version of the SDK running, + // SdkVersion will be outdated. + final SentryEnvelope fromSession = + SentryEnvelope.from(serializer, session, options.getSdkVersion()); + final File fileFromSession = getEnvelopeFile(fromSession); + writeEnvelopeToDisk(fileFromSession, fromSession); + } + private void writeCrashMarkerFile() { final File crashMarkerFile = new File(options.getCacheDirPath(), CRASH_MARKER_FILE); try (final OutputStream outputStream = new FileOutputStream(crashMarkerFile)) { @@ -367,7 +443,12 @@ public void discard(final @NotNull SentryEnvelope envelope) { private @NotNull File getCurrentSessionFile() { return new File( - directory.getAbsolutePath(), PREFIX_CURRENT_SESSION_FILE + SUFFIX_CURRENT_SESSION_FILE); + directory.getAbsolutePath(), PREFIX_CURRENT_SESSION_FILE + SUFFIX_SESSION_FILE); + } + + private @NotNull File getPreviousSessionFile() { + return new File( + directory.getAbsolutePath(), PREFIX_PREVIOUS_SESSION_FILE + SUFFIX_SESSION_FILE); } @Override diff --git a/sentry/src/main/java/io/sentry/hints/AbnormalExit.java b/sentry/src/main/java/io/sentry/hints/AbnormalExit.java index 6b3f0472a0..d8d5d30454 100644 --- a/sentry/src/main/java/io/sentry/hints/AbnormalExit.java +++ b/sentry/src/main/java/io/sentry/hints/AbnormalExit.java @@ -13,4 +13,10 @@ public interface AbnormalExit { default boolean ignoreCurrentThread() { return false; } + + /** When exactly the abnormal exit happened */ + @Nullable + default Long timestamp() { + return null; + } } diff --git a/sentry/src/main/java/io/sentry/hints/AllSessionsEndHint.java b/sentry/src/main/java/io/sentry/hints/AllSessionsEndHint.java new file mode 100644 index 0000000000..c423b90e99 --- /dev/null +++ b/sentry/src/main/java/io/sentry/hints/AllSessionsEndHint.java @@ -0,0 +1,5 @@ +package io.sentry.hints; + +/** An aggregator hint which marks envelopes to end all pending sessions */ +public class AllSessionsEndHint implements SessionEnd, PreviousSessionEnd { +} diff --git a/sentry/src/main/java/io/sentry/hints/PreviousSessionEnd.java b/sentry/src/main/java/io/sentry/hints/PreviousSessionEnd.java new file mode 100644 index 0000000000..0aaa305cf9 --- /dev/null +++ b/sentry/src/main/java/io/sentry/hints/PreviousSessionEnd.java @@ -0,0 +1,5 @@ +package io.sentry.hints; + +/** Hint that shows this is a previous session end envelope */ +public interface PreviousSessionEnd { +} diff --git a/sentry/src/main/java/io/sentry/hints/PreviousSessionEndHint.java b/sentry/src/main/java/io/sentry/hints/PreviousSessionEndHint.java new file mode 100644 index 0000000000..e87b9832c8 --- /dev/null +++ b/sentry/src/main/java/io/sentry/hints/PreviousSessionEndHint.java @@ -0,0 +1,4 @@ +package io.sentry.hints; + +public class PreviousSessionEndHint implements PreviousSessionEnd { +} diff --git a/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt b/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt index 2a4ee2dc40..a00c6c9e6b 100644 --- a/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt +++ b/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt @@ -11,7 +11,7 @@ import io.sentry.SentryOptions import io.sentry.Session import io.sentry.UncaughtExceptionHandlerIntegration.UncaughtExceptionHint import io.sentry.cache.EnvelopeCache.PREFIX_CURRENT_SESSION_FILE -import io.sentry.cache.EnvelopeCache.SUFFIX_CURRENT_SESSION_FILE +import io.sentry.cache.EnvelopeCache.SUFFIX_SESSION_FILE import io.sentry.hints.SessionEndHint import io.sentry.hints.SessionStartHint import io.sentry.protocol.User @@ -96,7 +96,7 @@ class EnvelopeCacheTest { val hints = HintUtils.createWithTypeCheckHint(SessionStartHint()) cache.store(envelope, hints) - val currentFile = File(fixture.options.cacheDirPath!!, "$PREFIX_CURRENT_SESSION_FILE$SUFFIX_CURRENT_SESSION_FILE") + val currentFile = File(fixture.options.cacheDirPath!!, "$PREFIX_CURRENT_SESSION_FILE$SUFFIX_SESSION_FILE") assertTrue(currentFile.exists()) file.deleteRecursively() @@ -113,7 +113,7 @@ class EnvelopeCacheTest { val hints = HintUtils.createWithTypeCheckHint(SessionStartHint()) cache.store(envelope, hints) - val currentFile = File(fixture.options.cacheDirPath!!, "$PREFIX_CURRENT_SESSION_FILE$SUFFIX_CURRENT_SESSION_FILE") + val currentFile = File(fixture.options.cacheDirPath!!, "$PREFIX_CURRENT_SESSION_FILE$SUFFIX_SESSION_FILE") assertTrue(currentFile.exists()) HintUtils.setTypeCheckHint(hints, SessionEndHint()) @@ -134,7 +134,7 @@ class EnvelopeCacheTest { val hints = HintUtils.createWithTypeCheckHint(SessionStartHint()) cache.store(envelope, hints) - val currentFile = File(fixture.options.cacheDirPath!!, "$PREFIX_CURRENT_SESSION_FILE$SUFFIX_CURRENT_SESSION_FILE") + val currentFile = File(fixture.options.cacheDirPath!!, "$PREFIX_CURRENT_SESSION_FILE$SUFFIX_SESSION_FILE") assertTrue(currentFile.exists()) val session = fixture.serializer.deserialize(currentFile.bufferedReader(Charsets.UTF_8), Session::class.java) From ab73382a99dba98ecbd4a72ff4c5f4866956600a Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 23 Mar 2023 12:40:44 +0100 Subject: [PATCH 03/23] Fix tests --- .../test/java/io/sentry/android/core/AnrV2IntegrationTest.kt | 4 ++-- .../io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt | 4 ++-- sentry/src/main/java/io/sentry/cache/EnvelopeCache.java | 1 + sentry/src/main/java/io/sentry/hints/AllSessionsEndHint.java | 3 +-- .../src/main/java/io/sentry/hints/PreviousSessionEndHint.java | 3 +-- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt index 6a8474ba9c..648a514ab0 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt @@ -135,11 +135,11 @@ class AnrV2IntegrationTest { @Test fun `when historical exit list is empty, does not process historical exits`() { - val integration = fixture.getSut(tmpDir, useImmediateExecutorService = false) + val integration = fixture.getSut(tmpDir) integration.register(fixture.hub, fixture.options) - verify(fixture.options.executorService, never()).submit(any()) + verify(fixture.hub, never()).captureEvent(any(), anyOrNull()) } @Test diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt index 7eb8e0baab..fafaf9028d 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt @@ -176,12 +176,12 @@ class AndroidEnvelopeCacheTest { } @Test - fun `when last reported anr file does not exist, returns 0 upon reading`() { + fun `when last reported anr file does not exist, returns null upon reading`() { fixture.getSut(tmpDir) val lastReportedAnr = AndroidEnvelopeCache.lastReportedAnr(fixture.options) - assertEquals(0L, lastReportedAnr) + assertEquals(null, lastReportedAnr) } @Test diff --git a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java index e3e1318b0e..75121723e8 100644 --- a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java +++ b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java @@ -234,6 +234,7 @@ public void store(final @NotNull SentryEnvelope envelope, final @NotNull Hint hi * * @param hint a hint coming with the envelope */ + @SuppressWarnings("JavaUtilDate") public void endPreviousSession(final @NotNull Hint hint) { if (HintUtils.hasType(hint, PreviousSessionEnd.class)) { final File previousSessionFile = getPreviousSessionFile(); diff --git a/sentry/src/main/java/io/sentry/hints/AllSessionsEndHint.java b/sentry/src/main/java/io/sentry/hints/AllSessionsEndHint.java index c423b90e99..792a23aaea 100644 --- a/sentry/src/main/java/io/sentry/hints/AllSessionsEndHint.java +++ b/sentry/src/main/java/io/sentry/hints/AllSessionsEndHint.java @@ -1,5 +1,4 @@ package io.sentry.hints; /** An aggregator hint which marks envelopes to end all pending sessions */ -public class AllSessionsEndHint implements SessionEnd, PreviousSessionEnd { -} +public final class AllSessionsEndHint implements SessionEnd, PreviousSessionEnd {} diff --git a/sentry/src/main/java/io/sentry/hints/PreviousSessionEndHint.java b/sentry/src/main/java/io/sentry/hints/PreviousSessionEndHint.java index e87b9832c8..78ad92da7d 100644 --- a/sentry/src/main/java/io/sentry/hints/PreviousSessionEndHint.java +++ b/sentry/src/main/java/io/sentry/hints/PreviousSessionEndHint.java @@ -1,4 +1,3 @@ package io.sentry.hints; -public class PreviousSessionEndHint implements PreviousSessionEnd { -} +public final class PreviousSessionEndHint implements PreviousSessionEnd {} From 63a8f7ef563dd0e8aa1315a55ab318d169855574 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Thu, 23 Mar 2023 11:46:12 +0000 Subject: [PATCH 04/23] Format code --- .../sentry/android/core/AnrV2Integration.java | 30 ++++++++-------- .../core/cache/AndroidEnvelopeCache.java | 1 - .../core/cache/AndroidEnvelopeCacheTest.kt | 6 ++-- .../java/io/sentry/cache/EnvelopeCache.java | 35 +++++++++++-------- .../io/sentry/hints/PreviousSessionEnd.java | 3 +- 5 files changed, 39 insertions(+), 36 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java index 640b0756da..8fce3cb50b 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java @@ -82,12 +82,7 @@ public void register(@NotNull IHub hub, @NotNull SentryOptions options) { if (this.options.isAnrEnabled()) { options .getExecutorService() - .submit( - new AnrProcessor( - context, - hub, - this.options, - dateProvider)); + .submit(new AnrProcessor(context, hub, this.options, dateProvider)); options.getLogger().log(SentryLevel.DEBUG, "AnrV2Integration installed."); addIntegrationToSdkVersion(); } @@ -122,10 +117,10 @@ static class AnrProcessor implements Runnable { @Override public void run() { final ActivityManager activityManager = - (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); List applicationExitInfoList = - activityManager.getHistoricalProcessExitReasons(null, 0, 0); + activityManager.getHistoricalProcessExitReasons(null, 0, 0); if (applicationExitInfoList.size() == 0) { endPreviousSession(); options.getLogger().log(SentryLevel.DEBUG, "No records in historical exit reasons."); @@ -164,7 +159,8 @@ public void run() { return; } - if (lastReportedAnrTimestamp != null && latestAnr.getTimestamp() <= lastReportedAnrTimestamp) { + if (lastReportedAnrTimestamp != null + && latestAnr.getTimestamp() <= lastReportedAnrTimestamp) { endPreviousSession(); options .getLogger() @@ -212,11 +208,15 @@ private void reportAsSentryEvent( final @NotNull ApplicationExitInfo exitInfo, final boolean shouldEnrich) { final long anrTimestamp = exitInfo.getTimestamp(); final boolean isBackground = - exitInfo.getImportance() != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; + exitInfo.getImportance() != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; final Throwable anrThrowable = buildAnrThrowable(exitInfo, isBackground); final AnrV2Hint anrHint = new AnrV2Hint( - options.getFlushTimeoutMillis(), options.getLogger(), anrTimestamp, shouldEnrich, isBackground); + options.getFlushTimeoutMillis(), + options.getLogger(), + anrTimestamp, + shouldEnrich, + isBackground); final Hint hint = HintUtils.createWithTypeCheckHint(anrHint); @@ -240,9 +240,7 @@ private void reportAsSentryEvent( } private @NotNull Throwable buildAnrThrowable( - final @NotNull ApplicationExitInfo exitInfo, - final boolean isBackground - ) { + final @NotNull ApplicationExitInfo exitInfo, final boolean isBackground) { String message = "ANR"; if (isBackground) { message = "Background " + message; @@ -267,8 +265,8 @@ private void endPreviousSession() { } @ApiStatus.Internal - public static final class AnrV2Hint extends BlockingFlushHint implements Backfillable, - AbnormalExit, PreviousSessionEnd { + public static final class AnrV2Hint extends BlockingFlushHint + implements Backfillable, AbnormalExit, PreviousSessionEnd { private final long timestamp; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java b/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java index 102d8f5688..bd3e0809b5 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java @@ -13,7 +13,6 @@ import io.sentry.android.core.SentryAndroidOptions; import io.sentry.android.core.internal.util.AndroidCurrentDateProvider; import io.sentry.cache.EnvelopeCache; -import io.sentry.hints.AbnormalExit; import io.sentry.transport.ICurrentDateProvider; import io.sentry.util.FileUtils; import io.sentry.util.HintUtils; diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt index fafaf9028d..095da5f32a 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt @@ -136,7 +136,8 @@ class AndroidEnvelopeCacheTest { 0, NoOpLogger.getInstance(), 12345678L, - false, false + false, + false ) ) cache.store(fixture.envelope, hints) @@ -153,7 +154,8 @@ class AndroidEnvelopeCacheTest { 0, NoOpLogger.getInstance(), 12345678L, - false, false + false, + false ) ) cache.store(fixture.envelope, hints) diff --git a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java index 75121723e8..ebaaeeb2da 100644 --- a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java +++ b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java @@ -104,10 +104,11 @@ public void store(final @NotNull SentryEnvelope envelope, final @NotNull Hint hi endPreviousSession(hint); /** - * Common cases when previous session is not ended with a SessionStart hint for envelope: - * - The previous session experienced Abnormal exit (ANR, OS kills app, User kills app) - * - The previous session experienced native crash - * - The previous session hasn't been ended properly (e.g. session was started in .init() and then in onForeground() after sessionTrackingIntervalMillis) + * Common cases when previous session is not ended with a SessionStart hint for envelope: - The + * previous session experienced Abnormal exit (ANR, OS kills app, User kills app) - The previous + * session experienced native crash - The previous session hasn't been ended properly (e.g. + * session was started in .init() and then in onForeground() after + * sessionTrackingIntervalMillis) */ if (HintUtils.hasType(hint, SessionStart.class)) { boolean crashedLastRun = false; @@ -156,7 +157,8 @@ public void store(final @NotNull SentryEnvelope envelope, final @NotNull Hint hi } else { // if there was no native crash, the session has potentially experienced Abnormal exit // so we end it with the current timestamp, but do not send it yet, as other envelopes - // may come later and change its attributes (status, etc.). We just save it as previous_session.json + // may come later and change its attributes (status, etc.). We just save it as + // previous_session.json session.end(); writeSessionToDisk(getPreviousSessionFile(), session); } @@ -226,11 +228,11 @@ public void store(final @NotNull SentryEnvelope envelope, final @NotNull Hint hi /** * Attempts to end previous session, relying on PreviousSessionEnd hint. If the hint is also * AbnormalExit, marks session as abnormal with abnormal mechanism and takes its timestamp. - *

- * If there was no abnormal exit, the previous session will be captured with the current session - * at latest, preserving the original end timestamp. - *

- * Otherwise, callers might also call it directly when necessary. + * + *

If there was no abnormal exit, the previous session will be captured with the current + * session at latest, preserving the original end timestamp. + * + *

Otherwise, callers might also call it directly when necessary. * * @param hint a hint coming with the envelope */ @@ -243,8 +245,8 @@ public void endPreviousSession(final @NotNull Hint hint) { final ISerializer serializer = options.getSerializer(); try (final Reader reader = - new BufferedReader( - new InputStreamReader(new FileInputStream(previousSessionFile), UTF_8))) { + new BufferedReader( + new InputStreamReader(new FileInputStream(previousSessionFile), UTF_8))) { final Session previousSession = serializer.deserialize(reader, Session.class); if (previousSession != null) { final Object sdkHint = HintUtils.getSentrySdkHint(hint); @@ -255,7 +257,11 @@ public void endPreviousSession(final @NotNull Hint hint) { // sanity check if the abnormal exit actually happened when the session was alive final Date sessionStart = previousSession.getStarted(); if (sessionStart == null || timestamp.before(sessionStart)) { - options.getLogger().log(WARNING, "Abnormal exit happened before previous session start, not ending the session."); + options + .getLogger() + .log( + WARNING, + "Abnormal exit happened before previous session start, not ending the session."); return; } } @@ -443,8 +449,7 @@ public void discard(final @NotNull SentryEnvelope envelope) { } private @NotNull File getCurrentSessionFile() { - return new File( - directory.getAbsolutePath(), PREFIX_CURRENT_SESSION_FILE + SUFFIX_SESSION_FILE); + return new File(directory.getAbsolutePath(), PREFIX_CURRENT_SESSION_FILE + SUFFIX_SESSION_FILE); } private @NotNull File getPreviousSessionFile() { diff --git a/sentry/src/main/java/io/sentry/hints/PreviousSessionEnd.java b/sentry/src/main/java/io/sentry/hints/PreviousSessionEnd.java index 0aaa305cf9..088a21b2f8 100644 --- a/sentry/src/main/java/io/sentry/hints/PreviousSessionEnd.java +++ b/sentry/src/main/java/io/sentry/hints/PreviousSessionEnd.java @@ -1,5 +1,4 @@ package io.sentry.hints; /** Hint that shows this is a previous session end envelope */ -public interface PreviousSessionEnd { -} +public interface PreviousSessionEnd {} From a8efc613a19b8830c790723ac094bc9ee5ec6840 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 29 Mar 2023 18:59:31 +0200 Subject: [PATCH 05/23] 2nd attempt --- .../android/core/AnrV2EventProcessor.java | 4 +- .../sentry/android/core/AnrV2Integration.java | 11 +- .../io/sentry/android/core/SentryAndroid.java | 6 +- sentry/src/main/java/io/sentry/Hub.java | 19 +- .../io/sentry/PreviousSessionFinalizer.java | 130 +++++++++++ .../main/java/io/sentry/SentryEnvelope.java | 6 + .../java/io/sentry/cache/CacheStrategy.java | 2 +- .../java/io/sentry/cache/EnvelopeCache.java | 217 +++++++----------- .../io/sentry/hints/AllSessionsEndHint.java | 4 - 9 files changed, 247 insertions(+), 152 deletions(-) create mode 100644 sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java delete mode 100644 sentry/src/main/java/io/sentry/hints/AllSessionsEndHint.java diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java index 80d8faaf9d..466d4dc8c3 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java @@ -300,7 +300,7 @@ private void setApp(final @NotNull SentryBaseEvent event) { } catch (Throwable e) { options .getLogger() - .log(SentryLevel.ERROR, "Failed to parse release from scope cache: %s", release); + .log(SentryLevel.WARNING, "Failed to parse release from scope cache: %s", release); } } @@ -361,7 +361,7 @@ private void setDist(final @NotNull SentryBaseEvent event) { } catch (Throwable e) { options .getLogger() - .log(SentryLevel.ERROR, "Failed to parse release from scope cache: %s", release); + .log(SentryLevel.WARNING, "Failed to parse release from scope cache: %s", release); } } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java index 640b0756da..d99293e313 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java @@ -5,11 +5,13 @@ import android.app.ApplicationExitInfo; import android.content.Context; import android.os.Looper; +import android.util.Log; import io.sentry.DateUtils; import io.sentry.Hint; import io.sentry.IHub; import io.sentry.ILogger; import io.sentry.Integration; +import io.sentry.SentryEnvelope; import io.sentry.SentryEvent; import io.sentry.SentryLevel; import io.sentry.SentryOptions; @@ -29,6 +31,7 @@ import io.sentry.util.HintUtils; import io.sentry.util.Objects; import java.io.Closeable; +import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -261,7 +264,13 @@ private void endPreviousSession() { final IEnvelopeCache envelopeCache = options.getEnvelopeDiskCache(); if (envelopeCache instanceof EnvelopeCache) { final Hint hint = HintUtils.createWithTypeCheckHint(new PreviousSessionEndHint()); - ((EnvelopeCache) envelopeCache).endPreviousSession(hint); + final SentryEnvelope sessionEnvelope = + ((EnvelopeCache) envelopeCache).endPreviousSession(SentryEnvelope.empty(), hint); + + // if there was no ANRs, we just capture the previous session asap, so it's not delayed for long + if (sessionEnvelope.getHeader().getEventId() != SentryId.EMPTY_ID) { + hub.captureEnvelope(sessionEnvelope); + } } } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java index eb33039a71..b90562f87e 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java @@ -125,8 +125,10 @@ public static synchronized void init( final @NotNull IHub hub = Sentry.getCurrentHub(); if (hub.getOptions().isEnableAutoSessionTracking() && ContextUtils.isForegroundImportance(context)) { - hub.addBreadcrumb(BreadcrumbFactory.forSession("session.start")); - hub.startSession(); + //hub.getOptions().getExecutorService().submit(() -> { + hub.addBreadcrumb(BreadcrumbFactory.forSession("session.start")); + hub.startSession(); + //}); } } catch (IllegalAccessException e) { logger.log(SentryLevel.FATAL, "Fatal error during SentryAndroid.init(...)", e); diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index 63ef82803b..6a7b5292e7 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -2,7 +2,6 @@ import io.sentry.Stack.StackItem; import io.sentry.clientreport.DiscardReason; -import io.sentry.hints.AllSessionsEndHint; import io.sentry.hints.SessionEndHint; import io.sentry.hints.SessionStartHint; import io.sentry.protocol.SentryId; @@ -318,12 +317,16 @@ public void endSession() { .log(SentryLevel.WARNING, "Instance is disabled and this 'endSession' call is a no-op."); } else { final StackItem item = this.stack.peek(); - final Session previousSession = item.getScope().endSession(); - if (previousSession != null) { - final Hint hint = HintUtils.createWithTypeCheckHint(new AllSessionsEndHint()); + endSessionInternal(item); + } + } - item.getClient().captureSession(previousSession, hint); - } + private void endSessionInternal(final @NotNull StackItem item) { + final Session previousSession = item.getScope().endSession(); + if (previousSession != null) { + final Hint hint = HintUtils.createWithTypeCheckHint(new SessionEndHint()); + + item.getClient().captureSession(previousSession, hint); } } @@ -344,7 +347,9 @@ public void close() { // Close the top-most client final StackItem item = stack.peek(); - // TODO: should we end session before closing client? + + // end session before closing the client, close() of the client will wait for it to be flushed + endSessionInternal(item); item.getClient().close(); } catch (Throwable e) { options.getLogger().log(SentryLevel.ERROR, "Error while closing the Hub.", e); diff --git a/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java b/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java new file mode 100644 index 0000000000..c8ed7598f7 --- /dev/null +++ b/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java @@ -0,0 +1,130 @@ +package io.sentry; + +import io.sentry.cache.EnvelopeCache; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.Charset; +import java.util.Date; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static io.sentry.SentryLevel.DEBUG; +import static io.sentry.SentryLevel.ERROR; +import static io.sentry.SentryLevel.INFO; +import static io.sentry.SentryLevel.WARNING; +import static io.sentry.cache.EnvelopeCache.NATIVE_CRASH_MARKER_FILE; + +/** + * Common cases when previous session is not ended properly (app background or crash): + * - The previous session experienced Abnormal exit (ANR, OS kills app, User kills app) + * - The previous session experienced native crash + */ +final class PreviousSessionFinalizer implements Runnable { + + @SuppressWarnings("CharsetObjectCanBeUsed") + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + private final @NotNull SentryOptions options; + + private final @NotNull IHub hub; + + PreviousSessionFinalizer(final @NotNull SentryOptions options, final @NotNull IHub hub) { + this.options = options; + this.hub = hub; + } + + @Override public void run() { + final String cacheDirPath = options.getCacheDirPath(); + if (cacheDirPath == null) { + options.getLogger().log(INFO, "Cache dir is not set, not finalizing the previous session."); + return; + } + + final File previousSessionFile = EnvelopeCache.getPreviousSessionFile(cacheDirPath); + final ISerializer serializer = options.getSerializer(); + + if (previousSessionFile.exists()) { + options.getLogger().log(WARNING, "Current session is not ended, we'd need to end it."); + + try (final Reader reader = + new BufferedReader( + new InputStreamReader(new FileInputStream(previousSessionFile), UTF_8))) { + + final Session session = serializer.deserialize(reader, Session.class); + if (session == null) { + options + .getLogger() + .log( + SentryLevel.ERROR, + "Stream from path %s resulted in a null envelope.", + previousSessionFile.getAbsolutePath()); + } else { + final File crashMarkerFile = + new File(options.getCacheDirPath(), NATIVE_CRASH_MARKER_FILE); + if (crashMarkerFile.exists()) { + options + .getLogger() + .log(INFO, "Crash marker file exists, last Session is gonna be Crashed."); + + final Date timestamp = getTimestampFromCrashMarkerFile(crashMarkerFile); + + if (!crashMarkerFile.delete()) { + options + .getLogger() + .log( + ERROR, + "Failed to delete the crash marker file. %s.", + crashMarkerFile.getAbsolutePath()); + } + session.update(Session.State.Crashed, null, true); + session.end(timestamp); + // if the App. has been upgraded and there's a new version of the SDK running, + // SdkVersion will be outdated. + final SentryEnvelope fromSession = + SentryEnvelope.from(serializer, session, options.getSdkVersion()); + hub.captureEnvelope(fromSession); + } else { + // if there was no native crash, the session has potentially experienced Abnormal exit + // so we end it with the current timestamp, but do not send it yet, as other envelopes + // may come later and change its attributes (status, etc.). We just save it as previous_session.json + session.end(); + writeSessionToDisk(getPreviousSessionFile(), session); + } + } + } catch (Throwable e) { + options.getLogger().log(SentryLevel.ERROR, "Error processing previous session.", e); + } + + // at this point the leftover session and its current session file already became a new + // envelope file to be sent or became a previous_session file + // so deleting it as the new session will take place. + if (!previousSessionFile.delete()) { + options.getLogger().log(WARNING, "Failed to delete the previous session file."); + } + } + } + + /** + * Reads the crash marker file and returns the timestamp as Date written in there + * + * @param markerFile the marker file + * @return the timestamp as Date + */ + private @Nullable Date getTimestampFromCrashMarkerFile(final @NotNull File markerFile) { + try (final BufferedReader reader = + new BufferedReader(new InputStreamReader(new FileInputStream(markerFile), UTF_8))) { + final String timestamp = reader.readLine(); + options.getLogger().log(DEBUG, "Crash marker file has %s timestamp.", timestamp); + return DateUtils.getDateTime(timestamp); + } catch (IOException e) { + options.getLogger().log(ERROR, "Error reading the crash marker file.", e); + } catch (IllegalArgumentException e) { + options.getLogger().log(SentryLevel.ERROR, e, "Error converting the crash timestamp."); + } + return null; + } +} diff --git a/sentry/src/main/java/io/sentry/SentryEnvelope.java b/sentry/src/main/java/io/sentry/SentryEnvelope.java index 5c3f141baf..90bba8c901 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelope.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelope.java @@ -3,9 +3,11 @@ import io.sentry.exception.SentryEnvelopeException; import io.sentry.protocol.SdkVersion; import io.sentry.protocol.SentryId; +import io.sentry.util.CollectionUtils; import io.sentry.util.Objects; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -92,4 +94,8 @@ public SentryEnvelope( sdkVersion, SentryEnvelopeItem.fromProfilingTrace(profilingTraceData, maxTraceFileSize, serializer)); } + + public static @NotNull SentryEnvelope empty() { + return new SentryEnvelope(new SentryEnvelopeHeader(SentryId.EMPTY_ID), Collections.emptyList()); + } } diff --git a/sentry/src/main/java/io/sentry/cache/CacheStrategy.java b/sentry/src/main/java/io/sentry/cache/CacheStrategy.java index dbb6a49c19..a162a6b357 100644 --- a/sentry/src/main/java/io/sentry/cache/CacheStrategy.java +++ b/sentry/src/main/java/io/sentry/cache/CacheStrategy.java @@ -276,7 +276,7 @@ private void saveNewEnvelope( } } - private @NotNull SentryEnvelope buildNewEnvelope( + protected @NotNull SentryEnvelope buildNewEnvelope( final @NotNull SentryEnvelope envelope, final @NotNull SentryEnvelopeItem sessionItem) { final List newEnvelopeItems = new ArrayList<>(); diff --git a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java index 75121723e8..9340ce1b50 100644 --- a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java +++ b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java @@ -9,7 +9,6 @@ import com.jakewharton.nopen.annotation.Open; import io.sentry.DateUtils; import io.sentry.Hint; -import io.sentry.ISerializer; import io.sentry.SentryCrashLastRunState; import io.sentry.SentryEnvelope; import io.sentry.SentryEnvelopeItem; @@ -88,12 +87,12 @@ public EnvelopeCache( } @Override - public void store(final @NotNull SentryEnvelope envelope, final @NotNull Hint hint) { + public void store(@NotNull SentryEnvelope envelope, final @NotNull Hint hint) { Objects.requireNonNull(envelope, "Envelope is required."); rotateCacheIfNeeded(allEnvelopeFiles()); - final File currentSessionFile = getCurrentSessionFile(); + final File currentSessionFile = getCurrentSessionFile(directory.getAbsolutePath()); if (HintUtils.hasType(hint, SessionEnd.class)) { if (!currentSessionFile.delete()) { @@ -101,78 +100,17 @@ public void store(final @NotNull SentryEnvelope envelope, final @NotNull Hint hi } } - endPreviousSession(hint); - - /** - * Common cases when previous session is not ended with a SessionStart hint for envelope: - * - The previous session experienced Abnormal exit (ANR, OS kills app, User kills app) - * - The previous session experienced native crash - * - The previous session hasn't been ended properly (e.g. session was started in .init() and then in onForeground() after sessionTrackingIntervalMillis) - */ + // TODO: get rid of this, move it to the executor service, but after ANR, do native crash handling there, also endSession on close(), + // TODO: rename current session synchronously on init if (HintUtils.hasType(hint, SessionStart.class)) { - boolean crashedLastRun = false; - - // TODO: should we move this to AppLifecycleIntegration? and do on SDK init? but it's too much - // on main-thread - if (currentSessionFile.exists()) { - options.getLogger().log(WARNING, "Current session is not ended, we'd need to end it."); - - try (final Reader reader = - new BufferedReader( - new InputStreamReader(new FileInputStream(currentSessionFile), UTF_8))) { - - final Session session = serializer.deserialize(reader, Session.class); - if (session == null) { - options - .getLogger() - .log( - SentryLevel.ERROR, - "Stream from path %s resulted in a null envelope.", - currentSessionFile.getAbsolutePath()); - } else { - final File crashMarkerFile = - new File(options.getCacheDirPath(), NATIVE_CRASH_MARKER_FILE); - if (crashMarkerFile.exists()) { - options - .getLogger() - .log(INFO, "Crash marker file exists, last Session is gonna be Crashed."); - - final Date timestamp = getTimestampFromCrashMarkerFile(crashMarkerFile); - - crashedLastRun = true; - if (!crashMarkerFile.delete()) { - options - .getLogger() - .log( - ERROR, - "Failed to delete the crash marker file. %s.", - crashMarkerFile.getAbsolutePath()); - } - session.update(Session.State.Crashed, null, true); - session.end(timestamp); - // if it was a native crash, we are safe to send session in an envelope, meaning - // there was no abnormal exit - saveSessionToEnvelope(session); - } else { - // if there was no native crash, the session has potentially experienced Abnormal exit - // so we end it with the current timestamp, but do not send it yet, as other envelopes - // may come later and change its attributes (status, etc.). We just save it as previous_session.json - session.end(); - writeSessionToDisk(getPreviousSessionFile(), session); - } - } - } catch (Throwable e) { - options.getLogger().log(SentryLevel.ERROR, "Error processing session.", e); - } + updateCurrentSession(currentSessionFile, envelope); - // at this point the leftover session and its current session file already became a new - // envelope file to be sent or became a previous_session file - // so deleting it as the new session will take place. - if (!currentSessionFile.delete()) { - options.getLogger().log(WARNING, "Failed to delete the current session file."); - } + boolean crashedLastRun = false; + final File crashMarkerFile = + new File(options.getCacheDirPath(), NATIVE_CRASH_MARKER_FILE); + if (crashMarkerFile.exists()) { + crashedLastRun = true; } - updateCurrentSession(currentSessionFile, envelope); // check java marker file if the native marker isnt there if (!crashedLastRun) { @@ -233,57 +171,86 @@ public void store(final @NotNull SentryEnvelope envelope, final @NotNull Hint hi * Otherwise, callers might also call it directly when necessary. * * @param hint a hint coming with the envelope + * @param envelope an original envelope that is being stored + * @return SentryEnvelope returns either a new envelope containing previous session in it, or an + * old one, if the previous session should not be sent with the current envelope. */ @SuppressWarnings("JavaUtilDate") - public void endPreviousSession(final @NotNull Hint hint) { + public @NotNull SentryEnvelope endPreviousSession( + final @NotNull SentryEnvelope envelope, + final @NotNull Hint hint + ) { if (HintUtils.hasType(hint, PreviousSessionEnd.class)) { - final File previousSessionFile = getPreviousSessionFile(); + final File previousSessionFile = getPreviousSessionFile(directory.getAbsolutePath()); + final Object sdkHint = HintUtils.getSentrySdkHint(hint); + if (previousSessionFile.exists()) { options.getLogger().log(WARNING, "Previous session is not ended, we'd need to end it."); - final ISerializer serializer = options.getSerializer(); - try (final Reader reader = - new BufferedReader( - new InputStreamReader(new FileInputStream(previousSessionFile), UTF_8))) { - final Session previousSession = serializer.deserialize(reader, Session.class); - if (previousSession != null) { - final Object sdkHint = HintUtils.getSentrySdkHint(hint); - if (sdkHint instanceof AbnormalExit) { - final Long abnormalExitTimestamp = ((AbnormalExit) sdkHint).timestamp(); - if (abnormalExitTimestamp != null) { - final Date timestamp = DateUtils.getDateTime(abnormalExitTimestamp); - // sanity check if the abnormal exit actually happened when the session was alive - final Date sessionStart = previousSession.getStarted(); - if (sessionStart == null || timestamp.before(sessionStart)) { - options.getLogger().log(WARNING, "Abnormal exit happened before previous session start, not ending the session."); - return; - } - } - final String abnormalMechanism = ((AbnormalExit) sdkHint).mechanism(); - previousSession.update(Session.State.Abnormal, null, true, abnormalMechanism); - } - saveSessionToEnvelope(previousSession); + return endSessionIntoEnvelope(previousSessionFile, envelope, sdkHint); + } else { + options.getLogger().log(DEBUG, "No session file to end."); + } + } + return envelope; + } - // at this point the previous session and its file already became a new envelope, so - // it's safe to delete it - if (!previousSessionFile.delete()) { - options.getLogger().log(WARNING, "Failed to delete the previous session file."); + @SuppressWarnings("JavaUtilDate") + private @NotNull SentryEnvelope endSessionIntoEnvelope( + final @NotNull File sessionFile, + final @NotNull SentryEnvelope envelope, + final @Nullable Object sdkHint + ) { + try (final Reader reader = + new BufferedReader( + new InputStreamReader(new FileInputStream(sessionFile), UTF_8))) { + final Session session = serializer.deserialize(reader, Session.class); + if (session != null) { + if (sdkHint instanceof AbnormalExit) { + final AbnormalExit abnormalHint = (AbnormalExit) sdkHint; + final @Nullable Long abnormalExitTimestamp = abnormalHint.timestamp(); + Date timestamp = null; + + if (abnormalExitTimestamp != null) { + timestamp = DateUtils.getDateTime(abnormalExitTimestamp); + // sanity check if the abnormal exit actually happened when the session was alive + final Date sessionStart = session.getStarted(); + if (sessionStart == null || timestamp.before(sessionStart)) { + options.getLogger().log(WARNING, "Abnormal exit happened before previous session start, not ending the session."); + return envelope; } } - } catch (Throwable e) { - options.getLogger().log(SentryLevel.ERROR, "Error processing previous session.", e); + + final String abnormalMechanism = abnormalHint.mechanism(); + session.update(Session.State.Abnormal, null, true, abnormalMechanism); + // we have to use the actual timestamp of the Abnormal Exit here to mark the session + // as finished at the time it happened + session.end(timestamp); + } + + final SentryEnvelope newEnvelope; + // if an envelope has items, we send the session in the same envelope, otherwise + // it's a dummy envelope and we build a new envelope and return it + if (envelope.getItems().iterator().hasNext()) { + final SentryEnvelopeItem sessionItem = + SentryEnvelopeItem.fromSession(serializer, session); + newEnvelope = buildNewEnvelope(envelope, sessionItem); + } else { + newEnvelope = + SentryEnvelope.from(serializer, session, options.getSdkVersion()); } + + // at this point the session and its file already became a new envelope, so + // it's safe to delete it + if (!sessionFile.delete()) { + options.getLogger().log(WARNING, "Failed to delete the session file."); + } + return newEnvelope; } + } catch (Throwable e) { + options.getLogger().log(SentryLevel.ERROR, "Error processing previous session.", e); } - } - - private void saveSessionToEnvelope(final @NotNull Session session) throws IOException { - // if the App. has been upgraded and there's a new version of the SDK running, - // SdkVersion will be outdated. - final SentryEnvelope fromSession = - SentryEnvelope.from(serializer, session, options.getSdkVersion()); - final File fileFromSession = getEnvelopeFile(fromSession); - writeEnvelopeToDisk(fileFromSession, fromSession); + return envelope; } private void writeCrashMarkerFile() { @@ -297,26 +264,6 @@ private void writeCrashMarkerFile() { } } - /** - * Reads the crash marker file and returns the timestamp as Date written in there - * - * @param markerFile the marker file - * @return the timestamp as Date - */ - private @Nullable Date getTimestampFromCrashMarkerFile(final @NotNull File markerFile) { - try (final BufferedReader reader = - new BufferedReader(new InputStreamReader(new FileInputStream(markerFile), UTF_8))) { - final String timestamp = reader.readLine(); - options.getLogger().log(DEBUG, "Crash marker file has %s timestamp.", timestamp); - return DateUtils.getDateTime(timestamp); - } catch (IOException e) { - options.getLogger().log(ERROR, "Error reading the crash marker file.", e); - } catch (IllegalArgumentException e) { - options.getLogger().log(SentryLevel.ERROR, e, "Error converting the crash timestamp."); - } - return null; - } - private void updateCurrentSession( final @NotNull File currentSessionFile, final @NotNull SentryEnvelope envelope) { final Iterable items = envelope.getItems(); @@ -442,14 +389,14 @@ public void discard(final @NotNull SentryEnvelope envelope) { return new File(directory.getAbsolutePath(), fileName); } - private @NotNull File getCurrentSessionFile() { + public static @NotNull File getCurrentSessionFile(final @NotNull String cacheDirPath) { return new File( - directory.getAbsolutePath(), PREFIX_CURRENT_SESSION_FILE + SUFFIX_SESSION_FILE); + cacheDirPath, PREFIX_CURRENT_SESSION_FILE + SUFFIX_SESSION_FILE); } - private @NotNull File getPreviousSessionFile() { + public static @NotNull File getPreviousSessionFile(final @NotNull String cacheDirPath) { return new File( - directory.getAbsolutePath(), PREFIX_PREVIOUS_SESSION_FILE + SUFFIX_SESSION_FILE); + cacheDirPath, PREFIX_PREVIOUS_SESSION_FILE + SUFFIX_SESSION_FILE); } @Override diff --git a/sentry/src/main/java/io/sentry/hints/AllSessionsEndHint.java b/sentry/src/main/java/io/sentry/hints/AllSessionsEndHint.java deleted file mode 100644 index 792a23aaea..0000000000 --- a/sentry/src/main/java/io/sentry/hints/AllSessionsEndHint.java +++ /dev/null @@ -1,4 +0,0 @@ -package io.sentry.hints; - -/** An aggregator hint which marks envelopes to end all pending sessions */ -public final class AllSessionsEndHint implements SessionEnd, PreviousSessionEnd {} From bce19aec4f4198b602d4e6aab7f6a00045a13037 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 30 Mar 2023 15:27:31 +0200 Subject: [PATCH 06/23] 3rd attempt --- .../sentry/android/core/AnrV2Integration.java | 34 ++-- sentry/src/main/java/io/sentry/Hub.java | 18 +-- .../io/sentry/PreviousSessionFinalizer.java | 36 +++-- sentry/src/main/java/io/sentry/Sentry.java | 19 +++ .../java/io/sentry/cache/EnvelopeCache.java | 150 ++++++++++-------- .../io/sentry/hints/PreviousSessionEnd.java | 5 - .../sentry/hints/PreviousSessionEndHint.java | 3 - 7 files changed, 135 insertions(+), 130 deletions(-) delete mode 100644 sentry/src/main/java/io/sentry/hints/PreviousSessionEnd.java delete mode 100644 sentry/src/main/java/io/sentry/hints/PreviousSessionEndHint.java diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java index d99293e313..16b47bd004 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java @@ -5,25 +5,20 @@ import android.app.ApplicationExitInfo; import android.content.Context; import android.os.Looper; -import android.util.Log; import io.sentry.DateUtils; import io.sentry.Hint; import io.sentry.IHub; import io.sentry.ILogger; import io.sentry.Integration; -import io.sentry.SentryEnvelope; import io.sentry.SentryEvent; import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.android.core.cache.AndroidEnvelopeCache; import io.sentry.cache.EnvelopeCache; -import io.sentry.cache.IEnvelopeCache; import io.sentry.exception.ExceptionMechanismException; import io.sentry.hints.AbnormalExit; import io.sentry.hints.Backfillable; import io.sentry.hints.BlockingFlushHint; -import io.sentry.hints.PreviousSessionEnd; -import io.sentry.hints.PreviousSessionEndHint; import io.sentry.protocol.Mechanism; import io.sentry.protocol.SentryId; import io.sentry.transport.CurrentDateProvider; @@ -31,7 +26,6 @@ import io.sentry.util.HintUtils; import io.sentry.util.Objects; import java.io.Closeable; -import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -130,11 +124,18 @@ public void run() { List applicationExitInfoList = activityManager.getHistoricalProcessExitReasons(null, 0, 0); if (applicationExitInfoList.size() == 0) { - endPreviousSession(); options.getLogger().log(SentryLevel.DEBUG, "No records in historical exit reasons."); return; } + if (!EnvelopeCache.waitPreviousSessionFlush()) { + options + .getLogger() + .log( + SentryLevel.WARNING, + "Timed out waiting to flush previous session to its own file."); + } + // making a deep copy as we're modifying the list final List exitInfos = new ArrayList<>(applicationExitInfoList); final @Nullable Long lastReportedAnrTimestamp = AndroidEnvelopeCache.lastReportedAnr(options); @@ -152,7 +153,6 @@ public void run() { } if (latestAnr == null) { - endPreviousSession(); options .getLogger() .log(SentryLevel.DEBUG, "No ANRs have been found in the historical exit reasons list."); @@ -160,7 +160,6 @@ public void run() { } if (latestAnr.getTimestamp() < threshold) { - endPreviousSession(); options .getLogger() .log(SentryLevel.DEBUG, "Latest ANR happened too long ago, returning early."); @@ -168,7 +167,6 @@ public void run() { } if (lastReportedAnrTimestamp != null && latestAnr.getTimestamp() <= lastReportedAnrTimestamp) { - endPreviousSession(); options .getLogger() .log(SentryLevel.DEBUG, "Latest ANR has already been reported, returning early."); @@ -259,25 +257,11 @@ private void reportAsSentryEvent( mechanism.setType("ANRv2"); return new ExceptionMechanismException(mechanism, error, error.getThread(), true); } - - private void endPreviousSession() { - final IEnvelopeCache envelopeCache = options.getEnvelopeDiskCache(); - if (envelopeCache instanceof EnvelopeCache) { - final Hint hint = HintUtils.createWithTypeCheckHint(new PreviousSessionEndHint()); - final SentryEnvelope sessionEnvelope = - ((EnvelopeCache) envelopeCache).endPreviousSession(SentryEnvelope.empty(), hint); - - // if there was no ANRs, we just capture the previous session asap, so it's not delayed for long - if (sessionEnvelope.getHeader().getEventId() != SentryId.EMPTY_ID) { - hub.captureEnvelope(sessionEnvelope); - } - } - } } @ApiStatus.Internal public static final class AnrV2Hint extends BlockingFlushHint implements Backfillable, - AbnormalExit, PreviousSessionEnd { + AbnormalExit { private final long timestamp; diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index 6a7b5292e7..e22259195d 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -317,16 +317,12 @@ public void endSession() { .log(SentryLevel.WARNING, "Instance is disabled and this 'endSession' call is a no-op."); } else { final StackItem item = this.stack.peek(); - endSessionInternal(item); - } - } - - private void endSessionInternal(final @NotNull StackItem item) { - final Session previousSession = item.getScope().endSession(); - if (previousSession != null) { - final Hint hint = HintUtils.createWithTypeCheckHint(new SessionEndHint()); + final Session previousSession = item.getScope().endSession(); + if (previousSession != null) { + final Hint hint = HintUtils.createWithTypeCheckHint(new SessionEndHint()); - item.getClient().captureSession(previousSession, hint); + item.getClient().captureSession(previousSession, hint); + } } } @@ -347,9 +343,7 @@ public void close() { // Close the top-most client final StackItem item = stack.peek(); - - // end session before closing the client, close() of the client will wait for it to be flushed - endSessionInternal(item); + // TODO: should we end session before closing client? item.getClient().close(); } catch (Throwable e) { options.getLogger().log(SentryLevel.ERROR, "Error while closing the Hub.", e); diff --git a/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java b/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java index c8ed7598f7..01b9475c67 100644 --- a/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java +++ b/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java @@ -44,6 +44,15 @@ final class PreviousSessionFinalizer implements Runnable { return; } + if (!EnvelopeCache.waitPreviousSessionFlush()) { + options + .getLogger() + .log( + SentryLevel.WARNING, + "Timed out waiting to flush previous session to its own file in session finalizer."); + return; + } + final File previousSessionFile = EnvelopeCache.getPreviousSessionFile(cacheDirPath); final ISerializer serializer = options.getSerializer(); @@ -63,6 +72,7 @@ final class PreviousSessionFinalizer implements Runnable { "Stream from path %s resulted in a null envelope.", previousSessionFile.getAbsolutePath()); } else { + Date timestamp = null; final File crashMarkerFile = new File(options.getCacheDirPath(), NATIVE_CRASH_MARKER_FILE); if (crashMarkerFile.exists()) { @@ -70,7 +80,7 @@ final class PreviousSessionFinalizer implements Runnable { .getLogger() .log(INFO, "Crash marker file exists, last Session is gonna be Crashed."); - final Date timestamp = getTimestampFromCrashMarkerFile(crashMarkerFile); + timestamp = getTimestampFromCrashMarkerFile(crashMarkerFile); if (!crashMarkerFile.delete()) { options @@ -81,27 +91,21 @@ final class PreviousSessionFinalizer implements Runnable { crashMarkerFile.getAbsolutePath()); } session.update(Session.State.Crashed, null, true); - session.end(timestamp); - // if the App. has been upgraded and there's a new version of the SDK running, - // SdkVersion will be outdated. - final SentryEnvelope fromSession = - SentryEnvelope.from(serializer, session, options.getSdkVersion()); - hub.captureEnvelope(fromSession); - } else { - // if there was no native crash, the session has potentially experienced Abnormal exit - // so we end it with the current timestamp, but do not send it yet, as other envelopes - // may come later and change its attributes (status, etc.). We just save it as previous_session.json - session.end(); - writeSessionToDisk(getPreviousSessionFile(), session); } + session.end(timestamp); + + // if the App. has been upgraded and there's a new version of the SDK running, + // SdkVersion will be outdated. + final SentryEnvelope fromSession = + SentryEnvelope.from(serializer, session, options.getSdkVersion()); + hub.captureEnvelope(fromSession); } } catch (Throwable e) { options.getLogger().log(SentryLevel.ERROR, "Error processing previous session.", e); } - // at this point the leftover session and its current session file already became a new - // envelope file to be sent or became a previous_session file - // so deleting it as the new session will take place. + // at this point the previous session and its session file already became a new envelope file + // to be sent, so deleting it if (!previousSessionFile.delete()) { options.getLogger().log(WARNING, "Failed to delete the previous session file."); } diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 27abd369fc..fd6e71f430 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -17,6 +17,7 @@ import io.sentry.util.thread.NoOpMainThreadChecker; import java.io.File; import java.lang.reflect.InvocationTargetException; +import java.nio.file.Files; import java.util.Arrays; import java.util.List; import org.jetbrains.annotations.ApiStatus; @@ -224,9 +225,27 @@ private static synchronized void init( integration.register(HubAdapter.getInstance(), options); } + finalizePreviousSession(options, HubAdapter.getInstance()); + notifyOptionsObservers(options); } + private static void finalizePreviousSession( + final @NotNull SentryOptions options, + final @NotNull IHub hub + ) { + // enqueue a task to finalize previous session. Since the executor + // is single-threaded, this task will be enqueued sequentially after all integrations that have + // to modify the previous session have done their work, even if they do that async. + try { + options + .getExecutorService() + .submit(new PreviousSessionFinalizer(options, hub)); + } catch (Throwable e) { + options.getLogger().log(SentryLevel.DEBUG, "Failed to notify options observers.", e); + } + } + @SuppressWarnings("FutureReturnValueIgnored") private static void notifyOptionsObservers(final @NotNull SentryOptions options) { // enqueue a task to trigger the static options change for the observers. Since the executor diff --git a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java index 9340ce1b50..317126f760 100644 --- a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java +++ b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java @@ -18,7 +18,6 @@ import io.sentry.Session; import io.sentry.UncaughtExceptionHandlerIntegration; import io.sentry.hints.AbnormalExit; -import io.sentry.hints.PreviousSessionEnd; import io.sentry.hints.SessionEnd; import io.sentry.hints.SessionStart; import io.sentry.transport.NoOpEnvelopeCache; @@ -46,6 +45,8 @@ import java.util.Map; import java.util.UUID; import java.util.WeakHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -66,6 +67,8 @@ public class EnvelopeCache extends CacheStrategy implements IEnvelopeCache { public static final String STARTUP_CRASH_MARKER_FILE = "startup_crash"; + private static final CountDownLatch previousSessionLatch = new CountDownLatch(1); + private final @NotNull Map fileNameMap = new WeakHashMap<>(); public static @NotNull IEnvelopeCache create(final @NotNull SentryOptions options) { @@ -93,6 +96,7 @@ public void store(@NotNull SentryEnvelope envelope, final @NotNull Hint hint) { rotateCacheIfNeeded(allEnvelopeFiles()); final File currentSessionFile = getCurrentSessionFile(directory.getAbsolutePath()); + final File previousSessionFile = getPreviousSessionFile(directory.getAbsolutePath()); if (HintUtils.hasType(hint, SessionEnd.class)) { if (!currentSessionFile.delete()) { @@ -100,9 +104,25 @@ public void store(@NotNull SentryEnvelope envelope, final @NotNull Hint hint) { } } - // TODO: get rid of this, move it to the executor service, but after ANR, do native crash handling there, also endSession on close(), - // TODO: rename current session synchronously on init + envelope = endPreviousSessionForAbnormalExit(envelope, hint); + if (HintUtils.hasType(hint, SessionStart.class)) { + if (currentSessionFile.exists()) { + options.getLogger().log(WARNING, "Current session is not ended, we'd need to end it."); + + try (final Reader reader = + new BufferedReader( + new InputStreamReader(new FileInputStream(currentSessionFile), UTF_8))) { + final Session session = serializer.deserialize(reader, Session.class); + if (session != null) { + writeSessionToDisk(previousSessionFile, session); + } + } catch (Throwable e) { + options.getLogger().log(SentryLevel.ERROR, "Error processing session.", e); + } + } + previousSessionLatch.countDown(); + updateCurrentSession(currentSessionFile, envelope); boolean crashedLastRun = false; @@ -162,13 +182,10 @@ public void store(@NotNull SentryEnvelope envelope, final @NotNull Hint hint) { } /** - * Attempts to end previous session, relying on PreviousSessionEnd hint. If the hint is also - * AbnormalExit, marks session as abnormal with abnormal mechanism and takes its timestamp. - *

- * If there was no abnormal exit, the previous session will be captured with the current session - * at latest, preserving the original end timestamp. + * Attempts to end previous session, relying on AbnormalExit hint, marks session as abnormal with + * abnormal mechanism and takes its timestamp. *

- * Otherwise, callers might also call it directly when necessary. + * If there was no abnormal exit, the previous session will be captured by PreviousSessionFinalizer. * * @param hint a hint coming with the envelope * @param envelope an original envelope that is being stored @@ -176,79 +193,62 @@ public void store(@NotNull SentryEnvelope envelope, final @NotNull Hint hint) { * old one, if the previous session should not be sent with the current envelope. */ @SuppressWarnings("JavaUtilDate") - public @NotNull SentryEnvelope endPreviousSession( + public @NotNull SentryEnvelope endPreviousSessionForAbnormalExit( final @NotNull SentryEnvelope envelope, final @NotNull Hint hint ) { - if (HintUtils.hasType(hint, PreviousSessionEnd.class)) { + final Object sdkHint = HintUtils.getSentrySdkHint(hint); + if (sdkHint instanceof AbnormalExit) { final File previousSessionFile = getPreviousSessionFile(directory.getAbsolutePath()); - final Object sdkHint = HintUtils.getSentrySdkHint(hint); if (previousSessionFile.exists()) { options.getLogger().log(WARNING, "Previous session is not ended, we'd need to end it."); - return endSessionIntoEnvelope(previousSessionFile, envelope, sdkHint); - } else { - options.getLogger().log(DEBUG, "No session file to end."); - } - } - return envelope; - } + try (final Reader reader = + new BufferedReader( + new InputStreamReader(new FileInputStream(previousSessionFile), UTF_8))) { + final Session session = serializer.deserialize(reader, Session.class); + if (session != null) { + final AbnormalExit abnormalHint = (AbnormalExit) sdkHint; + final @Nullable Long abnormalExitTimestamp = abnormalHint.timestamp(); + Date timestamp = null; + + if (abnormalExitTimestamp != null) { + timestamp = DateUtils.getDateTime(abnormalExitTimestamp); + // sanity check if the abnormal exit actually happened when the session was alive + final Date sessionStart = session.getStarted(); + if (sessionStart == null || timestamp.before(sessionStart)) { + options.getLogger() + .log(WARNING, + "Abnormal exit happened before previous session start, not ending the session."); + return envelope; + } + } - @SuppressWarnings("JavaUtilDate") - private @NotNull SentryEnvelope endSessionIntoEnvelope( - final @NotNull File sessionFile, - final @NotNull SentryEnvelope envelope, - final @Nullable Object sdkHint - ) { - try (final Reader reader = - new BufferedReader( - new InputStreamReader(new FileInputStream(sessionFile), UTF_8))) { - final Session session = serializer.deserialize(reader, Session.class); - if (session != null) { - if (sdkHint instanceof AbnormalExit) { - final AbnormalExit abnormalHint = (AbnormalExit) sdkHint; - final @Nullable Long abnormalExitTimestamp = abnormalHint.timestamp(); - Date timestamp = null; - - if (abnormalExitTimestamp != null) { - timestamp = DateUtils.getDateTime(abnormalExitTimestamp); - // sanity check if the abnormal exit actually happened when the session was alive - final Date sessionStart = session.getStarted(); - if (sessionStart == null || timestamp.before(sessionStart)) { - options.getLogger().log(WARNING, "Abnormal exit happened before previous session start, not ending the session."); - return envelope; + final String abnormalMechanism = abnormalHint.mechanism(); + session.update(Session.State.Abnormal, null, true, abnormalMechanism); + // we have to use the actual timestamp of the Abnormal Exit here to mark the session + // as finished at the time it happened + session.end(timestamp); + + final SentryEnvelopeItem sessionItem = + SentryEnvelopeItem.fromSession(serializer, session); + // send session in the same envelope as abnormal exit event + final SentryEnvelope newEnvelope = buildNewEnvelope(envelope, sessionItem); + + // at this point the session and its file already became a new envelope, so + // it's safe to delete it + if (!previousSessionFile.delete()) { + options.getLogger().log(WARNING, "Failed to delete the previous session file."); } + return newEnvelope; } - - final String abnormalMechanism = abnormalHint.mechanism(); - session.update(Session.State.Abnormal, null, true, abnormalMechanism); - // we have to use the actual timestamp of the Abnormal Exit here to mark the session - // as finished at the time it happened - session.end(timestamp); - } - - final SentryEnvelope newEnvelope; - // if an envelope has items, we send the session in the same envelope, otherwise - // it's a dummy envelope and we build a new envelope and return it - if (envelope.getItems().iterator().hasNext()) { - final SentryEnvelopeItem sessionItem = - SentryEnvelopeItem.fromSession(serializer, session); - newEnvelope = buildNewEnvelope(envelope, sessionItem); - } else { - newEnvelope = - SentryEnvelope.from(serializer, session, options.getSdkVersion()); - } - - // at this point the session and its file already became a new envelope, so - // it's safe to delete it - if (!sessionFile.delete()) { - options.getLogger().log(WARNING, "Failed to delete the session file."); + } catch (Throwable e) { + options.getLogger().log(SentryLevel.ERROR, "Error processing previous session.", e); } - return newEnvelope; + } else { + options.getLogger().log(DEBUG, "No previous session file to end."); } - } catch (Throwable e) { - options.getLogger().log(SentryLevel.ERROR, "Error processing previous session.", e); } return envelope; } @@ -440,4 +440,16 @@ public void discard(final @NotNull SentryEnvelope envelope) { } return new File[] {}; } + + /** + * Awaits until the previous session (if any) is flushed to its own file. + */ + public static boolean waitPreviousSessionFlush() { + try { + return previousSessionLatch.await(30, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return false; + } } diff --git a/sentry/src/main/java/io/sentry/hints/PreviousSessionEnd.java b/sentry/src/main/java/io/sentry/hints/PreviousSessionEnd.java deleted file mode 100644 index 0aaa305cf9..0000000000 --- a/sentry/src/main/java/io/sentry/hints/PreviousSessionEnd.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.sentry.hints; - -/** Hint that shows this is a previous session end envelope */ -public interface PreviousSessionEnd { -} diff --git a/sentry/src/main/java/io/sentry/hints/PreviousSessionEndHint.java b/sentry/src/main/java/io/sentry/hints/PreviousSessionEndHint.java deleted file mode 100644 index 78ad92da7d..0000000000 --- a/sentry/src/main/java/io/sentry/hints/PreviousSessionEndHint.java +++ /dev/null @@ -1,3 +0,0 @@ -package io.sentry.hints; - -public final class PreviousSessionEndHint implements PreviousSessionEnd {} From de79fbe424082e9d9cadf965e3a5fd5e1ff3bf85 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 30 Mar 2023 15:31:58 +0200 Subject: [PATCH 07/23] Formatting --- .../sentry/android/core/AnrV2Integration.java | 8 +-- .../io/sentry/android/core/SentryAndroid.java | 8 +-- .../io/sentry/PreviousSessionFinalizer.java | 63 ++++++++++--------- sentry/src/main/java/io/sentry/Sentry.java | 9 +-- .../main/java/io/sentry/SentryEnvelope.java | 1 - .../java/io/sentry/cache/EnvelopeCache.java | 42 ++++++------- 6 files changed, 61 insertions(+), 70 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java index 9759174386..2d52daad53 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java @@ -125,10 +125,10 @@ public void run() { if (!EnvelopeCache.waitPreviousSessionFlush()) { options - .getLogger() - .log( - SentryLevel.WARNING, - "Timed out waiting to flush previous session to its own file."); + .getLogger() + .log( + SentryLevel.WARNING, + "Timed out waiting to flush previous session to its own file."); } // making a deep copy as we're modifying the list diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java index b90562f87e..4a0fad3154 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java @@ -125,10 +125,10 @@ public static synchronized void init( final @NotNull IHub hub = Sentry.getCurrentHub(); if (hub.getOptions().isEnableAutoSessionTracking() && ContextUtils.isForegroundImportance(context)) { - //hub.getOptions().getExecutorService().submit(() -> { - hub.addBreadcrumb(BreadcrumbFactory.forSession("session.start")); - hub.startSession(); - //}); + // hub.getOptions().getExecutorService().submit(() -> { + hub.addBreadcrumb(BreadcrumbFactory.forSession("session.start")); + hub.startSession(); + // }); } } catch (IllegalAccessException e) { logger.log(SentryLevel.FATAL, "Fatal error during SentryAndroid.init(...)", e); diff --git a/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java b/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java index 01b9475c67..22f06aa256 100644 --- a/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java +++ b/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java @@ -1,5 +1,11 @@ package io.sentry; +import static io.sentry.SentryLevel.DEBUG; +import static io.sentry.SentryLevel.ERROR; +import static io.sentry.SentryLevel.INFO; +import static io.sentry.SentryLevel.WARNING; +import static io.sentry.cache.EnvelopeCache.NATIVE_CRASH_MARKER_FILE; + import io.sentry.cache.EnvelopeCache; import java.io.BufferedReader; import java.io.File; @@ -12,16 +18,10 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import static io.sentry.SentryLevel.DEBUG; -import static io.sentry.SentryLevel.ERROR; -import static io.sentry.SentryLevel.INFO; -import static io.sentry.SentryLevel.WARNING; -import static io.sentry.cache.EnvelopeCache.NATIVE_CRASH_MARKER_FILE; - /** - * Common cases when previous session is not ended properly (app background or crash): - * - The previous session experienced Abnormal exit (ANR, OS kills app, User kills app) - * - The previous session experienced native crash + * Common cases when previous session is not ended properly (app background or crash): - The + * previous session experienced Abnormal exit (ANR, OS kills app, User kills app) - The previous + * session experienced native crash */ final class PreviousSessionFinalizer implements Runnable { @@ -37,7 +37,8 @@ final class PreviousSessionFinalizer implements Runnable { this.hub = hub; } - @Override public void run() { + @Override + public void run() { final String cacheDirPath = options.getCacheDirPath(); if (cacheDirPath == null) { options.getLogger().log(INFO, "Cache dir is not set, not finalizing the previous session."); @@ -46,10 +47,10 @@ final class PreviousSessionFinalizer implements Runnable { if (!EnvelopeCache.waitPreviousSessionFlush()) { options - .getLogger() - .log( - SentryLevel.WARNING, - "Timed out waiting to flush previous session to its own file in session finalizer."); + .getLogger() + .log( + SentryLevel.WARNING, + "Timed out waiting to flush previous session to its own file in session finalizer."); return; } @@ -60,35 +61,35 @@ final class PreviousSessionFinalizer implements Runnable { options.getLogger().log(WARNING, "Current session is not ended, we'd need to end it."); try (final Reader reader = - new BufferedReader( - new InputStreamReader(new FileInputStream(previousSessionFile), UTF_8))) { + new BufferedReader( + new InputStreamReader(new FileInputStream(previousSessionFile), UTF_8))) { final Session session = serializer.deserialize(reader, Session.class); if (session == null) { options - .getLogger() - .log( - SentryLevel.ERROR, - "Stream from path %s resulted in a null envelope.", - previousSessionFile.getAbsolutePath()); + .getLogger() + .log( + SentryLevel.ERROR, + "Stream from path %s resulted in a null envelope.", + previousSessionFile.getAbsolutePath()); } else { Date timestamp = null; final File crashMarkerFile = - new File(options.getCacheDirPath(), NATIVE_CRASH_MARKER_FILE); + new File(options.getCacheDirPath(), NATIVE_CRASH_MARKER_FILE); if (crashMarkerFile.exists()) { options - .getLogger() - .log(INFO, "Crash marker file exists, last Session is gonna be Crashed."); + .getLogger() + .log(INFO, "Crash marker file exists, last Session is gonna be Crashed."); timestamp = getTimestampFromCrashMarkerFile(crashMarkerFile); if (!crashMarkerFile.delete()) { options - .getLogger() - .log( - ERROR, - "Failed to delete the crash marker file. %s.", - crashMarkerFile.getAbsolutePath()); + .getLogger() + .log( + ERROR, + "Failed to delete the crash marker file. %s.", + crashMarkerFile.getAbsolutePath()); } session.update(Session.State.Crashed, null, true); } @@ -97,7 +98,7 @@ final class PreviousSessionFinalizer implements Runnable { // if the App. has been upgraded and there's a new version of the SDK running, // SdkVersion will be outdated. final SentryEnvelope fromSession = - SentryEnvelope.from(serializer, session, options.getSdkVersion()); + SentryEnvelope.from(serializer, session, options.getSdkVersion()); hub.captureEnvelope(fromSession); } } catch (Throwable e) { @@ -120,7 +121,7 @@ final class PreviousSessionFinalizer implements Runnable { */ private @Nullable Date getTimestampFromCrashMarkerFile(final @NotNull File markerFile) { try (final BufferedReader reader = - new BufferedReader(new InputStreamReader(new FileInputStream(markerFile), UTF_8))) { + new BufferedReader(new InputStreamReader(new FileInputStream(markerFile), UTF_8))) { final String timestamp = reader.readLine(); options.getLogger().log(DEBUG, "Crash marker file has %s timestamp.", timestamp); return DateUtils.getDateTime(timestamp); diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index fd6e71f430..8df308ddeb 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -17,7 +17,6 @@ import io.sentry.util.thread.NoOpMainThreadChecker; import java.io.File; import java.lang.reflect.InvocationTargetException; -import java.nio.file.Files; import java.util.Arrays; import java.util.List; import org.jetbrains.annotations.ApiStatus; @@ -231,16 +230,12 @@ private static synchronized void init( } private static void finalizePreviousSession( - final @NotNull SentryOptions options, - final @NotNull IHub hub - ) { + final @NotNull SentryOptions options, final @NotNull IHub hub) { // enqueue a task to finalize previous session. Since the executor // is single-threaded, this task will be enqueued sequentially after all integrations that have // to modify the previous session have done their work, even if they do that async. try { - options - .getExecutorService() - .submit(new PreviousSessionFinalizer(options, hub)); + options.getExecutorService().submit(new PreviousSessionFinalizer(options, hub)); } catch (Throwable e) { options.getLogger().log(SentryLevel.DEBUG, "Failed to notify options observers.", e); } diff --git a/sentry/src/main/java/io/sentry/SentryEnvelope.java b/sentry/src/main/java/io/sentry/SentryEnvelope.java index 90bba8c901..7d8a01cca3 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelope.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelope.java @@ -3,7 +3,6 @@ import io.sentry.exception.SentryEnvelopeException; import io.sentry.protocol.SdkVersion; import io.sentry.protocol.SentryId; -import io.sentry.util.CollectionUtils; import io.sentry.util.Objects; import java.io.IOException; import java.util.ArrayList; diff --git a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java index 317126f760..13f6159fa9 100644 --- a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java +++ b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java @@ -111,8 +111,8 @@ public void store(@NotNull SentryEnvelope envelope, final @NotNull Hint hint) { options.getLogger().log(WARNING, "Current session is not ended, we'd need to end it."); try (final Reader reader = - new BufferedReader( - new InputStreamReader(new FileInputStream(currentSessionFile), UTF_8))) { + new BufferedReader( + new InputStreamReader(new FileInputStream(currentSessionFile), UTF_8))) { final Session session = serializer.deserialize(reader, Session.class); if (session != null) { writeSessionToDisk(previousSessionFile, session); @@ -126,8 +126,7 @@ public void store(@NotNull SentryEnvelope envelope, final @NotNull Hint hint) { updateCurrentSession(currentSessionFile, envelope); boolean crashedLastRun = false; - final File crashMarkerFile = - new File(options.getCacheDirPath(), NATIVE_CRASH_MARKER_FILE); + final File crashMarkerFile = new File(options.getCacheDirPath(), NATIVE_CRASH_MARKER_FILE); if (crashMarkerFile.exists()) { crashedLastRun = true; } @@ -184,19 +183,18 @@ public void store(@NotNull SentryEnvelope envelope, final @NotNull Hint hint) { /** * Attempts to end previous session, relying on AbnormalExit hint, marks session as abnormal with * abnormal mechanism and takes its timestamp. - *

- * If there was no abnormal exit, the previous session will be captured by PreviousSessionFinalizer. + * + *

If there was no abnormal exit, the previous session will be captured by + * PreviousSessionFinalizer. * * @param hint a hint coming with the envelope * @param envelope an original envelope that is being stored * @return SentryEnvelope returns either a new envelope containing previous session in it, or an - * old one, if the previous session should not be sent with the current envelope. + * old one, if the previous session should not be sent with the current envelope. */ @SuppressWarnings("JavaUtilDate") public @NotNull SentryEnvelope endPreviousSessionForAbnormalExit( - final @NotNull SentryEnvelope envelope, - final @NotNull Hint hint - ) { + final @NotNull SentryEnvelope envelope, final @NotNull Hint hint) { final Object sdkHint = HintUtils.getSentrySdkHint(hint); if (sdkHint instanceof AbnormalExit) { final File previousSessionFile = getPreviousSessionFile(directory.getAbsolutePath()); @@ -205,8 +203,8 @@ public void store(@NotNull SentryEnvelope envelope, final @NotNull Hint hint) { options.getLogger().log(WARNING, "Previous session is not ended, we'd need to end it."); try (final Reader reader = - new BufferedReader( - new InputStreamReader(new FileInputStream(previousSessionFile), UTF_8))) { + new BufferedReader( + new InputStreamReader(new FileInputStream(previousSessionFile), UTF_8))) { final Session session = serializer.deserialize(reader, Session.class); if (session != null) { final AbnormalExit abnormalHint = (AbnormalExit) sdkHint; @@ -218,9 +216,11 @@ public void store(@NotNull SentryEnvelope envelope, final @NotNull Hint hint) { // sanity check if the abnormal exit actually happened when the session was alive final Date sessionStart = session.getStarted(); if (sessionStart == null || timestamp.before(sessionStart)) { - options.getLogger() - .log(WARNING, - "Abnormal exit happened before previous session start, not ending the session."); + options + .getLogger() + .log( + WARNING, + "Abnormal exit happened before previous session start, not ending the session."); return envelope; } } @@ -232,7 +232,7 @@ public void store(@NotNull SentryEnvelope envelope, final @NotNull Hint hint) { session.end(timestamp); final SentryEnvelopeItem sessionItem = - SentryEnvelopeItem.fromSession(serializer, session); + SentryEnvelopeItem.fromSession(serializer, session); // send session in the same envelope as abnormal exit event final SentryEnvelope newEnvelope = buildNewEnvelope(envelope, sessionItem); @@ -390,13 +390,11 @@ public void discard(final @NotNull SentryEnvelope envelope) { } public static @NotNull File getCurrentSessionFile(final @NotNull String cacheDirPath) { - return new File( - cacheDirPath, PREFIX_CURRENT_SESSION_FILE + SUFFIX_SESSION_FILE); + return new File(cacheDirPath, PREFIX_CURRENT_SESSION_FILE + SUFFIX_SESSION_FILE); } public static @NotNull File getPreviousSessionFile(final @NotNull String cacheDirPath) { - return new File( - cacheDirPath, PREFIX_PREVIOUS_SESSION_FILE + SUFFIX_SESSION_FILE); + return new File(cacheDirPath, PREFIX_PREVIOUS_SESSION_FILE + SUFFIX_SESSION_FILE); } @Override @@ -441,9 +439,7 @@ public void discard(final @NotNull SentryEnvelope envelope) { return new File[] {}; } - /** - * Awaits until the previous session (if any) is flushed to its own file. - */ + /** Awaits until the previous session (if any) is flushed to its own file. */ public static boolean waitPreviousSessionFlush() { try { return previousSessionLatch.await(30, TimeUnit.SECONDS); From 5d09a5afc23484017a901f34a8635722ba92dd1c Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Fri, 31 Mar 2023 09:44:23 +0200 Subject: [PATCH 08/23] It works --- .../sentry/android/core/AnrV2Integration.java | 12 ++++--- .../io/sentry/android/core/SentryAndroid.java | 2 -- .../io/sentry/PreviousSessionFinalizer.java | 26 +++++++++----- sentry/src/main/java/io/sentry/Sentry.java | 1 + .../main/java/io/sentry/SentryEnvelope.java | 5 --- .../java/io/sentry/cache/EnvelopeCache.java | 36 ++++++------------- 6 files changed, 37 insertions(+), 45 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java index 2d52daad53..67529e7cf5 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java @@ -15,6 +15,7 @@ import io.sentry.SentryOptions; import io.sentry.android.core.cache.AndroidEnvelopeCache; import io.sentry.cache.EnvelopeCache; +import io.sentry.cache.IEnvelopeCache; import io.sentry.exception.ExceptionMechanismException; import io.sentry.hints.AbnormalExit; import io.sentry.hints.Backfillable; @@ -123,12 +124,15 @@ public void run() { return; } - if (!EnvelopeCache.waitPreviousSessionFlush()) { - options + final IEnvelopeCache cache = options.getEnvelopeDiskCache(); + if (cache instanceof EnvelopeCache) { + if (!((EnvelopeCache) cache).waitPreviousSessionFlush()) { + options .getLogger() .log( - SentryLevel.WARNING, - "Timed out waiting to flush previous session to its own file."); + SentryLevel.WARNING, + "Timed out waiting to flush previous session to its own file."); + } } // making a deep copy as we're modifying the list diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java index 4a0fad3154..eb33039a71 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java @@ -125,10 +125,8 @@ public static synchronized void init( final @NotNull IHub hub = Sentry.getCurrentHub(); if (hub.getOptions().isEnableAutoSessionTracking() && ContextUtils.isForegroundImportance(context)) { - // hub.getOptions().getExecutorService().submit(() -> { hub.addBreadcrumb(BreadcrumbFactory.forSession("session.start")); hub.startSession(); - // }); } } catch (IllegalAccessException e) { logger.log(SentryLevel.FATAL, "Fatal error during SentryAndroid.init(...)", e); diff --git a/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java b/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java index 22f06aa256..d26fcd8d45 100644 --- a/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java +++ b/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java @@ -7,6 +7,7 @@ import static io.sentry.cache.EnvelopeCache.NATIVE_CRASH_MARKER_FILE; import io.sentry.cache.EnvelopeCache; +import io.sentry.cache.IEnvelopeCache; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; @@ -19,9 +20,9 @@ import org.jetbrains.annotations.Nullable; /** - * Common cases when previous session is not ended properly (app background or crash): - The - * previous session experienced Abnormal exit (ANR, OS kills app, User kills app) - The previous - * session experienced native crash + * Common cases when previous session is not ended properly (app background or crash): + *

- The previous session experienced Abnormal exit (ANR, OS kills app, User kills app) + *

- The previous session experienced native crash */ final class PreviousSessionFinalizer implements Runnable { @@ -45,13 +46,16 @@ public void run() { return; } - if (!EnvelopeCache.waitPreviousSessionFlush()) { - options + final IEnvelopeCache cache = options.getEnvelopeDiskCache(); + if (cache instanceof EnvelopeCache) { + if (!((EnvelopeCache) cache).waitPreviousSessionFlush()) { + options .getLogger() .log( - SentryLevel.WARNING, - "Timed out waiting to flush previous session to its own file in session finalizer."); - return; + SentryLevel.WARNING, + "Timed out waiting to flush previous session to its own file in session finalizer."); + return; + } } final File previousSessionFile = EnvelopeCache.getPreviousSessionFile(cacheDirPath); @@ -93,7 +97,11 @@ public void run() { } session.update(Session.State.Crashed, null, true); } - session.end(timestamp); + // if the session has abnormal mechanism, we do not overwrite its end timestamp, because + // it's already set + if (session.getAbnormalMechanism() == null) { + session.end(timestamp); + } // if the App. has been upgraded and there's a new version of the SDK running, // SdkVersion will be outdated. diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 8df308ddeb..0d4ad1c235 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -229,6 +229,7 @@ private static synchronized void init( notifyOptionsObservers(options); } + @SuppressWarnings("FutureReturnValueIgnored") private static void finalizePreviousSession( final @NotNull SentryOptions options, final @NotNull IHub hub) { // enqueue a task to finalize previous session. Since the executor diff --git a/sentry/src/main/java/io/sentry/SentryEnvelope.java b/sentry/src/main/java/io/sentry/SentryEnvelope.java index 7d8a01cca3..5c3f141baf 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelope.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelope.java @@ -6,7 +6,6 @@ import io.sentry.util.Objects; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -93,8 +92,4 @@ public SentryEnvelope( sdkVersion, SentryEnvelopeItem.fromProfilingTrace(profilingTraceData, maxTraceFileSize, serializer)); } - - public static @NotNull SentryEnvelope empty() { - return new SentryEnvelope(new SentryEnvelopeHeader(SentryId.EMPTY_ID), Collections.emptyList()); - } } diff --git a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java index 13f6159fa9..125d046fd9 100644 --- a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java +++ b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java @@ -67,7 +67,7 @@ public class EnvelopeCache extends CacheStrategy implements IEnvelopeCache { public static final String STARTUP_CRASH_MARKER_FILE = "startup_crash"; - private static final CountDownLatch previousSessionLatch = new CountDownLatch(1); + private final CountDownLatch previousSessionLatch; private final @NotNull Map fileNameMap = new WeakHashMap<>(); @@ -87,6 +87,7 @@ public EnvelopeCache( final @NotNull String cacheDirPath, final int maxCacheItems) { super(options, cacheDirPath, maxCacheItems); + previousSessionLatch = new CountDownLatch(1); } @Override @@ -104,7 +105,7 @@ public void store(@NotNull SentryEnvelope envelope, final @NotNull Hint hint) { } } - envelope = endPreviousSessionForAbnormalExit(envelope, hint); + endPreviousSessionForAbnormalExit(hint); if (HintUtils.hasType(hint, SessionStart.class)) { if (currentSessionFile.exists()) { @@ -121,8 +122,6 @@ public void store(@NotNull SentryEnvelope envelope, final @NotNull Hint hint) { options.getLogger().log(SentryLevel.ERROR, "Error processing session.", e); } } - previousSessionLatch.countDown(); - updateCurrentSession(currentSessionFile, envelope); boolean crashedLastRun = false; @@ -152,6 +151,8 @@ public void store(@NotNull SentryEnvelope envelope, final @NotNull Hint hint) { } SentryCrashLastRunState.getInstance().setCrashedLastRun(crashedLastRun); + + previousSessionLatch.countDown(); } // TODO: probably we need to update the current session file for session updates to because of @@ -188,13 +189,9 @@ public void store(@NotNull SentryEnvelope envelope, final @NotNull Hint hint) { * PreviousSessionFinalizer. * * @param hint a hint coming with the envelope - * @param envelope an original envelope that is being stored - * @return SentryEnvelope returns either a new envelope containing previous session in it, or an - * old one, if the previous session should not be sent with the current envelope. */ @SuppressWarnings("JavaUtilDate") - public @NotNull SentryEnvelope endPreviousSessionForAbnormalExit( - final @NotNull SentryEnvelope envelope, final @NotNull Hint hint) { + public void endPreviousSessionForAbnormalExit(final @NotNull Hint hint) { final Object sdkHint = HintUtils.getSentrySdkHint(hint); if (sdkHint instanceof AbnormalExit) { final File previousSessionFile = getPreviousSessionFile(directory.getAbsolutePath()); @@ -221,7 +218,7 @@ public void store(@NotNull SentryEnvelope envelope, final @NotNull Hint hint) { .log( WARNING, "Abnormal exit happened before previous session start, not ending the session."); - return envelope; + return; } } @@ -230,18 +227,7 @@ public void store(@NotNull SentryEnvelope envelope, final @NotNull Hint hint) { // we have to use the actual timestamp of the Abnormal Exit here to mark the session // as finished at the time it happened session.end(timestamp); - - final SentryEnvelopeItem sessionItem = - SentryEnvelopeItem.fromSession(serializer, session); - // send session in the same envelope as abnormal exit event - final SentryEnvelope newEnvelope = buildNewEnvelope(envelope, sessionItem); - - // at this point the session and its file already became a new envelope, so - // it's safe to delete it - if (!previousSessionFile.delete()) { - options.getLogger().log(WARNING, "Failed to delete the previous session file."); - } - return newEnvelope; + writeSessionToDisk(previousSessionFile, session); } } catch (Throwable e) { options.getLogger().log(SentryLevel.ERROR, "Error processing previous session.", e); @@ -250,7 +236,6 @@ public void store(@NotNull SentryEnvelope envelope, final @NotNull Hint hint) { options.getLogger().log(DEBUG, "No previous session file to end."); } } - return envelope; } private void writeCrashMarkerFile() { @@ -440,11 +425,12 @@ public void discard(final @NotNull SentryEnvelope envelope) { } /** Awaits until the previous session (if any) is flushed to its own file. */ - public static boolean waitPreviousSessionFlush() { + public boolean waitPreviousSessionFlush() { try { - return previousSessionLatch.await(30, TimeUnit.SECONDS); + return previousSessionLatch.await(60_000L, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); + options.getLogger().log(DEBUG, "Timed out waiting for previous session to flush."); } return false; } From 67d444d632ade3687a8eefbcb80044462fc65793 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Fri, 31 Mar 2023 09:44:48 +0200 Subject: [PATCH 09/23] Formatting --- .../io/sentry/android/core/AnrV2Integration.java | 8 ++++---- .../java/io/sentry/PreviousSessionFinalizer.java | 14 ++++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java index 67529e7cf5..ebb872afad 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java @@ -128,10 +128,10 @@ public void run() { if (cache instanceof EnvelopeCache) { if (!((EnvelopeCache) cache).waitPreviousSessionFlush()) { options - .getLogger() - .log( - SentryLevel.WARNING, - "Timed out waiting to flush previous session to its own file."); + .getLogger() + .log( + SentryLevel.WARNING, + "Timed out waiting to flush previous session to its own file."); } } diff --git a/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java b/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java index d26fcd8d45..def1d7e695 100644 --- a/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java +++ b/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java @@ -21,8 +21,10 @@ /** * Common cases when previous session is not ended properly (app background or crash): - *

- The previous session experienced Abnormal exit (ANR, OS kills app, User kills app) - *

- The previous session experienced native crash + * + *

- The previous session experienced Abnormal exit (ANR, OS kills app, User kills app) + * + *

- The previous session experienced native crash */ final class PreviousSessionFinalizer implements Runnable { @@ -50,10 +52,10 @@ public void run() { if (cache instanceof EnvelopeCache) { if (!((EnvelopeCache) cache).waitPreviousSessionFlush()) { options - .getLogger() - .log( - SentryLevel.WARNING, - "Timed out waiting to flush previous session to its own file in session finalizer."); + .getLogger() + .log( + SentryLevel.WARNING, + "Timed out waiting to flush previous session to its own file in session finalizer."); return; } } From a8104a6636add3be30ae81fd0bc085b27622d53e Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Fri, 31 Mar 2023 23:30:28 +0200 Subject: [PATCH 10/23] Tests --- .../android/core/AnrV2IntegrationTest.kt | 63 +++++- sentry/src/main/java/io/sentry/Sentry.java | 4 +- .../java/io/sentry/cache/CacheStrategy.java | 2 +- .../java/io/sentry/cache/EnvelopeCache.java | 4 +- .../test/java/io/sentry/OutboxSenderTest.kt | 5 + .../io/sentry/PreviousSessionFinalizerTest.kt | 162 ++++++++++++++ sentry/src/test/java/io/sentry/SentryTest.kt | 78 +++++++ .../java/io/sentry/cache/EnvelopeCacheTest.kt | 202 +++++++++++------- 8 files changed, 435 insertions(+), 85 deletions(-) create mode 100644 sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt index 648a514ab0..e9d20e34f4 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt @@ -8,11 +8,14 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.Hint import io.sentry.IHub import io.sentry.ILogger +import io.sentry.SentryEnvelope import io.sentry.SentryLevel import io.sentry.android.core.AnrV2Integration.AnrV2Hint import io.sentry.android.core.cache.AndroidEnvelopeCache +import io.sentry.cache.EnvelopeCache import io.sentry.exception.ExceptionMechanismException import io.sentry.hints.DiskFlushNotification +import io.sentry.hints.SessionStartHint import io.sentry.protocol.SentryId import io.sentry.test.ImmediateExecutorService import io.sentry.util.HintUtils @@ -73,6 +76,7 @@ class AnrV2IntegrationTest { if (useImmediateExecutorService) ImmediateExecutorService() else mock() this.isAnrEnabled = isAnrEnabled this.flushTimeoutMillis = flushTimeoutMillis + setEnvelopeDiskCache(EnvelopeCache.create(this)) } options.cacheDirPath?.let { cacheDir -> lastReportedAnrFile = File(cacheDir, AndroidEnvelopeCache.LAST_ANR_REPORT) @@ -175,6 +179,16 @@ class AnrV2IntegrationTest { verify(fixture.hub, never()).captureEvent(any(), anyOrNull()) } + @Test + fun `when no ANRs have ever been reported, captures events`() { + val integration = fixture.getSut(tmpDir, lastReportedAnrTimestamp = null) + fixture.addAppExitInfo(timestamp = oldTimestamp) + + integration.register(fixture.hub, fixture.options) + + verify(fixture.hub).captureEvent(any(), anyOrNull()) + } + @Test fun `when latest ANR has not been reported, captures event with enriching`() { val integration = fixture.getSut(tmpDir, lastReportedAnrTimestamp = oldTimestamp) @@ -244,7 +258,7 @@ class AnrV2IntegrationTest { // shouldn't fall into timed out state, because we marked event as flushed on another thread verify(fixture.logger, never()).log( any(), - argThat { startsWith("Timed out") }, + argThat { startsWith("Timed out waiting to flush ANR event to disk.") }, any() ) } @@ -265,7 +279,7 @@ class AnrV2IntegrationTest { // we drop the event, it should not even come to this if-check verify(fixture.logger, never()).log( any(), - argThat { startsWith("Timed out") }, + argThat { startsWith("Timed out waiting to flush ANR event to disk.") }, any() ) } @@ -331,4 +345,49 @@ class AnrV2IntegrationTest { } ) } + + @Test + fun `abnormal mechanism is passed with the hint`() { + val integration = fixture.getSut(tmpDir, lastReportedAnrTimestamp = oldTimestamp) + fixture.addAppExitInfo(timestamp = newTimestamp) + + integration.register(fixture.hub, fixture.options) + + verify(fixture.hub).captureEvent( + any(), + argThat { + val hint = HintUtils.getSentrySdkHint(this) + (hint as AnrV2Hint).mechanism() == "anr_background" + } + ) + } + + @Test + fun `awaits for previous session flush if cache is EnvelopeCache`() { + val integration = fixture.getSut( + tmpDir, + lastReportedAnrTimestamp = oldTimestamp, + flushTimeoutMillis = 3000L + ) + fixture.addAppExitInfo(timestamp = newTimestamp) + + thread { + Thread.sleep(1000L) + val sessionHint = HintUtils.createWithTypeCheckHint(SessionStartHint()) + fixture.options.envelopeDiskCache.store( + SentryEnvelope(SentryId.EMPTY_ID, null, emptyList()), + sessionHint + ) + } + + integration.register(fixture.hub, fixture.options) + + // we store envelope with StartSessionHint on different thread after some delay, which + // triggers the previous session flush, so no timeout + verify(fixture.logger, never()).log( + any(), + argThat { startsWith("Timed out waiting to flush previous session to its own file.") }, + any() + ) + } } diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 0d4ad1c235..362b0beee3 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -224,9 +224,9 @@ private static synchronized void init( integration.register(HubAdapter.getInstance(), options); } - finalizePreviousSession(options, HubAdapter.getInstance()); - notifyOptionsObservers(options); + + finalizePreviousSession(options, HubAdapter.getInstance()); } @SuppressWarnings("FutureReturnValueIgnored") diff --git a/sentry/src/main/java/io/sentry/cache/CacheStrategy.java b/sentry/src/main/java/io/sentry/cache/CacheStrategy.java index a162a6b357..dbb6a49c19 100644 --- a/sentry/src/main/java/io/sentry/cache/CacheStrategy.java +++ b/sentry/src/main/java/io/sentry/cache/CacheStrategy.java @@ -276,7 +276,7 @@ private void saveNewEnvelope( } } - protected @NotNull SentryEnvelope buildNewEnvelope( + private @NotNull SentryEnvelope buildNewEnvelope( final @NotNull SentryEnvelope envelope, final @NotNull SentryEnvelopeItem sessionItem) { final List newEnvelopeItems = new ArrayList<>(); diff --git a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java index 125d046fd9..2b647737d6 100644 --- a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java +++ b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java @@ -91,7 +91,7 @@ public EnvelopeCache( } @Override - public void store(@NotNull SentryEnvelope envelope, final @NotNull Hint hint) { + public void store(final @NotNull SentryEnvelope envelope, final @NotNull Hint hint) { Objects.requireNonNull(envelope, "Envelope is required."); rotateCacheIfNeeded(allEnvelopeFiles()); @@ -427,7 +427,7 @@ public void discard(final @NotNull SentryEnvelope envelope) { /** Awaits until the previous session (if any) is flushed to its own file. */ public boolean waitPreviousSessionFlush() { try { - return previousSessionLatch.await(60_000L, TimeUnit.MILLISECONDS); + return previousSessionLatch.await(options.getFlushTimeoutMillis(), TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); options.getLogger().log(DEBUG, "Timed out waiting for previous session to flush."); diff --git a/sentry/src/test/java/io/sentry/OutboxSenderTest.kt b/sentry/src/test/java/io/sentry/OutboxSenderTest.kt index 3b24dcc620..4f8f1d9860 100644 --- a/sentry/src/test/java/io/sentry/OutboxSenderTest.kt +++ b/sentry/src/test/java/io/sentry/OutboxSenderTest.kt @@ -322,6 +322,11 @@ class OutboxSenderTest { assertFalse(fixture.getSut().isRelevantFileName(EnvelopeCache.STARTUP_CRASH_MARKER_FILE)) } + @Test + fun `when file name is previous session file, should be ignored`() { + assertFalse(fixture.getSut().isRelevantFileName(EnvelopeCache.PREFIX_PREVIOUS_SESSION_FILE)) + } + @Test fun `when file name is relevant, should return true`() { assertTrue(fixture.getSut().isRelevantFileName("123.envelope")) diff --git a/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt b/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt new file mode 100644 index 0000000000..ab5553fac7 --- /dev/null +++ b/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt @@ -0,0 +1,162 @@ +package io.sentry + +import io.sentry.Session.State.Crashed +import io.sentry.cache.EnvelopeCache +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.mockito.kotlin.any +import org.mockito.kotlin.argThat +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import java.io.File +import java.util.Date +import kotlin.test.assertFalse + +class PreviousSessionFinalizerTest { + + @get:Rule + val tmpDir = TemporaryFolder() + + class Fixture { + val options = SentryOptions() + val hub = mock() + val logger = mock() + lateinit var sessionFile: File + + internal fun getSut( + dir: TemporaryFolder?, + flushTimeoutMillis: Long = 0L, + sessionFileExists: Boolean = true, + session: Session? = null, + nativeCrashTimestamp: Date? = null + ): PreviousSessionFinalizer { + options.run { + setLogger(this@Fixture.logger) + isDebug = true + cacheDirPath = dir?.newFolder()?.absolutePath + this.flushTimeoutMillis = flushTimeoutMillis + } + options.cacheDirPath?.let { + sessionFile = EnvelopeCache.getPreviousSessionFile(it) + sessionFile.parentFile.mkdirs() + if (sessionFileExists) { + sessionFile.createNewFile() + } + if (session != null) { + options.serializer.serialize(session, sessionFile.bufferedWriter()) + } + if (nativeCrashTimestamp != null) { + val nativeCrashMarker = File(it, EnvelopeCache.NATIVE_CRASH_MARKER_FILE) + nativeCrashMarker.parentFile.mkdirs() + nativeCrashMarker.writeText(nativeCrashTimestamp.toString()) + } + } + return PreviousSessionFinalizer(options, hub) + } + + fun sessionFromEnvelope(envelope: SentryEnvelope): Session { + val sessionItem = envelope.items.find { it.header.type == SentryItemType.Session } + return options.serializer.deserialize( + sessionItem!!.data.inputStream().bufferedReader(), + Session::class.java + )!! + } + } + + private val fixture = Fixture() + + @Test + fun `if cacheDir is not set, does not send session update`() { + val finalizer = fixture.getSut(null) + finalizer.run() + + verify(fixture.hub, never()).captureEnvelope(any()) + } + + @Test + fun `if previous session file does not exist, does not send session update`() { + val finalizer = fixture.getSut(tmpDir, sessionFileExists = false) + finalizer.run() + + verify(fixture.hub, never()).captureEnvelope(any()) + } + + @Test + fun `if previous session file exists, but session is null, does not send session update`() { + val finalizer = fixture.getSut(tmpDir, sessionFileExists = true, session = null) + finalizer.run() + + verify(fixture.hub, never()).captureEnvelope(any()) + } + + @Test + fun `if previous session exists, sends session update with current end time`() { + val finalizer = fixture.getSut( + tmpDir, + session = Session(null, null, null, "io.sentry.sample@1.0") + ) + finalizer.run() + + verify(fixture.hub).captureEnvelope(argThat { + val session = fixture.sessionFromEnvelope(this) + session.release == "io.sentry.sample@1.0" && + session.timestamp!!.time - DateUtils.getCurrentDateTime().time < 1000 + }) + } + + @Test + fun `if previous session exists with abnormal mechanism, sends session update without chaging end timestamp`() { + val abnormalEndDate = Date(2023, 10, 1) + val finalizer = fixture.getSut( + tmpDir, + session = Session( + null, + null, + null, + "io.sentry.sample@1.0" + ).apply { + update(null, null, false, "mechanism") + end(abnormalEndDate) + } + ) + finalizer.run() + + verify(fixture.hub).captureEnvelope(argThat { + val session = fixture.sessionFromEnvelope(this) + session.release == "io.sentry.sample@1.0" && + session.timestamp!! == abnormalEndDate + }) + } + + @Test + fun `if native crash marker exists, marks previous session as crashed`() { + val finalizer = fixture.getSut( + tmpDir, + session = Session( + null, + null, + null, + "io.sentry.sample@1.0" + ), + nativeCrashTimestamp = Date(2023, 10, 1) + ) + finalizer.run() + + verify(fixture.hub).captureEnvelope(argThat { + val session = fixture.sessionFromEnvelope(this) + session.release == "io.sentry.sample@1.0" && + session.status == Crashed + }) + } + + @Test + fun `if previous session file exists, deletes previous session file`() { + val finalizer = fixture.getSut(tmpDir, sessionFileExists = true) + finalizer.run() + + verify(fixture.hub, never()).captureEnvelope(any()) + assertFalse(fixture.sessionFile.exists()) + } +} diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index b43d9c3491..0f2700a339 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -10,6 +10,7 @@ import io.sentry.test.ImmediateExecutorService import io.sentry.util.thread.IMainThreadChecker import io.sentry.util.thread.MainThreadChecker import org.awaitility.kotlin.await +import org.junit.Rule import org.junit.rules.TemporaryFolder import org.mockito.kotlin.any import org.mockito.kotlin.argThat @@ -33,6 +34,9 @@ class SentryTest { private val dsn = "http://key@localhost/proj" + @get:Rule + val tmpDir = TemporaryFolder() + @BeforeTest @AfterTest fun beforeTest() { @@ -571,6 +575,80 @@ class SentryTest { assertEquals("debug", optionsObserver.environment) } + @Test + fun `init finalizes previous session`() { + lateinit var previousSessionFile: File + + Sentry.init { + it.dsn = dsn + + it.release = "io.sentry.sample@2.0" + it.cacheDirPath = tmpDir.newFolder().absolutePath + + it.executorService = ImmediateExecutorService() + + previousSessionFile = EnvelopeCache.getPreviousSessionFile(it.cacheDirPath!!) + previousSessionFile.parentFile.mkdirs() + it.serializer.serialize( + Session(null, null, "release", "io.sentry.samples@2.0"), + previousSessionFile.bufferedWriter() + ) + assertEquals( + "release", + it.serializer.deserialize(previousSessionFile.bufferedReader(), Session::class.java)!!.environment + ) + + it.addIntegration { hub, _ -> + // this is just a hack to trigger the previousSessionFlush latch, so the finalizer + // does not time out waiting. We have to do it as integration, because this is where + // the hub is already initialized + hub.startSession() + } + } + + + assertFalse(previousSessionFile.exists()) + } + + @Test + fun `if there is work enqueued, init finalizes previous session after that work is done`() { + lateinit var previousSessionFile: File + val triggered = AtomicBoolean(false) + + Sentry.init { + it.dsn = dsn + + it.release = "io.sentry.sample@2.0" + it.cacheDirPath = tmpDir.newFolder().absolutePath + + previousSessionFile = EnvelopeCache.getPreviousSessionFile(it.cacheDirPath!!) + previousSessionFile.parentFile.mkdirs() + it.serializer.serialize( + Session(null, null, "release", "io.sentry.sample@1.0"), + previousSessionFile.bufferedWriter() + ) + + it.executorService.submit { + // here the previous session should still exist. Sentry.init will submit another runnable + // to finalize the previous session, but because the executor is single-threaded, the + // work will be enqueued and the previous session will be finalized after current work is + // finished, ensuring that even if something is using the previous session from a + // different thread, it will still be able to access it. + Thread.sleep(1000L) + val session = it.serializer.deserialize(previousSessionFile.bufferedReader(), Session::class.java) + assertEquals("io.sentry.sample@1.0", session!!.release) + assertEquals("release", session.environment) + triggered.set(true) + } + } + + // to trigger previous session flush + Sentry.startSession() + + await.untilTrue(triggered) + assertFalse(previousSessionFile.exists()) + } + private class InMemoryOptionsObserver : IOptionsObserver { var release: String? = null private set diff --git a/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt b/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt index a00c6c9e6b..08ab1c2ba5 100644 --- a/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt +++ b/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt @@ -1,29 +1,30 @@ package io.sentry.cache +import io.sentry.DateUtils import io.sentry.ILogger -import io.sentry.ISerializer import io.sentry.NoOpLogger import io.sentry.SentryCrashLastRunState import io.sentry.SentryEnvelope import io.sentry.SentryEvent -import io.sentry.SentryLevel import io.sentry.SentryOptions import io.sentry.Session +import io.sentry.Session.State +import io.sentry.Session.State.Ok import io.sentry.UncaughtExceptionHandlerIntegration.UncaughtExceptionHint import io.sentry.cache.EnvelopeCache.PREFIX_CURRENT_SESSION_FILE import io.sentry.cache.EnvelopeCache.SUFFIX_SESSION_FILE +import io.sentry.hints.AbnormalExit import io.sentry.hints.SessionEndHint import io.sentry.hints.SessionStartHint import io.sentry.protocol.User import io.sentry.util.HintUtils -import org.mockito.kotlin.any -import org.mockito.kotlin.eq import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever import java.io.File import java.nio.file.Files import java.nio.file.Path +import java.util.Date +import java.util.UUID +import java.util.concurrent.TimeUnit import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals @@ -34,22 +35,16 @@ import kotlin.test.assertTrue class EnvelopeCacheTest { private class Fixture { val dir: Path = Files.createTempDirectory("sentry-session-cache-test") - val serializer = mock() val options = SentryOptions() val logger = mock() - fun getSUT(): IEnvelopeCache { + fun getSUT(): EnvelopeCache { options.cacheDirPath = dir.toAbsolutePath().toFile().absolutePath - whenever(serializer.deserialize(any(), eq(Session::class.java))).thenAnswer { - Session("dis", User(), "env", "rel") - } - options.setLogger(logger) - options.setSerializer(serializer) options.setDebug(true) - return EnvelopeCache.create(options) + return EnvelopeCache.create(options) as EnvelopeCache } } @@ -69,7 +64,7 @@ class EnvelopeCacheTest { assertEquals(0, nofFiles()) - cache.store(SentryEnvelope.from(fixture.serializer, createSession(), null)) + cache.store(SentryEnvelope.from(fixture.options.serializer, createSession(), null)) assertEquals(1, nofFiles()) @@ -80,7 +75,7 @@ class EnvelopeCacheTest { fun `tolerates discarding unknown envelope`() { val cache = fixture.getSUT() - cache.discard(SentryEnvelope.from(fixture.serializer, createSession(), null)) + cache.discard(SentryEnvelope.from(fixture.options.serializer, createSession(), null)) // no exception thrown } @@ -91,7 +86,7 @@ class EnvelopeCacheTest { val file = File(fixture.options.cacheDirPath!!) - val envelope = SentryEnvelope.from(fixture.serializer, createSession(), null) + val envelope = SentryEnvelope.from(fixture.options.serializer, createSession(), null) val hints = HintUtils.createWithTypeCheckHint(SessionStartHint()) cache.store(envelope, hints) @@ -108,7 +103,7 @@ class EnvelopeCacheTest { val file = File(fixture.options.cacheDirPath!!) - val envelope = SentryEnvelope.from(fixture.serializer, createSession(), null) + val envelope = SentryEnvelope.from(fixture.options.serializer, createSession(), null) val hints = HintUtils.createWithTypeCheckHint(SessionStartHint()) cache.store(envelope, hints) @@ -129,7 +124,7 @@ class EnvelopeCacheTest { val file = File(fixture.options.cacheDirPath!!) - val envelope = SentryEnvelope.from(fixture.serializer, createSession(), null) + val envelope = SentryEnvelope.from(fixture.options.serializer, createSession(), null) val hints = HintUtils.createWithTypeCheckHint(SessionStartHint()) cache.store(envelope, hints) @@ -137,7 +132,7 @@ class EnvelopeCacheTest { val currentFile = File(fixture.options.cacheDirPath!!, "$PREFIX_CURRENT_SESSION_FILE$SUFFIX_SESSION_FILE") assertTrue(currentFile.exists()) - val session = fixture.serializer.deserialize(currentFile.bufferedReader(Charsets.UTF_8), Session::class.java) + val session = fixture.options.serializer.deserialize(currentFile.bufferedReader(Charsets.UTF_8), Session::class.java) assertNotNull(session) currentFile.delete() @@ -146,126 +141,177 @@ class EnvelopeCacheTest { } @Test - fun `when session start and current file already exist, close session and start a new one`() { + fun `when native crash marker file exist, mark isCrashedLastRun`() { val cache = fixture.getSUT() - val envelope = SentryEnvelope.from(fixture.serializer, createSession(), null) + val file = File(fixture.options.cacheDirPath!!) + val markerFile = File(fixture.options.cacheDirPath!!, EnvelopeCache.NATIVE_CRASH_MARKER_FILE) + markerFile.mkdirs() + assertTrue(markerFile.exists()) + + val envelope = SentryEnvelope.from(fixture.options.serializer, createSession(), null) val hints = HintUtils.createWithTypeCheckHint(SessionStartHint()) cache.store(envelope, hints) - val newEnvelope = SentryEnvelope.from(fixture.serializer, createSession(), null) + val newEnvelope = SentryEnvelope.from(fixture.options.serializer, createSession(), null) + + // since the first store call would set as readCrashedLastRun=true + SentryCrashLastRunState.getInstance().reset() cache.store(newEnvelope, hints) - verify(fixture.logger).log(eq(SentryLevel.WARNING), eq("Current session is not ended, we'd need to end it.")) + file.deleteRecursively() + + // passing empty string since readCrashedLastRun is already set + assertTrue(SentryCrashLastRunState.getInstance().isCrashedLastRun("", false)!!) } @Test - fun `when session start, current file already exist and crash marker file exist, end session and delete marker file`() { + fun `when java crash marker file exist, mark isCrashedLastRun`() { val cache = fixture.getSUT() - val file = File(fixture.options.cacheDirPath!!) - val markerFile = File(fixture.options.cacheDirPath!!, EnvelopeCache.NATIVE_CRASH_MARKER_FILE) + val markerFile = File(fixture.options.cacheDirPath!!, EnvelopeCache.CRASH_MARKER_FILE) markerFile.mkdirs() assertTrue(markerFile.exists()) - val envelope = SentryEnvelope.from(fixture.serializer, createSession(), null) + val envelope = SentryEnvelope.from(fixture.options.serializer, createSession(), null) val hints = HintUtils.createWithTypeCheckHint(SessionStartHint()) cache.store(envelope, hints) - val newEnvelope = SentryEnvelope.from(fixture.serializer, createSession(), null) + // passing empty string since readCrashedLastRun is already set + assertTrue(SentryCrashLastRunState.getInstance().isCrashedLastRun("", false)!!) + assertFalse(markerFile.exists()) + } - cache.store(newEnvelope, hints) - verify(fixture.logger).log(eq(SentryLevel.INFO), eq("Crash marker file exists, last Session is gonna be Crashed.")) + @Test + fun `write java marker file to disk when uncaught exception hint`() { + val cache = fixture.getSUT() + + val markerFile = File(fixture.options.cacheDirPath!!, EnvelopeCache.CRASH_MARKER_FILE) assertFalse(markerFile.exists()) - file.deleteRecursively() + + val envelope = SentryEnvelope.from(fixture.options.serializer, SentryEvent(), null) + + val hints = HintUtils.createWithTypeCheckHint(UncaughtExceptionHint(0, NoOpLogger.getInstance())) + cache.store(envelope, hints) + + assertTrue(markerFile.exists()) } @Test - fun `when session start, current file already exist and crash marker file exist, end session with given timestamp`() { + fun `store with StartSession hint flushes previous session`() { val cache = fixture.getSUT() - val file = File(fixture.options.cacheDirPath!!) - val markerFile = File(fixture.options.cacheDirPath!!, EnvelopeCache.NATIVE_CRASH_MARKER_FILE) - File(fixture.options.cacheDirPath!!, ".sentry-native").mkdirs() - markerFile.createNewFile() - val date = "2020-02-07T14:16:00.000Z" - markerFile.writeText(charset = Charsets.UTF_8, text = date) - val envelope = SentryEnvelope.from(fixture.serializer, createSession(), null) + val envelope = SentryEnvelope.from(fixture.options.serializer, SentryEvent(), null) val hints = HintUtils.createWithTypeCheckHint(SessionStartHint()) cache.store(envelope, hints) - val newEnvelope = SentryEnvelope.from(fixture.serializer, createSession(), null) - - cache.store(newEnvelope, hints) - assertFalse(markerFile.exists()) - file.deleteRecursively() - File(fixture.options.cacheDirPath!!).deleteRecursively() + assertTrue(cache.waitPreviousSessionFlush()) } @Test - fun `when native crash marker file exist, mark isCrashedLastRun`() { + fun `SessionStart hint saves unfinished session to previous_session file`() { val cache = fixture.getSUT() - val file = File(fixture.options.cacheDirPath!!) - val markerFile = File(fixture.options.cacheDirPath!!, EnvelopeCache.NATIVE_CRASH_MARKER_FILE) - markerFile.mkdirs() - assertTrue(markerFile.exists()) + val previousSessionFile = EnvelopeCache.getPreviousSessionFile(fixture.options.cacheDirPath!!) + val currentSessionFile = EnvelopeCache.getCurrentSessionFile(fixture.options.cacheDirPath!!) + val session = createSession() + fixture.options.serializer.serialize(session, currentSessionFile.bufferedWriter()) - val envelope = SentryEnvelope.from(fixture.serializer, createSession(), null) + assertFalse(previousSessionFile.exists()) + val envelope = SentryEnvelope.from(fixture.options.serializer, SentryEvent(), null) val hints = HintUtils.createWithTypeCheckHint(SessionStartHint()) cache.store(envelope, hints) - val newEnvelope = SentryEnvelope.from(fixture.serializer, createSession(), null) + assertTrue(previousSessionFile.exists()) + val persistedSession = fixture.options.serializer.deserialize(previousSessionFile.bufferedReader(), Session::class.java) + assertEquals("dis", persistedSession!!.distinctId) + } - // since the first store call would set as readCrashedLastRun=true - SentryCrashLastRunState.getInstance().reset() + @Test + fun `AbnormalExit hint marks previous session as abnormal with abnormal mechanism and current timestamp`() { + val cache = fixture.getSUT() - cache.store(newEnvelope, hints) - verify(fixture.logger).log(eq(SentryLevel.INFO), eq("Crash marker file exists, last Session is gonna be Crashed.")) - assertFalse(markerFile.exists()) - file.deleteRecursively() + val previousSessionFile = EnvelopeCache.getPreviousSessionFile(fixture.options.cacheDirPath!!) + val session = createSession() + fixture.options.serializer.serialize(session, previousSessionFile.bufferedWriter()) - // passing empty string since readCrashedLastRun is already set - assertTrue(SentryCrashLastRunState.getInstance().isCrashedLastRun("", false)!!) + val envelope = SentryEnvelope.from(fixture.options.serializer, SentryEvent(), null) + val abnormalHint = AbnormalExit { "abnormal_mechanism" } + val hints = HintUtils.createWithTypeCheckHint(abnormalHint) + cache.store(envelope, hints) + + val updatedSession = fixture.options.serializer.deserialize(previousSessionFile.bufferedReader(), Session::class.java) + assertEquals(State.Abnormal, updatedSession!!.status) + assertEquals("abnormal_mechanism", updatedSession.abnormalMechanism) + assertTrue { updatedSession.timestamp!!.time - DateUtils.getCurrentDateTime().time < 1000 } } @Test - fun `when java crash marker file exist, mark isCrashedLastRun`() { + fun `previous session uses AbnormalExit hint timestamp when available`() { val cache = fixture.getSUT() - val markerFile = File(fixture.options.cacheDirPath!!, EnvelopeCache.CRASH_MARKER_FILE) - markerFile.mkdirs() - assertTrue(markerFile.exists()) + val previousSessionFile = EnvelopeCache.getPreviousSessionFile(fixture.options.cacheDirPath!!) + val sessionStarted = Date(2023, 10, 1) + val sessionExitedWithAbnormal = sessionStarted.time + TimeUnit.HOURS.toMillis(3) + val session = createSession(sessionStarted) + fixture.options.serializer.serialize(session, previousSessionFile.bufferedWriter()) - val envelope = SentryEnvelope.from(fixture.serializer, createSession(), null) + val envelope = SentryEnvelope.from(fixture.options.serializer, SentryEvent(), null) + val abnormalHint = object : AbnormalExit { + override fun mechanism(): String = "abnormal_mechanism" - val hints = HintUtils.createWithTypeCheckHint(SessionStartHint()) + override fun timestamp(): Long = sessionExitedWithAbnormal + } + val hints = HintUtils.createWithTypeCheckHint(abnormalHint) cache.store(envelope, hints) - // passing empty string since readCrashedLastRun is already set - assertTrue(SentryCrashLastRunState.getInstance().isCrashedLastRun("", false)!!) - assertFalse(markerFile.exists()) + val updatedSession = fixture.options.serializer.deserialize(previousSessionFile.bufferedReader(), Session::class.java) + assertEquals(sessionExitedWithAbnormal, updatedSession!!.timestamp!!.time) } @Test - fun `write java marker file to disk when uncaught exception hint`() { + fun `when AbnormalExit happened before previous session start, does not mark as abnormal`() { val cache = fixture.getSUT() - val markerFile = File(fixture.options.cacheDirPath!!, EnvelopeCache.CRASH_MARKER_FILE) - assertFalse(markerFile.exists()) + val previousSessionFile = EnvelopeCache.getPreviousSessionFile(fixture.options.cacheDirPath!!) + val sessionStarted = Date(2023, 10, 1) + val sessionExitedWithAbnormal = sessionStarted.time - TimeUnit.HOURS.toMillis(3) + val session = createSession(sessionStarted) + fixture.options.serializer.serialize(session, previousSessionFile.bufferedWriter()) - val envelope = SentryEnvelope.from(fixture.serializer, SentryEvent(), null) + val envelope = SentryEnvelope.from(fixture.options.serializer, SentryEvent(), null) + val abnormalHint = object : AbnormalExit { + override fun mechanism(): String = "abnormal_mechanism" - val hints = HintUtils.createWithTypeCheckHint(UncaughtExceptionHint(0, NoOpLogger.getInstance())) + override fun timestamp(): Long = sessionExitedWithAbnormal + } + val hints = HintUtils.createWithTypeCheckHint(abnormalHint) cache.store(envelope, hints) - assertTrue(markerFile.exists()) + val updatedSession = fixture.options.serializer.deserialize(previousSessionFile.bufferedReader(), Session::class.java) + assertEquals(Ok, updatedSession!!.status) + assertEquals(null, updatedSession.abnormalMechanism) } - private fun createSession(): Session { - return Session("dis", User(), "env", "rel") + private fun createSession(started: Date? = null): Session { + return Session( + Ok, + started ?: DateUtils.getCurrentDateTime(), + DateUtils.getCurrentDateTime(), + 0, + "dis", + UUID.randomUUID(), + true, + null, + null, + null, + null, + "env", + "rel", + null + ) } } From 7da3a7371cc9a7dbc45f93e2bc9e3f4fd25ddde7 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Fri, 31 Mar 2023 23:30:48 +0200 Subject: [PATCH 11/23] Spotless --- .../io/sentry/PreviousSessionFinalizerTest.kt | 36 +++++++++++-------- sentry/src/test/java/io/sentry/SentryTest.kt | 1 - .../java/io/sentry/cache/EnvelopeCacheTest.kt | 1 - 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt b/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt index ab5553fac7..56b1d044c1 100644 --- a/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt +++ b/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt @@ -99,11 +99,13 @@ class PreviousSessionFinalizerTest { ) finalizer.run() - verify(fixture.hub).captureEnvelope(argThat { - val session = fixture.sessionFromEnvelope(this) - session.release == "io.sentry.sample@1.0" && - session.timestamp!!.time - DateUtils.getCurrentDateTime().time < 1000 - }) + verify(fixture.hub).captureEnvelope( + argThat { + val session = fixture.sessionFromEnvelope(this) + session.release == "io.sentry.sample@1.0" && + session.timestamp!!.time - DateUtils.getCurrentDateTime().time < 1000 + } + ) } @Test @@ -123,11 +125,13 @@ class PreviousSessionFinalizerTest { ) finalizer.run() - verify(fixture.hub).captureEnvelope(argThat { - val session = fixture.sessionFromEnvelope(this) - session.release == "io.sentry.sample@1.0" && - session.timestamp!! == abnormalEndDate - }) + verify(fixture.hub).captureEnvelope( + argThat { + val session = fixture.sessionFromEnvelope(this) + session.release == "io.sentry.sample@1.0" && + session.timestamp!! == abnormalEndDate + } + ) } @Test @@ -144,11 +148,13 @@ class PreviousSessionFinalizerTest { ) finalizer.run() - verify(fixture.hub).captureEnvelope(argThat { - val session = fixture.sessionFromEnvelope(this) - session.release == "io.sentry.sample@1.0" && - session.status == Crashed - }) + verify(fixture.hub).captureEnvelope( + argThat { + val session = fixture.sessionFromEnvelope(this) + session.release == "io.sentry.sample@1.0" && + session.status == Crashed + } + ) } @Test diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index 0f2700a339..176c1700fa 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -606,7 +606,6 @@ class SentryTest { } } - assertFalse(previousSessionFile.exists()) } diff --git a/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt b/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt index 08ab1c2ba5..33970f4093 100644 --- a/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt +++ b/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt @@ -16,7 +16,6 @@ import io.sentry.cache.EnvelopeCache.SUFFIX_SESSION_FILE import io.sentry.hints.AbnormalExit import io.sentry.hints.SessionEndHint import io.sentry.hints.SessionStartHint -import io.sentry.protocol.User import io.sentry.util.HintUtils import org.mockito.kotlin.mock import java.io.File From bc9dbe1a910b5fff8e91479773be9ee9bc718e41 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Fri, 31 Mar 2023 23:32:04 +0200 Subject: [PATCH 12/23] api dump --- sentry-android-core/api/sentry-android-core.api | 9 +++++---- sentry/api/sentry.api | 5 +++++ sentry/src/main/java/io/sentry/cache/EnvelopeCache.java | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 9a8c074679..e3e51346f8 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -71,10 +71,11 @@ public class io/sentry/android/core/AnrV2Integration : io/sentry/Integration, ja public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V } -public final class io/sentry/android/core/AnrV2Integration$AnrV2Hint : io/sentry/hints/BlockingFlushHint, io/sentry/hints/Backfillable { - public fun (JLio/sentry/ILogger;JZ)V +public final class io/sentry/android/core/AnrV2Integration$AnrV2Hint : io/sentry/hints/BlockingFlushHint, io/sentry/hints/AbnormalExit, io/sentry/hints/Backfillable { + public fun (JLio/sentry/ILogger;JZZ)V + public fun mechanism ()Ljava/lang/String; public fun shouldEnrich ()Z - public fun timestamp ()J + public fun timestamp ()Ljava/lang/Long; } public final class io/sentry/android/core/AppComponentsBreadcrumbsIntegration : android/content/ComponentCallbacks2, io/sentry/Integration, java/io/Closeable { @@ -297,7 +298,7 @@ public final class io/sentry/android/core/cache/AndroidEnvelopeCache : io/sentry public fun (Lio/sentry/android/core/SentryAndroidOptions;)V public fun getDirectory ()Ljava/io/File; public static fun hasStartupCrashMarker (Lio/sentry/SentryOptions;)Z - public static fun lastReportedAnr (Lio/sentry/SentryOptions;)J + public static fun lastReportedAnr (Lio/sentry/SentryOptions;)Ljava/lang/Long; public fun store (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 16ef8858d2..25170b8ddc 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2265,14 +2265,18 @@ public class io/sentry/cache/EnvelopeCache : io/sentry/cache/IEnvelopeCache { public static final field CRASH_MARKER_FILE Ljava/lang/String; public static final field NATIVE_CRASH_MARKER_FILE Ljava/lang/String; public static final field PREFIX_CURRENT_SESSION_FILE Ljava/lang/String; + public static final field PREFIX_PREVIOUS_SESSION_FILE Ljava/lang/String; public static final field STARTUP_CRASH_MARKER_FILE Ljava/lang/String; public static final field SUFFIX_ENVELOPE_FILE Ljava/lang/String; protected static final field UTF_8 Ljava/nio/charset/Charset; public fun (Lio/sentry/SentryOptions;Ljava/lang/String;I)V public static fun create (Lio/sentry/SentryOptions;)Lio/sentry/cache/IEnvelopeCache; public fun discard (Lio/sentry/SentryEnvelope;)V + public static fun getCurrentSessionFile (Ljava/lang/String;)Ljava/io/File; + public static fun getPreviousSessionFile (Ljava/lang/String;)Ljava/io/File; public fun iterator ()Ljava/util/Iterator; public fun store (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V + public fun waitPreviousSessionFlush ()Z } public abstract interface class io/sentry/cache/IEnvelopeCache : java/lang/Iterable { @@ -2454,6 +2458,7 @@ public final class io/sentry/exception/SentryHttpClientException : java/lang/Exc public abstract interface class io/sentry/hints/AbnormalExit { public fun ignoreCurrentThread ()Z public abstract fun mechanism ()Ljava/lang/String; + public fun timestamp ()Ljava/lang/Long; } public abstract interface class io/sentry/hints/ApplyScopeData { diff --git a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java index 2b647737d6..4646e8cea1 100644 --- a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java +++ b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java @@ -191,7 +191,7 @@ public void store(final @NotNull SentryEnvelope envelope, final @NotNull Hint hi * @param hint a hint coming with the envelope */ @SuppressWarnings("JavaUtilDate") - public void endPreviousSessionForAbnormalExit(final @NotNull Hint hint) { + private void endPreviousSessionForAbnormalExit(final @NotNull Hint hint) { final Object sdkHint = HintUtils.getSentrySdkHint(hint); if (sdkHint instanceof AbnormalExit) { final File previousSessionFile = getPreviousSessionFile(directory.getAbsolutePath()); From 6c09cd06c5e9ff0b99e3c65eb2db4db0141c190b Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 3 Apr 2023 11:50:33 +0200 Subject: [PATCH 13/23] Fix integration tests --- .../sentry/android/core/AnrV2Integration.java | 8 ++- .../android/core/AnrV2IntegrationTest.kt | 52 +++++++++++++++++-- .../uitest/android/AutomaticSpansTest.kt | 1 - .../io/sentry/PreviousSessionFinalizer.java | 9 ++++ .../java/io/sentry/cache/EnvelopeCache.java | 4 ++ .../io/sentry/PreviousSessionFinalizerTest.kt | 40 +++++++++++++- 6 files changed, 106 insertions(+), 8 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java index ebb872afad..ed28e29b8e 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java @@ -36,6 +36,8 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import static io.sentry.SentryLevel.DEBUG; + @SuppressLint("NewApi") // we check this in AnrIntegrationFactory public class AnrV2Integration implements Integration, Closeable { @@ -126,12 +128,16 @@ public void run() { final IEnvelopeCache cache = options.getEnvelopeDiskCache(); if (cache instanceof EnvelopeCache) { - if (!((EnvelopeCache) cache).waitPreviousSessionFlush()) { + if (options.isEnableAutoSessionTracking() && !((EnvelopeCache) cache).waitPreviousSessionFlush()) { options .getLogger() .log( SentryLevel.WARNING, "Timed out waiting to flush previous session to its own file."); + + // if we timed out waiting here, we can already flush the latch, because the timeout is big + // enough to wait for it only once and we don't have to wait again in PreviousSessionFinalizer + ((EnvelopeCache) cache).flushPreviousSession(); } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt index e9d20e34f4..b09abe9e14 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt @@ -66,7 +66,8 @@ class AnrV2IntegrationTest { isAnrEnabled: Boolean = true, flushTimeoutMillis: Long = 0L, lastReportedAnrTimestamp: Long? = null, - lastEventId: SentryId = SentryId() + lastEventId: SentryId = SentryId(), + sessionTrackingEnabled: Boolean = true ): AnrV2Integration { options.run { setLogger(this@Fixture.logger) @@ -76,6 +77,7 @@ class AnrV2IntegrationTest { if (useImmediateExecutorService) ImmediateExecutorService() else mock() this.isAnrEnabled = isAnrEnabled this.flushTimeoutMillis = flushTimeoutMillis + this.isEnableAutoSessionTracking = sessionTrackingEnabled setEnvelopeDiskCache(EnvelopeCache.create(this)) } options.cacheDirPath?.let { cacheDir -> @@ -238,7 +240,7 @@ class AnrV2IntegrationTest { val integration = fixture.getSut( tmpDir, lastReportedAnrTimestamp = oldTimestamp, - flushTimeoutMillis = 3000L + flushTimeoutMillis = 1000L ) fixture.addAppExitInfo(timestamp = newTimestamp) @@ -246,7 +248,7 @@ class AnrV2IntegrationTest { val hint = HintUtils.getSentrySdkHint(invocation.getArgument(1)) as DiskFlushNotification thread { - Thread.sleep(1000L) + Thread.sleep(500L) hint.markFlushed() } SentryId() @@ -367,12 +369,12 @@ class AnrV2IntegrationTest { val integration = fixture.getSut( tmpDir, lastReportedAnrTimestamp = oldTimestamp, - flushTimeoutMillis = 3000L + flushTimeoutMillis = 1000L ) fixture.addAppExitInfo(timestamp = newTimestamp) thread { - Thread.sleep(1000L) + Thread.sleep(500L) val sessionHint = HintUtils.createWithTypeCheckHint(SessionStartHint()) fixture.options.envelopeDiskCache.store( SentryEnvelope(SentryId.EMPTY_ID, null, emptyList()), @@ -390,4 +392,44 @@ class AnrV2IntegrationTest { any() ) } + + @Test + fun `does not await for previous session flush, if session tracking is disabled`() { + val integration = fixture.getSut( + tmpDir, + lastReportedAnrTimestamp = oldTimestamp, + flushTimeoutMillis = 500L, + sessionTrackingEnabled = false + ) + fixture.addAppExitInfo(timestamp = newTimestamp) + + integration.register(fixture.hub, fixture.options) + + verify(fixture.logger, never()).log( + any(), + argThat { startsWith("Timed out waiting to flush previous session to its own file.") }, + any() + ) + verify(fixture.hub).captureEvent(any(), any()) + } + + @Test + fun `flushes previous session latch, if timed out waiting`() { + val integration = fixture.getSut( + tmpDir, + lastReportedAnrTimestamp = oldTimestamp, + flushTimeoutMillis = 500L + ) + fixture.addAppExitInfo(timestamp = newTimestamp) + + integration.register(fixture.hub, fixture.options) + + verify(fixture.logger).log( + any(), + argThat { startsWith("Timed out waiting to flush previous session to its own file.") }, + any() + ) + // should return true, because latch is 0 now + assertTrue((fixture.options.envelopeDiskCache as EnvelopeCache).waitPreviousSessionFlush()) + } } diff --git a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/AutomaticSpansTest.kt b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/AutomaticSpansTest.kt index a2524c44a8..ce3be459cb 100644 --- a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/AutomaticSpansTest.kt +++ b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/AutomaticSpansTest.kt @@ -26,7 +26,6 @@ class AutomaticSpansTest : BaseUiTest() { options.tracesSampleRate = 1.0 options.profilesSampleRate = 1.0 options.isEnableAutoActivityLifecycleTracing = true - options.beforeSendTransaction options.isEnableTimeToFullDisplayTracing = true options.beforeSendTransaction = SentryOptions.BeforeSendTransactionCallback { transaction, _ -> diff --git a/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java b/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java index def1d7e695..01ac02063e 100644 --- a/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java +++ b/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java @@ -48,6 +48,15 @@ public void run() { return; } + if (!options.isEnableAutoSessionTracking()) { + options + .getLogger() + .log( + DEBUG, + "Session tracking is disabled, bailing from previous session finalizer."); + return; + } + final IEnvelopeCache cache = options.getEnvelopeDiskCache(); if (cache instanceof EnvelopeCache) { if (!((EnvelopeCache) cache).waitPreviousSessionFlush()) { diff --git a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java index 4646e8cea1..57a6b3844a 100644 --- a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java +++ b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java @@ -434,4 +434,8 @@ public boolean waitPreviousSessionFlush() { } return false; } + + public void flushPreviousSession() { + previousSessionLatch.countDown(); + } } diff --git a/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt b/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt index 56b1d044c1..48c8709d6e 100644 --- a/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt +++ b/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt @@ -30,13 +30,20 @@ class PreviousSessionFinalizerTest { flushTimeoutMillis: Long = 0L, sessionFileExists: Boolean = true, session: Session? = null, - nativeCrashTimestamp: Date? = null + nativeCrashTimestamp: Date? = null, + sessionTrackingEnabled: Boolean = true, + shouldAwait: Boolean = false ): PreviousSessionFinalizer { options.run { setLogger(this@Fixture.logger) isDebug = true cacheDirPath = dir?.newFolder()?.absolutePath this.flushTimeoutMillis = flushTimeoutMillis + isEnableAutoSessionTracking = sessionTrackingEnabled + setEnvelopeDiskCache(EnvelopeCache.create(this)) + if (!shouldAwait) { + (envelopeDiskCache as? EnvelopeCache)?.flushPreviousSession() + } } options.cacheDirPath?.let { sessionFile = EnvelopeCache.getPreviousSessionFile(it) @@ -165,4 +172,35 @@ class PreviousSessionFinalizerTest { verify(fixture.hub, never()).captureEnvelope(any()) assertFalse(fixture.sessionFile.exists()) } + + @Test + fun `if session tracking is disabled, does not wait for previous session flush`() { + val finalizer = fixture.getSut( + tmpDir, + flushTimeoutMillis = 500L, + sessionTrackingEnabled = false, + shouldAwait = true + ) + finalizer.run() + + verify(fixture.logger, never()).log( + any(), + argThat { startsWith("Timed out waiting to flush previous session to its own file in session finalizer.") }, + any() + ) + verify(fixture.hub, never()).captureEnvelope(any()) + } + + @Test + fun `awaits for previous session flush`() { + val finalizer = fixture.getSut(tmpDir, flushTimeoutMillis = 500L, shouldAwait = true) + finalizer.run() + + verify(fixture.logger).log( + any(), + argThat { startsWith("Timed out waiting to flush previous session to its own file in session finalizer.") }, + any() + ) + verify(fixture.hub, never()).captureEnvelope(any()) + } } From 5857dd0c722b3474627e2fbeef00136bdf850c7e Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Mon, 3 Apr 2023 09:53:01 +0000 Subject: [PATCH 14/23] Format code --- .../java/io/sentry/android/core/AnrV2Integration.java | 11 ++++++----- .../main/java/io/sentry/PreviousSessionFinalizer.java | 6 ++---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java index ed28e29b8e..dc5f793041 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java @@ -36,8 +36,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import static io.sentry.SentryLevel.DEBUG; - @SuppressLint("NewApi") // we check this in AnrIntegrationFactory public class AnrV2Integration implements Integration, Closeable { @@ -128,15 +126,18 @@ public void run() { final IEnvelopeCache cache = options.getEnvelopeDiskCache(); if (cache instanceof EnvelopeCache) { - if (options.isEnableAutoSessionTracking() && !((EnvelopeCache) cache).waitPreviousSessionFlush()) { + if (options.isEnableAutoSessionTracking() + && !((EnvelopeCache) cache).waitPreviousSessionFlush()) { options .getLogger() .log( SentryLevel.WARNING, "Timed out waiting to flush previous session to its own file."); - // if we timed out waiting here, we can already flush the latch, because the timeout is big - // enough to wait for it only once and we don't have to wait again in PreviousSessionFinalizer + // if we timed out waiting here, we can already flush the latch, because the timeout is + // big + // enough to wait for it only once and we don't have to wait again in + // PreviousSessionFinalizer ((EnvelopeCache) cache).flushPreviousSession(); } } diff --git a/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java b/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java index 01ac02063e..458c6532ba 100644 --- a/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java +++ b/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java @@ -50,10 +50,8 @@ public void run() { if (!options.isEnableAutoSessionTracking()) { options - .getLogger() - .log( - DEBUG, - "Session tracking is disabled, bailing from previous session finalizer."); + .getLogger() + .log(DEBUG, "Session tracking is disabled, bailing from previous session finalizer."); return; } From f1f4bb243dec05e303747ac8f240fe913cac864e Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 3 Apr 2023 12:13:29 +0200 Subject: [PATCH 15/23] Api dump --- sentry/api/sentry.api | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 25170b8ddc..d2c365025d 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2272,6 +2272,7 @@ public class io/sentry/cache/EnvelopeCache : io/sentry/cache/IEnvelopeCache { public fun (Lio/sentry/SentryOptions;Ljava/lang/String;I)V public static fun create (Lio/sentry/SentryOptions;)Lio/sentry/cache/IEnvelopeCache; public fun discard (Lio/sentry/SentryEnvelope;)V + public fun flushPreviousSession ()V public static fun getCurrentSessionFile (Ljava/lang/String;)Ljava/io/File; public static fun getPreviousSessionFile (Ljava/lang/String;)Ljava/io/File; public fun iterator ()Ljava/util/Iterator; From 0e17b6c3dc145d8626039084a5eb0f960ebe360c Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 4 Apr 2023 15:49:00 +0200 Subject: [PATCH 16/23] Handle new device properties --- .../io/sentry/android/core/AnrV2EventProcessor.java | 8 ++++++++ sentry/src/main/java/io/sentry/protocol/Device.java | 10 ++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java index 80d8faaf9d..e0add2b4f7 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java @@ -33,6 +33,7 @@ import io.sentry.SentryOptions; import io.sentry.SentryStackTraceFactory; import io.sentry.SpanContext; +import io.sentry.android.core.internal.util.CpuInfoUtils; import io.sentry.cache.PersistingOptionsObserver; import io.sentry.cache.PersistingScopeObserver; import io.sentry.hints.Backfillable; @@ -47,6 +48,7 @@ import io.sentry.protocol.User; import io.sentry.util.HintUtils; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -514,6 +516,12 @@ private void setDevice(final @NotNull SentryBaseEvent event) { device.setId(getDeviceId()); } + final @NotNull List cpuFrequencies = CpuInfoUtils.getInstance().readMaxFrequencies(); + if (!cpuFrequencies.isEmpty()) { + device.setProcessorFrequency(Collections.max(cpuFrequencies).doubleValue()); + device.setProcessorCount(cpuFrequencies.size()); + } + return device; } diff --git a/sentry/src/main/java/io/sentry/protocol/Device.java b/sentry/src/main/java/io/sentry/protocol/Device.java index 2d5123e017..46f1261888 100644 --- a/sentry/src/main/java/io/sentry/protocol/Device.java +++ b/sentry/src/main/java/io/sentry/protocol/Device.java @@ -480,7 +480,10 @@ public boolean equals(Object o) { && Objects.equals(language, device.language) && Objects.equals(locale, device.locale) && Objects.equals(connectionType, device.connectionType) - && Objects.equals(batteryTemperature, device.batteryTemperature); + && Objects.equals(batteryTemperature, device.batteryTemperature) + && Objects.equals(processorCount, device.processorCount) + && Objects.equals(processorFrequency, device.processorFrequency) + && Objects.equals(cpuDescription, device.cpuDescription); } @Override @@ -516,7 +519,10 @@ public int hashCode() { language, locale, connectionType, - batteryTemperature); + batteryTemperature, + processorCount, + processorFrequency, + cpuDescription); result = 31 * result + Arrays.hashCode(archs); return result; } From 9b823d32889e10a7bb9646c79414718d8cd521cd Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 4 Apr 2023 16:34:19 +0200 Subject: [PATCH 17/23] Address PR review --- .../main/java/io/sentry/android/core/AnrIntegration.java | 3 ++- .../java/io/sentry/android/core/AnrV2Integration.java | 2 +- sentry/src/main/java/io/sentry/SentryClient.java | 3 ++- sentry/src/main/java/io/sentry/cache/EnvelopeCache.java | 6 ++++-- sentry/src/main/java/io/sentry/hints/TransactionEnd.java | 5 +++++ .../test/java/io/sentry/PreviousSessionFinalizerTest.kt | 2 +- sentry/src/test/java/io/sentry/SentryClientTest.kt | 9 +++++---- 7 files changed, 20 insertions(+), 10 deletions(-) create mode 100644 sentry/src/main/java/io/sentry/hints/TransactionEnd.java diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java index 210f2128eb..4c6ac6373a 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java @@ -10,6 +10,7 @@ import io.sentry.SentryOptions; import io.sentry.exception.ExceptionMechanismException; import io.sentry.hints.AbnormalExit; +import io.sentry.hints.TransactionEnd; import io.sentry.protocol.Mechanism; import io.sentry.util.HintUtils; import io.sentry.util.Objects; @@ -141,7 +142,7 @@ public void close() throws IOException { * href="https://develop.sentry.dev/sdk/sessions/#crashed-abnormal-vs-errored">Develop Docs * because we don't know whether the app has recovered after it or not. */ - static final class AnrHint implements AbnormalExit { + static final class AnrHint implements AbnormalExit, TransactionEnd { private final boolean isBackgroundAnr; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java index dc5f793041..f56c6a26cd 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java @@ -117,7 +117,7 @@ public void run() { final ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - List applicationExitInfoList = + final List applicationExitInfoList = activityManager.getHistoricalProcessExitReasons(null, 0, 0); if (applicationExitInfoList.size() == 0) { options.getLogger().log(SentryLevel.DEBUG, "No records in historical exit reasons."); diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index e47c534e14..7d66fb0c85 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -4,6 +4,7 @@ import io.sentry.exception.SentryEnvelopeException; import io.sentry.hints.AbnormalExit; import io.sentry.hints.Backfillable; +import io.sentry.hints.TransactionEnd; import io.sentry.protocol.Contexts; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; @@ -196,7 +197,7 @@ private boolean shouldApplyScopeData( @Nullable ITransaction transaction = scope.getTransaction(); if (transaction != null) { // TODO if we want to do the same for crashes, e.g. check for event.isCrashed() - if (HintUtils.hasType(hint, AbnormalExit.class)) { + if (HintUtils.hasType(hint, TransactionEnd.class)) { transaction.forceFinish(SpanStatus.ABORTED, false); } } diff --git a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java index 57a6b3844a..f5c531628c 100644 --- a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java +++ b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java @@ -105,7 +105,9 @@ public void store(final @NotNull SentryEnvelope envelope, final @NotNull Hint hi } } - endPreviousSessionForAbnormalExit(hint); + if (HintUtils.hasType(hint, AbnormalExit.class)) { + tryEndPreviousSession(hint); + } if (HintUtils.hasType(hint, SessionStart.class)) { if (currentSessionFile.exists()) { @@ -191,7 +193,7 @@ public void store(final @NotNull SentryEnvelope envelope, final @NotNull Hint hi * @param hint a hint coming with the envelope */ @SuppressWarnings("JavaUtilDate") - private void endPreviousSessionForAbnormalExit(final @NotNull Hint hint) { + private void tryEndPreviousSession(final @NotNull Hint hint) { final Object sdkHint = HintUtils.getSentrySdkHint(hint); if (sdkHint instanceof AbnormalExit) { final File previousSessionFile = getPreviousSessionFile(directory.getAbsolutePath()); diff --git a/sentry/src/main/java/io/sentry/hints/TransactionEnd.java b/sentry/src/main/java/io/sentry/hints/TransactionEnd.java new file mode 100644 index 0000000000..95e0f29efd --- /dev/null +++ b/sentry/src/main/java/io/sentry/hints/TransactionEnd.java @@ -0,0 +1,5 @@ +package io.sentry.hints; + +/** Marker interface for events that should trigger finish of the current transaction on Scope */ +public interface TransactionEnd { +} diff --git a/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt b/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt index 48c8709d6e..8e27662b5b 100644 --- a/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt +++ b/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt @@ -116,7 +116,7 @@ class PreviousSessionFinalizerTest { } @Test - fun `if previous session exists with abnormal mechanism, sends session update without chaging end timestamp`() { + fun `if previous session exists with abnormal mechanism, sends session update without changing end timestamp`() { val abnormalEndDate = Date(2023, 10, 1) val finalizer = fixture.getSut( tmpDir, diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index bf8664ce68..19dff0a504 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -9,6 +9,7 @@ import io.sentry.hints.AbnormalExit import io.sentry.hints.ApplyScopeData import io.sentry.hints.Backfillable import io.sentry.hints.Cached +import io.sentry.hints.TransactionEnd import io.sentry.protocol.Contexts import io.sentry.protocol.Mechanism import io.sentry.protocol.Request @@ -2043,7 +2044,7 @@ class SentryClientTest { } @Test - fun `AbnormalExits automatically trigger force-stop of any running transaction`() { + fun `TransactionEnds automatically trigger force-stop of any running transaction`() { val sut = fixture.getSut() // build up a running transaction @@ -2059,10 +2060,10 @@ class SentryClientTest { whenever(scope.extras).thenReturn(emptyMap()) whenever(scope.contexts).thenReturn(Contexts()) - val abnormalExit = AbnormalExit { "ANR" } - val abnormalExitHint = HintUtils.createWithTypeCheckHint(abnormalExit) + val transactionEnd = object : TransactionEnd {} + val transactionEndHint = HintUtils.createWithTypeCheckHint(transactionEnd) - sut.captureEvent(SentryEvent(), scope, abnormalExitHint) + sut.captureEvent(SentryEvent(), scope, transactionEndHint) verify(transaction).forceFinish(SpanStatus.ABORTED, false) verify(fixture.transport).send( From 642462aea46101b9836f9672e9a2f175530a14ab Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 4 Apr 2023 16:35:05 +0200 Subject: [PATCH 18/23] Api dump --- sentry/api/sentry.api | 3 +++ sentry/src/main/java/io/sentry/hints/TransactionEnd.java | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 3f2390f321..d10f74b40b 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2529,6 +2529,9 @@ public abstract interface class io/sentry/hints/SubmissionResult { public abstract fun setResult (Z)V } +public abstract interface class io/sentry/hints/TransactionEnd { +} + public final class io/sentry/instrumentation/file/SentryFileInputStream : java/io/FileInputStream { public fun (Ljava/io/File;)V public fun (Ljava/io/FileDescriptor;)V diff --git a/sentry/src/main/java/io/sentry/hints/TransactionEnd.java b/sentry/src/main/java/io/sentry/hints/TransactionEnd.java index 95e0f29efd..d13027e712 100644 --- a/sentry/src/main/java/io/sentry/hints/TransactionEnd.java +++ b/sentry/src/main/java/io/sentry/hints/TransactionEnd.java @@ -1,5 +1,4 @@ package io.sentry.hints; /** Marker interface for events that should trigger finish of the current transaction on Scope */ -public interface TransactionEnd { -} +public interface TransactionEnd {} From d52be74de91de443b68f508c6f7e2b99753da99e Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 27 Apr 2023 13:43:31 +0200 Subject: [PATCH 19/23] Parse ANRv2 thread dump into threads interface (#2661) Co-authored-by: Sentry Github Bot Co-authored-by: Markus Hintersteiner --- .../android/core/AnrV2EventProcessor.java | 79 ++- .../sentry/android/core/AnrV2Integration.java | 36 +- .../core/internal/threaddump/Line.java | 33 + .../core/internal/threaddump/Lines.java | 92 +++ .../internal/threaddump/ThreadDumpParser.java | 333 +++++++++ .../android/core/AnrV2EventProcessorTest.kt | 111 ++- .../android/core/AnrV2IntegrationTest.kt | 82 ++- .../threaddump/ThreadDumpParserTest.kt | 67 ++ .../src/test/resources/thread_dump.txt | 660 ++++++++++++++++++ sentry/api/sentry.api | 50 +- .../main/java/io/sentry/JsonSerializer.java | 1 + .../java/io/sentry/MainEventProcessor.java | 3 +- .../io/sentry/SentryExceptionFactory.java | 35 +- .../main/java/io/sentry/SentryLockReason.java | 187 +++++ .../io/sentry/SentryStackTraceFactory.java | 36 +- .../file/FileIOSpanManager.java | 3 +- .../io/sentry/protocol/SentryStackFrame.java | 24 + .../java/io/sentry/protocol/SentryThread.java | 33 + .../io/sentry/JsonUnknownSerializationTest.kt | 4 +- .../io/sentry/SentryExceptionFactoryTest.kt | 55 +- .../io/sentry/SentryStackTraceFactoryTest.kt | 56 +- .../java/io/sentry/SentryThreadFactoryTest.kt | 2 +- .../SentryLockReasonSerializationTest.kt | 44 ++ .../SentryStackFrameSerializationTest.kt | 1 + .../protocol/SentryThreadSerializationTest.kt | 10 + .../src/test/resources/json/sentry_event.json | 11 + .../resources/json/sentry_lock_reason.json | 7 + .../resources/json/sentry_stack_frame.json | 3 +- .../test/resources/json/sentry_thread.json | 11 + 29 files changed, 1968 insertions(+), 101 deletions(-) create mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/Line.java create mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/Lines.java create mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java create mode 100644 sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt create mode 100644 sentry-android-core/src/test/resources/thread_dump.txt create mode 100644 sentry/src/main/java/io/sentry/SentryLockReason.java create mode 100644 sentry/src/test/java/io/sentry/protocol/SentryLockReasonSerializationTest.kt create mode 100644 sentry/src/test/resources/json/sentry_lock_reason.json diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java index 4a070786a4..7f338af54c 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java @@ -36,15 +36,19 @@ import io.sentry.android.core.internal.util.CpuInfoUtils; import io.sentry.cache.PersistingOptionsObserver; import io.sentry.cache.PersistingScopeObserver; +import io.sentry.hints.AbnormalExit; import io.sentry.hints.Backfillable; import io.sentry.protocol.App; import io.sentry.protocol.Contexts; import io.sentry.protocol.DebugImage; import io.sentry.protocol.DebugMeta; import io.sentry.protocol.Device; +import io.sentry.protocol.Mechanism; import io.sentry.protocol.OperatingSystem; import io.sentry.protocol.Request; import io.sentry.protocol.SdkVersion; +import io.sentry.protocol.SentryStackTrace; +import io.sentry.protocol.SentryThread; import io.sentry.protocol.User; import io.sentry.util.HintUtils; import java.util.ArrayList; @@ -88,8 +92,7 @@ public AnrV2EventProcessor( this.buildInfoProvider = buildInfoProvider; final SentryStackTraceFactory sentryStackTraceFactory = - new SentryStackTraceFactory( - this.options.getInAppExcludes(), this.options.getInAppIncludes()); + new SentryStackTraceFactory(this.options); sentryExceptionFactory = new SentryExceptionFactory(sentryStackTraceFactory); } @@ -109,7 +112,7 @@ public AnrV2EventProcessor( // we always set exception values, platform, os and device even if the ANR is not enrich-able // even though the OS context may change in the meantime (OS update), we consider this an // edge-case - setExceptions(event); + setExceptions(event, unwrappedHint); setPlatform(event); mergeOS(event); setDevice(event); @@ -125,7 +128,7 @@ public AnrV2EventProcessor( backfillScope(event); - backfillOptions(event); + backfillOptions(event, unwrappedHint); setStaticValues(event); @@ -264,22 +267,26 @@ private void setRequest(final @NotNull SentryBaseEvent event) { // endregion // region options persisted values - private void backfillOptions(final @NotNull SentryEvent event) { + private void backfillOptions(final @NotNull SentryEvent event, final @NotNull Object hint) { setRelease(event); setEnvironment(event); setDist(event); setDebugMeta(event); setSdk(event); - setApp(event); + setApp(event, hint); setOptionsTags(event); } - private void setApp(final @NotNull SentryBaseEvent event) { + private void setApp(final @NotNull SentryBaseEvent event, final @NotNull Object hint) { App app = event.getContexts().getApp(); if (app == null) { app = new App(); } app.setAppName(ContextUtils.getApplicationName(context, options.getLogger())); + // TODO: not entirely correct, because we define background ANRs as not the ones of + // IMPORTANCE_FOREGROUND, but this doesn't mean the app was in foreground when an ANR happened + // but it's our best effort for now. We could serialize AppState in theory. + app.setInForeground(!isBackgroundAnr(hint)); final PackageInfo packageInfo = ContextUtils.getPackageInfo(context, options.getLogger(), buildInfoProvider); @@ -339,10 +346,12 @@ private void setDebugMeta(final @NotNull SentryBaseEvent event) { final String proguardUuid = PersistingOptionsObserver.read(options, PROGUARD_UUID_FILENAME, String.class); - final DebugImage debugImage = new DebugImage(); - debugImage.setType(DebugImage.PROGUARD); - debugImage.setUuid(proguardUuid); - images.add(debugImage); + if (proguardUuid != null) { + final DebugImage debugImage = new DebugImage(); + debugImage.setType(DebugImage.PROGUARD); + debugImage.setUuid(proguardUuid); + images.add(debugImage); + } event.setDebugMeta(debugMeta); } } @@ -411,11 +420,51 @@ private void setPlatform(final @NotNull SentryBaseEvent event) { } } - private void setExceptions(final @NotNull SentryEvent event) { - final Throwable throwable = event.getThrowableMechanism(); - if (throwable != null) { - event.setExceptions(sentryExceptionFactory.getSentryExceptions(throwable)); + @Nullable + private SentryThread findMainThread(final @Nullable List threads) { + if (threads != null) { + for (SentryThread thread : threads) { + final String name = thread.getName(); + if (name != null && name.equals("main")) { + return thread; + } + } + } + return null; + } + + // by default we assume that the ANR is foreground, unless abnormalMechanism is "anr_background" + private boolean isBackgroundAnr(final @NotNull Object hint) { + if (hint instanceof AbnormalExit) { + final String abnormalMechanism = ((AbnormalExit) hint).mechanism(); + return "anr_background".equals(abnormalMechanism); + } + return false; + } + + private void setExceptions(final @NotNull SentryEvent event, final @NotNull Object hint) { + // AnrV2 threads contain a thread dump from the OS, so we just search for the main thread dump + // and make an exception out of its stacktrace + final Mechanism mechanism = new Mechanism(); + mechanism.setType("AppExitInfo"); + + final boolean isBackgroundAnr = isBackgroundAnr(hint); + String message = "ANR"; + if (isBackgroundAnr) { + message = "Background " + message; + } + final ApplicationNotResponding anr = + new ApplicationNotResponding(message, Thread.currentThread()); + + SentryThread mainThread = findMainThread(event.getThreads()); + if (mainThread == null) { + // if there's no main thread in the event threads, we just create a dummy thread so the + // exception is properly created as well, but without stacktrace + mainThread = new SentryThread(); + mainThread.setStacktrace(new SentryStackTrace()); } + event.setExceptions( + sentryExceptionFactory.getSentryExceptionsFromThread(mainThread, mechanism, anr)); } private void mergeUser(final @NotNull SentryBaseEvent event) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java index f56c6a26cd..afbcfc6c0c 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java @@ -4,7 +4,6 @@ import android.app.ActivityManager; import android.app.ApplicationExitInfo; import android.content.Context; -import android.os.Looper; import io.sentry.DateUtils; import io.sentry.Hint; import io.sentry.IHub; @@ -14,20 +13,23 @@ import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.android.core.cache.AndroidEnvelopeCache; +import io.sentry.android.core.internal.threaddump.Lines; +import io.sentry.android.core.internal.threaddump.ThreadDumpParser; import io.sentry.cache.EnvelopeCache; import io.sentry.cache.IEnvelopeCache; -import io.sentry.exception.ExceptionMechanismException; import io.sentry.hints.AbnormalExit; import io.sentry.hints.Backfillable; import io.sentry.hints.BlockingFlushHint; -import io.sentry.protocol.Mechanism; import io.sentry.protocol.SentryId; +import io.sentry.protocol.SentryThread; import io.sentry.transport.CurrentDateProvider; import io.sentry.transport.ICurrentDateProvider; import io.sentry.util.HintUtils; import io.sentry.util.Objects; +import java.io.BufferedReader; import java.io.Closeable; import java.io.IOException; +import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -221,7 +223,8 @@ private void reportAsSentryEvent( final long anrTimestamp = exitInfo.getTimestamp(); final boolean isBackground = exitInfo.getImportance() != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; - final Throwable anrThrowable = buildAnrThrowable(exitInfo, isBackground); + + final List threads = parseThreadDump(exitInfo, isBackground); final AnrV2Hint anrHint = new AnrV2Hint( options.getFlushTimeoutMillis(), @@ -232,7 +235,8 @@ private void reportAsSentryEvent( final Hint hint = HintUtils.createWithTypeCheckHint(anrHint); - final SentryEvent event = new SentryEvent(anrThrowable); + final SentryEvent event = new SentryEvent(); + event.setThreads(threads); event.setTimestamp(DateUtils.getDateTime(anrTimestamp)); event.setLevel(SentryLevel.FATAL); @@ -251,20 +255,20 @@ private void reportAsSentryEvent( } } - private @NotNull Throwable buildAnrThrowable( + private @Nullable List parseThreadDump( final @NotNull ApplicationExitInfo exitInfo, final boolean isBackground) { - String message = "ANR"; - if (isBackground) { - message = "Background " + message; + List threads = null; + try (final BufferedReader reader = + new BufferedReader(new InputStreamReader(exitInfo.getTraceInputStream()))) { + final Lines lines = Lines.readLines(reader); + + final ThreadDumpParser threadDumpParser = new ThreadDumpParser(options, isBackground); + threads = threadDumpParser.parse(lines); + } catch (Throwable e) { + options.getLogger().log(SentryLevel.WARNING, "Failed to parse ANR thread dump", e); } - // TODO: here we should actually parse the trace file and extract the thread dump from there - // and then we could properly get the main thread stracktrace and construct a proper exception - final ApplicationNotResponding error = - new ApplicationNotResponding(message, Looper.getMainLooper().getThread()); - final Mechanism mechanism = new Mechanism(); - mechanism.setType("ANRv2"); - return new ExceptionMechanismException(mechanism, error, error.getThread(), true); + return threads; } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/Line.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/Line.java new file mode 100644 index 0000000000..452c481ddc --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/Line.java @@ -0,0 +1,33 @@ +/* + * Adapted from https://cs.android.com/android/platform/superproject/+/master:development/tools/bugreport/src/com/android/bugreport/util/Line.java + * + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.sentry.android.core.internal.threaddump; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public final class Line { + public int lineno; + public @NotNull String text; + + public Line(final int lineno, final @NotNull String text) { + this.lineno = lineno; + this.text = text; + } +} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/Lines.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/Lines.java new file mode 100644 index 0000000000..5c314e1b23 --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/Lines.java @@ -0,0 +1,92 @@ +/* + * Adapted from https://cs.android.com/android/platform/superproject/+/master:development/tools/bugreport/src/com/android/bugreport/util/Lines.java + * + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.sentry.android.core.internal.threaddump; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * A stream of parsed lines. Can be rewound, and sub-regions cloned for recursive descent parsing. + */ +@ApiStatus.Internal +public final class Lines { + private final @NotNull ArrayList mList; + private final int mMin; + private final int mMax; + + /** The read position inside the list. */ + public int pos; + + /** Read the whole file into a Lines object. */ + public static Lines readLines(final @NotNull File file) throws IOException { + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + return Lines.readLines(reader); + } + } + + /** Read the whole file into a Lines object. */ + public static Lines readLines(final @NotNull BufferedReader in) throws IOException { + final ArrayList list = new ArrayList<>(); + + int lineno = 0; + String text; + while ((text = in.readLine()) != null) { + lineno++; + list.add(new Line(lineno, text)); + } + + return new Lines(list); + } + + /** Construct with a list of lines. */ + public Lines(final @NotNull ArrayList list) { + this.mList = list; + mMin = 0; + mMax = mList.size(); + } + + /** If there are more lines to read within the current range. */ + public boolean hasNext() { + return pos < mMax; + } + + /** + * Return the next line, or null if there are no more lines to read. Also returns null in the + * error condition where pos is before the beginning. + */ + @Nullable + public Line next() { + if (pos >= mMin && pos < mMax) { + return this.mList.get(pos++); + } else { + return null; + } + } + + /** Move the read position back by one line. */ + public void rewind() { + pos--; + } +} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java new file mode 100644 index 0000000000..1f4b82d0e3 --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java @@ -0,0 +1,333 @@ +/* + * Adapted from https://cs.android.com/android/platform/superproject/+/master:development/tools/bugreport/src/com/android/bugreport/stacks/ThreadSnapshotParser.java + * + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.sentry.android.core.internal.threaddump; + +import io.sentry.SentryLevel; +import io.sentry.SentryLockReason; +import io.sentry.SentryOptions; +import io.sentry.SentryStackTraceFactory; +import io.sentry.protocol.SentryStackFrame; +import io.sentry.protocol.SentryStackTrace; +import io.sentry.protocol.SentryThread; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class ThreadDumpParser { + private static final Pattern BEGIN_MANAGED_THREAD_RE = + Pattern.compile("\"(.*)\" (.*) ?prio=(\\d+)\\s+tid=(\\d+)\\s*(.*)"); + private static final Pattern NATIVE_RE = + Pattern.compile(" (?:native: )?#\\d+ \\S+ [0-9a-fA-F]+\\s+(.*)\\s+\\((.*)\\+(\\d+)\\)"); + private static final Pattern NATIVE_NO_LOC_RE = + Pattern.compile(" (?:native: )?#\\d+ \\S+ [0-9a-fA-F]+\\s+(.*)\\s*\\(?(.*)\\)?"); + private static final Pattern JAVA_RE = + Pattern.compile(" at (?:(.+)\\.)?([^.]+)\\.([^.]+)\\((.*):([\\d-]+)\\)"); + private static final Pattern JNI_RE = + Pattern.compile(" at (?:(.+)\\.)?([^.]+)\\.([^.]+)\\(Native method\\)"); + private static final Pattern LOCKED_RE = + Pattern.compile(" - locked \\<([0x0-9a-fA-F]{1,16})\\> \\(a (?:(.+)\\.)?([^.]+)\\)"); + private static final Pattern SLEEPING_ON_RE = + Pattern.compile(" - sleeping on \\<([0x0-9a-fA-F]{1,16})\\> \\(a (?:(.+)\\.)?([^.]+)\\)"); + private static final Pattern WAITING_ON_RE = + Pattern.compile(" - waiting on \\<([0x0-9a-fA-F]{1,16})\\> \\(a (?:(.+)\\.)?([^.]+)\\)"); + private static final Pattern WAITING_TO_LOCK_RE = + Pattern.compile( + " - waiting to lock \\<([0x0-9a-fA-F]{1,16})\\> \\(a (?:(.+)\\.)?([^.]+)\\)"); + private static final Pattern WAITING_TO_LOCK_HELD_RE = + Pattern.compile( + " - waiting to lock \\<([0x0-9a-fA-F]{1,16})\\> \\(a (?:(.+)\\.)?([^.]+)\\)" + + "(?: held by thread (\\d+))"); + private static final Pattern WAITING_TO_LOCK_UNKNOWN_RE = + Pattern.compile(" - waiting to lock an unknown object"); + private static final Pattern BLANK_RE = Pattern.compile("\\s+"); + + private final @NotNull SentryOptions options; + + private final boolean isBackground; + + private final @NotNull SentryStackTraceFactory stackTraceFactory; + + public ThreadDumpParser(final @NotNull SentryOptions options, final boolean isBackground) { + this.options = options; + this.isBackground = isBackground; + this.stackTraceFactory = new SentryStackTraceFactory(options); + } + + @NotNull + public List parse(final @NotNull Lines lines) { + final List sentryThreads = new ArrayList<>(); + + final Matcher beginManagedThreadRe = BEGIN_MANAGED_THREAD_RE.matcher(""); + + while (lines.hasNext()) { + final Line line = lines.next(); + if (line == null) { + options.getLogger().log(SentryLevel.WARNING, "Internal error while parsing thread dump."); + return sentryThreads; + } + final String text = line.text; + // we only handle managed threads, as unmanaged/not attached do not have the thread id and + // our protocol does not support this case + if (matches(beginManagedThreadRe, text)) { + lines.rewind(); + + final SentryThread thread = parseThread(lines); + if (thread != null) { + sentryThreads.add(thread); + } + } + } + return sentryThreads; + } + + private SentryThread parseThread(final @NotNull Lines lines) { + final SentryThread sentryThread = new SentryThread(); + + final Matcher beginManagedThreadRe = BEGIN_MANAGED_THREAD_RE.matcher(""); + + // thread attributes + if (!lines.hasNext()) { + return null; + } + final Line line = lines.next(); + if (line == null) { + options.getLogger().log(SentryLevel.WARNING, "Internal error while parsing thread dump."); + return null; + } + if (matches(beginManagedThreadRe, line.text)) { + final Long tid = getLong(beginManagedThreadRe, 4, null); + if (tid == null) { + options.getLogger().log(SentryLevel.DEBUG, "No thread id in the dump, skipping thread."); + // tid is required by our protocol + return null; + } + sentryThread.setId(tid); + sentryThread.setName(beginManagedThreadRe.group(1)); + final String state = beginManagedThreadRe.group(5); + // sanitizing thread that have more details after their actual state, e.g. + // "Native (still starting up)" <- we just need "Native" here + if (state != null) { + if (state.contains(" ")) { + sentryThread.setState(state.substring(0, state.indexOf(' '))); + } else { + sentryThread.setState(state); + } + } + final String threadName = sentryThread.getName(); + if (threadName != null) { + final boolean isMain = threadName.equals("main"); + sentryThread.setMain(isMain); + // since it's an ANR, the crashed thread will always be main + sentryThread.setCrashed(isMain); + sentryThread.setCurrent(isMain && !isBackground); + } + } + + // thread stacktrace + final SentryStackTrace stackTrace = parseStacktrace(lines, sentryThread); + sentryThread.setStacktrace(stackTrace); + return sentryThread; + } + + @NotNull + private SentryStackTrace parseStacktrace( + final @NotNull Lines lines, final @NotNull SentryThread thread) { + final List frames = new ArrayList<>(); + boolean isLastFrameJava = false; + + final Matcher nativeRe = NATIVE_RE.matcher(""); + final Matcher nativeNoLocRe = NATIVE_NO_LOC_RE.matcher(""); + final Matcher javaRe = JAVA_RE.matcher(""); + final Matcher jniRe = JNI_RE.matcher(""); + final Matcher lockedRe = LOCKED_RE.matcher(""); + final Matcher waitingOnRe = WAITING_ON_RE.matcher(""); + final Matcher sleepingOnRe = SLEEPING_ON_RE.matcher(""); + final Matcher waitingToLockHeldRe = WAITING_TO_LOCK_HELD_RE.matcher(""); + final Matcher waitingToLockRe = WAITING_TO_LOCK_RE.matcher(""); + final Matcher waitingToLockUnknownRe = WAITING_TO_LOCK_UNKNOWN_RE.matcher(""); + final Matcher blankRe = BLANK_RE.matcher(""); + + while (lines.hasNext()) { + final Line line = lines.next(); + if (line == null) { + options.getLogger().log(SentryLevel.WARNING, "Internal error while parsing thread dump."); + break; + } + final String text = line.text; + if (matches(nativeRe, text)) { + final SentryStackFrame frame = new SentryStackFrame(); + frame.setPackage(nativeRe.group(1)); + frame.setSymbol(nativeRe.group(2)); + frame.setLineno(getInteger(nativeRe, 3, null)); + frames.add(frame); + isLastFrameJava = false; + } else if (matches(nativeNoLocRe, text)) { + final SentryStackFrame frame = new SentryStackFrame(); + frame.setPackage(nativeNoLocRe.group(1)); + frame.setSymbol(nativeNoLocRe.group(2)); + frames.add(frame); + isLastFrameJava = false; + } else if (matches(javaRe, text)) { + final SentryStackFrame frame = new SentryStackFrame(); + final String packageName = javaRe.group(1); + final String className = javaRe.group(2); + final String module = String.format("%s.%s", packageName, className); + frame.setModule(module); + frame.setFunction(javaRe.group(3)); + frame.setFilename(javaRe.group(4)); + frame.setLineno(getUInteger(javaRe, 5, null)); + frame.setInApp(stackTraceFactory.isInApp(module)); + frames.add(frame); + isLastFrameJava = true; + } else if (matches(jniRe, text)) { + final SentryStackFrame frame = new SentryStackFrame(); + final String packageName = jniRe.group(1); + final String className = jniRe.group(2); + final String module = String.format("%s.%s", packageName, className); + frame.setModule(module); + frame.setFunction(jniRe.group(3)); + frame.setInApp(stackTraceFactory.isInApp(module)); + frames.add(frame); + isLastFrameJava = true; + } else if (matches(lockedRe, text)) { + if (isLastFrameJava) { + final SentryLockReason lock = new SentryLockReason(); + lock.setType(SentryLockReason.LOCKED); + lock.setAddress(lockedRe.group(1)); + lock.setPackageName(lockedRe.group(2)); + lock.setClassName(lockedRe.group(3)); + combineThreadLocks(thread, lock); + } + } else if (matches(waitingOnRe, text)) { + if (isLastFrameJava) { + final SentryLockReason lock = new SentryLockReason(); + lock.setType(SentryLockReason.WAITING); + lock.setAddress(waitingOnRe.group(1)); + lock.setPackageName(waitingOnRe.group(2)); + lock.setClassName(waitingOnRe.group(3)); + combineThreadLocks(thread, lock); + } + } else if (matches(sleepingOnRe, text)) { + if (isLastFrameJava) { + final SentryLockReason lock = new SentryLockReason(); + lock.setType(SentryLockReason.SLEEPING); + lock.setAddress(sleepingOnRe.group(1)); + lock.setPackageName(sleepingOnRe.group(2)); + lock.setClassName(sleepingOnRe.group(3)); + combineThreadLocks(thread, lock); + } + } else if (matches(waitingToLockHeldRe, text)) { + if (isLastFrameJava) { + final SentryLockReason lock = new SentryLockReason(); + lock.setType(SentryLockReason.BLOCKED); + lock.setAddress(waitingToLockHeldRe.group(1)); + lock.setPackageName(waitingToLockHeldRe.group(2)); + lock.setClassName(waitingToLockHeldRe.group(3)); + lock.setThreadId(getLong(waitingToLockHeldRe, 4, null)); + combineThreadLocks(thread, lock); + } + } else if (matches(waitingToLockRe, text)) { + if (isLastFrameJava) { + final SentryLockReason lock = new SentryLockReason(); + lock.setType(SentryLockReason.BLOCKED); + lock.setAddress(waitingToLockRe.group(1)); + lock.setPackageName(waitingToLockRe.group(2)); + lock.setClassName(waitingToLockRe.group(3)); + combineThreadLocks(thread, lock); + } + } else if (matches(waitingToLockUnknownRe, text)) { + if (isLastFrameJava) { + final SentryLockReason lock = new SentryLockReason(); + lock.setType(SentryLockReason.BLOCKED); + combineThreadLocks(thread, lock); + } + } else if (text.length() == 0 || matches(blankRe, text)) { + break; + } + } + + // Sentry expects frames to be in reverse order + Collections.reverse(frames); + final SentryStackTrace stackTrace = new SentryStackTrace(frames); + // it's a thread dump + stackTrace.setSnapshot(true); + return stackTrace; + } + + private boolean matches(final @NotNull Matcher matcher, final @NotNull String text) { + matcher.reset(text); + return matcher.matches(); + } + + private void combineThreadLocks( + final @NotNull SentryThread thread, final @NotNull SentryLockReason lockReason) { + Map heldLocks = thread.getHeldLocks(); + if (heldLocks == null) { + heldLocks = new HashMap<>(); + } + final SentryLockReason prev = heldLocks.get(lockReason.getAddress()); + if (prev != null) { + // higher type prevails as we are tagging thread with the most severe lock reason + prev.setType(Math.max(prev.getType(), lockReason.getType())); + } else { + heldLocks.put(lockReason.getAddress(), new SentryLockReason(lockReason)); + } + thread.setHeldLocks(heldLocks); + } + + @Nullable + private Long getLong( + final @NotNull Matcher matcher, final int group, final @Nullable Long defaultValue) { + final String str = matcher.group(group); + if (str == null || str.length() == 0) { + return defaultValue; + } else { + return Long.parseLong(str); + } + } + + @Nullable + private Integer getInteger( + final @NotNull Matcher matcher, final int group, final @Nullable Integer defaultValue) { + final String str = matcher.group(group); + if (str == null || str.length() == 0) { + return defaultValue; + } else { + return Integer.parseInt(str); + } + } + + @Nullable + private Integer getUInteger( + final @NotNull Matcher matcher, final int group, final @Nullable Integer defaultValue) { + final String str = matcher.group(group); + if (str == null || str.length() == 0) { + return defaultValue; + } else { + final Integer parsed = Integer.parseInt(str); + return parsed >= 0 ? parsed : defaultValue; + } + } +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt index 96cbba43fe..a52aee527a 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt @@ -32,6 +32,7 @@ import io.sentry.cache.PersistingScopeObserver.TAGS_FILENAME import io.sentry.cache.PersistingScopeObserver.TRACE_FILENAME import io.sentry.cache.PersistingScopeObserver.TRANSACTION_FILENAME import io.sentry.cache.PersistingScopeObserver.USER_FILENAME +import io.sentry.hints.AbnormalExit import io.sentry.hints.Backfillable import io.sentry.protocol.Browser import io.sentry.protocol.Contexts @@ -42,6 +43,9 @@ import io.sentry.protocol.OperatingSystem import io.sentry.protocol.Request import io.sentry.protocol.Response import io.sentry.protocol.SdkVersion +import io.sentry.protocol.SentryStackFrame +import io.sentry.protocol.SentryStackTrace +import io.sentry.protocol.SentryThread import io.sentry.protocol.User import io.sentry.util.HintUtils import org.junit.Rule @@ -61,6 +65,7 @@ import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertSame +import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) class AnrV2EventProcessorTest { @@ -301,6 +306,7 @@ class AnrV2EventProcessorTest { assertEquals("io.sentry.android.core.test", processed.contexts.app!!.appName) assertEquals("1.2.0", processed.contexts.app!!.appVersion) assertEquals("232", processed.contexts.app!!.appBuild) + assertEquals(true, processed.contexts.app!!.inForeground) // tags assertEquals("tag", processed.tags!!["option"]) } @@ -316,8 +322,8 @@ class AnrV2EventProcessorTest { val processed = processor.process(original, hint) assertEquals("io.sentry.samples", processed!!.release) - assertNull(processed!!.contexts.app!!.appVersion) - assertNull(processed!!.contexts.app!!.appBuild) + assertNull(processed.contexts.app!!.appVersion) + assertNull(processed.contexts.app!!.appBuild) } @Test @@ -412,6 +418,102 @@ class AnrV2EventProcessorTest { assertEquals("uuid", processed.debugMeta!!.images!![1].uuid) } + @Test + fun `when proguard uuid is not persisted, does not add to debug meta`() { + val hint = HintUtils.createWithTypeCheckHint(BackfillableHint()) + + val processed = processEvent(hint, populateOptionsCache = false) + + // proguard uuid + assertTrue(processed.debugMeta!!.images!!.isEmpty()) + } + + @Test + fun `populates exception from main thread`() { + val hint = HintUtils.createWithTypeCheckHint(AbnormalExitHint()) + val stacktrace = SentryStackTrace().apply { + frames = listOf( + SentryStackFrame().apply { + lineno = 777 + module = "io.sentry.samples.MainActivity" + function = "run" + } + ) + } + + val processed = processEvent(hint) { + threads = listOf( + SentryThread().apply { + name = "main" + id = 13 + this.stacktrace = stacktrace + } + ) + } + + val exception = processed.exceptions!!.first() + assertEquals(13, exception.threadId) + assertEquals("AppExitInfo", exception.mechanism!!.type) + assertEquals("ANR", exception.value) + assertEquals("ApplicationNotResponding", exception.type) + assertEquals("io.sentry.android.core", exception.module) + assertEquals(true, exception.stacktrace!!.snapshot) + val frame = exception.stacktrace!!.frames!!.first() + assertEquals(777, frame.lineno) + assertEquals("run", frame.function) + assertEquals("io.sentry.samples.MainActivity", frame.module) + } + + @Test + fun `populates exception without stacktrace when there is no main thread in threads`() { + val hint = HintUtils.createWithTypeCheckHint(AbnormalExitHint()) + + val processed = processEvent(hint) { + threads = listOf(SentryThread()) + } + + val exception = processed.exceptions!!.first() + assertEquals("AppExitInfo", exception.mechanism!!.type) + assertEquals("ANR", exception.value) + assertEquals("ApplicationNotResponding", exception.type) + assertEquals("io.sentry.android.core", exception.module) + assertNull(exception.stacktrace) + } + + @Test + fun `adds Background to the message when mechanism is anr_background`() { + val hint = HintUtils.createWithTypeCheckHint(AbnormalExitHint(mechanism = "anr_background")) + + val processed = processEvent(hint) { + threads = listOf( + SentryThread().apply { + name = "main" + stacktrace = SentryStackTrace() + } + ) + } + + val exception = processed.exceptions!!.first() + assertEquals("Background ANR", exception.value) + } + + @Test + fun `does not add Background to the message when mechanism is anr_foreground`() { + val hint = HintUtils.createWithTypeCheckHint(AbnormalExitHint(mechanism = "anr_foreground")) + + val processed = processEvent(hint) { + threads = listOf( + SentryThread().apply { + name = "main" + stacktrace = SentryStackTrace() + } + ) + } + + val exception = processed.exceptions!!.first() + assertEquals("ANR", exception.value) + } + private fun processEvent( hint: Hint, populateScopeCache: Boolean = false, @@ -428,6 +530,11 @@ class AnrV2EventProcessorTest { return processor.process(original, hint)!! } + internal class AbnormalExitHint(val mechanism: String? = null) : AbnormalExit, Backfillable { + override fun mechanism(): String? = mechanism + override fun shouldEnrich(): Boolean = true + } + internal class BackfillableHint(private val shouldEnrich: Boolean = true) : Backfillable { override fun shouldEnrich(): Boolean = shouldEnrich } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt index b09abe9e14..4db0d425c4 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt @@ -13,7 +13,6 @@ import io.sentry.SentryLevel import io.sentry.android.core.AnrV2Integration.AnrV2Hint import io.sentry.android.core.cache.AndroidEnvelopeCache import io.sentry.cache.EnvelopeCache -import io.sentry.exception.ExceptionMechanismException import io.sentry.hints.DiskFlushNotification import io.sentry.hints.SessionStartHint import io.sentry.protocol.SentryId @@ -29,6 +28,7 @@ import org.mockito.kotlin.check import org.mockito.kotlin.inOrder import org.mockito.kotlin.mock import org.mockito.kotlin.never +import org.mockito.kotlin.spy import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -78,6 +78,7 @@ class AnrV2IntegrationTest { this.isAnrEnabled = isAnrEnabled this.flushTimeoutMillis = flushTimeoutMillis this.isEnableAutoSessionTracking = sessionTrackingEnabled + addInAppInclude("io.sentry.samples") setEnvelopeDiskCache(EnvelopeCache.create(this)) } options.cacheDirPath?.let { cacheDir -> @@ -104,7 +105,41 @@ class AnrV2IntegrationTest { if (importance != null) { builder.setImportance(importance) } - shadowActivityManager.addApplicationExitInfo(builder.build()) + val exitInfo = spy(builder.build()) { + whenever(mock.traceInputStream).thenReturn( + """ +"main" prio=5 tid=1 Blocked + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x72a985e0 self=0xb400007cabc57380 + | sysTid=28941 nice=-10 cgrp=top-app sched=0/0 handle=0x7deceb74f8 + | state=S schedstat=( 324804784 183300334 997 ) utm=23 stm=8 core=3 HZ=100 + | stack=0x7ff93a9000-0x7ff93ab000 stackSize=8188KB + | held mutexes= + at io.sentry.samples.android.MainActivity${'$'}2.run(MainActivity.java:177) + - waiting to lock <0x0d3a2f0a> (a java.lang.Object) held by thread 5 + at android.os.Handler.handleCallback(Handler.java:942) + at android.os.Handler.dispatchMessage(Handler.java:99) + at android.os.Looper.loopOnce(Looper.java:201) + at android.os.Looper.loop(Looper.java:288) + at android.app.ActivityThread.main(ActivityThread.java:7872) + at java.lang.reflect.Method.invoke(Native method) + at com.android.internal.os.RuntimeInit${'$'}MethodAndArgsCaller.run(RuntimeInit.java:548) + at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936) + +"perfetto_hprof_listener" prio=10 tid=7 Native (still starting up) + | group="" sCount=1 ucsCount=0 flags=1 obj=0x0 self=0xb400007cabc5ab20 + | sysTid=28959 nice=-20 cgrp=top-app sched=0/0 handle=0x7b2021bcb0 + | state=S schedstat=( 72750 1679167 1 ) utm=0 stm=0 core=3 HZ=100 + | stack=0x7b20124000-0x7b20126000 stackSize=991KB + | held mutexes= + native: #00 pc 00000000000a20f4 /apex/com.android.runtime/lib64/bionic/libc.so (read+4) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #01 pc 000000000001d840 /apex/com.android.art/lib64/libperfetto_hprof.so (void* std::__1::__thread_proxy >, ArtPlugin_Initialize::${'$'}_34> >(void*)+260) (BuildId: 525cc92a7dc49130157aeb74f6870364) + native: #02 pc 00000000000b63b0 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+208) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #03 pc 00000000000530b8 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + (no managed stack frames) + """.trimIndent().byteInputStream() + ) + } + shadowActivityManager.addApplicationExitInfo(exitInfo) } } @@ -202,13 +237,31 @@ class AnrV2IntegrationTest { check { assertEquals(newTimestamp, it.timestamp.time) assertEquals(SentryLevel.FATAL, it.level) - assertTrue { - it.throwable is ApplicationNotResponding && - it.throwable!!.message == "Background ANR" - } - assertTrue { - (it.throwableMechanism as ExceptionMechanismException).exceptionMechanism.type == "ANRv2" - } + val mainThread = it.threads!!.first() + assertEquals("main", mainThread.name) + assertEquals(1, mainThread.id) + assertEquals("Blocked", mainThread.state) + assertEquals(true, mainThread.isCrashed) + assertEquals(true, mainThread.isMain) + assertEquals("0x0d3a2f0a", mainThread.heldLocks!!.values.first().address) + assertEquals(5, mainThread.heldLocks!!.values.first().threadId) + val lastFrame = mainThread.stacktrace!!.frames!!.last() + assertEquals("io.sentry.samples.android.MainActivity$2", lastFrame.module) + assertEquals("MainActivity.java", lastFrame.filename) + assertEquals("run", lastFrame.function) + assertEquals(177, lastFrame.lineno) + assertEquals(true, lastFrame.isInApp) + val otherThread = it.threads!![1] + assertEquals("perfetto_hprof_listener", otherThread.name) + assertEquals(7, otherThread.id) + assertEquals("Native", otherThread.state) + assertEquals(false, otherThread.isCrashed) + assertEquals(false, otherThread.isMain) + val firstFrame = otherThread.stacktrace!!.frames!!.first() + assertEquals( + "/apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b)", + firstFrame.`package` + ) }, argThat { val hint = HintUtils.getSentrySdkHint(this) @@ -218,7 +271,7 @@ class AnrV2IntegrationTest { } @Test - fun `when latest ANR has foreground importance, does not add Background to the name`() { + fun `when latest ANR has foreground importance, sets abnormal mechanism to anr_foreground`() { val integration = fixture.getSut(tmpDir, lastReportedAnrTimestamp = oldTimestamp) fixture.addAppExitInfo( timestamp = newTimestamp, @@ -228,10 +281,11 @@ class AnrV2IntegrationTest { integration.register(fixture.hub, fixture.options) verify(fixture.hub).captureEvent( - argThat { - throwable is ApplicationNotResponding && throwable!!.message == "ANR" - }, - anyOrNull() + any(), + argThat { + val hint = HintUtils.getSentrySdkHint(this) + (hint as AnrV2Hint).mechanism() == "anr_foreground" + } ) } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt new file mode 100644 index 0000000000..92afb3823f --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt @@ -0,0 +1,67 @@ +package io.sentry.android.core.internal.threaddump + +import io.sentry.SentryLockReason +import io.sentry.SentryOptions +import java.io.File +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class ThreadDumpParserTest { + + @Test + fun `parses thread dump into SentryThread list`() { + val lines = Lines.readLines(File("src/test/resources/thread_dump.txt")) + val parser = ThreadDumpParser( + SentryOptions().apply { addInAppInclude("io.sentry.samples") }, + false + ) + val threads = parser.parse(lines) + // just verifying a few important threads, as there are many + val main = threads.find { it.name == "main" } + assertEquals(1, main!!.id) + assertEquals("Blocked", main.state) + assertEquals(true, main.isCrashed) + assertEquals(true, main.isMain) + assertEquals(true, main.isCurrent) + assertNotNull(main.heldLocks!!["0x0d3a2f0a"]) + assertEquals(SentryLockReason.BLOCKED, main.heldLocks!!["0x0d3a2f0a"]!!.type) + assertEquals(5, main.heldLocks!!["0x0d3a2f0a"]!!.threadId) + val lastFrame = main.stacktrace!!.frames!!.last() + assertEquals("io.sentry.samples.android.MainActivity$2", lastFrame.module) + assertEquals("MainActivity.java", lastFrame.filename) + assertEquals("run", lastFrame.function) + assertEquals(177, lastFrame.lineno) + assertEquals(true, lastFrame.isInApp) + + val blockingThread = threads.find { it.name == "Thread-9" } + assertEquals(5, blockingThread!!.id) + assertEquals("Sleeping", blockingThread.state) + assertEquals(false, blockingThread.isCrashed) + assertEquals(false, blockingThread.isMain) + assertNotNull(blockingThread.heldLocks!!["0x0d3a2f0a"]) + assertEquals(SentryLockReason.LOCKED, blockingThread.heldLocks!!["0x0d3a2f0a"]!!.type) + assertEquals(null, blockingThread.heldLocks!!["0x0d3a2f0a"]!!.threadId) + assertNotNull(blockingThread.heldLocks!!["0x09228c2d"]) + assertEquals(SentryLockReason.SLEEPING, blockingThread.heldLocks!!["0x09228c2d"]!!.type) + assertEquals(null, blockingThread.heldLocks!!["0x09228c2d"]!!.threadId) + + val randomThread = + threads.find { it.name == "io.sentry.android.core.internal.util.SentryFrameMetricsCollector" } + assertEquals(19, randomThread!!.id) + assertEquals("Native", randomThread.state) + assertEquals(false, randomThread.isCrashed) + assertEquals(false, randomThread.isMain) + assertEquals(false, randomThread.isCurrent) + assertEquals( + "/apex/com.android.runtime/lib64/bionic/libc.so (__epoll_pwait+8) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b)", + randomThread.stacktrace!!.frames!!.last().`package` + ) + val firstFrame = randomThread.stacktrace!!.frames!!.first() + assertEquals("android.os.HandlerThread", firstFrame.module) + assertEquals("run", firstFrame.function) + assertEquals("HandlerThread.java", firstFrame.filename) + assertEquals(67, firstFrame.lineno) + assertEquals(null, firstFrame.isInApp) + } +} diff --git a/sentry-android-core/src/test/resources/thread_dump.txt b/sentry-android-core/src/test/resources/thread_dump.txt new file mode 100644 index 0000000000..e1f3aa2c64 --- /dev/null +++ b/sentry-android-core/src/test/resources/thread_dump.txt @@ -0,0 +1,660 @@ + +----- pid 28941 at 2023-04-04 22:06:31.064728684+0200 ----- +Cmd line: io.sentry.samples.android +Build fingerprint: 'google/sdk_gphone64_arm64/emu64a:13/TE1A.220922.012/9302419:userdebug/dev-keys' +ABI: 'arm64' +Build type: optimized +Zygote loaded classes=21575 post zygote classes=2000 +Dumping registered class loaders +#0 dalvik.system.PathClassLoader: [], parent #1 +#1 java.lang.BootClassLoader: [], no parent +#2 dalvik.system.PathClassLoader: [/data/app/~~YQzOOq5EdbRpDSLuP4W5bg==/io.sentry.samples.android-rf1QICG-91naFlmtGfmOTQ==/base.apk!classes20.dex:/data/app/~~YQzOOq5EdbRpDSLuP4W5bg==/io.sentry.samples.android-rf1QICG-91naFlmtGfmOTQ==/base.apk!classes18.dex:/data/app/~~YQzOOq5EdbRpDSLuP4W5bg==/io.sentry.samples.android-rf1QICG-91naFlmtGfmOTQ==/base.apk!classes17.dex:/data/app/~~YQzOOq5EdbRpDSLuP4W5bg==/io.sentry.samples.android-rf1QICG-91naFlmtGfmOTQ==/base.apk!classes9.dex:/data/app/~~YQzOOq5EdbRpDSLuP4W5bg==/io.sentry.samples.android-rf1QICG-91naFlmtGfmOTQ==/base.apk!classes7.dex:/data/app/~~YQzOOq5EdbRpDSLuP4W5bg==/io.sentry.samples.android-rf1QICG-91naFlmtGfmOTQ==/base.apk!classes11.dex:/data/app/~~YQzOOq5EdbRpDSLuP4W5bg==/io.sentry.samples.android-rf1QICG-91naFlmtGfmOTQ==/base.apk!classes13.dex:/data/app/~~YQzOOq5EdbRpDSLuP4W5bg==/io.sentry.samples.android-rf1QICG-91naFlmtGfmOTQ==/base.apk!classes12.dex:/data/app/~~YQzOOq5EdbRpDSLuP4W5bg==/io.sentry.samples.android-rf1QICG-91naFlmtGfmOTQ==/base.apk!classes8.dex:/data/app/~~YQzOOq5EdbRpDSLuP4W5bg==/io.sentry.samples.android-rf1QICG-91naFlmtGfmOTQ==/base.apk!classes15.dex:/data/app/~~YQzOOq5EdbRpDSLuP4W5bg==/io.sentry.samples.android-rf1QICG-91naFlmtGfmOTQ==/base.apk!classes10.dex:/data/app/~~YQzOOq5EdbRpDSLuP4W5bg==/io.sentry.samples.android-rf1QICG-91naFlmtGfmOTQ==/base.apk!classes16.dex:/data/app/~~YQzOOq5EdbRpDSLuP4W5bg==/io.sentry.samples.android-rf1QICG-91naFlmtGfmOTQ==/base.apk!classes19.dex:/data/app/~~YQzOOq5EdbRpDSLuP4W5bg==/io.sentry.samples.android-rf1QICG-91naFlmtGfmOTQ==/base.apk:/data/app/~~YQzOOq5EdbRpDSLuP4W5bg==/io.sentry.samples.android-rf1QICG-91naFlmtGfmOTQ==/base.apk!classes21.dex:/data/app/~~YQzOOq5EdbRpDSLuP4W5bg==/io.sentry.samples.android-rf1QICG-91naFlmtGfmOTQ==/base.apk!classes14.dex:/data/app/~~YQzOOq5EdbRpDSLuP4W5bg==/io.sentry.samples.android-rf1QICG-91naFlmtGfmOTQ==/base.apk!classes3.dex:/data/app/~~YQzOOq5EdbRpDSLuP4W5bg==/io.sentry.samples.android-rf1QICG-91naFlmtGfmOTQ==/base.apk!classes2.dex:/data/app/~~YQzOOq5EdbRpDSLuP4W5bg==/io.sentry.samples.android-rf1QICG-91naFlmtGfmOTQ==/base.apk!classes4.dex], parent #1 +Done dumping class loaders +Classes initialized: 0 in 0 +Intern table: 31377 strong; 1217 weak +JNI: CheckJNI is on; globals=412 (plus 73 weak) +Libraries: /data/app/~~YQzOOq5EdbRpDSLuP4W5bg==/io.sentry.samples.android-rf1QICG-91naFlmtGfmOTQ==/lib/arm64/libsentry-android.so /data/app/~~YQzOOq5EdbRpDSLuP4W5bg==/io.sentry.samples.android-rf1QICG-91naFlmtGfmOTQ==/lib/arm64/libsentry.so /system/lib64/liblog.so libandroid.so libaudioeffect_jni.so libcompiler_rt.so libframework-connectivity-jni.so libframework-connectivity-tiramisu-jni.so libicu_jni.so libjavacore.so libjavacrypto.so libjnigraphics.so libmedia_jni.so libopenjdk.so librs_jni.so librtp_jni.so libsoundpool.so libstats_jni.so libwebviewchromium_loader.so (19) +Heap: 40% free, 4484KB/7592KB; 169353 objects +Dumping cumulative Gc timings +Start Dumping Averages for 1 iterations for concurrent copying +SweepSystemWeaks: Sum: 5.115ms Avg: 5.115ms +Process mark stacks and References: Sum: 1.689ms Avg: 1.689ms +VisitConcurrentRoots: Sum: 1.549ms Avg: 1.549ms +MarkingPhase: Sum: 1.212ms Avg: 1.212ms +ScanImmuneSpaces: Sum: 913us Avg: 913us +GrayAllDirtyImmuneObjects: Sum: 500us Avg: 500us +ClearFromSpace: Sum: 245us Avg: 245us +SweepLargeObjects: Sum: 113us Avg: 113us +CaptureThreadRootsForMarking: Sum: 96us Avg: 96us +EnqueueFinalizerReferences: Sum: 94us Avg: 94us +ScanCardsForSpace: Sum: 69us Avg: 69us +FlipOtherThreads: Sum: 33us Avg: 33us +ForwardSoftReferences: Sum: 30us Avg: 30us +InitializePhase: Sum: 28us Avg: 28us +ResumeRunnableThreads: Sum: 19us Avg: 19us +VisitNonThreadRoots: Sum: 17us Avg: 17us +MarkStackAsLive: Sum: 15us Avg: 15us +RecordFree: Sum: 13us Avg: 13us +MarkZygoteLargeObjects: Sum: 10us Avg: 10us +SweepAllocSpace: Sum: 10us Avg: 10us +ProcessReferences: Sum: 10us Avg: 10us +(Paused)GrayAllNewlyDirtyImmuneObjects: Sum: 9us Avg: 9us +SwapBitmaps: Sum: 7us Avg: 7us +EmptyRBMarkBitStack: Sum: 4us Avg: 4us +CopyingPhase: Sum: 2us Avg: 2us +ThreadListFlip: Sum: 2us Avg: 2us +(Paused)ClearCards: Sum: 2us Avg: 2us +ReclaimPhase: Sum: 1us Avg: 1us +(Paused)FlipCallback: Sum: 0 Avg: 0 +UnBindBitmaps: Sum: 0 Avg: 0 +ResumeOtherThreads: Sum: 0 Avg: 0 +FlipThreadRoots: Sum: 0 Avg: 0 +Sweep: Sum: 0 Avg: 0 +(Paused)SetFromSpace: Sum: 0 Avg: 0 +Done Dumping Averages +concurrent copying paused: Sum: 20us 99% C.I. 5us-15us Avg: 10us Max: 15us +concurrent copying freed-bytes: Avg: 7364KB Max: 7364KB Min: 7364KB +Freed-bytes histogram: 7040:1 +concurrent copying total time: 11.807ms mean time: 11.807ms +concurrent copying freed: 53426 objects with total size 7364KB +concurrent copying throughput: 4.85691e+06/s / 653MB/s per cpu-time: 1077389714/s / 1027MB/s +concurrent copying tracing throughput: 198MB/s per cpu-time: 312MB/s +Average major GC reclaim bytes ratio 1.11802 over 1 GC cycles +Average major GC copied live bytes ratio 0.725846 over 5 major GCs +Cumulative bytes moved 30421320 +Cumulative objects moved 534294 +Peak regions allocated 50 (12MB) / 768 (192MB) +Total madvise time 1.829ms +Average minor GC reclaim bytes ratio inf over 0 GC cycles +Average minor GC copied live bytes ratio 0.28859 over 2 minor GCs +Cumulative bytes moved 3695344 +Cumulative objects moved 89281 +Peak regions allocated 50 (12MB) / 768 (192MB) +Total time spent in GC: 11.807ms +Mean GC size throughput: 609MB/s per cpu-time: 937MB/s +Mean GC object throughput: 4.52494e+06 objects/s +Total number of allocations 222779 +Total bytes allocated 11MB +Total bytes freed 7364KB +Free memory 3107KB +Free memory until GC 3107KB +Free memory until OOME 187MB +Total memory 7592KB +Max memory 192MB +Zygote space size 7744KB +Total mutator paused time: 20us +Total time waiting for GC to complete: 8.054ms +Total GC count: 1 +Total GC time: 11.807ms +Total blocking GC count: 1 +Total blocking GC time: 11.873ms +Total pre-OOME GC count: 0 +Histogram of GC count per 10000 ms: 0:1 +Histogram of blocking GC count per 10000 ms: 0:1 +Native bytes total: 18678627 registered: 635795 +Total native bytes at last GC: 21677907 +/system/framework/oat/arm64/android.hidl.manager-V1.0-java.odex: verify +/system/framework/oat/arm64/android.hidl.base-V1.0-java.odex: verify +/system/framework/oat/arm64/android.test.base.odex: verify +Current JIT code cache size (used / resident): 59KB / 64KB +Current JIT data cache size (used / resident): 47KB / 52KB +Zygote JIT code cache size (at point of fork): 19KB / 32KB +Zygote JIT data cache size (at point of fork): 14KB / 32KB +Current JIT mini-debug-info size: 32KB +Current JIT capacity: 128KB +Current number of JIT JNI stub entries: 0 +Current number of JIT code cache entries: 44 +Total number of JIT baseline compilations: 31 +Total number of JIT optimized compilations: 1 +Total number of JIT compilations for on stack replacement: 0 +Total number of JIT code cache collections: 1 +Memory used for stack maps: Avg: 533B Max: 2728B Min: 32B +Memory used for compiled code: Avg: 2014B Max: 7140B Min: 196B +Memory used for profiling info: Avg: 371B Max: 1296B Min: 24B +Start Dumping Averages for 47 iterations for JIT timings +Compiling optimized: Sum: 66.471ms Avg: 1.414ms +Code cache collection: Sum: 21.797ms Avg: 463.765us +Compiling baseline: Sum: 14.750ms Avg: 313.829us +TrimMaps: Sum: 717us Avg: 15.255us +Done Dumping Averages +Memory used for compilation: Avg: 156KB Max: 1693KB Min: 16KB +ProfileSaver total_bytes_written=0 +ProfileSaver total_number_of_writes=0 +ProfileSaver total_number_of_code_cache_queries=0 +ProfileSaver total_number_of_skipped_writes=0 +ProfileSaver total_number_of_failed_writes=0 +ProfileSaver total_ms_of_sleep=5000 +ProfileSaver total_ms_of_work=0 +ProfileSaver total_number_of_hot_spikes=0 +ProfileSaver total_number_of_wake_ups=0 + +*** ART internal metrics *** + Metadata: + timestamp_since_start_ms: 18063 + Metrics: + ClassLoadingTotalTime: count = 26287 + ClassVerificationTotalTime: count = 99135 + ClassVerificationCount: count = 1044 + WorldStopTimeDuringGCAvg: count = 21 + YoungGcCount: count = 0 + FullGcCount: count = 1 + TotalBytesAllocated: count = 10151688 + TotalGcCollectionTime: count = 11 + YoungGcThroughputAvg: count = 0 + FullGcThroughputAvg: count = 393 + YoungGcTracingThroughputAvg: count = 0 + FullGcTracingThroughputAvg: count = 184 + JitMethodCompileTotalTime: count = 70979 + JitMethodCompileCount: count = 32 + YoungGcCollectionTime: range = 0...60000, buckets: 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + FullGcCollectionTime: range = 0...60000, buckets: 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + YoungGcThroughput: range = 0...10000, buckets: 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + FullGcThroughput: range = 0...10000, buckets: 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + YoungGcTracingThroughput: range = 0...10000, buckets: 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + FullGcTracingThroughput: range = 0...10000, buckets: 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + GcWorldStopTime: count = 21 + GcWorldStopCount: count = 1 + YoungGcScannedBytes: count = 0 + YoungGcFreedBytes: count = 0 + YoungGcDuration: count = 0 + FullGcScannedBytes: count = 2293021 + FullGcFreedBytes: count = 4957152 + FullGcDuration: count = 11 +*** Done dumping ART internal metrics *** + +suspend all histogram: Sum: 3.220ms 99% C.I. 0.161us-58.527us Avg: 5.639us Max: 1225us +DALVIK THREADS (29): +"Signal Catcher" daemon prio=10 tid=6 Runnable + | group="system" sCount=0 ucsCount=0 flags=0 obj=0x136c0248 self=0xb400007cabc689a0 + | sysTid=28957 nice=-20 cgrp=top-app sched=0/0 handle=0x7b21319cb0 + | state=R schedstat=( 6825503 402209 26 ) utm=0 stm=0 core=0 HZ=100 + | stack=0x7b21222000-0x7b21224000 stackSize=991KB + | held mutexes= "mutator lock"(shared held) + native: #00 pc 000000000053a6e0 /apex/com.android.art/lib64/libart.so (art::DumpNativeStack(std::__1::basic_ostream >&, int, BacktraceMap*, char const*, art::ArtMethod*, void*, bool)+128) (BuildId: e24a1818231cfb1649cb83a5d2869598) + native: #01 pc 00000000006f0e84 /apex/com.android.art/lib64/libart.so (art::Thread::DumpStack(std::__1::basic_ostream >&, bool, BacktraceMap*, bool) const+236) (BuildId: e24a1818231cfb1649cb83a5d2869598) + native: #02 pc 00000000006fe710 /apex/com.android.art/lib64/libart.so (art::DumpCheckpoint::Run(art::Thread*)+208) (BuildId: e24a1818231cfb1649cb83a5d2869598) + native: #03 pc 0000000000364248 /apex/com.android.art/lib64/libart.so (art::ThreadList::RunCheckpoint(art::Closure*, art::Closure*)+440) (BuildId: e24a1818231cfb1649cb83a5d2869598) + native: #04 pc 00000000006fceb0 /apex/com.android.art/lib64/libart.so (art::ThreadList::Dump(std::__1::basic_ostream >&, bool)+280) (BuildId: e24a1818231cfb1649cb83a5d2869598) + native: #05 pc 00000000006fc8a4 /apex/com.android.art/lib64/libart.so (art::ThreadList::DumpForSigQuit(std::__1::basic_ostream >&)+292) (BuildId: e24a1818231cfb1649cb83a5d2869598) + native: #06 pc 00000000006d5974 /apex/com.android.art/lib64/libart.so (art::Runtime::DumpForSigQuit(std::__1::basic_ostream >&)+184) (BuildId: e24a1818231cfb1649cb83a5d2869598) + native: #07 pc 00000000006e1a20 /apex/com.android.art/lib64/libart.so (art::SignalCatcher::HandleSigQuit()+468) (BuildId: e24a1818231cfb1649cb83a5d2869598) + native: #08 pc 0000000000574230 /apex/com.android.art/lib64/libart.so (art::SignalCatcher::Run(void*)+264) (BuildId: e24a1818231cfb1649cb83a5d2869598) + native: #09 pc 00000000000b63b0 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+208) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #10 pc 00000000000530b8 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + (no managed stack frames) + +"main" prio=5 tid=1 Blocked + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x72a985e0 self=0xb400007cabc57380 + | sysTid=28941 nice=-10 cgrp=top-app sched=0/0 handle=0x7deceb74f8 + | state=S schedstat=( 324804784 183300334 997 ) utm=23 stm=8 core=3 HZ=100 + | stack=0x7ff93a9000-0x7ff93ab000 stackSize=8188KB + | held mutexes= + at io.sentry.samples.android.MainActivity$2.run(MainActivity.java:177) + - waiting to lock <0x0d3a2f0a> (a java.lang.Object) held by thread 5 + at android.os.Handler.handleCallback(Handler.java:942) + at android.os.Handler.dispatchMessage(Handler.java:99) + at android.os.Looper.loopOnce(Looper.java:201) + at android.os.Looper.loop(Looper.java:288) + at android.app.ActivityThread.main(ActivityThread.java:7872) + at java.lang.reflect.Method.invoke(Native method) + at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548) + at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936) + +"perfetto_hprof_listener" prio=10 tid=7 Native (still starting up) + | group="" sCount=1 ucsCount=0 flags=1 obj=0x0 self=0xb400007cabc5ab20 + | sysTid=28959 nice=-20 cgrp=top-app sched=0/0 handle=0x7b2021bcb0 + | state=S schedstat=( 72750 1679167 1 ) utm=0 stm=0 core=3 HZ=100 + | stack=0x7b20124000-0x7b20126000 stackSize=991KB + | held mutexes= + native: #00 pc 00000000000a20f4 /apex/com.android.runtime/lib64/bionic/libc.so (read+4) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #01 pc 000000000001d840 /apex/com.android.art/lib64/libperfetto_hprof.so (void* std::__1::__thread_proxy >, ArtPlugin_Initialize::$_34> >(void*)+260) (BuildId: 525cc92a7dc49130157aeb74f6870364) + native: #02 pc 00000000000b63b0 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+208) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #03 pc 00000000000530b8 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + (no managed stack frames) + +"ADB-JDWP Connection Control Thread" daemon prio=0 tid=8 WaitingInMainDebuggerLoop + | group="system" sCount=1 ucsCount=0 flags=1 obj=0x136c02c0 self=0xb400007cabc5c6f0 + | sysTid=28960 nice=-20 cgrp=top-app sched=0/0 handle=0x7b2011dcb0 + | state=S schedstat=( 1003335 1331749 27 ) utm=0 stm=0 core=3 HZ=100 + | stack=0x7b20026000-0x7b20028000 stackSize=991KB + | held mutexes= + native: #00 pc 00000000000a34b8 /apex/com.android.runtime/lib64/bionic/libc.so (__ppoll+8) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #01 pc 000000000005dc1c /apex/com.android.runtime/lib64/bionic/libc.so (poll+92) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #02 pc 00000000000099e4 /apex/com.android.art/lib64/libadbconnection.so (adbconnection::AdbConnectionState::RunPollLoop(art::Thread*)+724) (BuildId: 3952e992b55a158a16b3d569cf8894e7) + native: #03 pc 00000000000080ac /apex/com.android.art/lib64/libadbconnection.so (adbconnection::CallbackFunction(void*)+1320) (BuildId: 3952e992b55a158a16b3d569cf8894e7) + native: #04 pc 00000000000b63b0 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+208) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #05 pc 00000000000530b8 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + (no managed stack frames) + +"HeapTaskDaemon" daemon prio=5 tid=9 WaitingForTaskProcessor + | group="system" sCount=1 ucsCount=0 flags=1 obj=0x136c1690 self=0xb400007cabc80f00 + | sysTid=28962 nice=4 cgrp=top-app sched=0/0 handle=0x7ad35e8cb0 + | state=S schedstat=( 9212958 1006583 26 ) utm=0 stm=0 core=3 HZ=100 + | stack=0x7ad34e5000-0x7ad34e7000 stackSize=1039KB + | held mutexes= + native: #00 pc 000000000004df60 /apex/com.android.runtime/lib64/bionic/libc.so (syscall+32) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #01 pc 000000000048771c /apex/com.android.art/lib64/libart.so (art::ConditionVariable::TimedWait(art::Thread*, long, int)+252) (BuildId: e24a1818231cfb1649cb83a5d2869598) + native: #02 pc 000000000046cf20 /apex/com.android.art/lib64/libart.so (art::gc::TaskProcessor::GetTask(art::Thread*)+196) (BuildId: e24a1818231cfb1649cb83a5d2869598) + native: #03 pc 000000000046ce10 /apex/com.android.art/lib64/libart.so (art::gc::TaskProcessor::RunAllTasks(art::Thread*)+32) (BuildId: e24a1818231cfb1649cb83a5d2869598) + at dalvik.system.VMRuntime.runHeapTasks(Native method) + at java.lang.Daemons$HeapTaskDaemon.runInternal(Daemons.java:609) + at java.lang.Daemons$Daemon.run(Daemons.java:140) + at java.lang.Thread.run(Thread.java:1012) + +"FinalizerDaemon" daemon prio=5 tid=10 Waiting + | group="system" sCount=1 ucsCount=0 flags=1 obj=0x136c0338 self=0xb400007cabc7f330 + | sysTid=28964 nice=4 cgrp=top-app sched=0/0 handle=0x7ad33d4cb0 + | state=S schedstat=( 1727043 1091459 22 ) utm=0 stm=0 core=0 HZ=100 + | stack=0x7ad32d1000-0x7ad32d3000 stackSize=1039KB + | held mutexes= + at java.lang.Object.wait(Native method) + - waiting on <0x07d7437b> (a java.lang.Object) + at java.lang.Object.wait(Object.java:442) + at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:203) + - locked <0x07d7437b> (a java.lang.Object) + at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:224) + at java.lang.Daemons$FinalizerDaemon.runInternal(Daemons.java:300) + at java.lang.Daemons$Daemon.run(Daemons.java:140) + at java.lang.Thread.run(Thread.java:1012) + +"FinalizerWatchdogDaemon" daemon prio=5 tid=11 Sleeping + | group="system" sCount=1 ucsCount=0 flags=1 obj=0x136c03b0 self=0xb400007cabc7bb90 + | sysTid=28965 nice=4 cgrp=top-app sched=0/0 handle=0x7ad32cacb0 + | state=S schedstat=( 176667 969916 4 ) utm=0 stm=0 core=3 HZ=100 + | stack=0x7ad31c7000-0x7ad31c9000 stackSize=1039KB + | held mutexes= + at java.lang.Thread.sleep(Native method) + - sleeping on <0x09596598> (a java.lang.Object) + at java.lang.Thread.sleep(Thread.java:450) + - locked <0x09596598> (a java.lang.Object) + at java.lang.Thread.sleep(Thread.java:355) + at java.lang.Daemons$FinalizerWatchdogDaemon.sleepForNanos(Daemons.java:438) + at java.lang.Daemons$FinalizerWatchdogDaemon.waitForProgress(Daemons.java:480) + at java.lang.Daemons$FinalizerWatchdogDaemon.runInternal(Daemons.java:369) + at java.lang.Daemons$Daemon.run(Daemons.java:140) + at java.lang.Thread.run(Thread.java:1012) + +"ReferenceQueueDaemon" daemon prio=5 tid=12 Waiting + | group="system" sCount=1 ucsCount=0 flags=1 obj=0x136c0428 self=0xb400007cabc7d760 + | sysTid=28963 nice=4 cgrp=top-app sched=0/0 handle=0x7ad34decb0 + | state=S schedstat=( 437749 1043042 4 ) utm=0 stm=0 core=1 HZ=100 + | stack=0x7ad33db000-0x7ad33dd000 stackSize=1039KB + | held mutexes= + at java.lang.Object.wait(Native method) + - waiting on <0x0394c1f1> (a java.lang.Class) + at java.lang.Object.wait(Object.java:442) + at java.lang.Object.wait(Object.java:568) + at java.lang.Daemons$ReferenceQueueDaemon.runInternal(Daemons.java:232) + - locked <0x0394c1f1> (a java.lang.Class) + at java.lang.Daemons$Daemon.run(Daemons.java:140) + at java.lang.Thread.run(Thread.java:1012) + +"Jit thread pool worker thread 0" daemon prio=5 tid=13 Native + | group="system" sCount=1 ucsCount=0 flags=1 obj=0x136c04a0 self=0xb400007cabc87e40 + | sysTid=28961 nice=9 cgrp=top-app sched=0/0 handle=0x7ad36eecb0 + | state=S schedstat=( 17155174 12570536 78 ) utm=1 stm=0 core=3 HZ=100 + | stack=0x7ad35ef000-0x7ad35f1000 stackSize=1023KB + | held mutexes= + native: #00 pc 000000000004df5c /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #01 pc 000000000047cc80 /apex/com.android.art/lib64/libart.so (art::ConditionVariable::WaitHoldingLocks(art::Thread*)+140) (BuildId: e24a1818231cfb1649cb83a5d2869598) + native: #02 pc 000000000047cb18 /apex/com.android.art/lib64/libart.so (art::ThreadPool::GetTask(art::Thread*)+120) (BuildId: e24a1818231cfb1649cb83a5d2869598) + native: #03 pc 00000000006199e4 /apex/com.android.art/lib64/libart.so (art::ThreadPoolWorker::Run()+136) (BuildId: e24a1818231cfb1649cb83a5d2869598) + native: #04 pc 00000000006198c4 /apex/com.android.art/lib64/libart.so (art::ThreadPoolWorker::Callback(void*)+160) (BuildId: e24a1818231cfb1649cb83a5d2869598) + native: #05 pc 00000000000b63b0 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+208) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #06 pc 00000000000530b8 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + (no managed stack frames) + +"binder:28941_1" prio=5 tid=14 Native + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x136c0518 self=0xb400007cabc82ad0 + | sysTid=28966 nice=0 cgrp=top-app sched=0/0 handle=0x7ace0a9cb0 + | state=S schedstat=( 574039 4838087 11 ) utm=0 stm=0 core=1 HZ=100 + | stack=0x7acdfb2000-0x7acdfb4000 stackSize=991KB + | held mutexes= + native: #00 pc 00000000000a23d8 /apex/com.android.runtime/lib64/bionic/libc.so (__ioctl+8) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #01 pc 000000000005b50c /apex/com.android.runtime/lib64/bionic/libc.so (ioctl+156) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #02 pc 0000000000094690 /system/lib64/libbinder.so (android::IPCThreadState::joinThreadPool(bool)+316) (BuildId: ee18e52b95e38eaab55a9a48518c8c3b) + native: #03 pc 0000000000094540 /system/lib64/libbinder.so (android::PoolThread::threadLoop()+24) (BuildId: ee18e52b95e38eaab55a9a48518c8c3b) + native: #04 pc 00000000000148e8 /system/lib64/libutils.so (android::Thread::_threadLoop(void*)+528) (BuildId: 5a0d720732600c94ad8354a1188e9f52) + native: #05 pc 00000000000c8918 /system/lib64/libandroid_runtime.so (android::AndroidRuntime::javaThreadShell(void*)+140) (BuildId: a31474ac581b716d4588f8c97eb06009) + native: #06 pc 00000000000b63b0 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+208) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #07 pc 00000000000530b8 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + (no managed stack frames) + +"binder:28941_2" prio=5 tid=15 Native + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x136c0590 self=0xb400007cabc86270 + | sysTid=28967 nice=0 cgrp=top-app sched=0/0 handle=0x7accfabcb0 + | state=S schedstat=( 4159627 3435666 30 ) utm=0 stm=0 core=2 HZ=100 + | stack=0x7acceb4000-0x7acceb6000 stackSize=991KB + | held mutexes= + native: #00 pc 00000000000a23d8 /apex/com.android.runtime/lib64/bionic/libc.so (__ioctl+8) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #01 pc 000000000005b50c /apex/com.android.runtime/lib64/bionic/libc.so (ioctl+156) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #02 pc 0000000000094690 /system/lib64/libbinder.so (android::IPCThreadState::joinThreadPool(bool)+316) (BuildId: ee18e52b95e38eaab55a9a48518c8c3b) + native: #03 pc 0000000000094540 /system/lib64/libbinder.so (android::PoolThread::threadLoop()+24) (BuildId: ee18e52b95e38eaab55a9a48518c8c3b) + native: #04 pc 00000000000148e8 /system/lib64/libutils.so (android::Thread::_threadLoop(void*)+528) (BuildId: 5a0d720732600c94ad8354a1188e9f52) + native: #05 pc 00000000000c8918 /system/lib64/libandroid_runtime.so (android::AndroidRuntime::javaThreadShell(void*)+140) (BuildId: a31474ac581b716d4588f8c97eb06009) + native: #06 pc 00000000000b63b0 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+208) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #07 pc 00000000000530b8 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + (no managed stack frames) + +"binder:28941_3" prio=5 tid=16 Native + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x136c0608 self=0xb400007cabc846a0 + | sysTid=28975 nice=0 cgrp=top-app sched=0/0 handle=0x7acbeadcb0 + | state=S schedstat=( 2250710 7051668 27 ) utm=0 stm=0 core=1 HZ=100 + | stack=0x7acbdb6000-0x7acbdb8000 stackSize=991KB + | held mutexes= + native: #00 pc 00000000000a23d8 /apex/com.android.runtime/lib64/bionic/libc.so (__ioctl+8) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #01 pc 000000000005b50c /apex/com.android.runtime/lib64/bionic/libc.so (ioctl+156) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #02 pc 0000000000094690 /system/lib64/libbinder.so (android::IPCThreadState::joinThreadPool(bool)+316) (BuildId: ee18e52b95e38eaab55a9a48518c8c3b) + native: #03 pc 0000000000094540 /system/lib64/libbinder.so (android::PoolThread::threadLoop()+24) (BuildId: ee18e52b95e38eaab55a9a48518c8c3b) + native: #04 pc 00000000000148e8 /system/lib64/libutils.so (android::Thread::_threadLoop(void*)+528) (BuildId: 5a0d720732600c94ad8354a1188e9f52) + native: #05 pc 00000000000c8918 /system/lib64/libandroid_runtime.so (android::AndroidRuntime::javaThreadShell(void*)+140) (BuildId: a31474ac581b716d4588f8c97eb06009) + native: #06 pc 00000000000b63b0 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+208) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #07 pc 00000000000530b8 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + (no managed stack frames) + +"Profile Saver" daemon prio=5 tid=17 Native + | group="system" sCount=1 ucsCount=0 flags=1 obj=0x136c0680 self=0xb400007cabc89a10 + | sysTid=28980 nice=9 cgrp=top-app sched=0/0 handle=0x7ac9a80cb0 + | state=S schedstat=( 6243002 407667 10 ) utm=0 stm=0 core=2 HZ=100 + | stack=0x7ac9989000-0x7ac998b000 stackSize=991KB + | held mutexes= + native: #00 pc 000000000004df5c /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #01 pc 000000000047cc80 /apex/com.android.art/lib64/libart.so (art::ConditionVariable::WaitHoldingLocks(art::Thread*)+140) (BuildId: e24a1818231cfb1649cb83a5d2869598) + native: #02 pc 0000000000543774 /apex/com.android.art/lib64/libart.so (art::ProfileSaver::Run()+372) (BuildId: e24a1818231cfb1649cb83a5d2869598) + native: #03 pc 0000000000538fc0 /apex/com.android.art/lib64/libart.so (art::ProfileSaver::RunProfileSaverThread(void*)+148) (BuildId: e24a1818231cfb1649cb83a5d2869598) + native: #04 pc 00000000000b63b0 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+208) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #05 pc 00000000000530b8 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + (no managed stack frames) + +"io.sentry.android.core.internal.util.SentryFrameMetricsCollector" prio=5 tid=19 Native + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x136c06f8 self=0xb400007cabc8b5e0 + | sysTid=28991 nice=0 cgrp=top-app sched=0/0 handle=0x7ac74d0cb0 + | state=S schedstat=( 6494997 2246873 96 ) utm=0 stm=0 core=0 HZ=100 + | stack=0x7ac73cd000-0x7ac73cf000 stackSize=1039KB + | held mutexes= + native: #00 pc 00000000000a33b8 /apex/com.android.runtime/lib64/bionic/libc.so (__epoll_pwait+8) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #01 pc 0000000000010dfc /system/lib64/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+176) (BuildId: 5a0d720732600c94ad8354a1188e9f52) + native: #02 pc 000000000015a56c /system/lib64/libandroid_runtime.so (android::android_os_MessageQueue_nativePollOnce(_JNIEnv*, _jobject*, long, int)+44) (BuildId: a31474ac581b716d4588f8c97eb06009) + at android.os.MessageQueue.nativePollOnce(Native method) + at android.os.MessageQueue.next(MessageQueue.java:335) + at android.os.Looper.loopOnce(Looper.java:161) + at android.os.Looper.loop(Looper.java:288) + at android.os.HandlerThread.run(HandlerThread.java:67) + +"pool-2-thread-1" prio=5 tid=20 TimedWaiting + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x136c0818 self=0xb400007cabc95cc0 + | sysTid=28993 nice=0 cgrp=top-app sched=0/0 handle=0x7ac63a6cb0 + | state=S schedstat=( 51027282 10362044 134 ) utm=2 stm=2 core=2 HZ=100 + | stack=0x7ac62a3000-0x7ac62a5000 stackSize=1039KB + | held mutexes= + at jdk.internal.misc.Unsafe.park(Native method) + - waiting on an unknown object + at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:234) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2123) + at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1188) + at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:905) + at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1063) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1123) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637) + at java.lang.Thread.run(Thread.java:1012) + +"SentryAsyncConnection-0" daemon prio=5 tid=18 Waiting + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x136c0970 self=0xb400007cabc8d1b0 + | sysTid=28994 nice=0 cgrp=top-app sched=0/0 handle=0x7ac857acb0 + | state=S schedstat=( 48202960 58606620 247 ) utm=4 stm=0 core=1 HZ=100 + | stack=0x7ac8477000-0x7ac8479000 stackSize=1039KB + | held mutexes= + at jdk.internal.misc.Unsafe.park(Native method) + - waiting on an unknown object + at java.util.concurrent.locks.LockSupport.park(LockSupport.java:194) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2081) + at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:433) + at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1063) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1123) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637) + at java.lang.Thread.run(Thread.java:1012) + +"FileObserver" prio=5 tid=21 Native + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x136c0ae8 self=0xb400007cabc92520 + | sysTid=28995 nice=0 cgrp=top-app sched=0/0 handle=0x7ac8450cb0 + | state=S schedstat=( 147291 1694959 5 ) utm=0 stm=0 core=3 HZ=100 + | stack=0x7ac834d000-0x7ac834f000 stackSize=1039KB + | held mutexes= + native: #00 pc 00000000000a20f4 /apex/com.android.runtime/lib64/bionic/libc.so (read+4) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #01 pc 00000000001b0fe4 /system/lib64/libandroid_runtime.so (android::android_os_fileobserver_observe(_JNIEnv*, _jobject*, int)+164) (BuildId: a31474ac581b716d4588f8c97eb06009) + at android.os.FileObserver$ObserverThread.observe(Native method) + at android.os.FileObserver$ObserverThread.run(FileObserver.java:116) + +"Timer-0" daemon prio=5 tid=22 Waiting + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x136c0b68 self=0xb400007cabc940f0 + | sysTid=28996 nice=0 cgrp=top-app sched=0/0 handle=0x7ac8346cb0 + | state=S schedstat=( 32541 2753958 2 ) utm=0 stm=0 core=1 HZ=100 + | stack=0x7ac8243000-0x7ac8245000 stackSize=1039KB + | held mutexes= + at java.lang.Object.wait(Native method) + - waiting on <0x0ad431d6> (a java.util.TaskQueue) + at java.lang.Object.wait(Object.java:442) + at java.lang.Object.wait(Object.java:568) + at java.util.TimerThread.mainLoop(Timer.java:534) + - locked <0x0ad431d6> (a java.util.TaskQueue) + at java.util.TimerThread.run(Timer.java:513) + +"ConnectivityThread" prio=5 tid=23 Native + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x136c0bf0 self=0xb400007cabc90950 + | sysTid=28997 nice=0 cgrp=top-app sched=0/0 handle=0x7ac823ccb0 + | state=S schedstat=( 4569546 2179707 44 ) utm=0 stm=0 core=1 HZ=100 + | stack=0x7ac8139000-0x7ac813b000 stackSize=1039KB + | held mutexes= + native: #00 pc 00000000000a33b8 /apex/com.android.runtime/lib64/bionic/libc.so (__epoll_pwait+8) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #01 pc 0000000000010dfc /system/lib64/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+176) (BuildId: 5a0d720732600c94ad8354a1188e9f52) + native: #02 pc 000000000015a56c /system/lib64/libandroid_runtime.so (android::android_os_MessageQueue_nativePollOnce(_JNIEnv*, _jobject*, long, int)+44) (BuildId: a31474ac581b716d4588f8c97eb06009) + at android.os.MessageQueue.nativePollOnce(Native method) + at android.os.MessageQueue.next(MessageQueue.java:335) + at android.os.Looper.loopOnce(Looper.java:161) + at android.os.Looper.loop(Looper.java:288) + at android.os.HandlerThread.run(HandlerThread.java:67) + +"LeakCanary-Heap-Dump" prio=5 tid=24 Native + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x136c0d10 self=0xb400007cabc8ed80 + | sysTid=29000 nice=0 cgrp=top-app sched=0/0 handle=0x7ac80d2cb0 + | state=S schedstat=( 10523711 3945164 46 ) utm=0 stm=0 core=2 HZ=100 + | stack=0x7ac7fcf000-0x7ac7fd1000 stackSize=1039KB + | held mutexes= + native: #00 pc 00000000000a33b8 /apex/com.android.runtime/lib64/bionic/libc.so (__epoll_pwait+8) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #01 pc 0000000000010dfc /system/lib64/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+176) (BuildId: 5a0d720732600c94ad8354a1188e9f52) + native: #02 pc 000000000015a56c /system/lib64/libandroid_runtime.so (android::android_os_MessageQueue_nativePollOnce(_JNIEnv*, _jobject*, long, int)+44) (BuildId: a31474ac581b716d4588f8c97eb06009) + at android.os.MessageQueue.nativePollOnce(Native method) + at android.os.MessageQueue.next(MessageQueue.java:335) + at android.os.Looper.loopOnce(Looper.java:161) + at android.os.Looper.loop(Looper.java:288) + at android.os.HandlerThread.run(HandlerThread.java:67) + +"plumber-android-leaks" prio=5 tid=25 TimedWaiting + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x136c0e30 self=0xb400007cabc99460 + | sysTid=29001 nice=10 cgrp=top-app sched=0/0 handle=0x7ac7fc8cb0 + | state=S schedstat=( 3488459 357249 18 ) utm=0 stm=0 core=1 HZ=100 + | stack=0x7ac7ec5000-0x7ac7ec7000 stackSize=1039KB + | held mutexes= + at jdk.internal.misc.Unsafe.park(Native method) + - waiting on an unknown object + at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:234) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2123) + at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1188) + at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:905) + at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1063) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1123) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637) + at leakcanary.AndroidLeakFixes$Companion$backgroundExecutor$1$thread$1.run(AndroidLeakFixes.kt:728) + +"RenderThread" daemon prio=7 tid=26 Native + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x136c0f88 self=0xb400007cabc97890 + | sysTid=29004 nice=-10 cgrp=top-app sched=0/0 handle=0x7ac7ebecb0 + | state=S schedstat=( 160761573 37179129 468 ) utm=4 stm=11 core=1 HZ=100 + | stack=0x7ac7dc7000-0x7ac7dc9000 stackSize=991KB + | held mutexes= + native: #00 pc 00000000000a33b8 /apex/com.android.runtime/lib64/bionic/libc.so (__epoll_pwait+8) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #01 pc 0000000000010dfc /system/lib64/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+176) (BuildId: 5a0d720732600c94ad8354a1188e9f52) + native: #02 pc 000000000057c4c0 /system/lib64/libhwui.so (android::uirenderer::renderthread::RenderThread::threadLoop()+220) (BuildId: 5e787210ce0f171dbee073e4a14a376c) + native: #03 pc 00000000000148e8 /system/lib64/libutils.so (android::Thread::_threadLoop(void*)+528) (BuildId: 5a0d720732600c94ad8354a1188e9f52) + native: #04 pc 00000000000b63b0 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+208) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #05 pc 00000000000530b8 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + (no managed stack frames) + +"OkHttp ConnectionPool" daemon prio=5 tid=29 TimedWaiting + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x136c1000 self=0xb400007cabca03a0 + | sysTid=29010 nice=0 cgrp=top-app sched=0/0 handle=0x7ac7905cb0 + | state=S schedstat=( 198166 0 1 ) utm=0 stm=0 core=1 HZ=100 + | stack=0x7ac7802000-0x7ac7804000 stackSize=1039KB + | held mutexes= + at java.lang.Object.wait(Native method) + - waiting on <0x050c9c57> (a com.android.okhttp.ConnectionPool) + at com.android.okhttp.ConnectionPool$1.run(ConnectionPool.java:106) + - locked <0x050c9c57> (a com.android.okhttp.ConnectionPool) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637) + at java.lang.Thread.run(Thread.java:1012) + +"FrameMetricsAggregator" prio=5 tid=30 Native + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x136c1128 self=0xb400007cabca1f70 + | sysTid=29011 nice=0 cgrp=top-app sched=0/0 handle=0x7ac77fbcb0 + | state=S schedstat=( 7302250 10493628 107 ) utm=0 stm=0 core=1 HZ=100 + | stack=0x7ac76f8000-0x7ac76fa000 stackSize=1039KB + | held mutexes= + native: #00 pc 00000000000a33b8 /apex/com.android.runtime/lib64/bionic/libc.so (__epoll_pwait+8) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #01 pc 0000000000010dfc /system/lib64/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+176) (BuildId: 5a0d720732600c94ad8354a1188e9f52) + native: #02 pc 000000000015a56c /system/lib64/libandroid_runtime.so (android::android_os_MessageQueue_nativePollOnce(_JNIEnv*, _jobject*, long, int)+44) (BuildId: a31474ac581b716d4588f8c97eb06009) + at android.os.MessageQueue.nativePollOnce(Native method) + at android.os.MessageQueue.next(MessageQueue.java:335) + at android.os.Looper.loopOnce(Looper.java:161) + at android.os.Looper.loop(Looper.java:288) + at android.os.HandlerThread.run(HandlerThread.java:67) + +"hwuiTask0" daemon prio=6 tid=31 Native + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x136c1248 self=0xb400007cabca3b40 + | sysTid=29026 nice=-2 cgrp=top-app sched=0/0 handle=0x7ac75f3cb0 + | state=S schedstat=( 148373 0 2 ) utm=0 stm=0 core=1 HZ=100 + | stack=0x7ac74fc000-0x7ac74fe000 stackSize=991KB + | held mutexes= + native: #00 pc 000000000004df5c /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #01 pc 0000000000052664 /apex/com.android.runtime/lib64/bionic/libc.so (__futex_wait_ex(void volatile*, bool, int, bool, timespec const*)+144) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #02 pc 00000000000b56cc /apex/com.android.runtime/lib64/bionic/libc.so (pthread_cond_wait+76) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #03 pc 00000000000699e0 /system/lib64/libc++.so (std::__1::condition_variable::wait(std::__1::unique_lock&)+20) (BuildId: 6ae0290e5bfb8abb216bde2a4ee48d9e) + native: #04 pc 0000000000250af8 /system/lib64/libhwui.so (android::uirenderer::CommonPool::workerLoop()+96) (BuildId: 5e787210ce0f171dbee073e4a14a376c) + native: #05 pc 0000000000250d5c /system/lib64/libhwui.so (android::uirenderer::CommonPool::CommonPool()::$_0::operator()() const (.__uniq.99815402873434996937524029735804459536)+188) (BuildId: 5e787210ce0f171dbee073e4a14a376c) + native: #06 pc 0000000000250c9c /system/lib64/libhwui.so (void* std::__1::__thread_proxy >, android::uirenderer::CommonPool::CommonPool()::$_0> >(void*) (.__uniq.99815402873434996937524029735804459536)+40) (BuildId: 5e787210ce0f171dbee073e4a14a376c) + native: #07 pc 00000000000b63b0 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+208) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #08 pc 00000000000530b8 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + (no managed stack frames) + +"hwuiTask1" daemon prio=6 tid=32 Native + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x136c12c0 self=0xb400007cabcb19c0 + | sysTid=29027 nice=-2 cgrp=top-app sched=0/0 handle=0x7ab7b63cb0 + | state=S schedstat=( 112416 0 2 ) utm=0 stm=0 core=0 HZ=100 + | stack=0x7ab7a6c000-0x7ab7a6e000 stackSize=991KB + | held mutexes= + native: #00 pc 000000000004df5c /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #01 pc 0000000000052664 /apex/com.android.runtime/lib64/bionic/libc.so (__futex_wait_ex(void volatile*, bool, int, bool, timespec const*)+144) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #02 pc 00000000000b56cc /apex/com.android.runtime/lib64/bionic/libc.so (pthread_cond_wait+76) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #03 pc 00000000000699e0 /system/lib64/libc++.so (std::__1::condition_variable::wait(std::__1::unique_lock&)+20) (BuildId: 6ae0290e5bfb8abb216bde2a4ee48d9e) + native: #04 pc 0000000000250af8 /system/lib64/libhwui.so (android::uirenderer::CommonPool::workerLoop()+96) (BuildId: 5e787210ce0f171dbee073e4a14a376c) + native: #05 pc 0000000000250d5c /system/lib64/libhwui.so (android::uirenderer::CommonPool::CommonPool()::$_0::operator()() const (.__uniq.99815402873434996937524029735804459536)+188) (BuildId: 5e787210ce0f171dbee073e4a14a376c) + native: #06 pc 0000000000250c9c /system/lib64/libhwui.so (void* std::__1::__thread_proxy >, android::uirenderer::CommonPool::CommonPool()::$_0> >(void*) (.__uniq.99815402873434996937524029735804459536)+40) (BuildId: 5e787210ce0f171dbee073e4a14a376c) + native: #07 pc 00000000000b63b0 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+208) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #08 pc 00000000000530b8 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + (no managed stack frames) + +"Okio Watchdog" daemon prio=5 tid=33 Waiting + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x136c1338 self=0xb400007cabcb6d30 + | sysTid=29029 nice=0 cgrp=top-app sched=0/0 handle=0x7ab7967cb0 + | state=S schedstat=( 231164 396334 6 ) utm=0 stm=0 core=0 HZ=100 + | stack=0x7ab7864000-0x7ab7866000 stackSize=1039KB + | held mutexes= + at java.lang.Object.wait(Native method) + - waiting on <0x004b4344> (a java.lang.Class) + at java.lang.Object.wait(Object.java:442) + at java.lang.Object.wait(Object.java:568) + at com.android.okhttp.okio.AsyncTimeout.awaitTimeout(AsyncTimeout.java:313) + - locked <0x004b4344> (a java.lang.Class) + at com.android.okhttp.okio.AsyncTimeout.access$000(AsyncTimeout.java:42) + at com.android.okhttp.okio.AsyncTimeout$Watchdog.run(AsyncTimeout.java:288) + +"binder:28941_4" prio=5 tid=35 Native + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x136c13b0 self=0xb400007cabcb8900 + | sysTid=29039 nice=0 cgrp=top-app sched=0/0 handle=0x7ab06b5cb0 + | state=S schedstat=( 11108162 4295500 140 ) utm=1 stm=0 core=2 HZ=100 + | stack=0x7ab05be000-0x7ab05c0000 stackSize=991KB + | held mutexes= + native: #00 pc 00000000000a23d8 /apex/com.android.runtime/lib64/bionic/libc.so (__ioctl+8) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #01 pc 000000000005b50c /apex/com.android.runtime/lib64/bionic/libc.so (ioctl+156) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #02 pc 0000000000094690 /system/lib64/libbinder.so (android::IPCThreadState::joinThreadPool(bool)+316) (BuildId: ee18e52b95e38eaab55a9a48518c8c3b) + native: #03 pc 0000000000094540 /system/lib64/libbinder.so (android::PoolThread::threadLoop()+24) (BuildId: ee18e52b95e38eaab55a9a48518c8c3b) + native: #04 pc 00000000000148e8 /system/lib64/libutils.so (android::Thread::_threadLoop(void*)+528) (BuildId: 5a0d720732600c94ad8354a1188e9f52) + native: #05 pc 00000000000c8918 /system/lib64/libandroid_runtime.so (android::AndroidRuntime::javaThreadShell(void*)+140) (BuildId: a31474ac581b716d4588f8c97eb06009) + native: #06 pc 00000000000b63b0 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+208) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #07 pc 00000000000530b8 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + (no managed stack frames) + +"Thread-9" prio=5 tid=5 Sleeping + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x136c1600 self=0xb400007cabcae220 + | sysTid=29157 nice=0 cgrp=top-app sched=0/0 handle=0x7b21c88cb0 + | state=S schedstat=( 38083 149917 1 ) utm=0 stm=0 core=1 HZ=100 + | stack=0x7b21b85000-0x7b21b87000 stackSize=1039KB + | held mutexes= + at java.lang.Thread.sleep(Native method) + - sleeping on <0x09228c2d> (a java.lang.Object) + at java.lang.Thread.sleep(Thread.java:450) + - locked <0x09228c2d> (a java.lang.Object) + at java.lang.Thread.sleep(Thread.java:355) + at io.sentry.samples.android.MainActivity$1.run(MainActivity.java:162) + - locked <0x0d3a2f0a> (a java.lang.Object) + at java.lang.Thread.run(Thread.java:1012) + +"binder:28941_3" prio=5 (not attached) + | sysTid=29028 nice=0 cgrp=top-app + | state=S schedstat=( 3124378 30612789 84 ) utm=0 stm=0 core=0 HZ=100 + native: #00 pc 000000000004df5c /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #01 pc 0000000000052664 /apex/com.android.runtime/lib64/bionic/libc.so (__futex_wait_ex(void volatile*, bool, int, bool, timespec const*)+144) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #02 pc 00000000000b56cc /apex/com.android.runtime/lib64/bionic/libc.so (pthread_cond_wait+76) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #03 pc 00000000000699e0 /system/lib64/libc++.so (std::__1::condition_variable::wait(std::__1::unique_lock&)+20) (BuildId: 6ae0290e5bfb8abb216bde2a4ee48d9e) + native: #04 pc 00000000000a048c /system/lib64/libgui.so (android::AsyncWorker::run()+112) (BuildId: 383a37b5342fd0249afb25e7134deb33) + native: #05 pc 00000000000a0878 /system/lib64/libgui.so (void* std::__1::__thread_proxy >, void (android::AsyncWorker::*)(), android::AsyncWorker*> >(void*)+80) (BuildId: 383a37b5342fd0249afb25e7134deb33) + native: #06 pc 00000000000b63b0 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+208) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + native: #07 pc 00000000000530b8 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) + +----- end 28941 ----- + +----- Waiting Channels: pid 28941 at 2023-04-04 22:06:31.057056350+0200 ----- +Cmd line: io.sentry.samples.android + +sysTid=28941 futex_wait_queue_me +sysTid=28957 do_sigtimedwait +sysTid=28959 pipe_read +sysTid=28960 do_sys_poll +sysTid=28961 futex_wait_queue_me +sysTid=28962 futex_wait_queue_me +sysTid=28963 futex_wait_queue_me +sysTid=28964 futex_wait_queue_me +sysTid=28965 futex_wait_queue_me +sysTid=28966 binder_wait_for_work +sysTid=28967 binder_wait_for_work +sysTid=28975 binder_wait_for_work +sysTid=28980 futex_wait_queue_me +sysTid=28991 do_epoll_wait +sysTid=28993 futex_wait_queue_me +sysTid=28994 futex_wait_queue_me +sysTid=28995 inotify_read +sysTid=28996 futex_wait_queue_me +sysTid=28997 do_epoll_wait +sysTid=29000 do_epoll_wait +sysTid=29001 futex_wait_queue_me +sysTid=29004 do_epoll_wait +sysTid=29010 futex_wait_queue_me +sysTid=29011 do_epoll_wait +sysTid=29026 futex_wait_queue_me +sysTid=29027 futex_wait_queue_me +sysTid=29028 futex_wait_queue_me +sysTid=29029 futex_wait_queue_me +sysTid=29039 binder_wait_for_work +sysTid=29157 futex_wait_queue_me + +----- end 28941 ----- diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index d10f74b40b..a94b9a32fa 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -1498,6 +1498,7 @@ public final class io/sentry/SentryEvent$JsonKeys { public final class io/sentry/SentryExceptionFactory { public fun (Lio/sentry/SentryStackTraceFactory;)V public fun getSentryExceptions (Ljava/lang/Throwable;)Ljava/util/List; + public fun getSentryExceptionsFromThread (Lio/sentry/protocol/SentryThread;Lio/sentry/protocol/Mechanism;Ljava/lang/Throwable;)Ljava/util/List; } public final class io/sentry/SentryInstantDate : io/sentry/SentryDate { @@ -1550,6 +1551,46 @@ public final class io/sentry/SentryLevel : java/lang/Enum, io/sentry/JsonSeriali public static fun values ()[Lio/sentry/SentryLevel; } +public final class io/sentry/SentryLockReason : io/sentry/JsonSerializable, io/sentry/JsonUnknown { + public static final field ANY I + public static final field BLOCKED I + public static final field LOCKED I + public static final field SLEEPING I + public static final field WAITING I + public fun ()V + public fun (Lio/sentry/SentryLockReason;)V + public fun equals (Ljava/lang/Object;)Z + public fun getAddress ()Ljava/lang/String; + public fun getClassName ()Ljava/lang/String; + public fun getPackageName ()Ljava/lang/String; + public fun getThreadId ()Ljava/lang/Long; + public fun getType ()I + public fun getUnknown ()Ljava/util/Map; + public fun hashCode ()I + public fun serialize (Lio/sentry/JsonObjectWriter;Lio/sentry/ILogger;)V + public fun setAddress (Ljava/lang/String;)V + public fun setClassName (Ljava/lang/String;)V + public fun setPackageName (Ljava/lang/String;)V + public fun setThreadId (Ljava/lang/Long;)V + public fun setType (I)V + public fun setUnknown (Ljava/util/Map;)V +} + +public final class io/sentry/SentryLockReason$Deserializer : io/sentry/JsonDeserializer { + public fun ()V + public fun deserialize (Lio/sentry/JsonObjectReader;Lio/sentry/ILogger;)Lio/sentry/SentryLockReason; + public synthetic fun deserialize (Lio/sentry/JsonObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; +} + +public final class io/sentry/SentryLockReason$JsonKeys { + public static final field ADDRESS Ljava/lang/String; + public static final field CLASS_NAME Ljava/lang/String; + public static final field PACKAGE_NAME Ljava/lang/String; + public static final field THREAD_ID Ljava/lang/String; + public static final field TYPE Ljava/lang/String; + public fun ()V +} + public final class io/sentry/SentryLongDate : io/sentry/SentryDate { public fun (J)V public fun nanoTimestamp ()J @@ -1800,9 +1841,10 @@ public final class io/sentry/SentrySpanStorage { } public final class io/sentry/SentryStackTraceFactory { - public fun (Ljava/util/List;Ljava/util/List;)V + public fun (Lio/sentry/SentryOptions;)V public fun getInAppCallStack ()Ljava/util/List; public fun getStackFrames ([Ljava/lang/StackTraceElement;)Ljava/util/List; + public fun isInApp (Ljava/lang/String;)Ljava/lang/Boolean; } public final class io/sentry/SentryThreadFactory { @@ -3516,6 +3558,7 @@ public final class io/sentry/protocol/SentryStackFrame : io/sentry/JsonSerializa public fun getPostContext ()Ljava/util/List; public fun getPreContext ()Ljava/util/List; public fun getRawFunction ()Ljava/lang/String; + public fun getSymbol ()Ljava/lang/String; public fun getSymbolAddr ()Ljava/lang/String; public fun getUnknown ()Ljava/util/Map; public fun getVars ()Ljava/util/Map; @@ -3539,6 +3582,7 @@ public final class io/sentry/protocol/SentryStackFrame : io/sentry/JsonSerializa public fun setPostContext (Ljava/util/List;)V public fun setPreContext (Ljava/util/List;)V public fun setRawFunction (Ljava/lang/String;)V + public fun setSymbol (Ljava/lang/String;)V public fun setSymbolAddr (Ljava/lang/String;)V public fun setUnknown (Ljava/util/Map;)V public fun setVars (Ljava/util/Map;)V @@ -3565,6 +3609,7 @@ public final class io/sentry/protocol/SentryStackFrame$JsonKeys { public static final field PACKAGE Ljava/lang/String; public static final field PLATFORM Ljava/lang/String; public static final field RAW_FUNCTION Ljava/lang/String; + public static final field SYMBOL Ljava/lang/String; public static final field SYMBOL_ADDR Ljava/lang/String; public fun ()V } @@ -3598,6 +3643,7 @@ public final class io/sentry/protocol/SentryStackTrace$JsonKeys { public final class io/sentry/protocol/SentryThread : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun ()V + public fun getHeldLocks ()Ljava/util/Map; public fun getId ()Ljava/lang/Long; public fun getName ()Ljava/lang/String; public fun getPriority ()Ljava/lang/Integer; @@ -3612,6 +3658,7 @@ public final class io/sentry/protocol/SentryThread : io/sentry/JsonSerializable, public fun setCrashed (Ljava/lang/Boolean;)V public fun setCurrent (Ljava/lang/Boolean;)V public fun setDaemon (Ljava/lang/Boolean;)V + public fun setHeldLocks (Ljava/util/Map;)V public fun setId (Ljava/lang/Long;)V public fun setMain (Ljava/lang/Boolean;)V public fun setName (Ljava/lang/String;)V @@ -3631,6 +3678,7 @@ public final class io/sentry/protocol/SentryThread$JsonKeys { public static final field CRASHED Ljava/lang/String; public static final field CURRENT Ljava/lang/String; public static final field DAEMON Ljava/lang/String; + public static final field HELD_LOCKS Ljava/lang/String; public static final field ID Ljava/lang/String; public static final field MAIN Ljava/lang/String; public static final field NAME Ljava/lang/String; diff --git a/sentry/src/main/java/io/sentry/JsonSerializer.java b/sentry/src/main/java/io/sentry/JsonSerializer.java index e78ec2265d..8018211d56 100644 --- a/sentry/src/main/java/io/sentry/JsonSerializer.java +++ b/sentry/src/main/java/io/sentry/JsonSerializer.java @@ -98,6 +98,7 @@ public JsonSerializer(@NotNull SentryOptions options) { deserializersByClass.put(SentryException.class, new SentryException.Deserializer()); deserializersByClass.put(SentryItemType.class, new SentryItemType.Deserializer()); deserializersByClass.put(SentryLevel.class, new SentryLevel.Deserializer()); + deserializersByClass.put(SentryLockReason.class, new SentryLockReason.Deserializer()); deserializersByClass.put(SentryPackage.class, new SentryPackage.Deserializer()); deserializersByClass.put(SentryRuntime.class, new SentryRuntime.Deserializer()); deserializersByClass.put(SentrySpan.class, new SentrySpan.Deserializer()); diff --git a/sentry/src/main/java/io/sentry/MainEventProcessor.java b/sentry/src/main/java/io/sentry/MainEventProcessor.java index ad8fc83612..eaacc5fd64 100644 --- a/sentry/src/main/java/io/sentry/MainEventProcessor.java +++ b/sentry/src/main/java/io/sentry/MainEventProcessor.java @@ -32,8 +32,7 @@ public MainEventProcessor(final @NotNull SentryOptions options) { this.options = Objects.requireNonNull(options, "The SentryOptions is required."); final SentryStackTraceFactory sentryStackTraceFactory = - new SentryStackTraceFactory( - this.options.getInAppExcludes(), this.options.getInAppIncludes()); + new SentryStackTraceFactory(this.options); sentryExceptionFactory = new SentryExceptionFactory(sentryStackTraceFactory); sentryThreadFactory = new SentryThreadFactory(sentryStackTraceFactory, this.options); diff --git a/sentry/src/main/java/io/sentry/SentryExceptionFactory.java b/sentry/src/main/java/io/sentry/SentryExceptionFactory.java index 49be910044..f268a11bf6 100644 --- a/sentry/src/main/java/io/sentry/SentryExceptionFactory.java +++ b/sentry/src/main/java/io/sentry/SentryExceptionFactory.java @@ -5,6 +5,7 @@ import io.sentry.protocol.SentryException; import io.sentry.protocol.SentryStackFrame; import io.sentry.protocol.SentryStackTrace; +import io.sentry.protocol.SentryThread; import io.sentry.util.Objects; import java.util.ArrayDeque; import java.util.ArrayList; @@ -34,6 +35,22 @@ public SentryExceptionFactory(final @NotNull SentryStackTraceFactory sentryStack Objects.requireNonNull(sentryStackTraceFactory, "The SentryStackTraceFactory is required."); } + @NotNull + public List getSentryExceptionsFromThread( + final @NotNull SentryThread thread, + final @NotNull Mechanism mechanism, + final @NotNull Throwable throwable) { + final SentryStackTrace threadStacktrace = thread.getStacktrace(); + if (threadStacktrace == null) { + return new ArrayList<>(0); + } + final List exceptions = new ArrayList<>(1); + exceptions.add( + getSentryException( + throwable, mechanism, thread.getId(), threadStacktrace.getFrames(), true)); + return exceptions; + } + /** * Creates a new instance from the given {@code throwable}. * @@ -63,14 +80,17 @@ public List getSentryExceptions(final @NotNull Throwable throwa * @param throwable Java exception to send to Sentry. * @param exceptionMechanism The optional {@link Mechanism} of the {@code throwable}. Or null if * none exist. - * @param thread The optional {@link Thread} which the exception originated. Or null if not known. + * @param threadId The optional id of a {@link Thread} which the exception originated. Or null if + * not known. + * @param frames stack frames that should be assigned to the stacktrace of this exception. * @param snapshot if the captured {@link java.lang.Thread}'s stacktrace is a snapshot, See {@link * SentryStackTrace#getSnapshot()} */ private @NotNull SentryException getSentryException( @NotNull final Throwable throwable, @Nullable final Mechanism exceptionMechanism, - @Nullable final Thread thread, + @Nullable final Long threadId, + @Nullable final List frames, final boolean snapshot) { final Package exceptionPackage = throwable.getClass().getPackage(); @@ -88,8 +108,6 @@ public List getSentryExceptions(final @NotNull Throwable throwa final String exceptionPackageName = exceptionPackage != null ? exceptionPackage.getName() : null; - final List frames = - sentryStackTraceFactory.getStackFrames(throwable.getStackTrace()); if (frames != null && !frames.isEmpty()) { final SentryStackTrace sentryStackTrace = new SentryStackTrace(frames); if (snapshot) { @@ -98,9 +116,7 @@ public List getSentryExceptions(final @NotNull Throwable throwa exception.setStacktrace(sentryStackTrace); } - if (thread != null) { - exception.setThreadId(thread.getId()); - } + exception.setThreadId(threadId); exception.setType(exceptionClassName); exception.setMechanism(exceptionMechanism); exception.setModule(exceptionPackageName); @@ -143,8 +159,11 @@ Deque extractExceptionQueue(final @NotNull Throwable throwable) thread = Thread.currentThread(); } + final List frames = + sentryStackTraceFactory.getStackFrames(currentThrowable.getStackTrace()); SentryException exception = - getSentryException(currentThrowable, exceptionMechanism, thread, snapshot); + getSentryException( + currentThrowable, exceptionMechanism, thread.getId(), frames, snapshot); exceptions.addFirst(exception); currentThrowable = currentThrowable.getCause(); } diff --git a/sentry/src/main/java/io/sentry/SentryLockReason.java b/sentry/src/main/java/io/sentry/SentryLockReason.java new file mode 100644 index 0000000000..607e461ee9 --- /dev/null +++ b/sentry/src/main/java/io/sentry/SentryLockReason.java @@ -0,0 +1,187 @@ +package io.sentry; + +import io.sentry.util.CollectionUtils; +import io.sentry.util.Objects; +import io.sentry.vendor.gson.stream.JsonToken; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** Represents an instance of a held lock (java monitor object) in a thread. */ +public final class SentryLockReason implements JsonUnknown, JsonSerializable { + + public static final int LOCKED = 1; + public static final int WAITING = 2; + public static final int SLEEPING = 4; + public static final int BLOCKED = 8; + + public static final int ANY = LOCKED | WAITING | SLEEPING | BLOCKED; + + private int type; + private @Nullable String address; + private @Nullable String packageName; + private @Nullable String className; + private @Nullable Long threadId; + private @Nullable Map unknown; + + public SentryLockReason() {} + + public SentryLockReason(final @NotNull SentryLockReason other) { + this.type = other.type; + this.address = other.address; + this.packageName = other.packageName; + this.className = other.className; + this.threadId = other.threadId; + this.unknown = CollectionUtils.newConcurrentHashMap(other.unknown); + } + + @SuppressWarnings("unused") + public int getType() { + return type; + } + + public void setType(final int type) { + this.type = type; + } + + @Nullable + public String getAddress() { + return address; + } + + public void setAddress(final @Nullable String address) { + this.address = address; + } + + @Nullable + public String getPackageName() { + return packageName; + } + + public void setPackageName(final @Nullable String packageName) { + this.packageName = packageName; + } + + @Nullable + public String getClassName() { + return className; + } + + public void setClassName(final @Nullable String className) { + this.className = className; + } + + @Nullable + public Long getThreadId() { + return threadId; + } + + public void setThreadId(final @Nullable Long threadId) { + this.threadId = threadId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SentryLockReason that = (SentryLockReason) o; + return Objects.equals(address, that.address); + } + + @Override + public int hashCode() { + return Objects.hash(address); + } + + // region json + + @Nullable + @Override + public Map getUnknown() { + return unknown; + } + + @Override + public void setUnknown(final @Nullable Map unknown) { + this.unknown = unknown; + } + + public static final class JsonKeys { + public static final String TYPE = "type"; + public static final String ADDRESS = "address"; + public static final String PACKAGE_NAME = "package_name"; + public static final String CLASS_NAME = "class_name"; + public static final String THREAD_ID = "thread_id"; + } + + @Override + public void serialize(@NotNull JsonObjectWriter writer, @NotNull ILogger logger) + throws IOException { + writer.beginObject(); + writer.name(JsonKeys.TYPE).value(type); + if (address != null) { + writer.name(JsonKeys.ADDRESS).value(address); + } + if (packageName != null) { + writer.name(JsonKeys.PACKAGE_NAME).value(packageName); + } + if (className != null) { + writer.name(JsonKeys.CLASS_NAME).value(className); + } + if (threadId != null) { + writer.name(JsonKeys.THREAD_ID).value(threadId); + } + if (unknown != null) { + for (String key : unknown.keySet()) { + Object value = unknown.get(key); + writer.name(key); + writer.value(logger, value); + } + } + writer.endObject(); + } + + public static final class Deserializer implements JsonDeserializer { + + @Override + public @NotNull SentryLockReason deserialize( + @NotNull JsonObjectReader reader, @NotNull ILogger logger) throws Exception { + final SentryLockReason sentryLockReason = new SentryLockReason(); + Map unknown = null; + reader.beginObject(); + while (reader.peek() == JsonToken.NAME) { + final String nextName = reader.nextName(); + switch (nextName) { + case JsonKeys.TYPE: + sentryLockReason.type = reader.nextInt(); + break; + case JsonKeys.ADDRESS: + sentryLockReason.address = reader.nextStringOrNull(); + break; + case JsonKeys.PACKAGE_NAME: + sentryLockReason.packageName = reader.nextStringOrNull(); + break; + case JsonKeys.CLASS_NAME: + sentryLockReason.className = reader.nextStringOrNull(); + break; + case JsonKeys.THREAD_ID: + sentryLockReason.threadId = reader.nextLongOrNull(); + break; + default: + if (unknown == null) { + unknown = new ConcurrentHashMap<>(); + } + reader.nextUnknown(logger, unknown, nextName); + break; + } + } + sentryLockReason.setUnknown(unknown); + reader.endObject(); + return sentryLockReason; + } + } + + // endregion +} diff --git a/sentry/src/main/java/io/sentry/SentryStackTraceFactory.java b/sentry/src/main/java/io/sentry/SentryStackTraceFactory.java index d804c0a4d8..9e63e133f3 100644 --- a/sentry/src/main/java/io/sentry/SentryStackTraceFactory.java +++ b/sentry/src/main/java/io/sentry/SentryStackTraceFactory.java @@ -8,22 +8,15 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.TestOnly; /** class responsible for converting Java StackTraceElements to SentryStackFrames */ @ApiStatus.Internal public final class SentryStackTraceFactory { - /** list of inApp excludes */ - private final @Nullable List inAppExcludes; + private final @NotNull SentryOptions options; - /** list of inApp includes */ - private final @Nullable List inAppIncludes; - - public SentryStackTraceFactory( - @Nullable final List inAppExcludes, @Nullable List inAppIncludes) { - this.inAppExcludes = inAppExcludes; - this.inAppIncludes = inAppIncludes; + public SentryStackTraceFactory(final @NotNull SentryOptions options) { + this.options = options; } /** @@ -76,27 +69,26 @@ public List getStackFrames(@Nullable final StackTraceElement[] * @param className the className * @return true if it is or false otherwise */ - @TestOnly @Nullable - Boolean isInApp(final @Nullable String className) { + public Boolean isInApp(final @Nullable String className) { if (className == null || className.isEmpty()) { return true; } - if (inAppIncludes != null) { - for (String include : inAppIncludes) { - if (className.startsWith(include)) { - return true; - } + final List inAppIncludes = options.getInAppIncludes(); + for (String include : inAppIncludes) { + if (className.startsWith(include)) { + return true; } } - if (inAppExcludes != null) { - for (String exclude : inAppExcludes) { - if (className.startsWith(exclude)) { - return false; - } + + final List inAppExcludes = options.getInAppExcludes(); + for (String exclude : inAppExcludes) { + if (className.startsWith(exclude)) { + return false; } } + return null; } diff --git a/sentry/src/main/java/io/sentry/instrumentation/file/FileIOSpanManager.java b/sentry/src/main/java/io/sentry/instrumentation/file/FileIOSpanManager.java index 0ace5d8b63..93127c20f2 100644 --- a/sentry/src/main/java/io/sentry/instrumentation/file/FileIOSpanManager.java +++ b/sentry/src/main/java/io/sentry/instrumentation/file/FileIOSpanManager.java @@ -39,8 +39,7 @@ final class FileIOSpanManager { this.currentSpan = currentSpan; this.file = file; this.options = options; - this.stackTraceFactory = - new SentryStackTraceFactory(options.getInAppExcludes(), options.getInAppIncludes()); + this.stackTraceFactory = new SentryStackTraceFactory(options); SentryIntegrationPackageStorage.getInstance().addIntegration("FileIO"); } diff --git a/sentry/src/main/java/io/sentry/protocol/SentryStackFrame.java b/sentry/src/main/java/io/sentry/protocol/SentryStackFrame.java index 832074f2a2..210c7726d6 100644 --- a/sentry/src/main/java/io/sentry/protocol/SentryStackFrame.java +++ b/sentry/src/main/java/io/sentry/protocol/SentryStackFrame.java @@ -93,6 +93,14 @@ public final class SentryStackFrame implements JsonUnknown, JsonSerializable { */ private @Nullable String instructionAddr; + /** + * Potentially mangled name of the symbol as it appears in an executable. + * + *

This is different from a function name by generally being the mangled name that appears + * natively in the binary. This is relevant for languages like Swift, C++ or Rust. + */ + private @Nullable String symbol; + @SuppressWarnings("unused") private @Nullable Map unknown; @@ -267,6 +275,15 @@ public void setRawFunction(final @Nullable String rawFunction) { this.rawFunction = rawFunction; } + @Nullable + public String getSymbol() { + return symbol; + } + + public void setSymbol(final @Nullable String symbol) { + this.symbol = symbol; + } + // region json @Nullable @@ -296,6 +313,7 @@ public static final class JsonKeys { public static final String SYMBOL_ADDR = "symbol_addr"; public static final String INSTRUCTION_ADDR = "instruction_addr"; public static final String RAW_FUNCTION = "raw_function"; + public static final String SYMBOL = "symbol"; } @Override @@ -347,6 +365,9 @@ public void serialize(@NotNull JsonObjectWriter writer, @NotNull ILogger logger) if (rawFunction != null) { writer.name(JsonKeys.RAW_FUNCTION).value(rawFunction); } + if (symbol != null) { + writer.name(JsonKeys.SYMBOL).value(symbol); + } if (unknown != null) { for (String key : unknown.keySet()) { Object value = unknown.get(key); @@ -412,6 +433,9 @@ public static final class Deserializer implements JsonDeserializer(); diff --git a/sentry/src/main/java/io/sentry/protocol/SentryThread.java b/sentry/src/main/java/io/sentry/protocol/SentryThread.java index 1c955064e3..b3e1c7c6a6 100644 --- a/sentry/src/main/java/io/sentry/protocol/SentryThread.java +++ b/sentry/src/main/java/io/sentry/protocol/SentryThread.java @@ -6,8 +6,10 @@ import io.sentry.JsonObjectWriter; import io.sentry.JsonSerializable; import io.sentry.JsonUnknown; +import io.sentry.SentryLockReason; import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.jetbrains.annotations.NotNull; @@ -38,6 +40,8 @@ public final class SentryThread implements JsonUnknown, JsonSerializable { private @Nullable Boolean main; private @Nullable SentryStackTrace stacktrace; + private @Nullable Map heldLocks; + @SuppressWarnings("unused") private @Nullable Map unknown; @@ -208,6 +212,24 @@ public void setState(final @Nullable String state) { this.state = state; } + /** + * Gets locks held by this thread. + * + * @return locks held by this thread + */ + public @Nullable Map getHeldLocks() { + return heldLocks; + } + + /** + * Sets locks held by this thread. + * + * @param heldLocks list of locks held by this thread + */ + public void setHeldLocks(final @Nullable Map heldLocks) { + this.heldLocks = heldLocks; + } + // region json @Nullable @@ -231,6 +253,7 @@ public static final class JsonKeys { public static final String DAEMON = "daemon"; public static final String MAIN = "main"; public static final String STACKTRACE = "stacktrace"; + public static final String HELD_LOCKS = "held_locks"; } @Override @@ -264,6 +287,9 @@ public void serialize(@NotNull JsonObjectWriter writer, @NotNull ILogger logger) if (stacktrace != null) { writer.name(JsonKeys.STACKTRACE).value(logger, stacktrace); } + if (heldLocks != null) { + writer.name(JsonKeys.HELD_LOCKS).value(logger, heldLocks); + } if (unknown != null) { for (String key : unknown.keySet()) { Object value = unknown.get(key); @@ -313,6 +339,13 @@ public static final class Deserializer implements JsonDeserializer sentryThread.stacktrace = reader.nextOrNull(logger, new SentryStackTrace.Deserializer()); break; + case JsonKeys.HELD_LOCKS: + final Map heldLocks = + reader.nextMapOrNull(logger, new SentryLockReason.Deserializer()); + if (heldLocks != null) { + sentryThread.heldLocks = new HashMap<>(heldLocks); + } + break; default: if (unknown == null) { unknown = new ConcurrentHashMap<>(); diff --git a/sentry/src/test/java/io/sentry/JsonUnknownSerializationTest.kt b/sentry/src/test/java/io/sentry/JsonUnknownSerializationTest.kt index f24ce46766..2396275e61 100644 --- a/sentry/src/test/java/io/sentry/JsonUnknownSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/JsonUnknownSerializationTest.kt @@ -51,7 +51,7 @@ class JsonUnknownSerializationTest( companion object { @JvmStatic - @Parameterized.Parameters + @Parameterized.Parameters(name = "{0}") fun data(): Collection> { val app = givenJsonUnknown(App()) val breadcrumb = givenJsonUnknown(Breadcrumb()) @@ -75,6 +75,7 @@ class JsonUnknownSerializationTest( val sentryStackFrame = givenJsonUnknown(SentryStackFrame()) val sentryStackTrace = givenJsonUnknown(SentryStackTrace()) val sentryThread = givenJsonUnknown(SentryThread()) + val sentryLockReason = givenJsonUnknown(SentryLockReason()) val sentryTransaction = givenJsonUnknown(SentryTransactionSerializationTest.Fixture().getSut()) val session = givenJsonUnknown(SessionSerializationTest.Fixture().getSut()) val skdVersion = givenJsonUnknown(SdkVersion("3e934135-3f2b-49bc-8756-9f025b55143e", "3e31738e-4106-42d0-8be2-4a3a1bc648d3")) @@ -110,6 +111,7 @@ class JsonUnknownSerializationTest( arrayOf(sentryStackFrame, sentryStackFrame, SentryStackFrame.Deserializer()::deserialize), arrayOf(sentryStackTrace, sentryStackTrace, SentryStackTrace.Deserializer()::deserialize), arrayOf(sentryThread, sentryThread, SentryThread.Deserializer()::deserialize), + arrayOf(sentryLockReason, sentryLockReason, SentryLockReason.Deserializer()::deserialize), arrayOf(sentryTransaction, sentryTransaction, SentryTransaction.Deserializer()::deserialize), arrayOf(session, session, Session.Deserializer()::deserialize), arrayOf(skdVersion, skdVersion, SdkVersion.Deserializer()::deserialize), diff --git a/sentry/src/test/java/io/sentry/SentryExceptionFactoryTest.kt b/sentry/src/test/java/io/sentry/SentryExceptionFactoryTest.kt index 16120ce10d..c4794afd39 100644 --- a/sentry/src/test/java/io/sentry/SentryExceptionFactoryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryExceptionFactoryTest.kt @@ -2,6 +2,9 @@ package io.sentry import io.sentry.exception.ExceptionMechanismException import io.sentry.protocol.Mechanism +import io.sentry.protocol.SentryStackFrame +import io.sentry.protocol.SentryStackTrace +import io.sentry.protocol.SentryThread import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -15,7 +18,11 @@ import kotlin.test.assertTrue class SentryExceptionFactoryTest { private class Fixture { - fun getSut(stackTraceFactory: SentryStackTraceFactory = SentryStackTraceFactory(listOf("io.sentry"), listOf())): SentryExceptionFactory { + fun getSut( + stackTraceFactory: SentryStackTraceFactory = SentryStackTraceFactory( + SentryOptions().apply { addInAppExclude("io.sentry") } + ) + ): SentryExceptionFactory { return SentryExceptionFactory(stackTraceFactory) } } @@ -88,7 +95,8 @@ class SentryExceptionFactoryTest { fun `When ExceptionMechanismException has threads snapshot, stack trace should set snapshot flag`() { val error = Exception("Exception") - val throwable = ExceptionMechanismException(Mechanism(), error, Thread.currentThread(), true) + val throwable = + ExceptionMechanismException(Mechanism(), error, Thread.currentThread(), true) val sentryExceptions = fixture.getSut().getSentryExceptions(throwable) assertTrue(sentryExceptions[0].stacktrace?.snapshot!!) @@ -138,6 +146,49 @@ class SentryExceptionFactoryTest { assertEquals(thread.id, queue.first.threadId) } + @Test + fun `returns empty list if stacktrace is not available for SentryThread`() { + val thread = SentryThread() + val mechanism = Mechanism() + val throwable = Exception("msg") + + val exceptions = fixture.getSut().getSentryExceptionsFromThread(thread, mechanism, throwable) + + assertTrue(exceptions.isEmpty()) + } + + @Test + fun `returns proper exception backfilled from SentryThread`() { + val thread = SentryThread().apply { + id = 121 + stacktrace = SentryStackTrace().apply { + frames = listOf( + SentryStackFrame().apply { + lineno = 777 + module = "io.sentry.samples.MainActivity" + function = "run" + } + ) + } + } + val mechanism = Mechanism().apply { type = "AppExitInfo" } + val throwable = Exception("msg") + + val exceptions = fixture.getSut().getSentryExceptionsFromThread(thread, mechanism, throwable) + + val exception = exceptions.first() + assertEquals("AppExitInfo", exception.mechanism!!.type) + assertEquals("java.lang", exception.module) + assertEquals("Exception", exception.type) + assertEquals("msg", exception.value) + assertEquals(121, exception.threadId) + assertEquals(true, exception.stacktrace!!.snapshot) + val frame = exception.stacktrace!!.frames!!.first() + assertEquals("io.sentry.samples.MainActivity", frame.module) + assertEquals("run", frame.function) + assertEquals(777, frame.lineno) + } + internal class InnerClassThrowable constructor(cause: Throwable? = null) : Throwable(cause) private val anonymousException = object : Exception() { diff --git a/sentry/src/test/java/io/sentry/SentryStackTraceFactoryTest.kt b/sentry/src/test/java/io/sentry/SentryStackTraceFactoryTest.kt index 57fd6f05af..8a3a90e6d3 100644 --- a/sentry/src/test/java/io/sentry/SentryStackTraceFactoryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryStackTraceFactoryTest.kt @@ -9,7 +9,7 @@ import kotlin.test.assertNull import kotlin.test.assertTrue class SentryStackTraceFactoryTest { - private val sut = SentryStackTraceFactory(listOf(), listOf()) + private val sut = SentryStackTraceFactory(SentryOptions()) @Test fun `when getStackFrames is called passing a valid Array, not empty result`() { @@ -60,7 +60,9 @@ class SentryStackTraceFactoryTest { fun `when getStackFrames is called passing a valid inAppExcludes, inApp should be false if prefix matches it`() { val element = generateStackTrace("io.mysentry.MyActivity") val elements = arrayOf(element) - val sentryStackTraceFactory = SentryStackTraceFactory(listOf("io.mysentry"), null) + val sentryStackTraceFactory = SentryStackTraceFactory( + SentryOptions().apply { addInAppExclude("io.mysentry") } + ) val sentryElements = sentryStackTraceFactory.getStackFrames(elements) assertFalse(sentryElements!!.first().isInApp!!) @@ -70,7 +72,10 @@ class SentryStackTraceFactoryTest { fun `when getStackFrames is called passing a valid inAppExcludes, inApp should be undecided if prefix doesnt matches it`() { val element = generateStackTrace("io.myapp.MyActivity") val elements = arrayOf(element) - val sentryStackTraceFactory = SentryStackTraceFactory(listOf("io.mysentry"), null) + val sentryStackTraceFactory = SentryStackTraceFactory( + SentryOptions().apply { addInAppExclude("io.mysentry") } + + ) val sentryElements = sentryStackTraceFactory.getStackFrames(elements) assertNull(sentryElements!!.first().isInApp) @@ -80,7 +85,7 @@ class SentryStackTraceFactoryTest { fun `when getStackFrames is called passing an invalid inAppExcludes, inApp should undecided`() { val element = generateStackTrace("io.mysentry.MyActivity") val elements = arrayOf(element) - val sentryStackTraceFactory = SentryStackTraceFactory(null, null) + val sentryStackTraceFactory = SentryStackTraceFactory(SentryOptions()) val sentryElements = sentryStackTraceFactory.getStackFrames(elements) assertNull(sentryElements!!.first().isInApp) @@ -92,7 +97,9 @@ class SentryStackTraceFactoryTest { fun `when getStackFrames is called passing a valid inAppIncludes, inApp should be true if prefix matches it`() { val element = generateStackTrace("io.mysentry.MyActivity") val elements = arrayOf(element) - val sentryStackTraceFactory = SentryStackTraceFactory(null, listOf("io.mysentry")) + val sentryStackTraceFactory = SentryStackTraceFactory( + SentryOptions().apply { addInAppInclude("io.mysentry") } + ) val sentryElements = sentryStackTraceFactory.getStackFrames(elements) assertTrue(sentryElements!!.first().isInApp!!) @@ -102,7 +109,9 @@ class SentryStackTraceFactoryTest { fun `when getStackFrames is called passing a valid inAppIncludes, inApp should be undecided if prefix doesnt matches it`() { val element = generateStackTrace("io.myapp.MyActivity") val elements = arrayOf(element) - val sentryStackTraceFactory = SentryStackTraceFactory(null, listOf("io.mysentry")) + val sentryStackTraceFactory = SentryStackTraceFactory( + SentryOptions().apply { addInAppInclude("io.mysentry") } + ) val sentryElements = sentryStackTraceFactory.getStackFrames(elements) assertNull(sentryElements!!.first().isInApp) @@ -112,7 +121,7 @@ class SentryStackTraceFactoryTest { fun `when getStackFrames is called passing an invalid inAppIncludes, inApp should be undecided`() { val element = generateStackTrace("io.mysentry.MyActivity") val elements = arrayOf(element) - val sentryStackTraceFactory = SentryStackTraceFactory(null, null) + val sentryStackTraceFactory = SentryStackTraceFactory(SentryOptions()) val sentryElements = sentryStackTraceFactory.getStackFrames(elements) assertNull(sentryElements!!.first().isInApp) @@ -123,7 +132,12 @@ class SentryStackTraceFactoryTest { fun `when getStackFrames is called passing a valid inAppIncludes and inAppExcludes, inApp should take precedence`() { val element = generateStackTrace("io.mysentry.MyActivity") val elements = arrayOf(element) - val sentryStackTraceFactory = SentryStackTraceFactory(listOf("io.mysentry"), listOf("io.mysentry")) + val sentryStackTraceFactory = SentryStackTraceFactory( + SentryOptions().apply { + addInAppExclude("io.mysentry") + addInAppInclude("io.mysentry") + } + ) val sentryElements = sentryStackTraceFactory.getStackFrames(elements) assertTrue(sentryElements!!.first().isInApp!!) @@ -131,7 +145,13 @@ class SentryStackTraceFactoryTest { @Test fun `when class is defined in the app, inApp is true`() { - val sentryStackTraceFactory = SentryStackTraceFactory(listOf("io.mysentry.not"), listOf("io.mysentry.inApp")) + val sentryStackTraceFactory = + SentryStackTraceFactory( + SentryOptions().apply { + addInAppExclude("io.mysentry.not") + addInAppInclude("io.mysentry.inApp") + } + ) assertTrue(sentryStackTraceFactory.isInApp("io.mysentry.inApp.ClassName")!!) assertTrue(sentryStackTraceFactory.isInApp("io.mysentry.inApp.somePackage.ClassName")!!) assertFalse(sentryStackTraceFactory.isInApp("io.mysentry.not.ClassName")!!) @@ -140,7 +160,9 @@ class SentryStackTraceFactoryTest { @Test fun `when class is not in the list, is left undecided`() { - val sentryStackTraceFactory = SentryStackTraceFactory(listOf(), listOf("io.mysentry")) + val sentryStackTraceFactory = SentryStackTraceFactory( + SentryOptions().apply { addInAppInclude("io.mysentry") } + ) assertNull(sentryStackTraceFactory.isInApp("com.getsentry")) } @@ -187,7 +209,9 @@ class SentryStackTraceFactoryTest { fun `when stacktrace is not available, returns empty list for call stack`() { val exception = Exception() exception.stackTrace = arrayOf() - val sut = SentryStackTraceFactory(listOf(), listOf("io.mysentry")) + val sut = SentryStackTraceFactory( + SentryOptions().apply { addInAppInclude("io.mysentry") } + ) val callStack = sut.getInAppCallStack(exception) @@ -202,7 +226,9 @@ class SentryStackTraceFactoryTest { generateStackTrace("io.sentry.instrumentation.file.SentryFileOutputStream"), generateStackTrace("com.example.myapp.MainActivity") ) - val sut = SentryStackTraceFactory(listOf(), listOf("io.mysentry")) + val sut = SentryStackTraceFactory( + SentryOptions().apply { addInAppInclude("io.mysentry") } + ) val callStack = sut.getInAppCallStack(exception) @@ -219,7 +245,9 @@ class SentryStackTraceFactoryTest { generateStackTrace("com.example.myapp.MainActivity"), generateStackTrace("com.thirdparty.Adapter") ) - val sut = SentryStackTraceFactory(listOf(), listOf("com.example")) + val sut = SentryStackTraceFactory( + SentryOptions().apply { addInAppInclude("com.example") } + ) val callStack = sut.getInAppCallStack(exception) @@ -238,7 +266,7 @@ class SentryStackTraceFactoryTest { generateStackTrace("sun.misc.unsafe.park.Object"), generateStackTrace("java.lang.Object") ) - val sut = SentryStackTraceFactory(listOf(), listOf()) + val sut = SentryStackTraceFactory(SentryOptions()) val callStack = sut.getInAppCallStack(exception) diff --git a/sentry/src/test/java/io/sentry/SentryThreadFactoryTest.kt b/sentry/src/test/java/io/sentry/SentryThreadFactoryTest.kt index 49065708c0..bc8276ef05 100644 --- a/sentry/src/test/java/io/sentry/SentryThreadFactoryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryThreadFactoryTest.kt @@ -12,7 +12,7 @@ class SentryThreadFactoryTest { class Fixture { internal fun getSut(attachStacktrace: Boolean = true) = SentryThreadFactory( - SentryStackTraceFactory(listOf("io.sentry"), listOf()), + SentryStackTraceFactory(SentryOptions().apply { addInAppExclude("io.sentry") }), with(SentryOptions()) { isAttachStacktrace = attachStacktrace this diff --git a/sentry/src/test/java/io/sentry/protocol/SentryLockReasonSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/SentryLockReasonSerializationTest.kt new file mode 100644 index 0000000000..380f471f00 --- /dev/null +++ b/sentry/src/test/java/io/sentry/protocol/SentryLockReasonSerializationTest.kt @@ -0,0 +1,44 @@ +package io.sentry.protocol + +import io.sentry.ILogger +import io.sentry.SentryLockReason +import org.junit.Test +import org.mockito.kotlin.mock +import kotlin.test.assertEquals + +class SentryLockReasonSerializationTest { + + class Fixture { + val logger = mock() + + fun getSut() = SentryLockReason().apply { + address = "0x0d3a2f0a" + type = SentryLockReason.BLOCKED + threadId = 11 + className = "Object" + packageName = "java.lang" + } + } + private val fixture = Fixture() + + @Test + fun serialize() { + val expected = SerializationUtils.sanitizedFile("json/sentry_lock_reason.json") + val actual = SerializationUtils.serializeToString(fixture.getSut(), fixture.logger) + + assertEquals(expected, actual) + } + + @Test + fun deserialize() { + val expectedJson = SerializationUtils.sanitizedFile("json/sentry_lock_reason.json") + val actual = SerializationUtils.deserializeJson( + expectedJson, + SentryLockReason.Deserializer(), + fixture.logger + ) + val actualJson = SerializationUtils.serializeToString(actual, fixture.logger) + + assertEquals(expectedJson, actualJson) + } +} diff --git a/sentry/src/test/java/io/sentry/protocol/SentryStackFrameSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/SentryStackFrameSerializationTest.kt index 1f9d74321d..d57b15e0a6 100644 --- a/sentry/src/test/java/io/sentry/protocol/SentryStackFrameSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/SentryStackFrameSerializationTest.kt @@ -32,6 +32,7 @@ class SentryStackFrameSerializationTest { symbolAddr = "180e12cd-1fa8-405d-8dd8-e87b33fa2eb0" instructionAddr = "19864a78-2466-461f-9f0b-93a5c9ae7622" rawFunction = "f33035a4-0cf0-453d-b6f4-d7c27e9af924" + symbol = "d9807ffe-d517-11ed-afa1-0242ac120002" } } private val fixture = Fixture() diff --git a/sentry/src/test/java/io/sentry/protocol/SentryThreadSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/SentryThreadSerializationTest.kt index c3c5597ae3..b1ba0af79c 100644 --- a/sentry/src/test/java/io/sentry/protocol/SentryThreadSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/SentryThreadSerializationTest.kt @@ -5,6 +5,7 @@ import io.sentry.ILogger import io.sentry.JsonObjectReader import io.sentry.JsonObjectWriter import io.sentry.JsonSerializable +import io.sentry.SentryLockReason import org.junit.Test import org.mockito.kotlin.mock import java.io.StringReader @@ -50,6 +51,15 @@ class SentryThreadSerializationTest { "e7a3db0b-8cad-4eab-8315-03d5dc2edcd9" to "8bba0819-ac58-4e5c-bec7-32e1033b7bdf" ) snapshot = true + heldLocks = mapOf( + "0x0d3a2f0a" to SentryLockReason().apply { + address = "0x0d3a2f0a" + className = "Object" + packageName = "java.lang" + type = SentryLockReason.BLOCKED + threadId = 11 + } + ) } } } diff --git a/sentry/src/test/resources/json/sentry_event.json b/sentry/src/test/resources/json/sentry_event.json index 55b057504b..3e1cc83322 100644 --- a/sentry/src/test/resources/json/sentry_event.json +++ b/sentry/src/test/resources/json/sentry_event.json @@ -52,6 +52,17 @@ "e7a3db0b-8cad-4eab-8315-03d5dc2edcd9": "8bba0819-ac58-4e5c-bec7-32e1033b7bdf" }, "snapshot": true + }, + "held_locks": + { + "0x0d3a2f0a": + { + "type": 8, + "address": "0x0d3a2f0a", + "package_name": "java.lang", + "class_name": "Object", + "thread_id": 11 + } } } ] diff --git a/sentry/src/test/resources/json/sentry_lock_reason.json b/sentry/src/test/resources/json/sentry_lock_reason.json new file mode 100644 index 0000000000..49fd6da553 --- /dev/null +++ b/sentry/src/test/resources/json/sentry_lock_reason.json @@ -0,0 +1,7 @@ +{ + "type": 8, + "address": "0x0d3a2f0a", + "package_name": "java.lang", + "class_name": "Object", + "thread_id": 11 +} diff --git a/sentry/src/test/resources/json/sentry_stack_frame.json b/sentry/src/test/resources/json/sentry_stack_frame.json index 39c8687f8d..1fd0182049 100644 --- a/sentry/src/test/resources/json/sentry_stack_frame.json +++ b/sentry/src/test/resources/json/sentry_stack_frame.json @@ -13,5 +13,6 @@ "image_addr": "27ec1be5-e8a1-485c-b020-f4d9f80a6624", "symbol_addr": "180e12cd-1fa8-405d-8dd8-e87b33fa2eb0", "instruction_addr": "19864a78-2466-461f-9f0b-93a5c9ae7622", - "raw_function": "f33035a4-0cf0-453d-b6f4-d7c27e9af924" + "raw_function": "f33035a4-0cf0-453d-b6f4-d7c27e9af924", + "symbol": "d9807ffe-d517-11ed-afa1-0242ac120002" } diff --git a/sentry/src/test/resources/json/sentry_thread.json b/sentry/src/test/resources/json/sentry_thread.json index da11f6f788..f2cb009849 100644 --- a/sentry/src/test/resources/json/sentry_thread.json +++ b/sentry/src/test/resources/json/sentry_thread.json @@ -35,5 +35,16 @@ "e7a3db0b-8cad-4eab-8315-03d5dc2edcd9": "8bba0819-ac58-4e5c-bec7-32e1033b7bdf" }, "snapshot": true + }, + "held_locks": + { + "0x0d3a2f0a": + { + "type": 8, + "address": "0x0d3a2f0a", + "package_name": "java.lang", + "class_name": "Object", + "thread_id": 11 + } } } From dfdeb795a47436ff2038f76f95ddcfd645be8ace Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 3 May 2023 22:29:50 +0200 Subject: [PATCH 20/23] Small tweaks --- .../main/java/io/sentry/android/core/AnrV2Integration.java | 6 +++++- sentry/src/main/java/io/sentry/Sentry.java | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java index afbcfc6c0c..24ddf958aa 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java @@ -80,9 +80,13 @@ public void register(@NotNull IHub hub, @NotNull SentryOptions options) { } if (this.options.isAnrEnabled()) { - options + try { + options .getExecutorService() .submit(new AnrProcessor(context, hub, this.options, dateProvider)); + } catch (Throwable e) { + options.getLogger().log(SentryLevel.DEBUG, "Failed to start AnrProcessor.", e); + } options.getLogger().log(SentryLevel.DEBUG, "AnrV2Integration installed."); addIntegrationToSdkVersion(); } diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 2b7ddafb5e..b4e31dd773 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -247,7 +247,7 @@ private static void finalizePreviousSession( try { options.getExecutorService().submit(new PreviousSessionFinalizer(options, hub)); } catch (Throwable e) { - options.getLogger().log(SentryLevel.DEBUG, "Failed to notify options observers.", e); + options.getLogger().log(SentryLevel.DEBUG, "Failed to finalize previous session.", e); } } From d2dd64382ce8e4795fed07e38eb54e81fca83a56 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 3 May 2023 22:30:41 +0200 Subject: [PATCH 21/23] Spotless --- .../main/java/io/sentry/android/core/AnrV2Integration.java | 4 ++-- sentry-test-support/api/sentry-test-support.api | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java index 24ddf958aa..17dbff486e 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java @@ -82,8 +82,8 @@ public void register(@NotNull IHub hub, @NotNull SentryOptions options) { if (this.options.isAnrEnabled()) { try { options - .getExecutorService() - .submit(new AnrProcessor(context, hub, this.options, dateProvider)); + .getExecutorService() + .submit(new AnrProcessor(context, hub, this.options, dateProvider)); } catch (Throwable e) { options.getLogger().log(SentryLevel.DEBUG, "Failed to start AnrProcessor.", e); } diff --git a/sentry-test-support/api/sentry-test-support.api b/sentry-test-support/api/sentry-test-support.api index 1432f2414f..ef506c52c9 100644 --- a/sentry-test-support/api/sentry-test-support.api +++ b/sentry-test-support/api/sentry-test-support.api @@ -10,6 +10,7 @@ public final class io/sentry/SkipError : java/lang/Error { public final class io/sentry/test/ImmediateExecutorService : io/sentry/ISentryExecutorService { public fun ()V public fun close (J)V + public fun isClosed ()Z public fun schedule (Ljava/lang/Runnable;J)Ljava/util/concurrent/Future; public fun submit (Ljava/lang/Runnable;)Ljava/util/concurrent/Future; public fun submit (Ljava/util/concurrent/Callable;)Ljava/util/concurrent/Future; From ff24f81f59e3cd3747ae9ed6c8f88b187658d848 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 4 May 2023 15:51:11 +0200 Subject: [PATCH 22/23] Changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f38599a59e..b9226e83d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ ## Unreleased +### Features + +- New ANR detection based on [ApplicationExitInfo API](https://developer.android.com/reference/android/app/ApplicationExitInfo) + - This implementation completely replaces the old one (based on a watchdog) on devices running Android 11 and above: + - New implementation provides more precise ANR events/ANR rate detection as well as system thread dump information. The new implementation reports ANRs exactly as Google Play Console, without producing false positives or missing important background ANR events. + - However, despite producing many false positives, the old implementation is capable of better enriching ANR errors (which is not available with the new implementation), for example: + - Capturing screenshots at the time of ANR event; + - Capturing transactions and profiling data corresponding to the ANR event; + - Auxiliary information (such as current memory load) at the time of ANR event. + - If you would like us to provide support for the old approach working alongside the new one on Android 11 and above (e.g. for raising events for slow code on main thread), consider upvoting [this issue](https://github.com/getsentry/sentry-java/issues/2693). + - The old watchdog implementation will continue working for older API versions (Android < 11) + ### Fixes - Use `configureScope` instead of `withScope` in `Hub.close()`. This ensures that the main scope releases the in-memory data when closing a hub instance. ([#2688](https://github.com/getsentry/sentry-java/pull/2688)) From 893ecf879ebdb3d5ec698b6149e7afafac7b1c19 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 4 May 2023 16:01:30 +0200 Subject: [PATCH 23/23] Pr id --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9226e83d7..8ae55713ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features -- New ANR detection based on [ApplicationExitInfo API](https://developer.android.com/reference/android/app/ApplicationExitInfo) +- New ANR detection based on [ApplicationExitInfo API](https://developer.android.com/reference/android/app/ApplicationExitInfo) ([#2697](https://github.com/getsentry/sentry-java/pull/2697)) - This implementation completely replaces the old one (based on a watchdog) on devices running Android 11 and above: - New implementation provides more precise ANR events/ANR rate detection as well as system thread dump information. The new implementation reports ANRs exactly as Google Play Console, without producing false positives or missing important background ANR events. - However, despite producing many false positives, the old implementation is capable of better enriching ANR errors (which is not available with the new implementation), for example: