diff --git a/CHANGELOG.md b/CHANGELOG.md index f5eda56541..6af90577b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,18 @@ ## Unreleased -## Features +### Features - Add Screenshot and ViewHierarchy to integrations list ([#2698](https://github.com/getsentry/sentry-java/pull/2698)) +- 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: + - 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 diff --git a/sentry-android-core/.gitignore b/sentry-android-core/.gitignore index 25b845be8a..772ea5870f 100644 --- a/sentry-android-core/.gitignore +++ b/sentry-android-core/.gitignore @@ -1,3 +1,5 @@ INSTALLATION last_crash +.options-cache/ +.scope-cache/ /sentry diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index b705fb27db..ae31c5cb8a 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -55,6 +55,29 @@ 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/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 ()Ljava/lang/Long; +} + 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 @@ -297,9 +320,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;)Ljava/lang/Long; 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 1172ce21af..66d83cd971 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/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/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..7f338af54c --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java @@ -0,0 +1,620 @@ +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.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; +import java.util.Collections; +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); + + 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, unwrappedHint); + 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, unwrappedHint); + + 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, final @NotNull Object hint) { + setRelease(event); + setEnvironment(event); + setDist(event); + setDebugMeta(event); + setSdk(event); + setApp(event, hint); + setOptionsTags(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); + 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.WARNING, "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); + + if (proguardUuid != null) { + 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.WARNING, "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); + } + } + + @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) { + 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()); + } + + final @NotNull List cpuFrequencies = CpuInfoUtils.getInstance().readMaxFrequencies(); + if (!cpuFrequencies.isEmpty()) { + device.setProcessorFrequency(Collections.max(cpuFrequencies).doubleValue()); + device.setProcessorCount(cpuFrequencies.size()); + } + + 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..17dbff486e --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java @@ -0,0 +1,316 @@ +package io.sentry.android.core; + +import android.annotation.SuppressLint; +import android.app.ActivityManager; +import android.app.ApplicationExitInfo; +import android.content.Context; +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.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.hints.AbnormalExit; +import io.sentry.hints.Backfillable; +import io.sentry.hints.BlockingFlushHint; +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; +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()) { + 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(); + } + } + + @Override + public void close() throws IOException { + if (options != null) { + options.getLogger().log(SentryLevel.DEBUG, "AnrV2Integration removed."); + } + } + + static class AnrProcessor implements Runnable { + + private final @NotNull Context context; + private final @NotNull IHub hub; + private final @NotNull SentryAndroidOptions options; + private final long threshold; + + AnrProcessor( + final @NotNull Context context, + final @NotNull IHub hub, + final @NotNull SentryAndroidOptions options, + final @NotNull ICurrentDateProvider dateProvider) { + this.context = context; + 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 ActivityManager activityManager = + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + + final List applicationExitInfoList = + activityManager.getHistoricalProcessExitReasons(null, 0, 0); + if (applicationExitInfoList.size() == 0) { + options.getLogger().log(SentryLevel.DEBUG, "No records in historical exit reasons."); + return; + } + + final IEnvelopeCache cache = options.getEnvelopeDiskCache(); + if (cache instanceof EnvelopeCache) { + 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(); + } + } + + // 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 + 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 (lastReportedAnrTimestamp != null + && 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 @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 + // 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 (lastReportedAnr != null && 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 boolean isBackground = + exitInfo.getImportance() != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; + + final List threads = parseThreadDump(exitInfo, isBackground); + final AnrV2Hint anrHint = + new AnrV2Hint( + options.getFlushTimeoutMillis(), + options.getLogger(), + anrTimestamp, + shouldEnrich, + isBackground); + + final Hint hint = HintUtils.createWithTypeCheckHint(anrHint); + + final SentryEvent event = new SentryEvent(); + event.setThreads(threads); + 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 @Nullable List parseThreadDump( + final @NotNull ApplicationExitInfo exitInfo, final boolean isBackground) { + 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); + } + + return threads; + } + } + + @ApiStatus.Internal + public static final class AnrV2Hint extends BlockingFlushHint + implements Backfillable, AbnormalExit { + + 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 isBackgroundAnr) { + super(flushTimeoutMillis, logger); + this.timestamp = timestamp; + this.shouldEnrich = shouldEnrich; + this.isBackgroundAnr = isBackgroundAnr; + } + + @Override + public Long timestamp() { + return timestamp; + } + + @Override + 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/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 a372012029..d7023a4299 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; @@ -39,10 +36,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.Collections; import java.util.Date; @@ -110,7 +104,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); } @@ -118,7 +112,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); } @@ -258,7 +253,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 @@ -272,28 +267,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) { @@ -310,14 +283,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) { @@ -337,7 +310,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); @@ -390,7 +363,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)); @@ -425,15 +399,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) { @@ -458,46 +423,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). * @@ -745,20 +674,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"); @@ -811,56 +726,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. * @@ -882,42 +747,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..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 @@ -5,23 +5,31 @@ 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.Nullable; import org.jetbrains.annotations.TestOnly; @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 +53,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 +66,21 @@ public void store(@NotNull SentryEnvelope envelope, @NotNull Hint hint) { writeStartupCrashMarkerFile(); } } + + HintUtils.runIfHasType( + hint, + AnrV2Integration.AnrV2Hint.class, + (anrHint) -> { + final @Nullable Long timestamp = anrHint.timestamp(); + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Writing last reported ANR marker with timestamp %d", + timestamp); + + writeLastReportedAnrMarker(timestamp); + }); } @TestOnly @@ -74,7 +98,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 +115,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 +136,43 @@ public static boolean hasStartupCrashMarker(final @NotNull SentryOptions options } return false; } + + 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"); + + 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 content.equals("null") ? null : 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 null; + } + + 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"); + 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/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/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..a52aee527a --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt @@ -0,0 +1,541 @@ +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.AbnormalExit +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.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 +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 +import kotlin.test.assertTrue + +@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) + assertEquals(true, processed.contexts.app!!.inForeground) + // 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) + } + + @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, + 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 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 new file mode 100644 index 0000000000..4db0d425c4 --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt @@ -0,0 +1,489 @@ +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.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.hints.DiskFlushNotification +import io.sentry.hints.SessionStartHint +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.spy +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(), + sessionTrackingEnabled: Boolean = true + ): 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 + this.isEnableAutoSessionTracking = sessionTrackingEnabled + addInAppInclude("io.sentry.samples") + setEnvelopeDiskCache(EnvelopeCache.create(this)) + } + 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) + } + 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) + } + } + + 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) + + integration.register(fixture.hub, fixture.options) + + verify(fixture.hub, never()).captureEvent(any(), anyOrNull()) + } + + @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 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) + 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) + 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) + (hint as AnrV2Hint).shouldEnrich() + } + ) + } + + @Test + fun `when latest ANR has foreground importance, sets abnormal mechanism to anr_foreground`() { + 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( + any(), + argThat { + val hint = HintUtils.getSentrySdkHint(this) + (hint as AnrV2Hint).mechanism() == "anr_foreground" + } + ) + } + + @Test + fun `waits for ANR events to be flushed on disk`() { + val integration = fixture.getSut( + tmpDir, + lastReportedAnrTimestamp = oldTimestamp, + flushTimeoutMillis = 1000L + ) + fixture.addAppExitInfo(timestamp = newTimestamp) + + whenever(fixture.hub.captureEvent(any(), any())).thenAnswer { invocation -> + val hint = HintUtils.getSentrySdkHint(invocation.getArgument(1)) + as DiskFlushNotification + thread { + Thread.sleep(500L) + 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 waiting to flush ANR event to disk.") }, + 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 waiting to flush ANR event to disk.") }, + 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 + } + ) + } + + @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 = 1000L + ) + fixture.addAppExitInfo(timestamp = newTimestamp) + + thread { + Thread.sleep(500L) + 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() + ) + } + + @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-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..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 @@ -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,135 @@ 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, + 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, + 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 null upon reading`() { + fixture.getSut(tmpDir) + + val lastReportedAnr = AndroidEnvelopeCache.lastReportedAnr(fixture.options) + + assertEquals(null, 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-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-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-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..ef506c52c9 100644 --- a/sentry-test-support/api/sentry-test-support.api +++ b/sentry-test-support/api/sentry-test-support.api @@ -7,6 +7,15 @@ 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 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; +} + 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..3f48e530ce --- /dev/null +++ b/sentry-test-support/src/main/kotlin/io/sentry/test/Mocks.kt @@ -0,0 +1,19 @@ +// 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) {} + override fun isClosed(): Boolean = false +} diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 666fc6080b..769f64fc76 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 @@ -88,6 +91,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 static fun fromMap (Ljava/util/Map;Lio/sentry/SentryOptions;)Lio/sentry/Breadcrumb; public fun getCategory ()Ljava/lang/String; @@ -98,6 +102,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; @@ -495,13 +500,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 { @@ -540,6 +563,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 @@ -625,6 +649,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 } @@ -688,6 +713,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 @@ -1256,6 +1282,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; @@ -1454,6 +1481,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 } @@ -1477,6 +1505,12 @@ 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 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 { public fun ()V public fun (Ljava/time/Instant;)V @@ -1527,6 +1561,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 @@ -1556,6 +1630,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; @@ -1598,6 +1673,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; @@ -1607,6 +1683,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; @@ -1774,9 +1851,14 @@ 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 { + public fun (Lio/sentry/SentryStackTraceFactory;Lio/sentry/SentryOptions;)V } public final class io/sentry/SentryTraceHeader { @@ -1967,6 +2049,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; @@ -1978,6 +2061,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 @@ -2211,6 +2295,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 @@ -2245,14 +2333,19 @@ 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 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; 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 { @@ -2261,6 +2354,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; @@ -2388,11 +2527,22 @@ 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 { } +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 { } @@ -2432,6 +2582,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 @@ -2602,6 +2755,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; @@ -2612,6 +2766,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 @@ -2647,9 +2802,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 @@ -2768,6 +2925,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; @@ -2799,6 +2957,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; @@ -2930,6 +3089,7 @@ public final class io/sentry/protocol/Geo$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; @@ -2939,6 +3099,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 @@ -3069,12 +3230,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 @@ -3105,6 +3268,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; @@ -3116,6 +3280,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 @@ -3214,6 +3379,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; @@ -3221,6 +3387,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 @@ -3403,6 +3570,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; @@ -3426,6 +3594,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 @@ -3452,6 +3621,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 } @@ -3485,6 +3655,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; @@ -3499,6 +3670,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 @@ -3518,6 +3690,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; @@ -3595,6 +3768,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 static fun fromMap (Ljava/util/Map;Lio/sentry/SentryOptions;)Lio/sentry/protocol/User; public fun getData ()Ljava/util/Map; public fun getEmail ()Ljava/lang/String; @@ -3606,6 +3780,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 6d580d160c..de48d0d3d7 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; @@ -519,6 +520,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 0a7e30c45f..8018211d56 100644 --- a/sentry/src/main/java/io/sentry/JsonSerializer.java +++ b/sentry/src/main/java/io/sentry/JsonSerializer.java @@ -40,6 +40,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; @@ -97,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()); @@ -118,6 +120,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 { @@ -126,6 +153,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. } @@ -225,4 +254,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/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/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/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/PreviousSessionFinalizer.java b/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java new file mode 100644 index 0000000000..458c6532ba --- /dev/null +++ b/sentry/src/main/java/io/sentry/PreviousSessionFinalizer.java @@ -0,0 +1,152 @@ +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 io.sentry.cache.IEnvelopeCache; +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; + +/** + * 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; + } + + 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()) { + 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(); + + 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 { + Date timestamp = null; + 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."); + + 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); + } + // 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. + 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 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."); + } + } + } + + /** + * 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/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 c3eea1d9b1..b4e31dd773 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -232,6 +232,48 @@ private static synchronized void init( for (final Integration integration : options.getIntegrations()) { integration.register(HubAdapter.getInstance(), options); } + + notifyOptionsObservers(options); + + finalizePreviousSession(options, HubAdapter.getInstance()); + } + + @SuppressWarnings("FutureReturnValueIgnored") + 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 finalize previous session.", 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 + // 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 a6d6795050..7570611b58 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -3,7 +3,8 @@ 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.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); } } @@ -317,7 +318,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() @@ -461,9 +470,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..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; @@ -12,12 +13,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; @@ -32,13 +35,29 @@ 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}. * * @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)); } @@ -61,14 +80,17 @@ List getSentryExceptions(final @NotNull Throwable throwable) { * @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(); @@ -86,8 +108,6 @@ List getSentryExceptions(final @NotNull Throwable throwable) { 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) { @@ -96,9 +116,7 @@ List getSentryExceptions(final @NotNull Throwable throwable) { exception.setStacktrace(sentryStackTrace); } - if (thread != null) { - exception.setThreadId(thread.getId()); - } + exception.setThreadId(threadId); exception.setType(exceptionClassName); exception.setMechanism(exceptionMechanism); exception.setModule(exceptionPackageName); @@ -141,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/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index e19064f1da..830fa116e9 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -279,7 +279,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 @@ -1344,10 +1346,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/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/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..f5c531628c 100644 --- a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java +++ b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java @@ -16,7 +16,8 @@ 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.AbnormalExit; import io.sentry.hints.SessionEnd; import io.sentry.hints.SessionStart; import io.sentry.transport.NoOpEnvelopeCache; @@ -44,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; @@ -56,12 +59,16 @@ 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; public static final String STARTUP_CRASH_MARKER_FILE = "startup_crash"; + private final CountDownLatch previousSessionLatch; + private final @NotNull Map fileNameMap = new WeakHashMap<>(); public static @NotNull IEnvelopeCache create(final @NotNull SentryOptions options) { @@ -80,6 +87,7 @@ public EnvelopeCache( final @NotNull String cacheDirPath, final int maxCacheItems) { super(options, cacheDirPath, maxCacheItems); + previousSessionLatch = new CountDownLatch(1); } @Override @@ -88,7 +96,8 @@ public void store(final @NotNull SentryEnvelope envelope, final @NotNull Hint hi rotateCacheIfNeeded(allEnvelopeFiles()); - final File currentSessionFile = getCurrentSessionFile(); + final File currentSessionFile = getCurrentSessionFile(directory.getAbsolutePath()); + final File previousSessionFile = getPreviousSessionFile(directory.getAbsolutePath()); if (HintUtils.hasType(hint, SessionEnd.class)) { if (!currentSessionFile.delete()) { @@ -96,70 +105,33 @@ public void store(final @NotNull SentryEnvelope envelope, final @NotNull Hint hi } } - if (HintUtils.hasType(hint, SessionStart.class)) { - boolean crashedLastRun = false; + if (HintUtils.hasType(hint, AbnormalExit.class)) { + tryEndPreviousSession(hint); + } - // TODO: should we move this to AppLifecycleIntegration? and do on SDK init? but it's too much - // on main-thread + 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) { - 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); - Date timestamp = null; - if (crashMarkerFile.exists()) { - options - .getLogger() - .log(INFO, "Crash marker file exists, last Session is gonna be Crashed."); - - 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 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); + if (session != null) { + writeSessionToDisk(previousSessionFile, session); } } 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 - // so deleting it as the new session will take place. - if (!currentSessionFile.delete()) { - options.getLogger().log(WARNING, "Failed to delete the current session file."); - } } updateCurrentSession(currentSessionFile, envelope); + boolean crashedLastRun = false; + final File crashMarkerFile = new File(options.getCacheDirPath(), NATIVE_CRASH_MARKER_FILE); + if (crashMarkerFile.exists()) { + crashedLastRun = true; + } + // check java marker file if the native marker isnt there if (!crashedLastRun) { final File javaCrashMarkerFile = new File(options.getCacheDirPath(), CRASH_MARKER_FILE); @@ -181,6 +153,8 @@ public void store(final @NotNull SentryEnvelope envelope, final @NotNull Hint hi } SentryCrashLastRunState.getInstance().setCrashedLastRun(crashedLastRun); + + previousSessionLatch.countDown(); } // TODO: probably we need to update the current session file for session updates to because of @@ -204,11 +178,68 @@ 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(); } } + /** + * 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. + * + * @param hint a hint coming with the envelope + */ + @SuppressWarnings("JavaUtilDate") + private void tryEndPreviousSession(final @NotNull Hint hint) { + final Object sdkHint = HintUtils.getSentrySdkHint(hint); + if (sdkHint instanceof AbnormalExit) { + final File previousSessionFile = getPreviousSessionFile(directory.getAbsolutePath()); + + if (previousSessionFile.exists()) { + 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))) { + 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; + } + } + + 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); + writeSessionToDisk(previousSessionFile, session); + } + } catch (Throwable e) { + options.getLogger().log(SentryLevel.ERROR, "Error processing previous session.", e); + } + } else { + options.getLogger().log(DEBUG, "No previous session file to end."); + } + } + } + private void writeCrashMarkerFile() { final File crashMarkerFile = new File(options.getCacheDirPath(), CRASH_MARKER_FILE); try (final OutputStream outputStream = new FileOutputStream(crashMarkerFile)) { @@ -220,26 +251,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(); @@ -365,9 +376,12 @@ public void discard(final @NotNull SentryEnvelope envelope) { return new File(directory.getAbsolutePath(), fileName); } - private @NotNull File getCurrentSessionFile() { - return new File( - directory.getAbsolutePath(), PREFIX_CURRENT_SESSION_FILE + SUFFIX_CURRENT_SESSION_FILE); + public static @NotNull File getCurrentSessionFile(final @NotNull String cacheDirPath) { + 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); } @Override @@ -411,4 +425,19 @@ public void discard(final @NotNull SentryEnvelope envelope) { } return new File[] {}; } + + /** Awaits until the previous session (if any) is flushed to its own file. */ + public boolean waitPreviousSessionFlush() { + try { + 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."); + } + return false; + } + + public void flushPreviousSession() { + previousSessionLatch.countDown(); + } } 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/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/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/hints/TransactionEnd.java b/sentry/src/main/java/io/sentry/hints/TransactionEnd.java new file mode 100644 index 0000000000..d13027e712 --- /dev/null +++ b/sentry/src/main/java/io/sentry/hints/TransactionEnd.java @@ -0,0 +1,4 @@ +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/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/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 89787fd75f..46f1261888 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; @@ -444,6 +446,87 @@ public void setCpuDescription(@Nullable final String cpuDescription) { this.cpuDescription = cpuDescription; } + @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) + && Objects.equals(processorCount, device.processorCount) + && Objects.equals(processorFrequency, device.processorFrequency) + && Objects.equals(cpuDescription, device.cpuDescription); + } + + @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, + processorCount, + processorFrequency, + cpuDescription); + 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/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/main/java/io/sentry/protocol/User.java b/sentry/src/main/java/io/sentry/protocol/User.java index 4a6e96e974..a6ff46b4d0 100644 --- a/sentry/src/main/java/io/sentry/protocol/User.java +++ b/sentry/src/main/java/io/sentry/protocol/User.java @@ -9,6 +9,7 @@ import io.sentry.SentryLevel; import io.sentry.SentryOptions; 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; @@ -333,6 +334,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 f80d4bf63c..cbd5494726 100644 --- a/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java +++ b/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java @@ -8,6 +8,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; @@ -86,7 +87,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 8625c87079..1c9243d2ff 100644 --- a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt +++ b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt @@ -1133,6 +1133,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/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/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..8e27662b5b --- /dev/null +++ b/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt @@ -0,0 +1,206 @@ +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, + 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) + 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 changing 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()) + } + + @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()) + } +} 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 89bd316632..19dff0a504 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -7,8 +7,9 @@ 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.hints.TransactionEnd import io.sentry.protocol.Contexts import io.sentry.protocol.Mechanism import io.sentry.protocol.Request @@ -697,6 +698,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 -> @@ -1998,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 @@ -2014,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( @@ -2239,10 +2285,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? { @@ -2250,6 +2292,10 @@ class SentryClientTest { } } } + + private class BackfillableHint : Backfillable { + override fun shouldEnrich(): Boolean = false + } } class DropEverythingEventProcessor : EventProcessor { 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/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 204635f87c..57927840b0 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -334,6 +334,16 @@ class SentryOptionsTest { assertEquals("debug", options.environment) } + @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/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/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index 0dfe70bf1f..928c43bb7c 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -4,9 +4,13 @@ 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.Rule import org.junit.rules.TemporaryFolder import org.mockito.kotlin.any import org.mockito.kotlin.argThat @@ -17,6 +21,7 @@ import org.mockito.kotlin.whenever 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 @@ -31,6 +36,9 @@ class SentryTest { private val dsn = "http://key@localhost/proj" + @get:Rule + val tmpDir = TemporaryFolder() + @BeforeTest @AfterTest fun beforeTest() { @@ -114,7 +122,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 @@ -126,7 +137,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 @@ -547,6 +561,179 @@ class SentryTest { assertEquals(executorService, sentryOptions!!.executorService) } + @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) + } + + @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 + 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/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/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..33970f4093 100644 --- a/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt +++ b/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt @@ -1,28 +1,29 @@ 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_CURRENT_SESSION_FILE -import io.sentry.hints.DiskFlushNotification +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 @@ -33,22 +34,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 } } @@ -68,7 +63,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()) @@ -79,7 +74,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 } @@ -90,12 +85,12 @@ 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) - 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() @@ -107,12 +102,12 @@ 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) - 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()) @@ -128,15 +123,15 @@ 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) - 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) + val session = fixture.options.serializer.deserialize(currentFile.bufferedReader(Charsets.UTF_8), Session::class.java) assertNotNull(session) currentFile.delete() @@ -145,130 +140,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 disk flush 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(DiskFlushHint()) + override fun timestamp(): Long = sessionExitedWithAbnormal + } + val hints = HintUtils.createWithTypeCheckHint(abnormalHint) cache.store(envelope, hints) - assertTrue(markerFile.exists()) - } - - internal class DiskFlushHint : DiskFlushNotification { - override fun markFlushed() {} + 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 + ) } } 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/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/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)) + } } 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 + } } }