diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 49091f3bbd..4220bca310 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,10 +35,11 @@ jobs: run: make preMerge - name: Upload coverage to Codecov - uses: codecov/codecov-action@e0b68c6749509c5f83f984dd99a76a1c1a231044 # pin@v3 + uses: codecov/codecov-action@5ecb98a3c6b747ed38dc09f787459979aebb39be # pin@v4 with: name: sentry-java fail_ci_if_error: false + token: ${{ secrets.CODECOV_TOKEN }} - name: Upload test results if: always() diff --git a/.github/workflows/update-deps.yml b/.github/workflows/update-deps.yml index 24fce64050..83d90bb919 100644 --- a/.github/workflows/update-deps.yml +++ b/.github/workflows/update-deps.yml @@ -13,7 +13,7 @@ jobs: native: uses: getsentry/github-workflows/.github/workflows/updater.yml@v2 with: - path: sentry-android-ndk/sentry-native + path: scripts/update-sentry-native-ndk.sh name: Native SDK secrets: # If a custom token is used instead, a CI would be triggered on a created PR. diff --git a/.gitmodules b/.gitmodules index fe6c3b7cc0..e69de29bb2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "sentry-android-ndk/sentry-native"] - path = sentry-android-ndk/sentry-native - url = https://github.com/getsentry/sentry-native diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b578ed17f..57f12ff897 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,121 @@ ## Unreleased +### Behavioural Changes + +- (Android) The JNI layer for sentry-native has now been moved from sentry-java to sentry-native ([#3189](https://github.com/getsentry/sentry-java/pull/3189)) + - This now includes prefab support for sentry-native, allowing you to link and access the sentry-native API within your native app code + - Checkout the `sentry-samples/sentry-samples-android` example on how to configure CMake and consume `sentry.h` + +### Features + +- Publish Gradle module metadata ([#3422](https://github.com/getsentry/sentry-java/pull/3422)) +- Add data fetching environment hint to breadcrumb for GraphQL (#3413) ([#3431](https://github.com/getsentry/sentry-java/pull/3431)) + +### Fixes + +- Fix faulty `span.frame_delay` calculation for early app start spans ([#3427](https://github.com/getsentry/sentry-java/pull/3427)) + +### Dependencies + +- Bump Native SDK from v0.7.0 to v0.7.5 ([#3441](https://github.com/getsentry/sentry-java/pull/3189)) + - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#075) + - [diff](https://github.com/getsentry/sentry-native/compare/0.7.0...0.7.5) + +## 8.0.0-alpha.1 + +Version 8 of the Sentry Android/Java SDK brings a variety of features and fixes. The most notable changes are: + +- New `Scope` types have been introduced, see "Behavioural Changes" for more details. +- Lifecycle tokens have been introduced to manage `Scope` lifecycle, see "Behavioural Changes" for more details. +- `Hub` has been replaced by `Scopes` + +### Behavioural Changes + +- We're introducing some new `Scope` types in the SDK, allowing for better control over what data is attached where. Previously there was a stack of scopes that was pushed and popped. Instead we now fork scopes for a given lifecycle and then restore the previous scopes. Since `Hub` is gone, it is also never cloned anymore. Separation of data now happens through the different scope types while making it easier to manipulate exactly what you need without having to attach data at the right time to have it apply where wanted. + - Global scope is attached to all events created by the SDK. It can also be modified before `Sentry.init` has been called. It can be manipulated using `Sentry.configureScope(ScopeType.GLOBAL, (scope) -> { ... })`. + - Isolation scope can be used e.g. to attach data to all events that come up while handling an incoming request. It can also be used for other isolation purposes. It can be manipulated using `Sentry.configureScope(ScopeType.ISOLATION, (scope) -> { ... })`. The SDK automatically forks isolation scope in certain cases like incoming requests, CRON jobs, Spring `@Async` and more. + - Current scope is forked often and data added to it is only added to events that are created while this scope is active. Data is also passed on to newly forked child scopes but not to parents. +- `Sentry.popScope` has been deprecated, please call `.close()` on the token returned by `Sentry.pushScope` instead or use it in a way described in more detail in "Migration Guide". +- We have chosen a default scope that is used for `Sentry.configureScope()` as well as API like `Sentry.setTag()` + - For Android the type defaults to `CURRENT` scope + - For Backend and other JVM applicatons it defaults to `ISOLATION` scope +- Event processors on `Scope` can now be ordered by overriding the `getOrder` method on implementations of `EventProcessor`. NOTE: This order only applies to event processors on `Scope` but not `SentryOptions` at the moment. Feel free to request this if you need it. +- `Hub` is deprecated in favor of `Scopes`, alongside some `Hub` relevant APIs. More details can be found in the "Migration Guide" section. + +### Breaking Changes + +- `Contexts` no longer extends `ConcurrentHashMap`, instead we offer a selected set of methods. + +### Migration Guide / Deprecations + +- `Hub` has been deprecated, we're replacing the following: + - `IHub` has been replaced by `IScopes`, however you should be able to simply pass `IHub` instances to code expecting `IScopes`, allowing for an easier migration. + - `HubAdapter.getInstance()` has been replaced by `ScopesAdapter.getInstance()` + - The `.clone()` method on `IHub`/`IScopes` has been deprecated, please use `.pushScope()` or `.pushIsolationScope()` instead + - Some internal methods like `.getCurrentHub()` and `.setCurrentHub()` have also been replaced. +- `Sentry.popScope` has been replaced by calling `.close()` on the token returned by `Sentry.pushScope()` and `Sentry.pushIsolationScope()`. The token can also be used in a `try` block like this: + +``` +try (final @NotNull ISentryLifecycleToken ignored = Sentry.pushScope()) { + // this block has its separate current scope +} +``` + +as well as: + + +``` +try (final @NotNull ISentryLifecycleToken ignored = Sentry.pushIsolationScope()) { + // this block has its separate isolation scope +} +``` + +You may also use `LifecycleHelper.close(token)`, e.g. in case you need to pass the token around for closing later. + +### Features + +- Report exceptions returned by Throwable.getSuppressed() to Sentry as exception groups ([#3396] https://github.com/getsentry/sentry-java/pull/3396) + +## 7.9.0 + +### Features + +- Add start_type to app context ([#3379](https://github.com/getsentry/sentry-java/pull/3379)) +- Add ttid/ttfd contribution flags ([#3386](https://github.com/getsentry/sentry-java/pull/3386)) + +### Fixes + +- (Internal) Metrics code cleanup ([#3403](https://github.com/getsentry/sentry-java/pull/3403)) +- Fix Frame measurements in app start transactions ([#3382](https://github.com/getsentry/sentry-java/pull/3382)) +- Fix timing metric value different from span duration ([#3368](https://github.com/getsentry/sentry-java/pull/3368)) +- Do not always write startup crash marker ([#3409](https://github.com/getsentry/sentry-java/pull/3409)) + - This may have been causing the SDK init logic to block the main thread + +## 7.8.0 + +### Features + +- Add description to OkHttp spans ([#3320](https://github.com/getsentry/sentry-java/pull/3320)) +- Enable backpressure management by default ([#3284](https://github.com/getsentry/sentry-java/pull/3284)) + +### Fixes + +- Add rate limit to Metrics ([#3334](https://github.com/getsentry/sentry-java/pull/3334)) +- Fix java.lang.ClassNotFoundException: org.springframework.web.servlet.HandlerMapping in Spring Boot Servlet mode without WebMVC ([#3336](https://github.com/getsentry/sentry-java/pull/3336)) +- Fix normalization of metrics keys, tags and values ([#3332](https://github.com/getsentry/sentry-java/pull/3332)) + +## 7.7.0 + ### Features - Add support for Spring Rest Client ([#3199](https://github.com/getsentry/sentry-java/pull/3199)) +- Extend Proxy options with proxy type ([#3326](https://github.com/getsentry/sentry-java/pull/3326)) + +### Fixes + +- Fixed default deadline timeout to 30s instead of 300s ([#3322](https://github.com/getsentry/sentry-java/pull/3322)) +- Fixed `Fix java.lang.ClassNotFoundException: org.springframework.web.servlet.HandlerExceptionResolver` in Spring Boot Servlet mode without WebMVC ([#3333](https://github.com/getsentry/sentry-java/pull/3333)) ## 7.6.0 diff --git a/LICENSE b/LICENSE index 6b8b8d58af..f49694a15b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Sentry +Copyright (c) 2019-2024 Sentry Copyright (c) 2015 Salomon BRYS for Android ANRWatchDog Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/build.gradle.kts b/build.gradle.kts index acb1fba051..03b5723da0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,10 +32,6 @@ buildscript { classpath(Config.QualityPlugins.errorpronePlugin) classpath(Config.QualityPlugins.gradleVersionsPlugin) - // add classpath of androidNativeBundle - // com.ydq.android.gradle.build.tool:nativeBundle:{version}} - classpath(Config.NativePlugins.nativeBundlePlugin) - // add classpath of sentry android gradle plugin // classpath("io.sentry:sentry-android-gradle-plugin:{version}") @@ -78,6 +74,7 @@ allprojects { repositories { google() mavenCentral() + mavenLocal() } group = Config.Sentry.group version = properties[Config.Sentry.versionNameProp].toString() @@ -160,16 +157,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/Config.kt b/buildSrc/src/main/java/Config.kt index edc7ff3934..a3ccdc3af5 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -256,9 +256,4 @@ object Config { val errorprone = "com.google.errorprone:error_prone_core:2.11.0" val errorProneNullAway = "com.uber.nullaway:nullaway:0.9.5" } - - object NativePlugins { - val nativeBundlePlugin = "io.github.howardpang:androidNativeBundle:1.1.1" - val nativeBundleExport = "com.ydq.android.gradle.native-aar.export" - } } 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 7c0234ebb6..a4760b08dd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ android.useAndroidX=true android.defaults.buildfeatures.buildconfig=true # Release information -versionName=7.6.0 +versionName=8.0.0-alpha.1 # Override the SDK name on native crashes on Android sentryAndroidSdkName=sentry.native.android diff --git a/scripts/update-sentry-native-ndk.sh b/scripts/update-sentry-native-ndk.sh new file mode 100755 index 0000000000..544dc403ac --- /dev/null +++ b/scripts/update-sentry-native-ndk.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd $(dirname "$0")/../ +GRADLE_NDK_FILEPATH=sentry-android-ndk/build.gradle.kts +GRADLE_SAMPLE_FILEPATH=sentry-samples/sentry-samples-android/build.gradle.kts + +case $1 in +get-version) + version=$(perl -ne 'print "$1\n" if ( m/io\.sentry:sentry-native-ndk:([0-9.]+)+/ )' $GRADLE_NDK_FILEPATH) + + echo "v$version" + ;; +get-repo) + echo "https://github.com/getsentry/sentry-native.git" + ;; +set-version) + version=$2 + + # Remove leading "v" + if [[ "$version" == v* ]]; then + version="${version:1}" + fi + + echo "Setting sentry-native-ndk version to '$version'" + + PATTERN="io\.sentry:sentry-native-ndk:([0-9.]+)+" + perl -pi -e "s/$PATTERN/io.sentry:sentry-native-ndk:$version/g" $GRADLE_NDK_FILEPATH + perl -pi -e "s/$PATTERN/io.sentry:sentry-native-ndk:$version/g" $GRADLE_SAMPLE_FILEPATH + ;; +*) + echo "Unknown argument $1" + exit 1 + ;; +esac 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/PerformanceAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java index 6a1a7c67fa..afefed676f 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java @@ -16,6 +16,7 @@ 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; import io.sentry.protocol.MeasurementValue; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentrySpan; @@ -79,29 +80,44 @@ public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) { // the app start measurement is only sent once and only if the transaction has // the app.start span, which is automatically created by the SDK. - if (!sentStartMeasurement && hasAppStartSpan(transaction)) { - final @NotNull TimeSpan appStartTimeSpan = - AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(options); - final long appStartUpDurationMs = appStartTimeSpan.getDurationMs(); - - // if appStartUpDurationMs is 0, metrics are not ready to be sent - if (appStartUpDurationMs != 0) { - final MeasurementValue value = - new MeasurementValue( - (float) appStartUpDurationMs, MeasurementUnit.Duration.MILLISECOND.apiName()); - - final String appStartKey = - AppStartMetrics.getInstance().getAppStartType() == AppStartMetrics.AppStartType.COLD - ? MeasurementValue.KEY_APP_START_COLD - : MeasurementValue.KEY_APP_START_WARM; - - transaction.getMeasurements().put(appStartKey, value); - - attachColdAppStartSpans(AppStartMetrics.getInstance(), transaction); - sentStartMeasurement = true; + if (hasAppStartSpan(transaction)) { + if (!sentStartMeasurement) { + final @NotNull TimeSpan appStartTimeSpan = + AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(options); + final long appStartUpDurationMs = appStartTimeSpan.getDurationMs(); + + // if appStartUpDurationMs is 0, metrics are not ready to be sent + if (appStartUpDurationMs != 0) { + final MeasurementValue value = + new MeasurementValue( + (float) appStartUpDurationMs, MeasurementUnit.Duration.MILLISECOND.apiName()); + + final String appStartKey = + AppStartMetrics.getInstance().getAppStartType() == AppStartMetrics.AppStartType.COLD + ? MeasurementValue.KEY_APP_START_COLD + : MeasurementValue.KEY_APP_START_WARM; + + transaction.getMeasurements().put(appStartKey, value); + + attachColdAppStartSpans(AppStartMetrics.getInstance(), transaction); + sentStartMeasurement = true; + } + } + + @Nullable App appContext = transaction.getContexts().getApp(); + if (appContext == null) { + appContext = new App(); + transaction.getContexts().setApp(appContext); } + final String appStartType = + AppStartMetrics.getInstance().getAppStartType() == AppStartMetrics.AppStartType.COLD + ? "cold" + : "warm"; + appContext.setStartType(appStartType); } + setContributingFlags(transaction); + final SentryId eventId = transaction.getEventId(); final SpanContext spanContext = transaction.getContexts().getTrace(); @@ -121,6 +137,71 @@ public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) { return transaction; } + private void setContributingFlags(SentryTransaction transaction) { + + @Nullable SentrySpan ttidSpan = null; + @Nullable SentrySpan ttfdSpan = null; + for (final @NotNull SentrySpan span : transaction.getSpans()) { + if (ActivityLifecycleIntegration.TTID_OP.equals(span.getOp())) { + ttidSpan = span; + } else if (ActivityLifecycleIntegration.TTFD_OP.equals(span.getOp())) { + ttfdSpan = span; + } + // once both are found we can early exit + if (ttidSpan != null && ttfdSpan != null) { + break; + } + } + + if (ttidSpan == null && ttfdSpan == null) { + return; + } + + for (final @NotNull SentrySpan span : transaction.getSpans()) { + // as ttid and ttfd spans are artificially created, we don't want to set the flags on them + if (span == ttidSpan || span == ttfdSpan) { + continue; + } + + // let's assume main thread, unless it's set differently + boolean spanOnMainThread = true; + final @Nullable Map spanData = span.getData(); + if (spanData != null) { + final @Nullable Object threadName = spanData.get(SpanDataConvention.THREAD_NAME); + spanOnMainThread = threadName == null || "main".equals(threadName); + } + + // for ttid, only main thread spans are relevant + final boolean withinTtid = + (ttidSpan != null) + && isTimestampWithinSpan(span.getStartTimestamp(), ttidSpan) + && spanOnMainThread; + + final boolean withinTtfd = + (ttfdSpan != null) && isTimestampWithinSpan(span.getStartTimestamp(), ttfdSpan); + + if (withinTtid || withinTtfd) { + @Nullable Map data = span.getData(); + if (data == null) { + data = new ConcurrentHashMap<>(); + span.setData(data); + } + if (withinTtid) { + data.put(SpanDataConvention.CONTRIBUTES_TTID, true); + } + if (withinTtfd) { + data.put(SpanDataConvention.CONTRIBUTES_TTFD, true); + } + } + } + } + + private static boolean isTimestampWithinSpan( + final double timestamp, final @NotNull SentrySpan target) { + return timestamp >= target.getStartTimestamp() + && (target.getTimestamp() == null || timestamp <= target.getTimestamp()); + } + private boolean hasAppStartSpan(final @NotNull SentryTransaction txn) { final @NotNull List spans = txn.getSpans(); for (final @NotNull SentrySpan span : spans) { @@ -239,6 +320,9 @@ private static SentrySpan timeSpanToSentrySpan( defaultSpanData.put(SpanDataConvention.THREAD_ID, Looper.getMainLooper().getThread().getId()); defaultSpanData.put(SpanDataConvention.THREAD_NAME, "main"); + defaultSpanData.put(SpanDataConvention.CONTRIBUTES_TTID, true); + defaultSpanData.put(SpanDataConvention.CONTRIBUTES_TTFD, true); + return new SentrySpan( span.getStartTimestampSecs(), span.getProjectedStopTimestampSecs(), 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 b5f0332c67..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 @@ -1,5 +1,6 @@ package io.sentry.android.core; +import io.sentry.DateUtils; import io.sentry.IPerformanceContinuousCollector; import io.sentry.ISpan; import io.sentry.ITransaction; @@ -30,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(); @@ -122,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(); } } } @@ -135,20 +136,20 @@ private void captureFrameMetrics(@NotNull final ISpan span) { return; } - // ignore spans with no finish date 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()) { @@ -194,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; @@ -300,10 +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) { - return date.diff(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(EMPTY_NANO_TIME); + } + + // 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/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java b/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java index 28090ef6c7..fb5e81cfa9 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java @@ -57,7 +57,7 @@ public void store(@NotNull SentryEnvelope envelope, @NotNull Hint hint) { if (HintUtils.hasType(hint, UncaughtExceptionHandlerIntegration.UncaughtExceptionHint.class) && sdkInitTimeSpan.hasStarted()) { long timeSinceSdkInit = - currentDateProvider.getCurrentTimeMillis() - sdkInitTimeSpan.getStartTimestampMs(); + currentDateProvider.getCurrentTimeMillis() - sdkInitTimeSpan.getStartUptimeMs(); if (timeSinceSdkInit <= options.getStartupCrashDurationThresholdMillis()) { options .getLogger() diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java index 5397790588..dac78920f8 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java @@ -4,6 +4,7 @@ import io.sentry.DateUtils; import io.sentry.SentryDate; import io.sentry.SentryLongDate; +import java.util.concurrent.TimeUnit; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -21,6 +22,7 @@ public class TimeSpan implements Comparable { private @Nullable String description; + private long startSystemNanos; private long startUnixTimeMs; private long startUptimeMs; private long stopUptimeMs; @@ -29,6 +31,7 @@ public class TimeSpan implements Comparable { public void start() { startUptimeMs = SystemClock.uptimeMillis(); startUnixTimeMs = System.currentTimeMillis(); + startSystemNanos = System.nanoTime(); } /** @@ -40,6 +43,7 @@ public void setStartedAt(final long uptimeMs) { final long shiftMs = SystemClock.uptimeMillis() - startUptimeMs; startUnixTimeMs = System.currentTimeMillis() - shiftMs; + startSystemNanos = System.nanoTime() - TimeUnit.MILLISECONDS.toNanos(shiftMs); } /** Stops the time span */ @@ -162,6 +166,7 @@ public void reset() { startUptimeMs = 0; stopUptimeMs = 0; startUnixTimeMs = 0; + startSystemNanos = 0; } @Override diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt index e1593e600f..7577f1cd1e 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt @@ -7,6 +7,7 @@ import io.sentry.IScopes import io.sentry.MeasurementUnit import io.sentry.SentryTracer import io.sentry.SpanContext +import io.sentry.SpanDataConvention import io.sentry.SpanId import io.sentry.SpanStatus import io.sentry.TracesSamplingDecision @@ -27,6 +28,7 @@ import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertNull import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) @@ -464,6 +466,336 @@ class PerformanceAndroidEventProcessorTest { } } + @Test + fun `does not set start_type field for txns without app start span`() { + // given some ui.load txn + setAppStart(fixture.options, coldStart = true) + + val sut = fixture.getSut(enablePerformanceV2 = true) + val context = TransactionContext("Activity", UI_LOAD_OP) + val tracer = SentryTracer(context, fixture.scopes) + var tr = SentryTransaction(tracer) + + // when it contains no app start span and is processed + tr = sut.process(tr, Hint()) + + // start_type should not be set + assertNull(tr.contexts.app?.startType) + } + + @Test + fun `sets start_type field for app context`() { + // given some cold app start + setAppStart(fixture.options, coldStart = true) + + val sut = fixture.getSut(enablePerformanceV2 = true) + val context = TransactionContext("Activity", UI_LOAD_OP) + val tracer = SentryTracer(context, fixture.scopes) + var tr = SentryTransaction(tracer) + + val appStartSpan = SentrySpan( + 0.0, + 1.0, + tr.contexts.trace!!.traceId, + SpanId(), + null, + APP_START_COLD, + "App Start", + SpanStatus.OK, + null, + emptyMap(), + emptyMap(), + null, + null + ) + tr.spans.add(appStartSpan) + + // when the processor attaches the app start spans + tr = sut.process(tr, Hint()) + + // start_type should be set as well + assertEquals( + "cold", + tr.contexts.app!!.startType + ) + } + + @Test + fun `adds ttid and ttfd contributing span data`() { + val sut = fixture.getSut() + + val context = TransactionContext("Activity", UI_LOAD_OP) + val tracer = SentryTracer(context, fixture.scopes) + val tr = SentryTransaction(tracer) + + // given a ttid from 0.0 -> 1.0 + // and a ttfd from 0.0 -> 2.0 + val ttid = SentrySpan( + 0.0, + 1.0, + tr.contexts.trace!!.traceId, + SpanId(), + null, + ActivityLifecycleIntegration.TTID_OP, + "App Start", + SpanStatus.OK, + null, + emptyMap(), + emptyMap(), + null, + null + ) + + val ttfd = SentrySpan( + 0.0, + 2.0, + tr.contexts.trace!!.traceId, + SpanId(), + null, + ActivityLifecycleIntegration.TTFD_OP, + "App Start", + SpanStatus.OK, + null, + emptyMap(), + emptyMap(), + null, + null + ) + tr.spans.add(ttid) + tr.spans.add(ttfd) + + // and 3 spans + // one from 0.0 -> 0.5 + val ttidContrib = SentrySpan( + 0.0, + 0.5, + tr.contexts.trace!!.traceId, + SpanId(), + null, + "example.op", + "", + SpanStatus.OK, + null, + emptyMap(), + emptyMap(), + null, + null + ) + + // and another from 1.5 -> 3.5 + val ttfdContrib = SentrySpan( + 1.5, + 3.5, + tr.contexts.trace!!.traceId, + SpanId(), + null, + "example.op", + "", + SpanStatus.OK, + null, + emptyMap(), + emptyMap(), + null, + null + ) + + // and another from 2.1 -> 2.2 + val outsideSpan = SentrySpan( + 2.1, + 2.2, + tr.contexts.trace!!.traceId, + SpanId(), + null, + "example.op", + "", + SpanStatus.OK, + null, + emptyMap(), + emptyMap(), + null, + mutableMapOf( + "tag" to "value" + ) + ) + + tr.spans.add(ttidContrib) + tr.spans.add(ttfdContrib) + + // when the processor processes the txn + sut.process(tr, Hint()) + + // then the ttid/ttfd spans themselves should have no flags set + assertNull(ttid.data?.get(SpanDataConvention.CONTRIBUTES_TTID)) + assertNull(ttid.data?.get(SpanDataConvention.CONTRIBUTES_TTFD)) + + assertNull(ttfd.data?.get(SpanDataConvention.CONTRIBUTES_TTID)) + assertNull(ttfd.data?.get(SpanDataConvention.CONTRIBUTES_TTFD)) + + // then the first span should have ttid and ttfd contributing flags + assertTrue(ttidContrib.data?.get(SpanDataConvention.CONTRIBUTES_TTID) == true) + assertTrue(ttidContrib.data?.get(SpanDataConvention.CONTRIBUTES_TTFD) == true) + + // and the second one should contribute to ttfd only + assertNull(ttfdContrib.data?.get(SpanDataConvention.CONTRIBUTES_TTID)) + assertTrue(ttfdContrib.data?.get(SpanDataConvention.CONTRIBUTES_TTFD) == true) + + // and the third span should have no flags attached, as it's outside ttid/ttfd + assertNull(outsideSpan.data?.get(SpanDataConvention.CONTRIBUTES_TTID)) + assertNull(outsideSpan.data?.get(SpanDataConvention.CONTRIBUTES_TTFD)) + } + + @Test + fun `adds no ttid and ttfd contributing span data if txn contains no ttid or ttfd`() { + val sut = fixture.getSut() + + val context = TransactionContext("Activity", UI_LOAD_OP) + val tracer = SentryTracer(context, fixture.scopes) + val tr = SentryTransaction(tracer) + + val span = SentrySpan( + 0.0, + 1.0, + tr.contexts.trace!!.traceId, + SpanId(), + null, + "example.op", + "", + SpanStatus.OK, + null, + emptyMap(), + emptyMap(), + null, + null + ) + + tr.spans.add(span) + + // when the processor processes the txn + sut.process(tr, Hint()) + + // the span should have no flags attached + assertNull(span.data?.get(SpanDataConvention.CONTRIBUTES_TTID)) + assertNull(span.data?.get(SpanDataConvention.CONTRIBUTES_TTFD)) + } + + @Test + fun `sets ttid and ttfd contributing flags according to span threads`() { + val sut = fixture.getSut() + + val context = TransactionContext("Activity", UI_LOAD_OP) + val tracer = SentryTracer(context, fixture.scopes) + val tr = SentryTransaction(tracer) + + // given a ttid from 0.0 -> 1.0 + // and a ttfd from 0.0 -> 1.0 + val ttid = SentrySpan( + 0.0, + 1.0, + tr.contexts.trace!!.traceId, + SpanId(), + null, + ActivityLifecycleIntegration.TTID_OP, + "App Start", + SpanStatus.OK, + null, + emptyMap(), + emptyMap(), + null, + null + ) + + val ttfd = SentrySpan( + 0.0, + 1.0, + tr.contexts.trace!!.traceId, + SpanId(), + null, + ActivityLifecycleIntegration.TTFD_OP, + "App Start", + SpanStatus.OK, + null, + emptyMap(), + emptyMap(), + null, + null + ) + tr.spans.add(ttid) + tr.spans.add(ttfd) + + // one span with no thread info + val noThreadSpan = SentrySpan( + 0.0, + 0.5, + tr.contexts.trace!!.traceId, + SpanId(), + null, + "example.op", + "", + SpanStatus.OK, + null, + emptyMap(), + emptyMap(), + null, + null + ) + + // one span on the main thread + val mainThreadSpan = SentrySpan( + 0.0, + 0.5, + tr.contexts.trace!!.traceId, + SpanId(), + null, + "example.op", + "", + SpanStatus.OK, + null, + emptyMap(), + emptyMap(), + null, + mutableMapOf( + "thread.name" to "main" + ) + ) + + // and another one off the main thread + val backgroundThreadSpan = SentrySpan( + 0.0, + 0.5, + tr.contexts.trace!!.traceId, + SpanId(), + null, + "example.op", + "", + SpanStatus.OK, + null, + emptyMap(), + emptyMap(), + null, + mutableMapOf( + "thread.name" to "background" + ) + ) + + tr.spans.add(noThreadSpan) + tr.spans.add(mainThreadSpan) + tr.spans.add(backgroundThreadSpan) + + // when the processor processes the txn + sut.process(tr, Hint()) + + // then the span with no thread info + main thread span should contribute to ttid and ttfd + assertTrue(noThreadSpan.data?.get(SpanDataConvention.CONTRIBUTES_TTID) == true) + assertTrue(noThreadSpan.data?.get(SpanDataConvention.CONTRIBUTES_TTFD) == true) + + assertTrue(mainThreadSpan.data?.get(SpanDataConvention.CONTRIBUTES_TTID) == true) + assertTrue(mainThreadSpan.data?.get(SpanDataConvention.CONTRIBUTES_TTFD) == true) + + // and the background thread span only contributes to ttfd + assertNull(backgroundThreadSpan.data?.get(SpanDataConvention.CONTRIBUTES_TTID)) + assertTrue(backgroundThreadSpan.data?.get(SpanDataConvention.CONTRIBUTES_TTFD) == true) + } + private fun setAppStart(options: SentryAndroidOptions, coldStart: Boolean = true) { AppStartMetrics.getInstance().apply { appStartType = when (coldStart) { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SpanFrameMetricsCollectorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SpanFrameMetricsCollectorTest.kt index 0d34a8fb07..d8ff8fde2e 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SpanFrameMetricsCollectorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SpanFrameMetricsCollectorTest.kt @@ -4,7 +4,6 @@ import io.sentry.ISpan import io.sentry.ITransaction import io.sentry.NoOpSpan import io.sentry.NoOpTransaction -import io.sentry.SentryLongDate import io.sentry.SentryNanotimeDate import io.sentry.SpanContext import io.sentry.android.core.internal.util.SentryFrameMetricsCollector @@ -40,9 +39,7 @@ class SpanFrameMetricsCollectorTest { options.frameMetricsCollector = frameMetricsCollector options.isEnableFramesTracking = enabled options.isEnablePerformanceV2 = enabled - options.setDateProvider { - SentryLongDate(timeNanos) - } + options.dateProvider = SentryAndroidDateProvider() return SpanFrameMetricsCollector(options, frameMetricsCollector) } @@ -55,10 +52,16 @@ class SpanFrameMetricsCollectorTest { val span = mock() val spanContext = SpanContext("op.fake") whenever(span.spanContext).thenReturn(spanContext) - whenever(span.startDate).thenReturn(SentryLongDate(startTimeStampNanos)) + whenever(span.startDate).thenReturn( + SentryNanotimeDate( + Date(), + startTimeStampNanos + ) + ) whenever(span.finishDate).thenReturn( if (endTimeStampNanos != null) { - SentryLongDate( + SentryNanotimeDate( + Date(), endTimeStampNanos ) } else { @@ -75,10 +78,16 @@ class SpanFrameMetricsCollectorTest { val span = mock() val spanContext = SpanContext("op.fake") whenever(span.spanContext).thenReturn(spanContext) - whenever(span.startDate).thenReturn(SentryLongDate(startTimeStampNanos)) + whenever(span.startDate).thenReturn( + SentryNanotimeDate( + Date(), + startTimeStampNanos + ) + ) whenever(span.finishDate).thenReturn( if (endTimeStampNanos != null) { - SentryLongDate( + SentryNanotimeDate( + Date(), endTimeStampNanos ) } else { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt index 92b5d773fd..ba70e5a879 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt @@ -51,10 +51,8 @@ class AndroidEnvelopeCacheTest { AppStartMetrics.getInstance().apply { if (options.isEnablePerformanceV2) { appStartTimeSpan.setStartedAt(appStartMillis) - appStartTimeSpan.setStartUnixTimeMs(appStartMillis) } else { sdkInitTimeSpan.setStartedAt(appStartMillis) - sdkInitTimeSpan.setStartUnixTimeMs(appStartMillis) } } } 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/AutomaticSpansTest.kt b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/AutomaticSpansTest.kt index 7f2d771b7d..d5dd0fd80e 100644 --- a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/AutomaticSpansTest.kt +++ b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/AutomaticSpansTest.kt @@ -2,13 +2,23 @@ package io.sentry.uitest.android import androidx.lifecycle.Lifecycle import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.matcher.ViewMatchers import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.Sentry import io.sentry.SentryLevel +import io.sentry.android.core.AndroidLogger import io.sentry.android.core.SentryAndroidOptions +import io.sentry.assertEnvelopeTransaction +import io.sentry.protocol.MeasurementValue import io.sentry.protocol.SentryTransaction +import org.junit.Assume import org.junit.runner.RunWith import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) @@ -49,4 +59,84 @@ class AutomaticSpansTest : BaseUiTest() { } } } + + @Test + fun checkAppStartFramesMeasurements() { + initSentry(true) { options: SentryAndroidOptions -> + options.tracesSampleRate = 1.0 + options.isEnableTimeToFullDisplayTracing = true + options.isEnablePerformanceV2 = false + } + + IdlingRegistry.getInstance().register(ProfilingSampleActivity.scrollingIdlingResource) + val sampleScenario = launchActivity() + swipeList(3) + Sentry.reportFullyDisplayed() + sampleScenario.moveToState(Lifecycle.State.DESTROYED) + IdlingRegistry.getInstance().unregister(ProfilingSampleActivity.scrollingIdlingResource) + relayIdlingResource.increment() + + relay.assert { + findEnvelope { + assertEnvelopeTransaction(it.items.toList(), AndroidLogger()).transaction == "ProfilingSampleActivity" + }.assert { + val transactionItem: SentryTransaction = it.assertTransaction() + it.assertNoOtherItems() + val measurements = transactionItem.measurements + val frozenFrames = measurements[MeasurementValue.KEY_FRAMES_FROZEN]?.value?.toInt() ?: 0 + val slowFrames = measurements[MeasurementValue.KEY_FRAMES_SLOW]?.value?.toInt() ?: 0 + val totalFrames = measurements[MeasurementValue.KEY_FRAMES_TOTAL]?.value?.toInt() ?: 0 + assertEquals("ProfilingSampleActivity", transactionItem.transaction) + // AGP matrix tests have no frames + Assume.assumeTrue(totalFrames > 0) + assertNotEquals(totalFrames, 0) + assertTrue(totalFrames > slowFrames + frozenFrames, "Expected total frames ($totalFrames) to be higher than the sum of slow ($slowFrames) and frozen ($frozenFrames) frames.") + } + assertNoOtherEnvelopes() + } + } + + @Test + fun checkAppStartFramesMeasurementsPerfV2() { + initSentry(true) { options: SentryAndroidOptions -> + options.tracesSampleRate = 1.0 + options.isEnableTimeToFullDisplayTracing = true + options.isEnablePerformanceV2 = true + } + + IdlingRegistry.getInstance().register(ProfilingSampleActivity.scrollingIdlingResource) + val sampleScenario = launchActivity() + swipeList(3) + Sentry.reportFullyDisplayed() + sampleScenario.moveToState(Lifecycle.State.DESTROYED) + IdlingRegistry.getInstance().unregister(ProfilingSampleActivity.scrollingIdlingResource) + relayIdlingResource.increment() + + relay.assert { + findEnvelope { + assertEnvelopeTransaction(it.items.toList(), AndroidLogger()).transaction == "ProfilingSampleActivity" + }.assert { + val transactionItem: SentryTransaction = it.assertTransaction() + it.assertNoOtherItems() + val measurements = transactionItem.measurements + val frozenFrames = measurements[MeasurementValue.KEY_FRAMES_FROZEN]?.value?.toInt() ?: 0 + val slowFrames = measurements[MeasurementValue.KEY_FRAMES_SLOW]?.value?.toInt() ?: 0 + val totalFrames = measurements[MeasurementValue.KEY_FRAMES_TOTAL]?.value?.toInt() ?: 0 + assertEquals("ProfilingSampleActivity", transactionItem.transaction) + // AGP matrix tests have no frames + Assume.assumeTrue(totalFrames > 0) + assertNotEquals(totalFrames, 0) + assertTrue(totalFrames > slowFrames + frozenFrames, "Expected total frames ($totalFrames) to be higher than the sum of slow ($slowFrames) and frozen ($frozenFrames) frames.") + } + assertNoOtherEnvelopes() + } + } + + private fun swipeList(times: Int) { + repeat(times) { + Thread.sleep(100) + Espresso.onView(ViewMatchers.withId(R.id.profiling_sample_list)).perform(ViewActions.swipeUp()) + Espresso.onIdle() + } + } } 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 6825933ea3..ebf729f6b6 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 @@ -157,7 +157,7 @@ class EnvelopeTests : BaseUiTest() { // Timestamps of measurements should differ at least 10 milliseconds from each other (1 until values.size).forEach { i -> assertTrue( - values[i].relativeStartNs.toLong() > values[i - 1].relativeStartNs.toLong() + TimeUnit.MILLISECONDS.toNanos( + values[i].relativeStartNs.toLong() >= values[i - 1].relativeStartNs.toLong() + TimeUnit.MILLISECONDS.toNanos( 10 ), "Measurement value timestamp for '$name' does not differ at least 10ms" diff --git a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/SdkInitTests.kt b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/SdkInitTests.kt index c942783548..b615406a3d 100644 --- a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/SdkInitTests.kt +++ b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/SdkInitTests.kt @@ -56,6 +56,7 @@ class SdkInitTests : BaseUiTest() { it.isDebug = true } relayIdlingResource.increment() + relayIdlingResource.increment() transaction.finish() sampleScenario.moveToState(Lifecycle.State.DESTROYED) val transaction2 = Sentry.startTransaction("e2etests2", "testInit") @@ -63,7 +64,23 @@ class SdkInitTests : BaseUiTest() { relay.assert { findEnvelope { - assertEnvelopeTransaction(it.items.toList(), AndroidLogger()).transaction == "e2etests2" + assertEnvelopeTransaction( + it.items.toList(), + AndroidLogger() + ).transaction == "e2etests" + }.assert { + val transactionItem: SentryTransaction = it.assertTransaction() + it.assertNoOtherItems() + assertEquals("e2etests", transactionItem.transaction) + } + } + + relay.assert { + findEnvelope { + assertEnvelopeTransaction( + it.items.toList(), + AndroidLogger() + ).transaction == "e2etests2" }.assert { val transactionItem: SentryTransaction = it.assertTransaction() // Profiling uses executorService, so if the executorService is shutdown it would fail @@ -105,7 +122,8 @@ class SdkInitTests : BaseUiTest() { Sentry.startTransaction("afterRestart", "emptyTransaction").finish() // We assert for less than 1 second just to account for slow devices in saucelabs or headless emulator - assertTrue(restartMs < 1000, "Expected less than 1000 ms for SDK restart. Got $restartMs ms") + // TODO: Revert back to 1000ms after making scope.close() faster again + assertTrue(restartMs < 2500, "Expected less than 2500 ms for SDK restart. Got $restartMs ms") relay.assert { findEnvelope { diff --git a/sentry-android-ndk/CMakeLists.txt b/sentry-android-ndk/CMakeLists.txt deleted file mode 100644 index c9a0181935..0000000000 --- a/sentry-android-ndk/CMakeLists.txt +++ /dev/null @@ -1,17 +0,0 @@ -cmake_minimum_required(VERSION 3.10) -project(Sentry-Android LANGUAGES C CXX) - -# Add sentry-android shared library -add_library(sentry-android SHARED src/main/jni/sentry.c) - -# make sure that we build it as a shared lib instead of a static lib -set(BUILD_SHARED_LIBS ON) -set(SENTRY_BUILD_SHARED_LIBS ON) - -# Adding sentry-native submodule subdirectory -add_subdirectory(${SENTRY_NATIVE_SRC} sentry_build) - -# Link to sentry-native -target_link_libraries(sentry-android PRIVATE - $ -) diff --git a/sentry-android-ndk/api/sentry-android-ndk.api b/sentry-android-ndk/api/sentry-android-ndk.api index e8f838ce8b..155a368b11 100644 --- a/sentry-android-ndk/api/sentry-android-ndk.api +++ b/sentry-android-ndk/api/sentry-android-ndk.api @@ -7,7 +7,7 @@ public final class io/sentry/android/ndk/BuildConfig { } public final class io/sentry/android/ndk/DebugImagesLoader : io/sentry/android/core/IDebugImagesLoader { - public fun (Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/ndk/NativeModuleListLoader;)V + public fun (Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/ndk/NativeModuleListLoader;)V public fun clearDebugImages ()V public fun loadDebugImages ()Ljava/util/List; } diff --git a/sentry-android-ndk/build.gradle.kts b/sentry-android-ndk/build.gradle.kts index f6564cd97f..fe67063139 100644 --- a/sentry-android-ndk/build.gradle.kts +++ b/sentry-android-ndk/build.gradle.kts @@ -5,38 +5,21 @@ plugins { kotlin("android") jacoco id(Config.QualityPlugins.jacocoAndroid) - id(Config.NativePlugins.nativeBundleExport) id(Config.QualityPlugins.gradleVersions) } -var sentryNativeSrc: String = "sentry-native" val sentryAndroidSdkName: String by project android { compileSdk = Config.Android.compileSdkVersion namespace = "io.sentry.android.ndk" - sentryNativeSrc = if (File("${project.projectDir}/sentry-native-local").exists()) { - "sentry-native-local" - } else { - "sentry-native" - } - println("sentry-android-ndk: $sentryNativeSrc") - defaultConfig { targetSdk = Config.Android.targetSdkVersion minSdk = Config.Android.minSdkVersionNdk // NDK requires a higher API level than core. testInstrumentationRunner = Config.TestLibs.androidJUnitRunner - externalNativeBuild { - cmake { - arguments.add(0, "-DANDROID_STL=c++_static") - arguments.add(0, "-DSENTRY_NATIVE_SRC=$sentryNativeSrc") - arguments.add(0, "-DSENTRY_SDK_NAME=$sentryAndroidSdkName") - } - } - ndk { abiFilters.addAll(Config.Android.abiFilters) } @@ -45,15 +28,6 @@ android { buildConfigField("String", "VERSION_NAME", "\"${project.version}\"") } - // we use the default NDK and CMake versions based on the AGP's version - // https://developer.android.com/studio/projects/install-ndk#apply-specific-version - - externalNativeBuild { - cmake { - path("CMakeLists.txt") - } - } - buildTypes { getByName("debug") getByName("release") { @@ -81,10 +55,6 @@ android { checkReleaseBuilds = false } - nativeBundleExport { - headerDir = "${project.projectDir}/$sentryNativeSrc/include" - } - // needed because of Kotlin 1.4.x configurations.all { resolutionStrategy.force(Config.CompileOnly.jetbrainsAnnotations) @@ -101,6 +71,8 @@ dependencies { api(projects.sentry) api(projects.sentryAndroidCore) + implementation("io.sentry:sentry-native-ndk:0.7.5") + compileOnly(Config.CompileOnly.jetbrainsAnnotations) testImplementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION)) diff --git a/sentry-android-ndk/sentry-native b/sentry-android-ndk/sentry-native deleted file mode 160000 index 4ec95c0725..0000000000 --- a/sentry-android-ndk/sentry-native +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4ec95c0725df5f34440db8fa8d37b4c519fce74e diff --git a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/DebugImagesLoader.java b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/DebugImagesLoader.java index cb38db498a..2e069dcc74 100644 --- a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/DebugImagesLoader.java +++ b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/DebugImagesLoader.java @@ -4,9 +4,10 @@ import io.sentry.SentryOptions; import io.sentry.android.core.IDebugImagesLoader; import io.sentry.android.core.SentryAndroidOptions; +import io.sentry.ndk.NativeModuleListLoader; import io.sentry.protocol.DebugImage; import io.sentry.util.Objects; -import java.util.Arrays; +import java.util.ArrayList; import java.util.List; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -45,9 +46,20 @@ public DebugImagesLoader( synchronized (debugImagesLock) { if (debugImages == null) { try { - final DebugImage[] debugImagesArr = moduleListLoader.loadModuleList(); + final io.sentry.ndk.DebugImage[] debugImagesArr = moduleListLoader.loadModuleList(); if (debugImagesArr != null) { - debugImages = Arrays.asList(debugImagesArr); + debugImages = new ArrayList<>(debugImagesArr.length); + for (io.sentry.ndk.DebugImage d : debugImagesArr) { + final DebugImage debugImage = new DebugImage(); + debugImage.setUuid(d.getUuid()); + debugImage.setType(d.getType()); + debugImage.setDebugId(d.getDebugId()); + debugImage.setCodeId(d.getCodeId()); + debugImage.setImageAddr(d.getImageAddr()); + debugImage.setImageSize(d.getImageSize()); + debugImage.setArch(d.getArch()); + debugImages.add(debugImage); + } options .getLogger() .log(SentryLevel.DEBUG, "Debug images loaded: %d", debugImages.size()); diff --git a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/INativeScope.java b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/INativeScope.java deleted file mode 100644 index a8d50e40fe..0000000000 --- a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/INativeScope.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.sentry.android.ndk; - -interface INativeScope { - void setTag(String key, String value); - - void removeTag(String key); - - void setExtra(String key, String value); - - void removeExtra(String key); - - void setUser(String id, String email, String ipAddress, String username); - - void removeUser(); - - void addBreadcrumb( - String level, String message, String category, String type, String timestamp, String data); -} diff --git a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NativeModuleListLoader.java b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NativeModuleListLoader.java deleted file mode 100644 index 464fcd3992..0000000000 --- a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NativeModuleListLoader.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.sentry.android.ndk; - -import io.sentry.protocol.DebugImage; -import org.jetbrains.annotations.Nullable; - -final class NativeModuleListLoader { - - public @Nullable DebugImage[] loadModuleList() { - return nativeLoadModuleList(); - } - - public void clearModuleList() { - nativeClearModuleList(); - } - - public static native DebugImage[] nativeLoadModuleList(); - - public static native void nativeClearModuleList(); -} diff --git a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NativeScope.java b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NativeScope.java deleted file mode 100644 index 9d82f9d5c8..0000000000 --- a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NativeScope.java +++ /dev/null @@ -1,55 +0,0 @@ -package io.sentry.android.ndk; - -final class NativeScope implements INativeScope { - @Override - public void setTag(String key, String value) { - nativeSetTag(key, value); - } - - @Override - public void removeTag(String key) { - nativeRemoveTag(key); - } - - @Override - public void setExtra(String key, String value) { - nativeSetExtra(key, value); - } - - @Override - public void removeExtra(String key) { - nativeRemoveExtra(key); - } - - @Override - public void setUser(String id, String email, String ipAddress, String username) { - nativeSetUser(id, email, ipAddress, username); - } - - @Override - public void removeUser() { - nativeRemoveUser(); - } - - @Override - public void addBreadcrumb( - String level, String message, String category, String type, String timestamp, String data) { - nativeAddBreadcrumb(level, message, category, type, timestamp, data); - } - - public static native void nativeSetTag(String key, String value); - - public static native void nativeRemoveTag(String key); - - public static native void nativeSetExtra(String key, String value); - - public static native void nativeRemoveExtra(String key); - - public static native void nativeSetUser( - String id, String email, String ipAddress, String username); - - public static native void nativeRemoveUser(); - - public static native void nativeAddBreadcrumb( - String level, String message, String category, String type, String timestamp, String data); -} 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 009bba9b81..4a4237ba08 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 @@ -5,6 +5,8 @@ import io.sentry.ScopeObserverAdapter; import io.sentry.SentryLevel; import io.sentry.SentryOptions; +import io.sentry.ndk.INativeScope; +import io.sentry.ndk.NativeScope; import io.sentry.protocol.User; import io.sentry.util.Objects; import java.util.Locale; diff --git a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java index 1ddc04c524..ebce1a12fd 100644 --- a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java +++ b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java @@ -1,6 +1,8 @@ package io.sentry.android.ndk; import io.sentry.android.core.SentryAndroidOptions; +import io.sentry.ndk.NativeModuleListLoader; +import io.sentry.ndk.NdkOptions; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -9,21 +11,6 @@ public final class SentryNdk { private SentryNdk() {} - static { - // On older Android versions, it was necessary to manually call "`System.loadLibrary` on all - // transitive dependencies before loading [the] main library." - // The dependencies of `libsentry.so` are currently `lib{c,m,dl,log}.so`. - // See - // https://android.googlesource.com/platform/bionic/+/master/android-changes-for-ndk-developers.md#changes-to-library-dependency-resolution - System.loadLibrary("log"); - System.loadLibrary("sentry"); - System.loadLibrary("sentry-android"); - } - - private static native void initSentryNative(@NotNull final SentryAndroidOptions options); - - private static native void shutdown(); - /** * Init the NDK integration * @@ -31,7 +18,18 @@ private SentryNdk() {} */ public static void init(@NotNull final SentryAndroidOptions options) { SentryNdkUtil.addPackage(options.getSdkVersion()); - initSentryNative(options); + + final @NotNull NdkOptions ndkOptions = + new NdkOptions( + options.getDsn(), + options.isDebug(), + options.getOutboxPath(), + options.getRelease(), + options.getEnvironment(), + options.getDist(), + options.getMaxBreadcrumbs(), + options.getNativeSdkName()); + io.sentry.ndk.SentryNdk.init(ndkOptions); // only add scope sync observer if the scope sync is enabled. if (options.isEnableScopeSync()) { @@ -43,6 +41,6 @@ public static void init(@NotNull final SentryAndroidOptions options) { /** Closes the NDK integration */ public static void close() { - shutdown(); + io.sentry.ndk.SentryNdk.close(); } } diff --git a/sentry-android-ndk/src/main/jni/sentry.c b/sentry-android-ndk/src/main/jni/sentry.c deleted file mode 100644 index d62ef56123..0000000000 --- a/sentry-android-ndk/src/main/jni/sentry.c +++ /dev/null @@ -1,494 +0,0 @@ -#include -#include -#include -#include -#include - -#define ENSURE(Expr) \ - if (!(Expr)) \ - return - -#define ENSURE_OR_FAIL(Expr) \ - if (!(Expr)) \ - goto fail - -static bool get_string_into(JNIEnv *env, jstring jstr, char* buf, size_t buf_len) -{ - jsize utf_len = (*env)->GetStringUTFLength(env, jstr); - if ((size_t)utf_len >= buf_len) { - return false; - } - - jsize j_len = (*env)->GetStringLength(env, jstr); - - (*env)->GetStringUTFRegion(env, jstr, 0, j_len, buf); - if ((*env)->ExceptionCheck(env) == JNI_TRUE) { - return false; - } - - buf[utf_len] = '\0'; - return true; -} - -static char* get_string(JNIEnv *env, jstring jstr) { - char *buf = NULL; - - jsize utf_len = (*env)->GetStringUTFLength(env, jstr); - size_t buf_len = (size_t)utf_len + 1; - buf = sentry_malloc(buf_len); - ENSURE_OR_FAIL(buf); - - ENSURE_OR_FAIL(get_string_into(env, jstr, buf, buf_len)); - - return buf; - -fail: - sentry_free(buf); - - return NULL; -} - -static char *call_get_string(JNIEnv *env, jobject obj, jmethodID mid) -{ - jstring j_str = (jstring)(*env)->CallObjectMethod(env, obj, mid); - ENSURE_OR_FAIL(j_str); - char* str = get_string(env, j_str); - (*env)->DeleteLocalRef(env, j_str); - - return str; - -fail: - return NULL; -} - -JNIEXPORT void JNICALL -Java_io_sentry_android_ndk_NativeScope_nativeSetTag( - JNIEnv *env, - jclass cls, - jstring key, - jstring value) { - const char *charKey = (*env)->GetStringUTFChars(env, key, 0); - const char *charValue = (*env)->GetStringUTFChars(env, value, 0); - - sentry_set_tag(charKey, charValue); - - (*env)->ReleaseStringUTFChars(env, key, charKey); - (*env)->ReleaseStringUTFChars(env, value, charValue); -} - -JNIEXPORT void JNICALL -Java_io_sentry_android_ndk_NativeScope_nativeRemoveTag(JNIEnv *env, jclass cls, jstring key) { - const char *charKey = (*env)->GetStringUTFChars(env, key, 0); - - sentry_remove_tag(charKey); - - (*env)->ReleaseStringUTFChars(env, key, charKey); -} - -JNIEXPORT void JNICALL -Java_io_sentry_android_ndk_NativeScope_nativeSetExtra( - JNIEnv *env, - jclass cls, - jstring key, - jstring value) { - const char *charKey = (*env)->GetStringUTFChars(env, key, 0); - const char *charValue = (*env)->GetStringUTFChars(env, value, 0); - - sentry_value_t sentryValue = sentry_value_new_string(charValue); - sentry_set_extra(charKey, sentryValue); - - (*env)->ReleaseStringUTFChars(env, key, charKey); - (*env)->ReleaseStringUTFChars(env, value, charValue); -} - -JNIEXPORT void JNICALL -Java_io_sentry_android_ndk_NativeScope_nativeRemoveExtra(JNIEnv *env, jclass cls, jstring key) { - const char *charKey = (*env)->GetStringUTFChars(env, key, 0); - - sentry_remove_extra(charKey); - - (*env)->ReleaseStringUTFChars(env, key, charKey); -} - -JNIEXPORT void JNICALL -Java_io_sentry_android_ndk_NativeScope_nativeSetUser( - JNIEnv *env, - jclass cls, - jstring id, - jstring email, - jstring ipAddress, - jstring username) { - sentry_value_t user = sentry_value_new_object(); - if (id) { - const char *charId = (*env)->GetStringUTFChars(env, id, 0); - sentry_value_set_by_key(user, "id", sentry_value_new_string(charId)); - (*env)->ReleaseStringUTFChars(env, id, charId); - } - if (email) { - const char *charEmail = (*env)->GetStringUTFChars(env, email, 0); - sentry_value_set_by_key( - user, "email", sentry_value_new_string(charEmail)); - (*env)->ReleaseStringUTFChars(env, email, charEmail); - } - if (ipAddress) { - const char *charIpAddress = (*env)->GetStringUTFChars(env, ipAddress, 0); - sentry_value_set_by_key( - user, "ip_address", sentry_value_new_string(charIpAddress)); - (*env)->ReleaseStringUTFChars(env, ipAddress, charIpAddress); - } - if (username) { - const char *charUsername = (*env)->GetStringUTFChars(env, username, 0); - sentry_value_set_by_key( - user, "username", sentry_value_new_string(charUsername)); - (*env)->ReleaseStringUTFChars(env, username, charUsername); - } - sentry_set_user(user); -} - -JNIEXPORT void JNICALL -Java_io_sentry_android_ndk_NativeScope_nativeRemoveUser(JNIEnv *env, jclass cls) { - sentry_remove_user(); -} - -JNIEXPORT void JNICALL -Java_io_sentry_android_ndk_NativeScope_nativeAddBreadcrumb( - JNIEnv *env, - jclass cls, - jstring level, - jstring message, - jstring category, - jstring type, - jstring timestamp, - jstring data) { - if (!level && !message && !category && !type) { - return; - } - const char *charMessage = NULL; - if (message) { - charMessage = (*env)->GetStringUTFChars(env, message, 0); - } - const char *charType = NULL; - if (type) { - charType = (*env)->GetStringUTFChars(env, type, 0); - } - sentry_value_t crumb = sentry_value_new_breadcrumb(charType, charMessage); - - if (charMessage) { - (*env)->ReleaseStringUTFChars(env, message, charMessage); - } - if (charType) { - (*env)->ReleaseStringUTFChars(env, type, charType); - } - - if (category) { - const char *charCategory = (*env)->GetStringUTFChars(env, category, 0); - sentry_value_set_by_key( - crumb, "category", sentry_value_new_string(charCategory)); - (*env)->ReleaseStringUTFChars(env, category, charCategory); - } - if (level) { - const char *charLevel = (*env)->GetStringUTFChars(env, level, 0); - sentry_value_set_by_key( - crumb, "level", sentry_value_new_string(charLevel)); - (*env)->ReleaseStringUTFChars(env, level, charLevel); - } - - if (timestamp) { - // overwrite timestamp that is already created on sentry_value_new_breadcrumb - const char *charTimestamp = (*env)->GetStringUTFChars(env, timestamp, 0); - sentry_value_set_by_key( - crumb, "timestamp", sentry_value_new_string(charTimestamp)); - (*env)->ReleaseStringUTFChars(env, timestamp, charTimestamp); - } - - if (data) { - const char *charData = (*env)->GetStringUTFChars(env, data, 0); - - // we create an object because the Java layer parses it as a Map - sentry_value_t dataObject = sentry_value_new_object(); - sentry_value_set_by_key(dataObject, "data", sentry_value_new_string(charData)); - - sentry_value_set_by_key(crumb, "data", dataObject); - - (*env)->ReleaseStringUTFChars(env, data, charData); - } - - sentry_add_breadcrumb(crumb); -} - -static void send_envelope(sentry_envelope_t *envelope, void *data) { - const char *outbox_path = (const char *) data; - char envelope_id_str[40]; - - sentry_uuid_t envelope_id = sentry_uuid_new_v4(); - sentry_uuid_as_string(&envelope_id, envelope_id_str); - - size_t outbox_len = strlen(outbox_path); - size_t final_len = outbox_len + 42; // "/" + envelope_id_str + "\0" = 42 - char* envelope_path = sentry_malloc(final_len); - ENSURE(envelope_path); - int written = snprintf(envelope_path, final_len, "%s/%s", outbox_path, envelope_id_str); - if (written > outbox_len && written < final_len) { - sentry_envelope_write_to_file(envelope, envelope_path); - } - - sentry_free(envelope_path); - sentry_envelope_free(envelope); -} - -JNIEXPORT void JNICALL -Java_io_sentry_android_ndk_SentryNdk_initSentryNative( - JNIEnv *env, - jclass cls, - jobject sentry_sdk_options) { - jclass options_cls = (*env)->GetObjectClass(env, sentry_sdk_options); - jmethodID outbox_path_mid = (*env)->GetMethodID(env, options_cls, "getOutboxPath", - "()Ljava/lang/String;"); - jmethodID dsn_mid = (*env)->GetMethodID(env, options_cls, "getDsn", "()Ljava/lang/String;"); - jmethodID is_debug_mid = (*env)->GetMethodID(env, options_cls, "isDebug", "()Z"); - jmethodID release_mid = (*env)->GetMethodID(env, options_cls, "getRelease", - "()Ljava/lang/String;"); - jmethodID environment_mid = (*env)->GetMethodID(env, options_cls, "getEnvironment", - "()Ljava/lang/String;"); - jmethodID dist_mid = (*env)->GetMethodID(env, options_cls, "getDist", "()Ljava/lang/String;"); - jmethodID max_crumbs_mid = (*env)->GetMethodID(env, options_cls, "getMaxBreadcrumbs", "()I"); - jmethodID native_sdk_name_mid = (*env)->GetMethodID(env, options_cls, "getNativeSdkName", - "()Ljava/lang/String;"); - - (*env)->DeleteLocalRef(env, options_cls); - - char *outbox_path = NULL; - sentry_transport_t *transport = NULL; - bool transport_owns_path = false; - sentry_options_t *options = NULL; - bool options_owns_transport = false; - char *dsn_str = NULL; - char *release_str = NULL; - char *environment_str = NULL; - char *dist_str = NULL; - char *native_sdk_name_str = NULL; - - options = sentry_options_new(); - ENSURE_OR_FAIL(options); - - // session tracking is enabled by default, but the Android SDK already handles it - sentry_options_set_auto_session_tracking(options, 0); - - jboolean debug = (jboolean)(*env)->CallBooleanMethod(env, sentry_sdk_options, is_debug_mid); - sentry_options_set_debug(options, debug); - - jint max_crumbs = (jint) (*env)->CallIntMethod(env, sentry_sdk_options, max_crumbs_mid); - sentry_options_set_max_breadcrumbs(options, max_crumbs); - - outbox_path = call_get_string(env, sentry_sdk_options, outbox_path_mid); - ENSURE_OR_FAIL(outbox_path); - - transport = sentry_transport_new(send_envelope); - ENSURE_OR_FAIL(transport); - sentry_transport_set_state(transport, outbox_path); - sentry_transport_set_free_func(transport, sentry_free); - transport_owns_path = true; - - sentry_options_set_transport(options, transport); - options_owns_transport = true; - - // give sentry-native its own database path it can work with, next to the outbox - size_t outbox_len = strlen(outbox_path); - size_t final_len = outbox_len + 15; // len(".sentry-native\0") = 15 - char* database_path = sentry_malloc(final_len); - ENSURE_OR_FAIL(database_path); - strncpy(database_path, outbox_path, final_len); - char *dir = strrchr(database_path, '/'); - if (dir) - { - strncpy(dir + 1, ".sentry-native", final_len - (dir + 1 - database_path)); - } - sentry_options_set_database_path(options, database_path); - sentry_free(database_path); - - dsn_str = call_get_string(env, sentry_sdk_options, dsn_mid); - ENSURE_OR_FAIL(dsn_str); - sentry_options_set_dsn(options, dsn_str); - sentry_free(dsn_str); - - release_str = call_get_string(env, sentry_sdk_options, release_mid); - if (release_str) { - sentry_options_set_release(options, release_str); - sentry_free(release_str); - } - - environment_str = call_get_string(env, sentry_sdk_options, environment_mid); - if (environment_str) - { - sentry_options_set_environment(options, environment_str); - sentry_free(environment_str); - } - - dist_str = call_get_string(env, sentry_sdk_options, dist_mid); - if (dist_str) - { - sentry_options_set_dist(options, dist_str); - sentry_free(dist_str); - } - - native_sdk_name_str = call_get_string(env, sentry_sdk_options, native_sdk_name_mid); - if (native_sdk_name_str) { - sentry_options_set_sdk_name(options, native_sdk_name_str); - sentry_free(native_sdk_name_str); - } - - sentry_init(options); - return; - -fail: - if (!transport_owns_path) { - sentry_free(outbox_path); - } - if (!options_owns_transport) { - sentry_transport_free(transport); - } - sentry_options_free(options); -} - -JNIEXPORT void JNICALL -Java_io_sentry_android_ndk_NativeModuleListLoader_nativeClearModuleList(JNIEnv *env, jclass cls) { - sentry_clear_modulecache(); -} - -JNIEXPORT jobjectArray JNICALL -Java_io_sentry_android_ndk_NativeModuleListLoader_nativeLoadModuleList(JNIEnv *env, jclass cls) { - sentry_value_t image_list_t = sentry_get_modules_list(); - jobjectArray image_list = NULL; - - if (sentry_value_get_type(image_list_t) == SENTRY_VALUE_TYPE_LIST) { - size_t len_t = sentry_value_get_length(image_list_t); - - jclass image_class = (*env)->FindClass(env, "io/sentry/protocol/DebugImage"); - image_list = (*env)->NewObjectArray(env, len_t, image_class, NULL); - - jmethodID image_addr_method = (*env)->GetMethodID(env, image_class, "setImageAddr", - "(Ljava/lang/String;)V"); - - jmethodID image_size_method = (*env)->GetMethodID(env, image_class, "setImageSize", - "(J)V"); - - jmethodID code_file_method = (*env)->GetMethodID(env, image_class, "setCodeFile", - "(Ljava/lang/String;)V"); - - jmethodID image_addr_ctor = (*env)->GetMethodID(env, image_class, "", - "()V"); - - jmethodID type_method = (*env)->GetMethodID(env, image_class, "setType", - "(Ljava/lang/String;)V"); - - jmethodID debug_id_method = (*env)->GetMethodID(env, image_class, "setDebugId", - "(Ljava/lang/String;)V"); - - jmethodID code_id_method = (*env)->GetMethodID(env, image_class, "setCodeId", - "(Ljava/lang/String;)V"); - - jmethodID debug_file_method = (*env)->GetMethodID(env, image_class, "setDebugFile", - "(Ljava/lang/String;)V"); - - for (size_t i = 0; i < len_t; i++) { - sentry_value_t image_t = sentry_value_get_by_index(image_list_t, i); - - if (!sentry_value_is_null(image_t)) { - jobject image = (*env)->NewObject(env, image_class, image_addr_ctor); - - sentry_value_t image_addr_t = sentry_value_get_by_key(image_t, "image_addr"); - if (!sentry_value_is_null(image_addr_t)) { - - const char *value_v = sentry_value_as_string(image_addr_t); - jstring value = (*env)->NewStringUTF(env, value_v); - - (*env)->CallVoidMethod(env, image, image_addr_method, value); - - // Local refs (eg NewStringUTF) are freed automatically when the native method - // returns, but if you're iterating a large array, it's recommended to release - // manually due to allocation limits (512) on Android < 8 or OOM. - // https://developer.android.com/training/articles/perf-jni.html#local-and-global-references - (*env)->DeleteLocalRef(env, value); - } - - sentry_value_t image_size_t = sentry_value_get_by_key(image_t, "image_size"); - if (!sentry_value_is_null(image_size_t)) { - - int32_t value_v = sentry_value_as_int32(image_size_t); - jlong value = (jlong) value_v; - - (*env)->CallVoidMethod(env, image, image_size_method, value); - } - - sentry_value_t code_file_t = sentry_value_get_by_key(image_t, "code_file"); - if (!sentry_value_is_null(code_file_t)) { - - const char *value_v = sentry_value_as_string(code_file_t); - jstring value = (*env)->NewStringUTF(env, value_v); - - (*env)->CallVoidMethod(env, image, code_file_method, value); - - (*env)->DeleteLocalRef(env, value); - } - - sentry_value_t code_type_t = sentry_value_get_by_key(image_t, "type"); - if (!sentry_value_is_null(code_type_t)) { - - const char *value_v = sentry_value_as_string(code_type_t); - jstring value = (*env)->NewStringUTF(env, value_v); - - (*env)->CallVoidMethod(env, image, type_method, value); - - (*env)->DeleteLocalRef(env, value); - } - - sentry_value_t debug_id_t = sentry_value_get_by_key(image_t, "debug_id"); - if (!sentry_value_is_null(code_type_t)) { - - const char *value_v = sentry_value_as_string(debug_id_t); - jstring value = (*env)->NewStringUTF(env, value_v); - - (*env)->CallVoidMethod(env, image, debug_id_method, value); - - (*env)->DeleteLocalRef(env, value); - } - - sentry_value_t code_id_t = sentry_value_get_by_key(image_t, "code_id"); - if (!sentry_value_is_null(code_id_t)) { - - const char *value_v = sentry_value_as_string(code_id_t); - jstring value = (*env)->NewStringUTF(env, value_v); - - (*env)->CallVoidMethod(env, image, code_id_method, value); - - (*env)->DeleteLocalRef(env, value); - } - - // not needed on Android, but keeping for forward compatibility - sentry_value_t debug_file_t = sentry_value_get_by_key(image_t, "debug_file"); - if (!sentry_value_is_null(debug_file_t)) { - - const char *value_v = sentry_value_as_string(debug_file_t); - jstring value = (*env)->NewStringUTF(env, value_v); - - (*env)->CallVoidMethod(env, image, debug_file_method, value); - - (*env)->DeleteLocalRef(env, value); - } - - (*env)->SetObjectArrayElement(env, image_list, i, image); - - (*env)->DeleteLocalRef(env, image); - } - } - - sentry_value_decref(image_list_t); - } - - return image_list; -} - -JNIEXPORT void JNICALL -Java_io_sentry_android_ndk_SentryNdk_shutdown(JNIEnv *env, jclass cls) { - sentry_close(); -} diff --git a/sentry-android-ndk/src/test/java/io/sentry/android/ndk/DebugImagesLoaderTest.kt b/sentry-android-ndk/src/test/java/io/sentry/android/ndk/DebugImagesLoaderTest.kt index db584c814f..927ce98c3b 100644 --- a/sentry-android-ndk/src/test/java/io/sentry/android/ndk/DebugImagesLoaderTest.kt +++ b/sentry-android-ndk/src/test/java/io/sentry/android/ndk/DebugImagesLoaderTest.kt @@ -1,11 +1,10 @@ package io.sentry.android.ndk import io.sentry.android.core.SentryAndroidOptions -import io.sentry.protocol.DebugImage +import io.sentry.ndk.NativeModuleListLoader import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -import java.lang.RuntimeException import kotlin.test.Test import kotlin.test.assertNotNull import kotlin.test.assertNull @@ -38,7 +37,7 @@ class DebugImagesLoaderTest { whenever(fixture.nativeLoader.loadModuleList()).thenReturn(arrayOf()) assertNotNull(sut.loadDebugImages()) - whenever(fixture.nativeLoader.loadModuleList()).thenReturn(arrayOf(DebugImage())) + whenever(fixture.nativeLoader.loadModuleList()).thenReturn(arrayOf(io.sentry.ndk.DebugImage())) assertTrue(sut.loadDebugImages()!!.isEmpty()) } diff --git a/sentry-android-ndk/src/test/java/io/sentry/android/ndk/NdkScopeObserverTest.kt b/sentry-android-ndk/src/test/java/io/sentry/android/ndk/NdkScopeObserverTest.kt index 335a7679e1..ad523a883e 100644 --- a/sentry-android-ndk/src/test/java/io/sentry/android/ndk/NdkScopeObserverTest.kt +++ b/sentry-android-ndk/src/test/java/io/sentry/android/ndk/NdkScopeObserverTest.kt @@ -5,6 +5,7 @@ import io.sentry.DateUtils import io.sentry.JsonSerializer import io.sentry.SentryLevel import io.sentry.SentryOptions +import io.sentry.ndk.INativeScope import io.sentry.protocol.User import org.mockito.kotlin.eq import org.mockito.kotlin.mock 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 3c7e590fe1..3925a83199 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 @@ -20,7 +20,7 @@ import okhttp3.Response * @param scopes The [IScopes], internal and only used for testing. * @param beforeSpan The [ISpan] can be customized or dropped with the [BeforeSpanCallback]. * @param captureFailedRequests The SDK will only capture HTTP Client errors if it is enabled, - * Defaults to false. + * Defaults to true. * @param failedRequestStatusCodes The SDK will only capture HTTP Client errors if the HTTP Response * status code is within the defined ranges. * @param failedRequestTargets The SDK will only capture HTTP Client errors if the HTTP Request URL 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-graphql/src/main/java/io/sentry/graphql/ExceptionReporter.java b/sentry-graphql/src/main/java/io/sentry/graphql/ExceptionReporter.java index 843ca77494..9bca0955e4 100644 --- a/sentry-graphql/src/main/java/io/sentry/graphql/ExceptionReporter.java +++ b/sentry-graphql/src/main/java/io/sentry/graphql/ExceptionReporter.java @@ -149,6 +149,9 @@ public boolean isSubscription() { return isSubscription; } + /** + * @deprecated please use {@link ExceptionDetails#getScopes()} instead. + */ @Deprecated public @NotNull IScopes getHub() { return scopes; diff --git a/sentry-graphql/src/main/java/io/sentry/graphql/SentryInstrumentation.java b/sentry-graphql/src/main/java/io/sentry/graphql/SentryInstrumentation.java index e4f85d12a2..2de9b82f75 100644 --- a/sentry-graphql/src/main/java/io/sentry/graphql/SentryInstrumentation.java +++ b/sentry-graphql/src/main/java/io/sentry/graphql/SentryInstrumentation.java @@ -18,12 +18,14 @@ import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLOutputType; import io.sentry.Breadcrumb; +import io.sentry.Hint; import io.sentry.IScopes; import io.sentry.ISpan; import io.sentry.NoOpScopes; import io.sentry.Sentry; import io.sentry.SentryIntegrationPackageStorage; import io.sentry.SpanStatus; +import io.sentry.TypeCheckHint; import io.sentry.util.StringUtils; import java.util.ArrayList; import java.util.Arrays; @@ -48,6 +50,9 @@ public final class SentryInstrumentation ); public static final @NotNull String SENTRY_SCOPES_CONTEXT_KEY = "sentry.scopes"; + /** + * @deprecated please use {@link SentryInstrumentation#SENTRY_SCOPES_CONTEXT_KEY} instead. + */ @Deprecated public static final @NotNull String SENTRY_HUB_CONTEXT_KEY = SENTRY_SCOPES_CONTEXT_KEY; @@ -297,13 +302,16 @@ private boolean isIgnored(final @Nullable String errorType) { return environment -> { final @Nullable ExecutionStepInfo executionStepInfo = environment.getExecutionStepInfo(); if (executionStepInfo != null) { + Hint hint = new Hint(); + hint.set(TypeCheckHint.GRAPHQL_DATA_FETCHING_ENVIRONMENT, environment); scopesFromContext(parameters.getExecutionContext().getGraphQLContext()) .addBreadcrumb( Breadcrumb.graphqlDataFetcher( StringUtils.toString(executionStepInfo.getPath()), GraphqlStringUtils.fieldToString(executionStepInfo.getField()), GraphqlStringUtils.typeToString(executionStepInfo.getType()), - GraphqlStringUtils.objectTypeToString(executionStepInfo.getObjectType()))); + GraphqlStringUtils.objectTypeToString(executionStepInfo.getObjectType())), + hint); } final TracingState tracingState = parameters.getInstrumentationState(); final ISpan transaction = tracingState.getTransaction(); diff --git a/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryInstrumentationAnotherTest.kt b/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryInstrumentationAnotherTest.kt index 6309155929..7b673a66a8 100644 --- a/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryInstrumentationAnotherTest.kt +++ b/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryInstrumentationAnotherTest.kt @@ -28,11 +28,13 @@ import graphql.schema.GraphQLObjectType import graphql.schema.GraphQLScalarType import graphql.schema.GraphQLSchema import io.sentry.Breadcrumb +import io.sentry.Hint import io.sentry.IScopes import io.sentry.Sentry import io.sentry.SentryOptions import io.sentry.SentryTracer import io.sentry.TransactionContext +import io.sentry.TypeCheckHint import io.sentry.graphql.ExceptionReporter.ExceptionDetails import io.sentry.graphql.SentryInstrumentation.SENTRY_EXCEPTIONS_CONTEXT_KEY import io.sentry.graphql.SentryInstrumentation.TracingState @@ -245,6 +247,10 @@ class SentryInstrumentationAnotherTest { assertEquals("myFieldName", breadcrumb.data["field"]) assertEquals("MyResponseType", breadcrumb.data["type"]) assertEquals("QUERY", breadcrumb.data["object_type"]) + }, + org.mockito.kotlin.check { hint -> + val environment = hint.getAs(TypeCheckHint.GRAPHQL_DATA_FETCHING_ENVIRONMENT, DataFetchingEnvironment::class.java) + assertNotNull(environment) } ) } diff --git a/sentry-jul/src/test/kotlin/io/sentry/jul/SentryHandlerTest.kt b/sentry-jul/src/test/kotlin/io/sentry/jul/SentryHandlerTest.kt index 64b9c84656..eaef25f070 100644 --- a/sentry-jul/src/test/kotlin/io/sentry/jul/SentryHandlerTest.kt +++ b/sentry-jul/src/test/kotlin/io/sentry/jul/SentryHandlerTest.kt @@ -63,6 +63,7 @@ class SentryHandlerTest { it.dsn = "http://key@localhost/proj" it.environment = "manual-environment" it.setTransportFactory { _, _ -> transport } + it.isEnableBackpressureHandling = false } fixture = Fixture(transport = transport) fixture.logger.severe("testing environment field") diff --git a/sentry-log4j2/src/test/kotlin/io/sentry/log4j2/SentryAppenderTest.kt b/sentry-log4j2/src/test/kotlin/io/sentry/log4j2/SentryAppenderTest.kt index 2713bb7f17..e24f12d659 100644 --- a/sentry-log4j2/src/test/kotlin/io/sentry/log4j2/SentryAppenderTest.kt +++ b/sentry-log4j2/src/test/kotlin/io/sentry/log4j2/SentryAppenderTest.kt @@ -87,6 +87,7 @@ class SentryAppenderTest { it.dsn = "http://key@localhost/proj" it.environment = "manual-environment" it.setTransportFactory(fixture.transportFactory) + it.isEnableBackpressureHandling = false } val logger = fixture.getSut() logger.error("testing environment field") diff --git a/sentry-logback/src/test/kotlin/io/sentry/logback/SentryAppenderTest.kt b/sentry-logback/src/test/kotlin/io/sentry/logback/SentryAppenderTest.kt index 3abc2cdd11..c08837b3ff 100644 --- a/sentry-logback/src/test/kotlin/io/sentry/logback/SentryAppenderTest.kt +++ b/sentry-logback/src/test/kotlin/io/sentry/logback/SentryAppenderTest.kt @@ -100,6 +100,7 @@ class SentryAppenderTest { it.environment = "manual-environment" it.setTransportFactory(fixture.transportFactory) it.setTag("tag-from-first-init", "some-value") + it.isEnableBackpressureHandling = false } fixture.start() diff --git a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEvent.kt b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEvent.kt index 153499210c..2f3862bd67 100644 --- a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEvent.kt +++ b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEvent.kt @@ -38,13 +38,15 @@ internal class SentryOkHttpEvent(private val scopes: IScopes, private val reques private var clientErrorResponse: Response? = null private val isReadingResponseBody = AtomicBoolean(false) private val isEventFinished = AtomicBoolean(false) + private val url: String + private val method: String init { val urlDetails = UrlUtils.parse(request.url.toString()) - val url = urlDetails.urlOrFallback + url = urlDetails.urlOrFallback val host: String = request.url.host val encodedPath: String = request.url.encodedPath - val method: String = request.method + method = request.method // We start the call span that will contain all the others val parentSpan = if (Platform.isAndroid()) scopes.transaction else scopes.span @@ -113,7 +115,7 @@ internal class SentryOkHttpEvent(private val scopes: IScopes, private val reques fun startSpan(event: String) { // Find the parent of the span being created. E.g. secureConnect is child of connect val parentSpan = findParentSpan(event) - val span = parentSpan?.startChild("http.client.$event") ?: return + val span = parentSpan?.startChild("http.client.$event", "$method $url") ?: return if (event == RESPONSE_BODY_EVENT) { // We save this event is reading the response body, so that it will not be auto-finished isReadingResponseBody.set(true) diff --git a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventListenerTest.kt b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventListenerTest.kt index 3a90a4c05c..388b494547 100644 --- a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventListenerTest.kt +++ b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventListenerTest.kt @@ -168,28 +168,35 @@ class SentryOkHttpEventListenerTest { } 1 -> { assertEquals("http.client.proxy_select", span.operation) + assertEquals("GET ${request.url}", span.description) assertNotNull(span.data["proxies"]) } 2 -> { assertEquals("http.client.dns", span.operation) + assertEquals("GET ${request.url}", span.description) assertNotNull(span.data["domain_name"]) assertNotNull(span.data["dns_addresses"]) } 3 -> { assertEquals("http.client.connect", span.operation) + assertEquals("GET ${request.url}", span.description) } 4 -> { assertEquals("http.client.connection", span.operation) + assertEquals("GET ${request.url}", span.description) } 5 -> { assertEquals("http.client.request_headers", span.operation) + assertEquals("GET ${request.url}", span.description) } 6 -> { assertEquals("http.client.response_headers", span.operation) + assertEquals("GET ${request.url}", span.description) assertEquals(201, span.data[SpanDataConvention.HTTP_STATUS_CODE_KEY]) } 7 -> { assertEquals("http.client.response_body", span.operation) + assertEquals("GET ${request.url}", span.description) } } } diff --git a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventTest.kt b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventTest.kt index 4d9f005143..23688d9d85 100644 --- a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventTest.kt +++ b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventTest.kt @@ -158,6 +158,7 @@ class SentryOkHttpEventTest { assertNotNull(span) assertTrue(spans.containsKey("span")) assertEquals("http.client.span", span.operation) + assertEquals("${fixture.mockRequest.method} ${fixture.mockRequest.url}", span.description) assertFalse(span.isFinished) } @@ -196,6 +197,7 @@ class SentryOkHttpEventTest { sut.finishSpan("span") { if (called == 0) { assertEquals("http.client.span", it.operation) + assertEquals("${fixture.mockRequest.method} ${fixture.mockRequest.url}", it.description) } else { assertEquals(sut.callRootSpan, it) } diff --git a/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryAutoConfigurationCustomizerProvider.java b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryAutoConfigurationCustomizerProvider.java index 82d015cd3b..6e776f94ed 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryAutoConfigurationCustomizerProvider.java +++ b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryAutoConfigurationCustomizerProvider.java @@ -56,6 +56,8 @@ public void customize(AutoConfigurationCustomizer autoConfiguration) { } } + ContextStorage.addWrapper((storage) -> new SentryContextStorage(storage)); + autoConfiguration .addTracerProviderCustomizer(this::configureSdkTracerProvider) .addPropertiesSupplier(this::getDefaultProperties); diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SpanDescriptionExtractor.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SpanDescriptionExtractor.java index f0f56cb8ee..da359bbd25 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SpanDescriptionExtractor.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SpanDescriptionExtractor.java @@ -14,6 +14,7 @@ @ApiStatus.Internal public final class SpanDescriptionExtractor { + // TODO [POTEL] remove these method overloads and pass in SpanData instead (span.toSpanData()) @SuppressWarnings("deprecation") public @NotNull OtelSpanInfo extractSpanInfo(final @NotNull SpanData otelSpan) { OtelSpanInfo spanInfo = extractSpanDescription(otelSpan); diff --git a/sentry-samples/sentry-samples-android/CMakeLists.txt b/sentry-samples/sentry-samples-android/CMakeLists.txt index ad170fe404..19dca2b80d 100644 --- a/sentry-samples/sentry-samples-android/CMakeLists.txt +++ b/sentry-samples/sentry-samples-android/CMakeLists.txt @@ -3,15 +3,12 @@ project(Sentry-Sample LANGUAGES C CXX) add_library(native-sample SHARED src/main/cpp/native-sample.cpp) -# make sure that we build it as a shared lib instead of a static lib -set(BUILD_SHARED_LIBS ON) -set(SENTRY_BUILD_SHARED_LIBS ON) - -add_subdirectory(../../sentry-android-ndk/${SENTRY_NATIVE_SRC} sentry_build) +find_package(sentry-native-ndk REQUIRED CONFIG) find_library(LOG_LIB log) target_link_libraries(native-sample PRIVATE ${LOG_LIB} - $ + sentry-native-ndk::sentry-android + sentry-native-ndk::sentry ) diff --git a/sentry-samples/sentry-samples-android/build.gradle.kts b/sentry-samples/sentry-samples-android/build.gradle.kts index ac77791268..871f46ce09 100644 --- a/sentry-samples/sentry-samples-android/build.gradle.kts +++ b/sentry-samples/sentry-samples-android/build.gradle.kts @@ -15,16 +15,8 @@ android { versionName = project.version.toString() externalNativeBuild { - val sentryNativeSrc = if (File("${project.projectDir}/../../sentry-android-ndk/sentry-native-local").exists()) { - "sentry-native-local" - } else { - "sentry-native" - } - println("sentry-samples-android: $sentryNativeSrc") - cmake { - arguments.add(0, "-DANDROID_STL=c++_static") - arguments.add(0, "-DSENTRY_NATIVE_SRC=$sentryNativeSrc") + arguments.add(0, "-DANDROID_STL=c++_shared") } } @@ -38,6 +30,7 @@ android { // Note that the viewBinding.enabled property is now deprecated. viewBinding = true compose = true + prefab = true } composeOptions { @@ -111,6 +104,7 @@ dependencies { implementation(projects.sentryAndroidFragment) implementation(projects.sentryAndroidTimber) implementation(projects.sentryCompose) + implementation(projects.sentryComposeHelper) implementation(Config.Libs.fragment) implementation(Config.Libs.timber) @@ -133,4 +127,6 @@ dependencies { implementation(Config.Libs.composeMaterial) debugImplementation(Config.Libs.leakCanary) + + implementation("io.sentry:sentry-native-ndk:0.7.5") } diff --git a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java index b2c8df213c..d7f5099aa2 100644 --- a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java +++ b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java @@ -33,6 +33,7 @@ import io.sentry.spring.jakarta.tracing.SentryTracingFilter; import io.sentry.spring.jakarta.tracing.SentryTransactionPointcutConfiguration; import io.sentry.spring.jakarta.tracing.SpringMvcTransactionNameProvider; +import io.sentry.spring.jakarta.tracing.SpringServletTransactionNameProvider; import io.sentry.spring.jakarta.tracing.TransactionNameProvider; import io.sentry.transport.ITransportGate; import io.sentry.transport.apache.ApacheHttpClientTransportFactory; @@ -49,6 +50,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; @@ -267,12 +269,6 @@ static class SentrySecurityConfiguration { return new SentryRequestResolver(scopes); } - @Bean - @ConditionalOnMissingBean(TransactionNameProvider.class) - public @NotNull TransactionNameProvider transactionNameProvider() { - return new SpringMvcTransactionNameProvider(); - } - @Bean @ConditionalOnMissingBean(name = "sentrySpringFilter") public @NotNull FilterRegistrationBean sentrySpringFilter( @@ -297,15 +293,38 @@ public FilterRegistrationBean sentryTracingFilter( return filter; } - @Bean - @ConditionalOnMissingBean + @Configuration(proxyBeanMethods = false) @ConditionalOnClass(HandlerExceptionResolver.class) - public @NotNull SentryExceptionResolver sentryExceptionResolver( - final @NotNull IScopes scopes, - final @NotNull TransactionNameProvider transactionNameProvider, - final @NotNull SentryProperties options) { - return new SentryExceptionResolver( - scopes, transactionNameProvider, options.getExceptionResolverOrder()); + @Open + static class SentryMvcModeConfig { + + @Bean + @ConditionalOnMissingBean + public @NotNull SentryExceptionResolver sentryExceptionResolver( + final @NotNull IScopes scopes, + final @NotNull TransactionNameProvider transactionNameProvider, + final @NotNull SentryProperties options) { + return new SentryExceptionResolver( + scopes, transactionNameProvider, options.getExceptionResolverOrder()); + } + + @Bean + @ConditionalOnMissingBean(TransactionNameProvider.class) + public @NotNull TransactionNameProvider transactionNameProvider() { + return new SpringMvcTransactionNameProvider(); + } + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingClass("org.springframework.web.servlet.HandlerExceptionResolver") + @Open + static class SentryServletModeConfig { + + @Bean + @ConditionalOnMissingBean(TransactionNameProvider.class) + public @NotNull TransactionNameProvider transactionNameProvider() { + return new SpringServletTransactionNameProvider(); + } } } diff --git a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt index df0fe96cf0..9805de6e21 100644 --- a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt @@ -26,6 +26,8 @@ import io.sentry.spring.jakarta.SentryUserFilter import io.sentry.spring.jakarta.SentryUserProvider import io.sentry.spring.jakarta.SpringSecuritySentryUserProvider import io.sentry.spring.jakarta.tracing.SentryTracingFilter +import io.sentry.spring.jakarta.tracing.SpringServletTransactionNameProvider +import io.sentry.spring.jakarta.tracing.TransactionNameProvider import io.sentry.transport.ITransport import io.sentry.transport.ITransportGate import io.sentry.transport.apache.ApacheHttpClientTransportFactory @@ -169,7 +171,7 @@ class SentryAutoConfigurationTest { "sentry.enabled=false", "sentry.send-modules=false", "sentry.ignored-checkins=slug1,slugB", - "sentry.enable-backpressure-handling=true", + "sentry.enable-backpressure-handling=false", "sentry.cron.default-checkin-margin=10", "sentry.cron.default-max-runtime=30", "sentry.cron.default-timezone=America/New_York", @@ -206,7 +208,7 @@ class SentryAutoConfigurationTest { assertThat(options.isEnabled).isEqualTo(false) assertThat(options.isSendModules).isEqualTo(false) assertThat(options.ignoredCheckIns).containsOnly("slug1", "slugB") - assertThat(options.isEnableBackpressureHandling).isEqualTo(true) + assertThat(options.isEnableBackpressureHandling).isEqualTo(false) assertThat(options.cron).isNotNull assertThat(options.cron!!.defaultCheckinMargin).isEqualTo(10L) assertThat(options.cron!!.defaultMaxRuntime).isEqualTo(30L) @@ -445,6 +447,15 @@ class SentryAutoConfigurationTest { } } + @Test + fun `when Spring MVC is not on the classpath, fallback TransactionNameProvider is configured`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.send-default-pii=true") + .withClassLoader(FilteredClassLoader(HandlerExceptionResolver::class.java)) + .run { + assertThat(it.getBean(TransactionNameProvider::class.java)).isInstanceOf(SpringServletTransactionNameProvider::class.java) + } + } + @Test fun `when tracing is enabled, creates tracing filter`() { contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.traces-sample-rate=1.0") diff --git a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/it/SentrySpringIntegrationTest.kt b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/it/SentrySpringIntegrationTest.kt index 13a207d8eb..1b997737ab 100644 --- a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/it/SentrySpringIntegrationTest.kt +++ b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/it/SentrySpringIntegrationTest.kt @@ -52,7 +52,7 @@ import kotlin.test.Test @SpringBootTest( classes = [App::class], webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - properties = ["sentry.dsn=http://key@localhost/proj", "sentry.send-default-pii=true", "sentry.traces-sample-rate=1.0", "sentry.max-request-body-size=medium"] + properties = ["sentry.dsn=http://key@localhost/proj", "sentry.send-default-pii=true", "sentry.traces-sample-rate=1.0", "sentry.max-request-body-size=medium", "sentry.enable-backpressure-handling=false"] ) class SentrySpringIntegrationTest { diff --git a/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java index 7d6a6a6fc4..df3ca097bc 100644 --- a/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java +++ b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java @@ -33,6 +33,7 @@ import io.sentry.spring.tracing.SentryTracingFilter; import io.sentry.spring.tracing.SentryTransactionPointcutConfiguration; import io.sentry.spring.tracing.SpringMvcTransactionNameProvider; +import io.sentry.spring.tracing.SpringServletTransactionNameProvider; import io.sentry.spring.tracing.TransactionNameProvider; import io.sentry.transport.ITransportGate; import io.sentry.transport.apache.ApacheHttpClientTransportFactory; @@ -49,6 +50,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; @@ -265,12 +267,6 @@ static class SentrySecurityConfiguration { return new SentryRequestResolver(scopes); } - @Bean - @ConditionalOnMissingBean(TransactionNameProvider.class) - public @NotNull TransactionNameProvider transactionNameProvider() { - return new SpringMvcTransactionNameProvider(); - } - @Bean @ConditionalOnMissingBean(name = "sentrySpringFilter") public @NotNull FilterRegistrationBean sentrySpringFilter( @@ -295,15 +291,38 @@ public FilterRegistrationBean sentryTracingFilter( return filter; } - @Bean - @ConditionalOnMissingBean + @Configuration(proxyBeanMethods = false) @ConditionalOnClass(HandlerExceptionResolver.class) - public @NotNull SentryExceptionResolver sentryExceptionResolver( - final @NotNull IScopes scopes, - final @NotNull TransactionNameProvider transactionNameProvider, - final @NotNull SentryProperties options) { - return new SentryExceptionResolver( - scopes, transactionNameProvider, options.getExceptionResolverOrder()); + @Open + static class SentryMvcModeConfig { + + @Bean + @ConditionalOnMissingBean + public @NotNull SentryExceptionResolver sentryExceptionResolver( + final @NotNull IScopes scopes, + final @NotNull TransactionNameProvider transactionNameProvider, + final @NotNull SentryProperties options) { + return new SentryExceptionResolver( + scopes, transactionNameProvider, options.getExceptionResolverOrder()); + } + + @Bean + @ConditionalOnMissingBean(TransactionNameProvider.class) + public @NotNull TransactionNameProvider transactionNameProvider() { + return new SpringMvcTransactionNameProvider(); + } + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingClass("org.springframework.web.servlet.HandlerExceptionResolver") + @Open + static class SentryServletModeConfig { + + @Bean + @ConditionalOnMissingBean(TransactionNameProvider.class) + public @NotNull TransactionNameProvider transactionNameProvider() { + return new SpringServletTransactionNameProvider(); + } } } diff --git a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt index e1fe220aea..a65926c934 100644 --- a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt @@ -26,6 +26,8 @@ import io.sentry.spring.SentryUserFilter import io.sentry.spring.SentryUserProvider import io.sentry.spring.SpringSecuritySentryUserProvider import io.sentry.spring.tracing.SentryTracingFilter +import io.sentry.spring.tracing.SpringServletTransactionNameProvider +import io.sentry.spring.tracing.TransactionNameProvider import io.sentry.transport.ITransport import io.sentry.transport.ITransportGate import io.sentry.transport.apache.ApacheHttpClientTransportFactory @@ -168,7 +170,7 @@ class SentryAutoConfigurationTest { "sentry.enabled=false", "sentry.send-modules=false", "sentry.ignored-checkins=slug1,slugB", - "sentry.enable-backpressure-handling=true", + "sentry.enable-backpressure-handling=false", "sentry.cron.default-checkin-margin=10", "sentry.cron.default-max-runtime=30", "sentry.cron.default-timezone=America/New_York", @@ -205,7 +207,7 @@ class SentryAutoConfigurationTest { assertThat(options.isEnabled).isEqualTo(false) assertThat(options.isSendModules).isEqualTo(false) assertThat(options.ignoredCheckIns).containsOnly("slug1", "slugB") - assertThat(options.isEnableBackpressureHandling).isEqualTo(true) + assertThat(options.isEnableBackpressureHandling).isEqualTo(false) assertThat(options.cron).isNotNull assertThat(options.cron!!.defaultCheckinMargin).isEqualTo(10L) assertThat(options.cron!!.defaultMaxRuntime).isEqualTo(30L) @@ -444,6 +446,17 @@ class SentryAutoConfigurationTest { } } + @Test + fun `when Spring MVC is not on the classpath, fallback TransactionNameProvider is configured`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.send-default-pii=true") + .withClassLoader(FilteredClassLoader(HandlerExceptionResolver::class.java)) + .run { + assertThat(it.getBean(TransactionNameProvider::class.java)).isInstanceOf( + SpringServletTransactionNameProvider::class.java + ) + } + } + @Test fun `when tracing is enabled, creates tracing filter`() { contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.traces-sample-rate=1.0") diff --git a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/it/SentrySpringIntegrationTest.kt b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/it/SentrySpringIntegrationTest.kt index 3bbcb2c3e7..bb6aa99fe9 100644 --- a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/it/SentrySpringIntegrationTest.kt +++ b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/it/SentrySpringIntegrationTest.kt @@ -52,7 +52,7 @@ import kotlin.test.Test @SpringBootTest( classes = [App::class], webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - properties = ["sentry.dsn=http://key@localhost/proj", "sentry.send-default-pii=true", "sentry.traces-sample-rate=1.0", "sentry.max-request-body-size=medium"] + properties = ["sentry.dsn=http://key@localhost/proj", "sentry.send-default-pii=true", "sentry.traces-sample-rate=1.0", "sentry.max-request-body-size=medium", "sentry.enable-backpressure-handling=false"] ) class SentrySpringIntegrationTest { diff --git a/sentry-spring-jakarta/api/sentry-spring-jakarta.api b/sentry-spring-jakarta/api/sentry-spring-jakarta.api index 7f414c3495..4eed502841 100644 --- a/sentry-spring-jakarta/api/sentry-spring-jakarta.api +++ b/sentry-spring-jakarta/api/sentry-spring-jakarta.api @@ -263,6 +263,12 @@ public final class io/sentry/spring/jakarta/tracing/SpringMvcTransactionNameProv public fun provideTransactionSource ()Lio/sentry/protocol/TransactionNameSource; } +public final class io/sentry/spring/jakarta/tracing/SpringServletTransactionNameProvider : io/sentry/spring/jakarta/tracing/TransactionNameProvider { + public fun ()V + public fun provideTransactionName (Ljakarta/servlet/http/HttpServletRequest;)Ljava/lang/String; + public fun provideTransactionSource ()Lio/sentry/protocol/TransactionNameSource; +} + public abstract interface class io/sentry/spring/jakarta/tracing/TransactionNameProvider { public abstract fun provideTransactionName (Ljakarta/servlet/http/HttpServletRequest;)Ljava/lang/String; public fun provideTransactionSource ()Lio/sentry/protocol/TransactionNameSource; diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SpringServletTransactionNameProvider.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SpringServletTransactionNameProvider.java new file mode 100644 index 0000000000..e0beda65a0 --- /dev/null +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SpringServletTransactionNameProvider.java @@ -0,0 +1,22 @@ +package io.sentry.spring.jakarta.tracing; + +import io.sentry.protocol.TransactionNameSource; +import jakarta.servlet.http.HttpServletRequest; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** Fallback TransactionNameProvider when Spring is used in servlet mode (without MVC). */ +@ApiStatus.Internal +public final class SpringServletTransactionNameProvider implements TransactionNameProvider { + @Override + public @Nullable String provideTransactionName(final @NotNull HttpServletRequest request) { + return request.getMethod() + " " + request.getRequestURI(); + } + + @Override + @ApiStatus.Internal + public @NotNull TransactionNameSource provideTransactionSource() { + return TransactionNameSource.URL; + } +} diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter.java index 86e17f27c3..e626e69d3b 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter.java @@ -35,7 +35,12 @@ public abstract class AbstractSentryWebFilter implements WebFilter { private final @NotNull SentryRequestResolver sentryRequestResolver; public static final String SENTRY_SCOPES_KEY = "sentry-scopes"; + + /** + * @deprecated please use {@link AbstractSentryWebFilter#SENTRY_SCOPES_KEY} instead. + */ @Deprecated public static final String SENTRY_HUB_KEY = SENTRY_SCOPES_KEY; + private static final String TRANSACTION_OP = "http.server"; public AbstractSentryWebFilter(final @NotNull IScopes scopes) { diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebfluxIntegrationTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebfluxIntegrationTest.kt index 59f9a700b6..9033028dfc 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebfluxIntegrationTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebfluxIntegrationTest.kt @@ -95,7 +95,7 @@ class SentryWebfluxIntegrationTest { checkEvent { event -> assertEquals("GET /throws", event.transaction) assertNotNull(event.exceptions) { - val ex = it.first() + val ex = it.last() assertEquals("something went wrong", ex.value) assertNotNull(ex.mechanism) { assertThat(it.isHandled).isFalse() @@ -180,6 +180,7 @@ open class App { it.setDebug(true) it.setTransportFactory(transportFactory) it.enableTracing = true + it.isEnableBackpressureHandling = false } } } diff --git a/sentry-spring/api/sentry-spring.api b/sentry-spring/api/sentry-spring.api index 7c6af0ecf5..9d31cdd62a 100644 --- a/sentry-spring/api/sentry-spring.api +++ b/sentry-spring/api/sentry-spring.api @@ -262,6 +262,12 @@ public final class io/sentry/spring/tracing/SpringMvcTransactionNameProvider : i public fun provideTransactionSource ()Lio/sentry/protocol/TransactionNameSource; } +public final class io/sentry/spring/tracing/SpringServletTransactionNameProvider : io/sentry/spring/tracing/TransactionNameProvider { + public fun ()V + public fun provideTransactionName (Ljavax/servlet/http/HttpServletRequest;)Ljava/lang/String; + public fun provideTransactionSource ()Lio/sentry/protocol/TransactionNameSource; +} + public abstract interface class io/sentry/spring/tracing/TransactionNameProvider { public abstract fun provideTransactionName (Ljavax/servlet/http/HttpServletRequest;)Ljava/lang/String; public fun provideTransactionSource ()Lio/sentry/protocol/TransactionNameSource; diff --git a/sentry-spring/src/main/java/io/sentry/spring/tracing/SpringServletTransactionNameProvider.java b/sentry-spring/src/main/java/io/sentry/spring/tracing/SpringServletTransactionNameProvider.java new file mode 100644 index 0000000000..0e8118e632 --- /dev/null +++ b/sentry-spring/src/main/java/io/sentry/spring/tracing/SpringServletTransactionNameProvider.java @@ -0,0 +1,22 @@ +package io.sentry.spring.tracing; + +import io.sentry.protocol.TransactionNameSource; +import javax.servlet.http.HttpServletRequest; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** Fallback TransactionNameProvider when Spring is used in servlet mode (without MVC). */ +@ApiStatus.Internal +public final class SpringServletTransactionNameProvider implements TransactionNameProvider { + @Override + public @Nullable String provideTransactionName(final @NotNull HttpServletRequest request) { + return request.getMethod() + " " + request.getRequestURI(); + } + + @Override + @ApiStatus.Internal + public @NotNull TransactionNameSource provideTransactionSource() { + return TransactionNameSource.URL; + } +} diff --git a/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryWebFilter.java b/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryWebFilter.java index 0601dcaeb3..3549b95c81 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryWebFilter.java +++ b/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryWebFilter.java @@ -35,7 +35,11 @@ @ApiStatus.Experimental public final class SentryWebFilter implements WebFilter { public static final String SENTRY_SCOPES_KEY = "sentry-scopes"; + /** + * @deprecated please use {@link SentryWebFilter#SENTRY_SCOPES_KEY} instead. + */ @Deprecated public static final String SENTRY_HUB_KEY = SENTRY_SCOPES_KEY; + private static final String TRANSACTION_OP = "http.server"; private static final String TRACE_ORIGIN = "auto.spring.webflux"; diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebfluxIntegrationTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebfluxIntegrationTest.kt index 8c5aeb1c0a..316aaf8738 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebfluxIntegrationTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebfluxIntegrationTest.kt @@ -95,7 +95,7 @@ class SentryWebfluxIntegrationTest { checkEvent { event -> assertEquals("GET /throws", event.transaction) assertNotNull(event.exceptions) { - val ex = it.first() + val ex = it.last() assertEquals("something went wrong", ex.value) assertNotNull(ex.mechanism) { assertThat(it.isHandled).isFalse() diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 59954294c2..3601236557 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -321,6 +321,7 @@ public final class io/sentry/DataCategory : java/lang/Enum { public static final field Attachment Lio/sentry/DataCategory; public static final field Default Lio/sentry/DataCategory; public static final field Error Lio/sentry/DataCategory; + public static final field MetricBucket Lio/sentry/DataCategory; public static final field Monitor Lio/sentry/DataCategory; public static final field Profile Lio/sentry/DataCategory; public static final field Security Lio/sentry/DataCategory; @@ -702,7 +703,6 @@ public abstract interface class io/sentry/IMetricsAggregator : java/io/Closeable public abstract fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JLio/sentry/metrics/LocalMetricsAggregator;)V public abstract fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;JLio/sentry/metrics/LocalMetricsAggregator;)V public abstract fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;JLio/sentry/metrics/LocalMetricsAggregator;)V - public abstract fun timing (Ljava/lang/String;Ljava/lang/Runnable;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;Lio/sentry/metrics/LocalMetricsAggregator;)V } public abstract interface class io/sentry/IOptionsObserver { @@ -956,6 +956,8 @@ public abstract interface class io/sentry/ISpan { public abstract fun getEventId ()Lio/sentry/protocol/SentryId; public abstract fun getFinishDate ()Lio/sentry/SentryDate; public abstract fun getLocalMetricsAggregator ()Lio/sentry/metrics/LocalMetricsAggregator; + public abstract fun getName ()Ljava/lang/String; + public abstract fun getNameSource ()Lio/sentry/protocol/TransactionNameSource; public abstract fun getOperation ()Ljava/lang/String; public abstract fun getSamplingDecision ()Lio/sentry/TracesSamplingDecision; public abstract fun getSpanContext ()Lio/sentry/SpanContext; @@ -972,6 +974,8 @@ public abstract interface class io/sentry/ISpan { public abstract fun setDescription (Ljava/lang/String;)V public abstract fun setMeasurement (Ljava/lang/String;Ljava/lang/Number;)V public abstract fun setMeasurement (Ljava/lang/String;Ljava/lang/Number;Lio/sentry/MeasurementUnit;)V + public abstract fun setName (Ljava/lang/String;)V + public abstract fun setName (Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;)V public abstract fun setOperation (Ljava/lang/String;)V public abstract fun setStatus (Lio/sentry/SpanStatus;)V public abstract fun setTag (Ljava/lang/String;Ljava/lang/String;)V @@ -1215,7 +1219,6 @@ public final class io/sentry/MetricsAggregator : io/sentry/IMetricsAggregator, j public fun run ()V public fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;JLio/sentry/metrics/LocalMetricsAggregator;)V public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;JLio/sentry/metrics/LocalMetricsAggregator;)V - public fun timing (Ljava/lang/String;Ljava/lang/Runnable;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;Lio/sentry/metrics/LocalMetricsAggregator;)V } public final class io/sentry/MonitorConfig : io/sentry/JsonSerializable, io/sentry/JsonUnknown { @@ -1544,6 +1547,11 @@ public final class io/sentry/NoOpScopesStorage : io/sentry/IScopesStorage { public fun set (Lio/sentry/IScopes;)Lio/sentry/ISentryLifecycleToken; } +public final class io/sentry/NoOpScopesStorage$NoOpScopesLifecycleToken : io/sentry/ISentryLifecycleToken { + public fun close ()V + public static fun getInstance ()Lio/sentry/NoOpScopesStorage$NoOpScopesLifecycleToken; +} + public final class io/sentry/NoOpSpan : io/sentry/ISpan { public fun finish ()V public fun finish (Lio/sentry/SpanStatus;)V @@ -1555,6 +1563,8 @@ public final class io/sentry/NoOpSpan : io/sentry/ISpan { public fun getFinishDate ()Lio/sentry/SentryDate; public static fun getInstance ()Lio/sentry/NoOpSpan; public fun getLocalMetricsAggregator ()Lio/sentry/metrics/LocalMetricsAggregator; + public fun getName ()Ljava/lang/String; + public fun getNameSource ()Lio/sentry/protocol/TransactionNameSource; public fun getOperation ()Ljava/lang/String; public fun getSamplingDecision ()Lio/sentry/TracesSamplingDecision; public fun getSpanContext ()Lio/sentry/SpanContext; @@ -1571,6 +1581,8 @@ public final class io/sentry/NoOpSpan : io/sentry/ISpan { public fun setDescription (Ljava/lang/String;)V public fun setMeasurement (Ljava/lang/String;Ljava/lang/Number;)V public fun setMeasurement (Ljava/lang/String;Ljava/lang/Number;Lio/sentry/MeasurementUnit;)V + public fun setName (Ljava/lang/String;)V + public fun setName (Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;)V public fun setOperation (Ljava/lang/String;)V public fun setStatus (Lio/sentry/SpanStatus;)V public fun setTag (Ljava/lang/String;Ljava/lang/String;)V @@ -1602,6 +1614,7 @@ public final class io/sentry/NoOpTransaction : io/sentry/ITransaction { public fun getLatestActiveSpan ()Lio/sentry/ISpan; public fun getLocalMetricsAggregator ()Lio/sentry/metrics/LocalMetricsAggregator; public fun getName ()Ljava/lang/String; + public fun getNameSource ()Lio/sentry/protocol/TransactionNameSource; public fun getOperation ()Ljava/lang/String; public fun getSamplingDecision ()Lio/sentry/TracesSamplingDecision; public fun getSpanContext ()Lio/sentry/SpanContext; @@ -2897,13 +2910,17 @@ public final class io/sentry/SentryOptions$Proxy { public fun ()V public fun (Ljava/lang/String;Ljava/lang/String;)V public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/net/Proxy$Type;)V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/net/Proxy$Type;Ljava/lang/String;Ljava/lang/String;)V public fun getHost ()Ljava/lang/String; public fun getPass ()Ljava/lang/String; public fun getPort ()Ljava/lang/String; + public fun getType ()Ljava/net/Proxy$Type; public fun getUser ()Ljava/lang/String; public fun setHost (Ljava/lang/String;)V public fun setPass (Ljava/lang/String;)V public fun setPort (Ljava/lang/String;)V + public fun setType (Ljava/net/Proxy$Type;)V public fun setUser (Ljava/lang/String;)V } @@ -2967,6 +2984,7 @@ public final class io/sentry/SentryTracer : io/sentry/ITransaction { public fun getLatestActiveSpan ()Lio/sentry/ISpan; public fun getLocalMetricsAggregator ()Lio/sentry/metrics/LocalMetricsAggregator; public fun getName ()Ljava/lang/String; + public fun getNameSource ()Lio/sentry/protocol/TransactionNameSource; public fun getOperation ()Ljava/lang/String; public fun getSamplingDecision ()Lio/sentry/TracesSamplingDecision; public fun getSpanContext ()Lio/sentry/SpanContext; @@ -3011,9 +3029,7 @@ public final class io/sentry/SentryTracer : io/sentry/ITransaction { public final class io/sentry/SentryWrapper { public fun ()V public static fun wrapCallable (Ljava/util/concurrent/Callable;)Ljava/util/concurrent/Callable; - public static fun wrapCallableIsolated (Ljava/util/concurrent/Callable;)Ljava/util/concurrent/Callable; public static fun wrapSupplier (Ljava/util/function/Supplier;)Ljava/util/function/Supplier; - public static fun wrapSupplierIsolated (Ljava/util/function/Supplier;)Ljava/util/function/Supplier; } public final class io/sentry/Session : io/sentry/JsonSerializable, io/sentry/JsonUnknown { @@ -3100,6 +3116,8 @@ public final class io/sentry/Span : io/sentry/ISpan { public fun getFinishDate ()Lio/sentry/SentryDate; public fun getLocalMetricsAggregator ()Lio/sentry/metrics/LocalMetricsAggregator; public fun getMeasurements ()Ljava/util/Map; + public fun getName ()Ljava/lang/String; + public fun getNameSource ()Lio/sentry/protocol/TransactionNameSource; public fun getOperation ()Ljava/lang/String; public fun getParentSpanId ()Lio/sentry/SpanId; public fun getSamplingDecision ()Lio/sentry/TracesSamplingDecision; @@ -3121,6 +3139,8 @@ public final class io/sentry/Span : io/sentry/ISpan { public fun setDescription (Ljava/lang/String;)V public fun setMeasurement (Ljava/lang/String;Ljava/lang/Number;)V public fun setMeasurement (Ljava/lang/String;Ljava/lang/Number;Lio/sentry/MeasurementUnit;)V + public fun setName (Ljava/lang/String;)V + public fun setName (Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;)V public fun setOperation (Ljava/lang/String;)V public fun setStatus (Lio/sentry/SpanStatus;)V public fun setTag (Ljava/lang/String;Ljava/lang/String;)V @@ -3202,6 +3222,8 @@ public final class io/sentry/SpanContext$JsonKeys { public abstract interface class io/sentry/SpanDataConvention { public static final field BLOCKED_MAIN_THREAD_KEY Ljava/lang/String; public static final field CALL_STACK_KEY Ljava/lang/String; + public static final field CONTRIBUTES_TTFD Ljava/lang/String; + public static final field CONTRIBUTES_TTID Ljava/lang/String; public static final field DB_NAME_KEY Ljava/lang/String; public static final field DB_SYSTEM_KEY Ljava/lang/String; public static final field FRAMES_DELAY Ljava/lang/String; @@ -3414,6 +3436,7 @@ public final class io/sentry/TypeCheckHint { public static final field ANDROID_VIEW Ljava/lang/String; public static final field APOLLO_REQUEST Ljava/lang/String; public static final field APOLLO_RESPONSE Ljava/lang/String; + public static final field GRAPHQL_DATA_FETCHING_ENVIRONMENT Ljava/lang/String; public static final field GRAPHQL_HANDLER_PARAMETERS Ljava/lang/String; public static final field JUL_LOG_RECORD Ljava/lang/String; public static final field LOG4J_LOG_EVENT Ljava/lang/String; @@ -3947,7 +3970,7 @@ public abstract interface class io/sentry/metrics/IMetricsClient { public final class io/sentry/metrics/LocalMetricsAggregator { public fun ()V - public fun add (Ljava/lang/String;Lio/sentry/metrics/MetricType;Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;J)V + public fun add (Ljava/lang/String;Lio/sentry/metrics/MetricType;Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;)V public fun getSummaries ()Ljava/util/Map; } @@ -4013,16 +4036,15 @@ public final class io/sentry/metrics/MetricsHelper { public static fun convertNanosTo (Lio/sentry/MeasurementUnit$Duration;J)D public static fun encodeMetrics (JLjava/util/Collection;Ljava/lang/StringBuilder;)V public static fun getCutoffTimestampMs (J)J - public static fun getDayBucketKey (Ljava/util/Calendar;)J public static fun getExportKey (Lio/sentry/metrics/MetricType;Ljava/lang/String;Lio/sentry/MeasurementUnit;)Ljava/lang/String; public static fun getMetricBucketKey (Lio/sentry/metrics/MetricType;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;)Ljava/lang/String; public static fun getTimeBucketKey (J)J public static fun mergeTags (Ljava/util/Map;Ljava/util/Map;)Ljava/util/Map; - public static fun sanitizeKey (Ljava/lang/String;)Ljava/lang/String; + public static fun sanitizeName (Ljava/lang/String;)Ljava/lang/String; + public static fun sanitizeTagKey (Ljava/lang/String;)Ljava/lang/String; + public static fun sanitizeTagValue (Ljava/lang/String;)Ljava/lang/String; public static fun sanitizeUnit (Ljava/lang/String;)Ljava/lang/String; - public static fun sanitizeValue (Ljava/lang/String;)Ljava/lang/String; public static fun setFlushShiftMs (J)V - public static fun toStatsdType (Lio/sentry/metrics/MetricType;)Ljava/lang/String; } public final class io/sentry/metrics/NoopMetricsAggregator : io/sentry/IMetricsAggregator, io/sentry/metrics/MetricsApi$IMetricsInterface { @@ -4039,7 +4061,6 @@ public final class io/sentry/metrics/NoopMetricsAggregator : io/sentry/IMetricsA public fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;JLio/sentry/metrics/LocalMetricsAggregator;)V public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;JLio/sentry/metrics/LocalMetricsAggregator;)V public fun startSpanForMetric (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ISpan; - public fun timing (Ljava/lang/String;Ljava/lang/Runnable;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;Lio/sentry/metrics/LocalMetricsAggregator;)V } public final class io/sentry/metrics/SetMetric : io/sentry/metrics/Metric { @@ -4124,6 +4145,7 @@ public final class io/sentry/protocol/App : io/sentry/JsonSerializable, io/sentr public fun getDeviceAppHash ()Ljava/lang/String; public fun getInForeground ()Ljava/lang/Boolean; public fun getPermissions ()Ljava/util/Map; + public fun getStartType ()Ljava/lang/String; public fun getUnknown ()Ljava/util/Map; public fun getViewNames ()Ljava/util/List; public fun hashCode ()I @@ -4137,6 +4159,7 @@ public final class io/sentry/protocol/App : io/sentry/JsonSerializable, io/sentr public fun setDeviceAppHash (Ljava/lang/String;)V public fun setInForeground (Ljava/lang/Boolean;)V public fun setPermissions (Ljava/util/Map;)V + public fun setStartType (Ljava/lang/String;)V public fun setUnknown (Ljava/util/Map;)V public fun setViewNames (Ljava/util/List;)V } @@ -4157,6 +4180,7 @@ public final class io/sentry/protocol/App$JsonKeys { public static final field BUILD_TYPE Ljava/lang/String; public static final field DEVICE_APP_HASH Ljava/lang/String; public static final field IN_FOREGROUND Ljava/lang/String; + public static final field START_TYPE Ljava/lang/String; public static final field VIEW_NAMES Ljava/lang/String; public fun ()V } @@ -4546,18 +4570,24 @@ public final class io/sentry/protocol/Mechanism : io/sentry/JsonSerializable, io public fun (Ljava/lang/Thread;)V public fun getData ()Ljava/util/Map; public fun getDescription ()Ljava/lang/String; + public fun getExceptionId ()Ljava/lang/Integer; public fun getHelpLink ()Ljava/lang/String; public fun getMeta ()Ljava/util/Map; + public fun getParentId ()Ljava/lang/Integer; public fun getSynthetic ()Ljava/lang/Boolean; public fun getType ()Ljava/lang/String; public fun getUnknown ()Ljava/util/Map; + public fun isExceptionGroup ()Ljava/lang/Boolean; public fun isHandled ()Ljava/lang/Boolean; public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V public fun setData (Ljava/util/Map;)V public fun setDescription (Ljava/lang/String;)V + public fun setExceptionGroup (Ljava/lang/Boolean;)V + public fun setExceptionId (Ljava/lang/Integer;)V public fun setHandled (Ljava/lang/Boolean;)V public fun setHelpLink (Ljava/lang/String;)V public fun setMeta (Ljava/util/Map;)V + public fun setParentId (Ljava/lang/Integer;)V public fun setSynthetic (Ljava/lang/Boolean;)V public fun setType (Ljava/lang/String;)V public fun setUnknown (Ljava/util/Map;)V @@ -4572,9 +4602,12 @@ public final class io/sentry/protocol/Mechanism$Deserializer : io/sentry/JsonDes public final class io/sentry/protocol/Mechanism$JsonKeys { public static final field DATA Ljava/lang/String; public static final field DESCRIPTION Ljava/lang/String; + public static final field EXCEPTION_ID Ljava/lang/String; public static final field HANDLED Ljava/lang/String; public static final field HELP_LINK Ljava/lang/String; + public static final field IS_EXCEPTION_GROUP Ljava/lang/String; public static final field META Ljava/lang/String; + public static final field PARENT_ID Ljava/lang/String; public static final field SYNTHETIC Ljava/lang/String; public static final field TYPE Ljava/lang/String; public fun ()V @@ -4951,6 +4984,7 @@ public final class io/sentry/protocol/SentrySpan : io/sentry/JsonSerializable, i public fun getUnknown ()Ljava/util/Map; public fun isFinished ()Z public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public fun setData (Ljava/util/Map;)V public fun setUnknown (Ljava/util/Map;)V } @@ -5418,6 +5452,7 @@ public final class io/sentry/util/ClassLoaderUtils { } public final class io/sentry/util/CollectionUtils { + public static fun contains ([Ljava/lang/Object;Ljava/lang/Object;)Z public static fun filterListEntries (Ljava/util/List;Lio/sentry/util/CollectionUtils$Predicate;)Ljava/util/List; public static fun filterMapEntries (Ljava/util/Map;Lio/sentry/util/CollectionUtils$Predicate;)Ljava/util/Map; public static fun map (Ljava/util/List;Lio/sentry/util/CollectionUtils$Mapper;)Ljava/util/List; @@ -5602,6 +5637,7 @@ public final class io/sentry/util/SampleRateUtils { public final class io/sentry/util/StringUtils { public static fun byteCountToString (J)Ljava/lang/String; public static fun calculateStringHash (Ljava/lang/String;Lio/sentry/ILogger;)Ljava/lang/String; + public static fun camelCase (Ljava/lang/String;)Ljava/lang/String; public static fun capitalize (Ljava/lang/String;)Ljava/lang/String; public static fun countOf (Ljava/lang/String;C)I public static fun getStringAfterDot (Ljava/lang/String;)Ljava/lang/String; diff --git a/sentry/src/main/java/io/sentry/DataCategory.java b/sentry/src/main/java/io/sentry/DataCategory.java index 6b119b43e4..c3d6520987 100644 --- a/sentry/src/main/java/io/sentry/DataCategory.java +++ b/sentry/src/main/java/io/sentry/DataCategory.java @@ -12,6 +12,7 @@ public enum DataCategory { Attachment("attachment"), Monitor("monitor"), Profile("profile"), + MetricBucket("metric_bucket"), Transaction("transaction"), Security("security"), UserReport("user_report"), diff --git a/sentry/src/main/java/io/sentry/DefaultScopesStorage.java b/sentry/src/main/java/io/sentry/DefaultScopesStorage.java index 4a054ee7cc..1ed80ceea8 100644 --- a/sentry/src/main/java/io/sentry/DefaultScopesStorage.java +++ b/sentry/src/main/java/io/sentry/DefaultScopesStorage.java @@ -21,8 +21,6 @@ public ISentryLifecycleToken set(@Nullable IScopes scopes) { @Override public void close() { - // TODO [HSM] prevent further storing? would this cause problems if singleton, closed and - // re-initialized? currentScopes.remove(); } diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index 702b27f391..8ba2ae9193 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -163,6 +163,10 @@ public void removeExtra(@NotNull String key) { return Sentry.pushIsolationScope(); } + /** + * @deprecated please call {@link ISentryLifecycleToken#close()} on the token returned by {@link + * ScopesAdapter#pushScope()} or {@link ScopesAdapter#pushIsolationScope()} instead. + */ @Override @Deprecated public void popScope() { @@ -199,6 +203,11 @@ public void flush(long timeoutMillis) { Sentry.flush(timeoutMillis); } + /** + * @deprecated please use {@link IScopes#forkedScopes(String)} or {@link + * IScopes#forkedCurrentScope(String)} instead. + */ + @Deprecated @Override public @NotNull IHub clone() { return Sentry.getCurrentScopes().clone(); diff --git a/sentry/src/main/java/io/sentry/HubScopesWrapper.java b/sentry/src/main/java/io/sentry/HubScopesWrapper.java index 3b4fbb6964..371321eaf2 100644 --- a/sentry/src/main/java/io/sentry/HubScopesWrapper.java +++ b/sentry/src/main/java/io/sentry/HubScopesWrapper.java @@ -158,6 +158,10 @@ public void removeExtra(@NotNull String key) { return scopes.pushIsolationScope(); } + /** + * @deprecated please call {@link ISentryLifecycleToken#close()} on the token returned by {@link + * IScopes#pushScope()} or {@link IScopes#pushIsolationScope()} instead. + */ @Override @Deprecated public void popScope() { @@ -194,7 +198,12 @@ public void flush(long timeoutMillis) { scopes.flush(timeoutMillis); } + /** + * @deprecated please use {@link IScopes#forkedScopes(String)} or {@link + * IScopes#forkedCurrentScope(String)} instead. + */ @Override + @Deprecated public @NotNull IHub clone() { return scopes.clone(); } diff --git a/sentry/src/main/java/io/sentry/IMetricsAggregator.java b/sentry/src/main/java/io/sentry/IMetricsAggregator.java index 41228c137d..0968c4f33c 100644 --- a/sentry/src/main/java/io/sentry/IMetricsAggregator.java +++ b/sentry/src/main/java/io/sentry/IMetricsAggregator.java @@ -103,21 +103,5 @@ void set( final long timestampMs, final @Nullable LocalMetricsAggregator localMetricsAggregator); - /** - * Emits a distribution with the time it takes to run a given code block. - * - * @param key A unique key identifying the metric - * @param callback The code block to measure - * @param unit An optional unit, see {@link MeasurementUnit.Duration}, defaults to seconds - * @param tags Optional Tags to associate with the metric - * @param localMetricsAggregator The local metrics aggregator for creating span summaries - */ - void timing( - final @NotNull String key, - final @NotNull Runnable callback, - final @NotNull MeasurementUnit.Duration unit, - final @Nullable Map tags, - final @Nullable LocalMetricsAggregator localMetricsAggregator); - void flush(boolean force); } diff --git a/sentry/src/main/java/io/sentry/ITransaction.java b/sentry/src/main/java/io/sentry/ITransaction.java index 9504e5b7a8..645b6e03d7 100644 --- a/sentry/src/main/java/io/sentry/ITransaction.java +++ b/sentry/src/main/java/io/sentry/ITransaction.java @@ -9,24 +9,6 @@ public interface ITransaction extends ISpan { - /** - * Sets transaction name. - * - * @param name - transaction name - */ - void setName(@NotNull String name); - - @ApiStatus.Internal - void setName(@NotNull String name, @NotNull TransactionNameSource transactionNameSource); - - /** - * Returns transaction name. - * - * @return transaction name - */ - @NotNull - String getName(); - /** * Returns the source of the transaction name. * diff --git a/sentry/src/main/java/io/sentry/MetricsAggregator.java b/sentry/src/main/java/io/sentry/MetricsAggregator.java index b9383ac865..71e5a40f3f 100644 --- a/sentry/src/main/java/io/sentry/MetricsAggregator.java +++ b/sentry/src/main/java/io/sentry/MetricsAggregator.java @@ -141,24 +141,6 @@ public void set( add(MetricType.Set, key, intValue, unit, tags, timestampMs, localMetricsAggregator); } - @Override - public void timing( - final @NotNull String key, - final @NotNull Runnable callback, - final @NotNull MeasurementUnit.Duration unit, - final @Nullable Map tags, - final @Nullable LocalMetricsAggregator localMetricsAggregator) { - final long startMs = nowMillis(); - final long startNanos = System.nanoTime(); - try { - callback.run(); - } finally { - final long durationNanos = (System.nanoTime() - startNanos); - final double value = MetricsHelper.convertNanosTo(unit, durationNanos); - add(MetricType.Distribution, key, value, unit, tags, startMs, localMetricsAggregator); - } - } - @SuppressWarnings({"FutureReturnValueIgnored", "UnusedVariable"}) private void add( final @NotNull MetricType type, @@ -174,8 +156,12 @@ private void add( } if (beforeEmitCallback != null) { - if (!beforeEmitCallback.execute(key, tags)) { - return; + try { + if (!beforeEmitCallback.execute(key, tags)) { + return; + } + } catch (Throwable e) { + logger.log(SentryLevel.ERROR, "The beforeEmit callback threw an exception.", e); } } @@ -224,7 +210,7 @@ private void add( // See develop docs: https://develop.sentry.dev/sdk/metrics/#sets if (localMetricsAggregator != null) { final double localValue = type == MetricType.Set ? addedWeight : value; - localMetricsAggregator.add(metricKey, type, key, localValue, unit, tags, timestampMs); + localMetricsAggregator.add(metricKey, type, key, localValue, unit, tags); } final boolean isOverWeight = isOverWeight(); @@ -342,7 +328,7 @@ public void run() { flush(false); synchronized (this) { - if (!isClosed) { + if (!isClosed && !buckets.isEmpty()) { executorService.schedule(this, MetricsHelper.FLUSHER_SLEEP_TIME_MS); } } diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index 76f36c9741..55893746d8 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -136,6 +136,10 @@ public void removeExtra(@NotNull String key) {} return NoOpScopesLifecycleToken.getInstance(); } + /** + * @deprecated please call {@link ISentryLifecycleToken#close()} on the token returned by {@link + * IScopes#pushScope()} or {@link IScopes#pushIsolationScope()} instead. + */ @Override @Deprecated public void popScope() {} @@ -164,6 +168,11 @@ public boolean isHealthy() { @Override public void flush(long timeoutMillis) {} + /** + * @deprecated please use {@link IScopes#forkedScopes(String)} or {@link + * IScopes#forkedCurrentScope(String)} instead. + */ + @Deprecated @Override public @NotNull IHub clone() { return instance; diff --git a/sentry/src/main/java/io/sentry/NoOpScopes.java b/sentry/src/main/java/io/sentry/NoOpScopes.java index 37a1ea754a..58c1207809 100644 --- a/sentry/src/main/java/io/sentry/NoOpScopes.java +++ b/sentry/src/main/java/io/sentry/NoOpScopes.java @@ -131,6 +131,10 @@ public void removeExtra(@NotNull String key) {} return NoOpScopesLifecycleToken.getInstance(); } + /** + * @deprecated please call {@link ISentryLifecycleToken#close()} on the token returned by {@link + * IScopes#pushScope()} or {@link IScopes#pushIsolationScope()} instead. + */ @Override @Deprecated public void popScope() {} @@ -159,6 +163,10 @@ public boolean isHealthy() { @Override public void flush(long timeoutMillis) {} + /** + * @deprecated please use {@link IScopes#forkedScopes(String)} or {@link + * IScopes#forkedCurrentScope(String)} instead. + */ @Deprecated @Override public @NotNull IHub clone() { diff --git a/sentry/src/main/java/io/sentry/NoOpSpan.java b/sentry/src/main/java/io/sentry/NoOpSpan.java index 533bbf0074..4acce66ec6 100644 --- a/sentry/src/main/java/io/sentry/NoOpSpan.java +++ b/sentry/src/main/java/io/sentry/NoOpSpan.java @@ -3,6 +3,7 @@ import io.sentry.metrics.LocalMetricsAggregator; import io.sentry.protocol.Contexts; import io.sentry.protocol.SentryId; +import io.sentry.protocol.TransactionNameSource; import java.util.List; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/sentry/src/main/java/io/sentry/NoOpTransaction.java b/sentry/src/main/java/io/sentry/NoOpTransaction.java index 9cbb706151..747621ee06 100644 --- a/sentry/src/main/java/io/sentry/NoOpTransaction.java +++ b/sentry/src/main/java/io/sentry/NoOpTransaction.java @@ -27,6 +27,11 @@ public void setName(@NotNull String name) {} @Override public void setName(@NotNull String name, @NotNull TransactionNameSource transactionNameSource) {} + @Override + public @NotNull TransactionNameSource getNameSource() { + return TransactionNameSource.CUSTOM; + } + @Override public @NotNull String getName() { return ""; diff --git a/sentry/src/main/java/io/sentry/Scope.java b/sentry/src/main/java/io/sentry/Scope.java index 688a77b024..cf0503d31c 100644 --- a/sentry/src/main/java/io/sentry/Scope.java +++ b/sentry/src/main/java/io/sentry/Scope.java @@ -92,8 +92,6 @@ public final class Scope implements IScope { private @NotNull ISentryClient client = NoOpSentryClient.getInstance(); - // TODO [HSM] intended only for global scope - // TODO [HSM] test for memory leak private final @NotNull Map, String>> throwableToSpan = Collections.synchronizedMap(new WeakHashMap<>()); diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index b5599ba449..d8e3fefa79 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -615,6 +615,10 @@ public ISentryLifecycleToken pushIsolationScope() { return Sentry.setCurrentScopes(this); } + /** + * @deprecated please call {@link ISentryLifecycleToken#close()} on the token returned by {@link + * IScopes#pushScope()} or {@link IScopes#pushIsolationScope()} instead. + */ @Override @Deprecated public void popScope() { @@ -630,7 +634,6 @@ public void popScope() { } } - // TODO [HSM] lots of testing required to see how ThreadLocal is affected @Override public void withScope(final @NotNull ScopeCallback callback) { if (!isEnabled()) { @@ -724,7 +727,12 @@ public void flush(long timeoutMillis) { } } + /** + * @deprecated please use {@link IScopes#forkedScopes(String)} or {@link + * IScopes#forkedCurrentScope(String)} instead. + */ @Override + @Deprecated @SuppressWarnings("deprecation") public @NotNull IHub clone() { if (!isEnabled()) { @@ -849,6 +857,9 @@ public void flush(long timeoutMillis) { transaction = spanFactory.createTransaction( transactionContext, this, transactionOptions, transactionPerformanceCollector); + // new SentryTracer( + // transactionContext, this, transactionOptions, + // transactionPerformanceCollector); // The listener is called only if the transaction exists, as the transaction is needed to // stop it diff --git a/sentry/src/main/java/io/sentry/ScopesAdapter.java b/sentry/src/main/java/io/sentry/ScopesAdapter.java index ff1f84a2aa..92387dc602 100644 --- a/sentry/src/main/java/io/sentry/ScopesAdapter.java +++ b/sentry/src/main/java/io/sentry/ScopesAdapter.java @@ -159,6 +159,10 @@ public void removeExtra(@NotNull String key) { return Sentry.pushIsolationScope(); } + /** + * @deprecated please call {@link ISentryLifecycleToken#close()} on the token returned by {@link + * IScopes#pushScope()} or {@link IScopes#pushIsolationScope()} instead. + */ @Override @Deprecated public void popScope() { @@ -195,6 +199,11 @@ public void flush(long timeoutMillis) { Sentry.flush(timeoutMillis); } + /** + * @deprecated please use {@link IScopes#forkedScopes(String)} or {@link + * IScopes#forkedCurrentScope(String)} instead. + */ + @Deprecated @Override @SuppressWarnings("deprecation") public @NotNull IHub clone() { diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index a5aa1da10b..7e4c63b962 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -56,8 +56,7 @@ private Sentry() {} * *

