diff --git a/sentry-compose/api/android/sentry-compose.api b/sentry-compose/api/android/sentry-compose.api index e30b863f6be..e951ebe14ec 100644 --- a/sentry-compose/api/android/sentry-compose.api +++ b/sentry-compose/api/android/sentry-compose.api @@ -6,6 +6,10 @@ public final class io/sentry/compose/BuildConfig { public fun ()V } +public final class io/sentry/compose/SentryComposeTracingKt { + public static final fun SentryTraced (Ljava/lang/String;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V +} + public final class io/sentry/compose/SentryNavigationIntegrationKt { public static final fun withSentryObservableEffect (Landroidx/navigation/NavHostController;ZZLandroidx/compose/runtime/Composer;II)Landroidx/navigation/NavHostController; } diff --git a/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryComposeTracing.kt b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryComposeTracing.kt new file mode 100644 index 00000000000..467ab236f8c --- /dev/null +++ b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryComposeTracing.kt @@ -0,0 +1,71 @@ +package io.sentry.compose + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.platform.testTag +import io.sentry.Sentry +import io.sentry.SpanOptions + +private const val OP_PARENT_COMPOSITION = "compose.composition" +private const val OP_COMPOSE = "compose" + +private const val OP_PARENT_RENDER = "compose.rendering" +private const val OP_RENDER = "render" + +@Immutable +private class ImmutableHolder(var item: T) + +private val localSentryCompositionParentSpan = compositionLocalOf { + ImmutableHolder( + Sentry.getRootSpan() + ?.startChild(OP_PARENT_COMPOSITION, null, SpanOptions(true, true, true)) + ) +} + +private val localSentryRenderingParentSpan = compositionLocalOf { + ImmutableHolder( + Sentry.getRootSpan() + ?.startChild(OP_PARENT_RENDER, null, SpanOptions(true, true, true)) + ) +} + +@ExperimentalComposeUiApi +@Composable +public fun SentryTraced( + tag: String, + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit +) { + val parentCompositionSpan = localSentryCompositionParentSpan.current + val parentRenderingSpan = localSentryRenderingParentSpan.current + val compositionSpan = parentCompositionSpan.item?.startChild(OP_COMPOSE, tag) + val firstRendered = remember { ImmutableHolder(false) } + + Box( + modifier = modifier + .testTag(tag) + .drawWithContent { + val renderSpan = if (!firstRendered.item) { + parentRenderingSpan.item?.startChild( + OP_RENDER, + tag + ) + } else { + null + } + drawContent() + firstRendered.item = true + renderSpan?.finish() + } + ) { + content() + } + compositionSpan?.finish() +} diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/compose/ComposeActivity.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/compose/ComposeActivity.kt index 259947399a0..fca9c00220c 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/compose/ComposeActivity.kt +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/compose/ComposeActivity.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalComposeUiApi::class) + package io.sentry.samples.android.compose import android.os.Bundle @@ -18,6 +20,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.input.TextFieldValue @@ -28,8 +31,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument -import io.sentry.Sentry -import io.sentry.compose.withSentryObservableEffect +import io.sentry.compose.SentryTraced import io.sentry.samples.android.GithubAPI import kotlinx.coroutines.launch @@ -39,15 +41,10 @@ class ComposeActivity : ComponentActivity() { super.onCreate(savedInstanceState) setContent { - val navController = rememberNavController().withSentryObservableEffect() + val navController = rememberNavController() SampleNavigation(navController) } } - - override fun onResume() { - super.onResume() - Sentry.getSpan()?.finish() - } } @Composable @@ -55,36 +52,38 @@ fun Landing( navigateGithub: () -> Unit, navigateGithubWithArgs: () -> Unit ) { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxSize() - ) { - Button( - onClick = { - navigateGithub() - }, - modifier = Modifier - .testTag("button_nav_github") - .padding(top = 32.dp) + SentryTraced(tag = "buttons_page") { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize() ) { - Text("Navigate to Github Page") - } - Button( - onClick = { navigateGithubWithArgs() }, - modifier = Modifier - .testTag("button_nav_github_args") - .padding(top = 32.dp) - ) { - Text("Navigate to Github Page With Args") - } - Button( - onClick = { throw RuntimeException("Crash from Compose") }, - modifier = Modifier - .testTag("button_crash") - .padding(top = 32.dp) - ) { - Text("Crash from Compose") + SentryTraced(tag = "button_nav_github") { + Button( + onClick = { + navigateGithub() + }, + modifier = Modifier.padding(top = 32.dp) + ) { + Text("Navigate to Github") + } + } + SentryTraced(tag = "button_nav_github_args") { + Button( + onClick = { navigateGithubWithArgs() }, + modifier = Modifier.padding(top = 32.dp) + ) { + Text("Navigate to Github Page With Args") + } + } + SentryTraced(tag = "button_crash") { + Button( + onClick = { throw RuntimeException("Crash from Compose") }, + modifier = Modifier.padding(top = 32.dp) + ) { + Text("Crash from Compose") + } + } } } } @@ -102,59 +101,67 @@ fun Github( result = GithubAPI.service.listReposAsync(user.text, perPage).random().full_name } - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxSize() - ) { - TextField( - value = user, - onValueChange = { newText -> - user = newText - } - ) - Text("Random repo $result") - Button( - onClick = { - scope.launch { - result = GithubAPI.service.listReposAsync(user.text, perPage).random().full_name - } - }, + SentryTraced("github-$user") { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier - .testTag("button_list_repos_async") - .padding(top = 32.dp) + .fillMaxSize() ) { - Text("Make Request") + TextField( + value = user, + onValueChange = { newText -> + user = newText + } + ) + Text("Random repo $result") + Button( + onClick = { + scope.launch { + result = + GithubAPI.service.listReposAsync(user.text, perPage).random().full_name + } + }, + modifier = Modifier + .testTag("button_list_repos_async") + .padding(top = 32.dp) + ) { + Text("Make Request") + } } } } @Composable fun SampleNavigation(navController: NavHostController) { - NavHost( - navController = navController, - startDestination = Destination.Landing.route - ) { - composable(Destination.Landing.route) { - Landing( - navigateGithub = { navController.navigate("github") }, - navigateGithubWithArgs = { navController.navigate("github/spotify?per_page=10") } - ) - } - composable(Destination.Github.route) { - Github() - } - composable( - Destination.GithubWithArgs.route, - arguments = listOf( - navArgument(Destination.USER_ARG) { type = NavType.StringType }, - navArgument(Destination.PER_PAGE_ARG) { type = NavType.IntType; defaultValue = 10 } - ) + SentryTraced(tag = "navhost") { + NavHost( + navController = navController, + startDestination = Destination.Landing.route ) { - Github( - it.arguments?.getString(Destination.USER_ARG) ?: "getsentry", - it.arguments?.getInt(Destination.PER_PAGE_ARG) ?: 10 - ) + composable(Destination.Landing.route) { + Landing( + navigateGithub = { navController.navigate("github") }, + navigateGithubWithArgs = { navController.navigate("github/spotify?per_page=10") } + ) + } + composable(Destination.Github.route) { + Github() + } + composable( + Destination.GithubWithArgs.route, + arguments = listOf( + navArgument(Destination.USER_ARG) { type = NavType.StringType }, + navArgument(Destination.PER_PAGE_ARG) { + type = NavType.IntType; defaultValue = 10 + } + ) + ) { + Github( + it.arguments?.getString(Destination.USER_ARG) ?: "getsentry", + it.arguments?.getInt(Destination.PER_PAGE_ARG) ?: 10 + ) + } } } } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index bd4e9ef9ffd..ac293238809 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -325,6 +325,7 @@ public final class io/sentry/Hub : io/sentry/IHub { public fun flush (J)V public fun getLastEventId ()Lio/sentry/protocol/SentryId; public fun getOptions ()Lio/sentry/SentryOptions; + public fun getRootSpan ()Lio/sentry/ISpan; public fun getSpan ()Lio/sentry/ISpan; public fun isCrashedLastRun ()Ljava/lang/Boolean; public fun isEnabled ()Z @@ -368,6 +369,7 @@ public final class io/sentry/HubAdapter : io/sentry/IHub { public static fun getInstance ()Lio/sentry/HubAdapter; public fun getLastEventId ()Lio/sentry/protocol/SentryId; public fun getOptions ()Lio/sentry/SentryOptions; + public fun getRootSpan ()Lio/sentry/ISpan; public fun getSpan ()Lio/sentry/ISpan; public fun isCrashedLastRun ()Ljava/lang/Boolean; public fun isEnabled ()Z @@ -436,6 +438,7 @@ public abstract interface class io/sentry/IHub { public abstract fun flush (J)V public abstract fun getLastEventId ()Lio/sentry/protocol/SentryId; public abstract fun getOptions ()Lio/sentry/SentryOptions; + public abstract fun getRootSpan ()Lio/sentry/ISpan; public abstract fun getSpan ()Lio/sentry/ISpan; public abstract fun isCrashedLastRun ()Ljava/lang/Boolean; public abstract fun isEnabled ()Z @@ -549,6 +552,8 @@ public abstract interface class io/sentry/ISpan { public abstract fun startChild (Ljava/lang/String;)Lio/sentry/ISpan; public abstract fun startChild (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ISpan; public abstract fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;)Lio/sentry/ISpan; + public abstract fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; + public abstract fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; public abstract fun toBaggageHeader (Ljava/util/List;)Lio/sentry/BaggageHeader; public abstract fun toSentryTrace ()Lio/sentry/SentryTraceHeader; public abstract fun traceContext ()Lio/sentry/TraceContext; @@ -756,6 +761,7 @@ public final class io/sentry/NoOpHub : io/sentry/IHub { public static fun getInstance ()Lio/sentry/NoOpHub; public fun getLastEventId ()Lio/sentry/protocol/SentryId; public fun getOptions ()Lio/sentry/SentryOptions; + public fun getRootSpan ()Lio/sentry/ISpan; public fun getSpan ()Lio/sentry/ISpan; public fun isCrashedLastRun ()Ljava/lang/Boolean; public fun isEnabled ()Z @@ -811,6 +817,8 @@ public final class io/sentry/NoOpSpan : io/sentry/ISpan { public fun startChild (Ljava/lang/String;)Lio/sentry/ISpan; public fun startChild (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ISpan; public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; public fun toBaggageHeader (Ljava/util/List;)Lio/sentry/BaggageHeader; public fun toSentryTrace ()Lio/sentry/SentryTraceHeader; public fun traceContext ()Lio/sentry/TraceContext; @@ -854,6 +862,8 @@ public final class io/sentry/NoOpTransaction : io/sentry/ITransaction { public fun startChild (Ljava/lang/String;)Lio/sentry/ISpan; public fun startChild (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ISpan; public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; public fun toBaggageHeader (Ljava/util/List;)Lio/sentry/BaggageHeader; public fun toSentryTrace ()Lio/sentry/SentryTraceHeader; public fun traceContext ()Lio/sentry/TraceContext; @@ -1054,6 +1064,7 @@ public final class io/sentry/Scope { public fun getContexts ()Lio/sentry/protocol/Contexts; public fun getLevel ()Lio/sentry/SentryLevel; public fun getRequest ()Lio/sentry/protocol/Request; + public fun getRootSpan ()Lio/sentry/ISpan; public fun getSession ()Lio/sentry/Session; public fun getSpan ()Lio/sentry/ISpan; public fun getTags ()Ljava/util/Map; @@ -1144,6 +1155,7 @@ public final class io/sentry/Sentry { public static fun flush (J)V public static fun getCurrentHub ()Lio/sentry/IHub; public static fun getLastEventId ()Lio/sentry/protocol/SentryId; + public static fun getRootSpan ()Lio/sentry/ISpan; public static fun getSpan ()Lio/sentry/ISpan; public static fun init ()V public static fun init (Lio/sentry/OptionsContainer;Lio/sentry/Sentry$OptionsConfiguration;)V @@ -1285,6 +1297,8 @@ public abstract class io/sentry/SentryDate : java/lang/Comparable { public fun compareTo (Lio/sentry/SentryDate;)I public synthetic fun compareTo (Ljava/lang/Object;)I public fun diff (Lio/sentry/SentryDate;)J + public final fun isAfter (Lio/sentry/SentryDate;)Z + public final fun isBefore (Lio/sentry/SentryDate;)Z public fun laterDateNanosTimestampByDiff (Lio/sentry/SentryDate;)J public abstract fun nanoTimestamp ()J } @@ -1761,6 +1775,8 @@ public final class io/sentry/SentryTracer : io/sentry/ITransaction { public fun startChild (Ljava/lang/String;)Lio/sentry/ISpan; public fun startChild (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ISpan; public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; public fun toBaggageHeader (Ljava/util/List;)Lio/sentry/BaggageHeader; public fun toSentryTrace ()Lio/sentry/SentryTraceHeader; public fun traceContext ()Lio/sentry/TraceContext; @@ -1837,7 +1853,7 @@ public final class io/sentry/ShutdownHookIntegration : io/sentry/Integration, ja } public final class io/sentry/Span : io/sentry/ISpan { - public fun (Lio/sentry/TransactionContext;Lio/sentry/SentryTracer;Lio/sentry/IHub;Lio/sentry/SentryDate;)V + public fun (Lio/sentry/TransactionContext;Lio/sentry/SentryTracer;Lio/sentry/IHub;Lio/sentry/SentryDate;Lio/sentry/SpanOptions;)V public fun finish ()V public fun finish (Lio/sentry/SpanStatus;)V public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;)V @@ -1846,6 +1862,7 @@ public final class io/sentry/Span : io/sentry/ISpan { public fun getDescription ()Ljava/lang/String; public fun getFinishDate ()Lio/sentry/SentryDate; public fun getOperation ()Ljava/lang/String; + public fun getOptions ()Lio/sentry/SpanOptions; public fun getParentSpanId ()Lio/sentry/SpanId; public fun getSamplingDecision ()Lio/sentry/TracesSamplingDecision; public fun getSpanContext ()Lio/sentry/SpanContext; @@ -1862,15 +1879,19 @@ public final class io/sentry/Span : io/sentry/ISpan { public fun isSampled ()Ljava/lang/Boolean; public fun setData (Ljava/lang/String;Ljava/lang/Object;)V public fun setDescription (Ljava/lang/String;)V + public fun setEndDate (Lio/sentry/SentryDate;)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 setOperation (Ljava/lang/String;)V + public fun setStartDate (Lio/sentry/SentryDate;)V public fun setStatus (Lio/sentry/SpanStatus;)V public fun setTag (Ljava/lang/String;Ljava/lang/String;)V public fun setThrowable (Ljava/lang/Throwable;)V public fun startChild (Ljava/lang/String;)Lio/sentry/ISpan; public fun startChild (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ISpan; public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; public fun toBaggageHeader (Ljava/util/List;)Lio/sentry/BaggageHeader; public fun toSentryTrace ()Lio/sentry/SentryTraceHeader; public fun traceContext ()Lio/sentry/TraceContext; @@ -1942,6 +1963,14 @@ public final class io/sentry/SpanId$Deserializer : io/sentry/JsonDeserializer { public synthetic fun deserialize (Lio/sentry/JsonObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; } +public final class io/sentry/SpanOptions { + public fun ()V + public fun (ZZZ)V + public fun isAutoFinish ()Z + public fun isTrimEnd ()Z + public fun isTrimStart ()Z +} + public final class io/sentry/SpanStatus : java/lang/Enum, io/sentry/JsonSerializable { public static final field ABORTED Lio/sentry/SpanStatus; public static final field ALREADY_EXISTS Lio/sentry/SpanStatus; diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index bcfa9b87f7e..7228341d2a4 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -777,6 +777,19 @@ public void flush(long timeoutMillis) { return span; } + @Override + public @Nullable ISpan getRootSpan() { + ISpan span = null; + if (!isEnabled()) { + options + .getLogger() + .log(SentryLevel.WARNING, "Instance is disabled and this 'getRootSpan' call is a no-op."); + } else { + span = stack.peek().getScope().getRootSpan(); + } + return span; + } + @Override @ApiStatus.Internal public void setSpanContext( diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index 0a7756863a2..413362304ca 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -220,6 +220,11 @@ public void setSpanContext( return Sentry.getCurrentHub().getSpan(); } + @Override + public @Nullable ISpan getRootSpan() { + return Sentry.getCurrentHub().getRootSpan(); + } + @Override public @NotNull SentryOptions getOptions() { return Sentry.getCurrentHub().getOptions(); diff --git a/sentry/src/main/java/io/sentry/IHub.java b/sentry/src/main/java/io/sentry/IHub.java index 01d8546d84a..547bb05fefc 100644 --- a/sentry/src/main/java/io/sentry/IHub.java +++ b/sentry/src/main/java/io/sentry/IHub.java @@ -543,6 +543,9 @@ void setSpanContext( @Nullable ISpan getSpan(); + @Nullable + ISpan getRootSpan(); + /** * Gets the {@link SentryOptions} attached to current scope. * diff --git a/sentry/src/main/java/io/sentry/ISpan.java b/sentry/src/main/java/io/sentry/ISpan.java index 0b10fdee259..5c27fe4388a 100644 --- a/sentry/src/main/java/io/sentry/ISpan.java +++ b/sentry/src/main/java/io/sentry/ISpan.java @@ -16,6 +16,11 @@ public interface ISpan { @NotNull ISpan startChild(@NotNull String operation); + @ApiStatus.Internal + @NotNull + ISpan startChild( + @NotNull String operation, @Nullable String description, @NotNull SpanOptions spanOptions); + @ApiStatus.Internal @NotNull ISpan startChild( @@ -24,6 +29,15 @@ ISpan startChild( @Nullable SentryDate timestamp, @NotNull Instrumenter instrumenter); + @ApiStatus.Internal + @NotNull + ISpan startChild( + @NotNull String operation, + @Nullable String description, + @Nullable SentryDate timestamp, + @NotNull Instrumenter instrumenter, + @NotNull SpanOptions spanOptions); + /** * Starts a child Span. * diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index 52bef0ff43a..a4056e502da 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -177,6 +177,11 @@ public void setSpanContext( return null; } + @Override + public @Nullable ISpan getRootSpan() { + return null; + } + @Override public @NotNull SentryOptions getOptions() { return emptyOptions; diff --git a/sentry/src/main/java/io/sentry/NoOpSpan.java b/sentry/src/main/java/io/sentry/NoOpSpan.java index e89d49b7997..fbfc28d77fe 100644 --- a/sentry/src/main/java/io/sentry/NoOpSpan.java +++ b/sentry/src/main/java/io/sentry/NoOpSpan.java @@ -20,6 +20,12 @@ public static NoOpSpan getInstance() { return NoOpSpan.getInstance(); } + @Override + public @NotNull ISpan startChild( + @NotNull String operation, @Nullable String description, @NotNull SpanOptions spanOptions) { + return NoOpSpan.getInstance(); + } + @Override public @NotNull ISpan startChild( @NotNull String operation, @@ -29,6 +35,16 @@ public static NoOpSpan getInstance() { return NoOpSpan.getInstance(); } + @Override + public @NotNull ISpan startChild( + @NotNull String operation, + @Nullable String description, + @Nullable SentryDate timestamp, + @NotNull Instrumenter instrumenter, + @NotNull SpanOptions spanOptions) { + return NoOpSpan.getInstance(); + } + @Override public @NotNull ISpan startChild( final @NotNull String operation, final @Nullable String description) { diff --git a/sentry/src/main/java/io/sentry/NoOpTransaction.java b/sentry/src/main/java/io/sentry/NoOpTransaction.java index dd04a4f6f31..30344eeb019 100644 --- a/sentry/src/main/java/io/sentry/NoOpTransaction.java +++ b/sentry/src/main/java/io/sentry/NoOpTransaction.java @@ -41,6 +41,12 @@ public void setName(@NotNull String name, @NotNull TransactionNameSource transac return NoOpSpan.getInstance(); } + @Override + public @NotNull ISpan startChild( + @NotNull String operation, @Nullable String description, @NotNull SpanOptions spanOptions) { + return NoOpSpan.getInstance(); + } + @Override public @NotNull ISpan startChild( @NotNull String operation, @@ -50,6 +56,16 @@ public void setName(@NotNull String name, @NotNull TransactionNameSource transac return NoOpSpan.getInstance(); } + @Override + public @NotNull ISpan startChild( + @NotNull String operation, + @Nullable String description, + @Nullable SentryDate timestamp, + @NotNull Instrumenter instrumenter, + @NotNull SpanOptions spanOptions) { + return NoOpSpan.getInstance(); + } + @Override public @NotNull ISpan startChild( final @NotNull String operation, final @Nullable String description) { diff --git a/sentry/src/main/java/io/sentry/Scope.java b/sentry/src/main/java/io/sentry/Scope.java index ac276686111..0397214f0b9 100644 --- a/sentry/src/main/java/io/sentry/Scope.java +++ b/sentry/src/main/java/io/sentry/Scope.java @@ -199,6 +199,16 @@ public ISpan getSpan() { return tx; } + /** + * Returns current active root Span, aka the Transaction. + * + * @return current active Span or Transaction or null if transaction has not been set. + */ + @Nullable + public ISpan getRootSpan() { + return transaction; + } + /** * Sets the current active transaction * diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 4abcba8b1c3..18a99463435 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -830,6 +830,15 @@ public static void endSession() { return getCurrentHub().getSpan(); } + /** + * Gets the current active transaction or span. + * + * @return the active span or null when no active transaction is running + */ + public static @Nullable ISpan getRootSpan() { + return getCurrentHub().getRootSpan(); + } + /** * Returns if the App has crashed (Process has terminated) during the last run. It only returns * true or false if offline caching {{@link SentryOptions#getCacheDirPath()} } is set with a valid diff --git a/sentry/src/main/java/io/sentry/SentryDate.java b/sentry/src/main/java/io/sentry/SentryDate.java index 097c3dca9a8..d2620ab3024 100644 --- a/sentry/src/main/java/io/sentry/SentryDate.java +++ b/sentry/src/main/java/io/sentry/SentryDate.java @@ -37,6 +37,14 @@ public long diff(final @NotNull SentryDate otherDate) { return nanoTimestamp() - otherDate.nanoTimestamp(); } + public final boolean isBefore(final @NotNull SentryDate otherDate) { + return diff(otherDate) < 0; + } + + public final boolean isAfter(final @NotNull SentryDate otherDate) { + return diff(otherDate) > 0; + } + @Override public int compareTo(@NotNull SentryDate otherDate) { return Long.valueOf(nanoTimestamp()).compareTo(otherDate.nanoTimestamp()); diff --git a/sentry/src/main/java/io/sentry/SentryTracer.java b/sentry/src/main/java/io/sentry/SentryTracer.java index 10c774a839b..e4bbfb7e891 100644 --- a/sentry/src/main/java/io/sentry/SentryTracer.java +++ b/sentry/src/main/java/io/sentry/SentryTracer.java @@ -124,7 +124,7 @@ public SentryTracer( Objects.requireNonNull(context, "context is required"); Objects.requireNonNull(hub, "hub is required"); this.measurements = new ConcurrentHashMap<>(); - this.root = new Span(context, this, hub, startTimestamp); + this.root = new Span(context, this, hub, startTimestamp, new SpanOptions()); this.name = context.getName(); this.instrumenter = context.getInstrumenter(); this.hub = hub; @@ -207,9 +207,25 @@ ISpan startChild( final @NotNull SpanId parentSpanId, final @NotNull String operation, final @Nullable String description) { - final ISpan span = createChild(parentSpanId, operation); - span.setDescription(description); - return span; + return startChild(parentSpanId, operation, description, new SpanOptions()); + } + + /** + * Starts a child Span with given trace id and parent span id. + * + * @param parentSpanId - parent span id + * @param operation - span operation name + * @param description - span description + * @param spanOptions - span options + * @return a new transaction span + */ + @NotNull + ISpan startChild( + final @NotNull SpanId parentSpanId, + final @NotNull String operation, + final @Nullable String description, + final @NotNull SpanOptions spanOptions) { + return createChild(parentSpanId, operation, description, spanOptions); } @NotNull @@ -219,18 +235,37 @@ ISpan startChild( final @Nullable String description, final @Nullable SentryDate timestamp, final @NotNull Instrumenter instrumenter) { - return createChild(parentSpanId, operation, description, timestamp, instrumenter); + return createChild( + parentSpanId, operation, description, timestamp, instrumenter, new SpanOptions()); + } + + @NotNull + ISpan startChild( + final @NotNull SpanId parentSpanId, + final @NotNull String operation, + final @Nullable String description, + final @Nullable SentryDate timestamp, + final @NotNull Instrumenter instrumenter, + final @NotNull SpanOptions spanOptions) { + return createChild(parentSpanId, operation, description, timestamp, instrumenter, spanOptions); } /** * Starts a child Span with given trace id and parent span id. * * @param parentSpanId - parent span id + * @param operation - the span operation + * @param description - the optional span description + * @param options - span options * @return a new transaction span */ @NotNull - private ISpan createChild(final @NotNull SpanId parentSpanId, final @NotNull String operation) { - return createChild(parentSpanId, operation, null, null, Instrumenter.SENTRY); + private ISpan createChild( + final @NotNull SpanId parentSpanId, + final @NotNull String operation, + final @Nullable String description, + final @NotNull SpanOptions options) { + return createChild(parentSpanId, operation, description, null, Instrumenter.SENTRY, options); } @NotNull @@ -239,7 +274,8 @@ private ISpan createChild( final @NotNull String operation, final @Nullable String description, @Nullable SentryDate timestamp, - final @NotNull Instrumenter instrumenter) { + final @NotNull Instrumenter instrumenter, + final @NotNull SpanOptions spanOptions) { if (root.isFinished()) { return NoOpSpan.getInstance(); } @@ -259,6 +295,7 @@ private ISpan createChild( operation, this.hub, timestamp, + spanOptions, __ -> { final FinishStatus finishStatus = this.finishStatus; if (idleTimeout != null) { @@ -284,24 +321,41 @@ private ISpan createChild( @Override public @NotNull ISpan startChild( - final @NotNull String operation, + @NotNull String operation, @Nullable String description, @Nullable SentryDate timestamp, @NotNull Instrumenter instrumenter) { - return createChild(operation, description, timestamp, instrumenter); + return startChild(operation, description, timestamp, instrumenter, new SpanOptions()); + } + + @Override + public @NotNull ISpan startChild( + final @NotNull String operation, + @Nullable String description, + @Nullable SentryDate timestamp, + @NotNull Instrumenter instrumenter, + @NotNull SpanOptions spanOptions) { + return createChild(operation, description, timestamp, instrumenter, spanOptions); } @Override public @NotNull ISpan startChild( final @NotNull String operation, final @Nullable String description) { - return createChild(operation, description, null, Instrumenter.SENTRY); + return startChild(operation, description, null, Instrumenter.SENTRY, new SpanOptions()); + } + + @Override + public @NotNull ISpan startChild( + @NotNull String operation, @Nullable String description, @NotNull SpanOptions spanOptions) { + return createChild(operation, description, null, Instrumenter.SENTRY, spanOptions); } private @NotNull ISpan createChild( final @NotNull String operation, final @Nullable String description, @Nullable SentryDate timestamp, - final @NotNull Instrumenter instrumenter) { + final @NotNull Instrumenter instrumenter, + final @NotNull SpanOptions spanOptions) { if (root.isFinished()) { return NoOpSpan.getInstance(); } @@ -311,7 +365,7 @@ private ISpan createChild( } if (children.size() < hub.getOptions().getMaxSpans()) { - return root.startChild(operation, description, timestamp, instrumenter); + return root.startChild(operation, description, timestamp, instrumenter, spanOptions); } else { hub.getOptions() .getLogger() @@ -343,6 +397,44 @@ public void finish(@Nullable SpanStatus status) { @Override @ApiStatus.Internal public void finish(@Nullable SpanStatus status, @Nullable SentryDate finishDate) { + + // auto-finish any open spans + for (Span span : getSpans()) { + if (span.getOptions().isAutoFinish()) { + if (finishDate != null) { + span.finish(getSpanContext().status, finishDate); + } else { + span.finish(); + } + } + } + + // trim span to children if enabled + for (Span span : children) { + @Nullable SentryDate minChildStart = null; + @Nullable SentryDate maxChildEnd = null; + + if (span.getOptions().isTrimStart() || span.getOptions().isTrimEnd()) { + for (Span child : children) { + if (child.getParentSpanId() != null && child.getParentSpanId().equals(span.getSpanId())) { + if (minChildStart == null || child.getStartDate().isBefore(minChildStart)) { + minChildStart = child.getStartDate(); + } + if (maxChildEnd == null + || (child.getFinishDate() != null && child.getFinishDate().isAfter(maxChildEnd))) { + maxChildEnd = child.getFinishDate(); + } + } + } + if (span.getOptions().isTrimStart() && minChildStart != null) { + span.setStartDate(minChildStart); + } + if (span.getOptions().isTrimEnd() && maxChildEnd != null) { + span.setEndDate(maxChildEnd); + } + } + } + this.finishStatus = FinishStatus.finishing(status); if (!root.isFinished() && (!waitForChildren || hasAllChildrenFinished())) { PerformanceCollectionData performanceCollectionData = null; diff --git a/sentry/src/main/java/io/sentry/Span.java b/sentry/src/main/java/io/sentry/Span.java index 428e73cb9ad..826e59fe459 100644 --- a/sentry/src/main/java/io/sentry/Span.java +++ b/sentry/src/main/java/io/sentry/Span.java @@ -9,13 +9,12 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.VisibleForTesting; @ApiStatus.Internal public final class Span implements ISpan { /** The moment in time when span was started. */ - private final @NotNull SentryDate startTimestamp; + private @NotNull SentryDate startTimestamp; /** The moment in time when span has ended. */ private @Nullable SentryDate timestamp; @@ -35,6 +34,8 @@ public final class Span implements ISpan { private final @NotNull AtomicBoolean finished = new AtomicBoolean(false); + private final @NotNull SpanOptions options; + private @Nullable SpanFinishedCallback spanFinishedCallback; private final @NotNull Map data = new ConcurrentHashMap<>(); @@ -45,7 +46,7 @@ public final class Span implements ISpan { final @NotNull SentryTracer transaction, final @NotNull String operation, final @NotNull IHub hub) { - this(traceId, parentSpanId, transaction, operation, hub, null, null); + this(traceId, parentSpanId, transaction, operation, hub, null, new SpanOptions(), null); } Span( @@ -55,12 +56,14 @@ public final class Span implements ISpan { final @NotNull String operation, final @NotNull IHub hub, final @Nullable SentryDate startTimestamp, + final @NotNull SpanOptions options, final @Nullable SpanFinishedCallback spanFinishedCallback) { this.context = new SpanContext( traceId, new SpanId(), operation, parentSpanId, transaction.getSamplingDecision()); this.transaction = Objects.requireNonNull(transaction, "transaction is required"); this.hub = Objects.requireNonNull(hub, "hub is required"); + this.options = options; this.spanFinishedCallback = spanFinishedCallback; if (startTimestamp != null) { this.startTimestamp = startTimestamp; @@ -69,12 +72,12 @@ public final class Span implements ISpan { } } - @VisibleForTesting public Span( final @NotNull TransactionContext context, final @NotNull SentryTracer sentryTracer, final @NotNull IHub hub, - final @Nullable SentryDate startTimestamp) { + final @Nullable SentryDate startTimestamp, + final @NotNull SpanOptions options) { this.context = Objects.requireNonNull(context, "context is required"); this.transaction = Objects.requireNonNull(sentryTracer, "sentryTracer is required"); this.hub = Objects.requireNonNull(hub, "hub is required"); @@ -84,6 +87,7 @@ public Span( } else { this.startTimestamp = hub.getOptions().getDateProvider().now(); } + this.options = options; } public @NotNull SentryDate getStartDate() { @@ -104,13 +108,14 @@ public Span( final @NotNull String operation, final @Nullable String description, final @Nullable SentryDate timestamp, - final @NotNull Instrumenter instrumenter) { + final @NotNull Instrumenter instrumenter, + @NotNull SpanOptions spanOptions) { if (finished.get()) { return NoOpSpan.getInstance(); } return transaction.startChild( - context.getSpanId(), operation, description, timestamp, instrumenter); + context.getSpanId(), operation, description, timestamp, instrumenter, spanOptions); } @Override @@ -123,6 +128,24 @@ public Span( return transaction.startChild(context.getSpanId(), operation, description); } + @Override + public @NotNull ISpan startChild( + @NotNull String operation, @Nullable String description, @NotNull SpanOptions spanOptions) { + if (finished.get()) { + return NoOpSpan.getInstance(); + } + return transaction.startChild(context.getSpanId(), operation, description, spanOptions); + } + + @Override + public @NotNull ISpan startChild( + @NotNull String operation, + @Nullable String description, + @Nullable SentryDate timestamp, + @NotNull Instrumenter instrumenter) { + return startChild(operation, description, timestamp, instrumenter, new SpanOptions()); + } + @Override public @NotNull SentryTraceHeader toSentryTrace() { return new SentryTraceHeader(context.getTraceId(), context.getSpanId(), context.getSampled()); @@ -317,4 +340,17 @@ public boolean isNoOp() { void setSpanFinishedCallback(final @Nullable SpanFinishedCallback callback) { this.spanFinishedCallback = callback; } + + public void setStartDate(SentryDate date) { + this.startTimestamp = date; + } + + public void setEndDate(SentryDate date) { + this.timestamp = date; + } + + @NotNull + public SpanOptions getOptions() { + return options; + } } diff --git a/sentry/src/main/java/io/sentry/SpanOptions.java b/sentry/src/main/java/io/sentry/SpanOptions.java new file mode 100644 index 00000000000..2218148dd1d --- /dev/null +++ b/sentry/src/main/java/io/sentry/SpanOptions.java @@ -0,0 +1,40 @@ +package io.sentry; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public final class SpanOptions { + + private final boolean trimStart; + private final boolean trimEnd; + private final boolean autoFinish; + + public SpanOptions() { + this(false, false, false); + } + + /** + * @param trimStart true if the start time should be trimmed to the minimum start time of it's + * children + * @param trimEnd true if the end time should be trimmed to the maximum end time of it's children + * @param autoFinish true if this span should be finished whenever the root transaction gets + * finished + */ + public SpanOptions(final boolean trimStart, final boolean trimEnd, final boolean autoFinish) { + this.trimStart = trimStart; + this.trimEnd = trimEnd; + this.autoFinish = autoFinish; + } + + public boolean isTrimStart() { + return trimStart; + } + + public boolean isTrimEnd() { + return trimEnd; + } + + public boolean isAutoFinish() { + return autoFinish; + } +} diff --git a/sentry/src/test/java/io/sentry/SentryLongDateTest.kt b/sentry/src/test/java/io/sentry/SentryLongDateTest.kt index a1b7f81fa54..1f97d3c852e 100644 --- a/sentry/src/test/java/io/sentry/SentryLongDateTest.kt +++ b/sentry/src/test/java/io/sentry/SentryLongDateTest.kt @@ -2,6 +2,8 @@ package io.sentry import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue class SentryLongDateTest { @@ -50,4 +52,48 @@ class SentryLongDateTest { assertEquals(1, 1672742031123456789.compareTo(1672742031123456788)) assertEquals(1, date1.compareTo(date2)) } + + // isBefore() + @Test + fun `isBefore() returns true if for an earlier date`() { + val date1 = SentryLongDate(0) + val date2 = SentryLongDate(1) + assertTrue(date1.isBefore(date2)) + } + + @Test + fun `isBefore() returns false for the same date`() { + val date1 = SentryLongDate(1) + val date2 = SentryLongDate(1) + assertFalse(date1.isBefore(date2)) + } + + @Test + fun `isBefore() returns false for a later date`() { + val date1 = SentryLongDate(2) + val date2 = SentryLongDate(1) + assertFalse(date1.isBefore(date2)) + } + + // isAfter() + @Test + fun `isAfter() returns true if for a later date`() { + val date1 = SentryLongDate(2) + val date2 = SentryLongDate(1) + assertTrue(date1.isAfter(date2)) + } + + @Test + fun `isAfter() returns false for the same date`() { + val date1 = SentryLongDate(1) + val date2 = SentryLongDate(1) + assertFalse(date1.isAfter(date2)) + } + + @Test + fun `isAfter() returns false for a sooner date`() { + val date1 = SentryLongDate(1) + val date2 = SentryLongDate(2) + assertFalse(date1.isAfter(date2)) + } } diff --git a/sentry/src/test/java/io/sentry/SentryTracerTest.kt b/sentry/src/test/java/io/sentry/SentryTracerTest.kt index d25631a5a3d..ba22ff8da67 100644 --- a/sentry/src/test/java/io/sentry/SentryTracerTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTracerTest.kt @@ -909,4 +909,130 @@ class SentryTracerTest { assertEquals("new-name-2", transaction.name) assertEquals(TransactionNameSource.CUSTOM, transaction.transactionNameSource) } + + @Test + fun `when spans have auto-finish enabled, finish them on transaction finish`() { + val transaction = fixture.getSut(waitForChildren = true, idleTimeout = 50, trimEnd = true, samplingDecision = TracesSamplingDecision(true)) + + // when a span is created with auto-finish + val span = transaction.startChild("composition", null, SpanOptions(false, false, true)) as Span + + // and the transaction is finished + transaction.finish() + + // then the span should be finished as well + assertTrue(span.isFinished) + + // and the transaction should be captured + verify(fixture.hub).captureTransaction( + check { + assertEquals(1, it.spans.size) + assertEquals(transaction.root.finishDate, span.finishDate) + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + + @Test + fun `when spans have auto-finish enabled, finish them on transaction finish using the transaction finish date`() { + val transaction = fixture.getSut(waitForChildren = true, idleTimeout = 50, trimEnd = true, samplingDecision = TracesSamplingDecision(true)) + + // when a span is created with auto-finish + val span = transaction.startChild("composition", null, SpanOptions(false, false, true)) as Span + + // and the transaction is finished + Thread.sleep(1) + val transactionFinishDate = SentryAutoDateProvider().now() + transaction.finish(SpanStatus.OK, transactionFinishDate) + + // then the span should be finished as well + assertTrue(span.isFinished) + + // and the transaction should be captured + verify(fixture.hub).captureTransaction( + check { + assertEquals(1, it.spans.size) + assertEquals(transactionFinishDate, span.finishDate) + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + + @Test + fun `when spans have trim-start enabled, trim them on transaction finish`() { + val transaction = fixture.getSut(waitForChildren = true, idleTimeout = 50, trimEnd = true, samplingDecision = TracesSamplingDecision(true)) + + // when a parent span is created + val parentSpan = transaction.startChild("composition", null, SpanOptions(true, false, true)) as Span + + // with a child which starts later + Thread.sleep(5) + val child1 = parentSpan.startChild("child1") as Span + child1.finish() + + val child2 = parentSpan.startChild("child2") as Span + Thread.sleep(5) + child2.finish() + + parentSpan.finish() + + val expectedParentStartDate = child1.startDate + val expectedParentEndDate = parentSpan.finishDate + + transaction.finish() + + assertTrue(parentSpan.isFinished) + assertEquals(expectedParentStartDate, parentSpan.startDate) + assertEquals(expectedParentEndDate, parentSpan.finishDate) + + verify(fixture.hub).captureTransaction( + check { + assertEquals(3, it.spans.size) + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + + @Test + fun `when spans have trim-end enabled, trim them on transaction finish`() { + val transaction = fixture.getSut(waitForChildren = true, idleTimeout = 50, trimEnd = true, samplingDecision = TracesSamplingDecision(true)) + + // when a parent span is created + val parentSpan = transaction.startChild("composition", null, SpanOptions(false, true, true)) as Span + + // with a child which starts later + Thread.sleep(5) + val child1 = parentSpan.startChild("child1") as Span + child1.finish() + + val child2 = parentSpan.startChild("child2") as Span + Thread.sleep(5) + child2.finish() + + parentSpan.finish() + + val expectedParentStartDate = parentSpan.startDate + val expectedParentEndDate = child2.finishDate + + transaction.finish() + + assertTrue(parentSpan.isFinished) + assertEquals(expectedParentStartDate, parentSpan.startDate) + assertEquals(expectedParentEndDate, parentSpan.finishDate) + + verify(fixture.hub).captureTransaction( + check { + assertEquals(3, it.spans.size) + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } }