diff --git a/CHANGELOG.md b/CHANGELOG.md index 3310c10e074..aef8f605ff7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,44 @@ # Changelog +## 7.0.0-beta.1 + +### Features + +**Breaking changes:** +- Capture failed HTTP requests by default ([#2794](https://github.com/getsentry/sentry-java/pull/2794)) +- Reduce flush timeout to 4s on Android to avoid ANRs ([#2858](https://github.com/getsentry/sentry-java/pull/2858)) +- Set ip_address to {{auto}} by default, even if sendDefaultPII is disabled ([#2860](https://github.com/getsentry/sentry-java/pull/2860)) + - Instead use the "Prevent Storing of IP Addresses" option in the "Security & Privacy" project settings on sentry.io +- Reduce timeout of AsyncHttpTransport to avoid ANR ([#2879](https://github.com/getsentry/sentry-java/pull/2879)) +- Add deadline timeout for automatic transactions ([#2865](https://github.com/getsentry/sentry-java/pull/2865)) + - This affects all automatically generated transactions on Android (UI, clicks), the default timeout is 30s +- Apollo v2 BeforeSpanCallback now allows returning null ([#2890](https://github.com/getsentry/sentry-java/pull/2890)) +- Automatic user interaction tracking: every click now starts a new automatic transaction ([#2891](https://github.com/getsentry/sentry-java/pull/2891)) + - Previously performing a click on the same UI widget twice would keep the existing transaction running, the new behavior now better aligns with other SDKs +- Android only: If global hub mode is enabled, Sentry.getSpan() returns the root span instead of the latest span ([#2855](https://github.com/getsentry/sentry-java/pull/2855)) +- Observe network state to upload any unsent envelopes ([#2910](https://github.com/getsentry/sentry-java/pull/2910)) + - Android: it works out-of-the-box as part of the default `SendCachedEnvelopeIntegration` + - JVM: you'd have to install `SendCachedEnvelopeFireAndForgetIntegration` as mentioned in https://docs.sentry.io/platforms/java/configuration/#configuring-offline-caching and provide your own implementation of `IConnectionStatusProvider` via `SentryOptions` +- Do not try to send and drop cached envelopes when rate-limiting is active ([#2937](https://github.com/getsentry/sentry-java/pull/2937)) + +### Fixes + +- Measure AppStart time till First Draw instead of `onResume` ([#2851](https://github.com/getsentry/sentry-java/pull/2851)) +- Do not overwrite UI transaction status if set by the user ([#2852](https://github.com/getsentry/sentry-java/pull/2852)) +- Capture unfinished transaction on Scope with status `aborted` in case a crash happens ([#2938](https://github.com/getsentry/sentry-java/pull/2938)) + - This will fix the link between transactions and corresponding crashes, you'll be able to see them in a single trace + +**Breaking changes:** +- Move enableNdk from SentryOptions to SentryAndroidOptions ([#2793](https://github.com/getsentry/sentry-java/pull/2793)) +- Fix Coroutine Context Propagation using CopyableThreadContextElement, requires `kotlinx-coroutines-core` version `1.6.1` or higher ([#2838](https://github.com/getsentry/sentry-java/pull/2838)) +- Bump min API to 19 ([#2883](https://github.com/getsentry/sentry-java/pull/2883)) +- Fix don't overwrite the span status of unfinished spans ([#2859](https://github.com/getsentry/sentry-java/pull/2859)) + - If you're using a self hosted version of sentry, sentry self hosted >= 22.12.0 is required +- Migrate from `default` interface methods to proper implementations in each interface implementor ([#2847](https://github.com/getsentry/sentry-java/pull/2847)) + - This prevents issues when using the SDK on older AGP versions (< 4.x.x) + - Make sure to align Sentry dependencies to the same version when bumping the SDK to 7.+, otherwise it will crash at runtime due to binary incompatibility. + (E.g. if you're using `-timber`, `-okhttp` or other packages) + ## 6.30.0 ### Features diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index af4d49a3bdb..12352f1b9ce 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -32,9 +32,9 @@ object Config { object Android { private val sdkVersion = 33 - val minSdkVersion = 14 + val minSdkVersion = 19 val minSdkVersionOkHttp = 21 - val minSdkVersionNdk = 16 + val minSdkVersionNdk = 19 val minSdkVersionCompose = 21 val targetSdkVersion = sdkVersion val compileSdkVersion = sdkVersion @@ -111,7 +111,7 @@ object Config { val retrofit2 = "$retrofit2Group:retrofit:$retrofit2Version" val retrofit2Gson = "$retrofit2Group:converter-gson:$retrofit2Version" - val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3" + val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1" val fragment = "androidx.fragment:fragment-ktx:1.3.5" diff --git a/gradle.properties b/gradle.properties index 1e9d0ec1b0b..a3566cfdb72 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ android.useAndroidX=true android.defaults.buildfeatures.buildconfig=true # Release information -versionName=6.30.0 +versionName=7.0.0-beta.1 # Override the SDK name on native crashes on Android sentryAndroidSdkName=sentry.native.android diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index c94dea1e5f6..42ff145ec1a 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -63,6 +63,7 @@ public final class io/sentry/android/core/AnrIntegrationFactory { 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 fun process (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction; } public class io/sentry/android/core/AnrV2Integration : io/sentry/Integration, java/io/Closeable { @@ -73,7 +74,10 @@ public class io/sentry/android/core/AnrV2Integration : io/sentry/Integration, ja 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 ignoreCurrentThread ()Z + public fun isFlushable (Lio/sentry/protocol/SentryId;)Z public fun mechanism ()Ljava/lang/String; + public fun setFlushable (Lio/sentry/protocol/SentryId;)V public fun shouldEnrich ()Z public fun timestamp ()Ljava/lang/Long; } @@ -205,9 +209,10 @@ public final class io/sentry/android/core/PhoneStateBreadcrumbsIntegration : io/ public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V } -public final class io/sentry/android/core/ScreenshotEventProcessor : io/sentry/EventProcessor, io/sentry/IntegrationName { +public final class io/sentry/android/core/ScreenshotEventProcessor : io/sentry/EventProcessor { public fun (Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/BuildInfoProvider;)V public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; + public fun process (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction; } public final class io/sentry/android/core/SentryAndroid { @@ -245,8 +250,10 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun isEnableAppLifecycleBreadcrumbs ()Z public fun isEnableAutoActivityLifecycleTracing ()Z public fun isEnableFramesTracking ()Z + public fun isEnableNdk ()Z public fun isEnableNetworkEventBreadcrumbs ()Z public fun isEnableRootCheck ()Z + public fun isEnableScopeSync ()Z public fun isEnableSystemEventBreadcrumbs ()Z public fun isReportHistoricalAnrs ()Z public fun setAnrEnabled (Z)V @@ -265,8 +272,10 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun setEnableAppLifecycleBreadcrumbs (Z)V public fun setEnableAutoActivityLifecycleTracing (Z)V public fun setEnableFramesTracking (Z)V + public fun setEnableNdk (Z)V public fun setEnableNetworkEventBreadcrumbs (Z)V public fun setEnableRootCheck (Z)V + public fun setEnableScopeSync (Z)V public fun setEnableSystemEventBreadcrumbs (Z)V public fun setNativeSdkName (Ljava/lang/String;)V public fun setProfilingTracesHz (I)V @@ -346,9 +355,10 @@ public final class io/sentry/android/core/UserInteractionIntegration : android/a public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V } -public final class io/sentry/android/core/ViewHierarchyEventProcessor : io/sentry/EventProcessor, io/sentry/IntegrationName { +public final class io/sentry/android/core/ViewHierarchyEventProcessor : io/sentry/EventProcessor { public fun (Lio/sentry/android/core/SentryAndroidOptions;)V public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; + public fun process (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction; public static fun snapshotViewHierarchy (Landroid/app/Activity;Lio/sentry/ILogger;)Lio/sentry/protocol/ViewHierarchy; public static fun snapshotViewHierarchy (Landroid/app/Activity;Ljava/util/List;Lio/sentry/util/thread/IMainThreadChecker;Lio/sentry/ILogger;)Lio/sentry/protocol/ViewHierarchy; public static fun snapshotViewHierarchy (Landroid/view/View;)Lio/sentry/protocol/ViewHierarchy; diff --git a/sentry-android-core/proguard-rules.pro b/sentry-android-core/proguard-rules.pro index 79daaace033..9b0e74c6d0f 100644 --- a/sentry-android-core/proguard-rules.pro +++ b/sentry-android-core/proguard-rules.pro @@ -31,7 +31,22 @@ -keepattributes LineNumberTable,SourceFile # Keep Classnames for integrations --keepnames class * implements io.sentry.IntegrationName +-keepnames class * implements io.sentry.Integration + +-dontwarn io.sentry.apollo.SentryApolloInterceptor +-keepnames class io.sentry.apollo.SentryApolloInterceptor + +-dontwarn io.sentry.apollo3.SentryApollo3HttpInterceptor +-keepnames class io.sentry.apollo3.SentryApollo3HttpInterceptor + +-dontwarn io.sentry.android.okhttp.SentryOkHttpInterceptor +-keepnames class io.sentry.android.okhttp.SentryOkHttpInterceptor + +-dontwarn io.sentry.android.navigation.SentryNavigationListener +-keepnames class io.sentry.android.navigation.SentryNavigationListener + +-keepnames class io.sentry.android.core.ScreenshotEventProcessor +-keepnames class io.sentry.android.core.ViewHierarchyEventProcessor # Keep any custom option classes like SentryAndroidOptions, as they're loaded via reflection # Also keep method names, as they're e.g. used by native via JNI calls diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index ee4ea10b729..2826366b4b6 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -2,8 +2,8 @@ import static io.sentry.MeasurementUnit.Duration.MILLISECOND; import static io.sentry.TypeCheckHint.ANDROID_ACTIVITY; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; -import android.annotation.SuppressLint; import android.app.Activity; import android.app.Application; import android.os.Build; @@ -127,7 +127,7 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio application.registerActivityLifecycleCallbacks(this); this.options.getLogger().log(SentryLevel.DEBUG, "ActivityLifecycleIntegration installed."); - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } private boolean isPerformanceEnabled(final @NotNull SentryAndroidOptions options) { @@ -192,6 +192,9 @@ private void startTracing(final @NotNull Activity activity) { final Boolean coldStart = AppStartState.getInstance().isColdStart(); final TransactionOptions transactionOptions = new TransactionOptions(); + transactionOptions.setDeadlineTimeout( + TransactionOptions.DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION); + if (options.isEnableActivityLifecycleTracingAutoFinish()) { transactionOptions.setIdleTimeout(options.getIdleTimeout()); transactionOptions.setTrimEnd(true); @@ -396,26 +399,13 @@ public synchronized void onActivityStarted(final @NotNull Activity activity) { addBreadcrumb(activity, "started"); } - @SuppressLint("NewApi") @Override public synchronized void onActivityResumed(final @NotNull Activity activity) { if (performanceEnabled) { - // app start span - @Nullable final SentryDate appStartStartTime = AppStartState.getInstance().getAppStartTime(); - @Nullable final SentryDate appStartEndTime = AppStartState.getInstance().getAppStartEndTime(); - // in case the SentryPerformanceProvider is disabled it does not set the app start times, - // and we need to set the end time manually here, - // the start time gets set manually in SentryAndroid.init() - if (appStartStartTime != null && appStartEndTime == null) { - AppStartState.getInstance().setAppStartEnd(); - } - finishAppStartSpan(); - final @Nullable ISpan ttidSpan = ttidSpanMap.get(activity); final @Nullable ISpan ttfdSpan = ttfdSpanMap.get(activity); final View rootView = activity.findViewById(android.R.id.content); - if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN - && rootView != null) { + if (rootView != null) { FirstDrawDoneListener.registerForNextDraw( rootView, () -> onFirstFrameDrawn(ttfdSpan, ttidSpan), buildInfoProvider); } else { @@ -540,6 +530,17 @@ private void cancelTtfdAutoClose() { } private void onFirstFrameDrawn(final @Nullable ISpan ttfdSpan, final @Nullable ISpan ttidSpan) { + // app start span + @Nullable final SentryDate appStartStartTime = AppStartState.getInstance().getAppStartTime(); + @Nullable final SentryDate appStartEndTime = AppStartState.getInstance().getAppStartEndTime(); + // in case the SentryPerformanceProvider is disabled it does not set the app start times, + // and we need to set the end time manually here, + // the start time gets set manually in SentryAndroid.init() + if (appStartStartTime != null && appStartEndTime == null) { + AppStartState.getInstance().setAppStartEnd(); + } + finishAppStartSpan(); + if (options != null && ttidSpan != null) { final SentryDate endDate = options.getDateProvider().now(); final long durationNanos = endDate.diff(ttidSpan.getStartDate()); 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 5a0fbef9b24..e014d1ac402 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 @@ -5,10 +5,10 @@ import android.app.Application; import android.content.Context; import android.content.pm.PackageInfo; -import android.os.Build; import io.sentry.DeduplicateMultithreadedEventProcessor; import io.sentry.DefaultTransactionPerformanceCollector; import io.sentry.ILogger; +import io.sentry.NoOpConnectionStatusProvider; import io.sentry.SendFireAndForgetEnvelopeSender; import io.sentry.SendFireAndForgetOutboxSender; import io.sentry.SentryLevel; @@ -16,6 +16,7 @@ import io.sentry.android.core.internal.debugmeta.AssetsDebugMetaLoader; import io.sentry.android.core.internal.gestures.AndroidViewGestureTargetLocator; import io.sentry.android.core.internal.modules.AssetsModulesLoader; +import io.sentry.android.core.internal.util.AndroidConnectionStatusProvider; import io.sentry.android.core.internal.util.AndroidMainThreadChecker; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.android.fragment.FragmentLifecycleIntegration; @@ -42,6 +43,8 @@ @SuppressWarnings("Convert2MethodRef") // older AGP versions do not support method references final class AndroidOptionsInitializer { + static final long DEFAULT_FLUSH_TIMEOUT_MS = 4000; + static final String SENTRY_COMPOSE_GESTURE_INTEGRATION_CLASS_NAME = "io.sentry.compose.gestures.ComposeGestureTargetLocator"; @@ -94,6 +97,9 @@ static void loadDefaultAndMetadataOptions( options.setDateProvider(new SentryAndroidDateProvider()); + // set a lower flush timeout on Android to avoid ANRs + options.setFlushTimeoutMillis(DEFAULT_FLUSH_TIMEOUT_MS); + ManifestMetadataReader.applyMetadata(context, options, buildInfoProvider); initializeCacheDirs(context, options); @@ -126,6 +132,11 @@ static void initializeIntegrationsAndProcessors( options.setEnvelopeDiskCache(new AndroidEnvelopeCache(options)); } + if (options.getConnectionStatusProvider() instanceof NoOpConnectionStatusProvider) { + options.setConnectionStatusProvider( + new AndroidConnectionStatusProvider(context, options.getLogger(), buildInfoProvider)); + } + options.addEventProcessor(new DeduplicateMultithreadedEventProcessor(options)); options.addEventProcessor( new DefaultAndroidEventProcessor(context, buildInfoProvider, options)); @@ -133,7 +144,7 @@ static void initializeIntegrationsAndProcessors( 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())); + options.setTransportGate(new AndroidTransportGate(options)); final SentryFrameMetricsCollector frameMetricsCollector = new SentryFrameMetricsCollector(context, options, buildInfoProvider); options.setTransactionProfiler( @@ -208,10 +219,7 @@ static void installDefaultIntegrations( // Integrations are registered in the same order. NDK before adding Watch outbox, // because sentry-native move files around and we don't want to watch that. - final Class sentryNdkClass = - isNdkAvailable(buildInfoProvider) - ? loadClass.loadClass(SENTRY_NDK_CLASS_NAME, options.getLogger()) - : null; + final Class sentryNdkClass = loadClass.loadClass(SENTRY_NDK_CLASS_NAME, options.getLogger()); options.addIntegration(new NdkIntegration(sentryNdkClass)); // this integration uses android.os.FileObserver, we can't move to sentry @@ -320,8 +328,4 @@ private static void initializeCacheDirs( final File cacheDir = new File(context.getCacheDir(), "sentry"); options.setCacheDirPath(cacheDir.getAbsolutePath()); } - - private static boolean isNdkAvailable(final @NotNull BuildInfoProvider buildInfoProvider) { - return buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN; - } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransportGate.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransportGate.java index fd9215970a3..fa138a802f2 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransportGate.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransportGate.java @@ -1,29 +1,26 @@ package io.sentry.android.core; -import android.content.Context; -import io.sentry.ILogger; -import io.sentry.android.core.internal.util.ConnectivityChecker; +import io.sentry.IConnectionStatusProvider; +import io.sentry.SentryOptions; import io.sentry.transport.ITransportGate; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.TestOnly; final class AndroidTransportGate implements ITransportGate { - private final Context context; - private final @NotNull ILogger logger; + private final @NotNull SentryOptions options; - AndroidTransportGate(final @NotNull Context context, final @NotNull ILogger logger) { - this.context = context; - this.logger = logger; + AndroidTransportGate(final @NotNull SentryOptions options) { + this.options = options; } @Override public boolean isConnected() { - return isConnected(ConnectivityChecker.getConnectionStatus(context, logger)); + return isConnected(options.getConnectionStatusProvider().getConnectionStatus()); } @TestOnly - boolean isConnected(final @NotNull ConnectivityChecker.Status status) { + boolean isConnected(final @NotNull IConnectionStatusProvider.ConnectionStatus status) { // let's assume its connected if there's no permission or something as we can't really know // whether is online or not. switch (status) { 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 4c6ac6373af..0b0d56b093e 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 @@ -1,5 +1,7 @@ package io.sentry.android.core; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; + import android.annotation.SuppressLint; import android.content.Context; import io.sentry.Hint; @@ -74,7 +76,7 @@ private void register(final @NotNull IHub hub, final @NotNull SentryAndroidOptio anrWatchDog.start(); options.getLogger().log(SentryLevel.DEBUG, "AnrIntegration installed."); - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } } } @@ -161,5 +163,10 @@ public String mechanism() { public boolean ignoreCurrentThread() { return true; } + + @Override + public @Nullable Long timestamp() { + return null; + } } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java index 703f9e1f80d..f8fbc498b08 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java @@ -48,6 +48,7 @@ import io.sentry.protocol.SdkVersion; import io.sentry.protocol.SentryStackTrace; import io.sentry.protocol.SentryThread; +import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.User; import io.sentry.util.HintUtils; import java.util.ArrayList; @@ -90,6 +91,15 @@ public AnrV2EventProcessor( sentryExceptionFactory = new SentryExceptionFactory(sentryStackTraceFactory); } + @Override + public @NotNull SentryTransaction process( + @NotNull SentryTransaction transaction, @NotNull Hint hint) { + // that's only necessary because on newer versions of Unity, if not overriding this method, it's + // throwing 'java.lang.AbstractMethodError: abstract method' and the reason is probably + // compilation mismatch + return transaction; + } + @Override public @Nullable SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) { final Object unwrappedHint = HintUtils.getSentrySdkHint(hint); @@ -475,35 +485,19 @@ private void setExceptions(final @NotNull SentryEvent event, final @NotNull Obje } 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); - } + @Nullable User user = event.getUser(); + if (user == null) { + user = new User(); + event.setUser(user); } // 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) { + 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; + if (user.getIpAddress() == null) { + user.setIpAddress(IpAddressUtils.DEFAULT_IP_ADDRESS); + } } private @Nullable String getDeviceId() { @@ -543,7 +537,7 @@ private void setDevice(final @NotNull SentryBaseEvent event) { private @NotNull Device getDevice() { Device device = new Device(); if (options.isSendDefaultPii()) { - device.setName(ContextUtils.getDeviceName(context, buildInfoProvider)); + device.setName(ContextUtils.getDeviceName(context)); } device.setManufacturer(Build.MANUFACTURER); device.setBrand(Build.BRAND); @@ -582,13 +576,8 @@ private void setDevice(final @NotNull SentryBaseEvent event) { 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 + return memInfo.totalMem; } private void mergeOS(final @NotNull SentryBaseEvent event) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java index f3d257d225b..6c7181641c2 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java @@ -1,5 +1,7 @@ package io.sentry.android.core; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; + import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.ApplicationExitInfo; @@ -93,7 +95,7 @@ public void register(@NotNull IHub hub, @NotNull SentryOptions options) { options.getLogger().log(SentryLevel.DEBUG, "Failed to start AnrProcessor.", e); } options.getLogger().log(SentryLevel.DEBUG, "AnrV2Integration installed."); - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } } @@ -365,6 +367,11 @@ public AnrV2Hint( this.isBackgroundAnr = isBackgroundAnr; } + @Override + public boolean ignoreCurrentThread() { + return false; + } + @Override public Long timestamp() { return timestamp; @@ -379,6 +386,14 @@ public boolean shouldEnrich() { public String mechanism() { return isBackgroundAnr ? "anr_background" : "anr_foreground"; } + + @Override + public boolean isFlushable(@Nullable SentryId eventId) { + return true; + } + + @Override + public void setFlushable(@NotNull SentryId eventId) {} } static final class ParseResult { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegration.java index c9a552a5b76..eef47076837 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegration.java @@ -1,6 +1,7 @@ package io.sentry.android.core; import static io.sentry.TypeCheckHint.ANDROID_CONFIGURATION; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; import android.content.ComponentCallbacks2; import android.content.Context; @@ -53,7 +54,7 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio options .getLogger() .log(SentryLevel.DEBUG, "AppComponentsBreadcrumbsIntegration installed."); - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } catch (Throwable e) { this.options.setEnableAppComponentBreadcrumbs(false); options.getLogger().log(SentryLevel.INFO, e, "ComponentCallbacks2 is not available."); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AppLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AppLifecycleIntegration.java index 7f4724967f4..3e8fe6383f8 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AppLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AppLifecycleIntegration.java @@ -1,5 +1,7 @@ package io.sentry.android.core; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; + import androidx.lifecycle.ProcessLifecycleOwner; import io.sentry.IHub; import io.sentry.Integration; @@ -94,7 +96,7 @@ private void addObserver(final @NotNull IHub hub) { try { ProcessLifecycleOwner.get().getLifecycle().addObserver(watcher); options.getLogger().log(SentryLevel.DEBUG, "AppLifecycleIntegration installed."); - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } catch (Throwable e) { // This is to handle a potential 'AbstractMethodError' gracefully. The error is triggered in // connection with conflicting dependencies of the androidx.lifecycle. diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AppStartState.java b/sentry-android-core/src/main/java/io/sentry/android/core/AppStartState.java index de690aa6683..0c38d04d48a 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AppStartState.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AppStartState.java @@ -45,6 +45,10 @@ synchronized void setAppStartEnd() { @TestOnly void setAppStartEnd(final long appStartEndMillis) { + if (this.appStartEndMillis != null) { + // only set app start end once + return; + } this.appStartEndMillis = appStartEndMillis; } 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 6a0457e50ef..2ce5ac4fb07 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 @@ -301,14 +301,8 @@ static boolean isForegroundImportance(final @NotNull Context context) { } } - @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; - } + static @Nullable String getDeviceName(final @NotNull Context context) { + return Settings.Global.getString(context.getContentResolver(), "device_name"); } @SuppressWarnings("deprecation") @@ -346,8 +340,6 @@ static boolean isForegroundImportance(final @NotNull Context context) { } } - // we perform an if-check for that, but lint fails to recognize - @SuppressLint("NewApi") static void setAppPackageInfo( final @NotNull PackageInfo packageInfo, final @NotNull BuildInfoProvider buildInfoProvider, @@ -356,26 +348,24 @@ static void setAppPackageInfo( app.setAppVersion(packageInfo.versionName); app.setAppBuild(ContextUtils.getVersionCode(packageInfo, buildInfoProvider)); - if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN) { - final Map permissions = new HashMap<>(); - final String[] requestedPermissions = packageInfo.requestedPermissions; - final int[] requestedPermissionsFlags = packageInfo.requestedPermissionsFlags; - - if (requestedPermissions != null - && requestedPermissions.length > 0 - && requestedPermissionsFlags != null - && requestedPermissionsFlags.length > 0) { - for (int i = 0; i < requestedPermissions.length; i++) { - String permission = requestedPermissions[i]; - permission = permission.substring(permission.lastIndexOf('.') + 1); - - final boolean granted = - (requestedPermissionsFlags[i] & REQUESTED_PERMISSION_GRANTED) - == REQUESTED_PERMISSION_GRANTED; - permissions.put(permission, granted ? "granted" : "not_granted"); - } + final Map permissions = new HashMap<>(); + final String[] requestedPermissions = packageInfo.requestedPermissions; + final int[] requestedPermissionsFlags = packageInfo.requestedPermissionsFlags; + + if (requestedPermissions != null + && requestedPermissions.length > 0 + && requestedPermissionsFlags != null + && requestedPermissionsFlags.length > 0) { + for (int i = 0; i < requestedPermissions.length; i++) { + String permission = requestedPermissions[i]; + permission = permission.substring(permission.lastIndexOf('.') + 1); + + final boolean granted = + (requestedPermissionsFlags[i] & REQUESTED_PERMISSION_GRANTED) + == REQUESTED_PERMISSION_GRANTED; + permissions.put(permission, granted ? "granted" : "not_granted"); } - app.setPermissions(permissions); } + app.setPermissions(permissions); } } 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 75cc4821367..0202bf2d866 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 @@ -6,6 +6,7 @@ import io.sentry.DateUtils; import io.sentry.EventProcessor; import io.sentry.Hint; +import io.sentry.IpAddressUtils; import io.sentry.SentryBaseEvent; import io.sentry.SentryEvent; import io.sentry.SentryLevel; @@ -93,13 +94,19 @@ private boolean shouldApplyScopeData( } private void mergeUser(final @NotNull SentryBaseEvent event) { - // userId should be set even if event is Cached as the userId is static and won't change anyway. - final User user = event.getUser(); + @Nullable User user = event.getUser(); if (user == null) { - event.setUser(getDefaultUser(context)); - } else if (user.getId() == null) { + user = new User(); + event.setUser(user); + } + + // userId should be set even if event is Cached as the userId is static and won't change anyway. + if (user.getId() == null) { user.setId(Installation.id(context)); } + if (user.getIpAddress() == null) { + user.setIpAddress(IpAddressUtils.DEFAULT_IP_ADDRESS); + } } private void setDevice( diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java b/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java index 78f9a593d9f..42970ce91c5 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java @@ -16,7 +16,6 @@ import android.util.DisplayMetrics; import io.sentry.DateUtils; import io.sentry.SentryLevel; -import io.sentry.android.core.internal.util.ConnectivityChecker; import io.sentry.android.core.internal.util.CpuInfoUtils; import io.sentry.android.core.internal.util.DeviceOrientations; import io.sentry.android.core.internal.util.RootChecker; @@ -64,7 +63,7 @@ public DeviceInfoUtil( final @Nullable ActivityManager.MemoryInfo memInfo = ContextUtils.getMemInfo(context, options.getLogger()); if (memInfo != null) { - totalMem = getMemorySize(memInfo); + totalMem = memInfo.totalMem; } else { totalMem = null; } @@ -97,7 +96,7 @@ public Device collectDeviceInformation( final @NotNull Device device = new Device(); if (options.isSendDefaultPii()) { - device.setName(ContextUtils.getDeviceName(context, buildInfoProvider)); + device.setName(ContextUtils.getDeviceName(context)); } device.setManufacturer(Build.MANUFACTURER); device.setBrand(Build.BRAND); @@ -191,8 +190,8 @@ private void setDeviceIO(final @NotNull Device device, final boolean includeDyna } Boolean connected; - switch (ConnectivityChecker.getConnectionStatus(context, options.getLogger())) { - case NOT_CONNECTED: + switch (options.getConnectionStatusProvider().getConnectionStatus()) { + case DISCONNECTED: connected = false; break; case CONNECTED: @@ -229,8 +228,7 @@ private void setDeviceIO(final @NotNull Device device, final boolean includeDyna if (device.getConnectionType() == null) { // wifi, ethernet or cellular, null if none - device.setConnectionType( - ConnectivityChecker.getConnectionType(context, options.getLogger(), buildInfoProvider)); + device.setConnectionType(options.getConnectionStatusProvider().getConnectionType()); } } @@ -344,16 +342,6 @@ private Device.DeviceOrientation getOrientation() { return deviceOrientation; } - @SuppressWarnings({"ObsoleteSdkInt", "NewApi"}) - @NotNull - private 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 - } - /** * Get the total amount of internal storage, in bytes. * @@ -362,8 +350,8 @@ private Long getMemorySize(final @NotNull ActivityManager.MemoryInfo memInfo) { @Nullable private Long getTotalInternalStorage(final @NotNull StatFs stat) { try { - long blockSize = getBlockSizeLong(stat); - long totalBlocks = getBlockCountLong(stat); + long blockSize = stat.getBlockSizeLong(); + long totalBlocks = stat.getBlockCountLong(); return totalBlocks * blockSize; } catch (Throwable e) { options.getLogger().log(SentryLevel.ERROR, "Error getting total internal storage amount.", e); @@ -371,45 +359,6 @@ private Long getTotalInternalStorage(final @NotNull StatFs stat) { } } - @SuppressWarnings({"ObsoleteSdkInt", "NewApi"}) - private long getBlockSizeLong(final @NotNull StatFs stat) { - if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - return stat.getBlockSizeLong(); - } - return getBlockSizeDep(stat); - } - - @SuppressWarnings({"deprecation"}) - private int getBlockSizeDep(final @NotNull StatFs stat) { - return stat.getBlockSize(); - } - - @SuppressWarnings({"ObsoleteSdkInt", "NewApi"}) - private long getBlockCountLong(final @NotNull StatFs stat) { - if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - return stat.getBlockCountLong(); - } - return getBlockCountDep(stat); - } - - @SuppressWarnings({"deprecation"}) - private int getBlockCountDep(final @NotNull StatFs stat) { - return stat.getBlockCount(); - } - - @SuppressWarnings({"ObsoleteSdkInt", "NewApi"}) - private long getAvailableBlocksLong(final @NotNull StatFs stat) { - if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - return stat.getAvailableBlocksLong(); - } - return getAvailableBlocksDep(stat); - } - - @SuppressWarnings({"deprecation"}) - private int getAvailableBlocksDep(final @NotNull StatFs stat) { - return stat.getAvailableBlocks(); - } - /** * Get the unused amount of internal storage, in bytes. * @@ -418,8 +367,8 @@ private int getAvailableBlocksDep(final @NotNull StatFs stat) { @Nullable private Long getUnusedInternalStorage(final @NotNull StatFs stat) { try { - long blockSize = getBlockSizeLong(stat); - long availableBlocks = getAvailableBlocksLong(stat); + long blockSize = stat.getBlockSizeLong(); + long availableBlocks = stat.getAvailableBlocksLong(); return availableBlocks * blockSize; } catch (Throwable e) { options @@ -443,23 +392,9 @@ private StatFs getExternalStorageStat(final @Nullable File internalStorage) { return null; } - @SuppressWarnings({"ObsoleteSdkInt", "NewApi"}) - @Nullable - private File[] getExternalFilesDirs() { - if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.KITKAT) { - return context.getExternalFilesDirs(null); - } else { - File single = context.getExternalFilesDir(null); - if (single != null) { - return new File[] {single}; - } - } - return null; - } - @Nullable private File getExternalStorageDep(final @Nullable File internalStorage) { - final @Nullable File[] externalFilesDirs = getExternalFilesDirs(); + final @Nullable File[] externalFilesDirs = context.getExternalFilesDirs(null); if (externalFilesDirs != null) { // return the 1st file which is not the emulated internal storage @@ -496,8 +431,8 @@ private File getExternalStorageDep(final @Nullable File internalStorage) { @Nullable private Long getTotalExternalStorage(final @NotNull StatFs stat) { try { - final long blockSize = getBlockSizeLong(stat); - final long totalBlocks = getBlockCountLong(stat); + final long blockSize = stat.getBlockSizeLong(); + final long totalBlocks = stat.getBlockCountLong(); return totalBlocks * blockSize; } catch (Throwable e) { options.getLogger().log(SentryLevel.ERROR, "Error getting total external storage amount.", e); @@ -521,8 +456,8 @@ private boolean isExternalStorageMounted() { @Nullable private Long getUnusedExternalStorage(final @NotNull StatFs stat) { try { - final long blockSize = getBlockSizeLong(stat); - final long availableBlocks = getAvailableBlocksLong(stat); + final long blockSize = stat.getBlockSizeLong(); + final long availableBlocks = stat.getAvailableBlocksLong(); return availableBlocks * blockSize; } catch (Throwable e) { options diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/EnvelopeFileObserverIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/EnvelopeFileObserverIntegration.java index 140b9449822..7dfa784555c 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/EnvelopeFileObserverIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/EnvelopeFileObserverIntegration.java @@ -43,7 +43,8 @@ public final void register(final @NotNull IHub hub, final @NotNull SentryOptions options.getEnvelopeReader(), options.getSerializer(), logger, - options.getFlushTimeoutMillis()); + options.getFlushTimeoutMillis(), + options.getMaxQueueSize()); observer = new EnvelopeFileObserver(path, outboxSender, logger, options.getFlushTimeoutMillis()); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/NdkIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/NdkIntegration.java index 4757c96879f..3a4a91498e7 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/NdkIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/NdkIntegration.java @@ -1,5 +1,7 @@ package io.sentry.android.core; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; + import io.sentry.IHub; import io.sentry.Integration; import io.sentry.SentryLevel; @@ -53,7 +55,7 @@ public final void register(final @NotNull IHub hub, final @NotNull SentryOptions method.invoke(null, args); this.options.getLogger().log(SentryLevel.DEBUG, "NdkIntegration installed."); - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } catch (NoSuchMethodException e) { disableNdkIntegration(this.options); this.options @@ -68,7 +70,7 @@ public final void register(final @NotNull IHub hub, final @NotNull SentryOptions } } - private void disableNdkIntegration(final @NotNull SentryOptions options) { + private void disableNdkIntegration(final @NotNull SentryAndroidOptions options) { options.setEnableNdk(false); options.setEnableScopeSync(false); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java index 317cb604677..6d6a0daa407 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java @@ -1,5 +1,7 @@ package io.sentry.android.core; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; + import android.annotation.SuppressLint; import android.content.Context; import android.net.ConnectivityManager; @@ -16,7 +18,7 @@ import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.TypeCheckHint; -import io.sentry.android.core.internal.util.ConnectivityChecker; +import io.sentry.android.core.internal.util.AndroidConnectionStatusProvider; import io.sentry.util.Objects; import java.io.Closeable; import java.io.IOException; @@ -69,7 +71,7 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio networkCallback = new NetworkBreadcrumbsNetworkCallback(hub, buildInfoProvider); final boolean registered = - ConnectivityChecker.registerNetworkCallback( + AndroidConnectionStatusProvider.registerNetworkCallback( context, logger, buildInfoProvider, networkCallback); // The specific error is logged in the ConnectivityChecker method @@ -79,14 +81,14 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio return; } logger.log(SentryLevel.DEBUG, "NetworkBreadcrumbsIntegration installed."); - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } } @Override public void close() throws IOException { if (networkCallback != null) { - ConnectivityChecker.unregisterNetworkCallback( + AndroidConnectionStatusProvider.unregisterNetworkCallback( context, logger, buildInfoProvider, networkCallback); logger.log(SentryLevel.DEBUG, "NetworkBreadcrumbsIntegration remove."); } @@ -208,7 +210,7 @@ static class NetworkBreadcrumbConnectionDetail { this.signalStrength = strength > -100 ? strength : 0; this.isVpn = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN); String connectionType = - ConnectivityChecker.getConnectionType(networkCapabilities, buildInfoProvider); + AndroidConnectionStatusProvider.getConnectionType(networkCapabilities, buildInfoProvider); this.type = connectionType != null ? connectionType : ""; } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/PhoneStateBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/PhoneStateBreadcrumbsIntegration.java index be23c668c7a..ea0426609c6 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/PhoneStateBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/PhoneStateBreadcrumbsIntegration.java @@ -1,6 +1,7 @@ package io.sentry.android.core; import static android.Manifest.permission.READ_PHONE_STATE; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; import android.content.Context; import android.telephony.TelephonyManager; @@ -53,7 +54,7 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio telephonyManager.listen(listener, android.telephony.PhoneStateListener.LISTEN_CALL_STATE); options.getLogger().log(SentryLevel.DEBUG, "PhoneStateBreadcrumbsIntegration installed."); - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } catch (Throwable e) { this.options .getLogger() diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java index 0812e550157..33e37a4d2e4 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java @@ -2,16 +2,17 @@ import static io.sentry.TypeCheckHint.ANDROID_ACTIVITY; import static io.sentry.android.core.internal.util.ScreenshotUtils.takeScreenshot; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; import android.app.Activity; import io.sentry.Attachment; import io.sentry.EventProcessor; import io.sentry.Hint; -import io.sentry.IntegrationName; import io.sentry.SentryEvent; import io.sentry.SentryLevel; import io.sentry.android.core.internal.util.AndroidCurrentDateProvider; import io.sentry.android.core.internal.util.Debouncer; +import io.sentry.protocol.SentryTransaction; import io.sentry.util.HintUtils; import io.sentry.util.Objects; import org.jetbrains.annotations.ApiStatus; @@ -23,7 +24,7 @@ * captured. */ @ApiStatus.Internal -public final class ScreenshotEventProcessor implements EventProcessor, IntegrationName { +public final class ScreenshotEventProcessor implements EventProcessor { private final @NotNull SentryAndroidOptions options; private final @NotNull BuildInfoProvider buildInfoProvider; @@ -40,10 +41,19 @@ public ScreenshotEventProcessor( this.debouncer = new Debouncer(AndroidCurrentDateProvider.getInstance(), DEBOUNCE_WAIT_TIME_MS); if (options.isAttachScreenshot()) { - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } } + @Override + public @NotNull SentryTransaction process( + @NotNull SentryTransaction transaction, @NotNull Hint hint) { + // that's only necessary because on newer versions of Unity, if not overriding this method, it's + // throwing 'java.lang.AbstractMethodError: abstract method' and the reason is probably + // compilation mismatch + return transaction; + } + @Override public @NotNull SentryEvent process(final @NotNull SentryEvent event, final @NotNull Hint hint) { if (!event.isErrored()) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SendCachedEnvelopeIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/SendCachedEnvelopeIntegration.java index e0d08325b45..061d2d232ca 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SendCachedEnvelopeIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SendCachedEnvelopeIntegration.java @@ -1,23 +1,36 @@ package io.sentry.android.core; +import io.sentry.DataCategory; +import io.sentry.IConnectionStatusProvider; import io.sentry.IHub; import io.sentry.Integration; import io.sentry.SendCachedEnvelopeFireAndForgetIntegration; import io.sentry.SentryLevel; import io.sentry.SentryOptions; +import io.sentry.transport.RateLimiter; import io.sentry.util.LazyEvaluator; import io.sentry.util.Objects; +import java.io.Closeable; +import java.io.IOException; import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -final class SendCachedEnvelopeIntegration implements Integration { +final class SendCachedEnvelopeIntegration + implements Integration, IConnectionStatusProvider.IConnectionStatusObserver, Closeable { private final @NotNull SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetFactory factory; private final @NotNull LazyEvaluator startupCrashMarkerEvaluator; + private final AtomicBoolean startupCrashHandled = new AtomicBoolean(false); + private @Nullable IConnectionStatusProvider connectionStatusProvider; + private @Nullable IHub hub; + private @Nullable SentryAndroidOptions options; + private @Nullable SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForget sender; public SendCachedEnvelopeIntegration( final @NotNull SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetFactory factory, @@ -28,8 +41,8 @@ public SendCachedEnvelopeIntegration( @Override public void register(@NotNull IHub hub, @NotNull SentryOptions options) { - Objects.requireNonNull(hub, "Hub is required"); - final SentryAndroidOptions androidOptions = + this.hub = Objects.requireNonNull(hub, "Hub is required"); + this.options = Objects.requireNonNull( (options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null, "SentryAndroidOptions is required"); @@ -40,51 +53,92 @@ public void register(@NotNull IHub hub, @NotNull SentryOptions options) { return; } - final SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForget sender = - factory.create(hub, androidOptions); + connectionStatusProvider = options.getConnectionStatusProvider(); + connectionStatusProvider.addConnectionStatusObserver(this); + + sender = factory.create(hub, options); + + sendCachedEnvelopes(hub, this.options); + } + + @Override + public void close() throws IOException { + if (connectionStatusProvider != null) { + connectionStatusProvider.removeConnectionStatusObserver(this); + } + } + + @Override + public void onConnectionStatusChanged( + final @NotNull IConnectionStatusProvider.ConnectionStatus status) { + if (hub != null && options != null) { + sendCachedEnvelopes(hub, options); + } + } + + @SuppressWarnings({"NullAway"}) + private synchronized void sendCachedEnvelopes( + final @NotNull IHub hub, final @NotNull SentryAndroidOptions options) { + + if (connectionStatusProvider != null + && connectionStatusProvider.getConnectionStatus() + == IConnectionStatusProvider.ConnectionStatus.DISCONNECTED) { + options.getLogger().log(SentryLevel.INFO, "SendCachedEnvelopeIntegration, no connection."); + return; + } + + // in case there's rate limiting active, skip processing + final @Nullable RateLimiter rateLimiter = hub.getRateLimiter(); + if (rateLimiter != null && rateLimiter.isActiveForCategory(DataCategory.All)) { + options + .getLogger() + .log(SentryLevel.INFO, "SendCachedEnvelopeIntegration, rate limiting active."); + return; + } if (sender == null) { - androidOptions.getLogger().log(SentryLevel.ERROR, "SendFireAndForget factory is null."); + options.getLogger().log(SentryLevel.ERROR, "SendCachedEnvelopeIntegration factory is null."); return; } try { - Future future = - androidOptions + final Future future = + options .getExecutorService() .submit( () -> { try { sender.send(); } catch (Throwable e) { - androidOptions + options .getLogger() .log(SentryLevel.ERROR, "Failed trying to send cached events.", e); } }); - if (startupCrashMarkerEvaluator.getValue()) { - androidOptions - .getLogger() - .log(SentryLevel.DEBUG, "Startup Crash marker exists, blocking flush."); + // startupCrashMarkerEvaluator remains true on subsequent runs, let's ensure we only block on + // the very first execution (=app start) + if (startupCrashMarkerEvaluator.getValue() + && startupCrashHandled.compareAndSet(false, true)) { + options.getLogger().log(SentryLevel.DEBUG, "Startup Crash marker exists, blocking flush."); try { - future.get(androidOptions.getStartupCrashFlushTimeoutMillis(), TimeUnit.MILLISECONDS); + future.get(options.getStartupCrashFlushTimeoutMillis(), TimeUnit.MILLISECONDS); } catch (TimeoutException e) { - androidOptions + options .getLogger() .log(SentryLevel.DEBUG, "Synchronous send timed out, continuing in the background."); } } - androidOptions.getLogger().log(SentryLevel.DEBUG, "SendCachedEnvelopeIntegration installed."); + options.getLogger().log(SentryLevel.DEBUG, "SendCachedEnvelopeIntegration installed."); } catch (RejectedExecutionException e) { - androidOptions + options .getLogger() .log( SentryLevel.ERROR, "Failed to call the executor. Cached events will not be sent. Did you call Sentry.close()?", e); } catch (Throwable e) { - androidOptions + options .getLogger() .log(SentryLevel.ERROR, "Failed to call the executor. Cached events will not be sent", e); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java index 62437a0425f..d57a467c53f 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java @@ -160,6 +160,15 @@ public final class SentryAndroidOptions extends SentryOptions { private @Nullable BeforeCaptureCallback beforeViewHierarchyCaptureCallback; + /** Turns NDK on or off. Default is enabled. */ + private boolean enableNdk = true; + + /** + * Enable the Java to NDK Scope sync. The default value for sentry-java is disabled and enabled + * for sentry-android. + */ + private boolean enableScopeSync = true; + public interface BeforeCaptureCallback { /** @@ -207,9 +216,6 @@ public SentryAndroidOptions() { setSentryClientName(BuildConfig.SENTRY_ANDROID_SDK_NAME + "/" + BuildConfig.VERSION_NAME); setSdkVersion(createSdkVersion()); setAttachServerName(false); - - // enable scope sync for Android by default - setEnableScopeSync(true); } private @NotNull SdkVersion createSdkVersion() { @@ -532,6 +538,42 @@ public void setBeforeViewHierarchyCaptureCallback( this.beforeViewHierarchyCaptureCallback = beforeViewHierarchyCaptureCallback; } + /** + * Check if NDK is ON or OFF Default is ON + * + * @return true if ON or false otherwise + */ + public boolean isEnableNdk() { + return enableNdk; + } + + /** + * Sets NDK to ON or OFF + * + * @param enableNdk true if ON or false otherwise + */ + public void setEnableNdk(boolean enableNdk) { + this.enableNdk = enableNdk; + } + + /** + * Returns if the Java to NDK Scope sync is enabled + * + * @return true if enabled or false otherwise + */ + public boolean isEnableScopeSync() { + return enableScopeSync; + } + + /** + * Enables or not the Java to NDK Scope sync + * + * @param enableScopeSync true if enabled or false otherwise + */ + public void setEnableScopeSync(boolean enableScopeSync) { + this.enableScopeSync = enableScopeSync; + } + public boolean isReportHistoricalAnrs() { return reportHistoricalAnrs; } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java index 7ba270351b3..91992b3c5e6 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java @@ -1,5 +1,6 @@ package io.sentry.android.core; +import android.annotation.SuppressLint; import android.app.Activity; import android.app.Application; import android.content.Context; @@ -7,7 +8,10 @@ import android.net.Uri; import android.os.Bundle; import android.os.SystemClock; +import android.view.View; +import io.sentry.NoOpLogger; import io.sentry.SentryDate; +import io.sentry.android.core.internal.util.FirstDrawDoneListener; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -33,8 +37,22 @@ public final class SentryPerformanceProvider extends EmptySecureContentProvider private @Nullable Application application; + private final @NotNull BuildInfoProvider buildInfoProvider; + + private final @NotNull MainLooperHandler mainHandler; + public SentryPerformanceProvider() { AppStartState.getInstance().setAppStartTime(appStartMillis, appStartTime); + buildInfoProvider = new BuildInfoProvider(NoOpLogger.getInstance()); + mainHandler = new MainLooperHandler(); + } + + SentryPerformanceProvider( + final @NotNull BuildInfoProvider buildInfoProvider, + final @NotNull MainLooperHandler mainHandler) { + AppStartState.getInstance().setAppStartTime(appStartMillis, appStartTime); + this.buildInfoProvider = buildInfoProvider; + this.mainHandler = mainHandler; } @Override @@ -100,12 +118,21 @@ public void onActivityCreated(@NotNull Activity activity, @Nullable Bundle saved @Override public void onActivityStarted(@NotNull Activity activity) {} + @SuppressLint("NewApi") @Override public void onActivityResumed(@NotNull Activity activity) { if (!firstActivityResumed) { // sets App start as finished when the very first activity calls onResume firstActivityResumed = true; - AppStartState.getInstance().setAppStartEnd(); + final View rootView = activity.findViewById(android.R.id.content); + if (rootView != null) { + FirstDrawDoneListener.registerForNextDraw( + rootView, () -> AppStartState.getInstance().setAppStartEnd(), buildInfoProvider); + } else { + // Posting a task to the main thread's handler will make it executed after it finished + // its current job. That is, right after the activity draws the layout. + mainHandler.post(() -> AppStartState.getInstance().setAppStartEnd()); + } } if (application != null) { application.unregisterActivityLifecycleCallbacks(this); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java index 26f8e2bfb5c..2d800847385 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java @@ -31,6 +31,7 @@ import static android.content.Intent.ACTION_TIMEZONE_CHANGED; import static android.content.Intent.ACTION_TIME_CHANGED; import static io.sentry.TypeCheckHint.ANDROID_INTENT; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; import android.content.BroadcastReceiver; import android.content.Context; @@ -103,7 +104,7 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio this.options .getLogger() .log(SentryLevel.DEBUG, "SystemEventsBreadcrumbsIntegration installed."); - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } catch (Throwable e) { this.options.setEnableSystemEventBreadcrumbs(false); this.options diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/TempSensorBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/TempSensorBreadcrumbsIntegration.java index e4577b43ecc..66c29ddefff 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/TempSensorBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/TempSensorBreadcrumbsIntegration.java @@ -2,6 +2,7 @@ import static android.content.Context.SENSOR_SERVICE; import static io.sentry.TypeCheckHint.ANDROID_SENSOR_EVENT; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; import android.content.Context; import android.hardware.Sensor; @@ -62,7 +63,7 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio options .getLogger() .log(SentryLevel.DEBUG, "TempSensorBreadcrumbsIntegration installed."); - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } else { this.options .getLogger() diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java index dda56272729..c361529671f 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java @@ -1,5 +1,7 @@ package io.sentry.android.core; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; + import android.app.Activity; import android.app.Application; import android.os.Bundle; @@ -119,7 +121,7 @@ public void register(@NotNull IHub hub, @NotNull SentryOptions options) { if (isAndroidXAvailable) { application.registerActivityLifecycleCallbacks(this); this.options.getLogger().log(SentryLevel.DEBUG, "UserInteractionIntegration installed."); - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } else { options .getLogger() diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java index b667e048884..569e227dc76 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java @@ -1,5 +1,7 @@ package io.sentry.android.core; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; + import android.app.Activity; import android.view.View; import android.view.ViewGroup; @@ -9,7 +11,6 @@ import io.sentry.Hint; import io.sentry.ILogger; import io.sentry.ISerializer; -import io.sentry.IntegrationName; import io.sentry.SentryEvent; import io.sentry.SentryLevel; import io.sentry.android.core.internal.gestures.ViewUtils; @@ -17,6 +18,7 @@ import io.sentry.android.core.internal.util.AndroidMainThreadChecker; import io.sentry.android.core.internal.util.Debouncer; import io.sentry.internal.viewhierarchy.ViewHierarchyExporter; +import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.ViewHierarchy; import io.sentry.protocol.ViewHierarchyNode; import io.sentry.util.HintUtils; @@ -34,7 +36,7 @@ /** ViewHierarchyEventProcessor responsible for taking a snapshot of the current view hierarchy. */ @ApiStatus.Internal -public final class ViewHierarchyEventProcessor implements EventProcessor, IntegrationName { +public final class ViewHierarchyEventProcessor implements EventProcessor { private final @NotNull SentryAndroidOptions options; private final @NotNull Debouncer debouncer; @@ -47,10 +49,19 @@ public ViewHierarchyEventProcessor(final @NotNull SentryAndroidOptions options) this.debouncer = new Debouncer(AndroidCurrentDateProvider.getInstance(), DEBOUNCE_WAIT_TIME_MS); if (options.isAttachViewHierarchy()) { - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } } + @Override + public @NotNull SentryTransaction process( + @NotNull SentryTransaction transaction, @NotNull Hint hint) { + // that's only necessary because on newer versions of Unity, if not overriding this method, it's + // throwing 'java.lang.AbstractMethodError: abstract method' and the reason is probably + // compilation mismatch + return transaction; + } + @Override public @NotNull SentryEvent process(final @NotNull SentryEvent event, final @NotNull Hint hint) { if (!event.isErrored()) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryGestureListener.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryGestureListener.java index e02a2cf0f6f..32a1ffb06a3 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryGestureListener.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryGestureListener.java @@ -32,6 +32,13 @@ @ApiStatus.Internal public final class SentryGestureListener implements GestureDetector.OnGestureListener { + private enum GestureType { + Click, + Scroll, + Swipe, + Unknown + } + static final String UI_ACTION = "ui.action"; private static final String TRACE_ORIGIN = "auto.ui.gesture_listener"; @@ -41,7 +48,7 @@ public final class SentryGestureListener implements GestureDetector.OnGestureLis private @Nullable UiElement activeUiElement = null; private @Nullable ITransaction activeTransaction = null; - private @Nullable String activeEventType = null; + private @NotNull GestureType activeEventType = GestureType.Unknown; private final ScrollState scrollState = new ScrollState(); @@ -61,7 +68,7 @@ public void onUp(final @NotNull MotionEvent motionEvent) { return; } - if (scrollState.type == null) { + if (scrollState.type == GestureType.Unknown) { options .getLogger() .log(SentryLevel.DEBUG, "Unable to define scroll type. No breadcrumb captured."); @@ -107,8 +114,8 @@ public boolean onSingleTapUp(final @Nullable MotionEvent motionEvent) { return false; } - addBreadcrumb(target, "click", Collections.emptyMap(), motionEvent); - startTracing(target, "click"); + addBreadcrumb(target, GestureType.Click, Collections.emptyMap(), motionEvent); + startTracing(target, GestureType.Click); return false; } @@ -123,7 +130,7 @@ public boolean onScroll( return false; } - if (scrollState.type == null) { + if (scrollState.type == GestureType.Unknown) { final @Nullable UiElement target = ViewUtils.findTarget( options, decorView, firstEvent.getX(), firstEvent.getY(), UiElement.Type.SCROLLABLE); @@ -140,7 +147,7 @@ public boolean onScroll( } scrollState.setTarget(target); - scrollState.type = "scroll"; + scrollState.type = GestureType.Scroll; } return false; } @@ -151,7 +158,7 @@ public boolean onFling( final @Nullable MotionEvent motionEvent1, final float v, final float v1) { - scrollState.type = "swipe"; + scrollState.type = GestureType.Swipe; return false; } @@ -164,7 +171,7 @@ public void onLongPress(MotionEvent motionEvent) {} // region utils private void addBreadcrumb( final @NotNull UiElement target, - final @NotNull String eventType, + final @NotNull GestureType eventType, final @NotNull Map additionalData, final @NotNull MotionEvent motionEvent) { @@ -172,24 +179,29 @@ private void addBreadcrumb( return; } + final String type = getGestureType(eventType); + final Hint hint = new Hint(); hint.set(ANDROID_MOTION_EVENT, motionEvent); hint.set(ANDROID_VIEW, target.getView()); hub.addBreadcrumb( Breadcrumb.userInteraction( - eventType, - target.getResourceName(), - target.getClassName(), - target.getTag(), - additionalData), + type, target.getResourceName(), target.getClassName(), target.getTag(), additionalData), hint); } - private void startTracing(final @NotNull UiElement target, final @NotNull String eventType) { - final UiElement uiElement = activeUiElement; + private void startTracing(final @NotNull UiElement target, final @NotNull GestureType eventType) { + + final boolean isNewGestureSameAsActive = + (eventType == activeEventType && target.equals(activeUiElement)); + final boolean isClickGesture = eventType == GestureType.Click; + // we always want to start new transaction/traces for clicks, for swipe/scroll only if the + // target changed + final boolean isNewInteraction = isClickGesture || !isNewGestureSameAsActive; + if (!(options.isTracingEnabled() && options.isEnableUserInteractionTracing())) { - if (!(target.equals(uiElement) && eventType.equals(activeEventType))) { + if (isNewInteraction) { TracingUtils.startNewTrace(hub); activeUiElement = target; activeEventType = eventType; @@ -206,9 +218,7 @@ private void startTracing(final @NotNull UiElement target, final @NotNull String final @Nullable String viewIdentifier = target.getIdentifier(); if (activeTransaction != null) { - if (target.equals(uiElement) - && eventType.equals(activeEventType) - && !activeTransaction.isFinished()) { + if (!isNewInteraction && !activeTransaction.isFinished()) { options .getLogger() .log( @@ -233,10 +243,12 @@ private void startTracing(final @NotNull UiElement target, final @NotNull String // we can only bind to the scope if there's no running transaction final String name = getActivityName(activity) + "." + viewIdentifier; - final String op = UI_ACTION + "." + eventType; + final String op = UI_ACTION + "." + getGestureType(eventType); final TransactionOptions transactionOptions = new TransactionOptions(); transactionOptions.setWaitForChildren(true); + transactionOptions.setDeadlineTimeout( + TransactionOptions.DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION); transactionOptions.setIdleTimeout(options.getIdleTimeout()); transactionOptions.setTrimEnd(true); @@ -258,17 +270,25 @@ private void startTracing(final @NotNull UiElement target, final @NotNull String void stopTracing(final @NotNull SpanStatus status) { if (activeTransaction != null) { - activeTransaction.finish(status); + final SpanStatus currentStatus = activeTransaction.getStatus(); + // status might be set by other integrations, let's not overwrite it + if (currentStatus == null) { + activeTransaction.finish(status); + } else { + activeTransaction.finish(); + } } hub.configureScope( scope -> { + // avoid method refs on Android due to some issues with older AGP setups + // noinspection Convert2MethodRef clearScope(scope); }); activeTransaction = null; if (activeUiElement != null) { activeUiElement = null; } - activeEventType = null; + activeEventType = GestureType.Unknown; } @VisibleForTesting @@ -329,11 +349,32 @@ void applyScope(final @NotNull Scope scope, final @NotNull ITransaction transact } return decorView; } + + @NotNull + private static String getGestureType(final @NotNull GestureType eventType) { + final @NotNull String type; + switch (eventType) { + case Click: + type = "click"; + break; + case Scroll: + type = "scroll"; + break; + case Swipe: + type = "swipe"; + break; + default: + case Unknown: + type = "unknown"; + break; + } + return type; + } // endregion // region scroll logic private static final class ScrollState { - private @Nullable String type = null; + private @NotNull GestureType type = GestureType.Unknown; private @Nullable UiElement target; private float startX = 0f; private float startY = 0f; @@ -370,7 +411,7 @@ private void setTarget(final @NotNull UiElement target) { private void reset() { target = null; - type = null; + type = GestureType.Unknown; startX = 0f; startY = 0f; } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ConnectivityChecker.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java similarity index 73% rename from sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ConnectivityChecker.java rename to sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java index 113ad55120d..30aeea685f2 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ConnectivityChecker.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java @@ -7,12 +7,17 @@ import android.net.Network; import android.net.NetworkCapabilities; import android.os.Build; +import androidx.annotation.NonNull; +import io.sentry.IConnectionStatusProvider; import io.sentry.ILogger; import io.sentry.SentryLevel; import io.sentry.android.core.BuildInfoProvider; +import java.util.HashMap; +import java.util.Map; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; /** * Note: ConnectivityManager sometimes throws SecurityExceptions on Android 11. Hence all relevant @@ -20,27 +25,29 @@ * details */ @ApiStatus.Internal -public final class ConnectivityChecker { +public final class AndroidConnectionStatusProvider implements IConnectionStatusProvider { - public enum Status { - CONNECTED, - NOT_CONNECTED, - NO_PERMISSION, - UNKNOWN - } + private final @NotNull Context context; + private final @NotNull ILogger logger; + private final @NotNull BuildInfoProvider buildInfoProvider; + private final @NotNull Map + registeredCallbacks; - private ConnectivityChecker() {} + public AndroidConnectionStatusProvider( + @NotNull Context context, + @NotNull ILogger logger, + @NotNull BuildInfoProvider buildInfoProvider) { + this.context = context; + this.logger = logger; + this.buildInfoProvider = buildInfoProvider; + this.registeredCallbacks = new HashMap<>(); + } - /** - * Return the Connection status - * - * @return the ConnectionStatus - */ - public static @NotNull ConnectivityChecker.Status getConnectionStatus( - final @NotNull Context context, final @NotNull ILogger logger) { + @Override + public @NotNull ConnectionStatus getConnectionStatus() { final ConnectivityManager connectivityManager = getConnectivityManager(context, logger); if (connectivityManager == null) { - return Status.UNKNOWN; + return ConnectionStatus.UNKNOWN; } return getConnectionStatus(context, connectivityManager, logger); // getActiveNetworkInfo might return null if VPN doesn't specify its @@ -50,6 +57,55 @@ private ConnectivityChecker() {} // connectivityManager.registerDefaultNetworkCallback(...) } + @Override + public @Nullable String getConnectionType() { + return getConnectionType(context, logger, buildInfoProvider); + } + + @SuppressLint("NewApi") // we have an if-check for that down below + @Override + public boolean addConnectionStatusObserver(final @NotNull IConnectionStatusObserver observer) { + if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP) { + logger.log(SentryLevel.DEBUG, "addConnectionStatusObserver requires Android 5+."); + return false; + } + + final ConnectivityManager.NetworkCallback callback = + new ConnectivityManager.NetworkCallback() { + @Override + public void onAvailable(@NonNull Network network) { + observer.onConnectionStatusChanged(getConnectionStatus()); + } + + @Override + public void onLosing(@NonNull Network network, int maxMsToLive) { + observer.onConnectionStatusChanged(getConnectionStatus()); + } + + @Override + public void onLost(@NonNull Network network) { + observer.onConnectionStatusChanged(getConnectionStatus()); + } + + @Override + public void onUnavailable() { + observer.onConnectionStatusChanged(getConnectionStatus()); + } + }; + + registeredCallbacks.put(observer, callback); + return registerNetworkCallback(context, logger, buildInfoProvider, callback); + } + + @Override + public void removeConnectionStatusObserver(final @NotNull IConnectionStatusObserver observer) { + final @Nullable ConnectivityManager.NetworkCallback callback = + registeredCallbacks.remove(observer); + if (callback != null) { + unregisterNetworkCallback(context, logger, buildInfoProvider, callback); + } + } + /** * Return the Connection status * @@ -59,25 +115,27 @@ private ConnectivityChecker() {} * @return true if connected or no permission to check, false otherwise */ @SuppressWarnings({"deprecation", "MissingPermission"}) - private static @NotNull ConnectivityChecker.Status getConnectionStatus( + private static @NotNull IConnectionStatusProvider.ConnectionStatus getConnectionStatus( final @NotNull Context context, final @NotNull ConnectivityManager connectivityManager, final @NotNull ILogger logger) { if (!Permissions.hasPermission(context, Manifest.permission.ACCESS_NETWORK_STATE)) { logger.log(SentryLevel.INFO, "No permission (ACCESS_NETWORK_STATE) to check network status."); - return Status.NO_PERMISSION; + return ConnectionStatus.NO_PERMISSION; } try { final android.net.NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo(); if (activeNetworkInfo == null) { logger.log(SentryLevel.INFO, "NetworkInfo is null, there's no active network."); - return Status.NOT_CONNECTED; + return ConnectionStatus.DISCONNECTED; } - return activeNetworkInfo.isConnected() ? Status.CONNECTED : Status.NOT_CONNECTED; + return activeNetworkInfo.isConnected() + ? ConnectionStatus.CONNECTED + : ConnectionStatus.DISCONNECTED; } catch (Throwable t) { logger.log(SentryLevel.ERROR, "Could not retrieve Connection Status", t); - return Status.UNKNOWN; + return ConnectionStatus.UNKNOWN; } } @@ -273,4 +331,11 @@ public static void unregisterNetworkCallback( logger.log(SentryLevel.ERROR, "unregisterNetworkCallback failed", t); } } + + @TestOnly + @NotNull + public Map + getRegisteredCallbacks() { + return registeredCallbacks; + } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidMainThreadChecker.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidMainThreadChecker.java index a893fa87d16..aa54790c472 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidMainThreadChecker.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidMainThreadChecker.java @@ -1,8 +1,10 @@ package io.sentry.android.core.internal.util; import android.os.Looper; +import io.sentry.protocol.SentryThread; import io.sentry.util.thread.IMainThreadChecker; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; /** Class that checks if a given thread is the Android Main/UI thread */ @ApiStatus.Internal @@ -20,4 +22,20 @@ private AndroidMainThreadChecker() {} public boolean isMainThread(final long threadId) { return Looper.getMainLooper().getThread().getId() == threadId; } + + @Override + public boolean isMainThread(final @NotNull Thread thread) { + return isMainThread(thread.getId()); + } + + @Override + public boolean isMainThread() { + return isMainThread(Thread.currentThread()); + } + + @Override + public boolean isMainThread(final @NotNull SentryThread sentryThread) { + final Long threadId = sentryThread.getId(); + return threadId != null && isMainThread(threadId); + } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FirstDrawDoneListener.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FirstDrawDoneListener.java index 10c160377b2..11978c7bec5 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FirstDrawDoneListener.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FirstDrawDoneListener.java @@ -1,12 +1,10 @@ package io.sentry.android.core.internal.util; -import android.annotation.SuppressLint; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.view.View; import android.view.ViewTreeObserver; -import androidx.annotation.RequiresApi; import io.sentry.android.core.BuildInfoProvider; import java.util.concurrent.atomic.AtomicReference; import org.jetbrains.annotations.NotNull; @@ -19,8 +17,6 @@ * href="https://github.com/firebase/firebase-android-sdk/blob/master/firebase-perf/src/main/java/com/google/firebase/perf/util/FirstDrawDoneListener.java">Firebase * under the Apache License, Version 2.0. */ -@SuppressLint("ObsoleteSdkInt") -@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) public class FirstDrawDoneListener implements ViewTreeObserver.OnDrawListener { private final @NotNull Handler mainThreadHandler = new Handler(Looper.getMainLooper()); private final @NotNull AtomicReference viewReference; @@ -35,8 +31,8 @@ public static void registerForNextDraw( // Handle bug prior to API 26 where OnDrawListener from the floating ViewTreeObserver is not // merged into the real ViewTreeObserver. // https://android.googlesource.com/platform/frameworks/base/+/9f8ec54244a5e0343b9748db3329733f259604f3 - if (buildInfoProvider.getSdkInfoVersion() < 26 - && !isAliveAndAttached(view, buildInfoProvider)) { + if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.O + && !isAliveAndAttached(view)) { view.addOnAttachStateChangeListener( new View.OnAttachStateChangeListener() { @Override @@ -83,17 +79,7 @@ public void onDraw() { * @return true if the View is already attached and the ViewTreeObserver is not a floating * placeholder. */ - private static boolean isAliveAndAttached( - final @NotNull View view, final @NotNull BuildInfoProvider buildInfoProvider) { - return view.getViewTreeObserver().isAlive() && isAttachedToWindow(view, buildInfoProvider); - } - - @SuppressLint("NewApi") - private static boolean isAttachedToWindow( - final @NotNull View view, final @NotNull BuildInfoProvider buildInfoProvider) { - if (buildInfoProvider.getSdkInfoVersion() >= 19) { - return view.isAttachedToWindow(); - } - return view.getWindowToken() != null; + private static boolean isAliveAndAttached(final @NotNull View view) { + return view.getViewTreeObserver().isAlive() && view.isAttachedToWindow(); } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java index 88f3e126dee..35cfaa002f2 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java @@ -1,10 +1,8 @@ package io.sentry.android.core.internal.util; -import android.annotation.SuppressLint; import android.app.Activity; import android.graphics.Bitmap; import android.graphics.Canvas; -import android.os.Build; import android.view.View; import androidx.annotation.Nullable; import io.sentry.ILogger; @@ -35,8 +33,10 @@ public class ScreenshotUtils { final @NotNull IMainThreadChecker mainThreadChecker, final @NotNull ILogger logger, final @NotNull BuildInfoProvider buildInfoProvider) { + // We are keeping BuildInfoProvider param for compatibility, as it's being used by + // cross-platform SDKs - if (!isActivityValid(activity, buildInfoProvider) + if (!isActivityValid(activity) || activity.getWindow() == null || activity.getWindow().getDecorView() == null || activity.getWindow().getDecorView().getRootView() == null) { @@ -91,13 +91,7 @@ public class ScreenshotUtils { return null; } - @SuppressLint("NewApi") - private static boolean isActivityValid( - final @NotNull Activity activity, final @NotNull BuildInfoProvider buildInfoProvider) { - if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - return !activity.isFinishing() && !activity.isDestroyed(); - } else { - return !activity.isFinishing(); - } + private static boolean isActivityValid(final @NotNull Activity activity) { + return !activity.isFinishing() && !activity.isDestroyed(); } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt index a07f6692d60..d74cb6d090f 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt @@ -4,6 +4,7 @@ import android.app.Activity import android.app.ActivityManager import android.app.ActivityManager.RunningAppProcessInfo import android.app.Application +import android.os.Build import android.os.Bundle import android.os.Looper import android.view.View @@ -43,7 +44,6 @@ import org.mockito.kotlin.clearInvocations 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 org.robolectric.Shadows.shadowOf @@ -84,7 +84,7 @@ class ActivityLifecycleIntegrationTest { val buildInfo = mock() fun getSut( - apiVersion: Int = 29, + apiVersion: Int = Build.VERSION_CODES.Q, importance: Int = RunningAppProcessInfo.IMPORTANCE_FOREGROUND, initializer: Sentry.OptionsConfiguration? = null ): ActivityLifecycleIntegration { @@ -354,7 +354,7 @@ class ActivityLifecycleIntegrationTest { } @Test - fun `Transaction op is ui_load`() { + fun `Transaction op is ui_load and idle+deadline timeouts are set`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) @@ -365,11 +365,14 @@ class ActivityLifecycleIntegrationTest { sut.onActivityCreated(activity, fixture.bundle) verify(fixture.hub).startTransaction( - check { + check { assertEquals("ui.load", it.operation) assertEquals(TransactionNameSource.COMPONENT, it.transactionNameSource) }, - any() + check { transactionOptions -> + assertEquals(fixture.options.idleTimeout, transactionOptions.idleTimeout) + assertEquals(TransactionOptions.DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION, transactionOptions.deadlineTimeout) + } ) } @@ -716,7 +719,7 @@ class ActivityLifecycleIntegrationTest { @Test fun `do not stop transaction on resumed if API less than 29 and ttid and ttfd are finished`() { - val sut = fixture.getSut(14) + val sut = fixture.getSut(Build.VERSION_CODES.P) fixture.options.tracesSampleRate = 1.0 fixture.options.isEnableTimeToFullDisplayTracing = true sut.register(fixture.hub, fixture.options) @@ -732,7 +735,7 @@ class ActivityLifecycleIntegrationTest { @Test fun `start transaction on created if API less than 29`() { - val sut = fixture.getSut(14) + val sut = fixture.getSut(Build.VERSION_CODES.P) fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) @@ -780,7 +783,7 @@ class ActivityLifecycleIntegrationTest { @Test fun `App start is Cold when savedInstanceState is null`() { - val sut = fixture.getSut(14) + val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) @@ -792,7 +795,7 @@ class ActivityLifecycleIntegrationTest { @Test fun `App start is Warm when savedInstanceState is not null`() { - val sut = fixture.getSut(14) + val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) @@ -805,7 +808,7 @@ class ActivityLifecycleIntegrationTest { @Test fun `Do not overwrite App start type after set`() { - val sut = fixture.getSut(14) + val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) @@ -889,13 +892,17 @@ class ActivityLifecycleIntegrationTest { AppStartState.getInstance().setColdStart(false) // when activity is created + val view = fixture.createView() val activity = mock() + whenever(activity.findViewById(any())).thenReturn(view) sut.onActivityCreated(activity, fixture.bundle) // then app-start end time should still be null assertNull(AppStartState.getInstance().appStartEndTime) // when activity is resumed sut.onActivityResumed(activity) + Thread.sleep(1) + runFirstDraw(view) // end-time should be set assertNotNull(AppStartState.getInstance().appStartEndTime) } @@ -936,10 +943,14 @@ class ActivityLifecycleIntegrationTest { AppStartState.getInstance().setColdStart(false) // when activity is created, started and resumed multiple times + val view = fixture.createView() val activity = mock() + whenever(activity.findViewById(any())).thenReturn(view) sut.onActivityCreated(activity, fixture.bundle) sut.onActivityStarted(activity) sut.onActivityResumed(activity) + Thread.sleep(1) + runFirstDraw(view) val firstAppStartEndTime = AppStartState.getInstance().appStartEndTime @@ -948,6 +959,8 @@ class ActivityLifecycleIntegrationTest { sut.onActivityStopped(activity) sut.onActivityStarted(activity) sut.onActivityResumed(activity) + Thread.sleep(1) + runFirstDraw(view) // then the end time should not be overwritten assertEquals( @@ -1452,7 +1465,7 @@ class ActivityLifecycleIntegrationTest { val activity = mock() sut.onActivityCreated(activity, fixture.bundle) - fixture.transaction.forceFinish(OK, false) + fixture.transaction.forceFinish(OK, false, null) verify(fixture.activityFramesTracker).setMetrics(activity, fixture.transaction.eventId) } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ConnectivityCheckerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidConnectionStatusProviderTest.kt similarity index 60% rename from sentry-android-core/src/test/java/io/sentry/android/core/ConnectivityCheckerTest.kt rename to sentry-android-core/src/test/java/io/sentry/android/core/AndroidConnectionStatusProviderTest.kt index ca2698ccd55..359fee49cc0 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ConnectivityCheckerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidConnectionStatusProviderTest.kt @@ -14,11 +14,13 @@ import android.net.NetworkCapabilities.TRANSPORT_ETHERNET import android.net.NetworkCapabilities.TRANSPORT_WIFI import android.net.NetworkInfo import android.os.Build -import io.sentry.android.core.internal.util.ConnectivityChecker +import io.sentry.IConnectionStatusProvider +import io.sentry.android.core.internal.util.AndroidConnectionStatusProvider import org.mockito.kotlin.any 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 kotlin.test.BeforeTest @@ -28,8 +30,9 @@ import kotlin.test.assertFalse import kotlin.test.assertNull import kotlin.test.assertTrue -class ConnectivityCheckerTest { +class AndroidConnectionStatusProviderTest { + private lateinit var connectionStatusProvider: AndroidConnectionStatusProvider private lateinit var contextMock: Context private lateinit var connectivityManager: ConnectivityManager private lateinit var networkInfo: NetworkInfo @@ -54,24 +57,26 @@ class ConnectivityCheckerTest { networkCapabilities = mock() whenever(connectivityManager.getNetworkCapabilities(any())).thenReturn(networkCapabilities) + + connectionStatusProvider = AndroidConnectionStatusProvider(contextMock, mock(), buildInfo) } @Test fun `When network is active and connected with permission, return CONNECTED for isConnected`() { whenever(networkInfo.isConnected).thenReturn(true) assertEquals( - ConnectivityChecker.Status.CONNECTED, - ConnectivityChecker.getConnectionStatus(contextMock, mock()) + IConnectionStatusProvider.ConnectionStatus.CONNECTED, + connectionStatusProvider.connectionStatus ) } @Test - fun `When network is active but not connected with permission, return NOT_CONNECTED for isConnected`() { + fun `When network is active but not connected with permission, return DISCONNECTED for isConnected`() { whenever(networkInfo.isConnected).thenReturn(false) assertEquals( - ConnectivityChecker.Status.NOT_CONNECTED, - ConnectivityChecker.getConnectionStatus(contextMock, mock()) + IConnectionStatusProvider.ConnectionStatus.DISCONNECTED, + connectionStatusProvider.connectionStatus ) } @@ -80,72 +85,73 @@ class ConnectivityCheckerTest { whenever(contextMock.checkPermission(any(), any(), any())).thenReturn(PERMISSION_DENIED) assertEquals( - ConnectivityChecker.Status.NO_PERMISSION, - ConnectivityChecker.getConnectionStatus(contextMock, mock()) + IConnectionStatusProvider.ConnectionStatus.NO_PERMISSION, + connectionStatusProvider.connectionStatus ) } @Test - fun `When network is not active, return NOT_CONNECTED for isConnected`() { + fun `When network is not active, return DISCONNECTED for isConnected`() { assertEquals( - ConnectivityChecker.Status.NOT_CONNECTED, - ConnectivityChecker.getConnectionStatus(contextMock, mock()) + IConnectionStatusProvider.ConnectionStatus.DISCONNECTED, + connectionStatusProvider.connectionStatus ) } @Test fun `When ConnectivityManager is not available, return UNKNOWN for isConnected`() { + whenever(contextMock.getSystemService(any())).thenReturn(null) assertEquals( - ConnectivityChecker.Status.UNKNOWN, - ConnectivityChecker.getConnectionStatus(mock(), mock()) + IConnectionStatusProvider.ConnectionStatus.UNKNOWN, + connectionStatusProvider.connectionStatus ) } @Test fun `When ConnectivityManager is not available, return null for getConnectionType`() { - assertNull(ConnectivityChecker.getConnectionType(mock(), mock(), buildInfo)) + assertNull(AndroidConnectionStatusProvider.getConnectionType(mock(), mock(), buildInfo)) } @Test fun `When sdkInfoVersion is not min Marshmallow, return null for getConnectionType`() { val buildInfo = mock() - whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.KITKAT) - assertNull(ConnectivityChecker.getConnectionType(mock(), mock(), buildInfo)) + assertNull(AndroidConnectionStatusProvider.getConnectionType(mock(), mock(), buildInfo)) } @Test fun `When there's no permission, return null for getConnectionType`() { whenever(contextMock.checkPermission(any(), any(), any())).thenReturn(PERMISSION_DENIED) - assertNull(ConnectivityChecker.getConnectionType(contextMock, mock(), buildInfo)) + assertNull(AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo)) } @Test fun `When network is not active, return null for getConnectionType`() { whenever(contextMock.getSystemService(any())).thenReturn(connectivityManager) - assertNull(ConnectivityChecker.getConnectionType(contextMock, mock(), buildInfo)) + assertNull(AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo)) } @Test fun `When network capabilities are not available, return null for getConnectionType`() { - assertNull(ConnectivityChecker.getConnectionType(contextMock, mock(), buildInfo)) + assertNull(AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo)) } @Test fun `When network capabilities has TRANSPORT_WIFI, return wifi`() { whenever(networkCapabilities.hasTransport(eq(TRANSPORT_WIFI))).thenReturn(true) - assertEquals("wifi", ConnectivityChecker.getConnectionType(contextMock, mock(), buildInfo)) + assertEquals("wifi", AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo)) } @Test fun `When network is TYPE_WIFI, return wifi`() { - whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.KITKAT) whenever(networkInfo.type).thenReturn(TYPE_WIFI) - assertEquals("wifi", ConnectivityChecker.getConnectionType(contextMock, mock(), buildInfo)) + assertEquals("wifi", AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo)) } @Test @@ -154,18 +160,18 @@ class ConnectivityCheckerTest { assertEquals( "ethernet", - ConnectivityChecker.getConnectionType(contextMock, mock(), buildInfo) + AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo) ) } @Test fun `When network is TYPE_ETHERNET, return ethernet`() { - whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.KITKAT) whenever(networkInfo.type).thenReturn(TYPE_ETHERNET) assertEquals( "ethernet", - ConnectivityChecker.getConnectionType(contextMock, mock(), buildInfo) + AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo) ) } @@ -175,18 +181,18 @@ class ConnectivityCheckerTest { assertEquals( "cellular", - ConnectivityChecker.getConnectionType(contextMock, mock(), buildInfo) + AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo) ) } @Test fun `When network is TYPE_MOBILE, return cellular`() { - whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.KITKAT) whenever(networkInfo.type).thenReturn(TYPE_MOBILE) assertEquals( "cellular", - ConnectivityChecker.getConnectionType(contextMock, mock(), buildInfo) + AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo) ) } @@ -197,7 +203,7 @@ class ConnectivityCheckerTest { whenever(contextMock.getSystemService(any())).thenReturn(connectivityManager) whenever(contextMock.checkPermission(any(), any(), any())).thenReturn(PERMISSION_DENIED) val registered = - ConnectivityChecker.registerNetworkCallback(contextMock, mock(), buildInfo, mock()) + AndroidConnectionStatusProvider.registerNetworkCallback(contextMock, mock(), buildInfo, mock()) assertFalse(registered) verify(connectivityManager, never()).registerDefaultNetworkCallback(any()) @@ -207,7 +213,7 @@ class ConnectivityCheckerTest { fun `When sdkInfoVersion is not min N, do not register any NetworkCallback`() { whenever(contextMock.getSystemService(any())).thenReturn(connectivityManager) val registered = - ConnectivityChecker.registerNetworkCallback(contextMock, mock(), buildInfo, mock()) + AndroidConnectionStatusProvider.registerNetworkCallback(contextMock, mock(), buildInfo, mock()) assertFalse(registered) verify(connectivityManager, never()).registerDefaultNetworkCallback(any()) @@ -219,7 +225,7 @@ class ConnectivityCheckerTest { whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.N) whenever(contextMock.getSystemService(any())).thenReturn(connectivityManager) val registered = - ConnectivityChecker.registerNetworkCallback(contextMock, mock(), buildInfo, mock()) + AndroidConnectionStatusProvider.registerNetworkCallback(contextMock, mock(), buildInfo, mock()) assertTrue(registered) verify(connectivityManager).registerDefaultNetworkCallback(any()) @@ -230,7 +236,7 @@ class ConnectivityCheckerTest { val buildInfo = mock() whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.KITKAT) whenever(contextMock.getSystemService(any())).thenReturn(connectivityManager) - ConnectivityChecker.unregisterNetworkCallback(contextMock, mock(), buildInfo, mock()) + AndroidConnectionStatusProvider.unregisterNetworkCallback(contextMock, mock(), buildInfo, mock()) verify(connectivityManager, never()).unregisterNetworkCallback(any()) } @@ -238,7 +244,7 @@ class ConnectivityCheckerTest { @Test fun `unregisterNetworkCallback calls connectivityManager unregisterDefaultNetworkCallback`() { whenever(contextMock.getSystemService(any())).thenReturn(connectivityManager) - ConnectivityChecker.unregisterNetworkCallback(contextMock, mock(), buildInfo, mock()) + AndroidConnectionStatusProvider.unregisterNetworkCallback(contextMock, mock(), buildInfo, mock()) verify(connectivityManager).unregisterNetworkCallback(any()) } @@ -248,7 +254,7 @@ class ConnectivityCheckerTest { whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.S) whenever(connectivityManager.activeNetwork).thenThrow(SecurityException("Android OS Bug")) - assertNull(ConnectivityChecker.getConnectionType(contextMock, mock(), buildInfo)) + assertNull(AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo)) } @Test @@ -256,8 +262,8 @@ class ConnectivityCheckerTest { whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.KITKAT) whenever(connectivityManager.activeNetworkInfo).thenThrow(SecurityException("Android OS Bug")) - assertNull(ConnectivityChecker.getConnectionType(contextMock, mock(), buildInfo)) - assertEquals(ConnectivityChecker.Status.UNKNOWN, ConnectivityChecker.getConnectionStatus(contextMock, mock())) + assertNull(AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo)) + assertEquals(IConnectionStatusProvider.ConnectionStatus.UNKNOWN, connectionStatusProvider.connectionStatus) } @Test @@ -266,7 +272,7 @@ class ConnectivityCheckerTest { SecurityException("Android OS Bug") ) assertFalse( - ConnectivityChecker.registerNetworkCallback( + AndroidConnectionStatusProvider.registerNetworkCallback( contextMock, mock(), buildInfo, @@ -283,10 +289,58 @@ class ConnectivityCheckerTest { var failed = false try { - ConnectivityChecker.unregisterNetworkCallback(contextMock, mock(), buildInfo, mock()) + AndroidConnectionStatusProvider.unregisterNetworkCallback(contextMock, mock(), buildInfo, mock()) } catch (t: Throwable) { failed = true } assertFalse(failed) } + + @Test + fun `connectionStatus returns NO_PERMISSIONS when context does not hold the permission`() { + whenever(contextMock.checkPermission(any(), any(), any())).thenReturn(PERMISSION_DENIED) + assertEquals(IConnectionStatusProvider.ConnectionStatus.NO_PERMISSION, connectionStatusProvider.connectionStatus) + } + + @Test + fun `connectionStatus returns ethernet when underlying mechanism provides ethernet`() { + whenever(networkCapabilities.hasTransport(eq(TRANSPORT_ETHERNET))).thenReturn(true) + assertEquals( + "ethernet", + connectionStatusProvider.connectionType + ) + } + + @Test + fun `adding and removing an observer works correctly`() { + whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.N) + + val observer = IConnectionStatusProvider.IConnectionStatusObserver { } + val addResult = connectionStatusProvider.addConnectionStatusObserver(observer) + assertTrue(addResult) + + connectionStatusProvider.removeConnectionStatusObserver(observer) + assertTrue(connectionStatusProvider.registeredCallbacks.isEmpty()) + } + + @Test + fun `underlying callbacks correctly trigger update`() { + whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.N) + + var callback: NetworkCallback? = null + whenever(connectivityManager.registerDefaultNetworkCallback(any())).then { invocation -> + callback = invocation.getArgument(0, NetworkCallback::class.java) + Unit + } + val observer = mock() + connectionStatusProvider.addConnectionStatusObserver(observer) + callback!!.onAvailable(mock()) + callback!!.onUnavailable() + callback!!.onLosing(mock(), 0) + callback!!.onLost(mock()) + callback!!.onUnavailable() + connectionStatusProvider.removeConnectionStatusObserver(observer) + + verify(observer, times(5)).onConnectionStatusChanged(any()) + } } 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 7f48dcd7ef8..1b147b106ae 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 @@ -2,6 +2,7 @@ package io.sentry.android.core import android.content.Context import android.content.res.AssetManager +import android.os.Build import android.os.Bundle import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -95,7 +96,7 @@ class AndroidOptionsInitializerTest { } fun initSutWithClassLoader( - minApi: Int = 16, + minApi: Int = Build.VERSION_CODES.KITKAT, classesToLoad: List = emptyList(), isFragmentAvailable: Boolean = false, isTimberAvailable: Boolean = false @@ -137,7 +138,7 @@ class AndroidOptionsInitializerTest { ) } - private fun createBuildInfo(minApi: Int = 16): BuildInfoProvider { + private fun createBuildInfo(minApi: Int): BuildInfoProvider { val buildInfo = mock() whenever(buildInfo.sdkInfoVersion).thenReturn(minApi) return buildInfo @@ -174,6 +175,20 @@ class AndroidOptionsInitializerTest { assertTrue(innerLogger.get(loggerField) is AndroidLogger) } + @Test + fun `flush timeout is set to Android specific default value`() { + fixture.initSut() + assertEquals(AndroidOptionsInitializer.DEFAULT_FLUSH_TIMEOUT_MS, fixture.sentryOptions.flushTimeoutMillis) + } + + @Test + fun `flush timeout can be overridden`() { + fixture.initSut(configureOptions = { + flushTimeoutMillis = 1234 + }) + assertEquals(1234, fixture.sentryOptions.flushTimeoutMillis) + } + @Test fun `AndroidEventProcessor added to processors list`() { fixture.initSut() @@ -335,14 +350,6 @@ class AndroidOptionsInitializerTest { assertNotNull((actual as NdkIntegration).sentryNdkClass) } - @Test - fun `NdkIntegration won't be enabled because API is lower than 16`() { - fixture.initSutWithClassLoader(minApi = 14, classesToLoad = listOfNotNull(NdkIntegration.SENTRY_NDK_CLASS_NAME)) - - val actual = fixture.sentryOptions.integrations.firstOrNull { it is NdkIntegration } - assertNull((actual as NdkIntegration).sentryNdkClass) - } - @Test fun `NdkIntegration won't be enabled, if class not found`() { fixture.initSutWithClassLoader(classesToLoad = emptyList()) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransportGateTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransportGateTest.kt index 1b1ee683184..d31118728f2 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransportGateTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransportGateTest.kt @@ -1,7 +1,6 @@ package io.sentry.android.core -import io.sentry.android.core.internal.util.ConnectivityChecker -import org.mockito.kotlin.mock +import io.sentry.IConnectionStatusProvider import kotlin.test.Test import kotlin.test.assertFalse import kotlin.test.assertNotNull @@ -11,7 +10,7 @@ class AndroidTransportGateTest { private class Fixture { fun getSut(): AndroidTransportGate { - return AndroidTransportGate(mock(), mock()) + return AndroidTransportGate(SentryAndroidOptions()) } } private val fixture = Fixture() @@ -23,21 +22,21 @@ class AndroidTransportGateTest { @Test fun `isConnected returns true if connection was not found`() { - assertTrue(fixture.getSut().isConnected(ConnectivityChecker.Status.UNKNOWN)) + assertTrue(fixture.getSut().isConnected(IConnectionStatusProvider.ConnectionStatus.UNKNOWN)) } @Test fun `isConnected returns true if connection is connected`() { - assertTrue(fixture.getSut().isConnected(ConnectivityChecker.Status.CONNECTED)) + assertTrue(fixture.getSut().isConnected(IConnectionStatusProvider.ConnectionStatus.CONNECTED)) } @Test fun `isConnected returns false if connection is not connected`() { - assertFalse(fixture.getSut().isConnected(ConnectivityChecker.Status.NOT_CONNECTED)) + assertFalse(fixture.getSut().isConnected(IConnectionStatusProvider.ConnectionStatus.DISCONNECTED)) } @Test fun `isConnected returns false if no permission`() { - assertTrue(fixture.getSut().isConnected(ConnectivityChecker.Status.NO_PERMISSION)) + assertTrue(fixture.getSut().isConnected(IConnectionStatusProvider.ConnectionStatus.NO_PERMISSION)) } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt index 69598a0e1e6..a792a5b3c58 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt @@ -3,6 +3,7 @@ package io.sentry.android.core import android.app.ActivityManager import android.app.ActivityManager.MemoryInfo import android.content.Context +import android.os.Build import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.Breadcrumb @@ -84,7 +85,7 @@ class AnrV2EventProcessorTest { fun getSut( dir: TemporaryFolder, - currentSdk: Int = 21, + currentSdk: Int = Build.VERSION_CODES.LOLLIPOP, populateScopeCache: Boolean = false, populateOptionsCache: Boolean = false ): AnrV2EventProcessor { @@ -406,6 +407,10 @@ class AnrV2EventProcessorTest { debugMeta = DebugMeta().apply { images = listOf(DebugImage().apply { type = DebugImage.PROGUARD; uuid = "uuid1" }) } + user = User().apply { + id = "42" + ipAddress = "2.4.8.16" + } } assertEquals("NotAndroid", processed.platform) @@ -427,6 +432,9 @@ class AnrV2EventProcessorTest { assertEquals(2, processed.debugMeta!!.images!!.size) assertEquals("uuid1", processed.debugMeta!!.images!![0].uuid) assertEquals("uuid", processed.debugMeta!!.images!![1].uuid) + + assertEquals("42", processed.user!!.id) + assertEquals("2.4.8.16", processed.user!!.ipAddress) } @Test @@ -543,6 +551,8 @@ class AnrV2EventProcessorTest { internal class AbnormalExitHint(val mechanism: String? = null) : AbnormalExit, Backfillable { override fun mechanism(): String? = mechanism + override fun ignoreCurrentThread(): Boolean = false + override fun timestamp(): Long? = null override fun shouldEnrich(): Boolean = true } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AppStartStateTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AppStartStateTest.kt index 421274b42a6..29cb7c0d7e2 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AppStartStateTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AppStartStateTest.kt @@ -1,6 +1,7 @@ package io.sentry.android.core import io.sentry.SentryInstantDate +import io.sentry.SentryLongDate import io.sentry.SentryNanotimeDate import java.util.Date import kotlin.test.BeforeTest @@ -58,6 +59,18 @@ class AppStartStateTest { assertSame(date, sut.appStartTime) } + @Test + fun `do not overwrite app start end time if already set`() { + val sut = AppStartState.getInstance() + + sut.setColdStart(true) + sut.setAppStartTime(1, SentryLongDate(1000000)) + sut.setAppStartEnd(2) + sut.setAppStartEnd(3) + + assertEquals(0, SentryLongDate(2000000).compareTo(sut.appStartEndTime!!)) + } + @Test fun `do not overwrite cold start value if already set`() { val sut = AppStartState.getInstance() diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt index 514826ff967..70b20e2ab2a 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt @@ -133,18 +133,6 @@ class DefaultAndroidEventProcessorTest { } } - @Test - fun `when Android version is below JELLY_BEAN, does not add permissions`() { - whenever(fixture.buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.ICE_CREAM_SANDWICH) - val sut = fixture.getSut(context) - - assertNotNull(sut.process(SentryEvent(), Hint())) { - // assert adds permissions - val unknown = it.contexts.app!!.permissions - assertNull(unknown) - } - } - @Test fun `When Transaction and hint is not Cached, data should be applied`() { val sut = fixture.getSut(context) @@ -292,6 +280,31 @@ class DefaultAndroidEventProcessorTest { } } + @Test + fun `when event user data does not have ip address set, sets {{auto}} as the ip address`() { + val sut = fixture.getSut(context) + val event = SentryEvent().apply { + user = User() + } + sut.process(event, Hint()) + assertNotNull(event.user) { + assertEquals("{{auto}}", it.ipAddress) + } + } + + @Test + fun `when event has ip address set, keeps original ip address`() { + val sut = fixture.getSut(context) + val event = SentryEvent() + event.user = User().apply { + ipAddress = "192.168.0.1" + } + sut.process(event, Hint()) + assertNotNull(event.user) { + assertEquals("192.168.0.1", it.ipAddress) + } + } + @Test fun `Processor won't throw exception`() { val sut = fixture.getSut(context) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt index f10594cf0b7..c2ffb9489ee 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt @@ -23,6 +23,7 @@ import io.sentry.protocol.Mechanism import io.sentry.protocol.SentryId import io.sentry.protocol.User import io.sentry.transport.ITransport +import io.sentry.transport.RateLimiter import org.junit.runner.RunWith import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -63,6 +64,10 @@ class InternalSentrySdkTest { override fun flush(timeoutMillis: Long) { // no-op } + + override fun getRateLimiter(): RateLimiter? { + return null + } } } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SendCachedEnvelopeIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SendCachedEnvelopeIntegrationTest.kt index 36094f78ebb..df7e8632553 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SendCachedEnvelopeIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SendCachedEnvelopeIntegrationTest.kt @@ -1,16 +1,21 @@ package io.sentry.android.core +import io.sentry.IConnectionStatusProvider +import io.sentry.IConnectionStatusProvider.ConnectionStatus import io.sentry.IHub import io.sentry.ILogger import io.sentry.SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForget import io.sentry.SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetFactory import io.sentry.SentryLevel.DEBUG +import io.sentry.test.ImmediateExecutorService +import io.sentry.transport.RateLimiter import io.sentry.util.LazyEvaluator import org.awaitility.kotlin.await import org.mockito.kotlin.any 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.ExecutionException @@ -32,8 +37,12 @@ class SendCachedEnvelopeIntegrationTest { hasStartupCrashMarker: Boolean = false, hasSender: Boolean = true, delaySend: Long = 0L, - taskFails: Boolean = false + taskFails: Boolean = false, + useImmediateExecutor: Boolean = false ): SendCachedEnvelopeIntegration { + if (useImmediateExecutor) { + options.executorService = ImmediateExecutorService() + } options.cacheDirPath = cacheDirPath options.setLogger(logger) options.isDebug = true @@ -117,4 +126,90 @@ class SendCachedEnvelopeIntegrationTest { await.untilFalse(fixture.flag) verify(fixture.sender).send() } + + @Test + fun `registers for network connection changes`() { + val sut = fixture.getSut(hasStartupCrashMarker = false) + + val connectionStatusProvider = mock() + fixture.options.connectionStatusProvider = connectionStatusProvider + + sut.register(fixture.hub, fixture.options) + verify(connectionStatusProvider).addConnectionStatusObserver(any()) + } + + @Test + fun `when theres no network connection does nothing`() { + val sut = fixture.getSut(hasStartupCrashMarker = false) + + val connectionStatusProvider = mock() + fixture.options.connectionStatusProvider = connectionStatusProvider + + whenever(connectionStatusProvider.connectionStatus).thenReturn( + ConnectionStatus.DISCONNECTED + ) + + sut.register(fixture.hub, fixture.options) + verify(fixture.sender, never()).send() + } + + @Test + fun `when the network is not disconnected the factory is initialized`() { + val sut = fixture.getSut(hasStartupCrashMarker = false) + + val connectionStatusProvider = mock() + fixture.options.connectionStatusProvider = connectionStatusProvider + + whenever(connectionStatusProvider.connectionStatus).thenReturn( + ConnectionStatus.UNKNOWN + ) + + sut.register(fixture.hub, fixture.options) + verify(fixture.factory).create(any(), any()) + } + + @Test + fun `whenever network connection status changes, retries sending for relevant statuses`() { + val sut = fixture.getSut(hasStartupCrashMarker = false, useImmediateExecutor = true) + + val connectionStatusProvider = mock() + fixture.options.connectionStatusProvider = connectionStatusProvider + whenever(connectionStatusProvider.connectionStatus).thenReturn( + ConnectionStatus.DISCONNECTED + ) + sut.register(fixture.hub, fixture.options) + + // when there's no connection no factory create call should be done + verify(fixture.sender, never()).send() + + // but for any other status processing should be triggered + // CONNECTED + whenever(connectionStatusProvider.connectionStatus).thenReturn(ConnectionStatus.CONNECTED) + sut.onConnectionStatusChanged(ConnectionStatus.CONNECTED) + verify(fixture.sender).send() + + // UNKNOWN + whenever(connectionStatusProvider.connectionStatus).thenReturn(ConnectionStatus.UNKNOWN) + sut.onConnectionStatusChanged(ConnectionStatus.UNKNOWN) + verify(fixture.sender, times(2)).send() + + // NO_PERMISSION + whenever(connectionStatusProvider.connectionStatus).thenReturn(ConnectionStatus.NO_PERMISSION) + sut.onConnectionStatusChanged(ConnectionStatus.NO_PERMISSION) + verify(fixture.sender, times(3)).send() + } + + @Test + fun `when rate limiter is active, does not send envelopes`() { + val sut = fixture.getSut(hasStartupCrashMarker = false) + val rateLimiter = mock { + whenever(mock.isActiveForCategory(any())).thenReturn(true) + } + whenever(fixture.hub.rateLimiter).thenReturn(rateLimiter) + + sut.register(fixture.hub, fixture.options) + + // no factory call should be done if there's rate limiting active + verify(fixture.sender, never()).send() + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt index 07e16af252a..044380d6bea 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt @@ -133,6 +133,19 @@ class SentryAndroidOptionsTest { assertNull(sentryOptions.nativeSdkName) } + @Test + fun `when options is initialized, enableScopeSync is enabled by default`() { + assertTrue(SentryAndroidOptions().isEnableScopeSync) + } + + @Test + fun `enableScopeSync can be properly disabled`() { + val options = SentryAndroidOptions() + options.isEnableScopeSync = false + + assertFalse(options.isEnableScopeSync) + } + private class CustomDebugImagesLoader : IDebugImagesLoader { override fun loadDebugImages(): List? = null override fun clearDebugImages() {} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt index cf3ca7c2eab..aa9d9cc26bf 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt @@ -1,14 +1,22 @@ package io.sentry.android.core +import android.app.Activity import android.app.Application import android.content.pm.ProviderInfo +import android.os.Build import android.os.Bundle +import android.os.Looper +import android.view.View +import android.view.ViewTreeObserver +import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.SentryNanotimeDate import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.robolectric.Shadows import java.util.Date import kotlin.test.BeforeTest import kotlin.test.Test @@ -84,11 +92,21 @@ class SentryPerformanceProviderTest { val mockContext = ContextUtilsTest.createMockContext(true) providerInfo.authority = AUTHORITY - val provider = SentryPerformanceProvider() + val provider = SentryPerformanceProvider( + mock { + whenever(mock.sdkInfoVersion).thenReturn(Build.VERSION_CODES.Q) + }, + MainLooperHandler() + ) provider.attachInfo(mockContext, providerInfo) - provider.onActivityCreated(mock(), Bundle()) - provider.onActivityResumed(mock()) + val view = createView() + val activity = mock() + whenever(activity.findViewById(any())).thenReturn(view) + provider.onActivityCreated(activity, Bundle()) + provider.onActivityResumed(activity) + Thread.sleep(1) + runFirstDraw(view) assertNotNull(AppStartState.getInstance().appStartInterval) assertNotNull(AppStartState.getInstance().appStartEndTime) @@ -97,6 +115,24 @@ class SentryPerformanceProviderTest { .unregisterActivityLifecycleCallbacks(any()) } + private fun createView(): View { + val view = View(ApplicationProvider.getApplicationContext()) + + // Adding a listener forces ViewTreeObserver.mOnDrawListeners to be initialized and non-null. + val dummyListener = ViewTreeObserver.OnDrawListener {} + view.viewTreeObserver.addOnDrawListener(dummyListener) + view.viewTreeObserver.removeOnDrawListener(dummyListener) + + return view + } + + private fun runFirstDraw(view: View) { + // Removes OnDrawListener in the next OnGlobalLayout after onDraw + view.viewTreeObserver.dispatchOnDraw() + view.viewTreeObserver.dispatchOnGlobalLayout() + Shadows.shadowOf(Looper.getMainLooper()).idle() + } + companion object { private const val AUTHORITY = "io.sentry.sample.SentryPerformanceProvider" } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt index 73f445940e0..a25e7411c75 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt @@ -22,6 +22,7 @@ import io.sentry.TraceContext import io.sentry.UserFeedback import io.sentry.protocol.SentryId import io.sentry.protocol.SentryTransaction +import io.sentry.transport.RateLimiter import org.junit.runner.RunWith import org.mockito.kotlin.mock import org.robolectric.annotation.Config @@ -167,5 +168,9 @@ class SessionTrackingIntegrationTest { override fun captureCheckIn(checkIn: CheckIn, scope: Scope?, hint: Hint?): SentryId { TODO("Not yet implemented") } + + override fun getRateLimiter(): RateLimiter? { + TODO("Not yet implemented") + } } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt index e128687903b..308632c6ed6 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt @@ -16,6 +16,7 @@ import io.sentry.SentryTracer import io.sentry.SpanContext import io.sentry.SpanId import io.sentry.SpanStatus +import io.sentry.SpanStatus.OUT_OF_RANGE import io.sentry.TransactionContext import io.sentry.TransactionOptions import io.sentry.android.core.SentryAndroidOptions @@ -27,6 +28,7 @@ import org.mockito.kotlin.clearInvocations import org.mockito.kotlin.doAnswer 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 kotlin.test.Test @@ -205,6 +207,24 @@ class SentryGestureListenerTracingTest { ) } + @Test + fun `captures transaction and both idle+deadline timeouts are set`() { + val sut = fixture.getSut() + + sut.onSingleTapUp(fixture.event) + + verify(fixture.hub).startTransaction( + any(), + check { transactionOptions -> + assertEquals(fixture.options.idleTimeout, transactionOptions.idleTimeout) + assertEquals( + TransactionOptions.DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION, + transactionOptions.deadlineTimeout + ) + } + ) + } + @Test fun `captures transaction with interaction event type as op`() { val sut = fixture.getSut() @@ -314,29 +334,38 @@ class SentryGestureListenerTracingTest { SpanContext(SentryId.EMPTY_ID, SpanId.EMPTY_ID, "op", null, null) ) + // when the same button is clicked twice + sut.onSingleTapUp(fixture.event) sut.onSingleTapUp(fixture.event) - verify(fixture.hub).startTransaction( + // then two transaction should be captured + verify(fixture.hub, times(2)).startTransaction( check { assertEquals("Activity.test_button", it.name) assertEquals(TransactionNameSource.COMPONENT, it.transactionNameSource) }, any() ) + } + + @Test + fun `captures transaction and sets trace origin`() { + val sut = fixture.getSut() - // second view interaction sut.onSingleTapUp(fixture.event) - verify(fixture.transaction).scheduleFinish() + assertEquals("auto.ui.gesture_listener.old_view_system", fixture.transaction.spanContext.origin) } @Test - fun `captures transaction and sets trace origin`() { + fun `preserves existing transaction status`() { val sut = fixture.getSut() sut.onSingleTapUp(fixture.event) - assertEquals("auto.ui.gesture_listener.old_view_system", fixture.transaction.spanContext.origin) + fixture.transaction.status = OUT_OF_RANGE + sut.stopTracing(SpanStatus.CANCELLED) + assertEquals(OUT_OF_RANGE, fixture.transaction.status) } internal open class ScrollableListView : AbsListView(mock()) { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/FirstDrawDoneListenerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/FirstDrawDoneListenerTest.kt index 07fc383e1de..a7b4cc3f8c8 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/FirstDrawDoneListenerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/FirstDrawDoneListenerTest.kt @@ -1,6 +1,7 @@ package io.sentry.android.core.internal.util import android.content.Context +import android.os.Build import android.os.Handler import android.os.Looper import android.view.View @@ -31,7 +32,7 @@ class FirstDrawDoneListenerTest { val buildInfo = mock() lateinit var onDrawListeners: ArrayList - fun getSut(apiVersion: Int = 26): View { + fun getSut(apiVersion: Int = Build.VERSION_CODES.O): View { whenever(buildInfo.sdkInfoVersion).thenReturn(apiVersion) val view = View(application) @@ -52,7 +53,7 @@ class FirstDrawDoneListenerTest { @Test fun `registerForNextDraw adds listener on attach state changed on sdk 25-`() { - val view = fixture.getSut(25) + val view = fixture.getSut(Build.VERSION_CODES.N_MR1) // OnDrawListener is not registered, it is delayed for later FirstDrawDoneListener.registerForNextDraw(view, {}, fixture.buildInfo) diff --git a/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleIntegration.kt b/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleIntegration.kt index 81f0e7e5052..4129ea43566 100644 --- a/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleIntegration.kt +++ b/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleIntegration.kt @@ -10,6 +10,7 @@ import io.sentry.Integration import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryLevel.DEBUG import io.sentry.SentryOptions +import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion import java.io.Closeable class FragmentLifecycleIntegration( @@ -48,7 +49,7 @@ class FragmentLifecycleIntegration( application.registerActivityLifecycleCallbacks(this) options.logger.log(DEBUG, "FragmentLifecycleIntegration installed.") - addIntegrationToSdkVersion() + addIntegrationToSdkVersion(javaClass) SentryIntegrationPackageStorage.getInstance() .addPackage("maven:io.sentry:sentry-android-fragment", BuildConfig.VERSION_NAME) } diff --git a/sentry-android-navigation/api/sentry-android-navigation.api b/sentry-android-navigation/api/sentry-android-navigation.api index 61a98025bff..79151bb3fb4 100644 --- a/sentry-android-navigation/api/sentry-android-navigation.api +++ b/sentry-android-navigation/api/sentry-android-navigation.api @@ -6,7 +6,7 @@ public final class io/sentry/android/navigation/BuildConfig { public fun ()V } -public final class io/sentry/android/navigation/SentryNavigationListener : androidx/navigation/NavController$OnDestinationChangedListener, io/sentry/IntegrationName { +public final class io/sentry/android/navigation/SentryNavigationListener : androidx/navigation/NavController$OnDestinationChangedListener { public static final field Companion Lio/sentry/android/navigation/SentryNavigationListener$Companion; public static final field NAVIGATION_OP Ljava/lang/String; public fun ()V diff --git a/sentry-android-navigation/src/main/java/io/sentry/android/navigation/SentryNavigationListener.kt b/sentry-android-navigation/src/main/java/io/sentry/android/navigation/SentryNavigationListener.kt index dae348bef52..8fdf8b0df88 100644 --- a/sentry-android-navigation/src/main/java/io/sentry/android/navigation/SentryNavigationListener.kt +++ b/sentry-android-navigation/src/main/java/io/sentry/android/navigation/SentryNavigationListener.kt @@ -9,7 +9,6 @@ import io.sentry.Hint import io.sentry.HubAdapter import io.sentry.IHub import io.sentry.ITransaction -import io.sentry.IntegrationName import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryLevel.DEBUG import io.sentry.SentryLevel.INFO @@ -19,6 +18,7 @@ import io.sentry.TransactionContext import io.sentry.TransactionOptions import io.sentry.TypeCheckHint import io.sentry.protocol.TransactionNameSource +import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion import io.sentry.util.TracingUtils import java.lang.ref.WeakReference @@ -38,7 +38,7 @@ class SentryNavigationListener @JvmOverloads constructor( private val enableNavigationBreadcrumbs: Boolean = true, private val enableNavigationTracing: Boolean = true, private val traceOriginAppendix: String? = null -) : NavController.OnDestinationChangedListener, IntegrationName { +) : NavController.OnDestinationChangedListener { private var previousDestinationRef: WeakReference? = null private var previousArgs: Bundle? = null @@ -48,7 +48,7 @@ class SentryNavigationListener @JvmOverloads constructor( private var activeTransaction: ITransaction? = null init { - addIntegrationToSdkVersion() + addIntegrationToSdkVersion(javaClass) SentryIntegrationPackageStorage.getInstance() .addPackage("maven:io.sentry:sentry-android-navigation", BuildConfig.VERSION_NAME) } @@ -132,15 +132,16 @@ class SentryNavigationListener @JvmOverloads constructor( // we add '/' to the name to match dart and web pattern name = "/" + name.substringBefore('/') // strip out arguments from the tx name - val transactonOptions = TransactionOptions().also { + val transactionOptions = TransactionOptions().also { it.isWaitForChildren = true it.idleTimeout = hub.options.idleTimeout + it.deadlineTimeout = TransactionOptions.DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION it.isTrimEnd = true } val transaction = hub.startTransaction( TransactionContext(name, TransactionNameSource.ROUTE, NAVIGATION_OP), - transactonOptions + transactionOptions ) transaction.spanContext.origin = traceOriginAppendix?.let { diff --git a/sentry-android-navigation/src/test/java/io/sentry/android/navigation/SentryNavigationListenerTest.kt b/sentry-android-navigation/src/test/java/io/sentry/android/navigation/SentryNavigationListenerTest.kt index 0d7441c549d..e1c899d136f 100644 --- a/sentry-android-navigation/src/test/java/io/sentry/android/navigation/SentryNavigationListenerTest.kt +++ b/sentry-android-navigation/src/test/java/io/sentry/android/navigation/SentryNavigationListenerTest.kt @@ -94,7 +94,12 @@ class SentryNavigationListenerTest { whenever(context.resources).thenReturn(resources) whenever(navController.context).thenReturn(context) whenever(destination.route).thenReturn(toRoute) - return SentryNavigationListener(hub, enableBreadcrumbs, enableTracing, traceOriginAppendix) + return SentryNavigationListener( + hub, + enableBreadcrumbs, + enableTracing, + traceOriginAppendix + ) } } @@ -355,7 +360,8 @@ class SentryNavigationListenerTest { fun `starts new trace if performance is disabled`() { val sut = fixture.getSut(enableTracing = false) - val argumentCaptor: ArgumentCaptor = ArgumentCaptor.forClass(ScopeCallback::class.java) + val argumentCaptor: ArgumentCaptor = + ArgumentCaptor.forClass(ScopeCallback::class.java) val scope = Scope(fixture.options) val propagationContextAtStart = scope.propagationContext whenever(fixture.hub.configureScope(argumentCaptor.capture())).thenAnswer { @@ -385,4 +391,18 @@ class SentryNavigationListenerTest { assertEquals("auto.navigation.jetpack_compose", fixture.transaction.spanContext.origin) } + + @Test + fun `Navigation listener transactions set automatic deadline timeout`() { + val sut = fixture.getSut() + + sut.onDestinationChanged(fixture.navController, fixture.destination, null) + + verify(fixture.hub).startTransaction( + any(), + check { options -> + assertEquals(TransactionOptions.DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION, options.deadlineTimeout) + } + ) + } } diff --git a/sentry-android-ndk/api/sentry-android-ndk.api b/sentry-android-ndk/api/sentry-android-ndk.api index afd7baf890e..30e9cbb7b68 100644 --- a/sentry-android-ndk/api/sentry-android-ndk.api +++ b/sentry-android-ndk/api/sentry-android-ndk.api @@ -6,7 +6,7 @@ public final class io/sentry/android/ndk/BuildConfig { public fun ()V } -public final class io/sentry/android/ndk/NdkScopeObserver : io/sentry/IScopeObserver { +public final class io/sentry/android/ndk/NdkScopeObserver : io/sentry/ScopeObserverAdapter { public fun (Lio/sentry/SentryOptions;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun removeExtra (Ljava/lang/String;)V diff --git a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NdkScopeObserver.java b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NdkScopeObserver.java index ebc4c9bc47d..009bba9b811 100644 --- a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NdkScopeObserver.java +++ b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NdkScopeObserver.java @@ -2,7 +2,7 @@ import io.sentry.Breadcrumb; import io.sentry.DateUtils; -import io.sentry.IScopeObserver; +import io.sentry.ScopeObserverAdapter; import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.protocol.User; @@ -14,7 +14,7 @@ import org.jetbrains.annotations.Nullable; @ApiStatus.Internal -public final class NdkScopeObserver implements IScopeObserver { +public final class NdkScopeObserver extends ScopeObserverAdapter { private final @NotNull SentryOptions options; private final @NotNull INativeScope nativeScope; diff --git a/sentry-android-okhttp/api/sentry-android-okhttp.api b/sentry-android-okhttp/api/sentry-android-okhttp.api index 11c140061e9..a96e3787a61 100644 --- a/sentry-android-okhttp/api/sentry-android-okhttp.api +++ b/sentry-android-okhttp/api/sentry-android-okhttp.api @@ -46,7 +46,7 @@ public final class io/sentry/android/okhttp/SentryOkHttpEventListener : okhttp3/ public final class io/sentry/android/okhttp/SentryOkHttpEventListener$Companion { } -public final class io/sentry/android/okhttp/SentryOkHttpInterceptor : io/sentry/IntegrationName, okhttp3/Interceptor { +public final class io/sentry/android/okhttp/SentryOkHttpInterceptor : okhttp3/Interceptor { public fun ()V public fun (Lio/sentry/IHub;)V public fun (Lio/sentry/IHub;Lio/sentry/android/okhttp/SentryOkHttpInterceptor$BeforeSpanCallback;ZLjava/util/List;Ljava/util/List;)V diff --git a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEvent.kt b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEvent.kt index 8a3414b6633..99139b261cc 100644 --- a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEvent.kt +++ b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEvent.kt @@ -14,6 +14,7 @@ import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.REQUEST_HEAD import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_BODY_EVENT import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_HEADERS_EVENT import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.SECURE_CONNECT_EVENT +import io.sentry.util.Platform import io.sentry.util.UrlUtils import okhttp3.Request import okhttp3.Response @@ -38,7 +39,8 @@ internal class SentryOkHttpEvent(private val hub: IHub, private val request: Req val method: String = request.method // We start the call span that will contain all the others - callRootSpan = hub.span?.startChild("http.client", "$method $url") + val parentSpan = if (Platform.isAndroid()) hub.transaction else hub.span + callRootSpan = parentSpan?.startChild("http.client", "$method $url") callRootSpan?.spanContext?.origin = TRACE_ORIGIN urlDetails.applyToSpan(callRootSpan) diff --git a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt index a5815b163be..4bda5a9c817 100644 --- a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt +++ b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt @@ -7,7 +7,6 @@ import io.sentry.HttpStatusCodeRange import io.sentry.HubAdapter import io.sentry.IHub import io.sentry.ISpan -import io.sentry.IntegrationName import io.sentry.SentryEvent import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS @@ -19,6 +18,8 @@ import io.sentry.exception.ExceptionMechanismException import io.sentry.exception.SentryHttpClientException import io.sentry.protocol.Mechanism import io.sentry.util.HttpUtils +import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion +import io.sentry.util.Platform import io.sentry.util.PropagationTargetsUtils import io.sentry.util.TracingUtils import io.sentry.util.UrlUtils @@ -45,19 +46,19 @@ import java.io.IOException class SentryOkHttpInterceptor( private val hub: IHub = HubAdapter.getInstance(), private val beforeSpan: BeforeSpanCallback? = null, - private val captureFailedRequests: Boolean = false, + private val captureFailedRequests: Boolean = true, private val failedRequestStatusCodes: List = listOf( HttpStatusCodeRange(HttpStatusCodeRange.DEFAULT_MIN, HttpStatusCodeRange.DEFAULT_MAX) ), private val failedRequestTargets: List = listOf(DEFAULT_PROPAGATION_TARGETS) -) : Interceptor, IntegrationName { +) : Interceptor { constructor() : this(HubAdapter.getInstance()) constructor(hub: IHub) : this(hub, null) constructor(beforeSpan: BeforeSpanCallback) : this(HubAdapter.getInstance(), beforeSpan) init { - addIntegrationToSdkVersion() + addIntegrationToSdkVersion(javaClass) SentryIntegrationPackageStorage.getInstance() .addPackage("maven:io.sentry:sentry-android-okhttp", BuildConfig.VERSION_NAME) } @@ -78,7 +79,8 @@ class SentryOkHttpInterceptor( isFromEventListener = true } else { // read the span from the bound scope - span = hub.span?.startChild("http.client", "$method $url") + val parentSpan = if (Platform.isAndroid()) hub.transaction else hub.span + span = parentSpan?.startChild("http.client", "$method $url") isFromEventListener = false } diff --git a/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpInterceptorTest.kt b/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpInterceptorTest.kt index c989fef0772..3bf8816542d 100644 --- a/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpInterceptorTest.kt +++ b/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpInterceptorTest.kt @@ -61,7 +61,7 @@ class SentryOkHttpInterceptorTest { beforeSpan: SentryOkHttpInterceptor.BeforeSpanCallback? = null, includeMockServerInTracePropagationTargets: Boolean = true, keepDefaultTracePropagationTargets: Boolean = false, - captureFailedRequests: Boolean = false, + captureFailedRequests: Boolean? = false, failedRequestTargets: List = listOf(".*"), failedRequestStatusCodes: List = listOf( HttpStatusCodeRange( @@ -97,13 +97,22 @@ class SentryOkHttpInterceptorTest { .setResponseCode(httpStatusCode) ) - val interceptor = SentryOkHttpInterceptor( - hub, - beforeSpan, - captureFailedRequests = captureFailedRequests, - failedRequestTargets = failedRequestTargets, - failedRequestStatusCodes = failedRequestStatusCodes - ) + val interceptor = when (captureFailedRequests) { + null -> SentryOkHttpInterceptor( + hub, + beforeSpan, + failedRequestTargets = failedRequestTargets, + failedRequestStatusCodes = failedRequestStatusCodes + ) + + else -> SentryOkHttpInterceptor( + hub, + beforeSpan, + captureFailedRequests = captureFailedRequests, + failedRequestTargets = failedRequestTargets, + failedRequestStatusCodes = failedRequestStatusCodes + ) + } return OkHttpClient.Builder().addInterceptor(interceptor).build() } } @@ -366,6 +375,17 @@ class SentryOkHttpInterceptorTest { } } + @Test + fun `captures failed requests by default`() { + val sut = fixture.getSut( + httpStatusCode = 500, + captureFailedRequests = null + ) + sut.newCall(getRequest()).execute() + + verify(fixture.hub).captureEvent(any(), any()) + } + @Test fun `captures an event if captureFailedRequests is enabled and within the range`() { val sut = fixture.getSut( diff --git a/sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/SentrySupportSQLiteDatabase.kt b/sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/SentrySupportSQLiteDatabase.kt index 4c944fb07da..ac48c1a504e 100644 --- a/sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/SentrySupportSQLiteDatabase.kt +++ b/sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/SentrySupportSQLiteDatabase.kt @@ -3,9 +3,7 @@ package io.sentry.android.sqlite import android.annotation.SuppressLint import android.database.Cursor import android.database.SQLException -import android.os.Build import android.os.CancellationSignal -import androidx.annotation.RequiresApi import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteQuery import androidx.sqlite.db.SupportSQLiteStatement @@ -62,7 +60,6 @@ internal class SentrySupportSQLiteDatabase( } } - @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) override fun query( query: SupportSQLiteQuery, cancellationSignal: CancellationSignal? diff --git a/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberIntegration.kt b/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberIntegration.kt index 6477a2531af..d043faa5f6d 100644 --- a/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberIntegration.kt +++ b/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberIntegration.kt @@ -7,6 +7,7 @@ import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryLevel import io.sentry.SentryOptions import io.sentry.android.timber.BuildConfig.VERSION_NAME +import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion import timber.log.Timber import java.io.Closeable @@ -28,7 +29,7 @@ class SentryTimberIntegration( logger.log(SentryLevel.DEBUG, "SentryTimberIntegration installed.") SentryIntegrationPackageStorage.getInstance().addPackage("maven:io.sentry:sentry-android-timber", VERSION_NAME) - addIntegrationToSdkVersion() + addIntegrationToSdkVersion(javaClass) } override fun close() { diff --git a/sentry-apache-http-client-5/api/sentry-apache-http-client-5.api b/sentry-apache-http-client-5/api/sentry-apache-http-client-5.api index 7d7479c9fd3..9f33a4f115e 100644 --- a/sentry-apache-http-client-5/api/sentry-apache-http-client-5.api +++ b/sentry-apache-http-client-5/api/sentry-apache-http-client-5.api @@ -2,6 +2,7 @@ public final class io/sentry/transport/apache/ApacheHttpClientTransport : io/sen public fun (Lio/sentry/SentryOptions;Lio/sentry/RequestDetails;Lorg/apache/hc/client5/http/impl/async/CloseableHttpAsyncClient;Lio/sentry/transport/RateLimiter;)V public fun close ()V public fun flush (J)V + public fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public fun send (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V } diff --git a/sentry-apache-http-client-5/src/main/java/io/sentry/transport/apache/ApacheHttpClientTransport.java b/sentry-apache-http-client-5/src/main/java/io/sentry/transport/apache/ApacheHttpClientTransport.java index 8ff2aa01bf8..b5a0dc5c9f6 100644 --- a/sentry-apache-http-client-5/src/main/java/io/sentry/transport/apache/ApacheHttpClientTransport.java +++ b/sentry-apache-http-client-5/src/main/java/io/sentry/transport/apache/ApacheHttpClientTransport.java @@ -189,6 +189,11 @@ public void flush(long timeoutMillis) { } } + @Override + public @NotNull RateLimiter getRateLimiter() { + return rateLimiter; + } + @Override public void close() throws IOException { options.getLogger().log(DEBUG, "Shutting down"); diff --git a/sentry-apollo-3/api/sentry-apollo-3.api b/sentry-apollo-3/api/sentry-apollo-3.api index e98f72e45c5..0fa4e717a0d 100644 --- a/sentry-apollo-3/api/sentry-apollo-3.api +++ b/sentry-apollo-3/api/sentry-apollo-3.api @@ -8,7 +8,7 @@ public final class io/sentry/apollo3/SentryApollo3ClientException : java/lang/Ex public final fun getSerialVersionUID ()J } -public final class io/sentry/apollo3/SentryApollo3HttpInterceptor : com/apollographql/apollo3/network/http/HttpInterceptor, io/sentry/IntegrationName { +public final class io/sentry/apollo3/SentryApollo3HttpInterceptor : com/apollographql/apollo3/network/http/HttpInterceptor { public static final field Companion Lio/sentry/apollo3/SentryApollo3HttpInterceptor$Companion; public static final field DEFAULT_CAPTURE_FAILED_REQUESTS Z public static final field SENTRY_APOLLO_3_OPERATION_TYPE Ljava/lang/String; @@ -20,7 +20,6 @@ public final class io/sentry/apollo3/SentryApollo3HttpInterceptor : com/apollogr public fun (Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;ZLjava/util/List;)V public synthetic fun (Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;ZLjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun dispose ()V - public fun getIntegrationName ()Ljava/lang/String; public fun intercept (Lcom/apollographql/apollo3/api/http/HttpRequest;Lcom/apollographql/apollo3/network/http/HttpInterceptorChain;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } diff --git a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt index 2276272fe73..c6aeb8755ae 100644 --- a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt @@ -14,7 +14,6 @@ import io.sentry.Hint import io.sentry.HubAdapter import io.sentry.IHub import io.sentry.ISpan -import io.sentry.IntegrationName import io.sentry.SentryEvent import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryLevel @@ -29,10 +28,12 @@ import io.sentry.protocol.Mechanism import io.sentry.protocol.Request import io.sentry.protocol.Response import io.sentry.util.HttpUtils +import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion import io.sentry.util.PropagationTargetsUtils import io.sentry.util.TracingUtils import io.sentry.util.UrlUtils import io.sentry.vendor.Base64 +import okhttp3.internal.platform.Platform import okio.Buffer import org.jetbrains.annotations.ApiStatus import java.util.Locale @@ -44,10 +45,10 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor( private val beforeSpan: BeforeSpanCallback? = null, private val captureFailedRequests: Boolean = DEFAULT_CAPTURE_FAILED_REQUESTS, private val failedRequestTargets: List = listOf(DEFAULT_PROPAGATION_TARGETS) -) : HttpInterceptor, IntegrationName { +) : HttpInterceptor { init { - addIntegrationToSdkVersion() + addIntegrationToSdkVersion("Apollo3") if (captureFailedRequests) { SentryIntegrationPackageStorage.getInstance() .addIntegration("Apollo3ClientError") @@ -64,7 +65,7 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor( request: HttpRequest, chain: HttpInterceptorChain ): HttpResponse { - val activeSpan = hub.span + val activeSpan = if (io.sentry.util.Platform.isAndroid()) hub.transaction else hub.span val operationName = getHeader(HEADER_APOLLO_OPERATION_NAME, request.headers) val operationType = decodeHeaderValue(request, SENTRY_APOLLO_3_OPERATION_TYPE) @@ -135,10 +136,6 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor( return requestBuilder.build() } - override fun getIntegrationName(): String { - return super.getIntegrationName().replace("Http", "") - } - private fun removeSentryInternalHeaders(headers: List): List { return headers.filterNot { it.name.equals(SENTRY_APOLLO_3_VARIABLES, true) || diff --git a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt index 5b9ab998863..68454efc28d 100644 --- a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt @@ -26,11 +26,13 @@ import io.sentry.TransactionContext import io.sentry.apollo3.SentryApollo3HttpInterceptor.BeforeSpanCallback import io.sentry.protocol.SdkVersion import io.sentry.protocol.SentryTransaction +import io.sentry.util.Apollo3PlatformTestManipulator import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.SocketPolicy +import org.junit.Before import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.check @@ -113,6 +115,11 @@ class SentryApollo3InterceptorTest { private val fixture = Fixture() + @Before + fun setup() { + Apollo3PlatformTestManipulator.pretendIsAndroid(false) + } + @Test fun `creates a span around the successful request`() { executeQuery() @@ -309,6 +316,20 @@ class SentryApollo3InterceptorTest { assert(packageInfo.version == BuildConfig.VERSION_NAME) } + @Test + fun `attaches to root transaction on Android`() { + Apollo3PlatformTestManipulator.pretendIsAndroid(true) + executeQuery(fixture.getSut()) + verify(fixture.hub).transaction + } + + @Test + fun `attaches to child span on non-Android`() { + Apollo3PlatformTestManipulator.pretendIsAndroid(false) + executeQuery(fixture.getSut()) + verify(fixture.hub).span + } + private fun assertTransactionDetails(it: SentryTransaction, httpStatusCode: Int? = 200, contentLength: Long? = 0L) { assertEquals(1, it.spans.size) val httpClientSpan = it.spans.first() @@ -330,6 +351,7 @@ class SentryApollo3InterceptorTest { var tx: ITransaction? = null if (isSpanActive) { tx = SentryTracer(TransactionContext("op", "desc", TracesSamplingDecision(true)), fixture.hub) + whenever(fixture.hub.transaction).thenReturn(tx) whenever(fixture.hub.span).thenReturn(tx) } diff --git a/sentry-apollo-3/src/test/java/io/sentry/util/Apollo3PlatformTestManipulator.kt b/sentry-apollo-3/src/test/java/io/sentry/util/Apollo3PlatformTestManipulator.kt new file mode 100644 index 00000000000..b639cab40e4 --- /dev/null +++ b/sentry-apollo-3/src/test/java/io/sentry/util/Apollo3PlatformTestManipulator.kt @@ -0,0 +1,8 @@ +package io.sentry.util + +object Apollo3PlatformTestManipulator { + + fun pretendIsAndroid(isAndroid: Boolean) { + Platform.isAndroid = isAndroid + } +} diff --git a/sentry-apollo/api/sentry-apollo.api b/sentry-apollo/api/sentry-apollo.api index bf1ab6abed7..8c18bce06eb 100644 --- a/sentry-apollo/api/sentry-apollo.api +++ b/sentry-apollo/api/sentry-apollo.api @@ -3,7 +3,7 @@ public final class io/sentry/apollo/BuildConfig { public static final field VERSION_NAME Ljava/lang/String; } -public final class io/sentry/apollo/SentryApolloInterceptor : com/apollographql/apollo/interceptor/ApolloInterceptor, io/sentry/IntegrationName { +public final class io/sentry/apollo/SentryApolloInterceptor : com/apollographql/apollo/interceptor/ApolloInterceptor { public fun ()V public fun (Lio/sentry/IHub;)V public fun (Lio/sentry/IHub;Lio/sentry/apollo/SentryApolloInterceptor$BeforeSpanCallback;)V diff --git a/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt b/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt index 0a11e1b7626..faa8a549a92 100644 --- a/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt +++ b/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt @@ -18,13 +18,13 @@ import io.sentry.Hint import io.sentry.HubAdapter import io.sentry.IHub import io.sentry.ISpan -import io.sentry.IntegrationName import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryLevel import io.sentry.SpanDataConvention import io.sentry.SpanStatus import io.sentry.TypeCheckHint.APOLLO_REQUEST import io.sentry.TypeCheckHint.APOLLO_RESPONSE +import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion import io.sentry.util.TracingUtils import java.util.Locale import java.util.concurrent.Executor @@ -34,18 +34,18 @@ private const val TRACE_ORIGIN = "auto.graphql.apollo" class SentryApolloInterceptor( private val hub: IHub = HubAdapter.getInstance(), private val beforeSpan: BeforeSpanCallback? = null -) : ApolloInterceptor, IntegrationName { +) : ApolloInterceptor { constructor(hub: IHub) : this(hub, null) constructor(beforeSpan: BeforeSpanCallback) : this(HubAdapter.getInstance(), beforeSpan) init { - addIntegrationToSdkVersion() + addIntegrationToSdkVersion(javaClass) SentryIntegrationPackageStorage.getInstance().addPackage("maven:io.sentry:sentry-apollo", BuildConfig.VERSION_NAME) } override fun interceptAsync(request: InterceptorRequest, chain: ApolloInterceptorChain, dispatcher: Executor, callBack: CallBack) { - val activeSpan = hub.span + val activeSpan = if (io.sentry.util.Platform.isAndroid()) hub.transaction else hub.span if (activeSpan == null) { val headers = addTracingHeaders(request, null) val modifiedRequest = request.toBuilder().requestHeaders(headers).build() @@ -149,7 +149,7 @@ class SentryApolloInterceptor( } private fun finish(span: ISpan, request: InterceptorRequest, response: InterceptorResponse? = null) { - var newSpan: ISpan = span + var newSpan: ISpan? = span if (beforeSpan != null) { try { newSpan = beforeSpan.execute(span, request, response) @@ -157,7 +157,12 @@ class SentryApolloInterceptor( hub.options.logger.log(SentryLevel.ERROR, "An error occurred while executing beforeSpan on ApolloInterceptor", e) } } - newSpan.finish() + if (newSpan == null) { + // span is dropped + span.spanContext.sampled = false + } else { + span.finish() + } response?.let { if (it.httpResponse.isPresent) { @@ -173,9 +178,9 @@ class SentryApolloInterceptor( breadcrumb.setData("response_body_size", contentLength) } - val hint = Hint().also { - it.set(APOLLO_REQUEST, httpRequest) - it.set(APOLLO_RESPONSE, httpResponse) + val hint = Hint().apply { + set(APOLLO_REQUEST, httpRequest) + set(APOLLO_RESPONSE, httpResponse) } hub.addBreadcrumb(breadcrumb, hint) } @@ -199,6 +204,6 @@ class SentryApolloInterceptor( * @param request the HTTP request executed by okHttp * @param response the HTTP response received by okHttp */ - fun execute(span: ISpan, request: InterceptorRequest, response: InterceptorResponse?): ISpan + fun execute(span: ISpan, request: InterceptorRequest, response: InterceptorResponse?): ISpan? } } diff --git a/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt b/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt index f45c310601b..d22c2fd3e58 100644 --- a/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt +++ b/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt @@ -19,11 +19,13 @@ import io.sentry.TracesSamplingDecision import io.sentry.TransactionContext import io.sentry.protocol.SdkVersion import io.sentry.protocol.SentryTransaction +import io.sentry.util.ApolloPlatformTestManipulator import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.SocketPolicy +import org.junit.Before import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.check @@ -35,6 +37,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull +import kotlin.test.assertTrue class SentryApolloInterceptorTest { @@ -92,6 +95,11 @@ class SentryApolloInterceptorTest { private val fixture = Fixture() + @Before + fun setup() { + ApolloPlatformTestManipulator.pretendIsAndroid(false) + } + @Test fun `creates a span around the successful request`() { executeQuery() @@ -180,6 +188,24 @@ class SentryApolloInterceptorTest { ) } + @Test + fun `when beforeSpan callback returns null, span is dropped`() { + executeQuery( + fixture.getSut { _, _, _ -> + null + } + ) + + verify(fixture.hub).captureTransaction( + check { + assertTrue(it.spans.isEmpty()) + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + @Test fun `when customizer throws, exception is handled`() { executeQuery( @@ -218,6 +244,20 @@ class SentryApolloInterceptorTest { assert(packageInfo.version == BuildConfig.VERSION_NAME) } + @Test + fun `attaches to root transaction on Android`() { + ApolloPlatformTestManipulator.pretendIsAndroid(true) + executeQuery(fixture.getSut()) + verify(fixture.hub).transaction + } + + @Test + fun `attaches to child span on non-Android`() { + ApolloPlatformTestManipulator.pretendIsAndroid(false) + executeQuery(fixture.getSut()) + verify(fixture.hub).span + } + private fun assertTransactionDetails(it: SentryTransaction) { assertEquals(1, it.spans.size) val httpClientSpan = it.spans.first() @@ -234,6 +274,7 @@ class SentryApolloInterceptorTest { var tx: ITransaction? = null if (isSpanActive) { tx = SentryTracer(TransactionContext("op", "desc", TracesSamplingDecision(true)), fixture.hub) + whenever(fixture.hub.transaction).thenReturn(tx) whenever(fixture.hub.span).thenReturn(tx) } diff --git a/sentry-apollo/src/test/java/io/sentry/util/ApolloPlatformTestManipulator.kt b/sentry-apollo/src/test/java/io/sentry/util/ApolloPlatformTestManipulator.kt new file mode 100644 index 00000000000..219b95d08a0 --- /dev/null +++ b/sentry-apollo/src/test/java/io/sentry/util/ApolloPlatformTestManipulator.kt @@ -0,0 +1,8 @@ +package io.sentry.util + +object ApolloPlatformTestManipulator { + + fun pretendIsAndroid(isAndroid: Boolean) { + Platform.isAndroid = isAndroid + } +} diff --git a/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryNavigationIntegration.kt b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryNavigationIntegration.kt index ef84b7579d2..4af368f0652 100644 --- a/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryNavigationIntegration.kt +++ b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryNavigationIntegration.kt @@ -13,10 +13,10 @@ import androidx.navigation.NavController import androidx.navigation.NavHostController import io.sentry.Breadcrumb import io.sentry.ITransaction -import io.sentry.IntegrationName import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryOptions import io.sentry.android.navigation.SentryNavigationListener +import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion private const val TRACE_ORIGIN_APPENDIX = "jetpack_compose" @@ -24,10 +24,10 @@ internal class SentryLifecycleObserver( private val navController: NavController, private val navListener: NavController.OnDestinationChangedListener = SentryNavigationListener(traceOriginAppendix = TRACE_ORIGIN_APPENDIX) -) : LifecycleEventObserver, IntegrationName { +) : LifecycleEventObserver { init { - addIntegrationToSdkVersion() + addIntegrationToSdkVersion("ComposeNavigation") SentryIntegrationPackageStorage.getInstance().addPackage("maven:io.sentry:sentry-compose", BuildConfig.VERSION_NAME) } @@ -39,10 +39,6 @@ internal class SentryLifecycleObserver( } } - override fun getIntegrationName(): String { - return "ComposeNavigation" - } - fun dispose() { navController.removeOnDestinationChangedListener(navListener) } diff --git a/sentry-kotlin-extensions/api/sentry-kotlin-extensions.api b/sentry-kotlin-extensions/api/sentry-kotlin-extensions.api index 5cc7b874555..d501240a3ab 100644 --- a/sentry-kotlin-extensions/api/sentry-kotlin-extensions.api +++ b/sentry-kotlin-extensions/api/sentry-kotlin-extensions.api @@ -1,7 +1,11 @@ -public final class io/sentry/kotlin/SentryContext : kotlin/coroutines/AbstractCoroutineContextElement, kotlinx/coroutines/ThreadContextElement { +public final class io/sentry/kotlin/SentryContext : kotlin/coroutines/AbstractCoroutineContextElement, kotlinx/coroutines/CopyableThreadContextElement { public fun ()V + public fun (Lio/sentry/IHub;)V + public synthetic fun (Lio/sentry/IHub;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun copyForChild ()Lkotlinx/coroutines/CopyableThreadContextElement; public fun fold (Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; public fun get (Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; + public fun mergeForChild (Lkotlin/coroutines/CoroutineContext$Element;)Lkotlin/coroutines/CoroutineContext; public fun minusKey (Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext; public fun plus (Lkotlin/coroutines/CoroutineContext;)Lkotlin/coroutines/CoroutineContext; public fun restoreThreadContext (Lkotlin/coroutines/CoroutineContext;Lio/sentry/IHub;)V diff --git a/sentry-kotlin-extensions/src/main/java/io/sentry/kotlin/SentryContext.kt b/sentry-kotlin-extensions/src/main/java/io/sentry/kotlin/SentryContext.kt index cd6042e7481..3cf22a20da3 100644 --- a/sentry-kotlin-extensions/src/main/java/io/sentry/kotlin/SentryContext.kt +++ b/sentry-kotlin-extensions/src/main/java/io/sentry/kotlin/SentryContext.kt @@ -2,18 +2,25 @@ package io.sentry.kotlin import io.sentry.IHub import io.sentry.Sentry -import kotlinx.coroutines.ThreadContextElement +import kotlinx.coroutines.CopyableThreadContextElement import kotlin.coroutines.AbstractCoroutineContextElement import kotlin.coroutines.CoroutineContext /** * Sentry context element for [CoroutineContext]. */ -public class SentryContext : ThreadContextElement, AbstractCoroutineContextElement(Key) { +public class SentryContext(private val hub: IHub = Sentry.getCurrentHub().clone()) : + CopyableThreadContextElement, AbstractCoroutineContextElement(Key) { private companion object Key : CoroutineContext.Key - private val hub: IHub = Sentry.getCurrentHub().clone() + override fun copyForChild(): CopyableThreadContextElement { + return SentryContext(hub.clone()) + } + + override fun mergeForChild(overwritingElement: CoroutineContext.Element): CoroutineContext { + return overwritingElement[Key] ?: SentryContext(hub.clone()) + } override fun updateThreadContext(context: CoroutineContext): IHub { val oldState = Sentry.getCurrentHub() diff --git a/sentry-kotlin-extensions/src/test/java/io/sentry/kotlin/SentryContextTest.kt b/sentry-kotlin-extensions/src/test/java/io/sentry/kotlin/SentryContextTest.kt index 63f14bac55e..b54ceabc511 100644 --- a/sentry-kotlin-extensions/src/test/java/io/sentry/kotlin/SentryContextTest.kt +++ b/sentry-kotlin-extensions/src/test/java/io/sentry/kotlin/SentryContextTest.kt @@ -1,6 +1,7 @@ package io.sentry.kotlin import io.sentry.Sentry +import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -8,6 +9,8 @@ import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull import kotlin.test.assertNull class SentryContextTest { @@ -80,6 +83,90 @@ class SentryContextTest { } } + @Test + fun testContextIsClonedWhenPassedToChild() = runBlocking { + Sentry.setTag("parent", "parentValue") + launch(SentryContext()) { + Sentry.setTag("c1", "c1value") + assertEquals("c1value", getTag("c1")) + assertEquals("parentValue", getTag("parent")) + assertNull(getTag("c2")) + + val c2 = launch() { + Sentry.setTag("c2", "c2value") + assertEquals("c2value", getTag("c2")) + assertEquals("parentValue", getTag("parent")) + assertNotNull(getTag("c1")) + } + + c2.join() + + assertNotNull(getTag("c1")) + assertNull(getTag("c2")) + } + assertNull(getTag("c1")) + assertNull(getTag("c2")) + } + + @Test + fun testExplicitlyPassedContextOverridesPropagatedContext() = runBlocking { + Sentry.setTag("parent", "parentValue") + launch(SentryContext()) { + Sentry.setTag("c1", "c1value") + assertEquals("c1value", getTag("c1")) + assertEquals("parentValue", getTag("parent")) + assertNull(getTag("c2")) + + val c2 = launch( + SentryContext( + Sentry.getCurrentHub().clone().also { + it.setTag("cloned", "clonedValue") + } + ) + ) { + Sentry.setTag("c2", "c2value") + assertEquals("c2value", getTag("c2")) + assertEquals("parentValue", getTag("parent")) + assertNotNull(getTag("c1")) + assertNotNull(getTag("cloned")) + } + + c2.join() + + assertNotNull(getTag("c1")) + assertNull(getTag("c2")) + assertNull(getTag("cloned")) + } + assertNull(getTag("c1")) + assertNull(getTag("c2")) + assertNull(getTag("cloned")) + } + + @Test + fun `mergeForChild returns copy of initial context if Key not present`() { + val initialContextElement = SentryContext( + Sentry.getCurrentHub().clone().also { + it.setTag("cloned", "clonedValue") + } + ) + val mergedContextElement = initialContextElement.mergeForChild(CoroutineName("test")) + + assertNotEquals(initialContextElement, mergedContextElement) + assertNotNull((mergedContextElement)[initialContextElement.key]) + } + + @Test + fun `mergeForChild returns passed context`() { + val initialContextElement = SentryContext( + Sentry.getCurrentHub().clone().also { + it.setTag("cloned", "clonedValue") + } + ) + val mergedContextElement = SentryContext().mergeForChild(initialContextElement) + + assertEquals(initialContextElement, mergedContextElement) + } + private fun getTag(tag: String): String? { var value: String? = null Sentry.configureScope { diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index b629b7993eb..daf5d789ea4 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -290,7 +290,7 @@ public final class io/sentry/EnvelopeReader : io/sentry/IEnvelopeReader { } public final class io/sentry/EnvelopeSender : io/sentry/IEnvelopeSender { - public fun (Lio/sentry/IHub;Lio/sentry/ISerializer;Lio/sentry/ILogger;J)V + public fun (Lio/sentry/IHub;Lio/sentry/ISerializer;Lio/sentry/ILogger;JI)V public synthetic fun processDirectory (Ljava/io/File;)V public fun processEnvelopeFile (Ljava/lang/String;Lio/sentry/Hint;)V } @@ -405,6 +405,7 @@ public final class io/sentry/HttpStatusCodeRange { public final class io/sentry/Hub : io/sentry/IHub { public fun (Lio/sentry/SentryOptions;)V + public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V public fun bindClient (Lio/sentry/ISentryClient;)V public fun captureCheckIn (Lio/sentry/CheckIn;)Lio/sentry/protocol/SentryId; @@ -428,8 +429,10 @@ public final class io/sentry/Hub : io/sentry/IHub { public fun getBaggage ()Lio/sentry/BaggageHeader; public fun getLastEventId ()Lio/sentry/protocol/SentryId; public fun getOptions ()Lio/sentry/SentryOptions; + public fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public fun getSpan ()Lio/sentry/ISpan; public fun getTraceparent ()Lio/sentry/SentryTraceHeader; + public fun getTransaction ()Lio/sentry/ITransaction; public fun isCrashedLastRun ()Ljava/lang/Boolean; public fun isEnabled ()Z public fun popScope ()V @@ -452,6 +455,7 @@ public final class io/sentry/Hub : io/sentry/IHub { } public final class io/sentry/HubAdapter : io/sentry/IHub { + public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V public fun bindClient (Lio/sentry/ISentryClient;)V public fun captureCheckIn (Lio/sentry/CheckIn;)Lio/sentry/protocol/SentryId; @@ -476,8 +480,10 @@ public final class io/sentry/HubAdapter : io/sentry/IHub { public static fun getInstance ()Lio/sentry/HubAdapter; public fun getLastEventId ()Lio/sentry/protocol/SentryId; public fun getOptions ()Lio/sentry/SentryOptions; + public fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public fun getSpan ()Lio/sentry/ISpan; public fun getTraceparent ()Lio/sentry/SentryTraceHeader; + public fun getTransaction ()Lio/sentry/ITransaction; public fun isCrashedLastRun ()Ljava/lang/Boolean; public fun isEnabled ()Z public fun popScope ()V @@ -505,6 +511,26 @@ public abstract interface class io/sentry/ICollector { public abstract fun setup ()V } +public abstract interface class io/sentry/IConnectionStatusProvider { + public abstract fun addConnectionStatusObserver (Lio/sentry/IConnectionStatusProvider$IConnectionStatusObserver;)Z + public abstract fun getConnectionStatus ()Lio/sentry/IConnectionStatusProvider$ConnectionStatus; + public abstract fun getConnectionType ()Ljava/lang/String; + public abstract fun removeConnectionStatusObserver (Lio/sentry/IConnectionStatusProvider$IConnectionStatusObserver;)V +} + +public final class io/sentry/IConnectionStatusProvider$ConnectionStatus : java/lang/Enum { + public static final field CONNECTED Lio/sentry/IConnectionStatusProvider$ConnectionStatus; + public static final field DISCONNECTED Lio/sentry/IConnectionStatusProvider$ConnectionStatus; + public static final field NO_PERMISSION Lio/sentry/IConnectionStatusProvider$ConnectionStatus; + public static final field UNKNOWN Lio/sentry/IConnectionStatusProvider$ConnectionStatus; + public static fun valueOf (Ljava/lang/String;)Lio/sentry/IConnectionStatusProvider$ConnectionStatus; + public static fun values ()[Lio/sentry/IConnectionStatusProvider$ConnectionStatus; +} + +public abstract interface class io/sentry/IConnectionStatusProvider$IConnectionStatusObserver { + public abstract fun onConnectionStatusChanged (Lio/sentry/IConnectionStatusProvider$ConnectionStatus;)V +} + public abstract interface class io/sentry/IEnvelopeReader { public abstract fun read (Ljava/io/InputStream;)Lio/sentry/SentryEnvelope; } @@ -514,7 +540,7 @@ public abstract interface class io/sentry/IEnvelopeSender { } public abstract interface class io/sentry/IHub { - public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V + public abstract fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public abstract fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V public fun addBreadcrumb (Ljava/lang/String;)V public fun addBreadcrumb (Ljava/lang/String;Ljava/lang/String;)V @@ -549,8 +575,10 @@ public abstract interface class io/sentry/IHub { public abstract fun getBaggage ()Lio/sentry/BaggageHeader; public abstract fun getLastEventId ()Lio/sentry/protocol/SentryId; public abstract fun getOptions ()Lio/sentry/SentryOptions; + public abstract fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public abstract fun getSpan ()Lio/sentry/ISpan; public abstract fun getTraceparent ()Lio/sentry/SentryTraceHeader; + public abstract fun getTransaction ()Lio/sentry/ITransaction; public abstract fun isCrashedLastRun ()Ljava/lang/Boolean; public abstract fun isEnabled ()Z public abstract fun popScope ()V @@ -592,30 +620,30 @@ public abstract interface class io/sentry/IMemoryCollector { } 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 fun setDist (Ljava/lang/String;)V + public abstract fun setEnvironment (Ljava/lang/String;)V + public abstract fun setProguardUuid (Ljava/lang/String;)V + public abstract fun setRelease (Ljava/lang/String;)V + public abstract fun setSdkVersion (Lio/sentry/protocol/SdkVersion;)V + public abstract fun setTags (Ljava/util/Map;)V } public abstract interface class io/sentry/IScopeObserver { - 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 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 setBreadcrumbs (Ljava/util/Collection;)V + public abstract fun setContexts (Lio/sentry/protocol/Contexts;)V + public abstract fun setExtra (Ljava/lang/String;Ljava/lang/String;)V + public abstract fun setExtras (Ljava/util/Map;)V + public abstract fun setFingerprint (Ljava/util/Collection;)V + public abstract fun setLevel (Lio/sentry/SentryLevel;)V + public abstract fun setRequest (Lio/sentry/protocol/Request;)V + public abstract fun setTag (Ljava/lang/String;Ljava/lang/String;)V + public abstract fun setTags (Ljava/util/Map;)V + public abstract fun setTrace (Lio/sentry/SpanContext;)V + public abstract fun setTransaction (Ljava/lang/String;)V + public abstract fun setUser (Lio/sentry/protocol/User;)V } public abstract interface class io/sentry/ISentryClient { @@ -642,6 +670,7 @@ public abstract interface class io/sentry/ISentryClient { public abstract fun captureUserFeedback (Lio/sentry/UserFeedback;)V public abstract fun close ()V public abstract fun flush (J)V + public abstract fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public abstract fun isEnabled ()Z } @@ -697,8 +726,8 @@ public abstract interface class io/sentry/ISpan { } public abstract interface class io/sentry/ITransaction : io/sentry/ISpan { - public abstract fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;Z)V - public abstract fun forceFinish (Lio/sentry/SpanStatus;Z)V + public abstract fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;ZLio/sentry/Hint;)V + public abstract fun forceFinish (Lio/sentry/SpanStatus;ZLio/sentry/Hint;)V public abstract fun getContexts ()Lio/sentry/protocol/Contexts; public abstract fun getEventId ()Lio/sentry/protocol/SentryId; public abstract fun getLatestActiveSpan ()Lio/sentry/Span; @@ -732,15 +761,10 @@ public final class io/sentry/Instrumenter : java/lang/Enum { public static fun values ()[Lio/sentry/Instrumenter; } -public abstract interface class io/sentry/Integration : io/sentry/IntegrationName { +public abstract interface class io/sentry/Integration { public abstract fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V } -public abstract interface class io/sentry/IntegrationName { - public fun addIntegrationToSdkVersion ()V - public fun getIntegrationName ()Ljava/lang/String; -} - public final class io/sentry/IpAddressUtils { public static final field DEFAULT_IP_ADDRESS Ljava/lang/String; public static fun isDefault (Ljava/lang/String;)Z @@ -851,12 +875,13 @@ public final class io/sentry/MainEventProcessor : io/sentry/EventProcessor, java public abstract interface class io/sentry/MeasurementUnit { public static final field NONE Ljava/lang/String; - public fun apiName ()Ljava/lang/String; + public abstract fun apiName ()Ljava/lang/String; public abstract fun name ()Ljava/lang/String; } public final class io/sentry/MeasurementUnit$Custom : io/sentry/MeasurementUnit { public fun (Ljava/lang/String;)V + public fun apiName ()Ljava/lang/String; public fun name ()Ljava/lang/String; } @@ -869,6 +894,7 @@ public final class io/sentry/MeasurementUnit$Duration : java/lang/Enum, io/sentr public static final field NANOSECOND Lio/sentry/MeasurementUnit$Duration; public static final field SECOND Lio/sentry/MeasurementUnit$Duration; public static final field WEEK Lio/sentry/MeasurementUnit$Duration; + public fun apiName ()Ljava/lang/String; public static fun valueOf (Ljava/lang/String;)Lio/sentry/MeasurementUnit$Duration; public static fun values ()[Lio/sentry/MeasurementUnit$Duration; } @@ -876,6 +902,7 @@ public final class io/sentry/MeasurementUnit$Duration : java/lang/Enum, io/sentr public final class io/sentry/MeasurementUnit$Fraction : java/lang/Enum, io/sentry/MeasurementUnit { public static final field PERCENT Lio/sentry/MeasurementUnit$Fraction; public static final field RATIO Lio/sentry/MeasurementUnit$Fraction; + public fun apiName ()Ljava/lang/String; public static fun valueOf (Ljava/lang/String;)Lio/sentry/MeasurementUnit$Fraction; public static fun values ()[Lio/sentry/MeasurementUnit$Fraction; } @@ -895,6 +922,7 @@ public final class io/sentry/MeasurementUnit$Information : java/lang/Enum, io/se public static final field PETABYTE Lio/sentry/MeasurementUnit$Information; public static final field TEBIBYTE Lio/sentry/MeasurementUnit$Information; public static final field TERABYTE Lio/sentry/MeasurementUnit$Information; + public fun apiName ()Ljava/lang/String; public static fun valueOf (Ljava/lang/String;)Lio/sentry/MeasurementUnit$Information; public static fun values ()[Lio/sentry/MeasurementUnit$Information; } @@ -1000,12 +1028,21 @@ public final class io/sentry/MonitorScheduleUnit : java/lang/Enum { public static fun values ()[Lio/sentry/MonitorScheduleUnit; } +public final class io/sentry/NoOpConnectionStatusProvider : io/sentry/IConnectionStatusProvider { + public fun ()V + public fun addConnectionStatusObserver (Lio/sentry/IConnectionStatusProvider$IConnectionStatusObserver;)Z + public fun getConnectionStatus ()Lio/sentry/IConnectionStatusProvider$ConnectionStatus; + public fun getConnectionType ()Ljava/lang/String; + public fun removeConnectionStatusObserver (Lio/sentry/IConnectionStatusProvider$IConnectionStatusObserver;)V +} + public final class io/sentry/NoOpEnvelopeReader : io/sentry/IEnvelopeReader { public static fun getInstance ()Lio/sentry/NoOpEnvelopeReader; public fun read (Ljava/io/InputStream;)Lio/sentry/SentryEnvelope; } public final class io/sentry/NoOpHub : io/sentry/IHub { + public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V public fun bindClient (Lio/sentry/ISentryClient;)V public fun captureCheckIn (Lio/sentry/CheckIn;)Lio/sentry/protocol/SentryId; @@ -1030,8 +1067,10 @@ public final class io/sentry/NoOpHub : io/sentry/IHub { public static fun getInstance ()Lio/sentry/NoOpHub; public fun getLastEventId ()Lio/sentry/protocol/SentryId; public fun getOptions ()Lio/sentry/SentryOptions; + public fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public fun getSpan ()Lio/sentry/ISpan; public fun getTraceparent ()Lio/sentry/SentryTraceHeader; + public fun getTransaction ()Lio/sentry/ITransaction; public fun isCrashedLastRun ()Ljava/lang/Boolean; public fun isEnabled ()Z public fun popScope ()V @@ -1101,8 +1140,8 @@ public final class io/sentry/NoOpTransaction : io/sentry/ITransaction { public fun finish ()V public fun finish (Lio/sentry/SpanStatus;)V public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;)V - public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;Z)V - public fun forceFinish (Lio/sentry/SpanStatus;Z)V + public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;ZLio/sentry/Hint;)V + public fun forceFinish (Lio/sentry/SpanStatus;ZLio/sentry/Hint;)V public fun getContexts ()Lio/sentry/protocol/Contexts; public fun getData (Ljava/lang/String;)Ljava/lang/Object; public fun getDescription ()Ljava/lang/String; @@ -1189,7 +1228,7 @@ public final class io/sentry/OptionsContainer { } public final class io/sentry/OutboxSender : io/sentry/IEnvelopeSender { - public fun (Lio/sentry/IHub;Lio/sentry/IEnvelopeReader;Lio/sentry/ISerializer;Lio/sentry/ILogger;J)V + public fun (Lio/sentry/IHub;Lio/sentry/IEnvelopeReader;Lio/sentry/ISerializer;Lio/sentry/ILogger;JI)V public synthetic fun processDirectory (Ljava/io/File;)V public fun processEnvelopeFile (Ljava/lang/String;Lio/sentry/Hint;)V } @@ -1426,9 +1465,30 @@ public abstract interface class io/sentry/ScopeCallback { public abstract fun run (Lio/sentry/Scope;)V } -public final class io/sentry/SendCachedEnvelopeFireAndForgetIntegration : io/sentry/Integration { +public abstract class io/sentry/ScopeObserverAdapter : io/sentry/IScopeObserver { + public fun ()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 final class io/sentry/SendCachedEnvelopeFireAndForgetIntegration : io/sentry/IConnectionStatusProvider$IConnectionStatusObserver, io/sentry/Integration, java/io/Closeable { public fun (Lio/sentry/SendCachedEnvelopeFireAndForgetIntegration$SendFireAndForgetFactory;)V - public final fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V + public fun close ()V + public fun onConnectionStatusChanged (Lio/sentry/IConnectionStatusProvider$ConnectionStatus;)V + public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V } public abstract interface class io/sentry/SendCachedEnvelopeFireAndForgetIntegration$SendFireAndForget { @@ -1616,6 +1676,7 @@ public final class io/sentry/SentryClient : io/sentry/ISentryClient { public fun captureUserFeedback (Lio/sentry/UserFeedback;)V public fun close ()V public fun flush (J)V + public fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public fun isEnabled ()Z } @@ -1912,6 +1973,7 @@ public class io/sentry/SentryOptions { public fun getCacheDirPath ()Ljava/lang/String; public fun getClientReportRecorder ()Lio/sentry/clientreport/IClientReportRecorder; public fun getCollectors ()Ljava/util/List; + public fun getConnectionStatusProvider ()Lio/sentry/IConnectionStatusProvider; public fun getConnectionTimeoutMillis ()I public fun getContextTags ()Ljava/util/List; public fun getDateProvider ()Lio/sentry/SentryDateProvider; @@ -1984,9 +2046,7 @@ public class io/sentry/SentryOptions { public fun isEnableAutoSessionTracking ()Z public fun isEnableDeduplication ()Z public fun isEnableExternalConfiguration ()Z - public fun isEnableNdk ()Z public fun isEnablePrettySerializationOutput ()Z - public fun isEnableScopeSync ()Z public fun isEnableShutdownHook ()Z public fun isEnableTimeToFullDisplayTracing ()Z public fun isEnableUncaughtExceptionHandler ()Z @@ -2009,6 +2069,7 @@ public class io/sentry/SentryOptions { public fun setBeforeSend (Lio/sentry/SentryOptions$BeforeSendCallback;)V public fun setBeforeSendTransaction (Lio/sentry/SentryOptions$BeforeSendTransactionCallback;)V public fun setCacheDirPath (Ljava/lang/String;)V + public fun setConnectionStatusProvider (Lio/sentry/IConnectionStatusProvider;)V public fun setConnectionTimeoutMillis (I)V public fun setDateProvider (Lio/sentry/SentryDateProvider;)V public fun setDebug (Z)V @@ -2020,9 +2081,7 @@ public class io/sentry/SentryOptions { public fun setEnableAutoSessionTracking (Z)V public fun setEnableDeduplication (Z)V public fun setEnableExternalConfiguration (Z)V - public fun setEnableNdk (Z)V public fun setEnablePrettySerializationOutput (Z)V - public fun setEnableScopeSync (Z)V public fun setEnableShutdownHook (Z)V public fun setEnableTimeToFullDisplayTracing (Z)V public fun setEnableTracing (Ljava/lang/Boolean;)V @@ -2163,8 +2222,8 @@ public final class io/sentry/SentryTracer : io/sentry/ITransaction { public fun finish ()V public fun finish (Lio/sentry/SpanStatus;)V public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;)V - public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;Z)V - public fun forceFinish (Lio/sentry/SpanStatus;Z)V + public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;ZLio/sentry/Hint;)V + public fun forceFinish (Lio/sentry/SpanStatus;ZLio/sentry/Hint;)V public fun getChildren ()Ljava/util/List; public fun getContexts ()Lio/sentry/protocol/Contexts; public fun getData ()Ljava/util/Map; @@ -2536,8 +2595,10 @@ public abstract interface class io/sentry/TransactionFinishedCallback { } public final class io/sentry/TransactionOptions : io/sentry/SpanOptions { + public static final field DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION J public fun ()V public fun getCustomSamplingContext ()Lio/sentry/CustomSamplingContext; + public fun getDeadlineTimeout ()Ljava/lang/Long; public fun getIdleTimeout ()Ljava/lang/Long; public fun getStartTimestamp ()Lio/sentry/SentryDate; public fun getTransactionFinishedCallback ()Lio/sentry/TransactionFinishedCallback; @@ -2545,6 +2606,7 @@ public final class io/sentry/TransactionOptions : io/sentry/SpanOptions { public fun isWaitForChildren ()Z public fun setBindToScope (Z)V public fun setCustomSamplingContext (Lio/sentry/CustomSamplingContext;)V + public fun setDeadlineTimeout (Ljava/lang/Long;)V public fun setIdleTimeout (Ljava/lang/Long;)V public fun setStartTimestamp (Lio/sentry/SentryDate;)V public fun setTransactionFinishedCallback (Lio/sentry/TransactionFinishedCallback;)V @@ -2608,8 +2670,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 class io/sentry/UncaughtExceptionHandlerIntegration$UncaughtExceptionHint : io/sentry/hints/BlockingFlushHint, io/sentry/hints/SessionEnd, io/sentry/hints/TransactionEnd { public fun (JLio/sentry/ILogger;)V + public fun isFlushable (Lio/sentry/protocol/SentryId;)Z + public fun setFlushable (Lio/sentry/protocol/SentryId;)V } public final class io/sentry/UserFeedback : io/sentry/JsonSerializable, io/sentry/JsonUnknown { @@ -2686,7 +2750,7 @@ public final class io/sentry/cache/PersistingOptionsObserver : io/sentry/IOption public fun setTags (Ljava/util/Map;)V } -public final class io/sentry/cache/PersistingScopeObserver : io/sentry/IScopeObserver { +public final class io/sentry/cache/PersistingScopeObserver : io/sentry/ScopeObserverAdapter { 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; @@ -2838,9 +2902,9 @@ 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 ignoreCurrentThread ()Z public abstract fun mechanism ()Ljava/lang/String; - public fun timestamp ()Ljava/lang/Long; + public abstract fun timestamp ()Ljava/lang/Long; } public abstract interface class io/sentry/hints/ApplyScopeData { @@ -2860,7 +2924,13 @@ public abstract interface class io/sentry/hints/Cached { } public abstract interface class io/sentry/hints/DiskFlushNotification { + public abstract fun isFlushable (Lio/sentry/protocol/SentryId;)Z public abstract fun markFlushed ()V + public abstract fun setFlushable (Lio/sentry/protocol/SentryId;)V +} + +public abstract interface class io/sentry/hints/Enqueable { + public abstract fun markEnqueued ()V } public final class io/sentry/hints/EventDropReason : java/lang/Enum { @@ -4240,6 +4310,7 @@ public final class io/sentry/transport/AsyncHttpTransport : io/sentry/transport/ public fun (Lio/sentry/transport/QueuedThreadPoolExecutor;Lio/sentry/SentryOptions;Lio/sentry/transport/RateLimiter;Lio/sentry/transport/ITransportGate;Lio/sentry/transport/HttpConnection;)V public fun close ()V public fun flush (J)V + public fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public fun send (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V } @@ -4254,6 +4325,7 @@ public abstract interface class io/sentry/transport/ICurrentDateProvider { public abstract interface class io/sentry/transport/ITransport : java/io/Closeable { public abstract fun flush (J)V + public abstract fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public fun send (Lio/sentry/SentryEnvelope;)V public abstract fun send (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V } @@ -4274,6 +4346,7 @@ public final class io/sentry/transport/NoOpTransport : io/sentry/transport/ITran public fun close ()V public fun flush (J)V public static fun getInstance ()Lio/sentry/transport/NoOpTransport; + public fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public fun send (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V } @@ -4286,6 +4359,7 @@ public final class io/sentry/transport/RateLimiter { public fun (Lio/sentry/SentryOptions;)V public fun (Lio/sentry/transport/ICurrentDateProvider;Lio/sentry/SentryOptions;)V public fun filter (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/SentryEnvelope; + public fun isActiveForCategory (Lio/sentry/DataCategory;)Z public fun updateRetryAfterLimits (Ljava/lang/String;Ljava/lang/String;I)V } @@ -4303,6 +4377,7 @@ public final class io/sentry/transport/StdoutTransport : io/sentry/transport/ITr public fun (Lio/sentry/ISerializer;)V public fun close ()V public fun flush (J)V + public fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public fun send (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V } @@ -4397,6 +4472,12 @@ public final class io/sentry/util/HttpUtils { public static fun isSecurityCookie (Ljava/lang/String;Ljava/util/List;)Z } +public final class io/sentry/util/IntegrationUtils { + public fun ()V + public static fun addIntegrationToSdkVersion (Ljava/lang/Class;)V + public static fun addIntegrationToSdkVersion (Ljava/lang/String;)V +} + public final class io/sentry/util/JsonSerializationUtils { public fun ()V public static fun atomicIntegerArrayToList (Ljava/util/concurrent/atomic/AtomicIntegerArray;)Ljava/util/List; @@ -4527,21 +4608,27 @@ public final class io/sentry/util/UrlUtils$UrlDetails { } public abstract interface class io/sentry/util/thread/IMainThreadChecker { - public fun isMainThread ()Z + public abstract fun isMainThread ()Z public abstract fun isMainThread (J)Z - public fun isMainThread (Lio/sentry/protocol/SentryThread;)Z - public fun isMainThread (Ljava/lang/Thread;)Z + public abstract fun isMainThread (Lio/sentry/protocol/SentryThread;)Z + public abstract fun isMainThread (Ljava/lang/Thread;)Z } public final class io/sentry/util/thread/MainThreadChecker : io/sentry/util/thread/IMainThreadChecker { public static fun getInstance ()Lio/sentry/util/thread/MainThreadChecker; + public fun isMainThread ()Z public fun isMainThread (J)Z + public fun isMainThread (Lio/sentry/protocol/SentryThread;)Z + public fun isMainThread (Ljava/lang/Thread;)Z } public final class io/sentry/util/thread/NoOpMainThreadChecker : io/sentry/util/thread/IMainThreadChecker { public fun ()V public static fun getInstance ()Lio/sentry/util/thread/NoOpMainThreadChecker; + public fun isMainThread ()Z public fun isMainThread (J)Z + public fun isMainThread (Lio/sentry/protocol/SentryThread;)Z + public fun isMainThread (Ljava/lang/Thread;)Z } public class io/sentry/vendor/Base64 { diff --git a/sentry/src/main/java/io/sentry/DirectoryProcessor.java b/sentry/src/main/java/io/sentry/DirectoryProcessor.java index d8924a38adb..5d60feba605 100644 --- a/sentry/src/main/java/io/sentry/DirectoryProcessor.java +++ b/sentry/src/main/java/io/sentry/DirectoryProcessor.java @@ -3,23 +3,37 @@ import static io.sentry.SentryLevel.ERROR; import io.sentry.hints.Cached; +import io.sentry.hints.Enqueable; import io.sentry.hints.Flushable; import io.sentry.hints.Retryable; import io.sentry.hints.SubmissionResult; +import io.sentry.transport.RateLimiter; import io.sentry.util.HintUtils; import java.io.File; +import java.util.Queue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; abstract class DirectoryProcessor { + private static final long ENVELOPE_PROCESSING_DELAY = 100L; + private final @NotNull IHub hub; private final @NotNull ILogger logger; private final long flushTimeoutMillis; - - DirectoryProcessor(final @NotNull ILogger logger, final long flushTimeoutMillis) { + private final Queue processedEnvelopes; + + DirectoryProcessor( + final @NotNull IHub hub, + final @NotNull ILogger logger, + final long flushTimeoutMillis, + final int maxQueueSize) { + this.hub = hub; this.logger = logger; this.flushTimeoutMillis = flushTimeoutMillis; + this.processedEnvelopes = + SynchronizedQueue.synchronizedQueue(new CircularFifoQueue<>(maxQueueSize)); } public void processDirectory(final @NotNull File directory) { @@ -60,14 +74,36 @@ public void processDirectory(final @NotNull File directory) { continue; } - logger.log(SentryLevel.DEBUG, "Processing file: %s", file.getAbsolutePath()); + final String filePath = file.getAbsolutePath(); + // if envelope has already been submitted into the transport queue, we don't process it + // again + if (processedEnvelopes.contains(filePath)) { + logger.log( + SentryLevel.DEBUG, + "File '%s' has already been processed so it will not be processed again.", + filePath); + continue; + } + + // in case there's rate limiting active, skip processing + final @Nullable RateLimiter rateLimiter = hub.getRateLimiter(); + if (rateLimiter != null && rateLimiter.isActiveForCategory(DataCategory.All)) { + logger.log(SentryLevel.INFO, "DirectoryProcessor, rate limiting active."); + return; + } + + logger.log(SentryLevel.DEBUG, "Processing file: %s", filePath); final SendCachedEnvelopeHint cachedHint = - new SendCachedEnvelopeHint(flushTimeoutMillis, logger); + new SendCachedEnvelopeHint(flushTimeoutMillis, logger, filePath, processedEnvelopes); final Hint hint = HintUtils.createWithTypeCheckHint(cachedHint); - processFile(file, hint); + + // a short delay between processing envelopes to avoid bursting our server and hitting + // another rate limit https://develop.sentry.dev/sdk/features/#additional-capabilities + // InterruptedException will be handled by the outer try-catch + Thread.sleep(ENVELOPE_PROCESSING_DELAY); } } catch (Throwable e) { logger.log(SentryLevel.ERROR, e, "Failed processing '%s'", directory.getAbsolutePath()); @@ -79,16 +115,24 @@ public void processDirectory(final @NotNull File directory) { protected abstract boolean isRelevantFileName(String fileName); private static final class SendCachedEnvelopeHint - implements Cached, Retryable, SubmissionResult, Flushable { + implements Cached, Retryable, SubmissionResult, Flushable, Enqueable { boolean retry = false; boolean succeeded = false; private final CountDownLatch latch; private final long flushTimeoutMillis; private final @NotNull ILogger logger; - - public SendCachedEnvelopeHint(final long flushTimeoutMillis, final @NotNull ILogger logger) { + private final @NotNull String filePath; + private final @NotNull Queue processedEnvelopes; + + public SendCachedEnvelopeHint( + final long flushTimeoutMillis, + final @NotNull ILogger logger, + final @NotNull String filePath, + final @NotNull Queue processedEnvelopes) { this.flushTimeoutMillis = flushTimeoutMillis; + this.filePath = filePath; + this.processedEnvelopes = processedEnvelopes; this.latch = new CountDownLatch(1); this.logger = logger; } @@ -124,5 +168,10 @@ public void setResult(boolean succeeded) { public boolean isSuccess() { return succeeded; } + + @Override + public void markEnqueued() { + processedEnvelopes.add(filePath); + } } } diff --git a/sentry/src/main/java/io/sentry/EnvelopeSender.java b/sentry/src/main/java/io/sentry/EnvelopeSender.java index b74606fa1a5..598caad2804 100644 --- a/sentry/src/main/java/io/sentry/EnvelopeSender.java +++ b/sentry/src/main/java/io/sentry/EnvelopeSender.java @@ -25,8 +25,9 @@ public EnvelopeSender( final @NotNull IHub hub, final @NotNull ISerializer serializer, final @NotNull ILogger logger, - final long flushTimeoutMillis) { - super(logger, flushTimeoutMillis); + final long flushTimeoutMillis, + final int maxQueueSize) { + super(hub, logger, flushTimeoutMillis, maxQueueSize); this.hub = Objects.requireNonNull(hub, "Hub is required."); this.serializer = Objects.requireNonNull(serializer, "Serializer is required."); this.logger = Objects.requireNonNull(logger, "Logger is required."); diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index 0ce59cc05c2..02f058ed039 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -7,6 +7,7 @@ import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.User; +import io.sentry.transport.RateLimiter; import io.sentry.util.ExceptionUtils; import io.sentry.util.HintUtils; import io.sentry.util.Objects; @@ -372,6 +373,11 @@ public void addBreadcrumb(final @NotNull Breadcrumb breadcrumb, final @Nullable } } + @Override + public void addBreadcrumb(final @NotNull Breadcrumb breadcrumb) { + addBreadcrumb(breadcrumb, new Hint()); + } + @Override public void setLevel(final @Nullable SentryLevel level) { if (!isEnabled()) { @@ -762,6 +768,22 @@ public void flush(long timeoutMillis) { return span; } + @Override + @ApiStatus.Internal + public @Nullable ITransaction getTransaction() { + ITransaction span = null; + if (!isEnabled()) { + options + .getLogger() + .log( + SentryLevel.WARNING, + "Instance is disabled and this 'getTransaction' call is a no-op."); + } else { + span = stack.peek().getScope().getTransaction(); + } + return span; + } + @Override @ApiStatus.Internal public void setSpanContext( @@ -884,4 +906,11 @@ private Scope buildLocalScope( this.lastEventId = sentryId; return sentryId; } + + @ApiStatus.Internal + @Override + public @Nullable RateLimiter getRateLimiter() { + final StackItem item = stack.peek(); + return item.getClient().getRateLimiter(); + } } diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index 88445b5232c..7baf146cd8a 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -3,6 +3,7 @@ import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.User; +import io.sentry.transport.RateLimiter; import java.util.List; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -87,6 +88,11 @@ public void addBreadcrumb(@NotNull Breadcrumb breadcrumb, @Nullable Hint hint) { Sentry.addBreadcrumb(breadcrumb, hint); } + @Override + public void addBreadcrumb(final @NotNull Breadcrumb breadcrumb) { + addBreadcrumb(breadcrumb, new Hint()); + } + @Override public void setLevel(@Nullable SentryLevel level) { Sentry.setLevel(level); @@ -221,6 +227,12 @@ public void setSpanContext( return Sentry.getCurrentHub().getSpan(); } + @Override + @ApiStatus.Internal + public @Nullable ITransaction getTransaction() { + return Sentry.getCurrentHub().getTransaction(); + } + @Override public @NotNull SentryOptions getOptions() { return Sentry.getCurrentHub().getOptions(); @@ -257,4 +269,10 @@ public void reportFullyDisplayed() { public @NotNull SentryId captureCheckIn(final @NotNull CheckIn checkIn) { return Sentry.captureCheckIn(checkIn); } + + @ApiStatus.Internal + @Override + public @Nullable RateLimiter getRateLimiter() { + return Sentry.getCurrentHub().getRateLimiter(); + } } diff --git a/sentry/src/main/java/io/sentry/IConnectionStatusProvider.java b/sentry/src/main/java/io/sentry/IConnectionStatusProvider.java new file mode 100644 index 00000000000..1d75098e564 --- /dev/null +++ b/sentry/src/main/java/io/sentry/IConnectionStatusProvider.java @@ -0,0 +1,57 @@ +package io.sentry; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public interface IConnectionStatusProvider { + + enum ConnectionStatus { + UNKNOWN, + CONNECTED, + DISCONNECTED, + NO_PERMISSION + } + + interface IConnectionStatusObserver { + /** + * Invoked whenever the connection status changed. + * + * @param status the new connection status + */ + void onConnectionStatusChanged(@NotNull ConnectionStatus status); + } + + /** + * Gets the connection status. + * + * @return the current connection status + */ + @NotNull + ConnectionStatus getConnectionStatus(); + + /** + * Gets the connection type. + * + * @return the current connection type. E.g. "ethernet", "wifi" or "cellular" + */ + @Nullable + String getConnectionType(); + + /** + * Adds an observer for listening to connection status changes. + * + * @param observer the observer to register + * @return true if the observer was sucessfully registered + */ + boolean addConnectionStatusObserver(@NotNull final IConnectionStatusObserver observer); + + /** + * Removes an observer. + * + * @param observer a previously added observer via {@link + * #addConnectionStatusObserver(IConnectionStatusObserver)} + */ + void removeConnectionStatusObserver(@NotNull final IConnectionStatusObserver observer); +} diff --git a/sentry/src/main/java/io/sentry/IHub.java b/sentry/src/main/java/io/sentry/IHub.java index 8ff69727b06..9882ee309f5 100644 --- a/sentry/src/main/java/io/sentry/IHub.java +++ b/sentry/src/main/java/io/sentry/IHub.java @@ -3,6 +3,7 @@ import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.User; +import io.sentry.transport.RateLimiter; import java.util.List; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -203,9 +204,7 @@ SentryId captureException( * * @param breadcrumb the breadcrumb */ - default void addBreadcrumb(@NotNull Breadcrumb breadcrumb) { - addBreadcrumb(breadcrumb, new Hint()); - } + void addBreadcrumb(@NotNull Breadcrumb breadcrumb); /** * Adds a breadcrumb to the current Scope @@ -558,6 +557,15 @@ void setSpanContext( @Nullable ISpan getSpan(); + /** + * Returns the transaction. + * + * @return the transaction or null when no active transaction is running. + */ + @ApiStatus.Internal + @Nullable + ITransaction getTransaction(); + /** * Gets the {@link SentryOptions} attached to current scope. * @@ -630,4 +638,8 @@ TransactionContext continueTrace( @ApiStatus.Experimental @NotNull SentryId captureCheckIn(final @NotNull CheckIn checkIn); + + @ApiStatus.Internal + @Nullable + RateLimiter getRateLimiter(); } diff --git a/sentry/src/main/java/io/sentry/IOptionsObserver.java b/sentry/src/main/java/io/sentry/IOptionsObserver.java index 519e9222b56..54cacc666ae 100644 --- a/sentry/src/main/java/io/sentry/IOptionsObserver.java +++ b/sentry/src/main/java/io/sentry/IOptionsObserver.java @@ -11,15 +11,15 @@ */ public interface IOptionsObserver { - default void setRelease(@Nullable String release) {} + void setRelease(@Nullable String release); - default void setProguardUuid(@Nullable String proguardUuid) {} + void setProguardUuid(@Nullable String proguardUuid); - default void setSdkVersion(@Nullable SdkVersion sdkVersion) {} + void setSdkVersion(@Nullable SdkVersion sdkVersion); - default void setEnvironment(@Nullable String environment) {} + void setEnvironment(@Nullable String environment); - default void setDist(@Nullable String dist) {} + void setDist(@Nullable String dist); - default void setTags(@NotNull Map tags) {} + 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 d8d8bc68e60..4a103668d2a 100644 --- a/sentry/src/main/java/io/sentry/IScopeObserver.java +++ b/sentry/src/main/java/io/sentry/IScopeObserver.java @@ -13,33 +13,33 @@ * subscribe to only those properties, that they are interested in. */ public interface IScopeObserver { - default void setUser(@Nullable User user) {} + void setUser(@Nullable User user); - default void addBreadcrumb(@NotNull Breadcrumb crumb) {} + void addBreadcrumb(@NotNull Breadcrumb crumb); - default void setBreadcrumbs(@NotNull Collection breadcrumbs) {} + void setBreadcrumbs(@NotNull Collection breadcrumbs); - default void setTag(@NotNull String key, @NotNull String value) {} + void setTag(@NotNull String key, @NotNull String value); - default void removeTag(@NotNull String key) {} + void removeTag(@NotNull String key); - default void setTags(@NotNull Map tags) {} + void setTags(@NotNull Map tags); - default void setExtra(@NotNull String key, @NotNull String value) {} + void setExtra(@NotNull String key, @NotNull String value); - default void removeExtra(@NotNull String key) {} + void removeExtra(@NotNull String key); - default void setExtras(@NotNull Map extras) {} + void setExtras(@NotNull Map extras); - default void setRequest(@Nullable Request request) {} + void setRequest(@Nullable Request request); - default void setFingerprint(@NotNull Collection fingerprint) {} + void setFingerprint(@NotNull Collection fingerprint); - default void setLevel(@Nullable SentryLevel level) {} + void setLevel(@Nullable SentryLevel level); - default void setContexts(@NotNull Contexts contexts) {} + void setContexts(@NotNull Contexts contexts); - default void setTransaction(@Nullable String transaction) {} + void setTransaction(@Nullable String transaction); - default void setTrace(@Nullable SpanContext spanContext) {} + void setTrace(@Nullable SpanContext spanContext); } diff --git a/sentry/src/main/java/io/sentry/ISentryClient.java b/sentry/src/main/java/io/sentry/ISentryClient.java index 6eb8d7d1d05..561f6ea6a40 100644 --- a/sentry/src/main/java/io/sentry/ISentryClient.java +++ b/sentry/src/main/java/io/sentry/ISentryClient.java @@ -3,6 +3,7 @@ import io.sentry.protocol.Message; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; +import io.sentry.transport.RateLimiter; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -268,4 +269,8 @@ SentryId captureTransaction( @NotNull @ApiStatus.Experimental SentryId captureCheckIn(@NotNull CheckIn checkIn, @Nullable Scope scope, @Nullable Hint hint); + + @ApiStatus.Internal + @Nullable + RateLimiter getRateLimiter(); } diff --git a/sentry/src/main/java/io/sentry/ITransaction.java b/sentry/src/main/java/io/sentry/ITransaction.java index 127dc448ee4..a34e4918022 100644 --- a/sentry/src/main/java/io/sentry/ITransaction.java +++ b/sentry/src/main/java/io/sentry/ITransaction.java @@ -99,13 +99,17 @@ ISpan startChild( * @param dropIfNoChildren true, if the transaction should be dropped when it e.g. contains no * child spans. Usually true, but can be set to falseS for situations were the transaction and * profile provide crucial context (e.g. ANRs) + * @param hint An optional hint to pass down to the client/transport layer */ @ApiStatus.Internal - void forceFinish(@NotNull final SpanStatus status, boolean dropIfNoChildren); + void forceFinish(@NotNull final SpanStatus status, boolean dropIfNoChildren, @Nullable Hint hint); @ApiStatus.Internal void finish( - @Nullable SpanStatus status, @Nullable SentryDate timestamp, boolean dropIfNoChildren); + @Nullable SpanStatus status, + @Nullable SentryDate timestamp, + boolean dropIfNoChildren, + @Nullable Hint hint); @ApiStatus.Internal void setContext(@NotNull String key, @NotNull Object context); diff --git a/sentry/src/main/java/io/sentry/Integration.java b/sentry/src/main/java/io/sentry/Integration.java index 110a8ea516d..54b17e4d515 100644 --- a/sentry/src/main/java/io/sentry/Integration.java +++ b/sentry/src/main/java/io/sentry/Integration.java @@ -6,7 +6,7 @@ * Code that provides middlewares, bindings or hooks into certain frameworks or environments, along * with code that inserts those bindings and activates them. */ -public interface Integration extends IntegrationName { +public interface Integration { /** * Registers an integration * diff --git a/sentry/src/main/java/io/sentry/IntegrationName.java b/sentry/src/main/java/io/sentry/IntegrationName.java deleted file mode 100644 index ca3e98cd2cb..00000000000 --- a/sentry/src/main/java/io/sentry/IntegrationName.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.sentry; - -public interface IntegrationName { - default String getIntegrationName() { - return this.getClass() - .getSimpleName() - .replace("Sentry", "") - .replace("Integration", "") - .replace("Interceptor", "") - .replace("EventProcessor", ""); - } - - default void addIntegrationToSdkVersion() { - SentryIntegrationPackageStorage.getInstance().addIntegration(getIntegrationName()); - } -} diff --git a/sentry/src/main/java/io/sentry/MainEventProcessor.java b/sentry/src/main/java/io/sentry/MainEventProcessor.java index d0468559583..abbf21c84e5 100644 --- a/sentry/src/main/java/io/sentry/MainEventProcessor.java +++ b/sentry/src/main/java/io/sentry/MainEventProcessor.java @@ -220,14 +220,13 @@ private void setTags(final @NotNull SentryBaseEvent event) { } 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); - } + @Nullable User user = event.getUser(); + if (user == null) { + user = new User(); + event.setUser(user); + } + if (user.getIpAddress() == null) { + user.setIpAddress(IpAddressUtils.DEFAULT_IP_ADDRESS); } } diff --git a/sentry/src/main/java/io/sentry/MeasurementUnit.java b/sentry/src/main/java/io/sentry/MeasurementUnit.java index 5dc00261d1e..0e8ebe9bc29 100644 --- a/sentry/src/main/java/io/sentry/MeasurementUnit.java +++ b/sentry/src/main/java/io/sentry/MeasurementUnit.java @@ -47,6 +47,11 @@ enum Duration implements MeasurementUnit { /** Week (`"week"`), 604,800 seconds. */ WEEK; + + @Override + public @NotNull String apiName() { + return name().toLowerCase(Locale.ROOT); + } } /** Size of information derived from bytes. */ @@ -92,6 +97,11 @@ enum Information implements MeasurementUnit { /** Exbibyte (`"exbibyte"`), 2^60 bytes. */ EXBIBYTE; + + @Override + public @NotNull String apiName() { + return name().toLowerCase(Locale.ROOT); + } } /** Fractions such as percentages. */ @@ -101,6 +111,11 @@ enum Fraction implements MeasurementUnit { /** Ratio expressed as a fraction of `100`. `100%` equals a ratio of `1.0`. */ PERCENT; + + @Override + public @NotNull String apiName() { + return name().toLowerCase(Locale.ROOT); + } } /** @@ -119,6 +134,11 @@ public Custom(@NotNull String name) { public @NotNull String name() { return name; } + + @Override + public @NotNull String apiName() { + return name().toLowerCase(Locale.ROOT); + } } @NotNull @@ -126,7 +146,6 @@ public Custom(@NotNull String name) { /** Unit adhering to the API spec. */ @ApiStatus.Internal - default @NotNull String apiName() { - return name().toLowerCase(Locale.ROOT); - } + @NotNull + String apiName(); } diff --git a/sentry/src/main/java/io/sentry/NoOpConnectionStatusProvider.java b/sentry/src/main/java/io/sentry/NoOpConnectionStatusProvider.java new file mode 100644 index 00000000000..a1d66c9115b --- /dev/null +++ b/sentry/src/main/java/io/sentry/NoOpConnectionStatusProvider.java @@ -0,0 +1,28 @@ +package io.sentry; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class NoOpConnectionStatusProvider implements IConnectionStatusProvider { + @Override + public @NotNull ConnectionStatus getConnectionStatus() { + return ConnectionStatus.UNKNOWN; + } + + @Override + public @Nullable String getConnectionType() { + return null; + } + + @Override + public boolean addConnectionStatusObserver(@NotNull IConnectionStatusObserver observer) { + return false; + } + + @Override + public void removeConnectionStatusObserver(@NotNull IConnectionStatusObserver observer) { + // no-op + } +} diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index ec6b45e3891..bf88e3beea1 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -3,6 +3,7 @@ import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.User; +import io.sentry.transport.RateLimiter; import java.util.List; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -78,6 +79,9 @@ public void close() {} @Override public void addBreadcrumb(@NotNull Breadcrumb breadcrumb, @Nullable Hint hint) {} + @Override + public void addBreadcrumb(final @NotNull Breadcrumb breadcrumb) {} + @Override public void setLevel(@Nullable SentryLevel level) {} @@ -180,6 +184,11 @@ public void setSpanContext( return null; } + @Override + public @Nullable ITransaction getTransaction() { + return null; + } + @Override public @NotNull SentryOptions getOptions() { return emptyOptions; @@ -214,4 +223,9 @@ public void reportFullyDisplayed() {} public @NotNull SentryId captureCheckIn(final @NotNull CheckIn checkIn) { return SentryId.EMPTY_ID; } + + @Override + public @Nullable RateLimiter getRateLimiter() { + return null; + } } diff --git a/sentry/src/main/java/io/sentry/NoOpSentryClient.java b/sentry/src/main/java/io/sentry/NoOpSentryClient.java index c0a71c27fc4..35de56aa672 100644 --- a/sentry/src/main/java/io/sentry/NoOpSentryClient.java +++ b/sentry/src/main/java/io/sentry/NoOpSentryClient.java @@ -2,6 +2,7 @@ import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; +import io.sentry.transport.RateLimiter; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -60,4 +61,9 @@ public SentryId captureEnvelope(@NotNull SentryEnvelope envelope, @Nullable Hint @NotNull CheckIn checkIn, @Nullable Scope scope, @Nullable Hint hint) { return SentryId.EMPTY_ID; } + + @Override + public @Nullable RateLimiter getRateLimiter() { + return null; + } } diff --git a/sentry/src/main/java/io/sentry/NoOpTransaction.java b/sentry/src/main/java/io/sentry/NoOpTransaction.java index 902c8a7da6c..5bf9f3a19f6 100644 --- a/sentry/src/main/java/io/sentry/NoOpTransaction.java +++ b/sentry/src/main/java/io/sentry/NoOpTransaction.java @@ -102,11 +102,15 @@ public void setName(@NotNull String name, @NotNull TransactionNameSource transac public void scheduleFinish() {} @Override - public void forceFinish(@NotNull SpanStatus status, boolean dropIfNoChildren) {} + public void forceFinish( + @NotNull SpanStatus status, boolean dropIfNoChildren, @Nullable Hint hint) {} @Override public void finish( - @Nullable SpanStatus status, @Nullable SentryDate timestamp, boolean dropIfNoChildren) {} + @Nullable SpanStatus status, + @Nullable SentryDate timestamp, + boolean dropIfNoChildren, + @Nullable Hint hint) {} @Override public boolean isFinished() { diff --git a/sentry/src/main/java/io/sentry/OutboxSender.java b/sentry/src/main/java/io/sentry/OutboxSender.java index 9e704f0a0e9..709cbb8580b 100644 --- a/sentry/src/main/java/io/sentry/OutboxSender.java +++ b/sentry/src/main/java/io/sentry/OutboxSender.java @@ -46,8 +46,9 @@ public OutboxSender( final @NotNull IEnvelopeReader envelopeReader, final @NotNull ISerializer serializer, final @NotNull ILogger logger, - final long flushTimeoutMillis) { - super(logger, flushTimeoutMillis); + final long flushTimeoutMillis, + final int maxQueueSize) { + super(hub, logger, flushTimeoutMillis, maxQueueSize); this.hub = Objects.requireNonNull(hub, "Hub is required."); this.envelopeReader = Objects.requireNonNull(envelopeReader, "Envelope reader is required."); this.serializer = Objects.requireNonNull(serializer, "Serializer is required."); diff --git a/sentry/src/main/java/io/sentry/ScopeObserverAdapter.java b/sentry/src/main/java/io/sentry/ScopeObserverAdapter.java new file mode 100644 index 00000000000..38d0cdf7a10 --- /dev/null +++ b/sentry/src/main/java/io/sentry/ScopeObserverAdapter.java @@ -0,0 +1,56 @@ +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; + +public abstract class ScopeObserverAdapter implements IScopeObserver { + @Override + public void setUser(@Nullable User user) {} + + @Override + public void addBreadcrumb(@NotNull Breadcrumb crumb) {} + + @Override + public void setBreadcrumbs(@NotNull Collection breadcrumbs) {} + + @Override + public void setTag(@NotNull String key, @NotNull String value) {} + + @Override + public void removeTag(@NotNull String key) {} + + @Override + public void setTags(@NotNull Map tags) {} + + @Override + public void setExtra(@NotNull String key, @NotNull String value) {} + + @Override + public void removeExtra(@NotNull String key) {} + + @Override + public void setExtras(@NotNull Map extras) {} + + @Override + public void setRequest(@Nullable Request request) {} + + @Override + public void setFingerprint(@NotNull Collection fingerprint) {} + + @Override + public void setLevel(@Nullable SentryLevel level) {} + + @Override + public void setContexts(@NotNull Contexts contexts) {} + + @Override + public void setTransaction(@Nullable String transaction) {} + + @Override + public void setTrace(@Nullable SpanContext spanContext) {} +} diff --git a/sentry/src/main/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegration.java b/sentry/src/main/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegration.java index 01f9b960183..d13fbf7fcbf 100644 --- a/sentry/src/main/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegration.java +++ b/sentry/src/main/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegration.java @@ -1,15 +1,25 @@ package io.sentry; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; + +import io.sentry.transport.RateLimiter; import io.sentry.util.Objects; +import java.io.Closeable; import java.io.File; +import java.io.IOException; import java.util.concurrent.RejectedExecutionException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -/** Sends cached events over when your App. is starting. */ -public final class SendCachedEnvelopeFireAndForgetIntegration implements Integration { +/** Sends cached events over when your App is starting or a network connection is present. */ +public final class SendCachedEnvelopeFireAndForgetIntegration + implements Integration, IConnectionStatusProvider.IConnectionStatusObserver, Closeable { private final @NotNull SendFireAndForgetFactory factory; + private @Nullable IConnectionStatusProvider connectionStatusProvider; + private @Nullable IHub hub; + private @Nullable SentryOptions options; + private @Nullable SendFireAndForget sender; public interface SendFireAndForget { void send(); @@ -52,11 +62,10 @@ public SendCachedEnvelopeFireAndForgetIntegration( this.factory = Objects.requireNonNull(factory, "SendFireAndForgetFactory is required"); } - @SuppressWarnings("FutureReturnValueIgnored") @Override - public final void register(final @NotNull IHub hub, final @NotNull SentryOptions options) { - Objects.requireNonNull(hub, "Hub is required"); - Objects.requireNonNull(options, "SentryOptions is required"); + public void register(final @NotNull IHub hub, final @NotNull SentryOptions options) { + this.hub = Objects.requireNonNull(hub, "Hub is required"); + this.options = Objects.requireNonNull(options, "SentryOptions is required"); final String cachedDir = options.getCacheDirPath(); if (!factory.hasValidPath(cachedDir, options.getLogger())) { @@ -64,7 +73,58 @@ public final void register(final @NotNull IHub hub, final @NotNull SentryOptions return; } - final SendFireAndForget sender = factory.create(hub, options); + options + .getLogger() + .log(SentryLevel.DEBUG, "SendCachedEventFireAndForgetIntegration installed."); + addIntegrationToSdkVersion(getClass()); + + connectionStatusProvider = options.getConnectionStatusProvider(); + connectionStatusProvider.addConnectionStatusObserver(this); + + sender = factory.create(hub, options); + + sendCachedEnvelopes(hub, options); + } + + @Override + public void close() throws IOException { + if (connectionStatusProvider != null) { + connectionStatusProvider.removeConnectionStatusObserver(this); + } + } + + @Override + public void onConnectionStatusChanged( + final @NotNull IConnectionStatusProvider.ConnectionStatus status) { + if (hub != null && options != null) { + sendCachedEnvelopes(hub, options); + } + } + + @SuppressWarnings({"FutureReturnValueIgnored", "NullAway"}) + private synchronized void sendCachedEnvelopes( + final @NotNull IHub hub, final @NotNull SentryOptions options) { + + // skip run only if we're certainly disconnected + if (connectionStatusProvider != null + && connectionStatusProvider.getConnectionStatus() + == IConnectionStatusProvider.ConnectionStatus.DISCONNECTED) { + options + .getLogger() + .log(SentryLevel.INFO, "SendCachedEnvelopeFireAndForgetIntegration, no connection."); + return; + } + + // in case there's rate limiting active, skip processing + final @Nullable RateLimiter rateLimiter = hub.getRateLimiter(); + if (rateLimiter != null && rateLimiter.isActiveForCategory(DataCategory.All)) { + options + .getLogger() + .log( + SentryLevel.INFO, + "SendCachedEnvelopeFireAndForgetIntegration, rate limiting active."); + return; + } if (sender == null) { options.getLogger().log(SentryLevel.ERROR, "SendFireAndForget factory is null."); @@ -84,11 +144,6 @@ public final void register(final @NotNull IHub hub, final @NotNull SentryOptions .log(SentryLevel.ERROR, "Failed trying to send cached events.", e); } }); - - options - .getLogger() - .log(SentryLevel.DEBUG, "SendCachedEventFireAndForgetIntegration installed."); - addIntegrationToSdkVersion(); } catch (RejectedExecutionException e) { options .getLogger() diff --git a/sentry/src/main/java/io/sentry/SendFireAndForgetEnvelopeSender.java b/sentry/src/main/java/io/sentry/SendFireAndForgetEnvelopeSender.java index ecf4cb79136..e44d18a8d6b 100644 --- a/sentry/src/main/java/io/sentry/SendFireAndForgetEnvelopeSender.java +++ b/sentry/src/main/java/io/sentry/SendFireAndForgetEnvelopeSender.java @@ -33,7 +33,11 @@ public SendFireAndForgetEnvelopeSender( final EnvelopeSender envelopeSender = new EnvelopeSender( - hub, options.getSerializer(), options.getLogger(), options.getFlushTimeoutMillis()); + hub, + options.getSerializer(), + options.getLogger(), + options.getFlushTimeoutMillis(), + options.getMaxQueueSize()); return processDir(envelopeSender, dirPath, options.getLogger()); } diff --git a/sentry/src/main/java/io/sentry/SendFireAndForgetOutboxSender.java b/sentry/src/main/java/io/sentry/SendFireAndForgetOutboxSender.java index 0666b37cda3..fda41610fdf 100644 --- a/sentry/src/main/java/io/sentry/SendFireAndForgetOutboxSender.java +++ b/sentry/src/main/java/io/sentry/SendFireAndForgetOutboxSender.java @@ -37,7 +37,8 @@ public SendFireAndForgetOutboxSender( options.getEnvelopeReader(), options.getSerializer(), options.getLogger(), - options.getFlushTimeoutMillis()); + options.getFlushTimeoutMillis(), + options.getMaxQueueSize()); return processDir(outboxSender, dirPath, options.getLogger()); } diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 71b588d55b9..9215388ff3e 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -15,6 +15,7 @@ import io.sentry.transport.NoOpEnvelopeCache; import io.sentry.util.DebugMetaPropertiesApplier; import io.sentry.util.FileUtils; +import io.sentry.util.Platform; import io.sentry.util.thread.IMainThreadChecker; import io.sentry.util.thread.MainThreadChecker; import io.sentry.util.thread.NoOpMainThreadChecker; @@ -922,10 +923,16 @@ public static void endSession() { /** * Gets the current active transaction or span. * - * @return the active span or null when no active transaction is running + * @return the active span or null when no active transaction is running. In case of + * globalHubMode=true, always the active transaction is returned, rather than the last active + * span. */ public static @Nullable ISpan getSpan() { - return getCurrentHub().getSpan(); + if (globalHubMode && Platform.isAndroid()) { + return getCurrentHub().getTransaction(); + } else { + return getCurrentHub().getSpan(); + } } /** diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 5ff5e3f35f6..88fc6ecd51a 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -4,11 +4,13 @@ import io.sentry.exception.SentryEnvelopeException; import io.sentry.hints.AbnormalExit; import io.sentry.hints.Backfillable; +import io.sentry.hints.DiskFlushNotification; import io.sentry.hints.TransactionEnd; import io.sentry.protocol.Contexts; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.transport.ITransport; +import io.sentry.transport.RateLimiter; import io.sentry.util.CheckInUtils; import io.sentry.util.HintUtils; import io.sentry.util.Objects; @@ -224,14 +226,19 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul sentryId = SentryId.EMPTY_ID; } - // if we encountered an abnormal exit finish tracing in order to persist and send + // if we encountered a crash/abnormal exit finish tracing in order to persist and send // any running transaction / profiling data if (scope != null) { - @Nullable ITransaction transaction = scope.getTransaction(); + final @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, TransactionEnd.class)) { - transaction.forceFinish(SpanStatus.ABORTED, false); + final Object sentrySdkHint = HintUtils.getSentrySdkHint(hint); + if (sentrySdkHint instanceof DiskFlushNotification) { + ((DiskFlushNotification) sentrySdkHint).setFlushable(transaction.getEventId()); + transaction.forceFinish(SpanStatus.ABORTED, false, hint); + } else { + transaction.forceFinish(SpanStatus.ABORTED, false, null); + } } } } @@ -929,6 +936,11 @@ public void flush(final long timeoutMillis) { transport.flush(timeoutMillis); } + @Override + public @Nullable RateLimiter getRateLimiter() { + return transport.getRateLimiter(); + } + private boolean sample() { // https://docs.sentry.io/development/sdk-dev/features/#event-sampling if (options.getSampleRate() != null && random != null) { diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 7b33347cbcb..7fdbf8d9cf7 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -103,9 +103,6 @@ public class SentryOptions { */ private boolean debug; - /** Turns NDK on or off. Default is enabled. */ - private boolean enableNdk = true; - /** Logger interface to log useful debugging information if debug is enabled */ private @NotNull ILogger logger = NoOpLogger.getInstance(); @@ -291,12 +288,6 @@ public class SentryOptions { private final @NotNull List optionsObservers = new CopyOnWriteArrayList<>(); - /** - * Enable the Java to NDK Scope sync. The default value for sentry-java is disabled and enabled - * for sentry-android. - */ - private boolean enableScopeSync; - /** * Enables loading additional options from external locations like {@code sentry.properties} file * or environment variables, system properties. @@ -434,6 +425,9 @@ public class SentryOptions { private final @NotNull FullyDisplayedReporter fullyDisplayedReporter = FullyDisplayedReporter.getInstance(); + private @NotNull IConnectionStatusProvider connectionStatusProvider = + new NoOpConnectionStatusProvider(); + /** Whether Sentry should be enabled */ private boolean enabled = true; @@ -601,24 +595,6 @@ public void setEnvelopeReader(final @Nullable IEnvelopeReader envelopeReader) { envelopeReader != null ? envelopeReader : NoOpEnvelopeReader.getInstance(); } - /** - * Check if NDK is ON or OFF Default is ON - * - * @return true if ON or false otherwise - */ - public boolean isEnableNdk() { - return enableNdk; - } - - /** - * Sets NDK to ON or OFF - * - * @param enableNdk true if ON or false otherwise - */ - public void setEnableNdk(boolean enableNdk) { - this.enableNdk = enableNdk; - } - /** * Returns the shutdown timeout in Millis * @@ -1398,24 +1374,6 @@ public List getOptionsObservers() { return optionsObservers; } - /** - * Returns if the Java to NDK Scope sync is enabled - * - * @return true if enabled or false otherwise - */ - public boolean isEnableScopeSync() { - return enableScopeSync; - } - - /** - * Enables or not the Java to NDK Scope sync - * - * @param enableScopeSync true if enabled or false otherwise - */ - public void setEnableScopeSync(boolean enableScopeSync) { - this.enableScopeSync = enableScopeSync; - } - /** * Returns if loading properties from external sources is enabled. * @@ -2220,6 +2178,16 @@ public void addCollector(final @NotNull ICollector collector) { return collectors; } + @NotNull + public IConnectionStatusProvider getConnectionStatusProvider() { + return connectionStatusProvider; + } + + public void setConnectionStatusProvider( + final @NotNull IConnectionStatusProvider connectionStatusProvider) { + this.connectionStatusProvider = connectionStatusProvider; + } + /** The BeforeSend callback */ public interface BeforeSendCallback { diff --git a/sentry/src/main/java/io/sentry/SentryTracer.java b/sentry/src/main/java/io/sentry/SentryTracer.java index e3cc1498e50..bdea0885959 100644 --- a/sentry/src/main/java/io/sentry/SentryTracer.java +++ b/sentry/src/main/java/io/sentry/SentryTracer.java @@ -37,10 +37,14 @@ public final class SentryTracer implements ITransaction { */ private @NotNull FinishStatus finishStatus = FinishStatus.NOT_FINISHED; - private volatile @Nullable TimerTask timerTask; + private volatile @Nullable TimerTask idleTimeoutTask; + private volatile @Nullable TimerTask deadlineTimeoutTask; + private volatile @Nullable Timer timer = null; private final @NotNull Object timerLock = new Object(); - private final @NotNull AtomicBoolean isFinishTimerRunning = new AtomicBoolean(false); + + private final @NotNull AtomicBoolean isIdleFinishTimerRunning = new AtomicBoolean(false); + private final @NotNull AtomicBoolean isDeadlineTimerRunning = new AtomicBoolean(false); private final @NotNull Baggage baggage; private @NotNull TransactionNameSource transactionNameSource; @@ -92,8 +96,11 @@ public SentryTracer( transactionPerformanceCollector.start(this); } - if (transactionOptions.getIdleTimeout() != null) { + if (transactionOptions.getIdleTimeout() != null + || transactionOptions.getDeadlineTimeout() != null) { timer = new Timer(true); + + scheduleDeadlineTimeout(); scheduleFinish(); } } @@ -101,38 +108,53 @@ public SentryTracer( @Override public void scheduleFinish() { synchronized (timerLock) { - cancelTimer(); if (timer != null) { - isFinishTimerRunning.set(true); - timerTask = - new TimerTask() { - @Override - public void run() { - finishFromTimer(); - } - }; - - try { - timer.schedule(timerTask, transactionOptions.getIdleTimeout()); - } catch (Throwable e) { - hub.getOptions() - .getLogger() - .log(SentryLevel.WARNING, "Failed to schedule finish timer", e); - // if we failed to schedule the finish timer for some reason, we finish it here right away - finishFromTimer(); + final @Nullable Long idleTimeout = transactionOptions.getIdleTimeout(); + + if (idleTimeout != null) { + cancelIdleTimer(); + isIdleFinishTimerRunning.set(true); + idleTimeoutTask = + new TimerTask() { + @Override + public void run() { + onIdleTimeoutReached(); + } + }; + + try { + timer.schedule(idleTimeoutTask, idleTimeout); + } catch (Throwable e) { + hub.getOptions() + .getLogger() + .log(SentryLevel.WARNING, "Failed to schedule finish timer", e); + // if we failed to schedule the finish timer for some reason, we finish it here right + // away + onIdleTimeoutReached(); + } } } } } - private void finishFromTimer() { - final SpanStatus status = getStatus(); + private void onIdleTimeoutReached() { + final @Nullable SpanStatus status = getStatus(); finish((status != null) ? status : SpanStatus.OK); - isFinishTimerRunning.set(false); + isIdleFinishTimerRunning.set(false); + } + + private void onDeadlineTimeoutReached() { + final @Nullable SpanStatus status = getStatus(); + forceFinish( + (status != null) ? status : SpanStatus.DEADLINE_EXCEEDED, + transactionOptions.getIdleTimeout() != null, + null); + isDeadlineTimerRunning.set(false); } @Override - public @NotNull void forceFinish(@NotNull SpanStatus status, boolean dropIfNoChildren) { + public @NotNull void forceFinish( + final @NotNull SpanStatus status, final boolean dropIfNoChildren, final @Nullable Hint hint) { if (isFinished()) { return; } @@ -148,12 +170,15 @@ private void finishFromTimer() { span.setSpanFinishedCallback(null); span.finish(status, finishTimestamp); } - finish(status, finishTimestamp, dropIfNoChildren); + finish(status, finishTimestamp, dropIfNoChildren, hint); } @Override public void finish( - @Nullable SpanStatus status, @Nullable SentryDate finishDate, boolean dropIfNoChildren) { + @Nullable SpanStatus status, + @Nullable SentryDate finishDate, + boolean dropIfNoChildren, + @Nullable Hint hint) { // try to get the high precision timestamp from the root span SentryDate finishTimestamp = root.getFinishDate(); @@ -193,14 +218,11 @@ public void finish( performanceCollectionData.clear(); } - // finish unfinished children - for (final Span child : children) { - if (!child.isFinished()) { - child.setSpanFinishedCallback( - null); // reset the callback, as we're already in the finish method - child.finish(SpanStatus.DEADLINE_EXCEEDED, finishTimestamp); - } - } + // any un-finished childs will remain unfinished + // as relay takes care of setting the end-timestamp + deadline_exceeded + // see + // https://github.com/getsentry/relay/blob/40697d0a1c54e5e7ad8d183fc7f9543b94fe3839/relay-general/src/store/transactions/processor.rs#L374-L378 + root.finish(finishStatus.spanStatus, finishTimestamp); hub.configureScope( @@ -222,6 +244,8 @@ public void finish( if (timer != null) { synchronized (timerLock) { if (timer != null) { + cancelIdleTimer(); + cancelDeadlineTimer(); timer.cancel(); timer = null; } @@ -240,16 +264,55 @@ public void finish( } transaction.getMeasurements().putAll(measurements); - hub.captureTransaction(transaction, traceContext(), null, profilingTraceData); + hub.captureTransaction(transaction, traceContext(), hint, profilingTraceData); + } + } + + private void cancelIdleTimer() { + synchronized (timerLock) { + if (idleTimeoutTask != null) { + idleTimeoutTask.cancel(); + isIdleFinishTimerRunning.set(false); + idleTimeoutTask = null; + } + } + } + + private void scheduleDeadlineTimeout() { + final @Nullable Long deadlineTimeOut = transactionOptions.getDeadlineTimeout(); + if (deadlineTimeOut != null) { + synchronized (timerLock) { + if (timer != null) { + cancelDeadlineTimer(); + isDeadlineTimerRunning.set(true); + deadlineTimeoutTask = + new TimerTask() { + @Override + public void run() { + onDeadlineTimeoutReached(); + } + }; + try { + timer.schedule(deadlineTimeoutTask, deadlineTimeOut); + } catch (Throwable e) { + hub.getOptions() + .getLogger() + .log(SentryLevel.WARNING, "Failed to schedule finish timer", e); + // if we failed to schedule the finish timer for some reason, we finish it here right + // away + onDeadlineTimeoutReached(); + } + } + } } } - private void cancelTimer() { + private void cancelDeadlineTimer() { synchronized (timerLock) { - if (timerTask != null) { - timerTask.cancel(); - isFinishTimerRunning.set(false); - timerTask = null; + if (deadlineTimeoutTask != null) { + deadlineTimeoutTask.cancel(); + isDeadlineTimerRunning.set(false); + deadlineTimeoutTask = null; } } } @@ -360,7 +423,7 @@ private ISpan createChild( Objects.requireNonNull(parentSpanId, "parentSpanId is required"); Objects.requireNonNull(operation, "operation is required"); - cancelTimer(); + cancelIdleTimer(); final Span span = new Span( root.getTraceId(), @@ -479,7 +542,7 @@ public void finish(@Nullable SpanStatus status) { @Override @ApiStatus.Internal public void finish(@Nullable SpanStatus status, @Nullable SentryDate finishDate) { - finish(status, finishDate, true); + finish(status, finishDate, true, null); } @Override @@ -720,8 +783,14 @@ Span getRoot() { @TestOnly @Nullable - TimerTask getTimerTask() { - return timerTask; + TimerTask getIdleTimeoutTask() { + return idleTimeoutTask; + } + + @TestOnly + @Nullable + TimerTask getDeadlineTimeoutTask() { + return deadlineTimeoutTask; } @TestOnly @@ -733,7 +802,13 @@ Timer getTimer() { @TestOnly @NotNull AtomicBoolean isFinishTimerRunning() { - return isFinishTimerRunning; + return isIdleFinishTimerRunning; + } + + @TestOnly + @NotNull + AtomicBoolean isDeadlineTimerRunning() { + return isDeadlineTimerRunning; } @TestOnly diff --git a/sentry/src/main/java/io/sentry/ShutdownHookIntegration.java b/sentry/src/main/java/io/sentry/ShutdownHookIntegration.java index c6a8785e46a..b144f2d88a3 100644 --- a/sentry/src/main/java/io/sentry/ShutdownHookIntegration.java +++ b/sentry/src/main/java/io/sentry/ShutdownHookIntegration.java @@ -1,5 +1,7 @@ package io.sentry; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; + import io.sentry.util.Objects; import java.io.Closeable; import java.io.IOException; @@ -33,7 +35,7 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio thread = new Thread(() -> hub.flush(options.getFlushTimeoutMillis())); runtime.addShutdownHook(thread); options.getLogger().log(SentryLevel.DEBUG, "ShutdownHookIntegration installed."); - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } else { options.getLogger().log(SentryLevel.INFO, "enableShutdownHook is disabled."); } diff --git a/sentry/src/main/java/io/sentry/TransactionOptions.java b/sentry/src/main/java/io/sentry/TransactionOptions.java index 3362d979408..0ae4b94ace3 100644 --- a/sentry/src/main/java/io/sentry/TransactionOptions.java +++ b/sentry/src/main/java/io/sentry/TransactionOptions.java @@ -1,10 +1,13 @@ package io.sentry; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; /** Sentry Transaction options */ public final class TransactionOptions extends SpanOptions { + @ApiStatus.Internal public static final long DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION = 300000; + /** * Arbitrary data used in {@link SamplingContext} to determine if transaction is going to be * sampled. @@ -34,6 +37,16 @@ public final class TransactionOptions extends SpanOptions { */ private @Nullable Long idleTimeout = null; + /** + * The deadline time, measured in ms, to wait until the transaction will be force-finished with + * deadline-exceeded status./ + * + *