For Android options will also be (temporarily) replaced by SentryAndroid static block. */ - // TODO [HSM] use SentryOptions.empty and address - // https://github.com/getsentry/sentry-java/issues/2541 + // TODO https://github.com/getsentry/sentry-java/issues/2541 private static final @NotNull IScope globalScope = new Scope(SentryOptions.empty()); /** Default value for globalHubMode is false */ @@ -79,6 +78,7 @@ private Sentry() {} /** * Returns the current (threads) hub, if none, clones the rootScopes and returns it. * + * @deprecated please use {@link Sentry#getCurrentScopes()} instead * @return the hub */ @ApiStatus.Internal // exposed for the coroutines integration in SentryContext @@ -127,6 +127,9 @@ private Sentry() {} return getCurrentScopes().forkedCurrentScope(creator); } + /** + * @deprecated please use {@link Sentry#setCurrentScopes} instead. + */ @ApiStatus.Internal // exposed for the coroutines integration in SentryContext @Deprecated @SuppressWarnings({"deprecation", "InlineMeSuggester"}) @@ -275,16 +278,15 @@ private static synchronized void init( options.getLogger().log(SentryLevel.INFO, "GlobalHubMode: '%s'", String.valueOf(globalHubMode)); Sentry.globalHubMode = globalHubMode; - globalScope.replaceOptions(options); final IScopes scopes = getCurrentScopes(); final IScope rootScope = new Scope(options); final IScope rootIsolationScope = new Scope(options); - rootScopes = new Scopes(rootScope, rootIsolationScope, globalScope, "Sentry.init"); - - getScopesStorage().set(rootScopes); scopes.close(true); + globalScope.replaceOptions(options); + rootScopes = new Scopes(rootScope, rootIsolationScope, globalScope, "Sentry.init"); + getScopesStorage().set(rootScopes); globalScope.bindClient(new SentryClient(options)); // If the executorService passed in the init is the same that was previously closed, we have to @@ -520,7 +522,7 @@ private static boolean initConfigurations(final @NotNull SentryOptions options) options.addPerformanceCollector(new JavaMemoryCollector()); } - if (options.isEnableBackpressureHandling()) { + if (options.isEnableBackpressureHandling() && Platform.isJvm()) { options.setBackpressureMonitor(new BackpressureMonitor(options, ScopesAdapter.getInstance())); options.getBackpressureMonitor().start(); } diff --git a/sentry/src/main/java/io/sentry/SentryExceptionFactory.java b/sentry/src/main/java/io/sentry/SentryExceptionFactory.java index 6652ebb504..2808df11c0 100644 --- a/sentry/src/main/java/io/sentry/SentryExceptionFactory.java +++ b/sentry/src/main/java/io/sentry/SentryExceptionFactory.java @@ -12,7 +12,7 @@ import java.util.Deque; import java.util.HashSet; import java.util.List; -import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -136,12 +136,20 @@ public List getSentryExceptions(final @NotNull Throwable throwa @TestOnly @NotNull Deque extractExceptionQueue(final @NotNull Throwable throwable) { - final Deque exceptions = new ArrayDeque<>(); - final Set circularityDetector = new HashSet<>(); + return extractExceptionQueueInternal( + throwable, new AtomicInteger(-1), new HashSet<>(), new ArrayDeque<>()); + } + + Deque extractExceptionQueueInternal( + final @NotNull Throwable throwable, + final @NotNull AtomicInteger exceptionId, + final @NotNull HashSet circularityDetector, + final @NotNull Deque exceptions) { Mechanism exceptionMechanism; Thread thread; Throwable currentThrowable = throwable; + int parentId = exceptionId.get(); // Stack the exceptions to send them in the reverse order while (currentThrowable != null && circularityDetector.add(currentThrowable)) { @@ -155,12 +163,11 @@ Deque extractExceptionQueue(final @NotNull Throwable throwable) thread = exceptionMechanismThrowable.getThread(); snapshot = exceptionMechanismThrowable.isSnapshot(); } else { - exceptionMechanism = null; + exceptionMechanism = new Mechanism(); thread = Thread.currentThread(); } - final boolean includeSentryFrames = - exceptionMechanism != null && Boolean.FALSE.equals(exceptionMechanism.isHandled()); + final boolean includeSentryFrames = Boolean.FALSE.equals(exceptionMechanism.isHandled()); final List frames = sentryStackTraceFactory.getStackFrames( currentThrowable.getStackTrace(), includeSentryFrames); @@ -168,7 +175,28 @@ Deque extractExceptionQueue(final @NotNull Throwable throwable) getSentryException( currentThrowable, exceptionMechanism, thread.getId(), frames, snapshot); exceptions.addFirst(exception); + + if (exceptionMechanism.getType() == null) { + exceptionMechanism.setType("chained"); + } + + if (exceptionId.get() >= 0) { + exceptionMechanism.setParentId(parentId); + } + + final int currentExceptionId = exceptionId.incrementAndGet(); + exceptionMechanism.setExceptionId(currentExceptionId); + + Throwable[] suppressed = currentThrowable.getSuppressed(); + if (suppressed != null && suppressed.length > 0) { + exceptionMechanism.setExceptionGroup(true); + for (Throwable suppressedThrowable : suppressed) { + extractExceptionQueueInternal( + suppressedThrowable, exceptionId, circularityDetector, exceptions); + } + } currentThrowable = currentThrowable.getCause(); + parentId = currentExceptionId; } return exceptions; diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 2ef2e6472e..386bf6fee1 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -25,6 +25,7 @@ import io.sentry.util.thread.IMainThreadChecker; import io.sentry.util.thread.NoOpMainThreadChecker; import java.io.File; +import java.net.Proxy; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -454,10 +455,9 @@ public class SentryOptions { /** Contains a list of monitor slugs for which check-ins should not be sent. */ @ApiStatus.Experimental private @Nullable List ignoredCheckIns = null; - @ApiStatus.Experimental private @NotNull IBackpressureMonitor backpressureMonitor = NoOpBackpressureMonitor.getInstance(); - @ApiStatus.Experimental private boolean enableBackpressureHandling = false; + private boolean enableBackpressureHandling = true; /** Whether to profile app launches, depending on profilesSampler or profilesSampleRate. */ private boolean enableAppStartProfiling = false; @@ -2697,26 +2697,41 @@ public static final class Proxy { private @Nullable String port; private @Nullable String user; private @Nullable String pass; + private @Nullable java.net.Proxy.Type type; + + public Proxy() { + this(null, null, null, null, null); + } + + public Proxy(@Nullable String host, @Nullable String port) { + this(host, port, null, null, null); + } + + public Proxy(@Nullable String host, @Nullable String port, @Nullable java.net.Proxy.Type type) { + this(host, port, type, null, null); + } + + public Proxy( + final @Nullable String host, + final @Nullable String port, + final @Nullable String user, + final @Nullable String pass) { + this(host, port, null, user, pass); + } public Proxy( final @Nullable String host, final @Nullable String port, + final @Nullable java.net.Proxy.Type type, final @Nullable String user, final @Nullable String pass) { this.host = host; this.port = port; + this.type = type; this.user = user; this.pass = pass; } - public Proxy() { - this(null, null, null, null); - } - - public Proxy(@Nullable String host, @Nullable String port) { - this(host, port, null, null); - } - public @Nullable String getHost() { return host; } @@ -2748,6 +2763,14 @@ public void setUser(final @Nullable String user) { public void setPass(final @Nullable String pass) { this.pass = pass; } + + public @Nullable java.net.Proxy.Type getType() { + return type; + } + + public void setType(final @Nullable java.net.Proxy.Type type) { + this.type = type; + } } public static final class Cron { diff --git a/sentry/src/main/java/io/sentry/SentryThreadFactory.java b/sentry/src/main/java/io/sentry/SentryThreadFactory.java index 832ec8ea72..8af5f0aa0c 100644 --- a/sentry/src/main/java/io/sentry/SentryThreadFactory.java +++ b/sentry/src/main/java/io/sentry/SentryThreadFactory.java @@ -105,7 +105,9 @@ List getCurrentThreads( final Thread thread = item.getKey(); final boolean crashed = (thread == currentThread && !ignoreCurrentThread) - || (mechanismThreadIds != null && mechanismThreadIds.contains(thread.getId())); + || (mechanismThreadIds != null + && mechanismThreadIds.contains(thread.getId()) + && !ignoreCurrentThread); result.add(getSentryThread(crashed, item.getValue(), item.getKey())); } diff --git a/sentry/src/main/java/io/sentry/SentryTracer.java b/sentry/src/main/java/io/sentry/SentryTracer.java index f94db947e3..a84f2c20c4 100644 --- a/sentry/src/main/java/io/sentry/SentryTracer.java +++ b/sentry/src/main/java/io/sentry/SentryTracer.java @@ -87,9 +87,9 @@ public SentryTracer( this.baggage = new Baggage(scopes.getOptions().getLogger()); } - // We are currently sending the performance data only in profiles, so there's no point in - // collecting them if a profile is not sampled - if (transactionPerformanceCollector != null && Boolean.TRUE.equals(isProfileSampled())) { + // We are currently sending the performance data only in profiles, but we are always sending + // performance measurements. + if (transactionPerformanceCollector != null) { transactionPerformanceCollector.start(this); } @@ -200,6 +200,13 @@ public void finish( this.finishStatus = FinishStatus.finishing(status); if (!root.isFinished() && (!transactionOptions.isWaitForChildren() || hasAllChildrenFinished())) { + + // 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); + List performanceCollectionData = null; if (transactionPerformanceCollector != null) { performanceCollectionData = transactionPerformanceCollector.stop(this); @@ -217,13 +224,6 @@ public void finish( performanceCollectionData.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); - scopes.configureScope( scope -> { scope.withTransaction( @@ -874,6 +874,11 @@ public void setName(@NotNull String name, @NotNull TransactionNameSource transac this.transactionNameSource = transactionNameSource; } + @Override + public @NotNull TransactionNameSource getNameSource() { + return getTransactionNameSource(); + } + @Override public @NotNull String getName() { return this.name; diff --git a/sentry/src/main/java/io/sentry/SentryWrapper.java b/sentry/src/main/java/io/sentry/SentryWrapper.java index 78505cf297..e4ccefed48 100644 --- a/sentry/src/main/java/io/sentry/SentryWrapper.java +++ b/sentry/src/main/java/io/sentry/SentryWrapper.java @@ -16,30 +16,8 @@ * scope(s) are forked, depends on the method used here. This prevents reused threads (e.g. from * thread-pools) from getting an incorrect state. */ -// TODO [HSM] only deliver isolated variant as default for now public final class SentryWrapper { - /** - * Helper method to wrap {@link Callable} - * - *

