-
-
Notifications
You must be signed in to change notification settings - Fork 468
feat(android): Add standalone app start tracing #5342
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
ddd9bed
fdd26df
e5b7a18
11898dc
5efab1d
f6c070d
215a549
50a2f41
26a83cc
a60d966
09bac52
5387e76
f7c9c62
9adfa01
87ff0d6
706adad
9bf80c4
5d6174e
dfb5aea
e8be515
b5a6336
f3ef6ce
9da8369
72268a3
cbaebe2
b1d738b
9f5cac5
51c2bff
b7fabc6
b871644
e681be9
cdc6178
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,6 +33,7 @@ | |
| import io.sentry.android.core.performance.AppStartMetrics; | ||
| import io.sentry.android.core.performance.TimeSpan; | ||
| import io.sentry.protocol.MeasurementValue; | ||
| import io.sentry.protocol.SentryId; | ||
| import io.sentry.protocol.TransactionNameSource; | ||
| import io.sentry.util.AutoClosableReentrantLock; | ||
| import io.sentry.util.Objects; | ||
|
|
@@ -55,12 +56,15 @@ public final class ActivityLifecycleIntegration | |
| implements Integration, Closeable, Application.ActivityLifecycleCallbacks { | ||
|
|
||
| static final String UI_LOAD_OP = "ui.load"; | ||
| static final String STANDALONE_APP_START_OP = "app.start"; | ||
| private static final String STANDALONE_APP_START_NAME = "App Start"; | ||
| static final String APP_START_WARM = "app.start.warm"; | ||
| static final String APP_START_COLD = "app.start.cold"; | ||
| static final String TTID_OP = "ui.load.initial_display"; | ||
| static final String TTFD_OP = "ui.load.full_display"; | ||
| static final long TTFD_TIMEOUT_MILLIS = 25000; | ||
| private static final String TRACE_ORIGIN = "auto.ui.activity"; | ||
| private static final String APP_START_SCREEN_DATA = "app.vitals.start.screen"; | ||
|
|
||
| private final @NotNull Application application; | ||
| private final @NotNull BuildInfoProvider buildInfoProvider; | ||
|
|
@@ -77,6 +81,7 @@ public final class ActivityLifecycleIntegration | |
|
|
||
| private @Nullable FullyDisplayedReporter fullyDisplayedReporter = null; | ||
| private @Nullable ISpan appStartSpan; | ||
| private @Nullable ITransaction appStartTransaction; | ||
| private final @NotNull WeakHashMap<Activity, ISpan> ttidSpanMap = new WeakHashMap<>(); | ||
| private final @NotNull WeakHashMap<Activity, ISpan> ttfdSpanMap = new WeakHashMap<>(); | ||
| private final @NotNull WeakHashMap<Activity, ActivityLifecycleSpanHelper> activitySpanHelpers = | ||
|
|
@@ -124,6 +129,11 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions | |
| timeToFullDisplaySpanEnabled = this.options.isEnableTimeToFullDisplayTracing(); | ||
|
|
||
| application.registerActivityLifecycleCallbacks(this); | ||
|
|
||
| if (performanceEnabled && this.options.isEnableStandaloneAppStartTracing()) { | ||
| AppStartMetrics.getInstance().setOnNoActivityStartedListener(this::onNoActivityStarted); | ||
| } | ||
|
|
||
| this.options.getLogger().log(SentryLevel.DEBUG, "ActivityLifecycleIntegration installed."); | ||
| addIntegrationToSdkVersion("ActivityLifecycle"); | ||
| } | ||
|
|
@@ -135,6 +145,7 @@ private boolean isPerformanceEnabled(final @NotNull SentryAndroidOptions options | |
| @Override | ||
| public void close() throws IOException { | ||
| application.unregisterActivityLifecycleCallbacks(this); | ||
| AppStartMetrics.getInstance().setOnNoActivityStartedListener(null); | ||
|
|
||
| if (options != null) { | ||
| options.getLogger().log(SentryLevel.DEBUG, "ActivityLifecycleIntegration removed."); | ||
|
|
@@ -239,33 +250,68 @@ 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( | ||
| activityName, | ||
| TransactionNameSource.COMPONENT, | ||
| UI_LOAD_OP, | ||
| appStartSamplingDecision), | ||
| transactionOptions); | ||
| final @Nullable SentryId storedAppStartTraceId = | ||
| AppStartMetrics.getInstance().getAppStartTraceId(); | ||
| // A non-null trace ID means a standalone app-start txn was already emitted. | ||
| final boolean isFollowingNonActivityStart = (storedAppStartTraceId != null); | ||
|
|
||
| final ITransaction transaction; | ||
| if (storedAppStartTraceId != null) { | ||
| transaction = | ||
| scopes.startTransaction( | ||
| new TransactionContext( | ||
| storedAppStartTraceId, | ||
| activityName, | ||
| TransactionNameSource.COMPONENT, | ||
| UI_LOAD_OP, | ||
| appStartSamplingDecision), | ||
| transactionOptions); | ||
| AppStartMetrics.getInstance().setAppStartTraceId(null); | ||
| } else { | ||
| transaction = | ||
| scopes.startTransaction( | ||
| new TransactionContext( | ||
| activityName, | ||
| TransactionNameSource.COMPONENT, | ||
| UI_LOAD_OP, | ||
| appStartSamplingDecision), | ||
| transactionOptions); | ||
| } | ||
|
|
||
| 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 (options.isEnableStandaloneAppStartTracing() && !isFollowingNonActivityStart) { | ||
| final TransactionOptions appStartTransactionOptions = new TransactionOptions(); | ||
| appStartTransactionOptions.setBindToScope(false); | ||
| appStartTransactionOptions.setStartTimestamp(appStartTime); | ||
| appStartTransactionOptions.setAppStartTransaction(appStartSamplingDecision != null); | ||
| setSpanOrigin(appStartTransactionOptions); | ||
|
|
||
| appStartTransaction = | ||
| scopes.startTransaction( | ||
| new TransactionContext( | ||
| transaction.getSpanContext().getTraceId(), | ||
| STANDALONE_APP_START_NAME, | ||
| TransactionNameSource.COMPONENT, | ||
| STANDALONE_APP_START_OP, | ||
| appStartSamplingDecision), | ||
| appStartTransactionOptions); | ||
| appStartTransaction.setData(APP_START_SCREEN_DATA, activityName); | ||
|
|
||
| finishAppStartSpan(); | ||
| } else if (!options.isEnableStandaloneAppStartTracing()) { | ||
| appStartSpan = | ||
| transaction.startChild( | ||
| getAppStartOp(coldStart), | ||
| getAppStartDesc(coldStart), | ||
| appStartTime, | ||
| Instrumenter.SENTRY, | ||
| spanOptions); | ||
|
|
||
| finishAppStartSpan(); | ||
| } | ||
| } | ||
| final @NotNull ISpan ttidSpan = | ||
| transaction.startChild( | ||
|
|
@@ -440,8 +486,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)); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -479,8 +524,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(); | ||
| } | ||
|
|
@@ -559,6 +603,9 @@ 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); | ||
| if (appStartTransaction != null && !appStartTransaction.isFinished()) { | ||
| appStartTransaction.finish(SpanStatus.CANCELLED); | ||
| } | ||
|
|
||
| // we finish the ttidSpan as cancelled in case it isn't completed yet | ||
| final ISpan ttidSpan = ttidSpanMap.get(activity); | ||
|
|
@@ -575,6 +622,7 @@ public void onActivityDestroyed(final @NotNull Activity activity) { | |
|
|
||
| // set it to null in case its been just finished as cancelled | ||
| appStartSpan = null; | ||
| appStartTransaction = null; | ||
| ttidSpanMap.remove(activity); | ||
| ttfdSpanMap.remove(activity); | ||
| } | ||
|
|
@@ -637,22 +685,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); | ||
|
|
@@ -664,10 +713,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) { | ||
|
|
@@ -677,6 +726,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 | ||
|
|
@@ -779,6 +839,16 @@ WeakHashMap<Activity, ISpan> getTtfdSpanMap() { | |
| } | ||
| } | ||
|
|
||
| private @Nullable ISpan getAppStartParent(final @NotNull Activity activity) { | ||
| if (appStartTransaction != null) { | ||
| return appStartTransaction; | ||
|
buenaflor marked this conversation as resolved.
buenaflor marked this conversation as resolved.
|
||
| } | ||
| if (appStartSpan != null) { | ||
| return appStartSpan; | ||
| } | ||
| return activitiesWithOngoingTransactions.get(activity); | ||
| } | ||
|
|
||
| private @NotNull String getAppStartOp(final boolean coldStart) { | ||
| if (coldStart) { | ||
| return APP_START_COLD; | ||
|
|
@@ -788,12 +858,63 @@ 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); | ||
| if (appStartTransaction != null && !appStartTransaction.isFinished()) { | ||
| appStartTransaction.finish(SpanStatus.OK, appStartEndTime); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private void onNoActivityStarted() { | ||
| if (scopes == null || options == null || !performanceEnabled) { | ||
| return; | ||
| } | ||
|
|
||
| final @NotNull AppStartMetrics metrics = AppStartMetrics.getInstance(); | ||
| // Profilers are stopped for non-activity starts; clear the decision so it doesn't | ||
| // leak to a later ui.load transaction if an activity eventually opens. | ||
| metrics.setAppStartSamplingDecision(null); | ||
|
|
||
| // For non-activity starts, appLaunchedInForeground is false, so we can't use | ||
| // getAppStartTimeSpanWithFallback (which gates on foreground). | ||
| final @NotNull TimeSpan appStartTimeSpan = metrics.getAppStartTimeSpanForStandalone(); | ||
|
|
||
| if (!appStartTimeSpan.hasStarted() || !appStartTimeSpan.hasStopped()) { | ||
| return; | ||
| } | ||
|
|
||
| final @Nullable SentryDate startTime = appStartTimeSpan.getStartTimestamp(); | ||
| final @Nullable SentryDate endTime = appStartTimeSpan.getProjectedStopTimestamp(); | ||
| if (startTime == null || endTime == null) { | ||
| return; | ||
| } | ||
|
|
||
| final TransactionOptions txnOptions = new TransactionOptions(); | ||
| txnOptions.setBindToScope(false); | ||
| txnOptions.setStartTimestamp(startTime); | ||
| setSpanOrigin(txnOptions); | ||
|
|
||
| final @NotNull TransactionContext txnContext = | ||
| new TransactionContext( | ||
| STANDALONE_APP_START_NAME, | ||
| TransactionNameSource.COMPONENT, | ||
| STANDALONE_APP_START_OP, | ||
| null); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any sampling synchronization concerns here? Eg, do we need to make sure whatever is applied to app.start is also applied to ui.load for all sampler types (including custom samplers)?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. possibly there is since both could be generating different this should also be solved by #5383 but we cannot rely on it yet at this point |
||
|
|
||
| final @NotNull ITransaction transaction = scopes.startTransaction(txnContext, txnOptions); | ||
| metrics.setAppStartTraceId(transaction.getSpanContext().getTraceId()); | ||
|
buenaflor marked this conversation as resolved.
|
||
|
|
||
| transaction.finish(SpanStatus.OK, endTime); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.