From fd54fa67792da6e1b2dbff801a7a59dfc54da02a Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Wed, 16 Nov 2022 12:06:06 +0100 Subject: [PATCH 01/17] added ttid span to ActivityLifecycleIntegration --- .../api/sentry-android-core.api | 1 + .../core/ActivityLifecycleIntegration.java | 52 +++++++++++++++++++ .../core/ActivityLifecycleIntegrationTest.kt | 32 ++++++++++++ 3 files changed, 85 insertions(+) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 4bdcfecd27..a7d72b0095 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -15,6 +15,7 @@ public final class io/sentry/android/core/ActivityLifecycleIntegration : android public fun onActivityDestroyed (Landroid/app/Activity;)V public fun onActivityPaused (Landroid/app/Activity;)V public fun onActivityPostResumed (Landroid/app/Activity;)V + public fun onActivityPrePaused (Landroid/app/Activity;)V public fun onActivityResumed (Landroid/app/Activity;)V public fun onActivitySaveInstanceState (Landroid/app/Activity;Landroid/os/Bundle;)V public fun onActivityStarted (Landroid/app/Activity;)V diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index a307b01679..0f4a915457 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -9,8 +9,12 @@ import android.content.Context; import android.os.Build; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.os.Process; +import androidx.annotation.NonNull; import io.sentry.Breadcrumb; +import io.sentry.DateUtils; import io.sentry.Hint; import io.sentry.IHub; import io.sentry.ISpan; @@ -56,6 +60,9 @@ public final class ActivityLifecycleIntegration private boolean foregroundImportance = false; private @Nullable ISpan appStartSpan; + private @Nullable ISpan ttidSpan; + private @NotNull Date lastPausedTime = DateUtils.getCurrentDateTime(); + private final @NotNull Handler mainHandler = new Handler(Looper.getMainLooper()); // WeakHashMap isn't thread safe but ActivityLifecycleCallbacks is only called from the // main-thread @@ -198,8 +205,24 @@ private void startTracing(final @NotNull Activity activity) { appStartSpan = transaction.startChild( getAppStartOp(coldStart), getAppStartDesc(coldStart), appStartTime); + // The first activity ttidSpan should start at the same time as the app start time + ttidSpan = transaction.startChild("TTID", activityName + ".ttid", appStartTime); + } else { + // Other activities (or in case appStartTime is not available) the ttid span should + // start when the previous activity called its onPause method. + ttidSpan = transaction.startChild("TTID", activityName + ".ttid", lastPausedTime); } + // Posting a task to the main thread's handler will make it executed after it finished + // its current job. That is, right after the activity draws the layout. + mainHandler.post( + () -> { + // finishes ttidSpan span + if (ttidSpan != null && !ttidSpan.isFinished()) { + ttidSpan.finish(); + } + }); + // lets bind to the scope so other integrations can pick it up hub.configureScope( scope -> { @@ -258,6 +281,11 @@ private void finishTransaction(final @Nullable ITransaction transaction) { return; } + // in case the ttidSpan isn't completed yet, we finish it as cancelled to avoid memory leak + if (ttidSpan != null && !ttidSpan.isFinished()) { + ttidSpan.finish(SpanStatus.CANCELLED); + } + SpanStatus status = transaction.getStatus(); // status might be set by other integrations, let's not overwrite it if (status == null) { @@ -340,8 +368,20 @@ public synchronized void onActivityPostResumed(final @NotNull Activity activity) } } + @Override + public void onActivityPrePaused(@NonNull Activity activity) { + // only executed if API >= 29 otherwise it happens on onActivityPaused + if (isAllActivityCallbacksAvailable) { + lastPausedTime = DateUtils.getCurrentDateTime(); + } + } + @Override public synchronized void onActivityPaused(final @NotNull Activity activity) { + // only executed if API < 29 otherwise it happens on onActivityPrePaused + if (!isAllActivityCallbacksAvailable) { + lastPausedTime = DateUtils.getCurrentDateTime(); + } addBreadcrumb(activity, "paused"); } @@ -366,12 +406,18 @@ public synchronized void onActivityDestroyed(final @NotNull Activity activity) { appStartSpan.finish(SpanStatus.CANCELLED); } + // in case the ttidSpan isn't completed yet, we finish it as cancelled to avoid memory leak + if (ttidSpan != null && !ttidSpan.isFinished()) { + ttidSpan.finish(SpanStatus.CANCELLED); + } + // in case people opt-out enableActivityLifecycleTracingAutoFinish and forgot to finish it, // we make sure to finish it when the activity gets destroyed. stopTracing(activity, true); // set it to null in case its been just finished as cancelled appStartSpan = null; + ttidSpan = null; // clear it up, so we don't start again for the same activity if the activity is in the activity // stack still. @@ -399,6 +445,12 @@ ISpan getAppStartSpan() { return appStartSpan; } + @TestOnly + @Nullable + ISpan getTtidSpan() { + return ttidSpan; + } + private void setColdStart(final @Nullable Bundle savedInstanceState) { if (!firstActivityCreated) { // if Activity has savedInstanceState then its a warm start diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt index 7c530b0758..b522f61b41 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt @@ -498,6 +498,38 @@ class ActivityLifecycleIntegrationTest { assertNull(sut.appStartSpan) } + @Test + fun `When Activity is destroyed, sets ttidSpan status to cancelled and finish it`() { + val sut = fixture.getSut() + fixture.options.tracesSampleRate = 1.0 + sut.register(fixture.hub, fixture.options) + + setAppStartTime() + + val activity = mock() + sut.onActivityCreated(activity, fixture.bundle) + sut.onActivityDestroyed(activity) + + val span = fixture.transaction.children.first { it.description?.endsWith(".ttid") == true } + assertEquals(span.status, SpanStatus.CANCELLED) + assertTrue(span.isFinished) + } + + @Test + fun `When Activity is destroyed, sets ttidSpan to null`() { + val sut = fixture.getSut() + fixture.options.tracesSampleRate = 1.0 + sut.register(fixture.hub, fixture.options) + + setAppStartTime() + + val activity = mock() + sut.onActivityCreated(activity, fixture.bundle) + sut.onActivityDestroyed(activity) + + assertNull(sut.ttidSpan) + } + @Test fun `When new Activity and transaction is created, finish previous ones`() { val sut = fixture.getSut() From 1d48b5aa0b2e04d15222e2673a16b6dfce052740 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Wed, 16 Nov 2022 17:07:14 +0100 Subject: [PATCH 02/17] SentryFrameMetricsCollector start collecting frameMetrics in onActivityStarted(), not onActivityCreated() anymore --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index af924736a8..d82778f935 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Features +- Add ttid span to ActivityLifecycleIntegration ([#2369](https://github.com/getsentry/sentry-java/pull/2369)) - Update Spring Boot Jakarta to Spring Boot 3.0.0-RC2 ([#2347](https://github.com/getsentry/sentry-java/pull/2347)) ## 6.7.0 From 501d3e7988619016994b51a6b9e95eba729fb914 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Wed, 16 Nov 2022 18:08:52 +0100 Subject: [PATCH 03/17] updated changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00546dff42..63b2d2b115 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Add ttid span to ActivityLifecycleIntegration ([#2369](https://github.com/getsentry/sentry-java/pull/2369)) + ## 6.7.1 ### Fixes From ff733e57fb7c54f52fbaae9f79996cd581efd3e4 Mon Sep 17 00:00:00 2001 From: Stefano Date: Wed, 16 Nov 2022 18:12:41 +0100 Subject: [PATCH 04/17] Update CHANGELOG.md --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63b2d2b115..08777dfd8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,6 @@ ### Features -- Add ttid span to ActivityLifecycleIntegration ([#2369](https://github.com/getsentry/sentry-java/pull/2369)) - Update Spring Boot Jakarta to Spring Boot 3.0.0-RC2 ([#2347](https://github.com/getsentry/sentry-java/pull/2347)) ## 6.7.0 From 0c9a0a81ef1feae4dbc6895d31286212b9a738e4 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Wed, 30 Nov 2022 16:35:00 -0800 Subject: [PATCH 05/17] ActivityLifecycleIntegration now keeps a map of ttidSpans, with one span per activity updated ttidSpan finishing with Firebase's method: span is finished after the very first frame has been drawn using view.getViewTreeObserver().addOnDrawListener added FirstDrawDoneListener Sentry.getSpan() could cause issues --- .../core/ActivityLifecycleIntegration.java | 73 +++++---- .../internal/util/FirstDrawDoneListener.java | 95 ++++++++++++ .../core/ActivityLifecycleIntegrationTest.kt | 5 +- .../util/FirstDrawDoneListenerTest.kt | 138 ++++++++++++++++++ .../sentry/samples/android/MainActivity.java | 3 +- .../samples/android/ProfilingActivity.kt | 5 - .../main/kotlin/io/sentry/test/Reflection.kt | 8 +- 7 files changed, 289 insertions(+), 38 deletions(-) create mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FirstDrawDoneListener.java create mode 100644 sentry-android-core/src/test/java/io/sentry/android/core/internal/util/FirstDrawDoneListenerTest.kt diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index 0f4a915457..5d0f2cfbcc 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -3,6 +3,7 @@ import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; import static io.sentry.TypeCheckHint.ANDROID_ACTIVITY; +import android.annotation.SuppressLint; import android.app.Activity; import android.app.ActivityManager; import android.app.Application; @@ -12,6 +13,7 @@ import android.os.Handler; import android.os.Looper; import android.os.Process; +import android.view.View; import androidx.annotation.NonNull; import io.sentry.Breadcrumb; import io.sentry.DateUtils; @@ -26,6 +28,7 @@ import io.sentry.SpanStatus; import io.sentry.TransactionContext; import io.sentry.TransactionOptions; +import io.sentry.android.core.internal.util.FirstDrawDoneListener; import io.sentry.protocol.TransactionNameSource; import io.sentry.util.Objects; import java.io.Closeable; @@ -48,6 +51,7 @@ public final class ActivityLifecycleIntegration static final String APP_START_COLD = "app.start.cold"; private final @NotNull Application application; + private final @NotNull BuildInfoProvider buildInfoProvider; private @Nullable IHub hub; private @Nullable SentryAndroidOptions options; @@ -60,7 +64,7 @@ public final class ActivityLifecycleIntegration private boolean foregroundImportance = false; private @Nullable ISpan appStartSpan; - private @Nullable ISpan ttidSpan; + private final @NotNull WeakHashMap ttidSpanMap = new WeakHashMap<>(); private @NotNull Date lastPausedTime = DateUtils.getCurrentDateTime(); private final @NotNull Handler mainHandler = new Handler(Looper.getMainLooper()); @@ -76,7 +80,8 @@ public ActivityLifecycleIntegration( final @NotNull BuildInfoProvider buildInfoProvider, final @NotNull ActivityFramesTracker activityFramesTracker) { this.application = Objects.requireNonNull(application, "Application is required"); - Objects.requireNonNull(buildInfoProvider, "BuildInfoProvider is required"); + this.buildInfoProvider = + Objects.requireNonNull(buildInfoProvider, "BuildInfoProvider is required"); this.activityFramesTracker = Objects.requireNonNull(activityFramesTracker, "ActivityFramesTracker is required"); @@ -152,7 +157,8 @@ private void stopPreviousTransactions() { for (final Map.Entry entry : activitiesWithOngoingTransactions.entrySet()) { final ITransaction transaction = entry.getValue(); - finishTransaction(transaction); + final ISpan ttidSpan = ttidSpanMap.get(entry.getKey()); + finishTransaction(transaction, ttidSpan); } } @@ -206,23 +212,15 @@ private void startTracing(final @NotNull Activity activity) { transaction.startChild( getAppStartOp(coldStart), getAppStartDesc(coldStart), appStartTime); // The first activity ttidSpan should start at the same time as the app start time - ttidSpan = transaction.startChild("TTID", activityName + ".ttid", appStartTime); + ttidSpanMap.put( + activity, transaction.startChild("TTID", activityName + ".ttid", appStartTime)); } else { // Other activities (or in case appStartTime is not available) the ttid span should // start when the previous activity called its onPause method. - ttidSpan = transaction.startChild("TTID", activityName + ".ttid", lastPausedTime); + ttidSpanMap.put( + activity, transaction.startChild("TTID", activityName + ".ttid", lastPausedTime)); } - // Posting a task to the main thread's handler will make it executed after it finished - // its current job. That is, right after the activity draws the layout. - mainHandler.post( - () -> { - // finishes ttidSpan span - if (ttidSpan != null && !ttidSpan.isFinished()) { - ttidSpan.finish(); - } - }); - // lets bind to the scope so other integrations can pick it up hub.configureScope( scope -> { @@ -269,11 +267,13 @@ private boolean isRunningTransaction(final @NotNull Activity activity) { private void stopTracing(final @NotNull Activity activity, final boolean shouldFinishTracing) { if (performanceEnabled && shouldFinishTracing) { final ITransaction transaction = activitiesWithOngoingTransactions.get(activity); - finishTransaction(transaction); + final ISpan ttidSpan = ttidSpanMap.get(activity); + finishTransaction(transaction, ttidSpan); } } - private void finishTransaction(final @Nullable ITransaction transaction) { + private void finishTransaction( + final @Nullable ITransaction transaction, final @Nullable ISpan ttidSpan) { if (transaction != null) { // if io.sentry.traces.activity.auto-finish.enable is disabled, transaction may be already // finished manually when this method is called. @@ -325,6 +325,7 @@ public synchronized void onActivityStarted(final @NotNull Activity activity) { addBreadcrumb(activity, "started"); } + @SuppressLint("NewApi") @Override public synchronized void onActivityResumed(final @NotNull Activity activity) { if (!firstActivityResumed) { @@ -350,6 +351,30 @@ public synchronized void onActivityResumed(final @NotNull Activity activity) { firstActivityResumed = true; } + final ISpan ttidSpan = ttidSpanMap.get(activity); + final View rootView = activity.findViewById(android.R.id.content); + if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN + && rootView != null) { + FirstDrawDoneListener.registerForNextDraw( + rootView, + () -> { + // finishes ttidSpan span + if (ttidSpan != null && !ttidSpan.isFinished()) { + ttidSpan.finish(); + } + }, + buildInfoProvider); + } else { + // Posting a task to the main thread's handler will make it executed after it finished + // its current job. That is, right after the activity draws the layout. + mainHandler.post( + () -> { + // finishes ttidSpan span + if (ttidSpan != null && !ttidSpan.isFinished()) { + ttidSpan.finish(); + } + }); + } addBreadcrumb(activity, "resumed"); // fallback call for API < 29 compatibility, otherwise it happens on onActivityPostResumed @@ -406,18 +431,14 @@ public synchronized void onActivityDestroyed(final @NotNull Activity activity) { appStartSpan.finish(SpanStatus.CANCELLED); } - // in case the ttidSpan isn't completed yet, we finish it as cancelled to avoid memory leak - if (ttidSpan != null && !ttidSpan.isFinished()) { - ttidSpan.finish(SpanStatus.CANCELLED); - } - // in case people opt-out enableActivityLifecycleTracingAutoFinish and forgot to finish it, // we make sure to finish it when the activity gets destroyed. stopTracing(activity, true); // set it to null in case its been just finished as cancelled appStartSpan = null; - ttidSpan = null; + // ttidSpan is finished in the stopTracing() method + ttidSpanMap.remove(activity); // clear it up, so we don't start again for the same activity if the activity is in the activity // stack still. @@ -446,9 +467,9 @@ ISpan getAppStartSpan() { } @TestOnly - @Nullable - ISpan getTtidSpan() { - return ttidSpan; + @NotNull + WeakHashMap getTtidSpanMap() { + return ttidSpanMap; } private void setColdStart(final @Nullable Bundle savedInstanceState) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FirstDrawDoneListener.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FirstDrawDoneListener.java new file mode 100644 index 0000000000..7c5574ead0 --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FirstDrawDoneListener.java @@ -0,0 +1,95 @@ +package io.sentry.android.core.internal.util; + +import android.annotation.SuppressLint; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.view.View; +import android.view.ViewTreeObserver; +import androidx.annotation.RequiresApi; +import io.sentry.android.core.BuildInfoProvider; +import java.util.concurrent.atomic.AtomicReference; +import org.jetbrains.annotations.NotNull; + +/** + * OnDrawListener that unregisters itself and invokes callback when the next draw is done. This API + * 16+ implementation is an approximation of the initial-display-time defined by Android Vitals. + */ +@SuppressLint("ObsoleteSdkInt") +@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) +public class FirstDrawDoneListener implements ViewTreeObserver.OnDrawListener { + private final @NotNull Handler mainThreadHandler = new Handler(Looper.getMainLooper()); + private final @NotNull AtomicReference viewReference; + private final @NotNull Runnable callback; + + /** Registers a post-draw callback for the next draw of a view. */ + public static void registerForNextDraw( + final @NotNull View view, + final @NotNull Runnable drawDoneCallback, + final @NotNull BuildInfoProvider buildInfoProvider) { + final FirstDrawDoneListener listener = new FirstDrawDoneListener(view, drawDoneCallback); + // Handle bug prior to API 26 where OnDrawListener from the floating ViewTreeObserver is not + // merged into the real ViewTreeObserver. + // https://android.googlesource.com/platform/frameworks/base/+/9f8ec54244a5e0343b9748db3329733f259604f3 + if (buildInfoProvider.getSdkInfoVersion() < 26 + && !isAliveAndAttached(view, buildInfoProvider)) { + view.addOnAttachStateChangeListener( + new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View view) { + view.getViewTreeObserver().addOnDrawListener(listener); + view.removeOnAttachStateChangeListener(this); + } + + @Override + public void onViewDetachedFromWindow(View view) { + view.removeOnAttachStateChangeListener(this); + } + }); + } else { + view.getViewTreeObserver().addOnDrawListener(listener); + } + } + + private FirstDrawDoneListener(final @NotNull View view, final @NotNull Runnable callback) { + this.viewReference = new AtomicReference<>(view); + this.callback = callback; + } + + @Override + public void onDraw() { + // Set viewReference to null so any onDraw past the first is a no-op + final View view = viewReference.getAndSet(null); + if (view == null) { + return; + } + // OnDrawListeners cannot be removed within onDraw, so we remove it with a + // GlobalLayoutListener + view.getViewTreeObserver() + .addOnGlobalLayoutListener(() -> view.getViewTreeObserver().removeOnDrawListener(this)); + mainThreadHandler.postAtFrontOfQueue(callback); + } + + /** + * Helper to avoid bug + * prior to API 26, where the floating ViewTreeObserver's OnDrawListeners are not merged into + * the real ViewTreeObserver during attach. + * + * @return true if the View is already attached and the ViewTreeObserver is not a floating + * placeholder. + */ + private static boolean isAliveAndAttached( + final @NotNull View view, final @NotNull BuildInfoProvider buildInfoProvider) { + return view.getViewTreeObserver().isAlive() && isAttachedToWindow(view, buildInfoProvider); + } + + @SuppressLint("NewApi") + private static boolean isAttachedToWindow( + final @NotNull View view, final @NotNull BuildInfoProvider buildInfoProvider) { + if (buildInfoProvider.getSdkInfoVersion() >= 19) { + return view.isAttachedToWindow(); + } + return view.getWindowToken() != null; + } +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt index b522f61b41..c3fb38755a 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt @@ -525,9 +525,10 @@ class ActivityLifecycleIntegrationTest { val activity = mock() sut.onActivityCreated(activity, fixture.bundle) - sut.onActivityDestroyed(activity) + assertNotNull(sut.ttidSpanMap[activity]) - assertNull(sut.ttidSpan) + sut.onActivityDestroyed(activity) + assertNull(sut.ttidSpanMap[activity]) } @Test diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/FirstDrawDoneListenerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/FirstDrawDoneListenerTest.kt new file mode 100644 index 0000000000..07fc383e1d --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/FirstDrawDoneListenerTest.kt @@ -0,0 +1,138 @@ +package io.sentry.android.core.internal.util + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.view.View +import android.view.ViewTreeObserver +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.sentry.android.core.BuildInfoProvider +import io.sentry.test.getProperty +import org.junit.runner.RunWith +import org.mockito.kotlin.inOrder +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.robolectric.Shadows +import java.util.concurrent.CopyOnWriteArrayList +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertIs +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +class FirstDrawDoneListenerTest { + + private class Fixture { + val application: Context = ApplicationProvider.getApplicationContext() + val buildInfo = mock() + lateinit var onDrawListeners: ArrayList + + fun getSut(apiVersion: Int = 26): View { + whenever(buildInfo.sdkInfoVersion).thenReturn(apiVersion) + val view = View(application) + + // Adding a listener forces ViewTreeObserver.mOnDrawListeners to be initialized and non-null. + val dummyListener = ViewTreeObserver.OnDrawListener {} + view.viewTreeObserver.addOnDrawListener(dummyListener) + view.viewTreeObserver.removeOnDrawListener(dummyListener) + + // Obtain mOnDrawListeners field through reflection + onDrawListeners = view.viewTreeObserver.getProperty("mOnDrawListeners") + assertTrue(onDrawListeners.isEmpty()) + + return view + } + } + + private val fixture = Fixture() + + @Test + fun `registerForNextDraw adds listener on attach state changed on sdk 25-`() { + val view = fixture.getSut(25) + + // OnDrawListener is not registered, it is delayed for later + FirstDrawDoneListener.registerForNextDraw(view, {}, fixture.buildInfo) + assertTrue(fixture.onDrawListeners.isEmpty()) + + // Register listener after the view is attached to a window + val listenerInfo = Class.forName("android.view.View\$ListenerInfo") + val mListenerInfo: Any = view.getProperty("mListenerInfo") + val mOnAttachStateChangeListeners: CopyOnWriteArrayList = + mListenerInfo.getProperty(listenerInfo, "mOnAttachStateChangeListeners") + assertFalse(mOnAttachStateChangeListeners.isEmpty()) + + // Dispatch onViewAttachedToWindow() + for (listener in mOnAttachStateChangeListeners) { + listener.onViewAttachedToWindow(view) + } + + assertFalse(fixture.onDrawListeners.isEmpty()) + assertIs(fixture.onDrawListeners[0]) + + // mOnAttachStateChangeListeners is automatically removed + assertTrue(mOnAttachStateChangeListeners.isEmpty()) + } + + @Test + fun `registerForNextDraw adds listener on sdk 26+`() { + val view = fixture.getSut() + + // Immediately register an OnDrawListener to ViewTreeObserver + FirstDrawDoneListener.registerForNextDraw(view, {}, fixture.buildInfo) + assertFalse(fixture.onDrawListeners.isEmpty()) + assertIs(fixture.onDrawListeners[0]) + } + + @Test + fun `registerForNextDraw posts callback to front of queue`() { + val view = fixture.getSut() + val handler = Handler(Looper.getMainLooper()) + val drawDoneCallback = mock() + val otherCallback = mock() + val inOrder = inOrder(drawDoneCallback, otherCallback) + FirstDrawDoneListener.registerForNextDraw(view, drawDoneCallback, fixture.buildInfo) + handler.post(otherCallback) // 3rd in queue + handler.postAtFrontOfQueue(otherCallback) // 2nd in queue + view.viewTreeObserver.dispatchOnDraw() // 1st in queue + verify(drawDoneCallback, never()).run() + verify(otherCallback, never()).run() + + // Execute all posted tasks + Shadows.shadowOf(Looper.getMainLooper()).idle() + inOrder.verify(drawDoneCallback).run() + inOrder.verify(otherCallback, times(2)).run() + inOrder.verifyNoMoreInteractions() + } + + @Test + fun `registerForNextDraw unregister itself after onDraw`() { + val view = fixture.getSut() + FirstDrawDoneListener.registerForNextDraw(view, {}, fixture.buildInfo) + assertFalse(fixture.onDrawListeners.isEmpty()) + + // Does not remove OnDrawListener before onDraw, even if OnGlobalLayout is triggered + view.viewTreeObserver.dispatchOnGlobalLayout() + assertFalse(fixture.onDrawListeners.isEmpty()) + + // Removes OnDrawListener in the next OnGlobalLayout after onDraw + view.viewTreeObserver.dispatchOnDraw() + view.viewTreeObserver.dispatchOnGlobalLayout() + assertTrue(fixture.onDrawListeners.isEmpty()) + } + + @Test + fun `registerForNextDraw calls the given callback on the main thread after onDraw`() { + val view = fixture.getSut() + val r: Runnable = mock() + FirstDrawDoneListener.registerForNextDraw(view, r, fixture.buildInfo) + view.viewTreeObserver.dispatchOnDraw() + + // Execute all tasks posted to main looper + Shadows.shadowOf(Looper.getMainLooper()).idle() + verify(r).run() + } +} diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java index 6f3c401633..3b3cc037b8 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java @@ -7,7 +7,6 @@ import io.sentry.ISpan; import io.sentry.MeasurementUnit; import io.sentry.Sentry; -import io.sentry.SpanStatus; import io.sentry.UserFeedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.User; @@ -206,7 +205,7 @@ protected void onResume() { final ISpan span = Sentry.getSpan(); if (span != null) { span.setMeasurement("screen_load_count", screenLoadCount, new MeasurementUnit.Custom("test")); - span.finish(SpanStatus.OK); + // span.finish(SpanStatus.OK); } } } diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/ProfilingActivity.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/ProfilingActivity.kt index 60def396b9..1ec37bee4b 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/ProfilingActivity.kt +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/ProfilingActivity.kt @@ -140,11 +140,6 @@ class ProfilingActivity : AppCompatActivity() { } } - override fun onResume() { - super.onResume() - Sentry.getSpan()?.finish() - } - override fun onBackPressed() { if (profileFinished) { super.onBackPressed() diff --git a/sentry-test-support/src/main/kotlin/io/sentry/test/Reflection.kt b/sentry-test-support/src/main/kotlin/io/sentry/test/Reflection.kt index f8b4df892e..f76d86e817 100644 --- a/sentry-test-support/src/main/kotlin/io/sentry/test/Reflection.kt +++ b/sentry-test-support/src/main/kotlin/io/sentry/test/Reflection.kt @@ -37,11 +37,13 @@ fun Class<*>.containsMethod(name: String, parameterTypes: Array>): Bool fun Class<*>.containsMethod(name: String, parameterTypes: Class<*>): Boolean = containsMethod(name, arrayOf(parameterTypes)) -inline fun Any.getProperty(name: String): T = +inline fun Any.getProperty(name: String): T = this.getProperty(this::class.java, name) + +inline fun Any.getProperty(clz: Class<*>, name: String): T = try { - this::class.java.getDeclaredField(name) + clz.getDeclaredField(name) } catch (_: NoSuchFieldException) { - this::class.java.superclass.getDeclaredField(name) + clz.superclass.getDeclaredField(name) }.apply { this.isAccessible = true }.get(this) as T From 37b5edc2b0ce0b159517b65c7de04982248388b9 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Wed, 30 Nov 2022 17:05:49 -0800 Subject: [PATCH 06/17] added Instrumenter.Sentry to ttid span --- CHANGELOG.md | 7 ++++++- .../sentry/android/core/ActivityLifecycleIntegration.java | 8 ++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9beb4249a0..c146dc80a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Add ttid span to ActivityLifecycleIntegration ([#2369](https://github.com/getsentry/sentry-java/pull/2369)) + ## 6.9.1 ### Fixes @@ -30,7 +36,6 @@ ### Features -- Add ttid span to ActivityLifecycleIntegration ([#2369](https://github.com/getsentry/sentry-java/pull/2369)) - Add FrameMetrics to Android profiling data ([#2342](https://github.com/getsentry/sentry-java/pull/2342)) ### Fixes diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index 8a03274059..f4b8612351 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -217,12 +217,16 @@ private void startTracing(final @NotNull Activity activity) { Instrumenter.SENTRY); // The first activity ttidSpan should start at the same time as the app start time ttidSpanMap.put( - activity, transaction.startChild("TTID", activityName + ".ttid", appStartTime)); + activity, + transaction.startChild( + "TTID", activityName + ".ttid", appStartTime, Instrumenter.SENTRY)); } else { // Other activities (or in case appStartTime is not available) the ttid span should // start when the previous activity called its onPause method. ttidSpanMap.put( - activity, transaction.startChild("TTID", activityName + ".ttid", lastPausedTime)); + activity, + transaction.startChild( + "TTID", activityName + ".ttid", lastPausedTime, Instrumenter.SENTRY)); } // lets bind to the scope so other integrations can pick it up From 13a6c1a87ab681d7ed029032dbf4e36dae63e974 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Fri, 2 Dec 2022 14:26:43 -0800 Subject: [PATCH 07/17] added reference to Firebase sdk --- .../android/core/internal/util/FirstDrawDoneListener.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FirstDrawDoneListener.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FirstDrawDoneListener.java index 7c5574ead0..10c160377b 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FirstDrawDoneListener.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FirstDrawDoneListener.java @@ -14,6 +14,10 @@ /** * OnDrawListener that unregisters itself and invokes callback when the next draw is done. This API * 16+ implementation is an approximation of the initial-display-time defined by Android Vitals. + * + *