When set to {@code null} the transaction won't be forcefully finished. + * + *

The default is 30 seconds. + */ + private @Nullable Long deadlineTimeout = null; + /** * When `waitForChildren` is set to `true` and this callback is set, it's called before the * transaction is captured. @@ -121,6 +134,28 @@ public void setWaitForChildren(boolean waitForChildren) { return idleTimeout; } + /** + * Sets the deadlineTimeout. If set, an transaction and it's child spans will be force-finished + * with status {@link SpanStatus#DEADLINE_EXCEEDED} in case the transaction isn't finished in + * time. + * + * @param deadlineTimeoutMs - the deadlineTimeout, in ms - or null if no deadline should be set + */ + @ApiStatus.Internal + public void setDeadlineTimeout(@Nullable Long deadlineTimeoutMs) { + this.deadlineTimeout = deadlineTimeoutMs; + } + + /** + * Gets the deadlineTimeout + * + * @return deadlineTimeout - the deadlineTimeout, in ms - or null if no deadline is set + */ + @ApiStatus.Internal + public @Nullable Long getDeadlineTimeout() { + return deadlineTimeout; + } + /** * Sets the idleTimeout * diff --git a/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java b/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java index 1264e3780ec..33e1a4a815b 100644 --- a/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java +++ b/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java @@ -1,15 +1,19 @@ package io.sentry; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; + import com.jakewharton.nopen.annotation.Open; import io.sentry.exception.ExceptionMechanismException; import io.sentry.hints.BlockingFlushHint; import io.sentry.hints.EventDropReason; import io.sentry.hints.SessionEnd; +import io.sentry.hints.TransactionEnd; 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.atomic.AtomicReference; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -79,7 +83,7 @@ public final void register(final @NotNull IHub hub, final @NotNull SentryOptions this.options .getLogger() .log(SentryLevel.DEBUG, "UncaughtExceptionHandlerIntegration installed."); - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } } @@ -95,6 +99,11 @@ public void uncaughtException(Thread thread, Throwable thrown) { final SentryEvent event = new SentryEvent(throwable); event.setLevel(SentryLevel.FATAL); + final ITransaction transaction = hub.getTransaction(); + if (transaction == null && event.getEventId() != null) { + // if there's no active transaction on scope, this event can trigger flush notification + exceptionHint.setFlushable(event.getEventId()); + } final Hint hint = HintUtils.createWithTypeCheckHint(exceptionHint); final @NotNull SentryId sentryId = hub.captureEvent(event, hint); @@ -154,10 +163,24 @@ public void close() { @Open // open for tests @ApiStatus.Internal - public static class UncaughtExceptionHint extends BlockingFlushHint implements SessionEnd { + public static class UncaughtExceptionHint extends BlockingFlushHint + implements SessionEnd, TransactionEnd { + + private final AtomicReference flushableEventId = new AtomicReference<>(); public UncaughtExceptionHint(final long flushTimeoutMillis, final @NotNull ILogger logger) { super(flushTimeoutMillis, logger); } + + @Override + public boolean isFlushable(final @Nullable SentryId eventId) { + final SentryId unwrapped = flushableEventId.get(); + return unwrapped != null && unwrapped.equals(eventId); + } + + @Override + public void setFlushable(final @NotNull SentryId eventId) { + flushableEventId.set(eventId); + } } } diff --git a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java index 24d7c62aa3d..0b5f8ac2fb7 100644 --- a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java +++ b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java @@ -67,6 +67,8 @@ public class EnvelopeCache extends CacheStrategy implements IEnvelopeCache { public static final String STARTUP_CRASH_MARKER_FILE = "startup_crash"; + private static final long SESSION_FLUSH_DISK_TIMEOUT_MS = 15000; + private final CountDownLatch previousSessionLatch; private final @NotNull Map fileNameMap = new WeakHashMap<>(); @@ -429,7 +431,9 @@ public void discard(final @NotNull SentryEnvelope envelope) { /** Awaits until the previous session (if any) is flushed to its own file. */ public boolean waitPreviousSessionFlush() { try { - return previousSessionLatch.await(options.getFlushTimeoutMillis(), TimeUnit.MILLISECONDS); + // use fixed timeout instead of configurable options.getFlushTimeoutMillis() to ensure there's + // enough time to flush the session to disk + return previousSessionLatch.await(SESSION_FLUSH_DISK_TIMEOUT_MS, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); options.getLogger().log(DEBUG, "Timed out waiting for previous session to flush."); diff --git a/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java b/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java index bfccc8d30f9..0c4a110733e 100644 --- a/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java +++ b/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java @@ -3,8 +3,8 @@ import static io.sentry.SentryLevel.ERROR; import io.sentry.Breadcrumb; -import io.sentry.IScopeObserver; import io.sentry.JsonDeserializer; +import io.sentry.ScopeObserverAdapter; import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.SpanContext; @@ -16,7 +16,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public final class PersistingScopeObserver implements IScopeObserver { +public final class PersistingScopeObserver extends ScopeObserverAdapter { public static final String SCOPE_CACHE = ".scope-cache"; public static final String USER_FILENAME = "user.json"; diff --git a/sentry/src/main/java/io/sentry/hints/AbnormalExit.java b/sentry/src/main/java/io/sentry/hints/AbnormalExit.java index d8d5d304541..f9e67bc7607 100644 --- a/sentry/src/main/java/io/sentry/hints/AbnormalExit.java +++ b/sentry/src/main/java/io/sentry/hints/AbnormalExit.java @@ -10,13 +10,9 @@ public interface AbnormalExit { String mechanism(); /** Whether the current thread should be ignored from being marked as crashed, e.g. a watchdog */ - default boolean ignoreCurrentThread() { - return false; - } + boolean ignoreCurrentThread(); /** When exactly the abnormal exit happened */ @Nullable - default Long timestamp() { - return null; - } + Long timestamp(); } diff --git a/sentry/src/main/java/io/sentry/hints/DiskFlushNotification.java b/sentry/src/main/java/io/sentry/hints/DiskFlushNotification.java index 52d32e8506b..cfe54d83119 100644 --- a/sentry/src/main/java/io/sentry/hints/DiskFlushNotification.java +++ b/sentry/src/main/java/io/sentry/hints/DiskFlushNotification.java @@ -1,5 +1,13 @@ package io.sentry.hints; +import io.sentry.protocol.SentryId; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + public interface DiskFlushNotification { void markFlushed(); + + boolean isFlushable(@Nullable SentryId eventId); + + void setFlushable(@NotNull SentryId eventId); } diff --git a/sentry/src/main/java/io/sentry/hints/Enqueable.java b/sentry/src/main/java/io/sentry/hints/Enqueable.java new file mode 100644 index 00000000000..96d16c714cb --- /dev/null +++ b/sentry/src/main/java/io/sentry/hints/Enqueable.java @@ -0,0 +1,6 @@ +package io.sentry.hints; + +/** Marker interface for envelopes to notify when they are submitted to the http transport queue */ +public interface Enqueable { + void markEnqueued(); +} 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 accee3b0510..956996ce04b 100644 --- a/sentry/src/main/java/io/sentry/instrumentation/file/FileIOSpanManager.java +++ b/sentry/src/main/java/io/sentry/instrumentation/file/FileIOSpanManager.java @@ -29,7 +29,7 @@ final class FileIOSpanManager { private final @NotNull SentryStackTraceFactory stackTraceFactory; static @Nullable ISpan startSpan(final @NotNull IHub hub, final @NotNull String op) { - final ISpan parent = hub.getSpan(); + final ISpan parent = Platform.isAndroid() ? hub.getTransaction() : hub.getSpan(); return parent != null ? parent.startChild(op) : null; } diff --git a/sentry/src/main/java/io/sentry/protocol/SentrySpan.java b/sentry/src/main/java/io/sentry/protocol/SentrySpan.java index 70b1b635cc3..b627aa4c9f5 100644 --- a/sentry/src/main/java/io/sentry/protocol/SentrySpan.java +++ b/sentry/src/main/java/io/sentry/protocol/SentrySpan.java @@ -61,8 +61,10 @@ public SentrySpan(final @NotNull Span span, final @Nullable Map this.tags = tagsCopy != null ? tagsCopy : new ConcurrentHashMap<>(); // we lose precision here, from potential nanosecond precision down to 10 microsecond precision this.timestamp = - DateUtils.nanosToSeconds( - span.getStartDate().laterDateNanosTimestampByDiff(span.getFinishDate())); + span.getFinishDate() == null + ? null + : DateUtils.nanosToSeconds( + span.getStartDate().laterDateNanosTimestampByDiff(span.getFinishDate())); // we lose precision here, from potential nanosecond precision down to 10 microsecond precision this.startTimestamp = DateUtils.nanosToSeconds(span.getStartDate().nanoTimestamp()); this.data = data; diff --git a/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java b/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java index cbd54947264..7efbcbcaf3e 100644 --- a/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java +++ b/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java @@ -13,6 +13,7 @@ import io.sentry.clientreport.DiscardReason; import io.sentry.hints.Cached; import io.sentry.hints.DiskFlushNotification; +import io.sentry.hints.Enqueable; import io.sentry.hints.Retryable; import io.sentry.hints.SubmissionResult; import io.sentry.util.HintUtils; @@ -103,6 +104,14 @@ public void send(final @NotNull SentryEnvelope envelope, final @NotNull Hint hin options .getClientReportRecorder() .recordLostEnvelope(DiscardReason.QUEUE_OVERFLOW, envelopeThatMayIncludeClientReport); + } else { + HintUtils.runIfHasType( + hint, + Enqueable.class, + enqueable -> { + enqueable.markEnqueued(); + options.getLogger().log(SentryLevel.DEBUG, "Envelope enqueued"); + }); } } } @@ -135,12 +144,17 @@ private static QueuedThreadPoolExecutor initExecutor( 1, maxQueueSize, new AsyncConnectionThreadFactory(), storeEvents, logger); } + @Override + public @NotNull RateLimiter getRateLimiter() { + return rateLimiter; + } + @Override public void close() throws IOException { executor.shutdown(); options.getLogger().log(SentryLevel.DEBUG, "Shutting down"); try { - if (!executor.awaitTermination(1, TimeUnit.MINUTES)) { + if (!executor.awaitTermination(options.getFlushTimeoutMillis(), TimeUnit.MILLISECONDS)) { options .getLogger() .log( @@ -230,8 +244,16 @@ public void run() { hint, DiskFlushNotification.class, (diskFlushNotification) -> { - diskFlushNotification.markFlushed(); - options.getLogger().log(SentryLevel.DEBUG, "Disk flush envelope fired"); + if (diskFlushNotification.isFlushable(envelope.getHeader().getEventId())) { + diskFlushNotification.markFlushed(); + options.getLogger().log(SentryLevel.DEBUG, "Disk flush envelope fired"); + } else { + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Not firing envelope flush as there's an ongoing transaction"); + } }); if (transportGate.isConnected()) { diff --git a/sentry/src/main/java/io/sentry/transport/ITransport.java b/sentry/src/main/java/io/sentry/transport/ITransport.java index 131a5f04070..09fc034246c 100644 --- a/sentry/src/main/java/io/sentry/transport/ITransport.java +++ b/sentry/src/main/java/io/sentry/transport/ITransport.java @@ -5,6 +5,7 @@ import java.io.Closeable; import java.io.IOException; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** A transport is in charge of sending the event to the Sentry server. */ public interface ITransport extends Closeable { @@ -20,4 +21,7 @@ default void send(@NotNull SentryEnvelope envelope) throws IOException { * @param timeoutMillis time in milliseconds */ void flush(long timeoutMillis); + + @Nullable + RateLimiter getRateLimiter(); } diff --git a/sentry/src/main/java/io/sentry/transport/NoOpEnvelopeCache.java b/sentry/src/main/java/io/sentry/transport/NoOpEnvelopeCache.java index 27ce1dc3c39..d4902cf8b26 100644 --- a/sentry/src/main/java/io/sentry/transport/NoOpEnvelopeCache.java +++ b/sentry/src/main/java/io/sentry/transport/NoOpEnvelopeCache.java @@ -3,7 +3,7 @@ import io.sentry.Hint; import io.sentry.SentryEnvelope; import io.sentry.cache.IEnvelopeCache; -import java.util.ArrayList; +import java.util.Collections; import java.util.Iterator; import org.jetbrains.annotations.NotNull; @@ -23,6 +23,6 @@ public void discard(@NotNull SentryEnvelope envelope) {} @NotNull @Override public Iterator iterator() { - return new ArrayList(0).iterator(); + return Collections.emptyIterator(); } } diff --git a/sentry/src/main/java/io/sentry/transport/NoOpTransport.java b/sentry/src/main/java/io/sentry/transport/NoOpTransport.java index 79d639ee0ba..f73605b049d 100644 --- a/sentry/src/main/java/io/sentry/transport/NoOpTransport.java +++ b/sentry/src/main/java/io/sentry/transport/NoOpTransport.java @@ -5,6 +5,7 @@ import java.io.IOException; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; @ApiStatus.Internal public final class NoOpTransport implements ITransport { @@ -24,6 +25,11 @@ public void send(final @NotNull SentryEnvelope envelope, final @NotNull Hint hin @Override public void flush(long timeoutMillis) {} + @Override + public @Nullable RateLimiter getRateLimiter() { + return null; + } + @Override public void close() throws IOException {} } diff --git a/sentry/src/main/java/io/sentry/transport/RateLimiter.java b/sentry/src/main/java/io/sentry/transport/RateLimiter.java index 0ad8dbc55a3..ed4c04c6309 100644 --- a/sentry/src/main/java/io/sentry/transport/RateLimiter.java +++ b/sentry/src/main/java/io/sentry/transport/RateLimiter.java @@ -48,7 +48,7 @@ public RateLimiter(final @NotNull SentryOptions options) { // Optimize for/No allocations if no items are under 429 List dropItems = null; for (SentryEnvelopeItem item : envelope.getItems()) { - // using the raw value of the enum to not expose SentryEnvelopeItemType + // using the raw value of the enum to not expose SentryEnvelopeItemType if (isRetryAfter(item.getHeader().getType().getItemType())) { if (dropItems == null) { dropItems = new ArrayList<>(); @@ -87,26 +87,8 @@ public RateLimiter(final @NotNull SentryOptions options) { return envelope; } - /** - * It marks the hint when sending has failed, so it's not necessary to wait the timeout - * - * @param hint the Hints - * @param retry if event should be retried or not - */ - private static void markHintWhenSendingFailed(final @NotNull Hint hint, final boolean retry) { - HintUtils.runIfHasType(hint, SubmissionResult.class, result -> result.setResult(false)); - HintUtils.runIfHasType(hint, Retryable.class, retryable -> retryable.setRetry(retry)); - } - - /** - * Check if an itemType is retry after or not - * - * @param itemType the itemType (eg event, session, etc...) - * @return true if retry after or false otherwise - */ @SuppressWarnings({"JdkObsolete", "JavaUtilDate"}) - private boolean isRetryAfter(final @NotNull String itemType) { - final DataCategory dataCategory = getCategoryFromItemType(itemType); + public boolean isActiveForCategory(final @NotNull DataCategory dataCategory) { final Date currentDate = new Date(currentDateProvider.getCurrentTimeMillis()); // check all categories @@ -131,6 +113,29 @@ private boolean isRetryAfter(final @NotNull String itemType) { return false; } + /** + * It marks the hint when sending has failed, so it's not necessary to wait the timeout + * + * @param hint the Hints + * @param retry if event should be retried or not + */ + private static void markHintWhenSendingFailed(final @NotNull Hint hint, final boolean retry) { + HintUtils.runIfHasType(hint, SubmissionResult.class, result -> result.setResult(false)); + HintUtils.runIfHasType(hint, Retryable.class, retryable -> retryable.setRetry(retry)); + } + + /** + * Check if an itemType is retry after or not + * + * @param itemType the itemType (eg event, session, etc...) + * @return true if retry after or false otherwise + */ + @SuppressWarnings({"JdkObsolete", "JavaUtilDate"}) + private boolean isRetryAfter(final @NotNull String itemType) { + final DataCategory dataCategory = getCategoryFromItemType(itemType); + return isActiveForCategory(dataCategory); + } + /** * Returns a rate limiting category from item itemType * diff --git a/sentry/src/main/java/io/sentry/transport/StdoutTransport.java b/sentry/src/main/java/io/sentry/transport/StdoutTransport.java index c503df9f94b..99aed10eac7 100644 --- a/sentry/src/main/java/io/sentry/transport/StdoutTransport.java +++ b/sentry/src/main/java/io/sentry/transport/StdoutTransport.java @@ -6,6 +6,7 @@ import io.sentry.util.Objects; import java.io.IOException; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public final class StdoutTransport implements ITransport { @@ -33,6 +34,11 @@ public void flush(long timeoutMillis) { System.out.println("Flushing"); } + @Override + public @Nullable RateLimiter getRateLimiter() { + return null; + } + @Override public void close() {} } diff --git a/sentry/src/main/java/io/sentry/util/IntegrationUtils.java b/sentry/src/main/java/io/sentry/util/IntegrationUtils.java new file mode 100644 index 00000000000..6d504c14516 --- /dev/null +++ b/sentry/src/main/java/io/sentry/util/IntegrationUtils.java @@ -0,0 +1,23 @@ +package io.sentry.util; + +import io.sentry.SentryIntegrationPackageStorage; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public final class IntegrationUtils { + public static void addIntegrationToSdkVersion(final @NotNull Class clazz) { + final String name = + clazz + .getSimpleName() + .replace("Sentry", "") + .replace("Integration", "") + .replace("Interceptor", "") + .replace("EventProcessor", ""); + addIntegrationToSdkVersion(name); + } + + public static void addIntegrationToSdkVersion(final @NotNull String name) { + SentryIntegrationPackageStorage.getInstance().addIntegration(name); + } +} diff --git a/sentry/src/main/java/io/sentry/util/Platform.java b/sentry/src/main/java/io/sentry/util/Platform.java index 0a5d06cec51..b08b6e584fb 100644 --- a/sentry/src/main/java/io/sentry/util/Platform.java +++ b/sentry/src/main/java/io/sentry/util/Platform.java @@ -6,7 +6,7 @@ @ApiStatus.Internal public final class Platform { - private static boolean isAndroid; + static boolean isAndroid; static boolean isJavaNinePlus; static { diff --git a/sentry/src/main/java/io/sentry/util/thread/IMainThreadChecker.java b/sentry/src/main/java/io/sentry/util/thread/IMainThreadChecker.java index 19cd1c76a0a..cf763b49592 100644 --- a/sentry/src/main/java/io/sentry/util/thread/IMainThreadChecker.java +++ b/sentry/src/main/java/io/sentry/util/thread/IMainThreadChecker.java @@ -15,18 +15,14 @@ public interface IMainThreadChecker { * @param thread the Thread * @return true if it is the main thread or false otherwise */ - default boolean isMainThread(Thread thread) { - return isMainThread(thread.getId()); - } + boolean isMainThread(final @NotNull Thread thread); /** * Checks if the calling/current thread is the Main/UI thread * * @return true if it is the main thread or false otherwise */ - default boolean isMainThread() { - return isMainThread(Thread.currentThread()); - } + boolean isMainThread(); /** * Checks if a given thread is the Main/UI thread @@ -34,8 +30,5 @@ default boolean isMainThread() { * @param sentryThread the SentryThread * @return true if it is the main thread or false otherwise */ - default boolean isMainThread(final @NotNull SentryThread sentryThread) { - final Long threadId = sentryThread.getId(); - return threadId != null && isMainThread(threadId); - } + boolean isMainThread(final @NotNull SentryThread sentryThread); } diff --git a/sentry/src/main/java/io/sentry/util/thread/MainThreadChecker.java b/sentry/src/main/java/io/sentry/util/thread/MainThreadChecker.java index 0f61c29ce6f..c81ccbd6683 100644 --- a/sentry/src/main/java/io/sentry/util/thread/MainThreadChecker.java +++ b/sentry/src/main/java/io/sentry/util/thread/MainThreadChecker.java @@ -1,6 +1,8 @@ package io.sentry.util.thread; +import io.sentry.protocol.SentryThread; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; /** * Class that checks if a given thread is the Main/UI thread. The Main thread is denoted by the @@ -25,4 +27,20 @@ private MainThreadChecker() {} public boolean isMainThread(long threadId) { return mainThreadId == threadId; } + + @Override + public boolean isMainThread(final @NotNull Thread thread) { + return isMainThread(thread.getId()); + } + + @Override + public boolean isMainThread() { + return isMainThread(Thread.currentThread()); + } + + @Override + public boolean isMainThread(final @NotNull SentryThread sentryThread) { + final Long threadId = sentryThread.getId(); + return threadId != null && isMainThread(threadId); + } } diff --git a/sentry/src/main/java/io/sentry/util/thread/NoOpMainThreadChecker.java b/sentry/src/main/java/io/sentry/util/thread/NoOpMainThreadChecker.java index bc1b6e58963..2248e363a4a 100644 --- a/sentry/src/main/java/io/sentry/util/thread/NoOpMainThreadChecker.java +++ b/sentry/src/main/java/io/sentry/util/thread/NoOpMainThreadChecker.java @@ -1,6 +1,8 @@ package io.sentry.util.thread; +import io.sentry.protocol.SentryThread; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; @ApiStatus.Internal public final class NoOpMainThreadChecker implements IMainThreadChecker { @@ -15,4 +17,19 @@ public static NoOpMainThreadChecker getInstance() { public boolean isMainThread(long threadId) { return false; } + + @Override + public boolean isMainThread(@NotNull Thread thread) { + return false; + } + + @Override + public boolean isMainThread() { + return false; + } + + @Override + public boolean isMainThread(@NotNull SentryThread sentryThread) { + return false; + } } diff --git a/sentry/src/test/java/io/sentry/DirectoryProcessorTest.kt b/sentry/src/test/java/io/sentry/DirectoryProcessorTest.kt index 718aff315c8..e87f4256d58 100644 --- a/sentry/src/test/java/io/sentry/DirectoryProcessorTest.kt +++ b/sentry/src/test/java/io/sentry/DirectoryProcessorTest.kt @@ -1,11 +1,15 @@ package io.sentry import io.sentry.hints.ApplyScopeData -import io.sentry.protocol.User +import io.sentry.hints.Enqueable +import io.sentry.hints.Retryable +import io.sentry.transport.RateLimiter import io.sentry.util.HintUtils import io.sentry.util.noFlushTimeout import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argWhere +import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never @@ -34,8 +38,31 @@ class DirectoryProcessorTest { options.setLogger(logger) } - fun getSut(): OutboxSender { - return OutboxSender(hub, envelopeReader, serializer, logger, 15000) + fun getSut(isRetryable: Boolean = false, isRateLimitingActive: Boolean = false): OutboxSender { + val hintCaptor = argumentCaptor() + whenever(hub.captureEvent(any(), hintCaptor.capture())).then { + HintUtils.runIfHasType( + hintCaptor.firstValue, + Enqueable::class.java + ) { enqueable: Enqueable -> + enqueable.markEnqueued() + + // activate rate limiting when a first envelope was processed + if (isRateLimitingActive) { + val rateLimiter = mock { + whenever(mock.isActiveForCategory(any())).thenReturn(true) + } + whenever(hub.rateLimiter).thenReturn(rateLimiter) + } + } + HintUtils.runIfHasType( + hintCaptor.firstValue, + Retryable::class.java + ) { retryable -> + retryable.isRetry = isRetryable + } + } + return OutboxSender(hub, envelopeReader, serializer, logger, 500, 30) } } @@ -57,9 +84,6 @@ class DirectoryProcessorTest { fun `process directory folder has a non ApplyScopeData hint`() { val path = getTempEnvelope("envelope-event-attachment.txt") assertTrue(File(path).exists()) // sanity check -// val session = createSession() -// whenever(fixture.envelopeReader.read(any())).thenReturn(SentryEnvelope.from(fixture.serializer, session, null)) -// whenever(fixture.serializer.deserializeSession(any())).thenReturn(session) val event = SentryEvent() val envelope = SentryEnvelope.from(fixture.serializer, event, null) @@ -79,6 +103,45 @@ class DirectoryProcessorTest { verify(fixture.hub, never()).captureEnvelope(any(), any()) } + @Test + fun `when envelope has already been submitted to the queue, does not process it again`() { + getTempEnvelope("envelope-event-attachment.txt") + + val event = SentryEvent() + val envelope = SentryEnvelope.from(fixture.serializer, event, null) + + whenever(fixture.envelopeReader.read(any())).thenReturn(envelope) + whenever(fixture.serializer.deserialize(any(), eq(SentryEvent::class.java))).thenReturn(event) + + // make it retryable so it doesn't get deleted + val sut = fixture.getSut(isRetryable = true) + sut.processDirectory(file) + + // process it once again + sut.processDirectory(file) + + // should only capture once + verify(fixture.hub).captureEvent(any(), anyOrNull()) + } + + @Test + fun `when rate limiting gets active in the middle of processing, stops processing`() { + getTempEnvelope("envelope-event-attachment.txt") + getTempEnvelope("envelope-event-attachment.txt") + + val event = SentryEvent() + val envelope = SentryEnvelope.from(fixture.serializer, event, null) + + whenever(fixture.envelopeReader.read(any())).thenReturn(envelope) + whenever(fixture.serializer.deserialize(any(), eq(SentryEvent::class.java))).thenReturn(event) + + val sut = fixture.getSut(isRateLimitingActive = true) + sut.processDirectory(file) + + // should only capture once + verify(fixture.hub).captureEvent(any(), anyOrNull()) + } + private fun getTempEnvelope(fileName: String): String { val testFile = this::class.java.classLoader.getResource(fileName) val testFileBytes = testFile!!.readBytes() @@ -86,8 +149,4 @@ class DirectoryProcessorTest { Files.write(Paths.get(targetFile.toURI()), testFileBytes) return targetFile.absolutePath } - - private fun createSession(): Session { - return Session("123", User(), "env", "release") - } } diff --git a/sentry/src/test/java/io/sentry/EnvelopeSenderTest.kt b/sentry/src/test/java/io/sentry/EnvelopeSenderTest.kt index ded72ebf382..6f0ea9cb8a6 100644 --- a/sentry/src/test/java/io/sentry/EnvelopeSenderTest.kt +++ b/sentry/src/test/java/io/sentry/EnvelopeSenderTest.kt @@ -5,9 +5,11 @@ import io.sentry.hints.Retryable import io.sentry.util.HintUtils import io.sentry.util.noFlushTimeout import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.doThrow import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever @@ -32,7 +34,13 @@ class EnvelopeSenderTest { } fun getSut(): EnvelopeSender { - return EnvelopeSender(hub!!, serializer!!, logger!!, options.flushTimeoutMillis) + return EnvelopeSender( + hub!!, + serializer!!, + logger!!, + options.flushTimeoutMillis, + options.maxQueueSize + ) } } @@ -74,7 +82,7 @@ class EnvelopeSenderTest { sut.processDirectory(File(tempDirectory.toUri())) testFile.deleteOnExit() verify(fixture.logger)!!.log(eq(SentryLevel.DEBUG), eq("File '%s' doesn't match extension expected."), any()) - verifyNoMoreInteractions(fixture.hub) + verify(fixture.hub, never())!!.captureEnvelope(any(), anyOrNull()) } @Test diff --git a/sentry/src/test/java/io/sentry/HubAdapterTest.kt b/sentry/src/test/java/io/sentry/HubAdapterTest.kt index aa302efb34e..0d1713e41d1 100644 --- a/sentry/src/test/java/io/sentry/HubAdapterTest.kt +++ b/sentry/src/test/java/io/sentry/HubAdapterTest.kt @@ -232,6 +232,11 @@ class HubAdapterTest { verify(hub).span } + @Test fun `getTransaction calls Hub`() { + HubAdapter.getInstance().transaction + verify(hub).transaction + } + @Test fun `getOptions calls Hub`() { HubAdapter.getInstance().options verify(hub).options diff --git a/sentry/src/test/java/io/sentry/HubTest.kt b/sentry/src/test/java/io/sentry/HubTest.kt index 2a7991f1207..03fcbcaf336 100644 --- a/sentry/src/test/java/io/sentry/HubTest.kt +++ b/sentry/src/test/java/io/sentry/HubTest.kt @@ -1645,15 +1645,32 @@ class HubTest { @Test fun `when there is no active transaction, getSpan returns null`() { val hub = generateHub() - assertNull(hub.getSpan()) + assertNull(hub.span) } @Test - fun `when there is active transaction bound to the scope, getSpan returns active transaction`() { + fun `when there is no active transaction, getTransaction returns null`() { + val hub = generateHub() + assertNull(hub.transaction) + } + + @Test + fun `when there is active transaction bound to the scope, getTransaction and getSpan return active transaction`() { val hub = generateHub() val tx = hub.startTransaction("aTransaction", "op") - hub.configureScope { it.setTransaction(tx) } - assertEquals(tx, hub.getSpan()) + hub.configureScope { it.transaction = tx } + + assertEquals(tx, hub.transaction) + assertEquals(tx, hub.span) + } + + @Test + fun `when there is a transaction but the hub is closed, getTransaction returns null`() { + val hub = generateHub() + hub.startTransaction("name", "op") + hub.close() + + assertNull(hub.transaction) } @Test @@ -1663,6 +1680,8 @@ class HubTest { hub.configureScope { it.setTransaction(tx) } hub.configureScope { it.setTransaction(tx) } val span = tx.startChild("op") + + assertEquals(tx, hub.transaction) assertEquals(span, hub.span) } // endregion diff --git a/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt b/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt index 4e6b4f3d99a..ec932ebc861 100644 --- a/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt +++ b/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt @@ -297,7 +297,7 @@ class MainEventProcessorTest { } @Test - fun `when event does not have ip address set and sendDefaultPii is set to true, sets {{auto}} as the ip address`() { + fun `when event does not have ip address set, sets {{auto}} as the ip address`() { val sut = fixture.getSut(sendDefaultPii = true) val event = SentryEvent() sut.process(event, Hint()) @@ -307,7 +307,7 @@ class MainEventProcessorTest { } @Test - fun `when event has ip address set and sendDefaultPii is set to true, keeps original ip address`() { + fun `when event has ip address set, keeps original ip address`() { val sut = fixture.getSut(sendDefaultPii = true) val event = SentryEvent() event.user = User().apply { @@ -319,17 +319,6 @@ class MainEventProcessorTest { } } - @Test - fun `when event does not have ip address set and sendDefaultPii is set to false, does not set ip address`() { - val sut = fixture.getSut(sendDefaultPii = false) - val event = SentryEvent() - event.user = User() - sut.process(event, Hint()) - assertNotNull(event.user) { - assertNull(it.ipAddress) - } - } - @Test fun `when event has environment set, does not overwrite environment`() { val sut = fixture.getSut(environment = null) @@ -628,5 +617,7 @@ class MainEventProcessorTest { override fun mechanism(): String? = null override fun ignoreCurrentThread(): Boolean = ignoreCurrentThread + + override fun timestamp(): Long? = null } } diff --git a/sentry/src/test/java/io/sentry/NoOpConnectionStatusProviderTest.kt b/sentry/src/test/java/io/sentry/NoOpConnectionStatusProviderTest.kt new file mode 100644 index 00000000000..0ccc911dcf4 --- /dev/null +++ b/sentry/src/test/java/io/sentry/NoOpConnectionStatusProviderTest.kt @@ -0,0 +1,33 @@ +package io.sentry + +import org.mockito.kotlin.mock +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull + +class NoOpConnectionStatusProviderTest { + + private val provider = NoOpConnectionStatusProvider() + + @Test + fun `provider returns unknown status`() { + assertEquals(IConnectionStatusProvider.ConnectionStatus.UNKNOWN, provider.connectionStatus) + } + + @Test + fun `connection type returns null`() { + assertNull(provider.connectionType) + } + + @Test + fun `adding a listener is a no-op and returns false`() { + val result = provider.addConnectionStatusObserver(mock()) + assertFalse(result) + } + + @Test + fun `removing a listener is a no-op`() { + provider.addConnectionStatusObserver(mock()) + } +} diff --git a/sentry/src/test/java/io/sentry/OutboxSenderTest.kt b/sentry/src/test/java/io/sentry/OutboxSenderTest.kt index 4f8f1d9860e..933fab0a21d 100644 --- a/sentry/src/test/java/io/sentry/OutboxSenderTest.kt +++ b/sentry/src/test/java/io/sentry/OutboxSenderTest.kt @@ -22,7 +22,6 @@ import java.util.Date import java.util.UUID import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -42,7 +41,7 @@ class OutboxSenderTest { } fun getSut(): OutboxSender { - return OutboxSender(hub, envelopeReader, serializer, logger, 15000) + return OutboxSender(hub, envelopeReader, serializer, logger, 15000, 30) } } @@ -275,38 +274,6 @@ class OutboxSenderTest { verify(fixture.logger).log(eq(SentryLevel.ERROR), any(), argWhere { it is FileNotFoundException }) } - @Test - fun `when hub is null, ctor throws`() { - val clazz = Class.forName("io.sentry.OutboxSender") - val ctor = clazz.getConstructor(IHub::class.java, IEnvelopeReader::class.java, ISerializer::class.java, ILogger::class.java, Long::class.java) - val params = arrayOf(null, mock(), mock(), mock(), null) - assertFailsWith { ctor.newInstance(params) } - } - - @Test - fun `when envelopeReader is null, ctor throws`() { - val clazz = Class.forName("io.sentry.OutboxSender") - val ctor = clazz.getConstructor(IHub::class.java, IEnvelopeReader::class.java, ISerializer::class.java, ILogger::class.java, Long::class.java) - val params = arrayOf(mock(), null, mock(), mock(), 15000) - assertFailsWith { ctor.newInstance(params) } - } - - @Test - fun `when serializer is null, ctor throws`() { - val clazz = Class.forName("io.sentry.OutboxSender") - val ctor = clazz.getConstructor(IHub::class.java, IEnvelopeReader::class.java, ISerializer::class.java, ILogger::class.java, Long::class.java) - val params = arrayOf(mock(), mock(), null, mock(), 15000) - assertFailsWith { ctor.newInstance(params) } - } - - @Test - fun `when logger is null, ctor throws`() { - val clazz = Class.forName("io.sentry.OutboxSender") - val ctor = clazz.getConstructor(IHub::class.java, IEnvelopeReader::class.java, ISerializer::class.java, ILogger::class.java, Long::class.java) - val params = arrayOf(mock(), mock(), mock(), null, 15000) - assertFailsWith { ctor.newInstance(params) } - } - @Test fun `when file name is null, should not be relevant`() { assertFalse(fixture.getSut().isRelevantFileName(null)) diff --git a/sentry/src/test/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegrationTest.kt b/sentry/src/test/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegrationTest.kt index a1c06d31dba..21a22861eb1 100644 --- a/sentry/src/test/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegrationTest.kt +++ b/sentry/src/test/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegrationTest.kt @@ -1,11 +1,15 @@ package io.sentry +import io.sentry.SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForget import io.sentry.protocol.SdkVersion +import io.sentry.test.ImmediateExecutorService +import io.sentry.transport.RateLimiter import org.mockito.kotlin.any 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.verifyNoMoreInteractions import org.mockito.kotlin.whenever import kotlin.test.Test import kotlin.test.assertFalse @@ -17,8 +21,10 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { var hub: IHub = mock() var logger: ILogger = mock() var options = SentryOptions() + val sender = mock() var callback = mock().apply { whenever(hasValidPath(any(), any())).thenCallRealMethod() + whenever(create(any(), any())).thenReturn(sender) } init { @@ -27,7 +33,10 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { options.sdkVersion = SdkVersion("test", "1.2.3") } - fun getSut(): SendCachedEnvelopeFireAndForgetIntegration { + fun getSut(useImmediateExecutor: Boolean = true): SendCachedEnvelopeFireAndForgetIntegration { + if (useImmediateExecutor) { + options.executorService = ImmediateExecutorService() + } return SendCachedEnvelopeFireAndForgetIntegration(callback) } } @@ -40,7 +49,7 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { val sut = fixture.getSut() sut.register(fixture.hub, fixture.options) verify(fixture.logger).log(eq(SentryLevel.ERROR), eq("No cache dir path is defined in options.")) - verifyNoMoreInteractions(fixture.hub) + verify(fixture.sender, never()).send() } @Test @@ -67,7 +76,7 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { fixture.options.cacheDirPath = "abc" sut.register(fixture.hub, fixture.options) verify(fixture.logger).log(eq(SentryLevel.ERROR), eq("SendFireAndForget factory is null.")) - verifyNoMoreInteractions(fixture.hub) + verify(fixture.sender, never()).send() } @Test @@ -87,11 +96,99 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { fixture.options.cacheDirPath = "cache" fixture.options.executorService.close(0) whenever(fixture.callback.create(any(), any())).thenReturn(mock()) - val sut = fixture.getSut() + val sut = fixture.getSut(useImmediateExecutor = false) sut.register(fixture.hub, fixture.options) verify(fixture.logger).log(eq(SentryLevel.ERROR), eq("Failed to call the executor. Cached events will not be sent. Did you call Sentry.close()?"), any()) } + @Test + fun `registers for network connection changes`() { + val connectionStatusProvider = mock() + fixture.options.connectionStatusProvider = connectionStatusProvider + fixture.options.cacheDirPath = "cache" + + val sut = fixture.getSut() + sut.register(fixture.hub, fixture.options) + verify(connectionStatusProvider).addConnectionStatusObserver(any()) + } + + @Test + fun `when theres no network connection does nothing`() { + val connectionStatusProvider = mock() + whenever(connectionStatusProvider.connectionStatus).thenReturn( + IConnectionStatusProvider.ConnectionStatus.DISCONNECTED + ) + fixture.options.connectionStatusProvider = connectionStatusProvider + fixture.options.cacheDirPath = "cache" + + val sut = fixture.getSut() + sut.register(fixture.hub, fixture.options) + + sut.register(fixture.hub, fixture.options) + verify(fixture.sender, never()).send() + } + + @Test + fun `when the network is not disconnected the factory is initialized`() { + val connectionStatusProvider = mock() + whenever(connectionStatusProvider.connectionStatus).thenReturn( + IConnectionStatusProvider.ConnectionStatus.UNKNOWN + ) + fixture.options.connectionStatusProvider = connectionStatusProvider + fixture.options.cacheDirPath = "cache" + + val sut = fixture.getSut() + sut.register(fixture.hub, fixture.options) + + verify(fixture.sender).send() + } + + @Test + fun `whenever network connection status changes, retries sending for relevant statuses`() { + val connectionStatusProvider = mock() + whenever(connectionStatusProvider.connectionStatus).thenReturn( + IConnectionStatusProvider.ConnectionStatus.DISCONNECTED + ) + fixture.options.connectionStatusProvider = connectionStatusProvider + fixture.options.cacheDirPath = "cache" + + val sut = fixture.getSut() + sut.register(fixture.hub, fixture.options) + + // when there's no connection no factory create call should be done + verify(fixture.sender, never()).send() + + // but for any other status processing should be triggered + // CONNECTED + whenever(connectionStatusProvider.connectionStatus).thenReturn(IConnectionStatusProvider.ConnectionStatus.CONNECTED) + sut.onConnectionStatusChanged(IConnectionStatusProvider.ConnectionStatus.CONNECTED) + verify(fixture.sender).send() + + // UNKNOWN + whenever(connectionStatusProvider.connectionStatus).thenReturn(IConnectionStatusProvider.ConnectionStatus.UNKNOWN) + sut.onConnectionStatusChanged(IConnectionStatusProvider.ConnectionStatus.UNKNOWN) + verify(fixture.sender, times(2)).send() + + // NO_PERMISSION + whenever(connectionStatusProvider.connectionStatus).thenReturn(IConnectionStatusProvider.ConnectionStatus.NO_PERMISSION) + sut.onConnectionStatusChanged(IConnectionStatusProvider.ConnectionStatus.NO_PERMISSION) + verify(fixture.sender, times(3)).send() + } + + @Test + fun `when rate limiter is active, does not send envelopes`() { + val sut = fixture.getSut() + val rateLimiter = mock { + whenever(mock.isActiveForCategory(any())).thenReturn(true) + } + whenever(fixture.hub.rateLimiter).thenReturn(rateLimiter) + + sut.register(fixture.hub, fixture.options) + + // no factory call should be done if there's rate limiting active + verify(fixture.sender, never()).send() + } + private class CustomFactory : SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetFactory { override fun create(hub: IHub, options: SentryOptions): SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForget? { return null diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index ccc749b76a3..8bdb7dd8502 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -11,6 +11,7 @@ import io.sentry.hints.AbnormalExit import io.sentry.hints.ApplyScopeData import io.sentry.hints.Backfillable import io.sentry.hints.Cached +import io.sentry.hints.DiskFlushNotification import io.sentry.hints.TransactionEnd import io.sentry.protocol.Contexts import io.sentry.protocol.Mechanism @@ -2259,7 +2260,50 @@ class SentryClientTest { sut.captureEvent(SentryEvent(), scope, transactionEndHint) - verify(transaction).forceFinish(SpanStatus.ABORTED, false) + verify(transaction).forceFinish(SpanStatus.ABORTED, false, null) + verify(fixture.transport).send( + check { + assertEquals(1, it.items.count()) + }, + any() + ) + } + + @Test + fun `when event has DiskFlushNotification, TransactionEnds set transaction id as flushable`() { + val sut = fixture.getSut() + + // build up a running transaction + val spanContext = SpanContext("op.load") + val transaction = mock() + whenever(transaction.name).thenReturn("transaction") + whenever(transaction.eventId).thenReturn(SentryId()) + whenever(transaction.spanContext).thenReturn(spanContext) + + // scope + val scope = mock() + whenever(scope.transaction).thenReturn(transaction) + whenever(scope.breadcrumbs).thenReturn(LinkedList()) + whenever(scope.extras).thenReturn(emptyMap()) + whenever(scope.contexts).thenReturn(Contexts()) + val scopePropagationContext = PropagationContext() + whenever(scope.propagationContext).thenReturn(scopePropagationContext) + doAnswer { (it.arguments[0] as IWithPropagationContext).accept(scopePropagationContext); scopePropagationContext }.whenever(scope).withPropagationContext(any()) + + var capturedEventId: SentryId? = null + val transactionEnd = object : TransactionEnd, DiskFlushNotification { + override fun markFlushed() {} + override fun isFlushable(eventId: SentryId?): Boolean = true + override fun setFlushable(eventId: SentryId) { + capturedEventId = eventId + } + } + val transactionEndHint = HintUtils.createWithTypeCheckHint(transactionEnd) + + sut.captureEvent(SentryEvent(), scope, transactionEndHint) + + assertEquals(transaction.eventId, capturedEventId) + verify(transaction).forceFinish(SpanStatus.ABORTED, false, transactionEndHint) verify(fixture.transport).send( check { assertEquals(1, it.items.count()) @@ -2648,6 +2692,8 @@ class SentryClientTest { private class AbnormalHint(private val mechanism: String? = null) : AbnormalExit { override fun mechanism(): String? = mechanism + override fun ignoreCurrentThread(): Boolean = false + override fun timestamp(): Long? = null } private fun eventProcessorThrows(): EventProcessor { diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index ce211c9414a..3fcc9fa88c2 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -209,11 +209,6 @@ class SentryOptionsTest { assertTrue(SentryOptions().isAttachStacktrace) } - @Test - fun `when options is initialized, enableScopeSync is false`() { - assertFalse(SentryOptions().isEnableScopeSync) - } - @Test fun `when options is initialized, isProfilingEnabled is false`() { assertFalse(SentryOptions().isProfilingEnabled) @@ -494,6 +489,31 @@ class SentryOptionsTest { } @Test + fun `when options are initialized, connectionStatusProvider is not null and default to noop`() { + assertNotNull(SentryOptions().connectionStatusProvider) + assertTrue(SentryOptions().connectionStatusProvider is NoOpConnectionStatusProvider) + } + + @Test + fun `when connectionStatusProvider is set, its returned as well`() { + val options = SentryOptions() + val customProvider = object : IConnectionStatusProvider { + override fun getConnectionStatus(): IConnectionStatusProvider.ConnectionStatus { + return IConnectionStatusProvider.ConnectionStatus.UNKNOWN + } + + override fun getConnectionType(): String? = null + + override fun addConnectionStatusObserver(observer: IConnectionStatusProvider.IConnectionStatusObserver) = false + + override fun removeConnectionStatusObserver(observer: IConnectionStatusProvider.IConnectionStatusObserver) { + // no-op + } + } + options.connectionStatusProvider = customProvider + assertEquals(customProvider, options.connectionStatusProvider) + } + fun `when options are initialized, enabled is set to true by default`() { assertTrue(SentryOptions().isEnabled) } diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index e9342c00045..26379ddd211 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -9,7 +9,9 @@ import io.sentry.internal.modules.IModulesLoader import io.sentry.internal.modules.NoOpModulesLoader import io.sentry.protocol.SdkVersion import io.sentry.protocol.SentryId +import io.sentry.protocol.SentryThread import io.sentry.test.ImmediateExecutorService +import io.sentry.util.PlatformTestManipulator import io.sentry.util.thread.IMainThreadChecker import io.sentry.util.thread.MainThreadChecker import org.awaitility.kotlin.await @@ -801,6 +803,65 @@ class SentryTest { assertIs(sentryOptions?.modulesLoader) } + @Test + fun `getSpan calls hub getSpan`() { + val hub = mock() + Sentry.init({ + it.dsn = dsn + }, false) + Sentry.setCurrentHub(hub) + Sentry.getSpan() + verify(hub).span + } + + @Test + fun `getSpan calls returns root span if globalhub mode is enabled on Android`() { + PlatformTestManipulator.pretendIsAndroid(true) + Sentry.init({ + it.dsn = dsn + it.enableTracing = true + it.sampleRate = 1.0 + }, true) + + val transaction = Sentry.startTransaction("name", "op-root", true) + transaction.startChild("op-child") + + val span = Sentry.getSpan()!! + assertEquals("op-root", span.operation) + PlatformTestManipulator.pretendIsAndroid(false) + } + + @Test + fun `getSpan calls returns child span if globalhub mode is enabled, but the platform is not Android`() { + PlatformTestManipulator.pretendIsAndroid(false) + Sentry.init({ + it.dsn = dsn + it.enableTracing = true + it.sampleRate = 1.0 + }, false) + + val transaction = Sentry.startTransaction("name", "op-root", true) + transaction.startChild("op-child") + + val span = Sentry.getSpan()!! + assertEquals("op-child", span.operation) + } + + @Test + fun `getSpan calls returns child span if globalhub mode is disabled`() { + Sentry.init({ + it.dsn = dsn + it.enableTracing = true + it.sampleRate = 1.0 + }, false) + + val transaction = Sentry.startTransaction("name", "op-root", true) + transaction.startChild("op-child") + + val span = Sentry.getSpan()!! + assertEquals("op-child", span.operation) + } + private class InMemoryOptionsObserver : IOptionsObserver { var release: String? = null private set @@ -842,6 +903,9 @@ class SentryTest { private class CustomMainThreadChecker : IMainThreadChecker { override fun isMainThread(threadId: Long): Boolean = false + override fun isMainThread(thread: Thread): Boolean = false + override fun isMainThread(): Boolean = false + override fun isMainThread(sentryThread: SentryThread): Boolean = false } private class CustomMemoryCollector : ICollector { diff --git a/sentry/src/test/java/io/sentry/SentryTracerTest.kt b/sentry/src/test/java/io/sentry/SentryTracerTest.kt index ee698af6c08..be5f05c3d85 100644 --- a/sentry/src/test/java/io/sentry/SentryTracerTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTracerTest.kt @@ -43,6 +43,7 @@ class SentryTracerTest { startTimestamp: SentryDate? = null, waitForChildren: Boolean = false, idleTimeout: Long? = null, + deadlineTimeout: Long? = null, trimEnd: Boolean = false, transactionFinishedCallback: TransactionFinishedCallback? = null, samplingDecision: TracesSamplingDecision? = null, @@ -54,6 +55,7 @@ class SentryTracerTest { transactionOptions.startTimestamp = startTimestamp transactionOptions.isWaitForChildren = waitForChildren transactionOptions.idleTimeout = idleTimeout + transactionOptions.deadlineTimeout = deadlineTimeout transactionOptions.isTrimEnd = trimEnd transactionOptions.transactionFinishedCallback = transactionFinishedCallback return SentryTracer(TransactionContext("name", "op", samplingDecision), hub, transactionOptions, performanceCollector) @@ -536,18 +538,27 @@ class SentryTracerTest { } @Test - fun `finishing unfinished spans with the transaction timestamp`() { + fun `when finishing, unfinished spans won't have any automatic end-date or status`() { val transaction = fixture.getSut(samplingDecision = TracesSamplingDecision(true)) + // span with no status set val span = transaction.startChild("op") as Span - transaction.startChild("op2") + span.finish(SpanStatus.INVALID_ARGUMENT) + + // span with a status + val span1 = transaction.startChild("op2") + transaction.finish(SpanStatus.INVALID_ARGUMENT) verify(fixture.hub, times(1)).captureTransaction( check { assertEquals(2, it.spans.size) - assertEquals(transaction.root.finishDate, span.finishDate) - assertEquals(SpanStatus.DEADLINE_EXCEEDED, it.spans[0].status) - assertEquals(SpanStatus.DEADLINE_EXCEEDED, it.spans[1].status) + // span status/timestamp is retained + assertNotNull(it.spans[0].status) + assertNotNull(it.spans[0].timestamp) + + // span status/timestamp remains untouched + assertNull(it.spans[1].status) + assertNull(it.spans[1].timestamp) }, anyOrNull(), anyOrNull(), @@ -751,17 +762,73 @@ class SentryTracerTest { } @Test - fun `when initialized without idleTimeout, does not schedule finish timer`() { + fun `when initialized without deadlineTimeout, does not schedule finish timer`() { val transaction = fixture.getSut() + assertNull(transaction.deadlineTimeoutTask) + } + + @Test + fun `when initialized with deadlineTimeout, schedules finish timer`() { + val transaction = fixture.getSut(deadlineTimeout = 50) + + assertTrue(transaction.isDeadlineTimerRunning.get()) + assertNotNull(transaction.deadlineTimeoutTask) + } - assertNull(transaction.timerTask) + @Test + fun `when deadline is reached transaction is finished`() { + // when a transaction with a deadline timeout is created + // and the tx and child keep on running + val transaction = fixture.getSut(deadlineTimeout = 20) + val span = transaction.startChild("op") + + // and the deadline is exceed + await.untilFalse(transaction.isDeadlineTimerRunning) + + // then both tx + span should be force finished + assertEquals(transaction.isFinished, true) + assertEquals(SpanStatus.DEADLINE_EXCEEDED, transaction.status) + assertEquals(SpanStatus.DEADLINE_EXCEEDED, span.status) + } + + @Test + fun `when transaction is finished before deadline is reached, deadline should not be running anymore`() { + val transaction = fixture.getSut(deadlineTimeout = 1000) + val span = transaction.startChild("op") + + span.finish(SpanStatus.OK) + transaction.finish(SpanStatus.OK) + + assertEquals(transaction.isDeadlineTimerRunning.get(), false) + assertNull(transaction.deadlineTimeoutTask) + assertEquals(transaction.isFinished, true) + assertEquals(SpanStatus.OK, transaction.status) + assertEquals(SpanStatus.OK, span.status) + } + + @Test + fun `when initialized with idleTimeout it has no influence on deadline timeout`() { + val transaction = fixture.getSut(idleTimeout = 3000, deadlineTimeout = 20) + val deadlineTimeoutTask = transaction.deadlineTimeoutTask + + val span = transaction.startChild("op") + // when the span finishes, it re-schedules the idle task + span.finish() + + // but the deadline timeout task should not be re-scheduled + assertEquals(deadlineTimeoutTask, transaction.deadlineTimeoutTask) + } + + @Test + fun `when initialized without idleTimeout, does not schedule finish timer`() { + val transaction = fixture.getSut() + assertNull(transaction.idleTimeoutTask) } @Test fun `when initialized with idleTimeout, schedules finish timer`() { val transaction = fixture.getSut(idleTimeout = 50) - - assertNotNull(transaction.timerTask) + assertNotNull(transaction.idleTimeoutTask) } @Test @@ -801,20 +868,20 @@ class SentryTracerTest { transaction.startChild("op") - assertNull(transaction.timerTask) + assertNull(transaction.idleTimeoutTask) } @Test fun `when a child is finished and the transaction is idle, resets the timer`() { val transaction = fixture.getSut(waitForChildren = true, idleTimeout = 3000) - val initialTime = transaction.timerTask!!.scheduledExecutionTime() + val initialTime = transaction.idleTimeoutTask!!.scheduledExecutionTime() val span = transaction.startChild("op") Thread.sleep(1) span.finish() - val timerAfterFinishingChild = transaction.timerTask!!.scheduledExecutionTime() + val timerAfterFinishingChild = transaction.idleTimeoutTask!!.scheduledExecutionTime() assertTrue { timerAfterFinishingChild > initialTime } } @@ -828,7 +895,7 @@ class SentryTracerTest { Thread.sleep(1) span.finish() - assertNull(transaction.timerTask) + assertNull(transaction.idleTimeoutTask) } @Test @@ -1128,7 +1195,7 @@ class SentryTracerTest { // and it's finished transaction.finish(SpanStatus.OK) // but forceFinish is called as well - transaction.forceFinish(SpanStatus.ABORTED, false) + transaction.forceFinish(SpanStatus.ABORTED, false, null) // then it should keep it's original status assertEquals(SpanStatus.OK, transaction.status) @@ -1151,7 +1218,7 @@ class SentryTracerTest { span0.finish(SpanStatus.OK) val span0FinishDate = span0.finishDate - transaction.forceFinish(SpanStatus.ABORTED, false) + transaction.forceFinish(SpanStatus.ABORTED, false, null) // then the first span should keep it's status assertTrue(span0.isFinished) @@ -1184,7 +1251,7 @@ class SentryTracerTest { ) // and force-finished but dropping is disabled - transaction.forceFinish(SpanStatus.ABORTED, false) + transaction.forceFinish(SpanStatus.ABORTED, false, null) // then a transaction should be captured with 0 spans verify(fixture.hub).captureTransaction( @@ -1207,7 +1274,7 @@ class SentryTracerTest { ) // and force-finish with dropping enabled - transaction.forceFinish(SpanStatus.ABORTED, true) + transaction.forceFinish(SpanStatus.ABORTED, true, null) // then the transaction should be captured with 0 spans verify(fixture.hub, never()).captureTransaction( @@ -1220,7 +1287,7 @@ class SentryTracerTest { @Test fun `when timer is cancelled, schedule finish does not crash`() { - val tracer = fixture.getSut(idleTimeout = 50) + val tracer = fixture.getSut(idleTimeout = 50, deadlineTimeout = 100) tracer.timer!!.cancel() tracer.scheduleFinish() } diff --git a/sentry/src/test/java/io/sentry/UncaughtExceptionHandlerIntegrationTest.kt b/sentry/src/test/java/io/sentry/UncaughtExceptionHandlerIntegrationTest.kt index 023194190bf..01353d5ac03 100644 --- a/sentry/src/test/java/io/sentry/UncaughtExceptionHandlerIntegrationTest.kt +++ b/sentry/src/test/java/io/sentry/UncaughtExceptionHandlerIntegrationTest.kt @@ -1,5 +1,6 @@ package io.sentry +import io.sentry.UncaughtExceptionHandlerIntegration.UncaughtExceptionHint import io.sentry.exception.ExceptionMechanismException import io.sentry.hints.DiskFlushNotification import io.sentry.hints.EventDropReason.MULTITHREADED_DEDUPLICATION @@ -249,4 +250,45 @@ class UncaughtExceptionHandlerIntegrationTest { any() ) } + + @Test + fun `when there is no active transaction on scope, sets current event id as flushable`() { + val eventCaptor = argumentCaptor() + whenever(fixture.hub.captureEvent(eventCaptor.capture(), any())) + .thenReturn(SentryId.EMPTY_ID) + + val sut = fixture.getSut() + + sut.register(fixture.hub, fixture.options) + sut.uncaughtException(fixture.thread, fixture.throwable) + + verify(fixture.hub).captureEvent( + any(), + argThat { + (HintUtils.getSentrySdkHint(this) as UncaughtExceptionHint) + .isFlushable(eventCaptor.firstValue.eventId) + } + ) + } + + @Test + fun `when there is active transaction on scope, does not set current event id as flushable`() { + val eventCaptor = argumentCaptor() + whenever(fixture.hub.transaction).thenReturn(mock()) + whenever(fixture.hub.captureEvent(eventCaptor.capture(), any())) + .thenReturn(SentryId.EMPTY_ID) + + val sut = fixture.getSut() + + sut.register(fixture.hub, fixture.options) + sut.uncaughtException(fixture.thread, fixture.throwable) + + verify(fixture.hub).captureEvent( + any(), + argThat { + !(HintUtils.getSentrySdkHint(this) as UncaughtExceptionHint) + .isFlushable(eventCaptor.firstValue.eventId) + } + ) + } } diff --git a/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt b/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt index 33970f40939..260c6ae9810 100644 --- a/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt +++ b/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt @@ -238,7 +238,11 @@ class EnvelopeCacheTest { fixture.options.serializer.serialize(session, previousSessionFile.bufferedWriter()) val envelope = SentryEnvelope.from(fixture.options.serializer, SentryEvent(), null) - val abnormalHint = AbnormalExit { "abnormal_mechanism" } + val abnormalHint = object : AbnormalExit { + override fun mechanism(): String? = "abnormal_mechanism" + override fun ignoreCurrentThread(): Boolean = false + override fun timestamp(): Long? = null + } val hints = HintUtils.createWithTypeCheckHint(abnormalHint) cache.store(envelope, hints) @@ -261,7 +265,7 @@ class EnvelopeCacheTest { val envelope = SentryEnvelope.from(fixture.options.serializer, SentryEvent(), null) val abnormalHint = object : AbnormalExit { override fun mechanism(): String = "abnormal_mechanism" - + override fun ignoreCurrentThread(): Boolean = false override fun timestamp(): Long = sessionExitedWithAbnormal } val hints = HintUtils.createWithTypeCheckHint(abnormalHint) @@ -284,7 +288,7 @@ class EnvelopeCacheTest { val envelope = SentryEnvelope.from(fixture.options.serializer, SentryEvent(), null) val abnormalHint = object : AbnormalExit { override fun mechanism(): String = "abnormal_mechanism" - + override fun ignoreCurrentThread(): Boolean = false override fun timestamp(): Long = sessionExitedWithAbnormal } val hints = HintUtils.createWithTypeCheckHint(abnormalHint) diff --git a/sentry/src/test/java/io/sentry/instrumentation/file/FileIOSpanManagerTest.kt b/sentry/src/test/java/io/sentry/instrumentation/file/FileIOSpanManagerTest.kt new file mode 100644 index 00000000000..00c89f27bea --- /dev/null +++ b/sentry/src/test/java/io/sentry/instrumentation/file/FileIOSpanManagerTest.kt @@ -0,0 +1,44 @@ +package io.sentry.instrumentation.file + +import io.sentry.IHub +import io.sentry.ISpan +import io.sentry.ITransaction +import io.sentry.util.PlatformTestManipulator +import org.junit.After +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import kotlin.test.Test + +class FileIOSpanManagerTest { + + @After + fun cleanup() { + PlatformTestManipulator.pretendIsAndroid(false) + } + + @Test + fun `startSpan uses transaction on Android platform`() { + val hub = mock() + val transaction = mock() + whenever(hub.transaction).thenReturn(transaction) + + PlatformTestManipulator.pretendIsAndroid(true) + + FileIOSpanManager.startSpan(hub, "op.read") + verify(transaction).startChild(any()) + } + + @Test + fun `startSpan uses last span on non-Android platforms`() { + val hub = mock() + val span = mock() + whenever(hub.span).thenReturn(span) + + PlatformTestManipulator.pretendIsAndroid(false) + + FileIOSpanManager.startSpan(hub, "op.read") + verify(span).startChild(any()) + } +} diff --git a/sentry/src/test/java/io/sentry/protocol/SentrySpanTest.kt b/sentry/src/test/java/io/sentry/protocol/SentrySpanTest.kt new file mode 100644 index 00000000000..27499be0a0c --- /dev/null +++ b/sentry/src/test/java/io/sentry/protocol/SentrySpanTest.kt @@ -0,0 +1,35 @@ +package io.sentry.protocol + +import io.sentry.IHub +import io.sentry.SentryLongDate +import io.sentry.SentryTracer +import io.sentry.Span +import io.sentry.SpanOptions +import io.sentry.TransactionContext +import org.junit.Test +import org.mockito.kotlin.mock +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class SentrySpanTest { + + @Test + fun `end timestamps is kept null if not provided`() { + // when a span with a start timestamp is generated + val span = Span( + TransactionContext("name", "op"), + mock(), + mock(), + SentryLongDate(1000000), + SpanOptions() + ) + + val sentrySpan = SentrySpan(span) + + // then the start timestamp should be correctly set + assertEquals(0.001, sentrySpan.startTimestamp) + + // but the end time should remain untouched + assertNull(sentrySpan.timestamp) + } +} diff --git a/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt b/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt index ce59db50255..2982e9567b1 100644 --- a/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt +++ b/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt @@ -12,6 +12,9 @@ import io.sentry.SentryOptionsManipulator import io.sentry.Session import io.sentry.clientreport.NoOpClientReportRecorder import io.sentry.dsnString +import io.sentry.hints.DiskFlushNotification +import io.sentry.hints.Enqueable +import io.sentry.protocol.SentryId import io.sentry.protocol.User import io.sentry.util.HintUtils import org.mockito.kotlin.any @@ -25,8 +28,11 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import java.io.IOException import java.util.Date +import java.util.concurrent.TimeUnit import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue class AsyncHttpTransportTest { @@ -321,6 +327,81 @@ class AsyncHttpTransportTest { ) } + @Test + fun `close uses flushTimeoutMillis option to schedule termination`() { + fixture.sentryOptions.flushTimeoutMillis = 123 + val sut = fixture.getSUT() + sut.close() + + verify(fixture.executor).awaitTermination(eq(123), eq(TimeUnit.MILLISECONDS)) + } + + @Test + fun `when DiskFlushNotification is not flushable, does not flush`() { + // given + val ev = SentryEvent() + val envelope = SentryEnvelope.from(fixture.sentryOptions.serializer, ev, null) + whenever(fixture.rateLimiter.filter(any(), anyOrNull())).thenAnswer { it.arguments[0] } + + var calledFlush = false + val sentryHint = object : DiskFlushNotification { + override fun markFlushed() { + calledFlush = true + } + override fun isFlushable(eventId: SentryId?): Boolean = false + override fun setFlushable(eventId: SentryId) = Unit + } + val hint = HintUtils.createWithTypeCheckHint(sentryHint) + + // when + fixture.getSUT().send(envelope, hint) + + // then + assertFalse(calledFlush) + } + + @Test + fun `when DiskFlushNotification is flushable, marks it as flushed`() { + // given + val ev = SentryEvent() + val envelope = SentryEnvelope.from(fixture.sentryOptions.serializer, ev, null) + whenever(fixture.rateLimiter.filter(any(), anyOrNull())).thenAnswer { it.arguments[0] } + + var calledFlush = false + val sentryHint = object : DiskFlushNotification { + override fun markFlushed() { + calledFlush = true + } + override fun isFlushable(eventId: SentryId?): Boolean = envelope.header.eventId == eventId + override fun setFlushable(eventId: SentryId) = Unit + } + val hint = HintUtils.createWithTypeCheckHint(sentryHint) + + // when + fixture.getSUT().send(envelope, hint) + + // then + assertTrue(calledFlush) + } + + @Test + fun `when event is Enqueable, marks it after sending to the queue`() { + val envelope = SentryEnvelope.from(fixture.sentryOptions.serializer, createSession(), null) + whenever(fixture.transportGate.isConnected).thenReturn(true) + whenever(fixture.rateLimiter.filter(any(), anyOrNull())).thenAnswer { it.arguments[0] } + whenever(fixture.connection.send(any())).thenReturn(TransportResult.success()) + + var called = false + val hint = HintUtils.createWithTypeCheckHint(object : Enqueable { + override fun markEnqueued() { + called = true + } + }) + fixture.getSUT().send(envelope, hint) + + assertTrue(called) + } + private fun createSession(): Session { return Session("123", User(), "env", "release") } diff --git a/sentry/src/test/java/io/sentry/util/PlatformTestManipulator.kt b/sentry/src/test/java/io/sentry/util/PlatformTestManipulator.kt index a849cf3d6dc..3eb4662f7ce 100644 --- a/sentry/src/test/java/io/sentry/util/PlatformTestManipulator.kt +++ b/sentry/src/test/java/io/sentry/util/PlatformTestManipulator.kt @@ -6,5 +6,9 @@ class PlatformTestManipulator { fun pretendJavaNinePlus(isJavaNinePlus: Boolean) { Platform.isJavaNinePlus = isJavaNinePlus } + + fun pretendIsAndroid(isAndroid: Boolean) { + Platform.isAndroid = isAndroid + } } }