Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Features

- Add opt-in Android session trace lifecycle support ([#5398](https://github.com/getsentry/sentry-java/pull/5398))

## 8.41.0

### Features
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ private void startTracing(final @NotNull Activity activity) {
if (scopes != null && !isRunningTransactionOrTrace(activity)) {
if (!performanceEnabled) {
activitiesWithOngoingTransactions.put(activity, NoOpTransaction.getInstance());
if (options.isEnableAutoTraceIdGeneration()) {
if (options.isEnableAutoTraceIdGeneration() && !options.isEnableSessionTraceLifecycle()) {
TracingUtils.startNewTrace(scopes);
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ final class ManifestMetadataReader {
static final String ENABLE_AUTO_TRACE_ID_GENERATION =
"io.sentry.traces.enable-auto-id-generation";

static final String ENABLE_SESSION_TRACE_LIFECYCLE =
"io.sentry.traces.enable-session-trace-lifecycle";

static final String DEADLINE_TIMEOUT = "io.sentry.traces.deadline-timeout";

static final String FEEDBACK_NAME_REQUIRED = "io.sentry.feedback.is-name-required";
Expand Down Expand Up @@ -510,6 +513,13 @@ static void applyMetadata(
ENABLE_AUTO_TRACE_ID_GENERATION,
options.isEnableAutoTraceIdGeneration()));

options.setEnableSessionTraceLifecycle(
readBool(
metadata,
logger,
ENABLE_SESSION_TRACE_LIFECYCLE,
options.isEnableSessionTraceLifecycle()));

options.setDeadlineTimeout(
readLong(metadata, logger, DEADLINE_TIMEOUT, options.getDeadlineTimeout()));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ private void startTracing(final @NotNull UiElement target, final @NotNull Gestur

if (!(options.isTracingEnabled() && options.isEnableUserInteractionTracing())) {
if (isNewInteraction) {
if (options.isEnableAutoTraceIdGeneration()) {
if (options.isEnableAutoTraceIdGeneration() && !options.isEnableSessionTraceLifecycle()) {
TracingUtils.startNewTrace(scopes);
}
activeUiElement = target;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1413,6 +1413,30 @@ class ActivityLifecycleIntegrationTest {
assertSame(propagationContextAtStart, scope.propagationContext)
}

@Test
fun `does not start a new trace if performance is disabled and session trace lifecycle is enabled`() {
val sut = fixture.getSut()
val activity = mock<Activity>()
fixture.options.tracesSampleRate = null
fixture.options.isEnableAutoTraceIdGeneration = true
fixture.options.isEnableSessionTraceLifecycle = true

val argumentCaptor: ArgumentCaptor<ScopeCallback> =
ArgumentCaptor.forClass(ScopeCallback::class.java)
val scope = Scope(fixture.options)
val propagationContextAtStart = scope.propagationContext
whenever(fixture.scopes.configureScope(argumentCaptor.capture())).thenAnswer {
argumentCaptor.value.run(scope)
}

sut.register(fixture.scopes, fixture.options)
sut.onActivityCreated(activity, fixture.bundle)

// once for the screen
verify(fixture.scopes).configureScope(any())
assertSame(propagationContextAtStart, scope.propagationContext)
}

@Test
fun `sets the activity as the current screen`() {
val sut = fixture.getSut()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,16 @@ class ManifestMetadataReaderTest {
assertFalse(fixture.options.isEnableAutoSessionTracking)
}

@Test
fun `applyMetadata reads session trace lifecycle to options`() {
val bundle = bundleOf(ManifestMetadataReader.ENABLE_SESSION_TRACE_LIFECYCLE to true)
val context = fixture.getContext(metaData = bundle)

ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)

assertTrue(fixture.options.isEnableSessionTraceLifecycle)
}

@Test
fun `applyMetadata reads environment to options`() {
// Arrange
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertSame
import org.mockito.ArgumentCaptor
import org.mockito.kotlin.any
import org.mockito.kotlin.check
Expand Down Expand Up @@ -62,13 +63,15 @@ class SentryGestureListenerTracingTest {
isEnableUserInteractionTracing: Boolean = true,
transaction: SentryTracer? = null,
isEnableAutoTraceIdGeneration: Boolean = true,
isEnableSessionTraceLifecycle: Boolean = false,
): SentryGestureListener {
options.tracesSampleRate = tracesSampleRate
options.isEnableUserInteractionTracing = isEnableUserInteractionTracing
options.isEnableUserInteractionBreadcrumbs = true
options.gestureTargetLocators =
listOf(AndroidViewGestureTargetLocator(LazyEvaluator { true }))
options.isEnableAutoTraceIdGeneration = isEnableAutoTraceIdGeneration
options.isEnableSessionTraceLifecycle = isEnableSessionTraceLifecycle

whenever(scopes.options).thenReturn(options)

Expand Down Expand Up @@ -398,6 +401,23 @@ class SentryGestureListenerTracingTest {
)
}

@Test
fun `when tracing is disabled and session trace lifecycle is enabled, does not start a new trace`() {
val sut =
fixture.getSut<View>(
tracesSampleRate = null,
isEnableAutoTraceIdGeneration = true,
isEnableSessionTraceLifecycle = true,
)
val scope = Scope(fixture.options)
val initialPropagationContext = scope.propagationContext

sut.onSingleTapUp(fixture.event)

verify(fixture.scopes, never()).configureScope(any())
assertSame(initialPropagationContext, scope.propagationContext)
}

internal open class ScrollableListView : AbsListView(mock()) {
override fun getAdapter(): ListAdapter = mock()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,9 @@ constructor(
arguments: Map<String, Any?>,
) {
if (!isPerformanceEnabled) {
TracingUtils.startNewTrace(scopes)
if (!scopes.options.isEnableSessionTraceLifecycle) {
TracingUtils.startNewTrace(scopes)
}
return
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotSame
import kotlin.test.assertNull
import kotlin.test.assertSame
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.kotlin.any
Expand Down Expand Up @@ -60,12 +61,14 @@ class SentryNavigationListenerTest {
hasViewIdInRes: Boolean = true,
transaction: SentryTracer? = null,
traceOriginAppendix: String? = null,
enableSessionTraceLifecycle: Boolean = false,
): SentryNavigationListener {
options =
SentryOptions().apply {
dsn = "http://key@localhost/proj"
setTracesSampleRate(tracesSampleRate)
isEnableScreenTracking = enableScreenTracking
isEnableSessionTraceLifecycle = enableSessionTraceLifecycle
}
whenever(scopes.options).thenReturn(options)

Expand Down Expand Up @@ -363,6 +366,24 @@ class SentryNavigationListenerTest {
assertNotSame(propagationContextAtStart, scope.propagationContext)
}

@Test
fun `does not start new trace if performance is disabled and session trace lifecycle is enabled`() {
val sut = fixture.getSut(enableNavigationTracing = false, enableSessionTraceLifecycle = true)

val argumentCaptor: ArgumentCaptor<ScopeCallback> =
ArgumentCaptor.forClass(ScopeCallback::class.java)
val scope = Scope(fixture.options)
val propagationContextAtStart = scope.propagationContext
whenever(fixture.scopes.configureScope(argumentCaptor.capture())).thenAnswer {
argumentCaptor.value.run(scope)
}

sut.onDestinationChanged(fixture.navController, fixture.destination, null)

verify(fixture.scopes).configureScope(any())
assertSame(propagationContextAtStart, scope.propagationContext)
}

@Test
fun `onDestinationChanged sets trace origin`() {
val sut = fixture.getSut()
Expand Down
4 changes: 4 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@ public final class io/sentry/ExternalOptions {
public fun isEnableMetrics ()Ljava/lang/Boolean;
public fun isEnablePrettySerializationOutput ()Ljava/lang/Boolean;
public fun isEnableQueueTracing ()Ljava/lang/Boolean;
public fun isEnableSessionTraceLifecycle ()Ljava/lang/Boolean;
public fun isEnableSpotlight ()Ljava/lang/Boolean;
public fun isEnabled ()Ljava/lang/Boolean;
public fun isForceInit ()Ljava/lang/Boolean;
Expand All @@ -550,6 +551,7 @@ public final class io/sentry/ExternalOptions {
public fun setEnableMetrics (Ljava/lang/Boolean;)V
public fun setEnablePrettySerializationOutput (Ljava/lang/Boolean;)V
public fun setEnableQueueTracing (Ljava/lang/Boolean;)V
public fun setEnableSessionTraceLifecycle (Ljava/lang/Boolean;)V
public fun setEnableSpotlight (Ljava/lang/Boolean;)V
public fun setEnableUncaughtExceptionHandler (Ljava/lang/Boolean;)V
public fun setEnabled (Ljava/lang/Boolean;)V
Expand Down Expand Up @@ -3719,6 +3721,7 @@ public class io/sentry/SentryOptions {
public fun isEnableQueueTracing ()Z
public fun isEnableScopePersistence ()Z
public fun isEnableScreenTracking ()Z
public fun isEnableSessionTraceLifecycle ()Z
public fun isEnableShutdownHook ()Z
public fun isEnableSpotlight ()Z
public fun isEnableTimeToFullDisplayTracing ()Z
Expand Down Expand Up @@ -3780,6 +3783,7 @@ public class io/sentry/SentryOptions {
public fun setEnableQueueTracing (Z)V
public fun setEnableScopePersistence (Z)V
public fun setEnableScreenTracking (Z)V
public fun setEnableSessionTraceLifecycle (Z)V
public fun setEnableShutdownHook (Z)V
public fun setEnableSpotlight (Z)V
public fun setEnableTimeToFullDisplayTracing (Z)V
Expand Down
19 changes: 19 additions & 0 deletions sentry/src/main/java/io/sentry/Baggage.java
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,25 @@ public Baggage(final @NotNull Baggage baggage) {
baggage.logger);
}

@ApiStatus.Internal
static @NotNull Baggage copyWithOverrides(
final @NotNull Baggage baggage,
final @NotNull SentryId traceId,
final @Nullable Double sampleRand) {
final @NotNull ConcurrentHashMap<String, String> keyValues =
new ConcurrentHashMap<>(baggage.keyValues);
keyValues.put(DSCKeys.TRACE_ID, traceId.toString());

return new Baggage(
keyValues,
baggage.sampleRate,
sampleRand,
baggage.thirdPartyHeader,
baggage.mutable,
baggage.shouldFreeze,
baggage.logger);
}

@ApiStatus.Internal
public Baggage(
final @NotNull ConcurrentHashMap<String, String> keyValues,
Expand Down
11 changes: 11 additions & 0 deletions sentry/src/main/java/io/sentry/ExternalOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public final class ExternalOptions {
private @Nullable ProfileLifecycle profileLifecycle;

private @Nullable Boolean strictTraceContinuation;
private @Nullable Boolean enableSessionTraceLifecycle;
private @Nullable String orgId;

private @Nullable SentryOptions.Cron cron;
Expand Down Expand Up @@ -229,6 +230,8 @@ public final class ExternalOptions {

options.setStrictTraceContinuation(
propertiesProvider.getBooleanProperty("enable-strict-trace-continuation"));
options.setEnableSessionTraceLifecycle(
propertiesProvider.getBooleanProperty("enable-session-trace-lifecycle"));
options.setOrgId(propertiesProvider.getProperty("org-id"));

options.setEnableSpotlight(propertiesProvider.getBooleanProperty("enable-spotlight"));
Expand Down Expand Up @@ -647,6 +650,14 @@ public void setStrictTraceContinuation(final @Nullable Boolean strictTraceContin
this.strictTraceContinuation = strictTraceContinuation;
}

public @Nullable Boolean isEnableSessionTraceLifecycle() {
return enableSessionTraceLifecycle;
}

public void setEnableSessionTraceLifecycle(final @Nullable Boolean enableSessionTraceLifecycle) {
this.enableSessionTraceLifecycle = enableSessionTraceLifecycle;
}

public @Nullable String getOrgId() {
return orgId;
}
Expand Down
3 changes: 3 additions & 0 deletions sentry/src/main/java/io/sentry/PropagationContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,12 @@ public static PropagationContext fromHeaders(

private final @NotNull Baggage baggage;

@ApiStatus.Internal
public PropagationContext() {
this(new SentryId(), new SpanId(), null, null, null);
}

@ApiStatus.Internal
public PropagationContext(final @NotNull PropagationContext propagationContext) {
this(
propagationContext.getTraceId(),
Expand All @@ -96,6 +98,7 @@ public PropagationContext(final @NotNull PropagationContext propagationContext)
propagationContext.isSampled());
}

@ApiStatus.Internal
public PropagationContext(
final @NotNull SentryId traceId,
final @NotNull SpanId spanId,
Expand Down
31 changes: 26 additions & 5 deletions sentry/src/main/java/io/sentry/Scopes.java
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,10 @@ public void startSession() {
getClient().captureSession(pair.getPrevious(), hint);
}

if (getOptions().isEnableSessionTraceLifecycle()) {
configureScope(scope -> scope.setPropagationContext(new PropagationContext()));
}

final Hint hint = HintUtils.createWithTypeCheckHint(new SessionStartHint());

getClient().captureSession(pair.getCurrent(), hint);
Expand Down Expand Up @@ -957,13 +961,18 @@ public void flush(long timeoutMillis) {
SentryLevel.INFO, "Tracing is disabled and this 'startTransaction' returns a no-op.");
transaction = NoOpTransaction.getInstance();
} else {
final Double sampleRand = getSampleRand(transactionContext);
final @NotNull TransactionContext effectiveTransactionContext =
maybeApplySessionTraceLifecycle(transactionContext);
final Double sampleRand = getSampleRand(effectiveTransactionContext);
final SamplingContext samplingContext =
new SamplingContext(
transactionContext, transactionOptions.getCustomSamplingContext(), sampleRand, null);
effectiveTransactionContext,
transactionOptions.getCustomSamplingContext(),
sampleRand,
null);
final @NotNull TracesSampler tracesSampler = getOptions().getInternalTracesSampler();
@NotNull TracesSamplingDecision samplingDecision = tracesSampler.sample(samplingContext);
transactionContext.setSamplingDecision(samplingDecision);
effectiveTransactionContext.setSamplingDecision(samplingDecision);

final @Nullable ISpanFactory maybeSpanFactory = transactionOptions.getSpanFactory();
final @NotNull ISpanFactory spanFactory =
Expand All @@ -977,15 +986,15 @@ public void flush(long timeoutMillis) {
if (samplingDecision.getSampled()
&& getOptions().isContinuousProfilingEnabled()
&& getOptions().getProfileLifecycle() == ProfileLifecycle.TRACE
&& transactionContext.getProfilerId().equals(SentryId.EMPTY_ID)) {
&& effectiveTransactionContext.getProfilerId().equals(SentryId.EMPTY_ID)) {
getOptions()
.getContinuousProfiler()
.startProfiler(ProfileLifecycle.TRACE, getOptions().getInternalTracesSampler());
}

transaction =
spanFactory.createTransaction(
transactionContext, this, transactionOptions, compositePerformanceCollector);
effectiveTransactionContext, this, transactionOptions, compositePerformanceCollector);
// new SentryTracer(
// transactionContext, this, transactionOptions,
// compositePerformanceCollector);
Expand Down Expand Up @@ -1013,6 +1022,18 @@ && getOptions().getProfileLifecycle() == ProfileLifecycle.TRACE
return transaction;
}

private @NotNull TransactionContext maybeApplySessionTraceLifecycle(
final @NotNull TransactionContext transactionContext) {
final @NotNull PropagationContext propagationContext =
getCombinedScopeView().getPropagationContext();
if (getOptions().isEnableSessionTraceLifecycle()
&& transactionContext.getParentSpanId() == null) {
return TransactionContext.fromPropagationContextAsRoot(
propagationContext, transactionContext);
}
return transactionContext;
}

private @NotNull Double getSampleRand(final @NotNull TransactionContext transactionContext) {
final @Nullable Baggage baggage = transactionContext.getBaggage();
if (baggage != null) {
Expand Down
Loading
Loading