Adapted from Firebase + * under the Apache License, Version 2.0. */ @SuppressLint("ObsoleteSdkInt") @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) From a72bddabbe58a67118ed53209c6bd24d8a734017 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Sat, 3 Dec 2022 21:27:46 -0800 Subject: [PATCH 08/17] added first ttfd internal implementation --- .../core/ActivityLifecycleIntegration.java | 42 +++++++++++++++++-- .../internal/util/FullyDrawnReporter.java | 42 +++++++++++++++++++ .../core/ActivityLifecycleIntegrationTest.kt | 16 +++++++ .../sentry/samples/android/MainActivity.java | 1 + .../sentry/samples/android/SecondActivity.kt | 2 + 5 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FullyDrawnReporter.java diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index f4b8612351..e4c310d84d 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -30,6 +30,7 @@ import io.sentry.TransactionContext; import io.sentry.TransactionOptions; import io.sentry.android.core.internal.util.FirstDrawDoneListener; +import io.sentry.android.core.internal.util.FullyDrawnReporter; import io.sentry.protocol.TransactionNameSource; import io.sentry.util.Objects; import java.io.Closeable; @@ -68,6 +69,7 @@ public final class ActivityLifecycleIntegration private final @NotNull WeakHashMap ttidSpanMap = new WeakHashMap<>(); private @NotNull Date lastPausedTime = DateUtils.getCurrentDateTime(); private final @NotNull Handler mainHandler = new Handler(Looper.getMainLooper()); + private final @NotNull WeakHashMap ttfdSpanMap = new WeakHashMap<>(); // WeakHashMap isn't thread safe but ActivityLifecycleCallbacks is only called from the // main-thread @@ -159,7 +161,8 @@ private void stopPreviousTransactions() { activitiesWithOngoingTransactions.entrySet()) { final ITransaction transaction = entry.getValue(); final ISpan ttidSpan = ttidSpanMap.get(entry.getKey()); - finishTransaction(transaction, ttidSpan); + final ISpan ttfdSpan = ttfdSpanMap.get(entry.getKey()); + finishTransaction(transaction, ttidSpan, ttfdSpan); } } @@ -228,6 +231,10 @@ private void startTracing(final @NotNull Activity activity) { transaction.startChild( "TTID", activityName + ".ttid", lastPausedTime, Instrumenter.SENTRY)); } + ttfdSpanMap.put( + activity, + transaction.startChild( + "TTFD", activityName + ".ttfd", lastPausedTime, Instrumenter.SENTRY)); // lets bind to the scope so other integrations can pick it up hub.configureScope( @@ -276,12 +283,13 @@ private void stopTracing(final @NotNull Activity activity, final boolean shouldF if (performanceEnabled && shouldFinishTracing) { final ITransaction transaction = activitiesWithOngoingTransactions.get(activity); final ISpan ttidSpan = ttidSpanMap.get(activity); - finishTransaction(transaction, ttidSpan); + final ISpan ttfdSpan = ttfdSpanMap.get(activity); + finishTransaction(transaction, ttidSpan, ttfdSpan); } } private void finishTransaction( - final @Nullable ITransaction transaction, final @Nullable ISpan ttidSpan) { + final @Nullable ITransaction transaction, final @Nullable ISpan ttidSpan, final @Nullable ISpan ttfdSpan) { if (transaction != null) { // if io.sentry.traces.activity.auto-finish.enable is disabled, transaction may be already // finished manually when this method is called. @@ -294,6 +302,11 @@ private void finishTransaction( ttidSpan.finish(SpanStatus.CANCELLED); } + // in case the ttfdSpan isn't completed yet, we finish it as cancelled to avoid memory leak + if (ttfdSpan != null && !ttfdSpan.isFinished()) { + ttfdSpan.finish(SpanStatus.CANCELLED); + } + SpanStatus status = transaction.getStatus(); // status might be set by other integrations, let's not overwrite it if (status == null) { @@ -319,6 +332,20 @@ public synchronized void onActivityCreated( startTracing(activity); firstActivityCreated = true; + + ISpan ttfdSpan = ttfdSpanMap.get(activity); + FullyDrawnReporter.getInstance().registerFullyDrawnListener(new FullyDrawnReporter.FullyDrawnReporterListener() { + @Override + public boolean onFullyDrawn(@NotNull final Activity reportedActivity) { + ISpan reportedTtfdSpan = ttfdSpanMap.get(reportedActivity); + // finishes ttfdSpan span + if (ttfdSpan == reportedTtfdSpan && reportedTtfdSpan != null && !ttfdSpan.isFinished()) { + ttfdSpan.finish(); + return true; + } + return false; + } + }); } @Override @@ -445,8 +472,9 @@ public synchronized void onActivityDestroyed(final @NotNull Activity activity) { // set it to null in case its been just finished as cancelled appStartSpan = null; - // ttidSpan is finished in the stopTracing() method + // ttidSpan and ttfd are finished in the stopTracing() method ttidSpanMap.remove(activity); + ttfdSpanMap.remove(activity); // clear it up, so we don't start again for the same activity if the activity is in the activity // stack still. @@ -480,6 +508,12 @@ WeakHashMap getTtidSpanMap() { return ttidSpanMap; } + @TestOnly + @NotNull + WeakHashMap getTtfdSpanMap() { + return ttfdSpanMap; + } + private void setColdStart(final @Nullable Bundle savedInstanceState) { if (!firstActivityCreated) { // if Activity has savedInstanceState then its a warm start diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FullyDrawnReporter.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FullyDrawnReporter.java new file mode 100644 index 0000000000..6a4dfa058d --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FullyDrawnReporter.java @@ -0,0 +1,42 @@ +package io.sentry.android.core.internal.util; + +import android.app.Activity; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +@ApiStatus.Internal +public final class FullyDrawnReporter { + + private static final @NotNull FullyDrawnReporter instance = new FullyDrawnReporter(); + private final @NotNull Map listeners = new ConcurrentHashMap<>(); + + private FullyDrawnReporter() {} + + public static @NotNull FullyDrawnReporter getInstance() { + return instance; + } + + public void registerFullyDrawnListener(@NotNull final FullyDrawnReporterListener listener) { + listeners.put(listener.uuid, listener); + } + + public void reportFullyDrawn(@NotNull final Activity reportedActivity) { + for (FullyDrawnReporterListener listener : listeners.values()) { + if (listener.onFullyDrawn(reportedActivity)) { + listeners.remove(listener.uuid); + } + } + } + + @ApiStatus.Internal + public abstract static class FullyDrawnReporterListener { + @NotNull final String uuid = UUID.randomUUID().toString(); + + public abstract boolean onFullyDrawn(@NotNull final Activity reportedActivity); + } +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt index c3fb38755a..63572d5341 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt @@ -531,6 +531,22 @@ class ActivityLifecycleIntegrationTest { assertNull(sut.ttidSpanMap[activity]) } + @Test + fun `When Activity is destroyed, sets ttfdSpan to null`() { + val sut = fixture.getSut() + fixture.options.tracesSampleRate = 1.0 + sut.register(fixture.hub, fixture.options) + + setAppStartTime() + + val activity = mock() + sut.onActivityCreated(activity, fixture.bundle) + assertNotNull(sut.ttfdSpanMap[activity]) + + sut.onActivityDestroyed(activity) + assertNull(sut.ttfdSpanMap[activity]) + } + @Test fun `When new Activity and transaction is created, finish previous ones`() { val sut = fixture.getSut() diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java index 3b3cc037b8..8d2ab708f5 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java @@ -196,6 +196,7 @@ protected void onCreate(Bundle savedInstanceState) { }); setContentView(binding.getRoot()); + Sentry.reportFullyDrawn(this); } @Override diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SecondActivity.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SecondActivity.kt index d788937756..269957c189 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SecondActivity.kt +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SecondActivity.kt @@ -76,6 +76,7 @@ class SecondActivity : AppCompatActivity() { showText(true, "error: ${t.message}") + Sentry.reportFullyDrawn(this@SecondActivity) // I opt out enableActivityLifecycleTracingAutoFinish so I know best when to end my transaction // be sure to finish all your spans before this val transaction = Sentry.getSpan() @@ -89,6 +90,7 @@ class SecondActivity : AppCompatActivity() { showText(text = "items: ${repos.size}") + Sentry.reportFullyDrawn(this@SecondActivity) // I opt out enableActivityLifecycleTracingAutoFinish so I know best when to end my transaction // be sure to finish all your spans before this val transaction = Sentry.getSpan() From d54c1f0e6d071c07860b2609e60899363d2ba77b Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Thu, 15 Dec 2022 20:47:59 +0100 Subject: [PATCH 09/17] added ttfd span added a FullyDrawnReporter instance to SentryAndroid added a new api SentryAndroid.reportFullyDrawn --- .../api/sentry-android-core.api | 2 + .../core/ActivityLifecycleIntegration.java | 55 +++++++++++++------ .../core/AndroidOptionsInitializer.java | 15 +++-- .../io/sentry/android/core/SentryAndroid.java | 12 +++- .../internal/util/FullyDrawnReporter.java | 9 ++- .../core/ActivityLifecycleIntegrationTest.kt | 27 +++++++-- .../core/AndroidOptionsInitializerTest.kt | 5 +- .../core/AndroidTransactionProfilerTest.kt | 5 +- .../sentry/samples/android/MainActivity.java | 15 ++++- .../sentry/samples/android/SecondActivity.kt | 5 +- 10 files changed, 113 insertions(+), 37 deletions(-) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index fff1af2668..91c625ae7e 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -10,6 +10,7 @@ public final class io/sentry/android/core/ActivityFramesTracker { public final class io/sentry/android/core/ActivityLifecycleIntegration : android/app/Application$ActivityLifecycleCallbacks, io/sentry/Integration, java/io/Closeable { public fun (Landroid/app/Application;Lio/sentry/android/core/BuildInfoProvider;Lio/sentry/android/core/ActivityFramesTracker;)V + public fun (Landroid/app/Application;Lio/sentry/android/core/BuildInfoProvider;Lio/sentry/android/core/ActivityFramesTracker;Lio/sentry/android/core/internal/util/FullyDrawnReporter;)V public fun close ()V public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V public fun onActivityDestroyed (Landroid/app/Activity;)V @@ -139,6 +140,7 @@ public final class io/sentry/android/core/SentryAndroid { public static fun init (Landroid/content/Context;Lio/sentry/ILogger;)V public static fun init (Landroid/content/Context;Lio/sentry/ILogger;Lio/sentry/Sentry$OptionsConfiguration;)V public static fun init (Landroid/content/Context;Lio/sentry/Sentry$OptionsConfiguration;)V + public static fun reportFullyDrawn (Landroid/app/Activity;)V } public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/SentryOptions { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index 126b4fe663..2ce5342a02 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -52,6 +52,7 @@ public final class ActivityLifecycleIntegration 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"; private final @NotNull Application application; private final @NotNull BuildInfoProvider buildInfoProvider; @@ -66,6 +67,7 @@ public final class ActivityLifecycleIntegration private boolean firstActivityResumed = false; private boolean foregroundImportance = false; + private final @NotNull FullyDrawnReporter fullyDrawnReporter; private @Nullable ISpan appStartSpan; private final @NotNull WeakHashMap ttidSpanMap = new WeakHashMap<>(); private @NotNull Date lastPausedTime = DateUtils.getCurrentDateTime(); @@ -83,11 +85,21 @@ public ActivityLifecycleIntegration( final @NotNull Application application, final @NotNull BuildInfoProvider buildInfoProvider, final @NotNull ActivityFramesTracker activityFramesTracker) { + this(application, buildInfoProvider, activityFramesTracker, FullyDrawnReporter.getInstance()); + } + + public ActivityLifecycleIntegration( + final @NotNull Application application, + final @NotNull BuildInfoProvider buildInfoProvider, + final @NotNull ActivityFramesTracker activityFramesTracker, + final @NotNull FullyDrawnReporter fullyDrawnReporter) { 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.fullyDrawnReporter = + Objects.requireNonNull(fullyDrawnReporter, "FullyDrawnReporter is required"); if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.Q) { isAllActivityCallbacksAvailable = true; @@ -233,9 +245,9 @@ private void startTracing(final @NotNull Activity activity) { TTID_OP, getTtidDesc(activityName), lastPausedTime, Instrumenter.SENTRY)); } ttfdSpanMap.put( - activity, - transaction.startChild( - "TTFD", activityName + ".ttfd", lastPausedTime, Instrumenter.SENTRY)); + activity, + transaction.startChild( + TTFD_OP, getTtfdDesc(activityName), lastPausedTime, Instrumenter.SENTRY)); // lets bind to the scope so other integrations can pick it up hub.configureScope( @@ -288,7 +300,9 @@ private void stopTracing(final @NotNull Activity activity, final boolean shouldF } private void finishTransaction( - final @Nullable ITransaction transaction, final @Nullable ISpan ttidSpan, final @Nullable ISpan ttfdSpan) { + final @Nullable ITransaction transaction, + final @Nullable ISpan ttidSpan, + final @Nullable ISpan ttfdSpan) { if (transaction != null) { // if io.sentry.traces.activity.auto-finish.enable is disabled, transaction may be already // finished manually when this method is called. @@ -327,18 +341,21 @@ public synchronized void onActivityCreated( firstActivityCreated = true; ISpan ttfdSpan = ttfdSpanMap.get(activity); - FullyDrawnReporter.getInstance().registerFullyDrawnListener(new FullyDrawnReporter.FullyDrawnReporterListener() { - @Override - public boolean onFullyDrawn(@NotNull final Activity reportedActivity) { - ISpan reportedTtfdSpan = ttfdSpanMap.get(reportedActivity); - // finishes ttfdSpan span - if (ttfdSpan == reportedTtfdSpan && reportedTtfdSpan != null && !ttfdSpan.isFinished()) { - ttfdSpan.finish(); - return true; - } - return false; - } - }); + fullyDrawnReporter.registerFullyDrawnListener( + new FullyDrawnReporter.FullyDrawnReporterListener() { + @Override + public boolean onFullyDrawn(@NotNull final Activity reportedActivity) { + ISpan reportedTtfdSpan = ttfdSpanMap.get(reportedActivity); + // finishes ttfdSpan span + if (ttfdSpan == reportedTtfdSpan + && reportedTtfdSpan != null + && !ttfdSpan.isFinished()) { + ttfdSpan.finish(); + return true; + } + return false; + } + }); } @Override @@ -448,7 +465,7 @@ public synchronized void onActivityDestroyed(final @NotNull Activity activity) { final ISpan ttidSpan = ttidSpanMap.get(activity); finishSpan(ttidSpan, SpanStatus.CANCELLED); - // we finish the ttfdSpan as cancelled in case it isn't completed yet + // we finish the ttfdSpan as cancelled in case it isn't completed yet final ISpan ttfdSpan = ttfdSpanMap.get(activity); finishSpan(ttfdSpan, SpanStatus.CANCELLED); @@ -523,6 +540,10 @@ private void setColdStart(final @Nullable Bundle savedInstanceState) { return activityName + " initial display"; } + private @NotNull String getTtfdDesc(final @NotNull String activityName) { + return activityName + " full display"; + } + private @NotNull String getAppStartDesc(final boolean coldStart) { if (coldStart) { return "Cold Start"; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index 821f147119..b00499b464 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -13,6 +13,7 @@ import io.sentry.SentryLevel; import io.sentry.android.core.cache.AndroidEnvelopeCache; import io.sentry.android.core.internal.modules.AssetsModulesLoader; +import io.sentry.android.core.internal.util.FullyDrawnReporter; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.android.fragment.FragmentLifecycleIntegration; import io.sentry.android.timber.SentryTimberIntegration; @@ -92,7 +93,8 @@ static void initializeIntegrationsAndProcessors( new BuildInfoProvider(new AndroidLogger()), new LoadClass(), false, - false); + false, + FullyDrawnReporter.getInstance()); } static void initializeIntegrationsAndProcessors( @@ -101,7 +103,8 @@ static void initializeIntegrationsAndProcessors( final @NotNull BuildInfoProvider buildInfoProvider, final @NotNull LoadClass loadClass, final boolean isFragmentAvailable, - final boolean isTimberAvailable) { + final boolean isTimberAvailable, + final @NotNull FullyDrawnReporter fullyDrawnReporter) { if (options.getCacheDirPath() != null && options.getEnvelopeDiskCache() instanceof NoOpEnvelopeCache) { @@ -118,7 +121,8 @@ static void initializeIntegrationsAndProcessors( loadClass, activityFramesTracker, isFragmentAvailable, - isTimberAvailable); + isTimberAvailable, + fullyDrawnReporter); options.addEventProcessor( new DefaultAndroidEventProcessor(context, buildInfoProvider, options)); @@ -139,7 +143,8 @@ private static void installDefaultIntegrations( final @NotNull LoadClass loadClass, final @NotNull ActivityFramesTracker activityFramesTracker, final boolean isFragmentAvailable, - final boolean isTimberAvailable) { + final boolean isTimberAvailable, + final @NotNull FullyDrawnReporter fullyDrawnReporter) { // read the startup crash marker here to avoid doing double-IO for the SendCachedEnvelope // integrations below @@ -177,7 +182,7 @@ private static void installDefaultIntegrations( if (context instanceof Application) { options.addIntegration( new ActivityLifecycleIntegration( - (Application) context, buildInfoProvider, activityFramesTracker)); + (Application) context, buildInfoProvider, activityFramesTracker, fullyDrawnReporter)); options.addIntegration(new UserInteractionIntegration((Application) context, loadClass)); if (isFragmentAvailable) { options.addIntegration(new FragmentLifecycleIntegration((Application) context, true, true)); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java index 9d046cbdb7..c582294e6e 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java @@ -1,5 +1,6 @@ package io.sentry.android.core; +import android.app.Activity; import android.content.Context; import android.os.SystemClock; import io.sentry.DateUtils; @@ -11,6 +12,7 @@ import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.android.core.internal.util.BreadcrumbFactory; +import io.sentry.android.core.internal.util.FullyDrawnReporter; import io.sentry.android.fragment.FragmentLifecycleIntegration; import io.sentry.android.timber.SentryTimberIntegration; import java.lang.reflect.InvocationTargetException; @@ -37,6 +39,9 @@ public final class SentryAndroid { private static final String FRAGMENT_CLASS_NAME = "androidx.fragment.app.FragmentManager$FragmentLifecycleCallbacks"; + private static final @NotNull FullyDrawnReporter fullyDrawnReporter = + FullyDrawnReporter.getInstance(); + private SentryAndroid() {} /** @@ -116,7 +121,8 @@ public static synchronized void init( buildInfoProvider, loadClass, isFragmentAvailable, - isTimberAvailable); + isTimberAvailable, + fullyDrawnReporter); deduplicateIntegrations(options, isFragmentAvailable, isTimberAvailable); }, @@ -190,4 +196,8 @@ private static void deduplicateIntegrations( } } } + + public static void reportFullyDrawn(@NotNull Activity activity) { + fullyDrawnReporter.reportFullyDrawn(activity); + } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FullyDrawnReporter.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FullyDrawnReporter.java index 6a4dfa058d..78a3317ff6 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FullyDrawnReporter.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FullyDrawnReporter.java @@ -1,19 +1,18 @@ package io.sentry.android.core.internal.util; import android.app.Activity; - -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; @ApiStatus.Internal public final class FullyDrawnReporter { private static final @NotNull FullyDrawnReporter instance = new FullyDrawnReporter(); - private final @NotNull Map listeners = new ConcurrentHashMap<>(); + private final @NotNull Map listeners = + new ConcurrentHashMap<>(); private FullyDrawnReporter() {} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt index 24270230cb..4eb11fc61b 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt @@ -16,6 +16,7 @@ import io.sentry.TraceContext import io.sentry.TransactionContext import io.sentry.TransactionFinishedCallback import io.sentry.TransactionOptions +import io.sentry.android.core.internal.util.FullyDrawnReporter import io.sentry.protocol.TransactionNameSource import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull @@ -30,6 +31,7 @@ import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertNotEquals import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertSame @@ -47,6 +49,7 @@ class ActivityLifecycleIntegrationTest { val bundle = mock() val context = TransactionContext("name", "op") val activityFramesTracker = mock() + val fullyDrawnReporter = FullyDrawnReporter.getInstance() val transactionFinishedCallback = mock() lateinit var transaction: SentryTracer val buildInfo = mock() @@ -66,7 +69,7 @@ class ActivityLifecycleIntegrationTest { whenever(am.runningAppProcesses).thenReturn(processes) - return ActivityLifecycleIntegration(application, buildInfo, activityFramesTracker) + return ActivityLifecycleIntegration(application, buildInfo, activityFramesTracker, fullyDrawnReporter) } } @@ -363,7 +366,7 @@ class ActivityLifecycleIntegrationTest { } @Test - fun `When tracing auto finish is enabled and ttid span is finished, it stops the transaction on onActivityPostResumed`() { + fun `When tracing auto finish is enabled and ttid and ttfd spans are finished, it stops the transaction on onActivityPostResumed`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) @@ -371,6 +374,7 @@ class ActivityLifecycleIntegrationTest { val activity = mock() sut.onActivityCreated(activity, fixture.bundle) sut.ttidSpanMap.values.first().finish() + sut.ttfdSpanMap.values.first().finish() sut.onActivityPostResumed(activity) verify(fixture.hub).captureTransaction( @@ -564,7 +568,7 @@ class ActivityLifecycleIntegrationTest { sut.onActivityCreated(activity, fixture.bundle) sut.onActivityDestroyed(activity) - val span = fixture.transaction.children.first { it.operation == "ttfd" } + val span = fixture.transaction.children.first { it.operation == ActivityLifecycleIntegration.TTFD_OP } assertEquals(SpanStatus.CANCELLED, span.status) assertTrue(span.isFinished) } @@ -625,7 +629,7 @@ class ActivityLifecycleIntegrationTest { } @Test - fun `stop transaction on resumed if API 29 less than 29 and ttid is finished`() { + fun `stop transaction on resumed if API 29 less than 29 and ttid and ttfd are finished`() { val sut = fixture.getSut(14) fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) @@ -633,11 +637,26 @@ class ActivityLifecycleIntegrationTest { val activity = mock() sut.onActivityCreated(activity, mock()) sut.ttidSpanMap.values.first().finish() + sut.ttfdSpanMap.values.first().finish() sut.onActivityResumed(activity) verify(fixture.hub).captureTransaction(any(), anyOrNull(), anyOrNull()) } + @Test + fun `reportFullyDrawn finishes the ttfd`() { + val sut = fixture.getSut() + fixture.options.tracesSampleRate = 1.0 + sut.register(fixture.hub, fixture.options) + + val activity = mock() + sut.onActivityCreated(activity, mock()) + sut.ttidSpanMap.values.first().finish() + fixture.fullyDrawnReporter.reportFullyDrawn(activity) + assertTrue(sut.ttfdSpanMap.values.first().isFinished) + assertNotEquals(SpanStatus.CANCELLED, sut.ttfdSpanMap.values.first().status) + } + @Test fun `App start is Cold when savedInstanceState is null`() { val sut = fixture.getSut(14) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index f3bc66b7f1..b5d908b630 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -9,6 +9,7 @@ import io.sentry.MainEventProcessor import io.sentry.SentryOptions import io.sentry.android.core.cache.AndroidEnvelopeCache import io.sentry.android.core.internal.modules.AssetsModulesLoader +import io.sentry.android.core.internal.util.FullyDrawnReporter import io.sentry.android.fragment.FragmentLifecycleIntegration import io.sentry.android.timber.SentryTimberIntegration import org.junit.runner.RunWith @@ -31,6 +32,7 @@ class AndroidOptionsInitializerTest { val sentryOptions = SentryAndroidOptions() lateinit var mockContext: Context val logger = mock() + val mockFullyDrawnReporter = mock() fun initSut( metadata: Bundle? = null, @@ -90,7 +92,8 @@ class AndroidOptionsInitializerTest { buildInfo, createClassMock(classToLoad), isFragmentAvailable, - isTimberAvailable + isTimberAvailable, + mockFullyDrawnReporter ) } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt index 1a3eec8588..50a422ee83 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt @@ -11,6 +11,7 @@ import io.sentry.ProfilingTraceData import io.sentry.SentryLevel import io.sentry.SentryTracer import io.sentry.TransactionContext +import io.sentry.android.core.internal.util.FullyDrawnReporter import io.sentry.android.core.internal.util.SentryFrameMetricsCollector import io.sentry.assertEnvelopeItem import io.sentry.test.getCtor @@ -48,6 +49,7 @@ class AndroidTransactionProfilerTest { whenever(it.sdkInfoVersion).thenReturn(Build.VERSION_CODES.LOLLIPOP) } val mockLogger = mock() + val mockFullyDrawnReporter = mock() var lastScheduledRunnable: Runnable? = null val mockExecutorService = object : ISentryExecutorService { override fun submit(runnable: Runnable): Future<*> { @@ -101,7 +103,8 @@ class AndroidTransactionProfilerTest { buildInfoProvider, LoadClass(), false, - false + false, + fixture.mockFullyDrawnReporter ) // Profiler doesn't start if the folder doesn't exists. // Usually it's generated when calling Sentry.init, but for tests we can create it manually. diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java index 8d2ab708f5..0de24dc86b 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java @@ -8,6 +8,7 @@ import io.sentry.MeasurementUnit; import io.sentry.Sentry; import io.sentry.UserFeedback; +import io.sentry.android.core.SentryAndroid; import io.sentry.protocol.SentryId; import io.sentry.protocol.User; import io.sentry.samples.android.compose.ComposeActivity; @@ -196,7 +197,19 @@ protected void onCreate(Bundle savedInstanceState) { }); setContentView(binding.getRoot()); - Sentry.reportFullyDrawn(this); + + // Let's say the activity is fully drawn after 1 second + new Thread( + () -> { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + SentryAndroid.reportFullyDrawn(this); + reportFullyDrawn(); + }) + .start(); } @Override diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SecondActivity.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SecondActivity.kt index 269957c189..5505032ba7 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SecondActivity.kt +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SecondActivity.kt @@ -6,6 +6,7 @@ import android.view.View import androidx.appcompat.app.AppCompatActivity import io.sentry.Sentry import io.sentry.SpanStatus +import io.sentry.android.core.SentryAndroid import io.sentry.samples.android.databinding.ActivitySecondBinding import retrofit2.Call import retrofit2.Callback @@ -76,7 +77,7 @@ class SecondActivity : AppCompatActivity() { showText(true, "error: ${t.message}") - Sentry.reportFullyDrawn(this@SecondActivity) + SentryAndroid.reportFullyDrawn(this@SecondActivity) // I opt out enableActivityLifecycleTracingAutoFinish so I know best when to end my transaction // be sure to finish all your spans before this val transaction = Sentry.getSpan() @@ -90,7 +91,7 @@ class SecondActivity : AppCompatActivity() { showText(text = "items: ${repos.size}") - Sentry.reportFullyDrawn(this@SecondActivity) + SentryAndroid.reportFullyDrawn(this@SecondActivity) // I opt out enableActivityLifecycleTracingAutoFinish so I know best when to end my transaction // be sure to finish all your spans before this val transaction = Sentry.getSpan() From 43c11073a0877d16ba8deb212008e6110d52d607 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Fri, 16 Dec 2022 09:51:32 +0100 Subject: [PATCH 10/17] updated changelog --- CHANGELOG.md | 1 + .../io/sentry/android/core/AndroidOptionsInitializerTest.kt | 2 +- .../src/main/java/io/sentry/samples/android/MainActivity.java | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7ce05d739..26cb6555e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- Add time-to-full-display span to Activity transactions ([#2432](https://github.com/getsentry/sentry-java/pull/2432)) - Add time-to-initial-display span to Activity transactions ([#2369](https://github.com/getsentry/sentry-java/pull/2369)) - Start a session after init if AutoSessionTracking is enabled ([#2356](https://github.com/getsentry/sentry-java/pull/2356)) - Provide automatic breadcrumbs and transactions for click/scroll events for Compose ([#2390](https://github.com/getsentry/sentry-java/pull/2390)) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index bcca54e88e..111bc35e05 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -9,8 +9,8 @@ import io.sentry.MainEventProcessor import io.sentry.SentryOptions import io.sentry.android.core.cache.AndroidEnvelopeCache import io.sentry.android.core.internal.modules.AssetsModulesLoader -import io.sentry.android.core.internal.util.FullyDrawnReporter import io.sentry.android.core.internal.util.AndroidMainThreadChecker +import io.sentry.android.core.internal.util.FullyDrawnReporter import io.sentry.android.fragment.FragmentLifecycleIntegration import io.sentry.android.timber.SentryTimberIntegration import org.junit.runner.RunWith diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java index 6a138309b6..135cc009e4 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java @@ -8,8 +8,8 @@ import io.sentry.MeasurementUnit; import io.sentry.Sentry; import io.sentry.UserFeedback; -import io.sentry.instrumentation.file.SentryFileOutputStream; import io.sentry.android.core.SentryAndroid; +import io.sentry.instrumentation.file.SentryFileOutputStream; import io.sentry.protocol.SentryId; import io.sentry.protocol.User; import io.sentry.samples.android.compose.ComposeActivity; From 4bb58c07e4b34b647a36b1c1275f37b4ba0e788f Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Fri, 16 Dec 2022 10:49:59 +0100 Subject: [PATCH 11/17] updated ui test --- .../androidTest/java/io/sentry/uitest/android/EnvelopeTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/EnvelopeTests.kt b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/EnvelopeTests.kt index 39acc1b26a..3c4120dec9 100644 --- a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/EnvelopeTests.kt +++ b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/EnvelopeTests.kt @@ -52,7 +52,6 @@ class EnvelopeTests : BaseUiTest() { options.profilesSampleRate = 1.0 } - relayIdlingResource.increment() IdlingRegistry.getInstance().register(ProfilingSampleActivity.scrollingIdlingResource) val transaction = Sentry.startTransaction("profiledTransaction", "test1") val sampleScenario = launchActivity() @@ -61,6 +60,7 @@ class EnvelopeTests : BaseUiTest() { IdlingRegistry.getInstance().unregister(ProfilingSampleActivity.scrollingIdlingResource) relayIdlingResource.increment() relayIdlingResource.increment() + relayIdlingResource.increment() transaction.finish() relay.assert { From edcb1ff38f6c5820ee055e18eee5f6ad2c78a2b5 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Thu, 9 Feb 2023 17:55:09 +0100 Subject: [PATCH 12/17] added Sentry.reportFullyDrawn() API added FullyDisplayedReporter and put it in the options added ttfd span in ActivityLifecycleIntegration ttid and ttfd spans are now finished with DEADLINE_EXCEEDED instead of CANCELLED when new activity is started or activity is destroyed added io.sentry.traces.time-to-full-display.enable manifest option and enableTimeToFullDisplayTracing option, disabled by default --- CHANGELOG.md | 5 +- .../api/sentry-android-core.api | 2 - .../core/ActivityLifecycleIntegration.java | 116 ++++++------- .../core/AndroidOptionsInitializer.java | 15 +- .../android/core/ManifestMetadataReader.java | 5 + .../io/sentry/android/core/SentryAndroid.java | 12 +- .../internal/util/FullyDrawnReporter.java | 41 ----- .../core/ActivityLifecycleIntegrationTest.kt | 153 ++++++++++++++++-- .../core/AndroidOptionsInitializerTest.kt | 5 +- .../core/AndroidTransactionProfilerTest.kt | 5 +- .../core/ManifestMetadataReaderTest.kt | 26 +++ .../src/main/AndroidManifest.xml | 3 + .../sentry/samples/android/MainActivity.java | 15 +- .../samples/android/PermissionsActivity.kt | 2 + .../samples/android/ProfilingActivity.kt | 1 + .../sentry/samples/android/SecondActivity.kt | 5 +- sentry/api/sentry.api | 18 +++ .../io/sentry/FullyDisplayedReporter.java | 42 +++++ sentry/src/main/java/io/sentry/Hub.java | 7 + .../src/main/java/io/sentry/HubAdapter.java | 5 + sentry/src/main/java/io/sentry/IHub.java | 10 ++ sentry/src/main/java/io/sentry/NoOpHub.java | 3 + sentry/src/main/java/io/sentry/Sentry.java | 12 ++ .../main/java/io/sentry/SentryOptions.java | 35 ++++ .../io/sentry/FullyDisplayedReporterTest.kt | 49 ++++++ sentry/src/test/java/io/sentry/HubTest.kt | 43 +++++ sentry/src/test/java/io/sentry/NoOpHubTest.kt | 3 + .../test/java/io/sentry/SentryOptionsTest.kt | 10 ++ 28 files changed, 487 insertions(+), 161 deletions(-) delete mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FullyDrawnReporter.java create mode 100644 sentry/src/main/java/io/sentry/FullyDisplayedReporter.java create mode 100644 sentry/src/test/java/io/sentry/FullyDisplayedReporterTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 188df4e702..83d29e766b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ - Remove authority from URLs sent to Sentry ([#2366](https://github.com/getsentry/sentry-java/pull/2366)) - Fix `sentry-bom` containing incorrect artifacts ([#2504](https://github.com/getsentry/sentry-java/pull/2504)) +### Features + +- Add time-to-full-display span to Activity automatic transactions ([#2432](https://github.com/getsentry/sentry-java/pull/2432)) + ### Dependencies - Bump Native SDK from v0.5.3 to v0.5.4 ([#2500](https://github.com/getsentry/sentry-java/pull/2500)) @@ -78,7 +82,6 @@ ### Features -- Add time-to-full-display span to Activity transactions ([#2432](https://github.com/getsentry/sentry-java/pull/2432)) - Add time-to-initial-display span to Activity transactions ([#2369](https://github.com/getsentry/sentry-java/pull/2369)) - Start a session after init if AutoSessionTracking is enabled ([#2356](https://github.com/getsentry/sentry-java/pull/2356)) - Provide automatic breadcrumbs and transactions for click/scroll events for Compose ([#2390](https://github.com/getsentry/sentry-java/pull/2390)) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 9d03d04a60..20ea1e97d9 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -10,7 +10,6 @@ public final class io/sentry/android/core/ActivityFramesTracker { public final class io/sentry/android/core/ActivityLifecycleIntegration : android/app/Application$ActivityLifecycleCallbacks, io/sentry/Integration, java/io/Closeable { public fun (Landroid/app/Application;Lio/sentry/android/core/BuildInfoProvider;Lio/sentry/android/core/ActivityFramesTracker;)V - public fun (Landroid/app/Application;Lio/sentry/android/core/BuildInfoProvider;Lio/sentry/android/core/ActivityFramesTracker;Lio/sentry/android/core/internal/util/FullyDrawnReporter;)V public fun close ()V public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V public fun onActivityDestroyed (Landroid/app/Activity;)V @@ -167,7 +166,6 @@ public final class io/sentry/android/core/SentryAndroid { public static fun init (Landroid/content/Context;Lio/sentry/ILogger;)V public static fun init (Landroid/content/Context;Lio/sentry/ILogger;Lio/sentry/Sentry$OptionsConfiguration;)V public static fun init (Landroid/content/Context;Lio/sentry/Sentry$OptionsConfiguration;)V - public static fun reportFullyDrawn (Landroid/app/Activity;)V } public final class io/sentry/android/core/SentryAndroidDateProvider : io/sentry/SentryDateProvider { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index f70303f05c..7edc1f6e16 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -12,6 +12,7 @@ import android.view.View; import androidx.annotation.NonNull; import io.sentry.Breadcrumb; +import io.sentry.FullyDisplayedReporter; import io.sentry.Hint; import io.sentry.IHub; import io.sentry.ISpan; @@ -26,7 +27,6 @@ import io.sentry.TransactionContext; import io.sentry.TransactionOptions; import io.sentry.android.core.internal.util.FirstDrawDoneListener; -import io.sentry.android.core.internal.util.FullyDrawnReporter; import io.sentry.protocol.TransactionNameSource; import io.sentry.util.Objects; import java.io.Closeable; @@ -34,6 +34,7 @@ import java.lang.ref.WeakReference; import java.util.Map; import java.util.WeakHashMap; +import java.util.concurrent.Future; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; @@ -47,6 +48,7 @@ public final class ActivityLifecycleIntegration 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 = 30000; private final @NotNull Application application; private final @NotNull BuildInfoProvider buildInfoProvider; @@ -55,18 +57,21 @@ public final class ActivityLifecycleIntegration private boolean performanceEnabled = false; + private boolean timeToFullDisplaySpanEnabled = false; + private boolean isAllActivityCallbacksAvailable; private boolean firstActivityCreated = false; private boolean firstActivityResumed = false; private boolean foregroundImportance = false; - private final @NotNull FullyDrawnReporter fullyDrawnReporter; + private @Nullable FullyDisplayedReporter fullyDisplayedReporter = null; private @Nullable ISpan appStartSpan; private final @NotNull WeakHashMap ttidSpanMap = new WeakHashMap<>(); private @NotNull SentryDate lastPausedTime = AndroidDateUtils.getCurrentSentryDateTime(); private final @NotNull Handler mainHandler = new Handler(Looper.getMainLooper()); - private final @NotNull WeakHashMap ttfdSpanMap = new WeakHashMap<>(); + private @Nullable ISpan ttfdSpan = null; + private @Nullable Future ttfdAutoCloseFuture = null; // WeakHashMap isn't thread safe but ActivityLifecycleCallbacks is only called from the // main-thread @@ -79,21 +84,11 @@ public ActivityLifecycleIntegration( final @NotNull Application application, final @NotNull BuildInfoProvider buildInfoProvider, final @NotNull ActivityFramesTracker activityFramesTracker) { - this(application, buildInfoProvider, activityFramesTracker, FullyDrawnReporter.getInstance()); - } - - public ActivityLifecycleIntegration( - final @NotNull Application application, - final @NotNull BuildInfoProvider buildInfoProvider, - final @NotNull ActivityFramesTracker activityFramesTracker, - final @NotNull FullyDrawnReporter fullyDrawnReporter) { 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.fullyDrawnReporter = - Objects.requireNonNull(fullyDrawnReporter, "FullyDrawnReporter is required"); if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.Q) { isAllActivityCallbacksAvailable = true; @@ -121,6 +116,8 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio this.options.isEnableActivityLifecycleBreadcrumbs()); performanceEnabled = isPerformanceEnabled(this.options); + fullyDisplayedReporter = this.options.getFullyDrawnReporter(); + timeToFullDisplaySpanEnabled = this.options.isEnableTimeToFullDisplayTracing(); if (this.options.isEnableActivityLifecycleBreadcrumbs() || performanceEnabled) { application.registerActivityLifecycleCallbacks(this); @@ -168,8 +165,7 @@ private void stopPreviousTransactions() { activitiesWithOngoingTransactions.entrySet()) { final ITransaction transaction = entry.getValue(); final ISpan ttidSpan = ttidSpanMap.get(entry.getKey()); - final ISpan ttfdSpan = ttfdSpanMap.get(entry.getKey()); - finishTransaction(transaction, ttidSpan, ttfdSpan); + finishTransaction(transaction, ttidSpan); } } @@ -216,6 +212,8 @@ private void startTracing(final @NotNull Activity activity) { new TransactionContext(activityName, TransactionNameSource.COMPONENT, UI_LOAD_OP), transactionOptions); + final @NotNull SentryDate ttidStartTime; + // 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 @@ -225,23 +223,30 @@ private void startTracing(final @NotNull Activity activity) { getAppStartDesc(coldStart), appStartTime, Instrumenter.SENTRY); - // The first activity ttidSpan should start at the same time as the app start time - ttidSpanMap.put( - activity, - transaction.startChild( - TTID_OP, getTtidDesc(activityName), appStartTime, Instrumenter.SENTRY)); + // The first activity ttid/ttfd spans should start at the app start time + ttidStartTime = appStartTime; } else { - // Other activities (or in case appStartTime is not available) the ttid span should - // start when the previous activity called its onPause method. - ttidSpanMap.put( - activity, - transaction.startChild( - TTID_OP, getTtidDesc(activityName), lastPausedTime, Instrumenter.SENTRY)); + // The ttid/ttfd spans should start when the previous activity called its onPause method + ttidStartTime = lastPausedTime; } - ttfdSpanMap.put( + ttidSpanMap.put( activity, transaction.startChild( - TTFD_OP, getTtfdDesc(activityName), lastPausedTime, Instrumenter.SENTRY)); + TTID_OP, getTtidDesc(activityName), ttidStartTime, Instrumenter.SENTRY)); + + if (timeToFullDisplaySpanEnabled && fullyDisplayedReporter != null && options != null) { + ttfdSpan = + transaction.startChild( + TTFD_OP, getTtfdDesc(activityName), ttidStartTime, Instrumenter.SENTRY); + ttfdAutoCloseFuture = + options + .getExecutorService() + .schedule( + () -> { + finishSpan(ttfdSpan, SpanStatus.DEADLINE_EXCEEDED); + }, + TTFD_TIMEOUT_MILLIS); + } // lets bind to the scope so other integrations can pick it up hub.configureScope( @@ -289,14 +294,12 @@ private boolean isRunningTransaction(final @NotNull Activity activity) { private void stopTracing(final @NotNull Activity activity, final boolean shouldFinishTracing) { if (performanceEnabled && shouldFinishTracing) { final ITransaction transaction = activitiesWithOngoingTransactions.get(activity); - finishTransaction(transaction, null, null); + finishTransaction(transaction, null); } } private void finishTransaction( - final @Nullable ITransaction transaction, - final @Nullable ISpan ttidSpan, - final @Nullable ISpan ttfdSpan) { + final @Nullable ITransaction transaction, final @Nullable ISpan ttidSpan) { if (transaction != null) { // if io.sentry.traces.activity.auto-finish.enable is disabled, transaction may be already // finished manually when this method is called. @@ -305,8 +308,9 @@ private void finishTransaction( } // in case the ttidSpan isn't completed yet, we finish it as cancelled to avoid memory leak - finishSpan(ttidSpan, SpanStatus.CANCELLED); - finishSpan(ttfdSpan, SpanStatus.CANCELLED); + finishSpan(ttidSpan, SpanStatus.DEADLINE_EXCEEDED); + finishSpan(ttfdSpan, SpanStatus.DEADLINE_EXCEEDED); + cancelTtfdAutoClose(); SpanStatus status = transaction.getStatus(); // status might be set by other integrations, let's not overwrite it @@ -334,22 +338,13 @@ public synchronized void onActivityCreated( firstActivityCreated = true; - ISpan ttfdSpan = ttfdSpanMap.get(activity); - fullyDrawnReporter.registerFullyDrawnListener( - new FullyDrawnReporter.FullyDrawnReporterListener() { - @Override - public boolean onFullyDrawn(@NotNull final Activity reportedActivity) { - ISpan reportedTtfdSpan = ttfdSpanMap.get(reportedActivity); - // finishes ttfdSpan span - if (ttfdSpan == reportedTtfdSpan - && reportedTtfdSpan != null - && !ttfdSpan.isFinished()) { - ttfdSpan.finish(); - return true; - } - return false; - } - }); + if (fullyDisplayedReporter != null) { + fullyDisplayedReporter.registerFullyDrawnListener( + () -> { + finishSpan(ttfdSpan); + cancelTtfdAutoClose(); + }); + } } @Override @@ -465,11 +460,11 @@ public synchronized void onActivityDestroyed(final @NotNull Activity activity) { // we finish the ttidSpan as cancelled in case it isn't completed yet final ISpan ttidSpan = ttidSpanMap.get(activity); - finishSpan(ttidSpan, SpanStatus.CANCELLED); + finishSpan(ttidSpan, SpanStatus.DEADLINE_EXCEEDED); // we finish the ttfdSpan as cancelled in case it isn't completed yet - final ISpan ttfdSpan = ttfdSpanMap.get(activity); - finishSpan(ttfdSpan, SpanStatus.CANCELLED); + finishSpan(ttfdSpan, SpanStatus.DEADLINE_EXCEEDED); + cancelTtfdAutoClose(); // in case people opt-out enableActivityLifecycleTracingAutoFinish and forgot to finish it, // we make sure to finish it when the activity gets destroyed. @@ -478,7 +473,7 @@ public synchronized void onActivityDestroyed(final @NotNull Activity activity) { // set it to null in case its been just finished as cancelled appStartSpan = null; ttidSpanMap.remove(activity); - ttfdSpanMap.remove(activity); + ttfdSpan = null; // clear it up, so we don't start again for the same activity if the activity is in the activity // stack still. @@ -494,6 +489,13 @@ private void finishSpan(@Nullable ISpan span) { } } + private void cancelTtfdAutoClose() { + if (ttfdAutoCloseFuture != null) { + ttfdAutoCloseFuture.cancel(false); + ttfdAutoCloseFuture = null; + } + } + private void finishSpan(@Nullable ISpan span, @NotNull SpanStatus status) { if (span != null && !span.isFinished()) { span.finish(status); @@ -525,9 +527,9 @@ WeakHashMap getTtidSpanMap() { } @TestOnly - @NotNull - WeakHashMap getTtfdSpanMap() { - return ttfdSpanMap; + @Nullable + ISpan getTtfdSpan() { + return ttfdSpan; } private void setColdStart(final @Nullable Bundle savedInstanceState) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index 9ccad1c0e0..16465c8b78 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -16,7 +16,6 @@ import io.sentry.android.core.internal.gestures.AndroidViewGestureTargetLocator; import io.sentry.android.core.internal.modules.AssetsModulesLoader; import io.sentry.android.core.internal.util.AndroidMainThreadChecker; -import io.sentry.android.core.internal.util.FullyDrawnReporter; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.android.fragment.FragmentLifecycleIntegration; import io.sentry.android.timber.SentryTimberIntegration; @@ -107,8 +106,7 @@ static void initializeIntegrationsAndProcessors( new BuildInfoProvider(new AndroidLogger()), new LoadClass(), false, - false, - FullyDrawnReporter.getInstance()); + false); } static void initializeIntegrationsAndProcessors( @@ -117,8 +115,7 @@ static void initializeIntegrationsAndProcessors( final @NotNull BuildInfoProvider buildInfoProvider, final @NotNull LoadClass loadClass, final boolean isFragmentAvailable, - final boolean isTimberAvailable, - final @NotNull FullyDrawnReporter fullyDrawnReporter) { + final boolean isTimberAvailable) { if (options.getCacheDirPath() != null && options.getEnvelopeDiskCache() instanceof NoOpEnvelopeCache) { @@ -135,8 +132,7 @@ static void initializeIntegrationsAndProcessors( loadClass, activityFramesTracker, isFragmentAvailable, - isTimberAvailable, - fullyDrawnReporter); + isTimberAvailable); options.addEventProcessor( new DefaultAndroidEventProcessor(context, buildInfoProvider, options)); @@ -183,8 +179,7 @@ private static void installDefaultIntegrations( final @NotNull LoadClass loadClass, final @NotNull ActivityFramesTracker activityFramesTracker, final boolean isFragmentAvailable, - final boolean isTimberAvailable, - final @NotNull FullyDrawnReporter fullyDrawnReporter) { + final boolean isTimberAvailable) { // read the startup crash marker here to avoid doing double-IO for the SendCachedEnvelope // integrations below @@ -224,7 +219,7 @@ private static void installDefaultIntegrations( if (context instanceof Application) { options.addIntegration( new ActivityLifecycleIntegration( - (Application) context, buildInfoProvider, activityFramesTracker, fullyDrawnReporter)); + (Application) context, buildInfoProvider, activityFramesTracker)); options.addIntegration(new CurrentActivityIntegration((Application) context)); options.addIntegration(new UserInteractionIntegration((Application) context, loadClass)); if (isFragmentAvailable) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java index 60bd3e338e..5d14718862 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java @@ -60,6 +60,8 @@ final class ManifestMetadataReader { "io.sentry.traces.activity.auto-finish.enable"; static final String TRACES_UI_ENABLE = "io.sentry.traces.user-interaction.enable"; + static final String TTFD_ENABLE = "io.sentry.traces.time-to-full-display.enable"; + static final String TRACES_PROFILING_ENABLE = "io.sentry.traces.profiling.enable"; static final String PROFILES_SAMPLE_RATE = "io.sentry.traces.profiling.sample-rate"; @@ -271,6 +273,9 @@ static void applyMetadata( options.setEnableUserInteractionTracing( readBool(metadata, logger, TRACES_UI_ENABLE, options.isEnableUserInteractionTracing())); + options.setEnableTimeToFullDisplayTracing( + readBool(metadata, logger, TTFD_ENABLE, options.isEnableTimeToFullDisplayTracing())); + final long idleTimeout = readLong(metadata, logger, IDLE_TIMEOUT, -1); if (idleTimeout != -1) { options.setIdleTimeout(idleTimeout); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java index 266e7f7662..820d0c51a1 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java @@ -1,6 +1,5 @@ package io.sentry.android.core; -import android.app.Activity; import android.content.Context; import android.os.SystemClock; import io.sentry.IHub; @@ -12,7 +11,6 @@ import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.android.core.internal.util.BreadcrumbFactory; -import io.sentry.android.core.internal.util.FullyDrawnReporter; import io.sentry.android.fragment.FragmentLifecycleIntegration; import io.sentry.android.timber.SentryTimberIntegration; import java.lang.reflect.InvocationTargetException; @@ -39,9 +37,6 @@ public final class SentryAndroid { private static final String FRAGMENT_CLASS_NAME = "androidx.fragment.app.FragmentManager$FragmentLifecycleCallbacks"; - private static final @NotNull FullyDrawnReporter fullyDrawnReporter = - FullyDrawnReporter.getInstance(); - private SentryAndroid() {} /** @@ -121,8 +116,7 @@ public static synchronized void init( buildInfoProvider, loadClass, isFragmentAvailable, - isTimberAvailable, - fullyDrawnReporter); + isTimberAvailable); deduplicateIntegrations(options, isFragmentAvailable, isTimberAvailable); }, @@ -196,8 +190,4 @@ private static void deduplicateIntegrations( } } } - - public static void reportFullyDrawn(@NotNull Activity activity) { - fullyDrawnReporter.reportFullyDrawn(activity); - } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FullyDrawnReporter.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FullyDrawnReporter.java deleted file mode 100644 index 78a3317ff6..0000000000 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FullyDrawnReporter.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.sentry.android.core.internal.util; - -import android.app.Activity; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -@ApiStatus.Internal -public final class FullyDrawnReporter { - - private static final @NotNull FullyDrawnReporter instance = new FullyDrawnReporter(); - private final @NotNull Map listeners = - new ConcurrentHashMap<>(); - - private FullyDrawnReporter() {} - - public static @NotNull FullyDrawnReporter getInstance() { - return instance; - } - - public void registerFullyDrawnListener(@NotNull final FullyDrawnReporterListener listener) { - listeners.put(listener.uuid, listener); - } - - public void reportFullyDrawn(@NotNull final Activity reportedActivity) { - for (FullyDrawnReporterListener listener : listeners.values()) { - if (listener.onFullyDrawn(reportedActivity)) { - listeners.remove(listener.uuid); - } - } - } - - @ApiStatus.Internal - public abstract static class FullyDrawnReporterListener { - @NotNull final String uuid = UUID.randomUUID().toString(); - - public abstract boolean onFullyDrawn(@NotNull final Activity reportedActivity); - } -} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt index e6c027ca10..01e3788eaf 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt @@ -6,7 +6,9 @@ import android.app.ActivityManager.RunningAppProcessInfo import android.app.Application import android.os.Bundle import io.sentry.Breadcrumb +import io.sentry.FullyDisplayedReporter import io.sentry.Hub +import io.sentry.ISentryExecutorService import io.sentry.Scope import io.sentry.SentryDate import io.sentry.SentryLevel @@ -18,8 +20,8 @@ import io.sentry.TraceContext import io.sentry.TransactionContext import io.sentry.TransactionFinishedCallback import io.sentry.TransactionOptions -import io.sentry.android.core.internal.util.FullyDrawnReporter import io.sentry.protocol.TransactionNameSource +import io.sentry.test.getProperty import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.check @@ -29,6 +31,9 @@ import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import java.util.Date +import java.util.concurrent.Callable +import java.util.concurrent.Future +import java.util.concurrent.FutureTask import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals @@ -51,7 +56,7 @@ class ActivityLifecycleIntegrationTest { val bundle = mock() val context = TransactionContext("name", "op") val activityFramesTracker = mock() - val fullyDrawnReporter = FullyDrawnReporter.getInstance() + val fullyDisplayedReporter = FullyDisplayedReporter.getInstance() val transactionFinishedCallback = mock() lateinit var transaction: SentryTracer val buildInfo = mock() @@ -71,7 +76,7 @@ class ActivityLifecycleIntegrationTest { whenever(am.runningAppProcesses).thenReturn(processes) - return ActivityLifecycleIntegration(application, buildInfo, activityFramesTracker, fullyDrawnReporter) + return ActivityLifecycleIntegration(application, buildInfo, activityFramesTracker) } } @@ -371,12 +376,13 @@ class ActivityLifecycleIntegrationTest { fun `When tracing auto finish is enabled and ttid and ttfd spans are finished, it stops the transaction on onActivityPostResumed`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 + fixture.options.isEnableTimeToFullDisplayTracing = true sut.register(fixture.hub, fixture.options) val activity = mock() sut.onActivityCreated(activity, fixture.bundle) sut.ttidSpanMap.values.first().finish() - sut.ttfdSpanMap.values.first().finish() + sut.ttfdSpan?.finish() sut.onActivityPostResumed(activity) verify(fixture.hub).captureTransaction( @@ -529,7 +535,7 @@ class ActivityLifecycleIntegrationTest { } @Test - fun `When Activity is destroyed, sets ttidSpan status to cancelled and finish it`() { + fun `When Activity is destroyed, sets ttidSpan status to deadline_exceeded and finish it`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) @@ -541,7 +547,7 @@ class ActivityLifecycleIntegrationTest { sut.onActivityDestroyed(activity) val span = fixture.transaction.children.first { it.operation == ActivityLifecycleIntegration.TTID_OP } - assertEquals(SpanStatus.CANCELLED, span.status) + assertEquals(SpanStatus.DEADLINE_EXCEEDED, span.status) assertTrue(span.isFinished) } @@ -562,9 +568,10 @@ class ActivityLifecycleIntegrationTest { } @Test - fun `When Activity is destroyed, sets ttfdSpan status to cancelled and finish it`() { + fun `When Activity is destroyed, sets ttfdSpan status to deadline_exceeded and finish it`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 + fixture.options.isEnableTimeToFullDisplayTracing = true sut.register(fixture.hub, fixture.options) setAppStartTime() @@ -574,7 +581,7 @@ class ActivityLifecycleIntegrationTest { sut.onActivityDestroyed(activity) val span = fixture.transaction.children.first { it.operation == ActivityLifecycleIntegration.TTFD_OP } - assertEquals(SpanStatus.CANCELLED, span.status) + assertEquals(SpanStatus.DEADLINE_EXCEEDED, span.status) assertTrue(span.isFinished) } @@ -582,16 +589,17 @@ class ActivityLifecycleIntegrationTest { fun `When Activity is destroyed, sets ttfdSpan to null`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 + fixture.options.isEnableTimeToFullDisplayTracing = true sut.register(fixture.hub, fixture.options) setAppStartTime() val activity = mock() sut.onActivityCreated(activity, fixture.bundle) - assertNotNull(sut.ttfdSpanMap[activity]) + assertNotNull(sut.ttfdSpan) sut.onActivityDestroyed(activity) - assertNull(sut.ttfdSpanMap[activity]) + assertNull(sut.ttfdSpan) } @Test @@ -637,12 +645,13 @@ class ActivityLifecycleIntegrationTest { fun `stop transaction on resumed if API 29 less than 29 and ttid and ttfd are finished`() { val sut = fixture.getSut(14) fixture.options.tracesSampleRate = 1.0 + fixture.options.isEnableTimeToFullDisplayTracing = true sut.register(fixture.hub, fixture.options) val activity = mock() sut.onActivityCreated(activity, mock()) sut.ttidSpanMap.values.first().finish() - sut.ttfdSpanMap.values.first().finish() + sut.ttfdSpan?.finish() sut.onActivityResumed(activity) verify(fixture.hub).captureTransaction(any(), anyOrNull(), anyOrNull(), anyOrNull()) @@ -652,14 +661,15 @@ class ActivityLifecycleIntegrationTest { fun `reportFullyDrawn finishes the ttfd`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 + fixture.options.isEnableTimeToFullDisplayTracing = true sut.register(fixture.hub, fixture.options) val activity = mock() sut.onActivityCreated(activity, mock()) sut.ttidSpanMap.values.first().finish() - fixture.fullyDrawnReporter.reportFullyDrawn(activity) - assertTrue(sut.ttfdSpanMap.values.first().isFinished) - assertNotEquals(SpanStatus.CANCELLED, sut.ttfdSpanMap.values.first().status) + fixture.fullyDisplayedReporter.reportFullyDrawn() + assertTrue(sut.ttfdSpan!!.isFinished) + assertNotEquals(SpanStatus.CANCELLED, sut.ttfdSpan?.status) } @Test @@ -882,6 +892,121 @@ class ActivityLifecycleIntegrationTest { sut.onActivityDestroyed(activity) } + @Test + fun `When transaction is started and isEnableTimeToFullDisplayTracing is disabled, no ttfd span is started`() { + val sut = fixture.getSut() + fixture.options.tracesSampleRate = 1.0 + fixture.options.isEnableTimeToFullDisplayTracing = false + sut.register(fixture.hub, fixture.options) + + val activity = mock() + sut.onActivityCreated(activity, fixture.bundle) + assertNull(sut.ttfdSpan) + } + + @Test + fun `When transaction is started and isEnableTimeToFullDisplayTracing is enabled, ttfd span is started`() { + val sut = fixture.getSut() + fixture.options.tracesSampleRate = 1.0 + fixture.options.isEnableTimeToFullDisplayTracing = true + sut.register(fixture.hub, fixture.options) + + val activity = mock() + sut.onActivityCreated(activity, fixture.bundle) + assertNotNull(sut.ttfdSpan) + } + + @Test + fun `When isEnableTimeToFullDisplayTracing is true and reportFullyDrawn is not called, ttfd span is finished automatically with timeout`() { + val sut = fixture.getSut() + var lastScheduledRunnable: Runnable? = null + val mockExecutorService = object : ISentryExecutorService { + override fun submit(runnable: Runnable): Future<*> = mock() + override fun submit(callable: Callable): Future = mock() + override fun schedule(runnable: Runnable, delayMillis: Long): Future<*> { + lastScheduledRunnable = runnable + return FutureTask {} + } + override fun close(timeoutMillis: Long) {} + } + fixture.options.tracesSampleRate = 1.0 + fixture.options.isEnableTimeToFullDisplayTracing = true + fixture.options.executorService = mockExecutorService + sut.register(fixture.hub, fixture.options) + val activity = mock() + sut.onActivityCreated(activity, fixture.bundle) + val ttfdSpan = sut.ttfdSpan + + // Assert the ttfd span is running and a timeout autoCancel task has been scheduled + assertNotNull(ttfdSpan) + assertFalse(ttfdSpan.isFinished) + assertNotNull(lastScheduledRunnable) + + // Run the autoClose task and assert the ttfd span is finished with deadlineExceeded + lastScheduledRunnable!!.run() + assertTrue(ttfdSpan.isFinished) + assertEquals(SpanStatus.DEADLINE_EXCEEDED, ttfdSpan.status) + } + + @Test + fun `When isEnableTimeToFullDisplayTracing is true and reportFullyDrawn is called, ttfd autoClose future is cancelled`() { + val sut = fixture.getSut() + fixture.options.tracesSampleRate = 1.0 + fixture.options.isEnableTimeToFullDisplayTracing = true + sut.register(fixture.hub, fixture.options) + val activity = mock() + sut.onActivityCreated(activity, fixture.bundle) + val ttfdSpan = sut.ttfdSpan + var autoCloseFuture = sut.getProperty?>("ttfdAutoCloseFuture") + + // Assert the ttfd span is running and a timeout autoCancel future has been scheduled + assertNotNull(ttfdSpan) + assertFalse(ttfdSpan.isFinished) + assertNotNull(autoCloseFuture) + + // ReportFullyDrawn should finish the ttfd span and cancel the future + fixture.options.fullyDrawnReporter.reportFullyDrawn() + assertTrue(ttfdSpan.isFinished) + assertNotEquals(SpanStatus.DEADLINE_EXCEEDED, ttfdSpan.status) + assertTrue(autoCloseFuture.isCancelled) + + // The current internal reference to autoClose future should be null after ReportFullyDrawn + autoCloseFuture = sut.getProperty?>("ttfdAutoCloseFuture") + assertNull(autoCloseFuture) + } + + @Test + fun `When isEnableTimeToFullDisplayTracing is true and another activity starts, the old ttfd is finished and the old autoClose future is cancelled`() { + val sut = fixture.getSut() + fixture.options.tracesSampleRate = 1.0 + fixture.options.isEnableTimeToFullDisplayTracing = true + sut.register(fixture.hub, fixture.options) + val activity = mock() + val activity2 = mock() + sut.onActivityCreated(activity, fixture.bundle) + val ttfdSpan = sut.ttfdSpan + val autoCloseFuture = sut.getProperty?>("ttfdAutoCloseFuture") + + // Assert the ttfd span is running and a timeout autoCancel future has been scheduled + assertNotNull(ttfdSpan) + assertFalse(ttfdSpan.isFinished) + assertNotNull(autoCloseFuture) + + // Starting a new Activity should finish the old ttfd span with deadlineExceeded and cancel the old future + sut.onActivityCreated(activity2, fixture.bundle) + assertTrue(ttfdSpan.isFinished) + assertEquals(SpanStatus.DEADLINE_EXCEEDED, ttfdSpan.status) + assertTrue(autoCloseFuture.isCancelled) + + // Another autoClose future and ttfd span should be started after the second activity starts + val autoCloseFuture2 = sut.getProperty?>("ttfdAutoCloseFuture") + val ttfdSpan2 = sut.ttfdSpan + assertNotNull(ttfdSpan2) + assertFalse(ttfdSpan2.isFinished) + assertNotNull(autoCloseFuture2) + assertFalse(autoCloseFuture2.isCancelled) + } + private fun setAppStartTime(date: SentryDate = SentryNanotimeDate(Date(0), 0)) { // set by SentryPerformanceProvider so forcing it here AppStartState.getInstance().setAppStartTime(0, date) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index cabf13f385..114646985a 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -12,7 +12,6 @@ import io.sentry.android.core.cache.AndroidEnvelopeCache import io.sentry.android.core.internal.gestures.AndroidViewGestureTargetLocator import io.sentry.android.core.internal.modules.AssetsModulesLoader import io.sentry.android.core.internal.util.AndroidMainThreadChecker -import io.sentry.android.core.internal.util.FullyDrawnReporter import io.sentry.android.fragment.FragmentLifecycleIntegration import io.sentry.android.timber.SentryTimberIntegration import io.sentry.compose.gestures.ComposeGestureTargetLocator @@ -38,7 +37,6 @@ class AndroidOptionsInitializerTest { val sentryOptions = SentryAndroidOptions() lateinit var mockContext: Context val logger = mock() - val mockFullyDrawnReporter = mock() fun initSut( metadata: Bundle? = null, @@ -98,8 +96,7 @@ class AndroidOptionsInitializerTest { buildInfo, createClassMock(classesToLoad), isFragmentAvailable, - isTimberAvailable, - mockFullyDrawnReporter + isTimberAvailable ) } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt index 0704c20293..ab5bc2fb29 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt @@ -14,7 +14,6 @@ import io.sentry.ProfilingTraceData import io.sentry.SentryLevel import io.sentry.SentryTracer import io.sentry.TransactionContext -import io.sentry.android.core.internal.util.FullyDrawnReporter import io.sentry.android.core.internal.util.SentryFrameMetricsCollector import io.sentry.profilemeasurements.ProfileMeasurement import io.sentry.test.getCtor @@ -54,7 +53,6 @@ class AndroidTransactionProfilerTest { whenever(it.sdkInfoVersion).thenReturn(Build.VERSION_CODES.LOLLIPOP) } val mockLogger = mock() - val mockFullyDrawnReporter = mock() var lastScheduledRunnable: Runnable? = null val mockExecutorService = object : ISentryExecutorService { override fun submit(runnable: Runnable): Future<*> { @@ -119,8 +117,7 @@ class AndroidTransactionProfilerTest { buildInfoProvider, LoadClass(), false, - false, - fixture.mockFullyDrawnReporter + false ) // Profiler doesn't start if the folder doesn't exists. // Usually it's generated when calling Sentry.init, but for tests we can create it manually. diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt index a99bee37d6..43126c2635 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt @@ -1137,4 +1137,30 @@ class ManifestMetadataReaderTest { // Assert assertFalse(fixture.options.isEnableFramesTracking) } + + @Test + fun `applyMetadata reads time-to-full-display tracking and sets it to enabled if true`() { + // Arrange + val bundle = bundleOf(ManifestMetadataReader.TTFD_ENABLE to true) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertTrue(fixture.options.isEnableTimeToFullDisplayTracing) + } + + @Test + fun `applyMetadata reads time-to-full-display tracking and sets it to disabled if false`() { + // Arrange + val bundle = bundleOf(ManifestMetadataReader.TTFD_ENABLE to false) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertFalse(fixture.options.isEnableTimeToFullDisplayTracing) + } } diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml index ebe16ed7b5..8842c5efa5 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -114,6 +114,9 @@ + + + diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java index 135cc009e4..6f77911d1c 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java @@ -8,7 +8,6 @@ import io.sentry.MeasurementUnit; import io.sentry.Sentry; import io.sentry.UserFeedback; -import io.sentry.android.core.SentryAndroid; import io.sentry.instrumentation.file.SentryFileOutputStream; import io.sentry.protocol.SentryId; import io.sentry.protocol.User; @@ -198,19 +197,7 @@ protected void onCreate(Bundle savedInstanceState) { }); setContentView(binding.getRoot()); - - // Let's say the activity is fully drawn after 1 second - new Thread( - () -> { - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - SentryAndroid.reportFullyDrawn(this); - reportFullyDrawn(); - }) - .start(); + Sentry.reportFullDisplayed(); } @Override diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/PermissionsActivity.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/PermissionsActivity.kt index dce7af4771..816516b7a6 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/PermissionsActivity.kt +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/PermissionsActivity.kt @@ -5,6 +5,7 @@ import android.os.Bundle import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts.RequestPermission import androidx.appcompat.app.AppCompatActivity +import io.sentry.Sentry import io.sentry.samples.android.databinding.ActivityPermissionsBinding class PermissionsActivity : AppCompatActivity() { @@ -37,5 +38,6 @@ class PermissionsActivity : AppCompatActivity() { } setContentView(binding.root) + Sentry.reportFullDisplayed() } } diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/ProfilingActivity.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/ProfilingActivity.kt index 1ec37bee4b..3ce39219dc 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/ProfilingActivity.kt +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/ProfilingActivity.kt @@ -69,6 +69,7 @@ class ProfilingActivity : AppCompatActivity() { }.start() } setContentView(binding.root) + Sentry.reportFullDisplayed() } private fun finishTransactionAndPrintResults(t: ITransaction) { diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SecondActivity.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SecondActivity.kt index 5505032ba7..8c79e7e827 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SecondActivity.kt +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SecondActivity.kt @@ -6,7 +6,6 @@ import android.view.View import androidx.appcompat.app.AppCompatActivity import io.sentry.Sentry import io.sentry.SpanStatus -import io.sentry.android.core.SentryAndroid import io.sentry.samples.android.databinding.ActivitySecondBinding import retrofit2.Call import retrofit2.Callback @@ -77,7 +76,7 @@ class SecondActivity : AppCompatActivity() { showText(true, "error: ${t.message}") - SentryAndroid.reportFullyDrawn(this@SecondActivity) + Sentry.reportFullDisplayed() // I opt out enableActivityLifecycleTracingAutoFinish so I know best when to end my transaction // be sure to finish all your spans before this val transaction = Sentry.getSpan() @@ -91,7 +90,7 @@ class SecondActivity : AppCompatActivity() { showText(text = "items: ${repos.size}") - SentryAndroid.reportFullyDrawn(this@SecondActivity) + Sentry.reportFullDisplayed() // I opt out enableActivityLifecycleTracingAutoFinish so I know best when to end my transaction // be sure to finish all your spans before this val transaction = Sentry.getSpan() diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 8692d59010..1a5f16c7dd 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -275,6 +275,16 @@ public final class io/sentry/ExternalOptions { public fun setTracesSampleRate (Ljava/lang/Double;)V } +public final class io/sentry/FullyDisplayedReporter { + public static fun getInstance ()Lio/sentry/FullyDisplayedReporter; + public fun registerFullyDrawnListener (Lio/sentry/FullyDisplayedReporter$FullyDrawnReporterListener;)V + public fun reportFullyDrawn ()V +} + +public abstract interface class io/sentry/FullyDisplayedReporter$FullyDrawnReporterListener { + public abstract fun onFullyDrawn ()V +} + public final class io/sentry/Hint { public fun ()V public fun addAttachment (Lio/sentry/Attachment;)V @@ -332,6 +342,7 @@ public final class io/sentry/Hub : io/sentry/IHub { public fun pushScope ()V public fun removeExtra (Ljava/lang/String;)V public fun removeTag (Ljava/lang/String;)V + public fun reportFullyDrawn ()V public fun setExtra (Ljava/lang/String;Ljava/lang/String;)V public fun setFingerprint (Ljava/util/List;)V public fun setLevel (Lio/sentry/SentryLevel;)V @@ -375,6 +386,7 @@ public final class io/sentry/HubAdapter : io/sentry/IHub { public fun pushScope ()V public fun removeExtra (Ljava/lang/String;)V public fun removeTag (Ljava/lang/String;)V + public fun reportFullyDrawn ()V public fun setExtra (Ljava/lang/String;Ljava/lang/String;)V public fun setFingerprint (Ljava/util/List;)V public fun setLevel (Lio/sentry/SentryLevel;)V @@ -443,6 +455,7 @@ public abstract interface class io/sentry/IHub { public abstract fun pushScope ()V public abstract fun removeExtra (Ljava/lang/String;)V public abstract fun removeTag (Ljava/lang/String;)V + public abstract fun reportFullyDrawn ()V public abstract fun setExtra (Ljava/lang/String;Ljava/lang/String;)V public abstract fun setFingerprint (Ljava/util/List;)V public abstract fun setLevel (Lio/sentry/SentryLevel;)V @@ -763,6 +776,7 @@ public final class io/sentry/NoOpHub : io/sentry/IHub { public fun pushScope ()V public fun removeExtra (Ljava/lang/String;)V public fun removeTag (Ljava/lang/String;)V + public fun reportFullyDrawn ()V public fun setExtra (Ljava/lang/String;Ljava/lang/String;)V public fun setFingerprint (Ljava/util/List;)V public fun setLevel (Lio/sentry/SentryLevel;)V @@ -1157,6 +1171,7 @@ public final class io/sentry/Sentry { public static fun pushScope ()V public static fun removeExtra (Ljava/lang/String;)V public static fun removeTag (Ljava/lang/String;)V + public static fun reportFullDisplayed ()V public static fun setCurrentHub (Lio/sentry/IHub;)V public static fun setExtra (Ljava/lang/String;Ljava/lang/String;)V public static fun setFingerprint (Ljava/util/List;)V @@ -1510,6 +1525,7 @@ public class io/sentry/SentryOptions { public fun getEventProcessors ()Ljava/util/List; public fun getExecutorService ()Lio/sentry/ISentryExecutorService; public fun getFlushTimeoutMillis ()J + public fun getFullyDrawnReporter ()Lio/sentry/FullyDisplayedReporter; public fun getGestureTargetLocators ()Ljava/util/List; public fun getHostnameVerifier ()Ljavax/net/ssl/HostnameVerifier; public fun getIdleTimeout ()Ljava/lang/Long; @@ -1565,6 +1581,7 @@ public class io/sentry/SentryOptions { public fun isEnableNdk ()Z public fun isEnableScopeSync ()Z public fun isEnableShutdownHook ()Z + public fun isEnableTimeToFullDisplayTracing ()Z public fun isEnableUncaughtExceptionHandler ()Z public fun isEnableUserInteractionBreadcrumbs ()Z public fun isEnableUserInteractionTracing ()Z @@ -1596,6 +1613,7 @@ public class io/sentry/SentryOptions { public fun setEnableNdk (Z)V public fun setEnableScopeSync (Z)V public fun setEnableShutdownHook (Z)V + public fun setEnableTimeToFullDisplayTracing (Z)V public fun setEnableUncaughtExceptionHandler (Z)V public fun setEnableUserInteractionBreadcrumbs (Z)V public fun setEnableUserInteractionTracing (Z)V diff --git a/sentry/src/main/java/io/sentry/FullyDisplayedReporter.java b/sentry/src/main/java/io/sentry/FullyDisplayedReporter.java new file mode 100644 index 0000000000..6896cb6643 --- /dev/null +++ b/sentry/src/main/java/io/sentry/FullyDisplayedReporter.java @@ -0,0 +1,42 @@ +package io.sentry; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public final class FullyDisplayedReporter { + + private static final @NotNull FullyDisplayedReporter instance = new FullyDisplayedReporter(); + + private final @NotNull List listeners = new ArrayList<>(); + + private FullyDisplayedReporter() {} + + public static @NotNull FullyDisplayedReporter getInstance() { + return instance; + } + + public void registerFullyDrawnListener(final @NotNull FullyDrawnReporterListener listener) { + synchronized (listeners) { + listeners.add(listener); + } + } + + public void reportFullyDrawn() { + synchronized (listeners) { + final @NotNull Iterator listenerIterator = listeners.iterator(); + while (listenerIterator.hasNext()) { + listenerIterator.next().onFullyDrawn(); + listenerIterator.remove(); + } + } + } + + @ApiStatus.Internal + public interface FullyDrawnReporterListener { + void onFullyDrawn(); + } +} diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index bcfa9b87f7..d88dc112ae 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -514,6 +514,13 @@ public void pushScope() { .isCrashedLastRun(options.getCacheDirPath(), !options.isEnableAutoSessionTracking()); } + @Override + public void reportFullyDrawn() { + if (options.isEnableTimeToFullDisplayTracing()) { + options.getFullyDrawnReporter().reportFullyDrawn(); + } + } + @Override public void popScope() { if (!isEnabled()) { diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index 0a7756863a..eceafcb13b 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -229,4 +229,9 @@ public void setSpanContext( public @Nullable Boolean isCrashedLastRun() { return Sentry.isCrashedLastRun(); } + + @Override + public void reportFullyDrawn() { + Sentry.reportFullDisplayed(); + } } diff --git a/sentry/src/main/java/io/sentry/IHub.java b/sentry/src/main/java/io/sentry/IHub.java index 01d8546d84..2bfb5e62c0 100644 --- a/sentry/src/main/java/io/sentry/IHub.java +++ b/sentry/src/main/java/io/sentry/IHub.java @@ -563,4 +563,14 @@ void setSpanContext( */ @Nullable Boolean isCrashedLastRun(); + + /** + * Report a screen has been fully loaded. That means all data needed by the UI was loaded. If + * time-to-full-display tracing {{@link SentryOptions#isEnableTimeToFullDisplayTracing()} } is + * disabled this call is ignored. + * + *