Forks current scope before execution and restores previous state afterwards. This prevents - * reused threads (e.g. from thread-pools) from getting an incorrect state. - * - * @param callable - the {@link Callable} to be wrapped - * @return the wrapped {@link Callable} - * @param - the result type of the {@link Callable} - */ - public static Callable wrapCallable(final @NotNull Callable callable) { - final IScopes newScopes = - Sentry.getCurrentScopes().forkedCurrentScope("SentryWrapper.wrapCallable"); - - return () -> { - try (ISentryLifecycleToken ignored = newScopes.makeCurrent()) { - return callable.call(); - } - }; - } - /** * Helper method to wrap {@link Callable} * @@ -50,7 +28,7 @@ public static Callable wrapCallable(final @NotNull Callable callable) * @return the wrapped {@link Callable} * @param - the result type of the {@link Callable} */ - public static Callable wrapCallableIsolated(final @NotNull Callable callable) { + public static Callable wrapCallable(final @NotNull Callable callable) { final IScopes newScopes = Sentry.getCurrentScopes().forkedScopes("SentryWrapper.wrapCallable"); return () -> { @@ -60,26 +38,6 @@ public static Callable wrapCallableIsolated(final @NotNull Callable ca }; } - /** - * Helper method to wrap {@link Supplier} - * - *

Forks current scope before execution and restores previous state afterwards. This prevents - * reused threads (e.g. from thread-pools) from getting an incorrect state. - * - * @param supplier - the {@link Supplier} to be wrapped - * @return the wrapped {@link Supplier} - * @param - the result type of the {@link Supplier} - */ - public static Supplier wrapSupplier(final @NotNull Supplier supplier) { - final IScopes newScopes = Sentry.forkedCurrentScope("SentryWrapper.wrapSupplier"); - - return () -> { - try (ISentryLifecycleToken ignore = newScopes.makeCurrent()) { - return supplier.get(); - } - }; - } - /** * Helper method to wrap {@link Supplier} * @@ -90,7 +48,7 @@ public static Supplier wrapSupplier(final @NotNull Supplier supplier) * @return the wrapped {@link Supplier} * @param - the result type of the {@link Supplier} */ - public static Supplier wrapSupplierIsolated(final @NotNull Supplier supplier) { + public static Supplier wrapSupplier(final @NotNull Supplier supplier) { final IScopes newScopes = Sentry.forkedScopes("SentryWrapper.wrapSupplier"); return () -> { diff --git a/sentry/src/main/java/io/sentry/Span.java b/sentry/src/main/java/io/sentry/Span.java index 3039c0a08a..35bbc42890 100644 --- a/sentry/src/main/java/io/sentry/Span.java +++ b/sentry/src/main/java/io/sentry/Span.java @@ -4,6 +4,7 @@ import io.sentry.protocol.Contexts; import io.sentry.protocol.MeasurementValue; import io.sentry.protocol.SentryId; +import io.sentry.protocol.TransactionNameSource; import io.sentry.util.LazyEvaluator; import io.sentry.util.Objects; import java.util.ArrayList; diff --git a/sentry/src/main/java/io/sentry/SpanContext.java b/sentry/src/main/java/io/sentry/SpanContext.java index 7a25bc8e5b..2d1b8c5fe7 100644 --- a/sentry/src/main/java/io/sentry/SpanContext.java +++ b/sentry/src/main/java/io/sentry/SpanContext.java @@ -150,6 +150,7 @@ public SpanId getParentSpanId() { } public @NotNull String getOperation() { + // TODO [POTEL] use span name here return op; } diff --git a/sentry/src/main/java/io/sentry/SpanDataConvention.java b/sentry/src/main/java/io/sentry/SpanDataConvention.java index b96bf41e66..f8fb82c3c8 100644 --- a/sentry/src/main/java/io/sentry/SpanDataConvention.java +++ b/sentry/src/main/java/io/sentry/SpanDataConvention.java @@ -21,4 +21,6 @@ public interface SpanDataConvention { String FRAMES_SLOW = "frames.slow"; String FRAMES_FROZEN = "frames.frozen"; String FRAMES_DELAY = "frames.delay"; + String CONTRIBUTES_TTID = "ui.contributes_to_ttid"; + String CONTRIBUTES_TTFD = "ui.contributes_to_ttfd"; } diff --git a/sentry/src/main/java/io/sentry/TransactionOptions.java b/sentry/src/main/java/io/sentry/TransactionOptions.java index 4daf9b0464..782e35a039 100644 --- a/sentry/src/main/java/io/sentry/TransactionOptions.java +++ b/sentry/src/main/java/io/sentry/TransactionOptions.java @@ -7,7 +7,7 @@ /** Sentry Transaction options */ public final class TransactionOptions extends SpanOptions { - @ApiStatus.Internal public static final long DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION = 300000; + @ApiStatus.Internal public static final long DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION = 30000; /** * Arbitrary data used in {@link SamplingContext} to determine if transaction is going to be diff --git a/sentry/src/main/java/io/sentry/TypeCheckHint.java b/sentry/src/main/java/io/sentry/TypeCheckHint.java index e9d219c385..cbe784db5b 100644 --- a/sentry/src/main/java/io/sentry/TypeCheckHint.java +++ b/sentry/src/main/java/io/sentry/TypeCheckHint.java @@ -55,6 +55,9 @@ public final class TypeCheckHint { /** Used for GraphQl handler exceptions. */ public static final String GRAPHQL_HANDLER_PARAMETERS = "graphql:handlerParameters"; + /** Used for GraphQl data fetcher breadcrumbs. */ + public static final String GRAPHQL_DATA_FETCHING_ENVIRONMENT = "graphql:dataFetchingEnvironment"; + /** Used for JUL breadcrumbs. */ public static final String JUL_LOG_RECORD = "jul:logRecord"; diff --git a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java index ed25c007f3..f51287d7d9 100644 --- a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java +++ b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java @@ -146,6 +146,9 @@ private DataCategory categoryFromItemType(SentryItemType itemType) { if (SentryItemType.Profile.equals(itemType)) { return DataCategory.Profile; } + if (SentryItemType.Statsd.equals(itemType)) { + return DataCategory.MetricBucket; + } if (SentryItemType.Attachment.equals(itemType)) { return DataCategory.Attachment; } diff --git a/sentry/src/main/java/io/sentry/metrics/LocalMetricsAggregator.java b/sentry/src/main/java/io/sentry/metrics/LocalMetricsAggregator.java index ee0f24eaae..2afbf8e19e 100644 --- a/sentry/src/main/java/io/sentry/metrics/LocalMetricsAggregator.java +++ b/sentry/src/main/java/io/sentry/metrics/LocalMetricsAggregator.java @@ -28,8 +28,7 @@ public void add( final @NotNull String key, final double value, final @Nullable MeasurementUnit unit, - final @Nullable Map tags, - final long timestampMs) { + final @Nullable Map tags) { final @NotNull String exportKey = MetricsHelper.getExportKey(type, key, unit); diff --git a/sentry/src/main/java/io/sentry/metrics/MetricType.java b/sentry/src/main/java/io/sentry/metrics/MetricType.java index 56fb10ee1e..fc816bc25a 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricType.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricType.java @@ -1,12 +1,19 @@ package io.sentry.metrics; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; /** The metric instrument type */ @ApiStatus.Internal public enum MetricType { - Counter, - Gauge, - Distribution, - Set + Counter("c"), + Gauge("g"), + Distribution("d"), + Set("s"); + + final @NotNull String statsdCode; + + MetricType(final @NotNull String statsdCode) { + this.statsdCode = statsdCode; + } } diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java index 0d90c278b2..45273d6885 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java @@ -3,6 +3,8 @@ import io.sentry.IMetricsAggregator; import io.sentry.ISpan; import io.sentry.MeasurementUnit; +import io.sentry.SentryDate; +import io.sentry.SentryNanotimeDate; import java.util.Map; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -425,23 +427,38 @@ public void timing( unit != null ? unit : MeasurementUnit.Duration.SECOND; final @NotNull Map enrichedTags = MetricsHelper.mergeTags(tags, aggregator.getDefaultTagsForMetrics()); - final @Nullable LocalMetricsAggregator localMetricsAggregator = - aggregator.getLocalMetricsAggregator(); + final @Nullable LocalMetricsAggregator localMetricsAggregator; final @Nullable ISpan span = aggregator.startSpanForMetric("metric.timing", key); + // If the span was started, we take its local aggregator, otherwise we request another one. + localMetricsAggregator = + span != null ? span.getLocalMetricsAggregator() : aggregator.getLocalMetricsAggregator(); + if (span != null && tags != null) { for (final @NotNull Map.Entry entry : tags.entrySet()) { span.setTag(entry.getKey(), entry.getValue()); } } + final long timestamp = System.currentTimeMillis(); + final long startNanos = System.nanoTime(); try { - aggregator - .getMetricsAggregator() - .timing(key, callback, durationUnit, enrichedTags, localMetricsAggregator); + callback.run(); } finally { + // If we have a span, the duration we emit is the same of the span, otherwise calculate it. + final long durationNanos; if (span != null) { span.finish(); + // We finish the span, so we should have a finish date, but it's still nullable. + final @NotNull SentryDate spanFinishDate = + span.getFinishDate() != null ? span.getFinishDate() : new SentryNanotimeDate(); + durationNanos = spanFinishDate.diff(span.getStartDate()); + } else { + durationNanos = System.nanoTime() - startNanos; } + final double value = MetricsHelper.convertNanosTo(durationUnit, durationNanos); + aggregator + .getMetricsAggregator() + .distribution(key, value, durationUnit, enrichedTags, timestamp, localMetricsAggregator); } } } diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java b/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java index e501a35552..46e22ca8ad 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java @@ -1,13 +1,11 @@ package io.sentry.metrics; import io.sentry.MeasurementUnit; -import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Random; -import java.util.TimeZone; import java.util.regex.Pattern; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -20,14 +18,9 @@ public final class MetricsHelper { public static final int MAX_TOTAL_WEIGHT = 100000; private static final int ROLLUP_IN_SECONDS = 10; - private static final Pattern INVALID_KEY_CHARACTERS_PATTERN = - Pattern.compile("[^a-zA-Z0-9_/.-]+"); - private static final Pattern INVALID_VALUE_CHARACTERS_PATTERN = - Pattern.compile("[^\\w\\d\\s_:/@\\.\\{\\}\\[\\]$-]+"); - // See - // https://docs.sysdig.com/en/docs/sysdig-monitor/integrations/working-with-integrations/custom-integrations/integrate-statsd-metrics/#characters-allowed-for-statsd-metric-names - private static final Pattern INVALID_METRIC_UNIT_CHARACTERS_PATTERN = - Pattern.compile("[^a-zA-Z0-9_/.]+"); + private static final Pattern UNIT_PATTERN = Pattern.compile("\\W+"); + private static final Pattern NAME_PATTERN = Pattern.compile("[^\\w\\-.]+"); + private static final Pattern TAG_KEY_PATTERN = Pattern.compile("[^\\w\\-./]+"); private static final char TAGS_PAIR_DELIMITER = ','; // Delimiter between key-value pairs private static final char TAGS_KEY_VALUE_DELIMITER = '='; // Delimiter between key and value @@ -36,15 +29,6 @@ public final class MetricsHelper { private static long FLUSH_SHIFT_MS = (long) (new Random().nextFloat() * (ROLLUP_IN_SECONDS * 1000f)); - public static long getDayBucketKey(final @NotNull Calendar timestamp) { - final Calendar utc = Calendar.getInstance(TimeZone.getTimeZone("UTC")); - utc.set(Calendar.YEAR, timestamp.get(Calendar.YEAR)); - utc.set(Calendar.MONTH, timestamp.get(Calendar.MONTH)); - utc.set(Calendar.DAY_OF_MONTH, timestamp.get(Calendar.DAY_OF_MONTH)); - - return utc.getTimeInMillis() / 1000; - } - public static long getTimeBucketKey(final long timestampMs) { final long seconds = timestampMs / 1000; final long bucketKey = (seconds / ROLLUP_IN_SECONDS) * ROLLUP_IN_SECONDS; @@ -59,27 +43,50 @@ public static long getCutoffTimestampMs(final long nowMs) { return nowMs - (ROLLUP_IN_SECONDS * 1000) - FLUSH_SHIFT_MS; } - public static @NotNull String sanitizeKey(final @NotNull String input) { - return INVALID_KEY_CHARACTERS_PATTERN.matcher(input).replaceAll("_"); + @NotNull + public static String sanitizeUnit(final @NotNull String unit) { + return UNIT_PATTERN.matcher(unit).replaceAll(""); } - public static String sanitizeValue(final @NotNull String input) { - return INVALID_VALUE_CHARACTERS_PATTERN.matcher(input).replaceAll(""); + @NotNull + public static String sanitizeName(final @NotNull String input) { + return NAME_PATTERN.matcher(input).replaceAll("_"); } - public static @NotNull String toStatsdType(final @NotNull MetricType type) { - switch (type) { - case Counter: - return "c"; - case Gauge: - return "g"; - case Distribution: - return "d"; - case Set: - return "s"; - default: - throw new IllegalArgumentException("Invalid Metric Type: " + type.name()); + @NotNull + public static String sanitizeTagKey(final @NotNull String input) { + return TAG_KEY_PATTERN.matcher(input).replaceAll(""); + } + + @NotNull + public static String sanitizeTagValue(final @NotNull String input) { + // see https://develop.sentry.dev/sdk/metrics/#tag-values-replacement-map + // Line feed -> \n + // Carriage return -> \r + // Tab -> \t + // Backslash -> \\ + // Pipe -> \\u{7c} + // Comma -> \\u{2c} + final StringBuilder output = new StringBuilder(input.length()); + for (int idx = 0; idx < input.length(); idx++) { + final char ch = input.charAt(idx); + if (ch == '\n') { + output.append("\\n"); + } else if (ch == '\r') { + output.append("\\r"); + } else if (ch == '\t') { + output.append("\\t"); + } else if (ch == '\\') { + output.append("\\\\"); + } else if (ch == '|') { + output.append("\\u{7c}"); + } else if (ch == ',') { + output.append("\\u{2c}"); + } else { + output.append(ch); + } } + return output.toString(); } @NotNull @@ -88,8 +95,8 @@ public static String getMetricBucketKey( final @NotNull String metricKey, final @Nullable MeasurementUnit unit, final @Nullable Map tags) { - final @NotNull String typePrefix = toStatsdType(type); - final @NotNull String serializedTags = GetTagsKey(tags); + final @NotNull String typePrefix = type.statsdCode; + final @NotNull String serializedTags = getTagsKey(tags); final @NotNull String unitName = getUnitName(unit); return String.format("%s_%s_%s_%s", typePrefix, metricKey, unitName, serializedTags); @@ -100,7 +107,8 @@ private static String getUnitName(final @Nullable MeasurementUnit unit) { return (unit != null) ? unit.apiName() : MeasurementUnit.NONE; } - private static String GetTagsKey(final @Nullable Map tags) { + @NotNull + private static String getTagsKey(final @Nullable Map tags) { if (tags == null || tags.isEmpty()) { return ""; } @@ -153,7 +161,7 @@ public static String getExportKey( final @NotNull String key, final @Nullable MeasurementUnit unit) { final @NotNull String unitName = getUnitName(unit); - return String.format("%s:%s@%s", toStatsdType(type), key, unitName); + return String.format("%s:%s@%s", type.statsdCode, key, unitName); } public static double convertNanosTo( @@ -197,7 +205,7 @@ public static void encodeMetrics( final @NotNull Collection metrics, final @NotNull StringBuilder writer) { for (Metric metric : metrics) { - writer.append(sanitizeKey(metric.getKey())); + writer.append(sanitizeName(metric.getKey())); writer.append("@"); final @Nullable MeasurementUnit unit = metric.getUnit(); @@ -211,14 +219,14 @@ public static void encodeMetrics( } writer.append("|"); - writer.append(toStatsdType(metric.getType())); + writer.append(metric.getType().statsdCode); final @Nullable Map tags = metric.getTags(); if (tags != null) { writer.append("|#"); boolean first = true; for (final @NotNull Map.Entry tag : tags.entrySet()) { - final @NotNull String tagKey = sanitizeKey(tag.getKey()); + final @NotNull String tagKey = sanitizeTagKey(tag.getKey()); if (first) { first = false; } else { @@ -226,7 +234,7 @@ public static void encodeMetrics( } writer.append(tagKey); writer.append(":"); - writer.append(sanitizeValue(tag.getValue())); + writer.append(sanitizeTagValue(tag.getValue())); } } @@ -236,11 +244,6 @@ public static void encodeMetrics( } } - @NotNull - public static String sanitizeUnit(@NotNull String unit) { - return INVALID_METRIC_UNIT_CHARACTERS_PATTERN.matcher(unit).replaceAll("_"); - } - @NotNull public static Map mergeTags( final @Nullable Map tags, final @NotNull Map defaultTags) { diff --git a/sentry/src/main/java/io/sentry/metrics/NoopMetricsAggregator.java b/sentry/src/main/java/io/sentry/metrics/NoopMetricsAggregator.java index 8e917006d2..f9038062b5 100644 --- a/sentry/src/main/java/io/sentry/metrics/NoopMetricsAggregator.java +++ b/sentry/src/main/java/io/sentry/metrics/NoopMetricsAggregator.java @@ -75,16 +75,6 @@ public void set( // no-op } - @Override - public void timing( - final @NotNull String key, - final @NotNull Runnable callback, - final @NotNull MeasurementUnit.Duration unit, - final @Nullable Map tags, - final @Nullable LocalMetricsAggregator localMetricsAggregator) { - callback.run(); - } - @Override public void flush(final boolean force) { // no-op diff --git a/sentry/src/main/java/io/sentry/protocol/App.java b/sentry/src/main/java/io/sentry/protocol/App.java index b7b41638db..bec57d22f3 100644 --- a/sentry/src/main/java/io/sentry/protocol/App.java +++ b/sentry/src/main/java/io/sentry/protocol/App.java @@ -40,8 +40,10 @@ public final class App implements JsonUnknown, JsonSerializable { private @Nullable String appBuild; /** Application permissions in the form of "permission_name" : "granted|not_granted" */ private @Nullable Map permissions; - /** The list of the visibile UI screens * */ + /** The list of the visible UI screens * */ private @Nullable List viewNames; + /** the app start type */ + private @Nullable String startType; /** * A flag indicating whether the app is in foreground or not. An app is in foreground when it's * visible to the user. @@ -61,6 +63,7 @@ public App() {} this.permissions = CollectionUtils.newConcurrentHashMap(app.permissions); this.inForeground = app.inForeground; this.viewNames = CollectionUtils.newArrayList(app.viewNames); + this.startType = app.startType; this.unknown = CollectionUtils.newConcurrentHashMap(app.unknown); } @@ -151,6 +154,15 @@ public void setViewNames(final @Nullable List viewNames) { this.viewNames = viewNames; } + @Nullable + public String getStartType() { + return startType; + } + + public void setStartType(final @Nullable String startType) { + this.startType = startType; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -165,7 +177,8 @@ public boolean equals(Object o) { && Objects.equals(appBuild, app.appBuild) && Objects.equals(permissions, app.permissions) && Objects.equals(inForeground, app.inForeground) - && Objects.equals(viewNames, app.viewNames); + && Objects.equals(viewNames, app.viewNames) + && Objects.equals(startType, app.startType); } @Override @@ -180,7 +193,8 @@ public int hashCode() { appBuild, permissions, inForeground, - viewNames); + viewNames, + startType); } // region json @@ -207,6 +221,7 @@ public static final class JsonKeys { public static final String APP_PERMISSIONS = "permissions"; public static final String IN_FOREGROUND = "in_foreground"; public static final String VIEW_NAMES = "view_names"; + public static final String START_TYPE = "start_type"; } @Override @@ -243,6 +258,9 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger if (viewNames != null) { writer.name(JsonKeys.VIEW_NAMES).value(logger, viewNames); } + if (startType != null) { + writer.name(JsonKeys.START_TYPE).value(startType); + } if (unknown != null) { for (String key : unknown.keySet()) { Object value = unknown.get(key); @@ -298,6 +316,9 @@ public static final class Deserializer implements JsonDeserializer { app.setViewNames(viewNames); } break; + case JsonKeys.START_TYPE: + app.startType = reader.nextStringOrNull(); + break; default: if (unknown == null) { unknown = new ConcurrentHashMap<>(); diff --git a/sentry/src/main/java/io/sentry/protocol/Mechanism.java b/sentry/src/main/java/io/sentry/protocol/Mechanism.java index 648aed39c2..8fc9aedf77 100644 --- a/sentry/src/main/java/io/sentry/protocol/Mechanism.java +++ b/sentry/src/main/java/io/sentry/protocol/Mechanism.java @@ -67,6 +67,18 @@ public final class Mechanism implements JsonUnknown, JsonSerializable { * for grouping or display purposes. */ private @Nullable Boolean synthetic; + /** + * Exception ID. Used. e.g. for exception groups to build a hierarchy. This is referenced as + * parent by child exceptions which for Java SDK means Throwable.getSuppressed(). + */ + private @Nullable Integer exceptionId; + /** Parent exception ID. Used e.g. for exception groups to build a hierarchy. */ + private @Nullable Integer parentId; + /** + * Whether this is a group of exceptions. For Java SDK this means there were suppressed + * exceptions. + */ + private @Nullable Boolean exceptionGroup; @SuppressWarnings("unused") private @Nullable Map unknown; @@ -140,6 +152,30 @@ public void setSynthetic(final @Nullable Boolean synthetic) { this.synthetic = synthetic; } + public @Nullable Integer getExceptionId() { + return exceptionId; + } + + public void setExceptionId(final @Nullable Integer exceptionId) { + this.exceptionId = exceptionId; + } + + public @Nullable Integer getParentId() { + return parentId; + } + + public void setParentId(final @Nullable Integer parentId) { + this.parentId = parentId; + } + + public @Nullable Boolean isExceptionGroup() { + return exceptionGroup; + } + + public void setExceptionGroup(final @Nullable Boolean exceptionGroup) { + this.exceptionGroup = exceptionGroup; + } + // JsonKeys public static final class JsonKeys { @@ -150,6 +186,9 @@ public static final class JsonKeys { public static final String META = "meta"; public static final String DATA = "data"; public static final String SYNTHETIC = "synthetic"; + public static final String EXCEPTION_ID = "exception_id"; + public static final String PARENT_ID = "parent_id"; + public static final String IS_EXCEPTION_GROUP = "is_exception_group"; } // JsonUnknown @@ -191,6 +230,15 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger if (synthetic != null) { writer.name(JsonKeys.SYNTHETIC).value(synthetic); } + if (exceptionId != null) { + writer.name(JsonKeys.EXCEPTION_ID).value(logger, exceptionId); + } + if (parentId != null) { + writer.name(JsonKeys.PARENT_ID).value(logger, parentId); + } + if (exceptionGroup != null) { + writer.name(JsonKeys.IS_EXCEPTION_GROUP).value(exceptionGroup); + } if (unknown != null) { for (String key : unknown.keySet()) { Object value = unknown.get(key); @@ -238,6 +286,15 @@ public static final class Deserializer implements JsonDeserializer { case JsonKeys.SYNTHETIC: mechanism.synthetic = reader.nextBooleanOrNull(); break; + case JsonKeys.EXCEPTION_ID: + mechanism.exceptionId = reader.nextIntegerOrNull(); + break; + case JsonKeys.PARENT_ID: + mechanism.parentId = reader.nextIntegerOrNull(); + break; + case JsonKeys.IS_EXCEPTION_GROUP: + mechanism.exceptionGroup = reader.nextBooleanOrNull(); + break; default: if (unknown == null) { unknown = new HashMap<>(); diff --git a/sentry/src/main/java/io/sentry/protocol/SentrySpan.java b/sentry/src/main/java/io/sentry/protocol/SentrySpan.java index 4c91c8fa00..2be4411d44 100644 --- a/sentry/src/main/java/io/sentry/protocol/SentrySpan.java +++ b/sentry/src/main/java/io/sentry/protocol/SentrySpan.java @@ -40,7 +40,7 @@ public final class SentrySpan implements JsonUnknown, JsonSerializable { private final @Nullable String origin; private final @NotNull Map tags; - private final @Nullable Map data; + private @Nullable Map data; private final @NotNull Map measurements; private final @Nullable Map> metricsSummaries; @@ -159,6 +159,10 @@ public boolean isFinished() { return data; } + public void setData(final @Nullable Map data) { + this.data = data; + } + public @Nullable String getOrigin() { return origin; } diff --git a/sentry/src/main/java/io/sentry/protocol/SentryTransaction.java b/sentry/src/main/java/io/sentry/protocol/SentryTransaction.java index 39e7704bd5..0ca789270e 100644 --- a/sentry/src/main/java/io/sentry/protocol/SentryTransaction.java +++ b/sentry/src/main/java/io/sentry/protocol/SentryTransaction.java @@ -225,7 +225,7 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger writer.name(JsonKeys.MEASUREMENTS).value(logger, measurements); } if (metricSummaries != null && !metricSummaries.isEmpty()) { - writer.name(SentrySpan.JsonKeys.METRICS_SUMMARY).value(logger, metricSummaries); + writer.name(JsonKeys.METRICS_SUMMARY).value(logger, metricSummaries); } writer.name(JsonKeys.TRANSACTION_INFO).value(logger, transactionInfo); new SentryBaseEvent.Serializer().serialize(this, writer, logger); diff --git a/sentry/src/main/java/io/sentry/transport/HttpConnection.java b/sentry/src/main/java/io/sentry/transport/HttpConnection.java index fb471cd902..5105c74af5 100644 --- a/sentry/src/main/java/io/sentry/transport/HttpConnection.java +++ b/sentry/src/main/java/io/sentry/transport/HttpConnection.java @@ -78,8 +78,14 @@ public HttpConnection( final String host = optionsProxy.getHost(); if (port != null && host != null) { try { + final @NotNull Proxy.Type type; + if (optionsProxy.getType() != null) { + type = optionsProxy.getType(); + } else { + type = Proxy.Type.HTTP; + } InetSocketAddress proxyAddr = new InetSocketAddress(host, Integer.parseInt(port)); - proxy = new Proxy(Proxy.Type.HTTP, proxyAddr); + proxy = new Proxy(type, proxyAddr); } catch (NumberFormatException e) { options .getLogger() diff --git a/sentry/src/main/java/io/sentry/transport/RateLimiter.java b/sentry/src/main/java/io/sentry/transport/RateLimiter.java index 4e9fde8a95..c4598820ab 100644 --- a/sentry/src/main/java/io/sentry/transport/RateLimiter.java +++ b/sentry/src/main/java/io/sentry/transport/RateLimiter.java @@ -12,6 +12,7 @@ import io.sentry.clientreport.DiscardReason; import io.sentry.hints.Retryable; import io.sentry.hints.SubmissionResult; +import io.sentry.util.CollectionUtils; import io.sentry.util.HintUtils; import io.sentry.util.StringUtils; import java.util.ArrayList; @@ -168,6 +169,10 @@ private boolean isRetryAfter(final @NotNull String itemType) { return DataCategory.Attachment; case "profile": return DataCategory.Profile; + // The envelope item type used for metrics is statsd, whereas the client report category is + // metric_bucket + case "statsd": + return DataCategory.MetricBucket; case "transaction": return DataCategory.Transaction; case "check_in": @@ -189,21 +194,25 @@ public void updateRetryAfterLimits( final @Nullable String sentryRateLimitHeader, final @Nullable String retryAfterHeader, final int errorCode) { + // example: 2700:metric_bucket:organization:quota_exceeded:custom,... if (sentryRateLimitHeader != null) { for (String limit : sentryRateLimitHeader.split(",", -1)) { // Java 11 or so has strip() :( limit = limit.replace(" ", ""); - final String[] retryAfterAndCategories = - limit.split(":", -1); // we only need for 1st and 2nd item though. + final String[] rateLimit = limit.split(":", -1); + // These can be ignored by the SDK. + // final String scope = rateLimit.length > 2 ? rateLimit[2] : null; + // final String reasonCode = rateLimit.length > 3 ? rateLimit[3] : null; + final @Nullable String limitNamespaces = rateLimit.length > 4 ? rateLimit[4] : null; - if (retryAfterAndCategories.length > 0) { - final String retryAfter = retryAfterAndCategories[0]; + if (rateLimit.length > 0) { + final String retryAfter = rateLimit[0]; long retryAfterMillis = parseRetryAfterOrDefault(retryAfter); - if (retryAfterAndCategories.length > 1) { - final String allCategories = retryAfterAndCategories[1]; + if (rateLimit.length > 1) { + final String allCategories = rateLimit[1]; // we dont care if Date is UTC as we just add the relative seconds final Date date = @@ -215,7 +224,7 @@ public void updateRetryAfterLimits( for (final String catItem : categories) { DataCategory dataCategory = DataCategory.Unknown; try { - final String catItemCapitalized = StringUtils.capitalize(catItem); + final String catItemCapitalized = StringUtils.camelCase(catItem); if (catItemCapitalized != null) { dataCategory = DataCategory.valueOf(catItemCapitalized); } else { @@ -228,6 +237,18 @@ public void updateRetryAfterLimits( if (DataCategory.Unknown.equals(dataCategory)) { continue; } + // SDK doesn't support namespaces, yet. Namespaces can be returned by relay in case + // of metric_bucket items. If the namespaces are empty or contain "custom" we apply + // the rate limit to all metrics, otherwise to none. + if (DataCategory.MetricBucket.equals(dataCategory) + && limitNamespaces != null + && !limitNamespaces.equals("")) { + final String[] namespaces = limitNamespaces.split(";", -1); + if (namespaces.length > 0 && !CollectionUtils.contains(namespaces, "custom")) { + continue; + } + } + applyRetryAfterOnlyIfLonger(dataCategory, date); } } else { diff --git a/sentry/src/main/java/io/sentry/util/CollectionUtils.java b/sentry/src/main/java/io/sentry/util/CollectionUtils.java index 7dd54ef2bf..f3a0e1d9d7 100644 --- a/sentry/src/main/java/io/sentry/util/CollectionUtils.java +++ b/sentry/src/main/java/io/sentry/util/CollectionUtils.java @@ -145,6 +145,22 @@ public static int size(final @NotNull Iterable data) { return filteredList; } + /** + * Returns true if the element is present in the array, false otherwise. + * + * @param array - the array + * @param element - the element + * @return true if the element is present in the array, false otherwise. + */ + public static boolean contains(final @NotNull T[] array, final @NotNull T element) { + for (final T t : array) { + if (element.equals(t)) { + return true; + } + } + return false; + } + /** * A simplified copy of Java 8 Predicate. * diff --git a/sentry/src/main/java/io/sentry/util/StringUtils.java b/sentry/src/main/java/io/sentry/util/StringUtils.java index ac95f9ad70..e4bf618501 100644 --- a/sentry/src/main/java/io/sentry/util/StringUtils.java +++ b/sentry/src/main/java/io/sentry/util/StringUtils.java @@ -10,6 +10,7 @@ import java.text.StringCharacterIterator; import java.util.Iterator; import java.util.Locale; +import java.util.regex.Pattern; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -21,6 +22,7 @@ public final class StringUtils { private static final String CORRUPTED_NIL_UUID = "0000-0000"; private static final String PROPER_NIL_UUID = "00000000-0000-0000-0000-000000000000"; + private static final @NotNull Pattern PATTERN_WORD_SNAKE_CASE = Pattern.compile("[\\W_]+"); private StringUtils() {} @@ -50,6 +52,25 @@ private StringUtils() {} return str.substring(0, 1).toUpperCase(Locale.ROOT) + str.substring(1).toLowerCase(Locale.ROOT); } + /** + * Converts a String to CamelCase format. E.g. metric_bucket => MetricBucket; + * + * @param str the String to convert + * @return the camel case converted String or itself if empty or null + */ + public static @Nullable String camelCase(final @Nullable String str) { + if (str == null || str.isEmpty()) { + return str; + } + + String[] words = PATTERN_WORD_SNAKE_CASE.split(str, -1); + StringBuilder builder = new StringBuilder(); + for (String w : words) { + builder.append(capitalize(w)); + } + return builder.toString(); + } + /** * Removes character specified by the delimiter parameter from the beginning and the end of the * string. diff --git a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt index b9bb296b89..04c181b194 100644 --- a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt @@ -269,9 +269,9 @@ class ExternalOptionsTest { } @Test - fun `creates options with enableBackpressureHandling set to true`() { - withPropertiesFile("enable-backpressure-handling=true") { options -> - assertTrue(options.isEnableBackpressureHandling == true) + fun `creates options with enableBackpressureHandling set to false`() { + withPropertiesFile("enable-backpressure-handling=false") { options -> + assertTrue(options.isEnableBackpressureHandling == false) } } diff --git a/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt b/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt index bd131bb4fa..1b2b2f36e1 100644 --- a/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt +++ b/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt @@ -268,21 +268,12 @@ class MetricsAggregatorTest { 20_001, null ) - aggregator.timing( - "name0", - { - Thread.sleep(2) - }, - MeasurementUnit.Duration.SECOND, - mapOf("key0" to "value0"), - null - ) aggregator.flush(true) verify(fixture.client).captureMetrics( check { val metrics = MetricsHelperTest.parseMetrics(it.encodeToStatsd()) - assertEquals(6, metrics.size) + assertEquals(5, metrics.size) } ) } @@ -309,13 +300,18 @@ class MetricsAggregatorTest { // then a flush is scheduled assertTrue(fixture.executorService.hasScheduledRunnables()) + // flush is executed, but there are other metric to capture and it's scheduled again + fixture.executorService.runAll() + verify(fixture.client, never()).captureMetrics(any()) + assertTrue(fixture.executorService.hasScheduledRunnables()) + // after the flush is executed, the metric is captured fixture.currentTimeMillis = 31_000 fixture.executorService.runAll() verify(fixture.client).captureMetrics(any()) - // and flushing is scheduled again - assertTrue(fixture.executorService.hasScheduledRunnables()) + // there is no other metric to capture, so flush is not scheduled again + assertFalse(fixture.executorService.hasScheduledRunnables()) } @Test @@ -347,8 +343,7 @@ class MetricsAggregatorTest { key, value, unit, - tags, - timestamp + tags ) } @@ -382,8 +377,7 @@ class MetricsAggregatorTest { key, 1.0, unit, - tags, - timestamp + tags ) // if the same set metric is emitted again @@ -403,8 +397,7 @@ class MetricsAggregatorTest { key, 0.0, unit, - tags, - timestamp + tags ) } @@ -515,4 +508,12 @@ class MetricsAggregatorTest { aggregator.flush(true) verify(fixture.client, never()).captureMetrics(any()) } + + @Test + fun `if before emit throws, metric is emitted`() { + val aggregator = fixture.getSut(beforeEmitMetricCallback = { key, tags -> throw RuntimeException() }) + aggregator.increment("key", 1.0, null, null, 20_001, null) + aggregator.flush(true) + verify(fixture.client).captureMetrics(any()) + } } diff --git a/sentry/src/test/java/io/sentry/SentryExceptionFactoryTest.kt b/sentry/src/test/java/io/sentry/SentryExceptionFactoryTest.kt index 888f17e0a3..8eb7f0e42d 100644 --- a/sentry/src/test/java/io/sentry/SentryExceptionFactoryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryExceptionFactoryTest.kt @@ -209,6 +209,193 @@ class SentryExceptionFactoryTest { assertEquals(777, frame.lineno) } + @Test + fun `when exception with mechanism suppressed exceptions, add them and show as group`() { + val exception = Exception("message") + val suppressedException = Exception("suppressed") + exception.addSuppressed(suppressedException) + + val mechanism = Mechanism() + mechanism.type = "ANR" + val thread = Thread() + val throwable = ExceptionMechanismException(mechanism, exception, thread) + + val queue = fixture.getSut().extractExceptionQueue(throwable) + + val suppressedInQueue = queue.pop() + val mainInQueue = queue.pop() + + assertEquals("suppressed", suppressedInQueue.value) + assertEquals(1, suppressedInQueue.mechanism?.exceptionId) + assertEquals(0, suppressedInQueue.mechanism?.parentId) + + assertEquals("message", mainInQueue.value) + assertEquals(0, mainInQueue.mechanism?.exceptionId) + assertEquals(true, mainInQueue.mechanism?.isExceptionGroup) + } + + @Test + fun `nested exception that contains suppressed exceptions is marked as group`() { + val exception = Exception("inner") + val suppressedException = Exception("suppressed") + exception.addSuppressed(suppressedException) + + val outerException = Exception("outer", exception) + + val queue = fixture.getSut().extractExceptionQueue(outerException) + + val suppressedInQueue = queue.pop() + val mainInQueue = queue.pop() + val outerInQueue = queue.pop() + + assertEquals("suppressed", suppressedInQueue.value) + assertEquals(2, suppressedInQueue.mechanism?.exceptionId) + assertEquals(1, suppressedInQueue.mechanism?.parentId) + + assertEquals("inner", mainInQueue.value) + assertEquals(1, mainInQueue.mechanism?.exceptionId) + assertEquals(0, mainInQueue.mechanism?.parentId) + assertEquals(true, mainInQueue.mechanism?.isExceptionGroup) + + assertEquals("outer", outerInQueue.value) + assertEquals(0, outerInQueue.mechanism?.exceptionId) + assertNull(outerInQueue.mechanism?.parentId) + assertNull(outerInQueue.mechanism?.isExceptionGroup) + } + + @Test + fun `nested exception within Mechanism that contains suppressed exceptions is marked as group`() { + val exception = Exception("inner") + val suppressedException = Exception("suppressed") + exception.addSuppressed(suppressedException) + + val mechanism = Mechanism() + mechanism.type = "ANR" + val thread = Thread() + + val outerException = ExceptionMechanismException(mechanism, Exception("outer", exception), thread) + + val queue = fixture.getSut().extractExceptionQueue(outerException) + + val suppressedInQueue = queue.pop() + val mainInQueue = queue.pop() + val outerInQueue = queue.pop() + + assertEquals("suppressed", suppressedInQueue.value) + assertEquals(2, suppressedInQueue.mechanism?.exceptionId) + assertEquals(1, suppressedInQueue.mechanism?.parentId) + + assertEquals("inner", mainInQueue.value) + assertEquals(1, mainInQueue.mechanism?.exceptionId) + assertEquals(0, mainInQueue.mechanism?.parentId) + assertEquals(true, mainInQueue.mechanism?.isExceptionGroup) + + assertEquals("outer", outerInQueue.value) + assertEquals(0, outerInQueue.mechanism?.exceptionId) + assertNull(outerInQueue.mechanism?.parentId) + assertNull(outerInQueue.mechanism?.isExceptionGroup) + } + + @Test + fun `nested exception with nested exception that contain suppressed exceptions are marked as group`() { + val innerMostException = Exception("innermost") + val innerMostSuppressed = Exception("innermostSuppressed") + innerMostException.addSuppressed(innerMostSuppressed) + + val innerException = Exception("inner", innerMostException) + val innerSuppressed = Exception("suppressed") + innerException.addSuppressed(innerSuppressed) + + val outerException = Exception("outer", innerException) + + val queue = fixture.getSut().extractExceptionQueue(outerException) + + val innerMostSuppressedInQueue = queue.pop() + val innerMostExceptionInQueue = queue.pop() + val innerSuppressedInQueue = queue.pop() + val innerExceptionInQueue = queue.pop() + val outerInQueue = queue.pop() + + assertEquals("innermostSuppressed", innerMostSuppressedInQueue.value) + assertEquals(4, innerMostSuppressedInQueue.mechanism?.exceptionId) + assertEquals(3, innerMostSuppressedInQueue.mechanism?.parentId) + assertNull(innerMostSuppressedInQueue.mechanism?.isExceptionGroup) + + assertEquals("innermost", innerMostExceptionInQueue.value) + assertEquals(3, innerMostExceptionInQueue.mechanism?.exceptionId) + assertEquals(1, innerMostExceptionInQueue.mechanism?.parentId) + assertEquals(true, innerMostExceptionInQueue.mechanism?.isExceptionGroup) + + assertEquals("suppressed", innerSuppressedInQueue.value) + assertEquals(2, innerSuppressedInQueue.mechanism?.exceptionId) + assertEquals(1, innerSuppressedInQueue.mechanism?.parentId) + assertNull(innerSuppressedInQueue.mechanism?.isExceptionGroup) + + assertEquals("inner", innerExceptionInQueue.value) + assertEquals(1, innerExceptionInQueue.mechanism?.exceptionId) + assertEquals(0, innerExceptionInQueue.mechanism?.parentId) + assertEquals(true, innerExceptionInQueue.mechanism?.isExceptionGroup) + + assertEquals("outer", outerInQueue.value) + assertEquals(0, outerInQueue.mechanism?.exceptionId) + assertNull(outerInQueue.mechanism?.parentId) + assertNull(outerInQueue.mechanism?.isExceptionGroup) + } + + @Test + fun `nested exception with nested exception that contain suppressed exceptions with a nested exception are marked as group`() { + val innerMostException = Exception("innermost") + + val innerMostSuppressedNestedException = Exception("innermostSuppressedNested") + val innerMostSuppressed = Exception("innermostSuppressed", innerMostSuppressedNestedException) + innerMostException.addSuppressed(innerMostSuppressed) + + val innerException = Exception("inner", innerMostException) + val innerSuppressed = Exception("suppressed") + innerException.addSuppressed(innerSuppressed) + + val outerException = Exception("outer", innerException) + + val queue = fixture.getSut().extractExceptionQueue(outerException) + + val innerMostSuppressedNestedExceptionInQueue = queue.pop() + val innerMostSuppressedInQueue = queue.pop() + val innerMostExceptionInQueue = queue.pop() + val innerSuppressedInQueue = queue.pop() + val innerExceptionInQueue = queue.pop() + val outerInQueue = queue.pop() + + assertEquals("innermostSuppressedNested", innerMostSuppressedNestedExceptionInQueue.value) + assertEquals(5, innerMostSuppressedNestedExceptionInQueue.mechanism?.exceptionId) + assertEquals(4, innerMostSuppressedNestedExceptionInQueue.mechanism?.parentId) + assertNull(innerMostSuppressedNestedExceptionInQueue.mechanism?.isExceptionGroup) + + assertEquals("innermostSuppressed", innerMostSuppressedInQueue.value) + assertEquals(4, innerMostSuppressedInQueue.mechanism?.exceptionId) + assertEquals(3, innerMostSuppressedInQueue.mechanism?.parentId) + assertNull(innerMostSuppressedInQueue.mechanism?.isExceptionGroup) + + assertEquals("innermost", innerMostExceptionInQueue.value) + assertEquals(3, innerMostExceptionInQueue.mechanism?.exceptionId) + assertEquals(1, innerMostExceptionInQueue.mechanism?.parentId) + assertEquals(true, innerMostExceptionInQueue.mechanism?.isExceptionGroup) + + assertEquals("suppressed", innerSuppressedInQueue.value) + assertEquals(2, innerSuppressedInQueue.mechanism?.exceptionId) + assertEquals(1, innerSuppressedInQueue.mechanism?.parentId) + assertNull(innerSuppressedInQueue.mechanism?.isExceptionGroup) + + assertEquals("inner", innerExceptionInQueue.value) + assertEquals(1, innerExceptionInQueue.mechanism?.exceptionId) + assertEquals(0, innerExceptionInQueue.mechanism?.parentId) + assertEquals(true, innerExceptionInQueue.mechanism?.isExceptionGroup) + + assertEquals("outer", outerInQueue.value) + assertEquals(0, outerInQueue.mechanism?.exceptionId) + assertNull(outerInQueue.mechanism?.parentId) + assertNull(outerInQueue.mechanism?.isExceptionGroup) + } + internal class InnerClassThrowable constructor(cause: Throwable? = null) : Throwable(cause) private val anonymousException = object : Exception() { diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 162ce184a5..b474d4e4e0 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -1,9 +1,9 @@ package io.sentry -import io.sentry.backpressure.NoOpBackpressureMonitor import io.sentry.util.StringUtils import org.mockito.kotlin.mock import java.io.File +import java.net.Proxy import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -350,7 +350,7 @@ class SentryOptionsTest { externalOptions.environment = "environment" externalOptions.release = "release" externalOptions.serverName = "serverName" - externalOptions.proxy = SentryOptions.Proxy("example.com", "8090") + externalOptions.proxy = SentryOptions.Proxy("example.com", "8090", Proxy.Type.SOCKS) externalOptions.setTag("tag1", "value1") externalOptions.setTag("tag2", "value2") externalOptions.enableUncaughtExceptionHandler = false @@ -370,7 +370,7 @@ class SentryOptionsTest { externalOptions.isEnablePrettySerializationOutput = false externalOptions.isSendModules = false externalOptions.ignoredCheckIns = listOf("slug1", "slug-B") - externalOptions.isEnableBackpressureHandling = true + externalOptions.isEnableBackpressureHandling = false externalOptions.cron = SentryOptions.Cron().apply { defaultCheckinMargin = 10L defaultMaxRuntime = 30L @@ -391,6 +391,7 @@ class SentryOptionsTest { assertNotNull(options.proxy) assertEquals("example.com", options.proxy!!.host) assertEquals("8090", options.proxy!!.port) + assertEquals(java.net.Proxy.Type.SOCKS, options.proxy!!.type) assertEquals(mapOf("tag1" to "value1", "tag2" to "value2"), options.tags) assertFalse(options.isEnableUncaughtExceptionHandler) assertEquals(true, options.enableTracing) @@ -407,7 +408,7 @@ class SentryOptionsTest { assertFalse(options.isEnablePrettySerializationOutput) assertFalse(options.isSendModules) assertEquals(listOf("slug1", "slug-B"), options.ignoredCheckIns) - assertTrue(options.isEnableBackpressureHandling) + assertFalse(options.isEnableBackpressureHandling) assertNotNull(options.cron) assertEquals(10L, options.cron?.defaultCheckinMargin) assertEquals(30L, options.cron?.defaultMaxRuntime) @@ -563,9 +564,8 @@ class SentryOptionsTest { } @Test - fun `when options are initialized, enableBackpressureHandling is set to false by default`() { - assertFalse(SentryOptions().isEnableBackpressureHandling) - assertTrue(SentryOptions().backpressureMonitor is NoOpBackpressureMonitor) + fun `when options are initialized, enableBackpressureHandling is set to true by default`() { + assertTrue(SentryOptions().isEnableBackpressureHandling) } @Test diff --git a/sentry/src/test/java/io/sentry/SentryTracerTest.kt b/sentry/src/test/java/io/sentry/SentryTracerTest.kt index d3404a853c..03e8149464 100644 --- a/sentry/src/test/java/io/sentry/SentryTracerTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTracerTest.kt @@ -1005,9 +1005,9 @@ class SentryTracerTest { } @Test - fun `when transaction is created, but not profiled, transactionPerformanceCollector is not started`() { + fun `when transaction is created, but not profiled, transactionPerformanceCollector is started anyway`() { val transaction = fixture.getSut() - verify(fixture.transactionPerformanceCollector, never()).start(anyOrNull()) + verify(fixture.transactionPerformanceCollector).start(anyOrNull()) } @Test diff --git a/sentry/src/test/java/io/sentry/SentryWrapperTest.kt b/sentry/src/test/java/io/sentry/SentryWrapperTest.kt index a830446428..6fd32ac57b 100644 --- a/sentry/src/test/java/io/sentry/SentryWrapperTest.kt +++ b/sentry/src/test/java/io/sentry/SentryWrapperTest.kt @@ -27,176 +27,7 @@ class SentryWrapperTest { } @Test - fun `scopes are reset to its state within the thread after supply is done`() { - Sentry.init { - it.dsn = dsn - it.beforeSend = SentryOptions.BeforeSendCallback { event, hint -> - event - } - } - - val mainScopes = Sentry.getCurrentScopes() - val threadedScopes = mainScopes.forkedCurrentScope("test") - - executor.submit { - Sentry.setCurrentScopes(threadedScopes) - }.get() - - assertEquals(mainScopes, Sentry.getCurrentScopes()) - - val callableFuture = - CompletableFuture.supplyAsync( - SentryWrapper.wrapSupplierIsolated { - assertNotEquals(mainScopes, Sentry.getCurrentScopes()) - assertNotEquals(threadedScopes, Sentry.getCurrentScopes()) - "Result 1" - }, - executor - ) - - callableFuture.join() - - executor.submit { - assertNotEquals(mainScopes, Sentry.getCurrentScopes()) - assertEquals(threadedScopes, Sentry.getCurrentScopes()) - }.get() - } - - @Test - fun `wrapped supply async does not isolate Scopes`() { - val capturedEvents = mutableListOf() - - Sentry.init { - it.dsn = dsn - it.beforeSend = SentryOptions.BeforeSendCallback { event, hint -> - capturedEvents.add(event) - event - } - } - - Sentry.addBreadcrumb("MyOriginalBreadcrumbBefore") - Sentry.captureMessage("OriginalMessageBefore") - - val callableFuture = - CompletableFuture.supplyAsync( - SentryWrapper.wrapSupplier { - Thread.sleep(20) - Sentry.addBreadcrumb("MyClonedBreadcrumb") - Sentry.captureMessage("ClonedMessage") - "Result 1" - }, - executor - ) - - val callableFuture2 = - CompletableFuture.supplyAsync( - SentryWrapper.wrapSupplier { - Thread.sleep(10) - Sentry.addBreadcrumb("MyClonedBreadcrumb2") - Sentry.captureMessage("ClonedMessage2") - "Result 2" - }, - executor - ) - - Sentry.addBreadcrumb("MyOriginalBreadcrumb") - Sentry.captureMessage("OriginalMessage") - - callableFuture.join() - callableFuture2.join() - - val mainEvent = capturedEvents.firstOrNull { it.message?.formatted == "OriginalMessage" } - val clonedEvent = capturedEvents.firstOrNull { it.message?.formatted == "ClonedMessage" } - val clonedEvent2 = capturedEvents.firstOrNull { it.message?.formatted == "ClonedMessage2" } - - assertEquals(2, mainEvent?.breadcrumbs?.size) - assertEquals(3, clonedEvent?.breadcrumbs?.size) - assertEquals(4, clonedEvent2?.breadcrumbs?.size) - } - - @Test - fun `wrapped callable does not isolate Scopes`() { - val capturedEvents = mutableListOf() - - Sentry.init { - it.dsn = dsn - it.beforeSend = SentryOptions.BeforeSendCallback { event, hint -> - capturedEvents.add(event) - event - } - } - - Sentry.addBreadcrumb("MyOriginalBreadcrumbBefore") - Sentry.captureMessage("OriginalMessageBefore") - println(Thread.currentThread().name) - - val future1 = executor.submit( - SentryWrapper.wrapCallable { - Thread.sleep(20) - Sentry.addBreadcrumb("MyClonedBreadcrumb") - Sentry.captureMessage("ClonedMessage") - "Result 1" - } - ) - - val future2 = executor.submit( - SentryWrapper.wrapCallable { - Thread.sleep(10) - Sentry.addBreadcrumb("MyClonedBreadcrumb2") - Sentry.captureMessage("ClonedMessage2") - "Result 2" - } - ) - - Sentry.addBreadcrumb("MyOriginalBreadcrumb") - Sentry.captureMessage("OriginalMessage") - - future1.get() - future2.get() - - val mainEvent = capturedEvents.firstOrNull { it.message?.formatted == "OriginalMessage" } - val clonedEvent = capturedEvents.firstOrNull { it.message?.formatted == "ClonedMessage" } - val clonedEvent2 = capturedEvents.firstOrNull { it.message?.formatted == "ClonedMessage2" } - - assertEquals(2, mainEvent?.breadcrumbs?.size) - assertEquals(3, clonedEvent?.breadcrumbs?.size) - assertEquals(4, clonedEvent2?.breadcrumbs?.size) - } - - @Test - fun `scopes are reset to its state within the thread after callable is done`() { - Sentry.init { - it.dsn = dsn - } - - val mainScopes = Sentry.getCurrentScopes() - val threadedScopes = Sentry.getCurrentScopes().forkedCurrentScope("test") - - executor.submit { - Sentry.setCurrentScopes(threadedScopes) - }.get() - - assertEquals(mainScopes, Sentry.getCurrentScopes()) - - val callableFuture = - executor.submit( - SentryWrapper.wrapCallable { - assertNotEquals(mainScopes, Sentry.getCurrentScopes()) - assertNotEquals(threadedScopes, Sentry.getCurrentScopes()) - "Result 1" - } - ) - - callableFuture.get() - - executor.submit { - assertNotEquals(mainScopes, Sentry.getCurrentScopes()) - assertEquals(threadedScopes, Sentry.getCurrentScopes()) - }.get() - } - - @Test - fun `scopes is reset to its state within the thread after isolated supply is done`() { + fun `scopes is reset to state within the thread after isolated supply is done`() { Sentry.init { it.dsn = dsn it.beforeSend = SentryOptions.BeforeSendCallback { event, hint -> @@ -215,7 +46,7 @@ class SentryWrapperTest { val callableFuture = CompletableFuture.supplyAsync( - SentryWrapper.wrapSupplierIsolated { + SentryWrapper.wrapSupplier { assertNotEquals(mainScopes, Sentry.getCurrentScopes()) assertNotEquals(threadedScopes, Sentry.getCurrentScopes()) "Result 1" @@ -248,7 +79,7 @@ class SentryWrapperTest { val callableFuture = CompletableFuture.supplyAsync( - SentryWrapper.wrapSupplierIsolated { + SentryWrapper.wrapSupplier { Thread.sleep(20) Sentry.addBreadcrumb("MyClonedBreadcrumb") Sentry.captureMessage("ClonedMessage") @@ -259,7 +90,7 @@ class SentryWrapperTest { val callableFuture2 = CompletableFuture.supplyAsync( - SentryWrapper.wrapSupplierIsolated { + SentryWrapper.wrapSupplier { Thread.sleep(10) Sentry.addBreadcrumb("MyClonedBreadcrumb2") Sentry.captureMessage("ClonedMessage2") @@ -300,7 +131,7 @@ class SentryWrapperTest { println(Thread.currentThread().name) val future1 = executor.submit( - SentryWrapper.wrapCallableIsolated { + SentryWrapper.wrapCallable { Thread.sleep(20) Sentry.addBreadcrumb("MyClonedBreadcrumb") Sentry.captureMessage("ClonedMessage") @@ -309,7 +140,7 @@ class SentryWrapperTest { ) val future2 = executor.submit( - SentryWrapper.wrapCallableIsolated { + SentryWrapper.wrapCallable { Thread.sleep(10) Sentry.addBreadcrumb("MyClonedBreadcrumb2") Sentry.captureMessage("ClonedMessage2") @@ -333,7 +164,7 @@ class SentryWrapperTest { } @Test - fun `scopes is reset to its state within the thread after isolated callable is done`() { + fun `scopes is reset to state within the thread after isolated callable is done`() { Sentry.init { it.dsn = dsn } @@ -349,7 +180,7 @@ class SentryWrapperTest { val callableFuture = executor.submit( - SentryWrapper.wrapCallableIsolated { + SentryWrapper.wrapCallable { assertNotEquals(mainScopes, Sentry.getCurrentScopes()) assertNotEquals(threadedScopes, Sentry.getCurrentScopes()) "Result 1" diff --git a/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt b/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt index d27e5c0228..274ec047d4 100644 --- a/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt +++ b/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt @@ -23,6 +23,7 @@ import io.sentry.UncaughtExceptionHandlerIntegration.UncaughtExceptionHint import io.sentry.UserFeedback import io.sentry.dsnString import io.sentry.hints.Retryable +import io.sentry.metrics.EncodedMetrics import io.sentry.protocol.SentryId import io.sentry.protocol.SentryTransaction import io.sentry.protocol.User @@ -67,13 +68,14 @@ class ClientReportTest { SentryEnvelopeItem.fromUserFeedback(opts.serializer, UserFeedback(SentryId(UUID.randomUUID()))), SentryEnvelopeItem.fromAttachment(opts.serializer, NoOpLogger.getInstance(), Attachment("{ \"number\": 10 }".toByteArray(), "log.json"), 1000), SentryEnvelopeItem.fromProfilingTrace(ProfilingTraceData(File(""), transaction), 1000, opts.serializer), - SentryEnvelopeItem.fromCheckIn(opts.serializer, CheckIn("monitor-slug-1", CheckInStatus.ERROR)) + SentryEnvelopeItem.fromCheckIn(opts.serializer, CheckIn("monitor-slug-1", CheckInStatus.ERROR)), + SentryEnvelopeItem.fromMetrics(EncodedMetrics(emptyMap())) ) clientReportRecorder.recordLostEnvelope(DiscardReason.NETWORK_ERROR, envelope) val clientReportAtEnd = clientReportRecorder.resetCountsAndGenerateClientReport() - testHelper.assertTotalCount(12, clientReportAtEnd) + testHelper.assertTotalCount(13, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.SAMPLE_RATE, DataCategory.Error, 3, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.BEFORE_SEND, DataCategory.Error, 2, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.QUEUE_OVERFLOW, DataCategory.Transaction, 1, clientReportAtEnd) @@ -83,6 +85,7 @@ class ClientReportTest { testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.Attachment, 1, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.Profile, 1, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.Monitor, 1, clientReportAtEnd) + testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.MetricBucket, 1, clientReportAtEnd) } @Test diff --git a/sentry/src/test/java/io/sentry/metrics/LocalMetricsAggregatorTest.kt b/sentry/src/test/java/io/sentry/metrics/LocalMetricsAggregatorTest.kt index d5bee5d2f8..83c5d3864d 100644 --- a/sentry/src/test/java/io/sentry/metrics/LocalMetricsAggregatorTest.kt +++ b/sentry/src/test/java/io/sentry/metrics/LocalMetricsAggregatorTest.kt @@ -15,7 +15,6 @@ class LocalMetricsAggregatorTest { val tags0 = mapOf( "tag" to "value0" ) - val timestamp = 0L // when a metric is emitted aggregator.add( @@ -24,8 +23,7 @@ class LocalMetricsAggregatorTest { key, 1.0, unit, - tags0, - timestamp + tags0 ) // and the same metric is emitted with different tags @@ -38,8 +36,7 @@ class LocalMetricsAggregatorTest { key, 1.0, unit, - tags1, - timestamp + tags1 ) // then the summary contain a single top level group for the metric @@ -70,8 +67,7 @@ class LocalMetricsAggregatorTest { key, 1.0, unit, - tags, - timestamp + tags ) aggregator.add( @@ -80,8 +76,7 @@ class LocalMetricsAggregatorTest { key, 2.0, unit, - tags, - timestamp + tags ) val metric = aggregator.summaries.values.first()[0] diff --git a/sentry/src/test/java/io/sentry/metrics/MetricsApiTest.kt b/sentry/src/test/java/io/sentry/metrics/MetricsApiTest.kt index 359563f78a..24ef6ffa5b 100644 --- a/sentry/src/test/java/io/sentry/metrics/MetricsApiTest.kt +++ b/sentry/src/test/java/io/sentry/metrics/MetricsApiTest.kt @@ -1,8 +1,10 @@ package io.sentry.metrics +import io.sentry.DateUtils import io.sentry.IMetricsAggregator import io.sentry.ISpan import io.sentry.MeasurementUnit +import io.sentry.SentryNanotimeDate import io.sentry.metrics.MetricsApi.IMetricsInterface import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull @@ -10,6 +12,7 @@ import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import kotlin.test.Test import kotlin.test.assertEquals @@ -25,7 +28,12 @@ class MetricsApiTest { fun getSut( defaultTags: Map = emptyMap(), - spanProvider: () -> ISpan? = { mock() } + spanProvider: () -> ISpan? = { + val span = mock() + val date = SentryNanotimeDate() + whenever(span.startDate).thenReturn(date) + span + } ): MetricsApi { val localAggregator = localMetricsAggregator @@ -280,12 +288,14 @@ class MetricsApiTest { api.timing("timing") { // no-op } - verify(fixture.aggregator).timing( + val spanLocalAggregator = fixture.lastSpan!!.localMetricsAggregator + verify(fixture.aggregator).distribution( anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), - eq(fixture.localMetricsAggregator) + anyOrNull(), + eq(spanLocalAggregator) ) } @@ -304,16 +314,37 @@ class MetricsApiTest { } @Test - fun `timing applies metric tags as span tags`() { + fun `timing value equals span duration`() { + val startDate = SentryNanotimeDate() + val finishNanos = startDate.nanoTimestamp() + 10000 + val finishDate = SentryNanotimeDate(DateUtils.nanosToDate(finishNanos), finishNanos) + val localAggregator = mock() val span = mock() - val api = fixture.getSut( - spanProvider = { - span - }, - defaultTags = mapOf( - "release" to "1.0" - ) + whenever(span.startDate).thenReturn(startDate) + whenever(span.finishDate).thenReturn(finishDate) + whenever(span.localMetricsAggregator).thenReturn(localAggregator) + + val api = fixture.getSut(spanProvider = { span }) + + api.timing("key") { + // no-op + } + + assertEquals("metric.timing", fixture.lastOp) + assertEquals("key", fixture.lastDescription) + verify(fixture.aggregator).distribution( + eq("key"), + eq((finishDate.diff(startDate)) / 1000000000.0), + eq(MeasurementUnit.Duration.SECOND), + anyOrNull(), + anyOrNull(), + eq(localAggregator) ) + } + + @Test + fun `timing applies metric tags as span tags`() { + val api = fixture.getSut(defaultTags = mapOf("release" to "1.0")) // when timing is called api.timing("key", { // no-op diff --git a/sentry/src/test/java/io/sentry/metrics/MetricsHelperTest.kt b/sentry/src/test/java/io/sentry/metrics/MetricsHelperTest.kt index 089f805a60..8112170f68 100644 --- a/sentry/src/test/java/io/sentry/metrics/MetricsHelperTest.kt +++ b/sentry/src/test/java/io/sentry/metrics/MetricsHelperTest.kt @@ -59,31 +59,46 @@ class MetricsHelperTest { } @Test - fun sanitizeKey() { - assertEquals("foo-bar", MetricsHelper.sanitizeKey("foo-bar")) - assertEquals("foo_bar", MetricsHelper.sanitizeKey("foo\$\$\$bar")) - assertEquals("fo_-bar", MetricsHelper.sanitizeKey("foö-bar")) + fun sanitizeName() { + assertEquals("foo-bar", MetricsHelper.sanitizeName("foo-bar")) + assertEquals("foo_bar", MetricsHelper.sanitizeName("foo\$\$\$bar")) + assertEquals("fo_-bar", MetricsHelper.sanitizeName("foö-bar")) } @Test - fun sanitizeValue() { - assertEquals("\$foo", MetricsHelper.sanitizeValue("%\$foo")) - assertEquals("blah{}", MetricsHelper.sanitizeValue("blah{}")) - assertEquals("snwmn", MetricsHelper.sanitizeValue("snöwmän")) - assertEquals("j e n g a", MetricsHelper.sanitizeValue("j e n g a!")) + fun sanitizeTagKey() { + // no replacement characters for tag keys + // - and / should be allowed + assertEquals("a/weird/tag-key/", MetricsHelper.sanitizeTagKey("a/weird/tag-key/:ä")) } @Test - fun sanitizeUnit() { - val items = listOf( - "Test123_." to "Test123_.", - "test{value}" to "test_value_", - "test-value" to "test_value" - ) + fun sanitizeTagValue() { + // https://github.com/getsentry/relay/blob/3208e3ce5b1fe4d147aa44e0e966807c256993de/relay-metrics/src/protocol.rs#L142 + assertEquals("plain", MetricsHelper.sanitizeTagValue("plain")) + assertEquals("plain text", MetricsHelper.sanitizeTagValue("plain text")) + assertEquals("plain%text", MetricsHelper.sanitizeTagValue("plain%text")) + + // Escape sequences + assertEquals("plain \\\\ text", MetricsHelper.sanitizeTagValue("plain \\ text")) + assertEquals("plain\\u{2c}text", MetricsHelper.sanitizeTagValue("plain,text")) + assertEquals("plain\\u{7c}text", MetricsHelper.sanitizeTagValue("plain|text")) + assertEquals("plain 😅", MetricsHelper.sanitizeTagValue("plain 😅")) + + // Escapable control characters (may be stripped by the parser) + assertEquals("plain\\ntext", MetricsHelper.sanitizeTagValue("plain\ntext")) + assertEquals("plain\\rtext", MetricsHelper.sanitizeTagValue("plain\rtext")) + assertEquals("plain\\ttext", MetricsHelper.sanitizeTagValue("plain\ttext")) + + // Unescapable control characters remain, as they'll be stripped by relay + assertEquals("plain\u0007text", MetricsHelper.sanitizeTagValue("plain\u0007text")) + assertEquals("plain\u009Ctext", MetricsHelper.sanitizeTagValue("plain\u009ctext")) + } - for (item in items) { - assertEquals(item.second, MetricsHelper.sanitizeUnit(item.first)) - } + @Test + fun sanitizeUnit() { + // no replacement characters for units + assertEquals("abcABC123_abcABC123", MetricsHelper.sanitizeUnit("abcABC123_-./äöü\$%&abcABC123")) } @Test @@ -162,10 +177,10 @@ class MetricsHelperTest { @Test fun toStatsdType() { - assertEquals("c", MetricsHelper.toStatsdType(MetricType.Counter)) - assertEquals("g", MetricsHelper.toStatsdType(MetricType.Gauge)) - assertEquals("s", MetricsHelper.toStatsdType(MetricType.Set)) - assertEquals("d", MetricsHelper.toStatsdType(MetricType.Distribution)) + assertEquals("c", MetricType.Counter.statsdCode) + assertEquals("g", MetricType.Gauge.statsdCode) + assertEquals("s", MetricType.Set.statsdCode) + assertEquals("d", MetricType.Distribution.statsdCode) } @Test diff --git a/sentry/src/test/java/io/sentry/protocol/AppSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/AppSerializationTest.kt index c39f93643a..85716fe913 100644 --- a/sentry/src/test/java/io/sentry/protocol/AppSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/AppSerializationTest.kt @@ -31,6 +31,7 @@ class AppSerializationTest { ) inForeground = true viewNames = listOf("MainActivity", "SidebarActivity") + startType = "cold" } } private val fixture = Fixture() diff --git a/sentry/src/test/java/io/sentry/protocol/AppTest.kt b/sentry/src/test/java/io/sentry/protocol/AppTest.kt index 3f51594fff..5c36f148c0 100644 --- a/sentry/src/test/java/io/sentry/protocol/AppTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/AppTest.kt @@ -21,6 +21,7 @@ class AppTest { app.permissions = mapOf(Pair("internet", "granted")) app.viewNames = listOf("MainActivity") app.inForeground = true + app.startType = "cold" val unknown = mapOf(Pair("unknown", "unknown")) app.unknown = unknown @@ -49,6 +50,7 @@ class AppTest { app.permissions = mapOf(Pair("internet", "granted")) app.viewNames = listOf("MainActivity") app.inForeground = true + app.startType = "cold" val unknown = mapOf(Pair("unknown", "unknown")) app.unknown = unknown @@ -67,6 +69,7 @@ class AppTest { assertEquals(listOf("MainActivity"), clone.viewNames) assertEquals(true, clone.inForeground) + assertEquals("cold", clone.startType) assertNotNull(clone.unknown) { assertEquals("unknown", it["unknown"]) } diff --git a/sentry/src/test/java/io/sentry/transport/HttpConnectionTest.kt b/sentry/src/test/java/io/sentry/transport/HttpConnectionTest.kt index f153c0305a..47bc34de48 100644 --- a/sentry/src/test/java/io/sentry/transport/HttpConnectionTest.kt +++ b/sentry/src/test/java/io/sentry/transport/HttpConnectionTest.kt @@ -230,6 +230,28 @@ class HttpConnectionTest { verify(fixture.authenticatorWrapper, never()).setDefault(any()) } + @Test + fun `When Proxy type is not set, it defaults to HTTP`() { + fixture.proxy = Proxy("proxy.example.com", "8080") + val transport = fixture.getSUT() + + transport.send(createEnvelope()) + + assertEquals(Type.HTTP, transport.proxy!!.type()) + } + + @Test + fun `When Proxy type is set to SOCKS, HTTP connection uses it`() { + fixture.proxy = Proxy("proxy.example.com", "8080").apply { + type = Type.SOCKS + } + val transport = fixture.getSUT() + + transport.send(createEnvelope()) + + assertEquals(Type.SOCKS, transport.proxy!!.type()) + } + @Test fun `sets common headers and on http connection`() { val transport = fixture.getSUT() diff --git a/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt b/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt index 2b1be98611..557085031c 100644 --- a/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt +++ b/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt @@ -20,6 +20,7 @@ import io.sentry.TransactionContext import io.sentry.UserFeedback import io.sentry.clientreport.DiscardReason import io.sentry.clientreport.IClientReportRecorder +import io.sentry.metrics.EncodedMetrics import io.sentry.protocol.SentryId import io.sentry.protocol.SentryTransaction import io.sentry.protocol.User @@ -102,13 +103,14 @@ class RateLimiterTest { val eventItem = SentryEnvelopeItem.fromEvent(fixture.serializer, SentryEvent()) val transaction = SentryTransaction(SentryTracer(TransactionContext("name", "op"), scopes)) val transactionItem = SentryEnvelopeItem.fromEvent(fixture.serializer, transaction) - val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, transactionItem)) + val statsdItem = SentryEnvelopeItem.fromMetrics(EncodedMetrics(emptyMap())) + val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, transactionItem, statsdItem)) - rateLimiter.updateRetryAfterLimits("1:transaction:key, 1:default;error;security:organization", null, 1) + rateLimiter.updateRetryAfterLimits("1:transaction:key, 1:default;error;metric_bucket;security:organization", null, 1) val result = rateLimiter.filter(envelope, Hint()) assertNotNull(result) - assertEquals(2, result.items.count()) + assertEquals(3, result.items.count()) } @Test @@ -199,8 +201,9 @@ class RateLimiterTest { val attachmentItem = SentryEnvelopeItem.fromAttachment(fixture.serializer, NoOpLogger.getInstance(), Attachment("{ \"number\": 10 }".toByteArray(), "log.json"), 1000) val profileItem = SentryEnvelopeItem.fromProfilingTrace(ProfilingTraceData(File(""), transaction), 1000, fixture.serializer) val checkInItem = SentryEnvelopeItem.fromCheckIn(fixture.serializer, CheckIn("monitor-slug-1", CheckInStatus.ERROR)) + val statsdItem = SentryEnvelopeItem.fromMetrics(EncodedMetrics(emptyMap())) - val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, userFeedbackItem, sessionItem, attachmentItem, profileItem, checkInItem)) + val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, userFeedbackItem, sessionItem, attachmentItem, profileItem, checkInItem, statsdItem)) rateLimiter.updateRetryAfterLimits(null, null, 429) val result = rateLimiter.filter(envelope, Hint()) @@ -213,6 +216,7 @@ class RateLimiterTest { verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(attachmentItem)) verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(profileItem)) verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(checkInItem)) + verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(statsdItem)) verifyNoMoreInteractions(fixture.clientReportRecorder) } @@ -272,6 +276,71 @@ class RateLimiterTest { verifyNoMoreInteractions(fixture.clientReportRecorder) } + @Test + fun `drop metrics items as lost`() { + val rateLimiter = fixture.getSUT() + val scopes = mock() + whenever(scopes.options).thenReturn(SentryOptions()) + + val eventItem = SentryEnvelopeItem.fromEvent(fixture.serializer, SentryEvent()) + val f = File.createTempFile("test", "trace") + val transaction = SentryTracer(TransactionContext("name", "op"), scopes) + val profileItem = SentryEnvelopeItem.fromProfilingTrace(ProfilingTraceData(f, transaction), 1000, fixture.serializer) + val statsdItem = SentryEnvelopeItem.fromMetrics(EncodedMetrics(emptyMap())) + val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, profileItem, statsdItem)) + + rateLimiter.updateRetryAfterLimits("60:metric_bucket:key", null, 1) + val result = rateLimiter.filter(envelope, Hint()) + + assertNotNull(result) + assertEquals(2, result.items.toList().size) + + verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(statsdItem)) + verifyNoMoreInteractions(fixture.clientReportRecorder) + } + + @Test + fun `drop metrics items if namespace is custom`() { + val rateLimiter = fixture.getSUT() + val statsdItem = SentryEnvelopeItem.fromMetrics(EncodedMetrics(emptyMap())) + val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(statsdItem)) + + rateLimiter.updateRetryAfterLimits("60:metric_bucket:key:quota_exceeded:custom", null, 1) + val result = rateLimiter.filter(envelope, Hint()) + assertNull(result) + + verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(statsdItem)) + verifyNoMoreInteractions(fixture.clientReportRecorder) + } + + @Test + fun `drop metrics items if namespaces is empty`() { + val rateLimiter = fixture.getSUT() + val statsdItem = SentryEnvelopeItem.fromMetrics(EncodedMetrics(emptyMap())) + val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(statsdItem)) + + rateLimiter.updateRetryAfterLimits("60:metric_bucket:key:quota_exceeded::", null, 1) + val result = rateLimiter.filter(envelope, Hint()) + assertNull(result) + + verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(statsdItem)) + verifyNoMoreInteractions(fixture.clientReportRecorder) + } + + @Test + fun `drop metrics items if namespaces is not present`() { + val rateLimiter = fixture.getSUT() + val statsdItem = SentryEnvelopeItem.fromMetrics(EncodedMetrics(emptyMap())) + val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(statsdItem)) + + rateLimiter.updateRetryAfterLimits("60:metric_bucket:key:quota_exceeded", null, 1) + val result = rateLimiter.filter(envelope, Hint()) + assertNull(result) + + verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(statsdItem)) + verifyNoMoreInteractions(fixture.clientReportRecorder) + } + @Test fun `any limit can be checked`() { val rateLimiter = fixture.getSUT() diff --git a/sentry/src/test/java/io/sentry/util/CollectionUtilsTest.kt b/sentry/src/test/java/io/sentry/util/CollectionUtilsTest.kt index 266d188b4d..ebdaff477b 100644 --- a/sentry/src/test/java/io/sentry/util/CollectionUtilsTest.kt +++ b/sentry/src/test/java/io/sentry/util/CollectionUtilsTest.kt @@ -4,6 +4,8 @@ import io.sentry.JsonObjectReader import java.io.StringReader import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue class CollectionUtilsTest { @@ -62,4 +64,19 @@ class CollectionUtilsTest { assertEquals("value1", result?.get("key1")) assertEquals("value3", result?.get("key3")) } + + @Test + fun `contains returns false for empty arrays`() { + assertFalse(CollectionUtils.contains(emptyArray(), "")) + } + + @Test + fun `contains returns true if element is present`() { + assertTrue(CollectionUtils.contains(arrayOf("one", "two", "three"), "two")) + } + + @Test + fun `contains returns false if element is not present`() { + assertFalse(CollectionUtils.contains(arrayOf("one", "two", "three"), "four")) + } } diff --git a/sentry/src/test/java/io/sentry/util/StringUtilsTest.kt b/sentry/src/test/java/io/sentry/util/StringUtilsTest.kt index d06f3859ae..fe2d026dd2 100644 --- a/sentry/src/test/java/io/sentry/util/StringUtilsTest.kt +++ b/sentry/src/test/java/io/sentry/util/StringUtilsTest.kt @@ -57,6 +57,31 @@ class StringUtilsTest { assertEquals("", StringUtils.capitalize("")) } + @Test + fun `camelCase string`() { + assertEquals("TestCase", StringUtils.camelCase("test_case")) + } + + @Test + fun `camelCase string even if its uppercase`() { + assertEquals("TestCase", StringUtils.camelCase("TEST CASE")) + } + + @Test + fun `camelCase do not throw if only 1 char`() { + assertEquals("T", StringUtils.camelCase("t")) + } + + @Test + fun `camelCase returns itself if null`() { + assertNull(StringUtils.camelCase(null)) + } + + @Test + fun `camelCase returns itself if empty`() { + assertEquals("", StringUtils.camelCase("")) + } + @Test fun `removeSurrounding returns null if argument is null`() { assertNull(StringUtils.removeSurrounding(null, "\"")) diff --git a/sentry/src/test/resources/json/app.json b/sentry/src/test/resources/json/app.json index e835258efa..76eab3b545 100644 --- a/sentry/src/test/resources/json/app.json +++ b/sentry/src/test/resources/json/app.json @@ -12,5 +12,6 @@ "CAMERA": "granted" }, "in_foreground": true, - "view_names": ["MainActivity", "SidebarActivity"] + "view_names": ["MainActivity", "SidebarActivity"], + "start_type": "cold" } diff --git a/sentry/src/test/resources/json/contexts.json b/sentry/src/test/resources/json/contexts.json index 97f06e0be6..574bb01921 100644 --- a/sentry/src/test/resources/json/contexts.json +++ b/sentry/src/test/resources/json/contexts.json @@ -14,7 +14,8 @@ "CAMERA": "granted" }, "in_foreground": true, - "view_names": ["MainActivity", "SidebarActivity"] + "view_names": ["MainActivity", "SidebarActivity"], + "start_type": "cold" }, "browser": { diff --git a/sentry/src/test/resources/json/sentry_base_event.json b/sentry/src/test/resources/json/sentry_base_event.json index 48b7c1ccb9..afaf040863 100644 --- a/sentry/src/test/resources/json/sentry_base_event.json +++ b/sentry/src/test/resources/json/sentry_base_event.json @@ -17,7 +17,8 @@ "CAMERA": "granted" }, "in_foreground": true, - "view_names": ["MainActivity", "SidebarActivity"] + "view_names": ["MainActivity", "SidebarActivity"], + "start_type": "cold" }, "browser": { diff --git a/sentry/src/test/resources/json/sentry_base_event_with_null_extra.json b/sentry/src/test/resources/json/sentry_base_event_with_null_extra.json index 773eb99e2f..018cc7aae7 100644 --- a/sentry/src/test/resources/json/sentry_base_event_with_null_extra.json +++ b/sentry/src/test/resources/json/sentry_base_event_with_null_extra.json @@ -17,7 +17,8 @@ "CAMERA": "granted" }, "in_foreground": true, - "view_names": ["MainActivity", "SidebarActivity"] + "view_names": ["MainActivity", "SidebarActivity"], + "start_type": "cold" }, "browser": { diff --git a/sentry/src/test/resources/json/sentry_event.json b/sentry/src/test/resources/json/sentry_event.json index 5d57960db9..6d0b351ed4 100644 --- a/sentry/src/test/resources/json/sentry_event.json +++ b/sentry/src/test/resources/json/sentry_event.json @@ -152,7 +152,8 @@ "CAMERA": "granted" }, "in_foreground": true, - "view_names": ["MainActivity", "SidebarActivity"] + "view_names": ["MainActivity", "SidebarActivity"], + "start_type": "cold" }, "browser": { diff --git a/sentry/src/test/resources/json/sentry_transaction.json b/sentry/src/test/resources/json/sentry_transaction.json index 93503569b1..944a0bfe92 100644 --- a/sentry/src/test/resources/json/sentry_transaction.json +++ b/sentry/src/test/resources/json/sentry_transaction.json @@ -100,7 +100,8 @@ "CAMERA": "granted" }, "in_foreground": true, - "view_names": ["MainActivity", "SidebarActivity"] + "view_names": ["MainActivity", "SidebarActivity"], + "start_type": "cold" }, "browser": { diff --git a/sentry/src/test/resources/json/sentry_transaction_legacy_date_format.json b/sentry/src/test/resources/json/sentry_transaction_legacy_date_format.json index 6e54117084..789c4fe2a9 100644 --- a/sentry/src/test/resources/json/sentry_transaction_legacy_date_format.json +++ b/sentry/src/test/resources/json/sentry_transaction_legacy_date_format.json @@ -100,7 +100,8 @@ "CAMERA": "granted" }, "in_foreground": true, - "view_names": ["MainActivity", "SidebarActivity"] + "view_names": ["MainActivity", "SidebarActivity"], + "start_type": "cold" }, "browser": { diff --git a/settings.gradle.kts b/settings.gradle.kts index 4d678bdd5c..1f7d5c2226 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -67,12 +67,3 @@ include( "sentry-android-integration-tests:test-app-sentry", "sentry-samples:sentry-samples-openfeign" ) - -gradle.beforeProject { - if (project.name == "sentry-android-ndk" || project.name == "sentry-samples-android") { - exec { - logger.log(LogLevel.LIFECYCLE, "Initializing git submodules") - commandLine("git", "submodule", "update", "--init", "--recursive") - } - } -}