Skip to content
Open
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
24 changes: 24 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,28 @@ public final class io/sentry/android/core/AppLifecycleIntegration : io/sentry/In
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
}

public final class io/sentry/android/core/AppStartExtension : io/sentry/IAppStartExtender {
public fun <init> (Lio/sentry/android/core/performance/AppStartMetrics;)V
public fun clear ()V
public fun extendAppStart ()V
public fun finishExtendedAppStart ()V
public fun finishTransaction (Lio/sentry/SentryDate;)V
public fun getExtendedAppStartSpan ()Lio/sentry/ISpan;
public fun getExtendedEndTime ()Lio/sentry/SentryDate;
public fun isActive ()Z
public fun setExtendAppStartListener (Lio/sentry/android/core/AppStartExtension$ExtendAppStartListener;)V
}

public abstract interface class io/sentry/android/core/AppStartExtension$ExtendAppStartListener {
public abstract fun onExtendAppStartRequested ()Lio/sentry/android/core/AppStartExtension$ExtendedAppStart;
}

public final class io/sentry/android/core/AppStartExtension$ExtendedAppStart {
public final field span Lio/sentry/ISpan;
public final field transaction Lio/sentry/ITransaction;
public fun <init> (Lio/sentry/ITransaction;Lio/sentry/ISpan;)V
}

public final class io/sentry/android/core/AppState : java/io/Closeable {
public fun addAppStateListener (Lio/sentry/android/core/AppState$AppStateListener;)V
public fun close ()V
Expand Down Expand Up @@ -745,6 +767,7 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr
public fun getAppStartBaggageHeader ()Ljava/lang/String;
public fun getAppStartContinuousProfiler ()Lio/sentry/IContinuousProfiler;
public fun getAppStartEndTime ()Lio/sentry/SentryDate;
public fun getAppStartExtension ()Lio/sentry/android/core/AppStartExtension;
public fun getAppStartProfiler ()Lio/sentry/ITransactionProfiler;
public fun getAppStartReason ()Ljava/lang/String;
public fun getAppStartSamplingDecision ()Lio/sentry/TracesSamplingDecision;
Expand All @@ -760,6 +783,7 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr
public static fun getInstance ()Lio/sentry/android/core/performance/AppStartMetrics;
public fun getSdkInitTimeSpan ()Lio/sentry/android/core/performance/TimeSpan;
public fun isAppLaunchedInForeground ()Z
public fun isAppStartWindowOpen ()Z
public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V
public fun onActivityDestroyed (Landroid/app/Activity;)V
public fun onActivityPaused (Landroid/app/Activity;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ static void initializeIntegrationsAndProcessors(
}

final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance();
options.setAppStartExtender(appStartMetrics.getAppStartExtension());

if (options.getModulesLoader() instanceof NoOpModulesLoader) {
options.setModulesLoader(new AssetsModulesLoader(context, options));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package io.sentry.android.core;

import io.sentry.IAppStartExtender;
import io.sentry.ISentryLifecycleToken;
import io.sentry.ISpan;
import io.sentry.ITransaction;
import io.sentry.NoOpSpan;
import io.sentry.Sentry;
import io.sentry.SentryDate;
import io.sentry.SentryLevel;
import io.sentry.SpanStatus;
import io.sentry.android.core.performance.AppStartMetrics;
import io.sentry.util.AutoClosableReentrantLock;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@ApiStatus.Internal
public final class AppStartExtension implements IAppStartExtender {

public static final class ExtendedAppStart {
public final @NotNull ITransaction transaction;
public final @NotNull ISpan span;

public ExtendedAppStart(final @NotNull ITransaction transaction, final @NotNull ISpan span) {
this.transaction = transaction;
this.span = span;
}
}

public interface ExtendAppStartListener {
@Nullable
ExtendedAppStart onExtendAppStartRequested();
}

private final @NotNull AppStartMetrics metrics;
private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock();

private @Nullable ExtendAppStartListener extendAppStartListener;
private @Nullable ISpan extendedSpan;
private @Nullable ITransaction extendedTransaction;

public AppStartExtension(final @NotNull AppStartMetrics metrics) {
this.metrics = metrics;
}

public void setExtendAppStartListener(final @Nullable ExtendAppStartListener listener) {
this.extendAppStartListener = listener;
}
Comment thread
sentry[bot] marked this conversation as resolved.
Comment on lines +47 to +49

@Override
public void extendAppStart() {
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
if (extendedSpan != null) {
Sentry.getCurrentScopes()
.getOptions()
.getLogger()
.log(SentryLevel.WARNING, "App start is already being extended.");
return;
}
if (!metrics.isAppStartWindowOpen()) {
Sentry.getCurrentScopes()
.getOptions()
.getLogger()
.log(
SentryLevel.WARNING,
"Cannot extend app start: the app start window has already passed.");
return;
}
final @Nullable ExtendAppStartListener listener = extendAppStartListener;
if (listener != null) {
final @Nullable ExtendedAppStart extended = listener.onExtendAppStartRequested();
if (extended != null) {
this.extendedTransaction = extended.transaction;
this.extendedSpan = extended.span;
}
}
}
}

@Override
public void finishExtendedAppStart() {
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
final @Nullable ISpan span = extendedSpan;
if (span != null && !span.isFinished()) {
span.finish(SpanStatus.OK);
}
}
}

@Override
public @NotNull ISpan getExtendedAppStartSpan() {
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
final @Nullable ISpan span = extendedSpan;
if (span != null && !span.isFinished()) {
return span;
}
return NoOpSpan.getInstance();
}
}

public boolean isActive() {
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
return extendedTransaction != null && !extendedTransaction.isFinished();
}
}