This method is safe to be called multiple times. If the time-to-full-display span is already + * finished, this call will be ignored. + */ + void reportFullyDrawn(); } diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index 52bef0ff43..7c5474fbf7 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -186,4 +186,7 @@ public void setSpanContext( public @Nullable Boolean isCrashedLastRun() { return null; } + + @Override + public void reportFullyDrawn() {} } diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 4abcba8b1c..13580cc402 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -844,6 +844,18 @@ public static void endSession() { return getCurrentHub().isCrashedLastRun(); } + /** + * Report a screen has been fully loaded. That means all data needed by the UI was loaded. If + * time-to-full-display tracing {{@link SentryOptions#isEnableTimeToFullDisplayTracing()} } is + * disabled this call is ignored. + * + *

This method is safe to be called multiple times. If the time-to-full-display span is already + * finished, this call will be ignored. + */ + public static void reportFullDisplayed() { + getCurrentHub().reportFullyDrawn(); + } + /** * Configuration options callback * diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 2ce67f06b3..9770c3af5e 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -399,6 +399,13 @@ public class SentryOptions { private @NotNull TransactionPerformanceCollector transactionPerformanceCollector = NoOpTransactionPerformanceCollector.getInstance(); + /** Enables the time-to-full-display spans in automatic ui transactions. */ + private boolean enableTimeToFullDisplayTracing = false; + + /** Screen fully displayed reporter, used for time-to-full-display spans. */ + private final @NotNull FullyDisplayedReporter fullyDisplayedReporter = + FullyDisplayedReporter.getInstance(); + /** * Adds an event processor * @@ -1911,6 +1918,34 @@ public void setTransactionPerformanceCollector( this.transactionPerformanceCollector = transactionPerformanceCollector; } + /** + * Gets if the time-to-full-display spans is tracked in automatic ui transactions. + * + * @return if the time-to-full-display is tracked. + */ + public boolean isEnableTimeToFullDisplayTracing() { + return enableTimeToFullDisplayTracing; + } + + /** + * Sets if the time-to-full-display spans should be tracked in automatic ui transactions. + * + * @param enableTimeToFullDisplayTracing if the time-to-full-display spans should be tracked. + */ + public void setEnableTimeToFullDisplayTracing(final boolean enableTimeToFullDisplayTracing) { + this.enableTimeToFullDisplayTracing = enableTimeToFullDisplayTracing; + } + + /** + * Gets the reporter to call when a screen is fully loaded, used for time-to-full-display spans. + * + * @return The reporter to call when a screen is fully loaded. + */ + @ApiStatus.Internal + public @NotNull FullyDisplayedReporter getFullyDrawnReporter() { + return fullyDisplayedReporter; + } + /** * Whether OPTIONS requests should be traced. * diff --git a/sentry/src/test/java/io/sentry/FullyDisplayedReporterTest.kt b/sentry/src/test/java/io/sentry/FullyDisplayedReporterTest.kt new file mode 100644 index 0000000000..85b309e4f2 --- /dev/null +++ b/sentry/src/test/java/io/sentry/FullyDisplayedReporterTest.kt @@ -0,0 +1,49 @@ +package io.sentry + +import io.sentry.FullyDisplayedReporter.FullyDrawnReporterListener +import io.sentry.test.getProperty +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class FullyDisplayedReporterTest { + + private val reporter = FullyDisplayedReporter.getInstance() + private val listeners = reporter.getProperty>("listeners") + private val listener1 = FullyDrawnReporterListener {} + private val listener2 = FullyDrawnReporterListener {} + private val mockListener1 = mock() + private val mockListener2 = mock() + + @AfterTest + fun shutdown() { + listeners.clear() + } + + @Test + fun `reporter can register multiple listeners`() { + reporter.registerFullyDrawnListener(mock()) + reporter.registerFullyDrawnListener(mock()) + assertEquals(2, listeners.size) + } + + @Test + fun `reportFullyDrawn calls all registered listeners`() { + reporter.registerFullyDrawnListener(mockListener1) + reporter.registerFullyDrawnListener(mockListener2) + reporter.reportFullyDrawn() + verify(mockListener1).onFullyDrawn() + verify(mockListener2).onFullyDrawn() + } + + @Test + fun `reportFullyDrawn removes current listeners`() { + reporter.registerFullyDrawnListener(listener1) + reporter.registerFullyDrawnListener(listener2) + reporter.reportFullyDrawn() + assertTrue(listeners.isEmpty()) + } +} diff --git a/sentry/src/test/java/io/sentry/HubTest.kt b/sentry/src/test/java/io/sentry/HubTest.kt index 774ca69297..75acf1fe53 100644 --- a/sentry/src/test/java/io/sentry/HubTest.kt +++ b/sentry/src/test/java/io/sentry/HubTest.kt @@ -1647,6 +1647,49 @@ class HubTest { assertFalse(nativeMarker.exists()) } + @Test + fun `reportFullyDrawn is ignored if TimeToFullDisplayTracing is disabled`() { + var called = false + val hub = generateHub { + it.fullyDrawnReporter.registerFullyDrawnListener { + called = !called + true + } + } + hub.reportFullyDrawn() + assertFalse(called) + } + + @Test + fun `reportFullyDrawn calls FullyDrawnReporter if TimeToFullDisplayTracing is enabled`() { + var called = false + val hub = generateHub { + it.isEnableTimeToFullDisplayTracing = true + it.fullyDrawnReporter.registerFullyDrawnListener { + called = !called + true + } + } + hub.reportFullyDrawn() + assertTrue(called) + } + + @Test + fun `reportFullyDrawn calls FullyDrawnReporter only once`() { + var called = false + val hub = generateHub { + it.isEnableTimeToFullDisplayTracing = true + it.fullyDrawnReporter.registerFullyDrawnListener { + called = !called + true + } + } + hub.reportFullyDrawn() + assertTrue(called) + hub.reportFullyDrawn() + assertTrue(called) + } + private val dsnTest = "https://key@sentry.io/proj" private fun generateHub(optionsConfiguration: Sentry.OptionsConfiguration? = null): IHub { diff --git a/sentry/src/test/java/io/sentry/NoOpHubTest.kt b/sentry/src/test/java/io/sentry/NoOpHubTest.kt index c430206048..d48310b663 100644 --- a/sentry/src/test/java/io/sentry/NoOpHubTest.kt +++ b/sentry/src/test/java/io/sentry/NoOpHubTest.kt @@ -81,4 +81,7 @@ class NoOpHubTest { @Test fun `setSpanContext doesnt throw`() = sut.setSpanContext(RuntimeException(), mock(), "") + + @Test + fun `reportFullyDrawn doesnt throw`() = sut.reportFullyDrawn() } diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 32a2420a78..3b62a0955b 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -405,4 +405,14 @@ class SentryOptionsTest { options.transactionPerformanceCollector = performanceCollector assertEquals(performanceCollector, options.transactionPerformanceCollector) } + + @Test + fun `when options are initialized, TimeToFullDisplayTracing is false`() { + assertFalse(SentryOptions().isEnableTimeToFullDisplayTracing) + } + + @Test + fun `when options are initialized, FullyDrawnReporter is set`() { + assertEquals(FullyDisplayedReporter.getInstance(), SentryOptions().fullyDrawnReporter) + } } From a4b66432342e45511b93187738e36e2891fd38bf Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Thu, 9 Feb 2023 18:07:38 +0100 Subject: [PATCH 13/17] added Sentry.reportFullyDrawn() API added FullyDisplayedReporter and put it in the options added ttfd span in ActivityLifecycleIntegration ttid and ttfd spans are now finished with DEADLINE_EXCEEDED instead of CANCELLED when new activity is started or activity is destroyed added io.sentry.traces.time-to-full-display.enable manifest option and enableTimeToFullDisplayTracing option, disabled by default --- .../sentry/android/core/ActivityLifecycleIntegration.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index 27a75f25b7..6d9bdc6d27 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -239,14 +239,14 @@ private void startTracing(final @NotNull Activity activity) { TTID_OP, getTtidDesc(activityName), ttidStartTime, Instrumenter.SENTRY)); if (timeToFullDisplaySpanEnabled && fullyDisplayedReporter != null && options != null) { - ttfdSpan = transaction.startChild( - TTFD_OP, getTtfdDesc(activityName), ttidStartTime, Instrumenter.SENTRY); + ttfdSpan = + transaction.startChild( + TTFD_OP, getTtfdDesc(activityName), ttidStartTime, Instrumenter.SENTRY); ttfdAutoCloseFuture = options .getExecutorService() .schedule( - () -> finishSpan(ttfdSpan, SpanStatus.DEADLINE_EXCEEDED), - TTFD_TIMEOUT_MILLIS); + () -> finishSpan(ttfdSpan, SpanStatus.DEADLINE_EXCEEDED), TTFD_TIMEOUT_MILLIS); } // lets bind to the scope so other integrations can pick it up From 09e758cec28af35c72dcc9dc06dd71783224b692 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Thu, 9 Feb 2023 18:09:37 +0100 Subject: [PATCH 14/17] updated changelog --- CHANGELOG.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a827493982..e8609d3d50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- Add time-to-full-display span to Activity automatic transactions ([#2432](https://github.com/getsentry/sentry-java/pull/2432)) - Add `main` flag to threads and `in_foreground` flag for app contexts ([#2516](https://github.com/getsentry/sentry-java/pull/2516)) ### Fixes @@ -19,10 +20,6 @@ - Remove authority from URLs sent to Sentry ([#2366](https://github.com/getsentry/sentry-java/pull/2366)) - Fix `sentry-bom` containing incorrect artifacts ([#2504](https://github.com/getsentry/sentry-java/pull/2504)) -### Features - -- Add time-to-full-display span to Activity automatic transactions ([#2432](https://github.com/getsentry/sentry-java/pull/2432)) - ### Dependencies - Bump Native SDK from v0.5.3 to v0.5.4 ([#2500](https://github.com/getsentry/sentry-java/pull/2500)) From 932e25b2cb4264ba63c8dc4247cda888bf9595ee Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Tue, 14 Feb 2023 17:03:32 +0100 Subject: [PATCH 15/17] renamed classes reenabled Activity auto instrumentation auto-finish Put `Sentry.reportFullDisplayed()` in all activities of the sample app --- CHANGELOG.md | 2 +- .../core/ActivityLifecycleIntegration.java | 12 +++--- .../core/ActivityLifecycleIntegrationTest.kt | 6 +-- .../src/main/AndroidManifest.xml | 4 +- .../sentry/samples/android/MainActivity.java | 3 +- .../sentry/samples/android/SecondActivity.kt | 8 ---- sentry/api/sentry.api | 18 ++++---- .../java/io/sentry/FullDisplayedReporter.java | 40 ++++++++++++++++++ .../io/sentry/FullyDisplayedReporter.java | 42 ------------------- sentry/src/main/java/io/sentry/Hub.java | 2 +- .../src/main/java/io/sentry/HubAdapter.java | 2 +- sentry/src/main/java/io/sentry/IHub.java | 2 +- sentry/src/main/java/io/sentry/NoOpHub.java | 2 +- sentry/src/main/java/io/sentry/Sentry.java | 2 +- .../main/java/io/sentry/SentryOptions.java | 14 +++---- ...erTest.kt => FullDisplayedReporterTest.kt} | 16 +++---- sentry/src/test/java/io/sentry/HubTest.kt | 8 ++-- sentry/src/test/java/io/sentry/NoOpHubTest.kt | 2 +- .../test/java/io/sentry/SentryOptionsTest.kt | 2 +- 19 files changed, 88 insertions(+), 99 deletions(-) create mode 100644 sentry/src/main/java/io/sentry/FullDisplayedReporter.java delete mode 100644 sentry/src/main/java/io/sentry/FullyDisplayedReporter.java rename sentry/src/test/java/io/sentry/{FullyDisplayedReporterTest.kt => FullDisplayedReporterTest.kt} (71%) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8609d3d50..4f984ef8c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features -- Add time-to-full-display span to Activity automatic transactions ([#2432](https://github.com/getsentry/sentry-java/pull/2432)) +- Add time-to-full-display span to Activity auto-instrumentation ([#2432](https://github.com/getsentry/sentry-java/pull/2432)) - Add `main` flag to threads and `in_foreground` flag for app contexts ([#2516](https://github.com/getsentry/sentry-java/pull/2516)) ### Fixes diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index 6d9bdc6d27..956b9bd5e7 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -12,7 +12,7 @@ import android.view.View; import androidx.annotation.NonNull; import io.sentry.Breadcrumb; -import io.sentry.FullyDisplayedReporter; +import io.sentry.FullDisplayedReporter; import io.sentry.Hint; import io.sentry.IHub; import io.sentry.ISpan; @@ -64,7 +64,7 @@ public final class ActivityLifecycleIntegration private boolean firstActivityCreated = false; private final boolean foregroundImportance; - private @Nullable FullyDisplayedReporter fullyDisplayedReporter = null; + private @Nullable FullDisplayedReporter fullDisplayedReporter = null; private @Nullable ISpan appStartSpan; private final @NotNull WeakHashMap ttidSpanMap = new WeakHashMap<>(); private @NotNull SentryDate lastPausedTime = AndroidDateUtils.getCurrentSentryDateTime(); @@ -115,7 +115,7 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio this.options.isEnableActivityLifecycleBreadcrumbs()); performanceEnabled = isPerformanceEnabled(this.options); - fullyDisplayedReporter = this.options.getFullyDrawnReporter(); + fullDisplayedReporter = this.options.getFullyDrawnReporter(); timeToFullDisplaySpanEnabled = this.options.isEnableTimeToFullDisplayTracing(); if (this.options.isEnableActivityLifecycleBreadcrumbs() || performanceEnabled) { @@ -238,7 +238,7 @@ private void startTracing(final @NotNull Activity activity) { transaction.startChild( TTID_OP, getTtidDesc(activityName), ttidStartTime, Instrumenter.SENTRY)); - if (timeToFullDisplaySpanEnabled && fullyDisplayedReporter != null && options != null) { + if (timeToFullDisplaySpanEnabled && fullDisplayedReporter != null && options != null) { ttfdSpan = transaction.startChild( TTFD_OP, getTtfdDesc(activityName), ttidStartTime, Instrumenter.SENTRY); @@ -339,8 +339,8 @@ public synchronized void onActivityCreated( firstActivityCreated = true; - if (fullyDisplayedReporter != null) { - fullyDisplayedReporter.registerFullyDrawnListener( + if (fullDisplayedReporter != null) { + fullDisplayedReporter.registerFullyDrawnListener( () -> { finishSpan(ttfdSpan); cancelTtfdAutoClose(); diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt index 30508f9bef..009bf4c404 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt @@ -7,7 +7,7 @@ import android.app.Application import android.os.Bundle import io.sentry.Breadcrumb import io.sentry.DateUtils -import io.sentry.FullyDisplayedReporter +import io.sentry.FullDisplayedReporter import io.sentry.Hub import io.sentry.ISentryExecutorService import io.sentry.Scope @@ -57,7 +57,7 @@ class ActivityLifecycleIntegrationTest { val bundle = mock() val context = TransactionContext("name", "op") val activityFramesTracker = mock() - val fullyDisplayedReporter = FullyDisplayedReporter.getInstance() + val fullDisplayedReporter = FullDisplayedReporter.getInstance() val transactionFinishedCallback = mock() lateinit var transaction: SentryTracer val buildInfo = mock() @@ -668,7 +668,7 @@ class ActivityLifecycleIntegrationTest { val activity = mock() sut.onActivityCreated(activity, mock()) sut.ttidSpanMap.values.first().finish() - fixture.fullyDisplayedReporter.reportFullyDrawn() + fixture.fullDisplayedReporter.reportFullyDrawn() assertTrue(sut.ttfdSpan!!.isFinished) assertNotEquals(SpanStatus.CANCELLED, sut.ttfdSpan?.status) } diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml index 8842c5efa5..e36fd7ae95 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -109,12 +109,12 @@ - + - + diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java index 6f77911d1c..528b7af1f5 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java @@ -197,7 +197,6 @@ protected void onCreate(Bundle savedInstanceState) { }); setContentView(binding.getRoot()); - Sentry.reportFullDisplayed(); } @Override @@ -207,7 +206,7 @@ protected void onResume() { final ISpan span = Sentry.getSpan(); if (span != null) { span.setMeasurement("screen_load_count", screenLoadCount, new MeasurementUnit.Custom("test")); - // span.finish(SpanStatus.OK); } + Sentry.reportFullDisplayed(); } } diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SecondActivity.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SecondActivity.kt index 8c79e7e827..e200fc5fb9 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SecondActivity.kt +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SecondActivity.kt @@ -77,10 +77,6 @@ class SecondActivity : AppCompatActivity() { showText(true, "error: ${t.message}") Sentry.reportFullDisplayed() - // I opt out enableActivityLifecycleTracingAutoFinish so I know best when to end my transaction - // be sure to finish all your spans before this - val transaction = Sentry.getSpan() - transaction?.finish(SpanStatus.INTERNAL_ERROR) } override fun onResponse(call: Call>, response: Response>) { @@ -91,10 +87,6 @@ class SecondActivity : AppCompatActivity() { showText(text = "items: ${repos.size}") Sentry.reportFullDisplayed() - // I opt out enableActivityLifecycleTracingAutoFinish so I know best when to end my transaction - // be sure to finish all your spans before this - val transaction = Sentry.getSpan() - transaction?.finish(SpanStatus.OK) } }) } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index e17b66669c..219c45a1c0 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -275,13 +275,13 @@ public final class io/sentry/ExternalOptions { public fun setTracesSampleRate (Ljava/lang/Double;)V } -public final class io/sentry/FullyDisplayedReporter { - public static fun getInstance ()Lio/sentry/FullyDisplayedReporter; - public fun registerFullyDrawnListener (Lio/sentry/FullyDisplayedReporter$FullyDrawnReporterListener;)V +public final class io/sentry/FullDisplayedReporter { + public static fun getInstance ()Lio/sentry/FullDisplayedReporter; + public fun registerFullyDrawnListener (Lio/sentry/FullDisplayedReporter$FullDisplayedReporterListener;)V public fun reportFullyDrawn ()V } -public abstract interface class io/sentry/FullyDisplayedReporter$FullyDrawnReporterListener { +public abstract interface class io/sentry/FullDisplayedReporter$FullDisplayedReporterListener { public abstract fun onFullyDrawn ()V } @@ -342,7 +342,7 @@ public final class io/sentry/Hub : io/sentry/IHub { public fun pushScope ()V public fun removeExtra (Ljava/lang/String;)V public fun removeTag (Ljava/lang/String;)V - public fun reportFullyDrawn ()V + public fun reportFullDisplayed ()V public fun setExtra (Ljava/lang/String;Ljava/lang/String;)V public fun setFingerprint (Ljava/util/List;)V public fun setLevel (Lio/sentry/SentryLevel;)V @@ -386,7 +386,7 @@ public final class io/sentry/HubAdapter : io/sentry/IHub { public fun pushScope ()V public fun removeExtra (Ljava/lang/String;)V public fun removeTag (Ljava/lang/String;)V - public fun reportFullyDrawn ()V + public fun reportFullDisplayed ()V public fun setExtra (Ljava/lang/String;Ljava/lang/String;)V public fun setFingerprint (Ljava/util/List;)V public fun setLevel (Lio/sentry/SentryLevel;)V @@ -455,7 +455,7 @@ public abstract interface class io/sentry/IHub { public abstract fun pushScope ()V public abstract fun removeExtra (Ljava/lang/String;)V public abstract fun removeTag (Ljava/lang/String;)V - public abstract fun reportFullyDrawn ()V + public abstract fun reportFullDisplayed ()V public abstract fun setExtra (Ljava/lang/String;Ljava/lang/String;)V public abstract fun setFingerprint (Ljava/util/List;)V public abstract fun setLevel (Lio/sentry/SentryLevel;)V @@ -776,7 +776,7 @@ public final class io/sentry/NoOpHub : io/sentry/IHub { public fun pushScope ()V public fun removeExtra (Ljava/lang/String;)V public fun removeTag (Ljava/lang/String;)V - public fun reportFullyDrawn ()V + public fun reportFullDisplayed ()V public fun setExtra (Ljava/lang/String;Ljava/lang/String;)V public fun setFingerprint (Ljava/util/List;)V public fun setLevel (Lio/sentry/SentryLevel;)V @@ -1525,7 +1525,7 @@ public class io/sentry/SentryOptions { public fun getEventProcessors ()Ljava/util/List; public fun getExecutorService ()Lio/sentry/ISentryExecutorService; public fun getFlushTimeoutMillis ()J - public fun getFullyDrawnReporter ()Lio/sentry/FullyDisplayedReporter; + public fun getFullyDrawnReporter ()Lio/sentry/FullDisplayedReporter; public fun getGestureTargetLocators ()Ljava/util/List; public fun getHostnameVerifier ()Ljavax/net/ssl/HostnameVerifier; public fun getIdleTimeout ()Ljava/lang/Long; diff --git a/sentry/src/main/java/io/sentry/FullDisplayedReporter.java b/sentry/src/main/java/io/sentry/FullDisplayedReporter.java new file mode 100644 index 0000000000..c04e59926a --- /dev/null +++ b/sentry/src/main/java/io/sentry/FullDisplayedReporter.java @@ -0,0 +1,40 @@ +package io.sentry; + +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public final class FullDisplayedReporter { + + private static final @NotNull FullDisplayedReporter instance = new FullDisplayedReporter(); + + private final @NotNull List listeners = + new CopyOnWriteArrayList<>(); + + private FullDisplayedReporter() {} + + public static @NotNull FullDisplayedReporter getInstance() { + return instance; + } + + public void registerFullyDrawnListener( + final @NotNull FullDisplayedReporter.FullDisplayedReporterListener listener) { + listeners.add(listener); + } + + public void reportFullyDrawn() { + final @NotNull Iterator listenerIterator = listeners.iterator(); + listeners.clear(); + while (listenerIterator.hasNext()) { + listenerIterator.next().onFullyDrawn(); + } + } + + @ApiStatus.Internal + public interface FullDisplayedReporterListener { + void onFullyDrawn(); + } +} diff --git a/sentry/src/main/java/io/sentry/FullyDisplayedReporter.java b/sentry/src/main/java/io/sentry/FullyDisplayedReporter.java deleted file mode 100644 index 6896cb6643..0000000000 --- a/sentry/src/main/java/io/sentry/FullyDisplayedReporter.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.sentry; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -@ApiStatus.Internal -public final class FullyDisplayedReporter { - - private static final @NotNull FullyDisplayedReporter instance = new FullyDisplayedReporter(); - - private final @NotNull List listeners = new ArrayList<>(); - - private FullyDisplayedReporter() {} - - public static @NotNull FullyDisplayedReporter getInstance() { - return instance; - } - - public void registerFullyDrawnListener(final @NotNull FullyDrawnReporterListener listener) { - synchronized (listeners) { - listeners.add(listener); - } - } - - public void reportFullyDrawn() { - synchronized (listeners) { - final @NotNull Iterator listenerIterator = listeners.iterator(); - while (listenerIterator.hasNext()) { - listenerIterator.next().onFullyDrawn(); - listenerIterator.remove(); - } - } - } - - @ApiStatus.Internal - public interface FullyDrawnReporterListener { - void onFullyDrawn(); - } -} diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index d88dc112ae..81e80ddf2f 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -515,7 +515,7 @@ public void pushScope() { } @Override - public void reportFullyDrawn() { + public void reportFullDisplayed() { if (options.isEnableTimeToFullDisplayTracing()) { options.getFullyDrawnReporter().reportFullyDrawn(); } diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index eceafcb13b..803866d9c8 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -231,7 +231,7 @@ public void setSpanContext( } @Override - public void reportFullyDrawn() { + public void reportFullDisplayed() { Sentry.reportFullDisplayed(); } } diff --git a/sentry/src/main/java/io/sentry/IHub.java b/sentry/src/main/java/io/sentry/IHub.java index 2bfb5e62c0..18537f33e7 100644 --- a/sentry/src/main/java/io/sentry/IHub.java +++ b/sentry/src/main/java/io/sentry/IHub.java @@ -572,5 +572,5 @@ void setSpanContext( *

This method is safe to be called multiple times. If the time-to-full-display span is already * finished, this call will be ignored. */ - void reportFullyDrawn(); + void reportFullDisplayed(); } diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index 7c5474fbf7..334291edfd 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -188,5 +188,5 @@ public void setSpanContext( } @Override - public void reportFullyDrawn() {} + public void reportFullDisplayed() {} } diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 13580cc402..7ce1aaeac2 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -853,7 +853,7 @@ public static void endSession() { * finished, this call will be ignored. */ public static void reportFullDisplayed() { - getCurrentHub().reportFullyDrawn(); + getCurrentHub().reportFullDisplayed(); } /** diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 9770c3af5e..cafb2d6067 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -399,12 +399,12 @@ public class SentryOptions { private @NotNull TransactionPerformanceCollector transactionPerformanceCollector = NoOpTransactionPerformanceCollector.getInstance(); - /** Enables the time-to-full-display spans in automatic ui transactions. */ + /** Enables the time-to-full-display spans in navigation transactions. */ private boolean enableTimeToFullDisplayTracing = false; /** Screen fully displayed reporter, used for time-to-full-display spans. */ - private final @NotNull FullyDisplayedReporter fullyDisplayedReporter = - FullyDisplayedReporter.getInstance(); + private final @NotNull FullDisplayedReporter fullDisplayedReporter = + FullDisplayedReporter.getInstance(); /** * Adds an event processor @@ -1919,7 +1919,7 @@ public void setTransactionPerformanceCollector( } /** - * Gets if the time-to-full-display spans is tracked in automatic ui transactions. + * Gets if the time-to-full-display spans is tracked in navigation transactions. * * @return if the time-to-full-display is tracked. */ @@ -1928,7 +1928,7 @@ public boolean isEnableTimeToFullDisplayTracing() { } /** - * Sets if the time-to-full-display spans should be tracked in automatic ui transactions. + * Sets if the time-to-full-display spans should be tracked in navigation transactions. * * @param enableTimeToFullDisplayTracing if the time-to-full-display spans should be tracked. */ @@ -1942,8 +1942,8 @@ public void setEnableTimeToFullDisplayTracing(final boolean enableTimeToFullDisp * @return The reporter to call when a screen is fully loaded. */ @ApiStatus.Internal - public @NotNull FullyDisplayedReporter getFullyDrawnReporter() { - return fullyDisplayedReporter; + public @NotNull FullDisplayedReporter getFullyDrawnReporter() { + return fullDisplayedReporter; } /** diff --git a/sentry/src/test/java/io/sentry/FullyDisplayedReporterTest.kt b/sentry/src/test/java/io/sentry/FullDisplayedReporterTest.kt similarity index 71% rename from sentry/src/test/java/io/sentry/FullyDisplayedReporterTest.kt rename to sentry/src/test/java/io/sentry/FullDisplayedReporterTest.kt index 85b309e4f2..b2d0f2b33c 100644 --- a/sentry/src/test/java/io/sentry/FullyDisplayedReporterTest.kt +++ b/sentry/src/test/java/io/sentry/FullDisplayedReporterTest.kt @@ -1,6 +1,6 @@ package io.sentry -import io.sentry.FullyDisplayedReporter.FullyDrawnReporterListener +import io.sentry.FullDisplayedReporter.FullDisplayedReporterListener import io.sentry.test.getProperty import org.mockito.kotlin.mock import org.mockito.kotlin.verify @@ -9,14 +9,14 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -class FullyDisplayedReporterTest { +class FullDisplayedReporterTest { - private val reporter = FullyDisplayedReporter.getInstance() - private val listeners = reporter.getProperty>("listeners") - private val listener1 = FullyDrawnReporterListener {} - private val listener2 = FullyDrawnReporterListener {} - private val mockListener1 = mock() - private val mockListener2 = mock() + private val reporter = FullDisplayedReporter.getInstance() + private val listeners = reporter.getProperty>("listeners") + private val listener1 = FullDisplayedReporterListener {} + private val listener2 = FullDisplayedReporterListener {} + private val mockListener1 = mock() + private val mockListener2 = mock() @AfterTest fun shutdown() { diff --git a/sentry/src/test/java/io/sentry/HubTest.kt b/sentry/src/test/java/io/sentry/HubTest.kt index 75acf1fe53..92a33e28ad 100644 --- a/sentry/src/test/java/io/sentry/HubTest.kt +++ b/sentry/src/test/java/io/sentry/HubTest.kt @@ -1656,7 +1656,7 @@ class HubTest { true } } - hub.reportFullyDrawn() + hub.reportFullDisplayed() assertFalse(called) } @@ -1670,7 +1670,7 @@ class HubTest { true } } - hub.reportFullyDrawn() + hub.reportFullDisplayed() assertTrue(called) } @@ -1684,9 +1684,9 @@ class HubTest { true } } - hub.reportFullyDrawn() + hub.reportFullDisplayed() assertTrue(called) - hub.reportFullyDrawn() + hub.reportFullDisplayed() assertTrue(called) } diff --git a/sentry/src/test/java/io/sentry/NoOpHubTest.kt b/sentry/src/test/java/io/sentry/NoOpHubTest.kt index d48310b663..2ff2a45ddd 100644 --- a/sentry/src/test/java/io/sentry/NoOpHubTest.kt +++ b/sentry/src/test/java/io/sentry/NoOpHubTest.kt @@ -83,5 +83,5 @@ class NoOpHubTest { fun `setSpanContext doesnt throw`() = sut.setSpanContext(RuntimeException(), mock(), "") @Test - fun `reportFullyDrawn doesnt throw`() = sut.reportFullyDrawn() + fun `reportFullyDrawn doesnt throw`() = sut.reportFullDisplayed() } diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 3b62a0955b..0246a18600 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -413,6 +413,6 @@ class SentryOptionsTest { @Test fun `when options are initialized, FullyDrawnReporter is set`() { - assertEquals(FullyDisplayedReporter.getInstance(), SentryOptions().fullyDrawnReporter) + assertEquals(FullDisplayedReporter.getInstance(), SentryOptions().fullyDrawnReporter) } } From 8821e3fa0d59b53bef688f01e7361b93fae23596 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Tue, 14 Feb 2023 18:14:18 +0100 Subject: [PATCH 16/17] renamed classes reenabled Activity auto instrumentation auto-finish Put `Sentry.reportFullDisplayed()` in all activities of the sample app --- .../android/core/ActivityLifecycleIntegration.java | 2 +- .../android/core/ActivityLifecycleIntegrationTest.kt | 2 +- sentry/src/main/java/io/sentry/Hub.java | 2 +- sentry/src/main/java/io/sentry/SentryOptions.java | 2 +- sentry/src/test/java/io/sentry/HubTest.kt | 12 ++++++------ sentry/src/test/java/io/sentry/SentryOptionsTest.kt | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index 956b9bd5e7..528aaf944b 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -115,7 +115,7 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio this.options.isEnableActivityLifecycleBreadcrumbs()); performanceEnabled = isPerformanceEnabled(this.options); - fullDisplayedReporter = this.options.getFullyDrawnReporter(); + fullDisplayedReporter = this.options.getFullDisplayedReporter(); timeToFullDisplaySpanEnabled = this.options.isEnableTimeToFullDisplayTracing(); if (this.options.isEnableActivityLifecycleBreadcrumbs() || performanceEnabled) { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt index 009bf4c404..5e265aeb92 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt @@ -1042,7 +1042,7 @@ class ActivityLifecycleIntegrationTest { assertNotNull(autoCloseFuture) // ReportFullyDrawn should finish the ttfd span and cancel the future - fixture.options.fullyDrawnReporter.reportFullyDrawn() + fixture.options.fullDisplayedReporter.reportFullyDrawn() assertTrue(ttfdSpan.isFinished) assertNotEquals(SpanStatus.DEADLINE_EXCEEDED, ttfdSpan.status) assertTrue(autoCloseFuture.isCancelled) diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index 81e80ddf2f..bf4fbe3afd 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -517,7 +517,7 @@ public void pushScope() { @Override public void reportFullDisplayed() { if (options.isEnableTimeToFullDisplayTracing()) { - options.getFullyDrawnReporter().reportFullyDrawn(); + options.getFullDisplayedReporter().reportFullyDrawn(); } } diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index cafb2d6067..1262f683b7 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -1942,7 +1942,7 @@ public void setEnableTimeToFullDisplayTracing(final boolean enableTimeToFullDisp * @return The reporter to call when a screen is fully loaded. */ @ApiStatus.Internal - public @NotNull FullDisplayedReporter getFullyDrawnReporter() { + public @NotNull FullDisplayedReporter getFullDisplayedReporter() { return fullDisplayedReporter; } diff --git a/sentry/src/test/java/io/sentry/HubTest.kt b/sentry/src/test/java/io/sentry/HubTest.kt index 92a33e28ad..a30811b260 100644 --- a/sentry/src/test/java/io/sentry/HubTest.kt +++ b/sentry/src/test/java/io/sentry/HubTest.kt @@ -1648,10 +1648,10 @@ class HubTest { } @Test - fun `reportFullyDrawn is ignored if TimeToFullDisplayTracing is disabled`() { + fun `reportFullDisplayed is ignored if TimeToFullDisplayTracing is disabled`() { var called = false val hub = generateHub { - it.fullyDrawnReporter.registerFullyDrawnListener { + it.fullDisplayedReporter.registerFullyDrawnListener { called = !called true } @@ -1661,11 +1661,11 @@ class HubTest { } @Test - fun `reportFullyDrawn calls FullyDrawnReporter if TimeToFullDisplayTracing is enabled`() { + fun `reportFullDisplayed calls FullDisplayedReporter if TimeToFullDisplayTracing is enabled`() { var called = false val hub = generateHub { it.isEnableTimeToFullDisplayTracing = true - it.fullyDrawnReporter.registerFullyDrawnListener { + it.fullDisplayedReporter.registerFullyDrawnListener { called = !called true } @@ -1675,11 +1675,11 @@ class HubTest { } @Test - fun `reportFullyDrawn calls FullyDrawnReporter only once`() { + fun `reportFullDisplayed calls FullDisplayedReporter only once`() { var called = false val hub = generateHub { it.isEnableTimeToFullDisplayTracing = true - it.fullyDrawnReporter.registerFullyDrawnListener { + it.fullDisplayedReporter.registerFullyDrawnListener { called = !called true } diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 0246a18600..7407aa11e8 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -413,6 +413,6 @@ class SentryOptionsTest { @Test fun `when options are initialized, FullyDrawnReporter is set`() { - assertEquals(FullDisplayedReporter.getInstance(), SentryOptions().fullyDrawnReporter) + assertEquals(FullDisplayedReporter.getInstance(), SentryOptions().fullDisplayedReporter) } } From 2bbc3f16f29a21393eef25bdee53cb595a36cdd4 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Tue, 14 Feb 2023 18:44:46 +0100 Subject: [PATCH 17/17] renamed classes --- sentry/api/sentry.api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 219c45a1c0..90dda301d4 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -1525,7 +1525,7 @@ public class io/sentry/SentryOptions { public fun getEventProcessors ()Ljava/util/List; public fun getExecutorService ()Lio/sentry/ISentryExecutorService; public fun getFlushTimeoutMillis ()J - public fun getFullyDrawnReporter ()Lio/sentry/FullDisplayedReporter; + public fun getFullDisplayedReporter ()Lio/sentry/FullDisplayedReporter; public fun getGestureTargetLocators ()Ljava/util/List; public fun getHostnameVerifier ()Ljavax/net/ssl/HostnameVerifier; public fun getIdleTimeout ()Ljava/lang/Long;