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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

### Features

- Add `enableStandaloneAppStartTracing` option to send app start as a standalone transaction instead of attaching it as a child span of the first activity transaction ([#5342](https://github.com/getsentry/sentry-java/pull/5342))
- Disabled by default; opt in via `SentryAndroidOptions.setEnableStandaloneAppStartTracing(true)` or manifest meta-data `io.sentry.standalone-app-start-tracing.enable`
- Emits a transaction named `App Start` with op `app.start`, carrying the existing app start measurements and phase spans (`process.load`, `contentprovider.load`, `application.load`, activity lifecycle spans) as direct children of the root
- The standalone transaction shares the same `traceId` as the first `ui.load` activity transaction so they remain linked in the trace view
- Also covers non-activity starts (broadcast receivers, services, content providers)
- Add support to configure reporting historical ANRs via `AndroidManifest.xml` using the `io.sentry.anr.report-historical` attribute ([#5387](https://github.com/getsentry/sentry-java/pull/5387))

## 8.41.0
Expand Down
10 changes: 10 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
public fun isEnablePerformanceV2 ()Z
public fun isEnableRootCheck ()Z
public fun isEnableScopeSync ()Z
public fun isEnableStandaloneAppStartTracing ()Z
public fun isEnableSystemEventBreadcrumbs ()Z
public fun isEnableSystemEventBreadcrumbsExtras ()Z
public fun isReportHistoricalAnrs ()Z
Expand Down Expand Up @@ -421,6 +422,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
public fun setEnablePerformanceV2 (Z)V
public fun setEnableRootCheck (Z)V
public fun setEnableScopeSync (Z)V
public fun setEnableStandaloneAppStartTracing (Z)V
public fun setEnableSystemEventBreadcrumbs (Z)V
public fun setEnableSystemEventBreadcrumbsExtras (Z)V
public fun setFrameMetricsCollector (Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;)V
Expand Down Expand Up @@ -737,11 +739,13 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr
public fun addActivityLifecycleTimeSpans (Lio/sentry/android/core/performance/ActivityLifecycleTimeSpan;)V
public fun clear ()V
public fun createProcessInitSpan ()Lio/sentry/android/core/performance/TimeSpan;
public fun finalizeHeadlessAppStartEndTime ()V
public fun getActivityLifecycleTimeSpans ()Ljava/util/List;
public fun getAppStartContinuousProfiler ()Lio/sentry/IContinuousProfiler;
public fun getAppStartProfiler ()Lio/sentry/ITransactionProfiler;
public fun getAppStartSamplingDecision ()Lio/sentry/TracesSamplingDecision;
public fun getAppStartTimeSpan ()Lio/sentry/android/core/performance/TimeSpan;
public fun getAppStartTimeSpanForHeadless ()Lio/sentry/android/core/performance/TimeSpan;
public fun getAppStartTimeSpanWithFallback (Lio/sentry/android/core/SentryAndroidOptions;)Lio/sentry/android/core/performance/TimeSpan;
public fun getAppStartType ()Lio/sentry/android/core/performance/AppStartMetrics$AppStartType;
public fun getApplicationOnCreateTimeSpan ()Lio/sentry/android/core/performance/TimeSpan;
Expand All @@ -768,7 +772,9 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr
public fun setAppStartSamplingDecision (Lio/sentry/TracesSamplingDecision;)V
public fun setAppStartType (Lio/sentry/android/core/performance/AppStartMetrics$AppStartType;)V
public fun setClassLoadedUptimeMs (J)V
public fun setOnMainIdleNoActivityCallback (Lio/sentry/android/core/performance/AppStartMetrics$OnMainIdleNoActivityCallback;)V
public fun shouldSendStartMeasurements ()Z
public fun shouldSendStartMeasurements (Z)Z
}

public final class io/sentry/android/core/performance/AppStartMetrics$AppStartType : java/lang/Enum {
Expand All @@ -779,6 +785,10 @@ public final class io/sentry/android/core/performance/AppStartMetrics$AppStartTy
public static fun values ()[Lio/sentry/android/core/performance/AppStartMetrics$AppStartType;
}

public abstract interface class io/sentry/android/core/performance/AppStartMetrics$OnMainIdleNoActivityCallback {
public abstract fun onMainIdleNoActivity ()V
}

public class io/sentry/android/core/performance/TimeSpan : java/lang/Comparable {
public fun <init> ()V
public fun compareTo (Lio/sentry/android/core/performance/TimeSpan;)I
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public final class ActivityLifecycleIntegration
new WeakHashMap<>();

private final @NotNull ActivityFramesTracker activityFramesTracker;
private final @NotNull FirstUiLoadCoordinator firstUiLoadCoordinator;
private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock();
private boolean fullyDisplayedCalled = false;
private final @NotNull AutoClosableReentrantLock fullyDisplayedLock =
Expand All @@ -98,12 +99,15 @@ public final class ActivityLifecycleIntegration
public ActivityLifecycleIntegration(
final @NotNull Application application,
final @NotNull BuildInfoProvider buildInfoProvider,
final @NotNull ActivityFramesTracker activityFramesTracker) {
final @NotNull ActivityFramesTracker activityFramesTracker,
final @NotNull FirstUiLoadCoordinator firstUiLoadCoordinator) {
this.application = Objects.requireNonNull(application, "Application is required");
this.buildInfoProvider =
Objects.requireNonNull(buildInfoProvider, "BuildInfoProvider is required");
this.activityFramesTracker =
Objects.requireNonNull(activityFramesTracker, "ActivityFramesTracker is required");
this.firstUiLoadCoordinator =
Objects.requireNonNull(firstUiLoadCoordinator, "FirstUiLoadCoordinator is required");

if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.Q) {
isAllActivityCallbacksAvailable = true;
Expand All @@ -124,6 +128,7 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions
timeToFullDisplaySpanEnabled = this.options.isEnableTimeToFullDisplayTracing();

application.registerActivityLifecycleCallbacks(this);

this.options.getLogger().log(SentryLevel.DEBUG, "ActivityLifecycleIntegration installed.");
addIntegrationToSdkVersion("ActivityLifecycle");
}
Expand Down Expand Up @@ -239,33 +244,53 @@ private void startTracing(final @NotNull Activity activity) {
transactionOptions.setAppStartTransaction(appStartSamplingDecision != null);
setSpanOrigin(transactionOptions);

// we can only bind to the scope if there's no running transaction
ITransaction transaction =
scopes.startTransaction(
new TransactionContext(
final @Nullable FirstUiLoadListener firstUiLoadListener =
options != null && options.isEnableStandaloneAppStartTracing()
? firstUiLoadCoordinator.getListener()
: null;

final boolean isFirstProcessStart =
!(firstActivityCreated || appStartTime == null || coldStart == null);

final @Nullable UiLoadStartPlan plan =
firstUiLoadListener != null
? firstUiLoadListener.planFirstUiLoad(
activityName, appStartSamplingDecision, isFirstProcessStart)
: null;

final @NotNull TransactionContext transactionContext =
plan != null
? plan.getTransactionContext()
: new TransactionContext(
activityName,
TransactionNameSource.COMPONENT,
UI_LOAD_OP,
appStartSamplingDecision),
transactionOptions);
appStartSamplingDecision);

final ITransaction transaction =
scopes.startTransaction(transactionContext, transactionOptions);

if (isFirstProcessStart && firstUiLoadListener != null) {
firstUiLoadListener.onFirstUiLoadTransactionStarted(
transaction, plan, appStartTime, activityName, appStartSamplingDecision);
}

final SpanOptions spanOptions = new SpanOptions();
setSpanOrigin(spanOptions);

// in case appStartTime isn't available, we don't create a span for it.
if (!(firstActivityCreated || appStartTime == null || coldStart == null)) {
// start specific span for app start
appStartSpan =
transaction.startChild(
getAppStartOp(coldStart),
getAppStartDesc(coldStart),
appStartTime,
Instrumenter.SENTRY,
spanOptions);

// in case there's already an end time (e.g. due to deferred SDK init)
// we can finish the app-start span
finishAppStartSpan();
if (isFirstProcessStart) {
if (firstUiLoadListener == null) {
// legacy path: app start is a child span of the ui.load transaction.
appStartSpan =
transaction.startChild(
getAppStartOp(coldStart),
getAppStartDesc(coldStart),
appStartTime,
Instrumenter.SENTRY,
spanOptions);

finishAppStartSpan();
}
}
final @NotNull ISpan ttidSpan =
transaction.startChild(
Expand Down Expand Up @@ -440,8 +465,7 @@ public void onActivityPostCreated(
final @NotNull Activity activity, final @Nullable Bundle savedInstanceState) {
final ActivityLifecycleSpanHelper helper = activitySpanHelpers.get(activity);
if (helper != null) {
helper.createAndStopOnCreateSpan(
appStartSpan != null ? appStartSpan : activitiesWithOngoingTransactions.get(activity));
helper.createAndStopOnCreateSpan(getAppStartParent(activity));
}
}

Expand Down Expand Up @@ -479,8 +503,7 @@ public void onActivityStarted(final @NotNull Activity activity) {
public void onActivityPostStarted(final @NotNull Activity activity) {
final ActivityLifecycleSpanHelper helper = activitySpanHelpers.get(activity);
if (helper != null) {
helper.createAndStopOnStartSpan(
appStartSpan != null ? appStartSpan : activitiesWithOngoingTransactions.get(activity));
helper.createAndStopOnStartSpan(getAppStartParent(activity));
// Needed to handle hybrid SDKs
helper.saveSpanToAppStartMetrics();
}
Expand Down Expand Up @@ -559,6 +582,11 @@ public void onActivityDestroyed(final @NotNull Activity activity) {
// in case the appStartSpan isn't completed yet, we finish it as cancelled to avoid
// memory leak
finishSpan(appStartSpan, SpanStatus.CANCELLED);
final @Nullable FirstUiLoadListener firstUiLoadListener =
firstUiLoadCoordinator.getListener();
if (firstUiLoadListener != null) {
firstUiLoadListener.onActivityDestroyed();
}

// we finish the ttidSpan as cancelled in case it isn't completed yet
final ISpan ttidSpan = ttidSpanMap.get(activity);
Expand Down Expand Up @@ -637,22 +665,23 @@ private void onFirstFrameDrawn(final @Nullable ISpan ttfdSpan, final @Nullable I
final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance();
final @NotNull TimeSpan appStartTimeSpan = appStartMetrics.getAppStartTimeSpan();
final @NotNull TimeSpan sdkInitTimeSpan = appStartMetrics.getSdkInitTimeSpan();
final @Nullable SentryDate firstFrameEndDate =
options != null && ttidSpan != null ? options.getDateProvider().now() : null;

// and we need to set the end time of the app start here, after the first frame is drawn.
if (appStartTimeSpan.hasStarted() && appStartTimeSpan.hasNotStopped()) {
appStartTimeSpan.stop();
stopTimeSpanAtDate(appStartTimeSpan, firstFrameEndDate);
}
if (sdkInitTimeSpan.hasStarted() && sdkInitTimeSpan.hasNotStopped()) {
sdkInitTimeSpan.stop();
stopTimeSpanAtDate(sdkInitTimeSpan, firstFrameEndDate);
}
finishAppStartSpan();
finishAppStartSpan(firstFrameEndDate);

// Sentry.reportFullyDisplayed can be run in any thread, so we have to ensure synchronization
// with first frame drawn
try (final @NotNull ISentryLifecycleToken ignored = fullyDisplayedLock.acquire()) {
if (options != null && ttidSpan != null) {
final SentryDate endDate = options.getDateProvider().now();
final long durationNanos = endDate.diff(ttidSpan.getStartDate());
if (options != null && ttidSpan != null && firstFrameEndDate != null) {
final long durationNanos = firstFrameEndDate.diff(ttidSpan.getStartDate());
final long durationMillis = TimeUnit.NANOSECONDS.toMillis(durationNanos);
ttidSpan.setMeasurement(
MeasurementValue.KEY_TIME_TO_INITIAL_DISPLAY, durationMillis, MILLISECOND);
Expand All @@ -664,10 +693,10 @@ private void onFirstFrameDrawn(final @Nullable ISpan ttfdSpan, final @Nullable I
MeasurementValue.KEY_TIME_TO_FULL_DISPLAY, durationMillis, MILLISECOND);
ttfdSpan.setMeasurement(
MeasurementValue.KEY_TIME_TO_FULL_DISPLAY, durationMillis, MILLISECOND);
finishSpan(ttfdSpan, endDate);
finishSpan(ttfdSpan, firstFrameEndDate);
}

finishSpan(ttidSpan, endDate);
finishSpan(ttidSpan, firstFrameEndDate);
} else {
finishSpan(ttidSpan);
if (fullyDisplayedCalled) {
Expand All @@ -677,6 +706,17 @@ private void onFirstFrameDrawn(final @Nullable ISpan ttfdSpan, final @Nullable I
}
}

private void stopTimeSpanAtDate(
final @NotNull TimeSpan timeSpan, final @Nullable SentryDate endDate) {
final @Nullable SentryDate startDate = timeSpan.getStartTimestamp();
if (endDate != null && startDate != null) {
final long durationMillis = TimeUnit.NANOSECONDS.toMillis(endDate.diff(startDate));
timeSpan.setStoppedAt(timeSpan.getStartUptimeMs() + durationMillis);
} else {
timeSpan.stop();
}
}

private void onFullFrameDrawn(final @NotNull ISpan ttidSpan, final @NotNull ISpan ttfdSpan) {
cancelTtfdAutoClose();
// Sentry.reportFullyDisplayed can be run in any thread, so we have to ensure synchronization
Expand Down Expand Up @@ -779,6 +819,20 @@ WeakHashMap<Activity, ISpan> getTtfdSpanMap() {
}
}

private @Nullable ISpan getAppStartParent(final @NotNull Activity activity) {
final @Nullable FirstUiLoadListener firstUiLoadListener = firstUiLoadCoordinator.getListener();
if (firstUiLoadListener != null) {
final @Nullable ISpan appStartTransaction = firstUiLoadListener.getAppStartTransaction();
if (appStartTransaction != null) {
return appStartTransaction;
}
}
if (appStartSpan != null) {
return appStartSpan;
}
return activitiesWithOngoingTransactions.get(activity);
}

private @NotNull String getAppStartOp(final boolean coldStart) {
if (coldStart) {
return APP_START_COLD;
Expand All @@ -788,12 +842,23 @@ WeakHashMap<Activity, ISpan> getTtfdSpanMap() {
}

private void finishAppStartSpan() {
finishAppStartSpan(null);
}

private void finishAppStartSpan(final @Nullable SentryDate endDate) {
final @Nullable SentryDate appStartEndTime =
AppStartMetrics.getInstance()
.getAppStartTimeSpanWithFallback(options)
.getProjectedStopTimestamp();
endDate != null
? endDate
: AppStartMetrics.getInstance()
.getAppStartTimeSpanWithFallback(options)
.getProjectedStopTimestamp();
if (performanceEnabled && appStartEndTime != null) {
finishSpan(appStartSpan, appStartEndTime);
final @Nullable FirstUiLoadListener firstUiLoadListener =
firstUiLoadCoordinator.getListener();
if (firstUiLoadListener != null && appStartEndTime != null) {
firstUiLoadListener.onFirstFrameDrawn(appStartEndTime);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -405,9 +405,16 @@ static void installDefaultIntegrations(

// registerActivityLifecycleCallbacks is only available if Context is an AppContext
if (context instanceof Application) {
final @NotNull FirstUiLoadCoordinator firstUiLoadCoordinator = new FirstUiLoadCoordinator();
if (AppStartIntegration.isEnabled(options)) {
options.addIntegration(new AppStartIntegration(firstUiLoadCoordinator));
}
options.addIntegration(
new ActivityLifecycleIntegration(
(Application) context, buildInfoProvider, activityFramesTracker));
(Application) context,
buildInfoProvider,
activityFramesTracker,
firstUiLoadCoordinator));
options.addIntegration(new ActivityBreadcrumbsIntegration((Application) context));
options.addIntegration(new UserInteractionIntegration((Application) context, loadClass));
options.addIntegration(new FeedbackShakeIntegration((Application) context));
Expand Down
Loading