public void finishTransaction(final @NotNull SentryDate endTimestamp) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will be used in the next PR 3/4

try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
final @Nullable ITransaction transaction = extendedTransaction;
if (transaction != null && !transaction.isFinished()) {
// If the extended span already finished after endTimestamp, end the transaction there so it
// contains the extended span and its duration matches the reported app start vital. When
// the
// span is still open, waitForChildren keeps the transaction open until it finishes.
final @Nullable ISpan span = extendedSpan;
final @Nullable SentryDate spanEnd = span == null ? null : span.getFinishDate();
final @NotNull SentryDate end =
spanEnd != null && spanEnd.isAfter(endTimestamp) ? spanEnd : endTimestamp;
transaction.finish(SpanStatus.OK, end);
}
}
}

public @Nullable SentryDate getExtendedEndTime() {
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
final @Nullable ISpan span = extendedSpan;
if (span == null) {
return null;
}
// A deadline timeout would report an artificially inflated duration; suppress the vital
// instead.
if (span.getStatus() == SpanStatus.DEADLINE_EXCEEDED) {
return null;
}
// Read the finish date, not isFinished(): finishing the extended span completes the
// waitForChildren transaction and runs the event processor re-entrantly before the span's
// finished flag is set, but the finish timestamp is already in place. Null until finished.
return span.getFinishDate();
}
}

public void clear() {
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
extendedSpan = null;
extendedTransaction = null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.sentry.NoOpLogger;
import io.sentry.SentryDate;
import io.sentry.TracesSamplingDecision;
import io.sentry.android.core.AppStartExtension;
import io.sentry.android.core.BuildInfoProvider;
import io.sentry.android.core.ContextUtils;
import io.sentry.android.core.CurrentActivityHolder;
Expand Down Expand Up @@ -98,6 +99,7 @@ public enum AppStartType {
private @Nullable String appStartBaggageHeader;
private @Nullable SentryDate appStartEndTime;
private @Nullable ApplicationStartInfo cachedStartInfo;
private final @NotNull AppStartExtension appStartExtension = new AppStartExtension(this);

public static @NotNull AppStartMetrics getInstance() {
if (instance == null) {
Expand Down Expand Up @@ -281,6 +283,7 @@ public void onAppStartSpansSent() {
shouldSendStartMeasurements = false;
contentProviderOnCreates.clear();
activityLifecycles.clear();
appStartExtension.clear();
}

public boolean shouldSendStartMeasurements(final boolean ignoreForegroundCheck) {
Expand Down Expand Up @@ -336,6 +339,21 @@ public long getClassLoadedUptimeMs() {
return new TimeSpan();
}

public @NotNull AppStartExtension getAppStartExtension() {
return appStartExtension;
}

/**
* Whether the app start window is still open, i.e. an app start can be extended: measurements
* haven't been sent yet, no activity has been created, and the first frame hasn't been drawn. The
* foreground check is ignored so headless app starts (broadcast/service) can also be extended.
*/
public boolean isAppStartWindowOpen() {
return shouldSendStartMeasurements(true)
&& activeActivitiesCounter.get() == 0
&& !firstDrawDone.get();
}

@TestOnly
void setFirstIdle(final long firstIdle) {
this.firstIdle = firstIdle;
Expand Down Expand Up @@ -377,6 +395,7 @@ public void clear() {
appStartBaggageHeader = null;
appStartEndTime = null;
cachedStartInfo = null;
appStartExtension.clear();
}

public @Nullable ITransactionProfiler getAppStartProfiler() {
Expand Down
Loading
Loading