diff --git a/.github/workflows/agp-matrix.yml b/.github/workflows/agp-matrix.yml index f469bbcfe6..a9c4293292 100644 --- a/.github/workflows/agp-matrix.yml +++ b/.github/workflows/agp-matrix.yml @@ -38,7 +38,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@579fbbe7221704325eb4c4d4bf20c2b0859fba76 # pin@v3 + uses: gradle/actions/setup-gradle@2cd2a6e951bd0b53f55a08e4e4c6f2586f3a36b9 # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b59dd7fccf..969ad6135e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@579fbbe7221704325eb4c4d4bf20c2b0859fba76 # pin@v3 + uses: gradle/actions/setup-gradle@2cd2a6e951bd0b53f55a08e4e4c6f2586f3a36b9 # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 5d09ac72cd..6636dc019d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -36,12 +36,12 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@579fbbe7221704325eb4c4d4bf20c2b0859fba76 # pin@v3 + uses: gradle/actions/setup-gradle@2cd2a6e951bd0b53f55a08e4e4c6f2586f3a36b9 # pin@v3 with: gradle-home-cache-cleanup: true - name: Initialize CodeQL - uses: github/codeql-action/init@cdcdbb579706841c47f7063dda365e292e5cad7a # pin@v2 + uses: github/codeql-action/init@23acc5c183826b7a8a97bce3cecc52db901f8251 # pin@v2 with: languages: ${{ matrix.language }} @@ -55,4 +55,4 @@ jobs: ./gradlew buildForCodeQL - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@cdcdbb579706841c47f7063dda365e292e5cad7a # pin@v2 + uses: github/codeql-action/analyze@23acc5c183826b7a8a97bce3cecc52db901f8251 # pin@v2 diff --git a/.github/workflows/enforce-license-compliance.yml b/.github/workflows/enforce-license-compliance.yml index c31fec539e..2c93ed9e4b 100644 --- a/.github/workflows/enforce-license-compliance.yml +++ b/.github/workflows/enforce-license-compliance.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup Gradle - uses: gradle/actions/setup-gradle@579fbbe7221704325eb4c4d4bf20c2b0859fba76 # pin@v3 + uses: gradle/actions/setup-gradle@2cd2a6e951bd0b53f55a08e4e4c6f2586f3a36b9 # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/generate-javadocs.yml b/.github/workflows/generate-javadocs.yml index 858cc3f82d..635a87609b 100644 --- a/.github/workflows/generate-javadocs.yml +++ b/.github/workflows/generate-javadocs.yml @@ -20,7 +20,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@579fbbe7221704325eb4c4d4bf20c2b0859fba76 # pin@v3 + uses: gradle/actions/setup-gradle@2cd2a6e951bd0b53f55a08e4e4c6f2586f3a36b9 # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index e99aa31f9a..d4981c5583 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -13,4 +13,4 @@ jobs: - uses: actions/checkout@v4 with: submodules: 'recursive' - - uses: gradle/wrapper-validation-action@699bb18358f12c5b78b37bb0111d3a0e2276e0e2 # pin@v1 + - uses: gradle/wrapper-validation-action@88425854a36845f9c881450d9660b5fd46bee142 # pin@v1 diff --git a/.github/workflows/integration-tests-benchmarks.yml b/.github/workflows/integration-tests-benchmarks.yml index 2b33b28528..2e885359ad 100644 --- a/.github/workflows/integration-tests-benchmarks.yml +++ b/.github/workflows/integration-tests-benchmarks.yml @@ -37,7 +37,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@579fbbe7221704325eb4c4d4bf20c2b0859fba76 # pin@v3 + uses: gradle/actions/setup-gradle@2cd2a6e951bd0b53f55a08e4e4c6f2586f3a36b9 # pin@v3 with: gradle-home-cache-cleanup: true @@ -47,7 +47,7 @@ jobs: run: make assembleBenchmarks - name: Run All Tests in SauceLab - uses: saucelabs/saucectl-run-action@7fe025ef1fdc6f211add3751a6c7d8bba27ba9b1 # pin@v3 + uses: saucelabs/saucectl-run-action@39e4f0666ca8ecb4b60847213c6e0fbd6a0c2bd8 # pin@v3 if: github.event_name != 'pull_request' && env.SAUCE_USERNAME != null env: GITHUB_TOKEN: ${{ github.token }} @@ -57,7 +57,7 @@ jobs: config-file: .sauce/sentry-uitest-android-benchmark.yml - name: Run one test in SauceLab - uses: saucelabs/saucectl-run-action@7fe025ef1fdc6f211add3751a6c7d8bba27ba9b1 # pin@v3 + uses: saucelabs/saucectl-run-action@39e4f0666ca8ecb4b60847213c6e0fbd6a0c2bd8 # pin@v3 if: github.event_name == 'pull_request' && env.SAUCE_USERNAME != null env: GITHUB_TOKEN: ${{ github.token }} @@ -86,7 +86,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@579fbbe7221704325eb4c4d4bf20c2b0859fba76 # pin@v3 + uses: gradle/actions/setup-gradle@2cd2a6e951bd0b53f55a08e4e4c6f2586f3a36b9 # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/integration-tests-ui.yml b/.github/workflows/integration-tests-ui.yml index 1d7038dd0a..cd5134d38f 100644 --- a/.github/workflows/integration-tests-ui.yml +++ b/.github/workflows/integration-tests-ui.yml @@ -32,7 +32,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@579fbbe7221704325eb4c4d4bf20c2b0859fba76 # pin@v3 + uses: gradle/actions/setup-gradle@2cd2a6e951bd0b53f55a08e4e4c6f2586f3a36b9 # pin@v3 with: gradle-home-cache-cleanup: true @@ -42,7 +42,7 @@ jobs: run: make assembleUiTests - name: Install SauceLabs CLI - uses: saucelabs/saucectl-run-action@7fe025ef1fdc6f211add3751a6c7d8bba27ba9b1 # pin@v3 + uses: saucelabs/saucectl-run-action@39e4f0666ca8ecb4b60847213c6e0fbd6a0c2bd8 # pin@v3 env: GITHUB_TOKEN: ${{ github.token }} with: diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index a5449ed95b..b021a6d8ec 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -26,7 +26,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@579fbbe7221704325eb4c4d4bf20c2b0859fba76 # pin@v3 + uses: gradle/actions/setup-gradle@2cd2a6e951bd0b53f55a08e4e4c6f2586f3a36b9 # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml index be9287ad79..d794ecb118 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -40,7 +40,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@579fbbe7221704325eb4c4d4bf20c2b0859fba76 # pin@v3 + uses: gradle/actions/setup-gradle@2cd2a6e951bd0b53f55a08e4e4c6f2586f3a36b9 # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/CHANGELOG.md b/CHANGELOG.md index e43a16c245..404b3eabdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## Unreleased + +### Fixes + +- Fix duplicate session start for React Native ([#3504](https://github.com/getsentry/sentry-java/pull/3504)) +- Move onFinishCallback before span or transaction is finished ([#3459](https://github.com/getsentry/sentry-java/pull/3459)) +- Add timestamp when a profile starts ([#3442](https://github.com/getsentry/sentry-java/pull/3442)) +- Move fragment auto span finish to onFragmentStarted ([#3424](https://github.com/getsentry/sentry-java/pull/3424)) +- Remove profiling timeout logic and disable profiling on API 21 ([#3478](https://github.com/getsentry/sentry-java/pull/3478)) +- Properly reset metric flush flag on metric emission ([#3493](https://github.com/getsentry/sentry-java/pull/3493)) +- Use SecureRandom in favor of Random for Metrics ([#3495](https://github.com/getsentry/sentry-java/pull/3495)) +- Fix UncaughtExceptionHandlerIntegration Memory Leak ([#3398](https://github.com/getsentry/sentry-java/pull/3398)) + +### Dependencies + +- Bump Native SDK from v0.7.0 to v0.7.2 ([#3314](https://github.com/getsentry/sentry-java/pull/3314)) + - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#072) + - [diff](https://github.com/getsentry/sentry-native/compare/0.7.0...0.7.2) + +## 7.10.0 + +### Features + +- Publish Gradle module metadata ([#3422](https://github.com/getsentry/sentry-java/pull/3422)) + +### Fixes + +- Fix faulty `span.frame_delay` calculation for early app start spans ([#3427](https://github.com/getsentry/sentry-java/pull/3427)) +- Fix crash when installing `ShutdownHookIntegration` and the VM is shutting down ([#3456](https://github.com/getsentry/sentry-java/pull/3456)) + ## 7.9.0 ### Features diff --git a/build.gradle.kts b/build.gradle.kts index 42acafadb1..998c547efb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -160,16 +160,7 @@ subprojects { if (this@subprojects.name.contains("-compose")) { this.configureForMultiplatform(this@subprojects) } else { - this.getByName("main").contents { - // non android modules - from("build${sep}libs") - from("build${sep}publications${sep}maven") - // android modules - from("build${sep}outputs${sep}aar") { - include("*-release*") - } - from("build${sep}publications${sep}release") - } + this.configureForJvm(this@subprojects) } // craft only uses zip archives this.forEach { dist -> diff --git a/buildSrc/src/main/java/Publication.kt b/buildSrc/src/main/java/Publication.kt index 1362e96522..08a81c703f 100644 --- a/buildSrc/src/main/java/Publication.kt +++ b/buildSrc/src/main/java/Publication.kt @@ -10,9 +10,12 @@ private object Consts { // configure distZip tasks for multiplatform fun DistributionContainer.configureForMultiplatform(project: Project) { val sep = File.separator + val version = project.properties["versionName"].toString() this.maybeCreate("android").contents { - from("build${sep}publications${sep}androidRelease") + from("build${sep}publications${sep}androidRelease") { + renameModule(project.name, "android", version = version) + } from("build${sep}outputs${sep}aar") { include("*-release*") rename { @@ -25,7 +28,9 @@ fun DistributionContainer.configureForMultiplatform(project: Project) { } } this.getByName("main").contents { - from("build${sep}publications${sep}kotlinMultiplatform") + from("build${sep}publications${sep}kotlinMultiplatform") { + renameModule(project.name, version = version) + } from("build${sep}kotlinToolingMetadata") from("build${sep}libs") { include("*compose-kotlin*") @@ -39,7 +44,9 @@ fun DistributionContainer.configureForMultiplatform(project: Project) { } this.maybeCreate("desktop").contents { // kotlin multiplatform modules - from("build${sep}publications${sep}desktop") + from("build${sep}publications${sep}desktop") { + renameModule(project.name, "desktop", version = version) + } from("build${sep}libs") { include("*desktop*") withJavadoc(renameTo = "compose-desktop") @@ -53,6 +60,26 @@ fun DistributionContainer.configureForMultiplatform(project: Project) { project.tasks.getByName("distZip").finalizedBy(*platformDists) } +fun DistributionContainer.configureForJvm(project: Project) { + val sep = File.separator + val version = project.properties["versionName"].toString() + + this.getByName("main").contents { + // non android modules + from("build${sep}libs") + from("build${sep}publications${sep}maven") { + renameModule(project.name, version = version) + } + // android modules + from("build${sep}outputs${sep}aar") { + include("*-release*") + } + from("build${sep}publications${sep}release") { + renameModule(project.name, version = version) + } + } +} + private fun CopySpec.withJavadoc(renameTo: String = "compose") { include("*javadoc*") rename { @@ -63,3 +90,13 @@ private fun CopySpec.withJavadoc(renameTo: String = "compose") { } } } + +private fun CopySpec.renameModule(projectName: String, renameTo: String = "", version: String) { + var target = "" + if (renameTo.isNotEmpty()) { + target = "-$renameTo" + } + rename { + it.replace("module.json", "$projectName$target-$version.module") + } +} diff --git a/gradle.properties b/gradle.properties index 00358cbb2f..43afd204d7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ android.useAndroidX=true android.defaults.buildfeatures.buildconfig=true # Release information -versionName=7.9.0 +versionName=7.10.0 # 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 8eb017346d..97f7cddb7c 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -81,7 +81,8 @@ public class io/sentry/android/core/AndroidProfiler$ProfileEndData { public class io/sentry/android/core/AndroidProfiler$ProfileStartData { public final field startCpuMillis J public final field startNanos J - public fun (JJ)V + public final field startTimestamp Ljava/util/Date; + public fun (JJLjava/util/Date;)V } public final class io/sentry/android/core/AnrIntegration : io/sentry/Integration, java/io/Closeable { @@ -204,6 +205,7 @@ public abstract interface class io/sentry/android/core/IDebugImagesLoader { public final class io/sentry/android/core/InternalSentrySdk { public fun ()V public static fun captureEnvelope ([B)Lio/sentry/protocol/SentryId; + public static fun getAppStartMeasurement ()Ljava/util/Map; public static fun getCurrentScope ()Lio/sentry/IScope; public static fun serializeScope (Landroid/content/Context;Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/IScope;)Ljava/util/Map; } diff --git a/sentry-android-core/build.gradle.kts b/sentry-android-core/build.gradle.kts index 4ab0aec423..2ec856cf5f 100644 --- a/sentry-android-core/build.gradle.kts +++ b/sentry-android-core/build.gradle.kts @@ -77,6 +77,7 @@ dependencies { compileOnly(projects.sentryAndroidFragment) compileOnly(projects.sentryAndroidTimber) compileOnly(projects.sentryCompose) + compileOnly(projects.sentryComposeHelper) // lifecycle processor, session tracking implementation(Config.Libs.lifecycleProcess) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java index 0018f8b75f..d24025c551 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java @@ -6,6 +6,7 @@ import android.os.Process; import android.os.SystemClock; import io.sentry.CpuCollectionData; +import io.sentry.DateUtils; import io.sentry.ILogger; import io.sentry.ISentryExecutorService; import io.sentry.MemoryCollectionData; @@ -17,6 +18,7 @@ import io.sentry.util.Objects; import java.io.File; import java.util.ArrayDeque; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -33,10 +35,13 @@ public class AndroidProfiler { public static class ProfileStartData { public final long startNanos; public final long startCpuMillis; + public final @NotNull Date startTimestamp; - public ProfileStartData(final long startNanos, final long startCpuMillis) { + public ProfileStartData( + final long startNanos, final long startCpuMillis, final @NotNull Date startTimestamp) { this.startNanos = startNanos; this.startCpuMillis = startCpuMillis; + this.startTimestamp = startTimestamp; } } @@ -79,7 +84,6 @@ public ProfileEndData( private @Nullable Future scheduledFinish = null; private @Nullable File traceFile = null; private @Nullable String frameMetricsCollectorId; - private volatile @Nullable ProfileEndData timedOutProfilingData = null; private final @NotNull SentryFrameMetricsCollector frameMetricsCollector; private final @NotNull ArrayDeque screenFrameRateMeasurements = new ArrayDeque<>(); @@ -182,8 +186,7 @@ public void onFrameMetricCollected( // We stop profiling after a timeout to avoid huge profiles to be sent try { scheduledFinish = - executorService.schedule( - () -> timedOutProfilingData = endAndCollect(true, null), PROFILING_TIMEOUT_MILLIS); + executorService.schedule(() -> endAndCollect(true, null), PROFILING_TIMEOUT_MILLIS); } catch (RejectedExecutionException e) { logger.log( SentryLevel.ERROR, @@ -192,6 +195,7 @@ public void onFrameMetricCollected( } profileStartNanos = SystemClock.elapsedRealtimeNanos(); + final @NotNull Date profileStartTimestamp = DateUtils.getCurrentDateTime(); long profileStartCpuMillis = Process.getElapsedCpuTime(); // We don't make any check on the file existence or writeable state, because we don't want to @@ -203,7 +207,7 @@ public void onFrameMetricCollected( // tests) Debug.startMethodTracingSampling(traceFile.getPath(), BUFFER_SIZE_BYTES, intervalUs); isRunning = true; - return new ProfileStartData(profileStartNanos, profileStartCpuMillis); + return new ProfileStartData(profileStartNanos, profileStartCpuMillis, profileStartTimestamp); } catch (Throwable e) { endAndCollect(false, null); logger.log(SentryLevel.ERROR, "Unable to start a profile: ", e); @@ -216,10 +220,6 @@ public void onFrameMetricCollected( public synchronized @Nullable ProfileEndData endAndCollect( final boolean isTimeout, final @Nullable List performanceCollectionData) { - // check if profiling timed out - if (timedOutProfilingData != null) { - return timedOutProfilingData; - } if (!isRunning) { logger.log(SentryLevel.WARNING, "Profiler not running"); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java index 3abfe1306a..d9ece7fb46 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java @@ -9,6 +9,7 @@ import android.os.Build; import android.os.Process; import android.os.SystemClock; +import io.sentry.DateUtils; import io.sentry.HubAdapter; import io.sentry.IHub; import io.sentry.ILogger; @@ -24,6 +25,7 @@ import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.util.Objects; import java.util.ArrayList; +import java.util.Date; import java.util.List; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -44,6 +46,7 @@ final class AndroidTransactionProfiler implements ITransactionProfiler { private @Nullable AndroidProfiler profiler = null; private long profileStartNanos; private long profileStartCpuMillis; + private @NotNull Date profileStartTimestamp; /** * @deprecated please use a constructor that doesn't takes a {@link IHub} instead, as it would be @@ -95,6 +98,7 @@ public AndroidTransactionProfiler( this.profilingTracesHz = profilingTracesHz; this.executorService = Objects.requireNonNull(executorService, "The ISentryExecutorService is required."); + this.profileStartTimestamp = DateUtils.getCurrentDateTime(); } private void init() { @@ -133,8 +137,9 @@ private void init() { @Override public synchronized void start() { - // Debug.startMethodTracingSampling() is only available since Lollipop - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP) return; + // Debug.startMethodTracingSampling() is only available since Lollipop, but Android Profiler + // causes crashes on api 21 -> https://github.com/getsentry/sentry-java/issues/3392 + if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) return; // Let's initialize trace folder and profiling interval init(); @@ -164,6 +169,7 @@ private boolean onFirstStart() { } profileStartNanos = startData.startNanos; profileStartCpuMillis = startData.startCpuMillis; + profileStartTimestamp = startData.startTimestamp; return true; } @@ -204,9 +210,9 @@ public synchronized void bindTransaction(final @NotNull ITransaction transaction return null; } - // onTransactionStart() is only available since Lollipop + // onTransactionStart() is only available since Lollipop_MR1 // and SystemClock.elapsedRealtimeNanos() since Jelly Bean - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP) return null; + if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) return null; // Transaction finished, but it's not in the current profile if (currentProfilingTransactionData == null @@ -274,6 +280,7 @@ public synchronized void bindTransaction(final @NotNull ITransaction transaction // done in the background when the trace file is read return new ProfilingTraceData( endData.traceFile, + profileStartTimestamp, transactionList, transactionName, transactionId, diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java index 9bdbe86a77..a4d1db09df 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java @@ -16,6 +16,7 @@ import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.Session; +import io.sentry.android.core.performance.ActivityLifecycleTimeSpan; import io.sentry.android.core.performance.AppStartMetrics; import io.sentry.android.core.performance.TimeSpan; import io.sentry.protocol.App; @@ -28,6 +29,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import org.jetbrains.annotations.ApiStatus; @@ -193,6 +195,63 @@ public static SentryId captureEnvelope(final @NotNull byte[] envelopeData) { return null; } + public static Map getAppStartMeasurement() { + final @NotNull AppStartMetrics metrics = AppStartMetrics.getInstance(); + final @NotNull List> spans = new ArrayList<>(); + + final @NotNull TimeSpan processInitNativeSpan = new TimeSpan(); + processInitNativeSpan.setStartedAt(metrics.getAppStartTimeSpan().getStartUptimeMs()); + processInitNativeSpan.setStartUnixTimeMs( + metrics.getAppStartTimeSpan().getStartTimestampMs()); // This has to go after setStartedAt + processInitNativeSpan.setStoppedAt(metrics.getClassLoadedUptimeMs()); + processInitNativeSpan.setDescription("Process Initialization"); + + addTimeSpanToSerializedSpans(processInitNativeSpan, spans); + addTimeSpanToSerializedSpans(metrics.getApplicationOnCreateTimeSpan(), spans); + + for (final TimeSpan span : metrics.getContentProviderOnCreateTimeSpans()) { + addTimeSpanToSerializedSpans(span, spans); + } + + for (final ActivityLifecycleTimeSpan span : metrics.getActivityLifecycleTimeSpans()) { + addTimeSpanToSerializedSpans(span.getOnCreate(), spans); + addTimeSpanToSerializedSpans(span.getOnStart(), spans); + } + + final @NotNull Map result = new HashMap<>(); + result.put("spans", spans); + result.put("type", metrics.getAppStartType().toString().toLowerCase(Locale.ROOT)); + if (metrics.getAppStartTimeSpan().hasStarted()) { + result.put("app_start_timestamp_ms", metrics.getAppStartTimeSpan().getStartTimestampMs()); + } + + return result; + } + + private static void addTimeSpanToSerializedSpans(TimeSpan span, List> spans) { + if (span.hasNotStarted()) { + HubAdapter.getInstance() + .getOptions() + .getLogger() + .log(SentryLevel.WARNING, "Can not convert not-started TimeSpan to Map for Hybrid SDKs."); + return; + } + + if (span.hasNotStopped()) { + HubAdapter.getInstance() + .getOptions() + .getLogger() + .log(SentryLevel.WARNING, "Can not convert not-stopped TimeSpan to Map for Hybrid SDKs."); + return; + } + + final @NotNull Map spanMap = new HashMap<>(); + spanMap.put("description", span.getDescription()); + spanMap.put("start_timestamp_ms", span.getStartTimestampMs()); + spanMap.put("end_timestamp_ms", span.getProjectedStopTimestampMs()); + spans.add(spanMap); + } + @Nullable private static Session updateSession( final @NotNull IHub hub, diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java index af68a026fb..46590826ef 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java @@ -11,6 +11,7 @@ import io.sentry.Sentry; import io.sentry.SentryLevel; import io.sentry.SentryOptions; +import io.sentry.Session; import io.sentry.android.core.internal.util.BreadcrumbFactory; import io.sentry.android.core.performance.AppStartMetrics; import io.sentry.android.core.performance.TimeSpan; @@ -19,7 +20,9 @@ import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** Sentry initialization class */ public final class SentryAndroid { @@ -146,8 +149,21 @@ public static synchronized void init( final @NotNull IHub hub = Sentry.getCurrentHub(); if (hub.getOptions().isEnableAutoSessionTracking() && ContextUtils.isForegroundImportance()) { - hub.addBreadcrumb(BreadcrumbFactory.forSession("session.start")); - hub.startSession(); + // The LifecycleWatcher of AppLifecycleIntegration may already started a session + // so only start a session if it's not already started + // This e.g. happens on React Native, or e.g. on deferred SDK init + final AtomicBoolean sessionStarted = new AtomicBoolean(false); + hub.configureScope( + scope -> { + final @Nullable Session currentSession = scope.getSession(); + if (currentSession != null && currentSession.getStarted() != null) { + sessionStarted.set(true); + } + }); + if (!sessionStarted.get()) { + hub.addBreadcrumb(BreadcrumbFactory.forSession("session.start")); + hub.startSession(); + } } } catch (IllegalAccessException e) { logger.log(SentryLevel.FATAL, "Fatal error during SentryAndroid.init(...)", e); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SpanFrameMetricsCollector.java b/sentry-android-core/src/main/java/io/sentry/android/core/SpanFrameMetricsCollector.java index d9ed1e5aa0..5535bccb91 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SpanFrameMetricsCollector.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SpanFrameMetricsCollector.java @@ -7,9 +7,7 @@ import io.sentry.NoOpSpan; import io.sentry.NoOpTransaction; import io.sentry.SentryDate; -import io.sentry.SentryLongDate; import io.sentry.SentryNanotimeDate; -import io.sentry.SentryTracer; import io.sentry.SpanDataConvention; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.protocol.MeasurementValue; @@ -33,7 +31,7 @@ public class SpanFrameMetricsCollector // grow indefinitely in case of a long running span private static final int MAX_FRAMES_COUNT = 3600; private static final long ONE_SECOND_NANOS = TimeUnit.SECONDS.toNanos(1); - private static final SentryNanotimeDate UNIX_START_DATE = new SentryNanotimeDate(new Date(0), 0); + private static final SentryNanotimeDate EMPTY_NANO_TIME = new SentryNanotimeDate(new Date(0), 0); private final boolean enabled; private final @NotNull Object lock = new Object(); @@ -125,7 +123,7 @@ public void onSpanFinished(final @NotNull ISpan span) { } else { // otherwise only remove old/irrelevant frames final @NotNull ISpan oldestSpan = runningSpans.first(); - frames.headSet(new Frame(realNanos(oldestSpan.getStartDate()))).clear(); + frames.headSet(new Frame(toNanoTime(oldestSpan.getStartDate()))).clear(); } } } @@ -138,22 +136,20 @@ private void captureFrameMetrics(@NotNull final ISpan span) { return; } - // Ignore spans with no finish date, but SentryTracer is not finished when executing this - // callback, yet, so in that case we use the current timestamp. - final @Nullable SentryDate spanFinishDate = - span instanceof SentryTracer ? new SentryNanotimeDate() : span.getFinishDate(); + final @Nullable SentryDate spanFinishDate = span.getFinishDate(); if (spanFinishDate == null) { return; } - final long spanEndNanos = realNanos(spanFinishDate); - final @NotNull SentryFrameMetrics frameMetrics = new SentryFrameMetrics(); - final long spanStartNanos = realNanos(span.getStartDate()); - if (spanStartNanos >= spanEndNanos) { + final long spanStartNanos = toNanoTime(span.getStartDate()); + final long spanEndNanos = toNanoTime(spanFinishDate); + final long spanDurationNanos = spanEndNanos - spanStartNanos; + if (spanDurationNanos <= 0) { return; } - final long spanDurationNanos = spanEndNanos - spanStartNanos; + final @NotNull SentryFrameMetrics frameMetrics = new SentryFrameMetrics(); + long frameDurationNanos = lastKnownFrameDurationNanos; if (!frames.isEmpty()) { @@ -199,11 +195,15 @@ private void captureFrameMetrics(@NotNull final ISpan span) { int totalFrameCount = frameMetrics.getTotalFrameCount(); final long nextScheduledFrameNanos = frameMetricsCollector.getLastKnownFrameStartTimeNanos(); - totalFrameCount += - addPendingFrameDelay( - frameMetrics, frameDurationNanos, spanEndNanos, nextScheduledFrameNanos); - totalFrameCount += interpolateFrameCount(frameMetrics, frameDurationNanos, spanDurationNanos); - + // nextScheduledFrameNanos might be -1 if no frames have been scheduled for drawing yet + // e.g. can happen during early app start + if (nextScheduledFrameNanos != -1) { + totalFrameCount += + addPendingFrameDelay( + frameMetrics, frameDurationNanos, spanEndNanos, nextScheduledFrameNanos); + totalFrameCount += + interpolateFrameCount(frameMetrics, frameDurationNanos, spanDurationNanos); + } final long frameDelayNanos = frameMetrics.getSlowFrameDelayNanos() + frameMetrics.getFrozenFrameDelayNanos(); final double frameDelayInSeconds = frameDelayNanos / 1e9d; @@ -305,19 +305,20 @@ private static int addPendingFrameDelay( * diff does ¯\_(ツ)_/¯ * * @param date the input date - * @return a timestamp in nano precision + * @return a non-unix timestamp in nano precision, similar to {@link System#nanoTime()}. */ - private static long realNanos(final @NotNull SentryDate date) { - // SentryNanotimeDate nanotime is based on System.nanotime(), like UNIX_START_DATE + private static long toNanoTime(final @NotNull SentryDate date) { + // SentryNanotimeDate nanotime is based on System.nanotime(), like EMPTY_NANO_TIME, + // thus diff will simply return the System.nanotime() value of date if (date instanceof SentryNanotimeDate) { - return date.diff(UNIX_START_DATE); + return date.diff(EMPTY_NANO_TIME); } - // SentryLongDate nanotime is based on current date converted to nanoseconds, which is a - // different order than frames based System.nanotime(). So we have to convert the nanotime of - // the SentryLongDate to a System.nanotime() compatible one. - return date.diff(new SentryLongDate(DateUtils.millisToNanos(System.currentTimeMillis()))) - + System.nanoTime(); + // e.g. SentryLongDate is unix time based - upscaled to nanos, + // we need to project it back to System.nanotime() format + long nowUnixInNanos = DateUtils.millisToNanos(System.currentTimeMillis()); + long shiftInNanos = nowUnixInNanos - date.nanoTimestamp(); + return System.nanoTime() - shiftInNanos; } private static class Frame implements Comparable { 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 2d20a16ec5..f936b6251c 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 @@ -303,7 +303,7 @@ class ActivityLifecycleIntegrationTest { sut.ttidSpanMap.values.first().finish() sut.ttfdSpanMap.values.first().finish() - // then transaction should not be immediatelly finished + // then transaction should not be immediately finished verify(fixture.hub, never()) .captureTransaction( anyOrNull(), diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt index a726b2c55b..8219a273d0 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt @@ -178,6 +178,7 @@ class AndroidProfilerTest { val endData = profiler.endAndCollect(false, null) assertNotNull(startData?.startNanos) assertNotNull(startData?.startCpuMillis) + assertNotNull(startData?.startTimestamp) assertNotNull(endData?.endNanos) assertNotNull(endData?.endCpuMillis) } @@ -218,7 +219,7 @@ class AndroidProfilerTest { } @Test - fun `timedOutData has timeout flag`() { + fun `timedOutData is not recorded`() { val profiler = fixture.getSut() // Start and finish first transaction profiling @@ -229,7 +230,7 @@ class AndroidProfilerTest { // First transaction finishes: timed out data is returned val endData = profiler.endAndCollect(false, null) - assert(endData!!.didTimeout) + assertNull(endData) } @Test diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt index 405aa6dc98..fd03d34631 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt @@ -53,7 +53,7 @@ class AndroidTransactionProfilerTest { private class Fixture { private val mockDsn = "http://key@localhost/proj" val buildInfo = mock { - whenever(it.sdkInfoVersion).thenReturn(Build.VERSION_CODES.LOLLIPOP) + whenever(it.sdkInfoVersion).thenReturn(Build.VERSION_CODES.LOLLIPOP_MR1) } val mockLogger = mock() var lastScheduledRunnable: Runnable? = null @@ -224,9 +224,9 @@ class AndroidTransactionProfilerTest { } @Test - fun `profiler works only on api 21+`() { + fun `profiler works only on api 22+`() { val buildInfo = mock { - whenever(it.sdkInfoVersion).thenReturn(Build.VERSION_CODES.KITKAT) + whenever(it.sdkInfoVersion).thenReturn(Build.VERSION_CODES.LOLLIPOP) } val profiler = fixture.getSut(context, buildInfo) profiler.start() @@ -379,7 +379,7 @@ class AndroidTransactionProfilerTest { } @Test - fun `timedOutData has timeout truncation reason`() { + fun `timedOutData is not recorded`() { val profiler = fixture.getSut(context) // Start and finish first transaction profiling @@ -391,8 +391,7 @@ class AndroidTransactionProfilerTest { // First transaction finishes: timed out data is returned val profilingTraceData = profiler.onTransactionFinish(fixture.transaction1, null, fixture.options) - assertEquals(profilingTraceData!!.transactionId, fixture.transaction1.eventId.toString()) - assertEquals(ProfilingTraceData.TRUNCATION_REASON_TIMEOUT, profilingTraceData.truncationReason) + assertNull(profilingTraceData) } @Test 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 a10d8add7b..65ecdb3b8c 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 @@ -1,5 +1,7 @@ package io.sentry.android.core +import android.app.Application +import android.content.ContentProvider import android.content.Context import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -17,6 +19,8 @@ import io.sentry.SentryExceptionFactory import io.sentry.SentryItemType import io.sentry.SentryOptions import io.sentry.Session +import io.sentry.android.core.performance.ActivityLifecycleTimeSpan +import io.sentry.android.core.performance.AppStartMetrics import io.sentry.exception.ExceptionMechanismException import io.sentry.protocol.App import io.sentry.protocol.Contexts @@ -101,6 +105,81 @@ class InternalSentrySdkTest { InternalSentrySdk.captureEnvelope(data) } + + fun mockFinishedAppStart() { + val metrics = AppStartMetrics.getInstance() + + metrics.appStartType = AppStartMetrics.AppStartType.WARM + + metrics.appStartTimeSpan.setStartedAt(20) // Can't be 0, as that's the default value if not set + metrics.appStartTimeSpan.setStartUnixTimeMs(20) // The order matters, unix time must be set after started at in tests to avoid overwrite + metrics.appStartTimeSpan.setStoppedAt(200) + metrics.classLoadedUptimeMs = 100 + + AppStartMetrics.onApplicationCreate(mock()) + metrics.applicationOnCreateTimeSpan.description = "Application created" + metrics.applicationOnCreateTimeSpan.setStartedAt(30) // Can't be 0, as that's the default value if not set + metrics.applicationOnCreateTimeSpan.setStartUnixTimeMs(30) // The order matters, unix time must be set after started at in tests to avoid overwrite + metrics.applicationOnCreateTimeSpan.setStoppedAt(40) + + val activityLifecycleSpan = ActivityLifecycleTimeSpan() + activityLifecycleSpan.onCreate.description = "Test Activity Lifecycle onCreate" + activityLifecycleSpan.onCreate.setStartedAt(50) // Can't be 0, as that's the default value if not set + activityLifecycleSpan.onCreate.setStartUnixTimeMs(50) // The order matters, unix time must be set after started at in tests to avoid overwrite + activityLifecycleSpan.onCreate.setStoppedAt(60) + + activityLifecycleSpan.onStart.description = "Test Activity Lifecycle onStart" + activityLifecycleSpan.onStart.setStartedAt(70) // Can't be 0, as that's the default value if not set + activityLifecycleSpan.onStart.setStartUnixTimeMs(70) // The order matters, unix time must be set after started at in tests to avoid overwrite + activityLifecycleSpan.onStart.setStoppedAt(80) + metrics.addActivityLifecycleTimeSpans(activityLifecycleSpan) + + AppStartMetrics.onContentProviderCreate(mock()) + metrics.contentProviderOnCreateTimeSpans[0].description = "Test Content Provider created" + metrics.contentProviderOnCreateTimeSpans[0].setStartedAt(90) + metrics.contentProviderOnCreateTimeSpans[0].setStartUnixTimeMs(90) + metrics.contentProviderOnCreateTimeSpans[0].setStoppedAt(100) + + metrics.appStartProfiler = mock() + metrics.appStartSamplingDecision = mock() + } + + fun mockMinimumFinishedAppStart() { + val metrics = AppStartMetrics.getInstance() + + metrics.appStartType = AppStartMetrics.AppStartType.WARM + + metrics.appStartTimeSpan.setStartedAt(20) // Can't be 0, as that's the default value if not set + metrics.appStartTimeSpan.setStartUnixTimeMs(20) // The order matters, unix time must be set after started at in tests to avoid overwrite + metrics.appStartTimeSpan.setStoppedAt(200) + metrics.classLoadedUptimeMs = 100 + + AppStartMetrics.onApplicationCreate(mock()) + metrics.applicationOnCreateTimeSpan.description = "Application created" + metrics.applicationOnCreateTimeSpan.setStartedAt(30) // Can't be 0, as that's the default value if not set + metrics.applicationOnCreateTimeSpan.setStartUnixTimeMs(30) // The order matters, unix time must be set after started at in tests to avoid overwrite + metrics.applicationOnCreateTimeSpan.setStoppedAt(40) + } + + fun mockUnfinishedAppStart() { + val metrics = AppStartMetrics.getInstance() + + metrics.appStartType = AppStartMetrics.AppStartType.WARM + + metrics.appStartTimeSpan.setStartedAt(20) // Can't be 0, as that's the default value if not set + metrics.appStartTimeSpan.setStartUnixTimeMs(20) // The order matters, unix time must be set after started at in tests to avoid overwrite + metrics.appStartTimeSpan.setStoppedAt(200) + metrics.classLoadedUptimeMs = 100 + + AppStartMetrics.onApplicationCreate(mock()) + metrics.applicationOnCreateTimeSpan.description = "Application created" + metrics.applicationOnCreateTimeSpan.setStartedAt(30) // Can't be 0, as that's the default value if not set + + val activityLifecycleSpan = ActivityLifecycleTimeSpan() // Expect the created spans are not started nor stopped + activityLifecycleSpan.onCreate.description = "Test Activity Lifecycle onCreate" + activityLifecycleSpan.onStart.description = "Test Activity Lifecycle onStart" + metrics.addActivityLifecycleTimeSpans(activityLifecycleSpan) + } } @BeforeTest @@ -302,4 +381,83 @@ class InternalSentrySdkTest { } assertEquals(Session.State.Crashed, scopeRef.get().session!!.status) } + + @Test + fun `getAppStartMeasurement returns correct serialized data from the app start instance`() { + Fixture().mockFinishedAppStart() + + val serializedAppStart = InternalSentrySdk.getAppStartMeasurement() + + assertEquals("warm", serializedAppStart["type"]) + assertEquals(20.toLong(), serializedAppStart["app_start_timestamp_ms"]) + + val actualSpans = serializedAppStart["spans"] as List<*> + assertEquals(5, actualSpans.size) + + val actualProcessSpan = actualSpans[0] as Map<*, *> + assertEquals("Process Initialization", actualProcessSpan["description"]) + assertEquals(20.toLong(), actualProcessSpan["start_timestamp_ms"]) + assertEquals(100.toLong(), actualProcessSpan["end_timestamp_ms"]) + + val actualAppSpan = actualSpans[1] as Map<*, *> + assertEquals("Application created", actualAppSpan["description"]) + assertEquals(30.toLong(), actualAppSpan["start_timestamp_ms"]) + assertEquals(40.toLong(), actualAppSpan["end_timestamp_ms"]) + + val actualContentProviderSpan = actualSpans[2] as Map<*, *> + assertEquals("Test Content Provider created", actualContentProviderSpan["description"]) + assertEquals(90.toLong(), actualContentProviderSpan["start_timestamp_ms"]) + assertEquals(100.toLong(), actualContentProviderSpan["end_timestamp_ms"]) + + val actualActivityOnCreateSpan = actualSpans[3] as Map<*, *> + assertEquals("Test Activity Lifecycle onCreate", actualActivityOnCreateSpan["description"]) + assertEquals(50.toLong(), actualActivityOnCreateSpan["start_timestamp_ms"]) + assertEquals(60.toLong(), actualActivityOnCreateSpan["end_timestamp_ms"]) + + val actualActivityOnStartSpan = actualSpans[4] as Map<*, *> + assertEquals("Test Activity Lifecycle onStart", actualActivityOnStartSpan["description"]) + assertEquals(70.toLong(), actualActivityOnStartSpan["start_timestamp_ms"]) + assertEquals(80.toLong(), actualActivityOnStartSpan["end_timestamp_ms"]) + } + + @Test + fun `getAppStartMeasurement returns correct serialized data from the minimum app start instance`() { + Fixture().mockMinimumFinishedAppStart() + + val serializedAppStart = InternalSentrySdk.getAppStartMeasurement() + + assertEquals("warm", serializedAppStart["type"]) + assertEquals(20.toLong(), serializedAppStart["app_start_timestamp_ms"]) + + val actualSpans = serializedAppStart["spans"] as List<*> + assertEquals(2, actualSpans.size) + + val actualProcessSpan = actualSpans[0] as Map<*, *> + assertEquals("Process Initialization", actualProcessSpan["description"]) + assertEquals(20.toLong(), actualProcessSpan["start_timestamp_ms"]) + assertEquals(100.toLong(), actualProcessSpan["end_timestamp_ms"]) + + val actualAppSpan = actualSpans[1] as Map<*, *> + assertEquals("Application created", actualAppSpan["description"]) + assertEquals(30.toLong(), actualAppSpan["start_timestamp_ms"]) + assertEquals(40.toLong(), actualAppSpan["end_timestamp_ms"]) + } + + @Test + fun `getAppStartMeasurement returns only stopped spans in serialized data`() { + Fixture().mockUnfinishedAppStart() + + val serializedAppStart = InternalSentrySdk.getAppStartMeasurement() + + assertEquals("warm", serializedAppStart["type"]) + assertEquals(20.toLong(), serializedAppStart["app_start_timestamp_ms"]) + + val actualSpans = serializedAppStart["spans"] as List<*> + assertEquals(1, actualSpans.size) + + val actualProcessSpan = actualSpans[0] as Map<*, *> + assertEquals("Process Initialization", actualProcessSpan["description"]) + assertEquals(20.toLong(), actualProcessSpan["start_timestamp_ms"]) + assertEquals(100.toLong(), actualProcessSpan["end_timestamp_ms"]) + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt index cd0f8ed8c0..990c3f4b13 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt @@ -12,6 +12,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.Breadcrumb import io.sentry.Hint import io.sentry.ILogger +import io.sentry.ISentryClient import io.sentry.Sentry import io.sentry.SentryEnvelope import io.sentry.SentryLevel @@ -50,6 +51,7 @@ import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.spy +import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.robolectric.annotation.Config @@ -314,8 +316,24 @@ class SentryAndroidTest { } } + @Test + fun `init does not start a session if one is already running`() { + val client = mock() + + initSentryWithForegroundImportance(true, { options -> + options.addIntegration { hub, _ -> + hub.bindClient(client) + // usually done by LifecycleWatcher + hub.startSession() + } + }) {} + + verify(client, times(1)).captureSession(any(), any()) + } + private fun initSentryWithForegroundImportance( inForeground: Boolean, + optionsConfig: (SentryAndroidOptions) -> Unit = {}, callback: (session: Session?) -> Unit ) { val context = ContextUtilsTestHelper.createMockContext() @@ -327,6 +345,7 @@ class SentryAndroidTest { options.release = "prod" options.dsn = "https://key@sentry.io/123" options.isEnableAutoSessionTracking = true + optionsConfig(options) } var session: Session? = null diff --git a/sentry-android-fragment/src/main/java/io/sentry/android/fragment/SentryFragmentLifecycleCallbacks.kt b/sentry-android-fragment/src/main/java/io/sentry/android/fragment/SentryFragmentLifecycleCallbacks.kt index 78f45474e2..5e03c99e0d 100644 --- a/sentry-android-fragment/src/main/java/io/sentry/android/fragment/SentryFragmentLifecycleCallbacks.kt +++ b/sentry-android-fragment/src/main/java/io/sentry/android/fragment/SentryFragmentLifecycleCallbacks.kt @@ -96,12 +96,13 @@ class SentryFragmentLifecycleCallbacks( override fun onFragmentStarted(fragmentManager: FragmentManager, fragment: Fragment) { addBreadcrumb(fragment, FragmentLifecycleState.STARTED) + + // ViewPager2 locks background fragments to STARTED state + stopTracing(fragment) } override fun onFragmentResumed(fragmentManager: FragmentManager, fragment: Fragment) { addBreadcrumb(fragment, FragmentLifecycleState.RESUMED) - - stopTracing(fragment) } override fun onFragmentPaused(fragmentManager: FragmentManager, fragment: Fragment) { diff --git a/sentry-android-fragment/src/test/java/io/sentry/android/fragment/SentryFragmentLifecycleCallbacksTest.kt b/sentry-android-fragment/src/test/java/io/sentry/android/fragment/SentryFragmentLifecycleCallbacksTest.kt index 9b7fd5c5f2..812d78de30 100644 --- a/sentry-android-fragment/src/test/java/io/sentry/android/fragment/SentryFragmentLifecycleCallbacksTest.kt +++ b/sentry-android-fragment/src/test/java/io/sentry/android/fragment/SentryFragmentLifecycleCallbacksTest.kt @@ -229,11 +229,11 @@ class SentryFragmentLifecycleCallbacksTest { } @Test - fun `When fragment is resumed, it should stop tracing if enabled`() { + fun `When fragment is started, it should stop tracing if enabled`() { val sut = fixture.getSut(enableAutoFragmentLifecycleTracing = true) sut.onFragmentCreated(fixture.fragmentManager, fixture.fragment, savedInstanceState = null) - sut.onFragmentResumed(fixture.fragmentManager, fixture.fragment) + sut.onFragmentStarted(fixture.fragmentManager, fixture.fragment) verify(fixture.span).finish( check { @@ -243,12 +243,12 @@ class SentryFragmentLifecycleCallbacksTest { } @Test - fun `When fragment is resumed, it should stop tracing if enabled but keep status`() { + fun `When fragment is started, it should stop tracing if enabled but keep status`() { val sut = fixture.getSut(enableAutoFragmentLifecycleTracing = true) whenever(fixture.span.status).thenReturn(SpanStatus.ABORTED) sut.onFragmentCreated(fixture.fragmentManager, fixture.fragment, savedInstanceState = null) - sut.onFragmentResumed(fixture.fragmentManager, fixture.fragment) + sut.onFragmentStarted(fixture.fragmentManager, fixture.fragment) verify(fixture.span).finish( check { diff --git a/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts b/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts index 16f6b9b9ad..98b00eb4f8 100644 --- a/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts +++ b/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts @@ -97,6 +97,7 @@ dependencies { if (applySentryIntegrations) { implementation(projects.sentryAndroid) implementation(projects.sentryCompose) + implementation(projects.sentryComposeHelper) } else { implementation(projects.sentryAndroidCore) } diff --git a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/EnvelopeTests.kt b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/EnvelopeTests.kt index ebf729f6b6..c8ebc87264 100644 --- a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/EnvelopeTests.kt +++ b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/EnvelopeTests.kt @@ -201,13 +201,9 @@ class EnvelopeTests : BaseUiTest() { assertEnvelopeTransaction(it.items.toList(), AndroidLogger()).transaction == "timedOutProfile" }.assert { val transactionItem: SentryTransaction = it.assertTransaction() - val profilingTraceData: ProfilingTraceData = it.assertProfile() + // Profile should not be present, as it timed out and is discarded it.assertNoOtherItems() assertEquals("timedOutProfile", transactionItem.transaction) - assertEquals("timedOutProfile", profilingTraceData.transactionName) - // The profile should timeout after 30 seconds - assertTrue(profilingTraceData.durationNs.toLong() < TimeUnit.SECONDS.toNanos(31), "Profile duration expected to be less than 31 seconds. It was ${profilingTraceData.durationNs.toLong()} ns") - assertEquals(ProfilingTraceData.TRUNCATION_REASON_TIMEOUT, profilingTraceData.truncationReason) } assertNoOtherEnvelopes() } diff --git a/sentry-android-ndk/sentry-native b/sentry-android-ndk/sentry-native index 4ec95c0725..0f1d664759 160000 --- a/sentry-android-ndk/sentry-native +++ b/sentry-android-ndk/sentry-native @@ -1 +1 @@ -Subproject commit 4ec95c0725df5f34440db8fa8d37b4c519fce74e +Subproject commit 0f1d664759cba187a846a562f9d55f3c62dffaa3 diff --git a/sentry-compose/build.gradle.kts b/sentry-compose/build.gradle.kts index 1291ea1696..114c08a22f 100644 --- a/sentry-compose/build.gradle.kts +++ b/sentry-compose/build.gradle.kts @@ -1,6 +1,4 @@ import com.android.build.gradle.internal.tasks.LibraryAarJarsTask -import groovy.util.Node -import groovy.util.NodeList import io.gitlab.arturbosch.detekt.Detekt import org.jetbrains.dokka.gradle.DokkaTask @@ -44,7 +42,7 @@ kotlin { compileOnly(compose.runtime) compileOnly(compose.ui) - api(projects.sentryComposeHelper) + compileOnly(projects.sentryComposeHelper) } } val androidMain by getting { @@ -149,32 +147,3 @@ dependencies { tasks.withType { mainScopeClassFiles.setFrom(embedComposeHelperConfig) } - -// we embed the sentry-compose-helper classes to the same .jar above -// so we need to exclude the dependency from the .pom publication and .module metadata -configure { - publications.withType(MavenPublication::class.java).all { - this.pom { - this.withXml { - (asNode().get("dependencies") as NodeList) - .flatMap { - if (it is Node) it.children() else NodeList() - } - .filterIsInstance() - .filter { dependency -> - val artifactIdNodes = dependency.get("artifactId") as NodeList - artifactIdNodes.any { - (it is Node && it.value().toString().contains("sentry-compose-helper")) - } - } - .forEach { dependency -> - dependency.parent().remove(dependency) - } - } - } - } -} - -tasks.withType { - enabled = false -} diff --git a/sentry-samples/sentry-samples-android/build.gradle.kts b/sentry-samples/sentry-samples-android/build.gradle.kts index ac77791268..a8d8897519 100644 --- a/sentry-samples/sentry-samples-android/build.gradle.kts +++ b/sentry-samples/sentry-samples-android/build.gradle.kts @@ -111,6 +111,7 @@ dependencies { implementation(projects.sentryAndroidFragment) implementation(projects.sentryAndroidTimber) implementation(projects.sentryCompose) + implementation(projects.sentryComposeHelper) implementation(Config.Libs.fragment) implementation(Config.Libs.timber) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 121ef1af76..6aa44b53f5 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -1420,7 +1420,7 @@ public final class io/sentry/ProfilingTraceData : io/sentry/JsonSerializable, io public static final field TRUNCATION_REASON_NORMAL Ljava/lang/String; public static final field TRUNCATION_REASON_TIMEOUT Ljava/lang/String; public fun (Ljava/io/File;Lio/sentry/ITransaction;)V - public fun (Ljava/io/File;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/util/concurrent/Callable;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V + public fun (Ljava/io/File;Ljava/util/Date;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/util/concurrent/Callable;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V public fun getAndroidApiLevel ()I public fun getBuildId ()Ljava/lang/String; public fun getCpuArchitecture ()Ljava/lang/String; @@ -1439,6 +1439,7 @@ public final class io/sentry/ProfilingTraceData : io/sentry/JsonSerializable, io public fun getProfileId ()Ljava/lang/String; public fun getRelease ()Ljava/lang/String; public fun getSampledProfile ()Ljava/lang/String; + public fun getTimestamp ()Ljava/util/Date; public fun getTraceFile ()Ljava/io/File; public fun getTraceId ()Ljava/lang/String; public fun getTransactionId ()Ljava/lang/String; @@ -1465,6 +1466,7 @@ public final class io/sentry/ProfilingTraceData : io/sentry/JsonSerializable, io public fun setProfileId (Ljava/lang/String;)V public fun setRelease (Ljava/lang/String;)V public fun setSampledProfile (Ljava/lang/String;)V + public fun setTimestamp (Ljava/util/Date;)V public fun setTraceId (Ljava/lang/String;)V public fun setTransactionId (Ljava/lang/String;)V public fun setTransactionName (Ljava/lang/String;)V @@ -1499,6 +1501,7 @@ public final class io/sentry/ProfilingTraceData$JsonKeys { public static final field PROFILE_ID Ljava/lang/String; public static final field RELEASE Ljava/lang/String; public static final field SAMPLED_PROFILE Ljava/lang/String; + public static final field TIMESTAMP Ljava/lang/String; public static final field TRACE_ID Ljava/lang/String; public static final field TRANSACTION_ID Ljava/lang/String; public static final field TRANSACTION_LIST Ljava/lang/String; @@ -2980,7 +2983,7 @@ public final class io/sentry/TypeCheckHint { public final class io/sentry/UncaughtExceptionHandlerIntegration : io/sentry/Integration, java/io/Closeable, java/lang/Thread$UncaughtExceptionHandler { public fun ()V public fun close ()V - public final fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V + public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V public fun uncaughtException (Ljava/lang/Thread;Ljava/lang/Throwable;)V } diff --git a/sentry/src/main/java/io/sentry/MetricsAggregator.java b/sentry/src/main/java/io/sentry/MetricsAggregator.java index 71e5a40f3f..ebc634700b 100644 --- a/sentry/src/main/java/io/sentry/MetricsAggregator.java +++ b/sentry/src/main/java/io/sentry/MetricsAggregator.java @@ -239,6 +239,8 @@ public void flush(boolean force) { force = true; } + flushScheduled = false; + final @NotNull Set flushableBuckets = getFlushableBuckets(force); if (flushableBuckets.isEmpty()) { logger.log(SentryLevel.DEBUG, "Metrics: nothing to flush"); diff --git a/sentry/src/main/java/io/sentry/ProfilingTraceData.java b/sentry/src/main/java/io/sentry/ProfilingTraceData.java index 35902f5f04..d1410245af 100644 --- a/sentry/src/main/java/io/sentry/ProfilingTraceData.java +++ b/sentry/src/main/java/io/sentry/ProfilingTraceData.java @@ -5,6 +5,7 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -64,6 +65,7 @@ public final class ProfilingTraceData implements JsonUnknown, JsonSerializable { private @NotNull String profileId; private @NotNull String environment; private @NotNull String truncationReason; + private @NotNull Date timestamp; private final @NotNull Map measurementsMap; // Stacktrace (file) @@ -78,6 +80,7 @@ public ProfilingTraceData( final @NotNull File traceFile, final @NotNull ITransaction transaction) { this( traceFile, + DateUtils.getCurrentDateTime(), new ArrayList<>(), transaction.getName(), transaction.getEventId().toString(), @@ -101,6 +104,7 @@ public ProfilingTraceData( public ProfilingTraceData( @NotNull File traceFile, + @NotNull Date profileStartTimestamp, @NotNull List transactions, @NotNull String transactionName, @NotNull String transactionId, @@ -120,6 +124,7 @@ public ProfilingTraceData( @NotNull String truncationReason, final @NotNull Map measurementsMap) { this.traceFile = traceFile; + this.timestamp = profileStartTimestamp; this.cpuArchitecture = cpuArchitecture; this.deviceCpuFrequenciesReader = deviceCpuFrequenciesReader; @@ -262,6 +267,10 @@ public boolean isDeviceIsEmulator() { return truncationReason; } + public @NotNull Date getTimestamp() { + return timestamp; + } + public @NotNull Map getMeasurementsMap() { return measurementsMap; } @@ -306,6 +315,10 @@ public void setDevicePhysicalMemoryBytes(final @NotNull String devicePhysicalMem this.devicePhysicalMemoryBytes = devicePhysicalMemoryBytes; } + public void setTimestamp(final @NotNull Date timestamp) { + this.timestamp = timestamp; + } + public void setTruncationReason(final @NotNull String truncationReason) { this.truncationReason = truncationReason; } @@ -386,6 +399,7 @@ public static final class JsonKeys { public static final String SAMPLED_PROFILE = "sampled_profile"; public static final String TRUNCATION_REASON = "truncation_reason"; public static final String MEASUREMENTS = "measurements"; + public static final String TIMESTAMP = "timestamp"; } @Override @@ -422,6 +436,7 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger writer.name(JsonKeys.SAMPLED_PROFILE).value(sampledProfile); } writer.name(JsonKeys.MEASUREMENTS).value(logger, measurementsMap); + writer.name(JsonKeys.TIMESTAMP).value(logger, timestamp); if (unknown != null) { for (String key : unknown.keySet()) { Object value = unknown.get(key); @@ -602,6 +617,12 @@ public static final class Deserializer implements JsonDeserializer performanceCollectionData = null; - if (transactionPerformanceCollector != null) { - performanceCollectionData = transactionPerformanceCollector.stop(this); - } + + final @NotNull AtomicReference> performanceCollectionData = + new AtomicReference<>(); + // We set the new spanFinishedCallback here instead of creation time, calling the old one to + // avoid the user overwrites it by setting a custom spanFinishedCallback on the root span + final @Nullable SpanFinishedCallback oldCallback = this.root.getSpanFinishedCallback(); + this.root.setSpanFinishedCallback( + span -> { + if (oldCallback != null) { + oldCallback.execute(span); + } + + // Let's call the finishCallback here, when the root span has a finished date but it's + // not finished, yet + final @Nullable TransactionFinishedCallback finishedCallback = + transactionOptions.getTransactionFinishedCallback(); + if (finishedCallback != null) { + finishedCallback.execute(this); + } + + if (transactionPerformanceCollector != null) { + performanceCollectionData.set(transactionPerformanceCollector.stop(this)); + } + }); + + // 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); ProfilingTraceData profilingTraceData = null; if (Boolean.TRUE.equals(isSampled()) && Boolean.TRUE.equals(isProfileSampled())) { profilingTraceData = hub.getOptions() .getTransactionProfiler() - .onTransactionFinish(this, performanceCollectionData, hub.getOptions()); + .onTransactionFinish(this, performanceCollectionData.get(), hub.getOptions()); } - if (performanceCollectionData != null) { - performanceCollectionData.clear(); + if (performanceCollectionData.get() != null) { + performanceCollectionData.get().clear(); } - // 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( scope -> { scope.withTransaction( @@ -232,11 +251,6 @@ public void finish( }); }); final SentryTransaction transaction = new SentryTransaction(this); - final TransactionFinishedCallback finishedCallback = - transactionOptions.getTransactionFinishedCallback(); - if (finishedCallback != null) { - finishedCallback.execute(this); - } if (timer != null) { synchronized (timerLock) { @@ -605,7 +619,9 @@ private boolean hasAllChildrenFinished() { final List spans = new ArrayList<>(this.children); if (!spans.isEmpty()) { for (final Span span : spans) { - if (!span.isFinished()) { + // This is used in the spanFinishCallback, when the span isn't finished, but has a finish + // date + if (!span.isFinished() && span.getFinishDate() == null) { return false; } } diff --git a/sentry/src/main/java/io/sentry/ShutdownHookIntegration.java b/sentry/src/main/java/io/sentry/ShutdownHookIntegration.java index b144f2d88a..4ffb47d7d1 100644 --- a/sentry/src/main/java/io/sentry/ShutdownHookIntegration.java +++ b/sentry/src/main/java/io/sentry/ShutdownHookIntegration.java @@ -33,9 +33,12 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio if (options.isEnableShutdownHook()) { thread = new Thread(() -> hub.flush(options.getFlushTimeoutMillis())); - runtime.addShutdownHook(thread); - options.getLogger().log(SentryLevel.DEBUG, "ShutdownHookIntegration installed."); - addIntegrationToSdkVersion(getClass()); + handleShutdownInProgress( + () -> { + runtime.addShutdownHook(thread); + options.getLogger().log(SentryLevel.DEBUG, "ShutdownHookIntegration installed."); + addIntegrationToSdkVersion(getClass()); + }); } else { options.getLogger().log(SentryLevel.INFO, "enableShutdownHook is disabled."); } @@ -44,16 +47,22 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio @Override public void close() throws IOException { if (thread != null) { - try { - runtime.removeShutdownHook(thread); - } catch (IllegalStateException e) { - @Nullable final String message = e.getMessage(); - // https://github.com/openjdk/jdk/blob/09b8a1959771213cb982d062f0a913285e4a0c6e/src/java.base/share/classes/java/lang/ApplicationShutdownHooks.java#L83 - if (message != null && message.equals("Shutdown in progress")) { - // ignore - } else { - throw e; - } + handleShutdownInProgress(() -> runtime.removeShutdownHook(thread)); + } + } + + private void handleShutdownInProgress(final @NotNull Runnable runnable) { + try { + runnable.run(); + } catch (IllegalStateException e) { + @Nullable final String message = e.getMessage(); + // https://github.com/openjdk/jdk/blob/09b8a1959771213cb982d062f0a913285e4a0c6e/src/java.base/share/classes/java/lang/ApplicationShutdownHooks.java#L83 + if (message != null + && (message.equals("Shutdown in progress") + || message.equals("VM already shutting down"))) { + // ignore + } else { + throw e; } } } diff --git a/sentry/src/main/java/io/sentry/Span.java b/sentry/src/main/java/io/sentry/Span.java index 850276dac3..14cf4825bf 100644 --- a/sentry/src/main/java/io/sentry/Span.java +++ b/sentry/src/main/java/io/sentry/Span.java @@ -37,7 +37,9 @@ public final class Span implements ISpan { private final @NotNull IHub hub; - private final @NotNull AtomicBoolean finished = new AtomicBoolean(false); + private boolean finished = false; + + private final @NotNull AtomicBoolean isFinishing = new AtomicBoolean(false); private final @NotNull SpanOptions options; @@ -122,7 +124,7 @@ public Span( final @Nullable SentryDate timestamp, final @NotNull Instrumenter instrumenter, @NotNull SpanOptions spanOptions) { - if (finished.get()) { + if (finished) { return NoOpSpan.getInstance(); } @@ -133,7 +135,7 @@ public Span( @Override public @NotNull ISpan startChild( final @NotNull String operation, final @Nullable String description) { - if (finished.get()) { + if (finished) { return NoOpSpan.getInstance(); } @@ -143,7 +145,7 @@ public Span( @Override public @NotNull ISpan startChild( @NotNull String operation, @Nullable String description, @NotNull SpanOptions spanOptions) { - if (finished.get()) { + if (finished) { return NoOpSpan.getInstance(); } return transaction.startChild(context.getSpanId(), operation, description, spanOptions); @@ -192,7 +194,7 @@ public void finish(@Nullable SpanStatus status) { @Override public void finish(final @Nullable SpanStatus status, final @Nullable SentryDate timestamp) { // the span can be finished only once - if (!finished.compareAndSet(false, true)) { + if (finished || !isFinishing.compareAndSet(false, true)) { return; } @@ -235,6 +237,7 @@ public void finish(final @Nullable SpanStatus status, final @Nullable SentryDate if (spanFinishedCallback != null) { spanFinishedCallback.execute(this); } + finished = true; } @Override @@ -284,7 +287,7 @@ public void setTag(final @NotNull String key, final @NotNull String value) { @Override public boolean isFinished() { - return finished.get(); + return finished; } public @NotNull Map getData() { @@ -409,6 +412,11 @@ void setSpanFinishedCallback(final @Nullable SpanFinishedCallback callback) { this.spanFinishedCallback = callback; } + @Nullable + SpanFinishedCallback getSpanFinishedCallback() { + return spanFinishedCallback; + } + private void updateStartDate(@NotNull SentryDate date) { this.startTimestamp = date; } diff --git a/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java b/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java index 33e1a4a815..da594e4cb4 100644 --- a/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java +++ b/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java @@ -43,7 +43,7 @@ public UncaughtExceptionHandlerIntegration() { } @Override - public final void register(final @NotNull IHub hub, final @NotNull SentryOptions options) { + public void register(final @NotNull IHub hub, final @NotNull SentryOptions options) { if (registered) { options .getLogger() @@ -75,7 +75,14 @@ public final void register(final @NotNull IHub hub, final @NotNull SentryOptions "default UncaughtExceptionHandler class='" + currentHandler.getClass().getName() + "'"); - defaultExceptionHandler = currentHandler; + + if (currentHandler instanceof UncaughtExceptionHandlerIntegration) { + final UncaughtExceptionHandlerIntegration currentHandlerIntegration = + (UncaughtExceptionHandlerIntegration) currentHandler; + defaultExceptionHandler = currentHandlerIntegration.defaultExceptionHandler; + } else { + defaultExceptionHandler = currentHandler; + } } threadAdapter.setDefaultUncaughtExceptionHandler(this); diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java b/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java index 46e22ca8ad..c4353c53a3 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java @@ -1,11 +1,11 @@ package io.sentry.metrics; import io.sentry.MeasurementUnit; +import java.security.SecureRandom; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.Random; import java.util.regex.Pattern; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -27,7 +27,7 @@ public final class MetricsHelper { private static final char TAGS_ESCAPE_CHAR = '\\'; private static long FLUSH_SHIFT_MS = - (long) (new Random().nextFloat() * (ROLLUP_IN_SECONDS * 1000f)); + (long) (new SecureRandom().nextFloat() * (ROLLUP_IN_SECONDS * 1000f)); public static long getTimeBucketKey(final long timestampMs) { final long seconds = timestampMs / 1000; diff --git a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt index 8dc4f804bc..30f337dce4 100644 --- a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt +++ b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt @@ -492,6 +492,7 @@ class JsonSerializerTest { @Test fun `serializes profilingTraceData`() { val profilingTraceData = ProfilingTraceData(fixture.traceFile, NoOpTransaction.getInstance()) + val now = Date() profilingTraceData.androidApiLevel = 21 profilingTraceData.deviceLocale = "deviceLocale" profilingTraceData.deviceManufacturer = "deviceManufacturer" @@ -503,6 +504,7 @@ class JsonSerializerTest { profilingTraceData.deviceCpuFrequencies = listOf(1, 2, 3, 4) profilingTraceData.devicePhysicalMemoryBytes = "2000000" profilingTraceData.buildId = "buildId" + profilingTraceData.timestamp = now profilingTraceData.transactions = listOf( ProfilingTransactionData(NoOpTransaction.getInstance(), 1, 2), ProfilingTransactionData(NoOpTransaction.getInstance(), 2, 3) @@ -559,6 +561,7 @@ class JsonSerializerTest { assertEquals("2000000", element["device_physical_memory_bytes"] as String) assertEquals("android", element["platform"] as String) assertEquals("buildId", element["build_id"] as String) + assertEquals(DateUtils.getTimestamp(now), element["timestamp"] as String) assertEquals( listOf( mapOf( @@ -655,6 +658,7 @@ class JsonSerializerTest { "device_physical_memory_bytes":"2000000", "platform":"android", "build_id":"buildId", + "timestamp":"2024-05-24T12:52:03.561Z", "transactions":[ { "id":"id", @@ -729,6 +733,7 @@ class JsonSerializerTest { assertEquals("2000000", profilingTraceData.devicePhysicalMemoryBytes) assertEquals("android", profilingTraceData.platform) assertEquals("buildId", profilingTraceData.buildId) + assertEquals(DateUtils.getDateTime("2024-05-24T12:52:03.561Z"), profilingTraceData.timestamp) val expectedTransactions = listOf( ProfilingTransactionData().apply { id = "id" diff --git a/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt b/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt index 1b2b2f36e1..01948114de 100644 --- a/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt +++ b/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt @@ -312,6 +312,19 @@ class MetricsAggregatorTest { // there is no other metric to capture, so flush is not scheduled again assertFalse(fixture.executorService.hasScheduledRunnables()) + + // once another metric is emitted + aggregator.increment( + "name1", + 1.0, + MeasurementUnit.Custom("unit0"), + mapOf("key0" to "value0"), + 20_001, + null + ) + + // then flush should be scheduled again + assertTrue(fixture.executorService.hasScheduledRunnables()) } @Test diff --git a/sentry/src/test/java/io/sentry/SentryTracerTest.kt b/sentry/src/test/java/io/sentry/SentryTracerTest.kt index c883937383..0942768a8a 100644 --- a/sentry/src/test/java/io/sentry/SentryTracerTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTracerTest.kt @@ -1378,4 +1378,15 @@ class SentryTracerTest { assertEquals(5, tracer.children.size) } + + @Test + fun `tracer is not finished when finishCallback is called`() { + val transaction = fixture.getSut(transactionFinishedCallback = { + assertFalse(it.isFinished) + assertNotNull(it.finishDate) + }) + assertFalse(transaction.isFinished) + assertNull(transaction.finishDate) + transaction.finish() + } } diff --git a/sentry/src/test/java/io/sentry/ShutdownHookIntegrationTest.kt b/sentry/src/test/java/io/sentry/ShutdownHookIntegrationTest.kt index d6ce0ff043..8218740b89 100644 --- a/sentry/src/test/java/io/sentry/ShutdownHookIntegrationTest.kt +++ b/sentry/src/test/java/io/sentry/ShutdownHookIntegrationTest.kt @@ -92,6 +92,16 @@ class ShutdownHookIntegrationTest { verify(fixture.runtime).removeShutdownHook(any()) } + @Test + fun `shutdown in progress is handled gracefully for registration`() { + val integration = fixture.getSut() + whenever(fixture.runtime.addShutdownHook(any())).thenThrow(java.lang.IllegalStateException("VM already shutting down")) + + integration.register(fixture.hub, fixture.options) + + verify(fixture.runtime).addShutdownHook(any()) + } + @Test fun `non shutdown in progress during removeShutdownHook is rethrown`() { val integration = fixture.getSut() diff --git a/sentry/src/test/java/io/sentry/SpanTest.kt b/sentry/src/test/java/io/sentry/SpanTest.kt index ae9d9bd07f..09bf01c791 100644 --- a/sentry/src/test/java/io/sentry/SpanTest.kt +++ b/sentry/src/test/java/io/sentry/SpanTest.kt @@ -495,6 +495,19 @@ class SpanTest { assertSame(span.localMetricsAggregator, span.localMetricsAggregator) } + // test to ensure that the span is not finished when the finishCallback is called + @Test + fun `span is not finished when finishCallback is called`() { + val span = fixture.getSut() + span.setSpanFinishedCallback { + assertFalse(span.isFinished) + assertNotNull(span.finishDate) + } + assertFalse(span.isFinished) + assertNull(span.finishDate) + span.finish() + } + private fun getTransaction(transactionContext: TransactionContext = TransactionContext("name", "op")): SentryTracer { return SentryTracer(transactionContext, fixture.hub) } diff --git a/sentry/src/test/java/io/sentry/UncaughtExceptionHandlerIntegrationTest.kt b/sentry/src/test/java/io/sentry/UncaughtExceptionHandlerIntegrationTest.kt index 01353d5ac0..aaa8cbe3fc 100644 --- a/sentry/src/test/java/io/sentry/UncaughtExceptionHandlerIntegrationTest.kt +++ b/sentry/src/test/java/io/sentry/UncaughtExceptionHandlerIntegrationTest.kt @@ -7,6 +7,7 @@ import io.sentry.hints.EventDropReason.MULTITHREADED_DEDUPLICATION import io.sentry.protocol.SentryId import io.sentry.util.HintUtils import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argThat import org.mockito.kotlin.argWhere import org.mockito.kotlin.argumentCaptor @@ -19,6 +20,7 @@ import java.io.PrintStream import java.nio.file.Files import kotlin.concurrent.thread import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -291,4 +293,54 @@ class UncaughtExceptionHandlerIntegrationTest { } ) } + + @Test + fun `multiple registrations do not cause the build-up of a tree of UncaughtExceptionHandlerIntegrations`() { + var currentDefaultHandler: Thread.UncaughtExceptionHandler? = null + + val handler = mock() + whenever(handler.defaultUncaughtExceptionHandler).thenAnswer { currentDefaultHandler } + + whenever(handler.setDefaultUncaughtExceptionHandler(anyOrNull())).then { + currentDefaultHandler = it.getArgument(0) + null + } + + val integration1 = UncaughtExceptionHandlerIntegration(handler) + integration1.register(fixture.hub, fixture.options) + + val integration2 = UncaughtExceptionHandlerIntegration(handler) + integration2.register(fixture.hub, fixture.options) + + assertEquals(currentDefaultHandler, integration2) + integration2.close() + + assertEquals(null, currentDefaultHandler) + } + + @Test + fun `multiple registrations do not cause the build-up of a tree of UncaughtExceptionHandlerIntegrations, reset to inital`() { + val initialUncaughtExceptionHandler = Thread.UncaughtExceptionHandler { _, _ -> } + + var currentDefaultHandler: Thread.UncaughtExceptionHandler? = initialUncaughtExceptionHandler + + val handler = mock() + whenever(handler.defaultUncaughtExceptionHandler).thenAnswer { currentDefaultHandler } + + whenever(handler.setDefaultUncaughtExceptionHandler(anyOrNull())).then { + currentDefaultHandler = it.getArgument(0) + null + } + + val integration1 = UncaughtExceptionHandlerIntegration(handler) + integration1.register(fixture.hub, fixture.options) + + val integration2 = UncaughtExceptionHandlerIntegration(handler) + integration2.register(fixture.hub, fixture.options) + + assertEquals(currentDefaultHandler, integration2) + integration2.close() + + assertEquals(initialUncaughtExceptionHandler, currentDefaultHandler) + } }