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
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 finishAppStart ()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,9 @@ static void initializeIntegrationsAndProcessors(
}

final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance();
final @NotNull AppStartExtension appStartExtension = appStartMetrics.getAppStartExtension();
appStartExtension.setLogger(options.getLogger());
options.setAppStartExtender(appStartExtension);

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,169 @@
package io.sentry.android.core;

import io.sentry.IAppStartExtender;
import io.sentry.ILogger;
import io.sentry.ISentryLifecycleToken;
import io.sentry.ISpan;
import io.sentry.ITransaction;
import io.sentry.NoOpLogger;
import io.sentry.NoOpSpan;
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;

/**
* Owns the lifecycle of an extended app start. Created and held by {@link AppStartMetrics}, it
* keeps the new "extend app start" concern out of that already-large class.
*
* <p>Both the eager standalone App Start {@link ITransaction} and its extended child {@link ISpan}
* are created by the integration (which has access to scopes) and returned to this component from
* {@link ExtendAppStartListener#onExtendAppStartRequested()}. This component owns them from then
* on: it never stores them in the integration's shared transaction field, so the per-activity
* cleanup can never cancel an eagerly-created extension.
*/
@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;
}
}

/**
* Notifies the integration that an extension was requested. The integration creates the
* standalone App Start transaction + extended child span (it has scopes) and returns them, or
* returns {@code null} to decline (e.g. standalone tracing is disabled). When no listener is
* registered, {@link #extendAppStart()} is inert and the whole API stays a no-op.
*/
public interface ExtendAppStartListener {
@Nullable
ExtendedAppStart onExtendAppStartRequested();
}

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

// Set once at SDK init via setLogger(), read later when an extension is requested. Defaults to a
// no-op because this component is created before SentryOptions (and its logger) exist.
private volatile @NotNull ILogger logger = NoOpLogger.getInstance();

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;
}

void setLogger(final @NotNull ILogger logger) {
this.logger = logger;
}

@Override
public void extendAppStart() {
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
if (extendedSpan != null) {
logger.log(SentryLevel.WARNING, "App start is already being extended.");
return;
}
// Ignore the foreground check: headless app starts (broadcast/service) run in a
// non-foreground process but can still be extended. The window gate still rejects an
// extension once an activity was created, the first frame was drawn, or measurements were
// already sent.
if (!metrics.isAppStartWindowOpen()) {
logger.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 finishAppStart() {
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();
}
}

/**
* Finishes the owned transaction at the natural app start end (first frame, or the headless stop
* time). {@code waitForChildren} holds the transaction open until the extended span finishes, so
* the app start vital is never captured before this point. Idempotent.
*/
public void finishTransaction(final @NotNull SentryDate endTimestamp) {
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
final @Nullable ITransaction transaction = extendedTransaction;
if (transaction != null && !transaction.isFinished()) {
transaction.finish(SpanStatus.OK, endTimestamp);
}
}
}

/**
* The effective end of the extended app start, used to extend the app start vital. Returns {@code
* null} when no extension finished, or when it finished via the deadline timeout - in the latter
* case the vital is suppressed instead of reporting an artificially inflated duration.
*/
public @Nullable SentryDate getExtendedEndTime() {
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
final @Nullable ISpan span = extendedSpan;
if (span == null || !span.isFinished()) {
return null;
}
if (span.getStatus() == SpanStatus.DEADLINE_EXCEEDED) {
return null;
}
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