Introduce process lifecycle observer to collect metrics about tabs when app goes to foreground/background.

upstream-sync
Sebastian Kaspari 3 years ago
parent 54d46c7e94
commit dfb3c4c9bf

@ -4965,6 +4965,46 @@ engine_tab:
- fenix-core@mozilla.com
- skaspari@mozilla.com
expires: "2021-12-31"
foreground_metrics:
type: event
description: |
Event collecting data about the state of tabs when the app comes back to
the foreground.
bugs:
- https://github.com/mozilla-mobile/android-components/issues/9997
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/18747#issuecomment-815731764
data_sensitivity:
- technical
notification_emails:
- fenix-core@mozilla.com
- skaspari@mozilla.com
expires: "2021-12-31"
extra_keys:
background_active_tabs:
description: |
Number of active tabs (with an engine session assigned) when the app
went to the background.
background_crashed_tabs:
description: |
Number of tabs marked as crashed when the app went to the background.
background_total_tabs:
description: |
Number of total tabs when the app went to the background.
foreground_active_tabs:
description: |
Number of active tabs (with an engine session assigned) when the
app came back to the foreground.
foreground_crashed_tabs:
description: |
Number of tabs marked as crashed when the app came back to the
foreground.
foreground_total_tabs:
description: |
Number of total tabs when the app came back to the foreground.
time_in_background:
description: |
Time (in milliseconds) the app was in the background.
synced_tabs:
synced_tabs_suggestion_clicked:

@ -13,6 +13,7 @@ import androidx.annotation.CallSuper
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.getSystemService
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.work.Configuration.Builder
import androidx.work.Configuration.Provider
import kotlinx.coroutines.Deferred
@ -59,6 +60,7 @@ import org.mozilla.fenix.push.PushFxaIntegration
import org.mozilla.fenix.push.WebPushEngineIntegration
import org.mozilla.fenix.session.PerformanceActivityLifecycleCallbacks
import org.mozilla.fenix.session.VisibilityLifecycleCallback
import org.mozilla.fenix.telemetry.TelemetryLifecycleObserver
import org.mozilla.fenix.utils.BrowsersCache
import java.util.concurrent.TimeUnit
@ -194,6 +196,8 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
components.startupActivityStateProvider.registerInAppOnCreate(this)
initVisualCompletenessQueueAndQueueTasks()
ProcessLifecycleOwner.get().lifecycle.addObserver(TelemetryLifecycleObserver(components.core.store))
components.appStartupTelemetry.onFenixApplicationOnCreate()
}
}

@ -0,0 +1,76 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.telemetry
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.support.base.android.Clock
import org.mozilla.fenix.GleanMetrics.EngineTab as EngineMetrics
import org.mozilla.fenix.GleanMetrics.EngineTab.foregroundMetricsKeys as MetricsKeys
/**
* [LifecycleObserver] to used on the process lifecycle to measure the amount of tabs getting killed
* while the app is in the background.
*
* See:
* - https://github.com/mozilla-mobile/android-components/issues/9624
* - https://github.com/mozilla-mobile/android-components/issues/9997
*/
class TelemetryLifecycleObserver(
private val store: BrowserStore
) : LifecycleObserver {
private var pausedState: TabState? = null
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun onPause() {
pausedState = createTabState()
}
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume() {
val lastState = pausedState ?: return
val currentState = createTabState()
EngineMetrics.foregroundMetrics.record(mapOf(
MetricsKeys.backgroundActiveTabs to lastState.activeEngineTabs.toString(),
MetricsKeys.backgroundCrashedTabs to lastState.crashedTabs.toString(),
MetricsKeys.backgroundTotalTabs to lastState.totalTabs.toString(),
MetricsKeys.foregroundActiveTabs to currentState.activeEngineTabs.toString(),
MetricsKeys.foregroundCrashedTabs to currentState.crashedTabs.toString(),
MetricsKeys.foregroundTotalTabs to currentState.totalTabs.toString(),
MetricsKeys.timeInBackground to (currentState.timestamp - lastState.timestamp).toString()
))
pausedState = null
}
private fun createTabState(): TabState {
val tabsWithEngineSession = store.state.tabs
.filter { tab -> tab.engineState.engineSession != null }
.filter { tab -> !tab.engineState.crashed }
.count()
val totalTabs = store.state.tabs.count()
val crashedTabs = store.state.tabs
.filter { tab -> tab.engineState.crashed }
.count()
return TabState(
activeEngineTabs = tabsWithEngineSession,
totalTabs = totalTabs,
crashedTabs = crashedTabs
)
}
}
private data class TabState(
val timestamp: Long = Clock.elapsedRealtime(),
val totalTabs: Int,
val crashedTabs: Int,
val activeEngineTabs: Int
)

@ -0,0 +1,124 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.telemetry
import androidx.test.core.app.ApplicationProvider
import io.mockk.mockk
import mozilla.components.browser.session.engine.EngineMiddleware
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.service.glean.testing.GleanTestRule
import mozilla.components.support.base.android.Clock
import mozilla.components.support.test.ext.joinBlocking
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.GleanMetrics.EngineTab as EngineMetrics
@RunWith(FenixRobolectricTestRunner::class)
class TelemetryLifecycleObserverTest {
@get:Rule
val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext())
private val clock = FakeClock()
@Before
fun setUp() {
Clock.delegate = clock
}
@After
fun tearDown() {
Clock.reset()
}
@Test
fun `resume without a pause does not record any metrics`() {
val store = BrowserStore()
val observer = TelemetryLifecycleObserver(store)
observer.onResume()
assertFalse(EngineMetrics.foregroundMetrics.testHasValue())
}
@Test
fun `resume after pause records metrics`() {
val store = BrowserStore()
val observer = TelemetryLifecycleObserver(store)
observer.onPause()
clock.elapsedTime = 550
observer.onResume()
assertTrue(EngineMetrics.foregroundMetrics.testHasValue())
val metrics = EngineMetrics.foregroundMetrics.testGetValue()
assertEquals(1, metrics.size)
val metric = metrics[0]
assertNotNull(metric.extra)
assertEquals("550", metric.extra!!["time_in_background"])
}
@Test
fun `resume records expected values`() {
val store = BrowserStore(
initialState = BrowserState(
tabs = listOf(
createTab("https://www.mozilla.org", id = "mozilla", engineSession = mockk(relaxed = true)),
createTab("https://news.google.com", id = "news"),
createTab("https://theverge.com", id = "theverge", engineSession = mockk(relaxed = true)),
createTab("https://www.google.com", id = "google", engineSession = mockk(relaxed = true)),
createTab("https://getpocket.com", id = "pocket", crashed = true)
)
),
middleware = EngineMiddleware.create(engine = mockk(), sessionLookup = { null })
)
val observer = TelemetryLifecycleObserver(store)
clock.elapsedTime = 120
observer.onPause()
store.dispatch(
EngineAction.KillEngineSessionAction("theverge")
).joinBlocking()
store.dispatch(
EngineAction.SuspendEngineSessionAction("mozilla")
).joinBlocking()
clock.elapsedTime = 10340
observer.onResume()
assertTrue(EngineMetrics.foregroundMetrics.testHasValue())
val metrics = EngineMetrics.foregroundMetrics.testGetValue()
assertEquals(1, metrics.size)
val metric = metrics[0]
assertNotNull(metric.extra)
assertEquals("10220", metric.extra!!["time_in_background"])
assertEquals("3", metric.extra!!["background_active_tabs"])
assertEquals("1", metric.extra!!["background_crashed_tabs"])
assertEquals("5", metric.extra!!["background_total_tabs"])
assertEquals("1", metric.extra!!["foreground_active_tabs"])
assertEquals("1", metric.extra!!["foreground_crashed_tabs"])
assertEquals("5", metric.extra!!["foreground_total_tabs"])
}
}

@ -391,7 +391,7 @@ class TelemetryMiddlewareTest {
}
}
private class FakeClock : Clock.Delegate {
internal class FakeClock : Clock.Delegate {
var elapsedTime: Long = 0
override fun elapsedRealtime(): Long = elapsedTime
}

@ -126,6 +126,7 @@ In addition to those built-in metrics, the following metrics are added to the pi
| downloads_management.item_deleted |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A counter for how often a user deletes one / more downloads at a time. |[mozilla-mobile/fenix#16728](https://github.com/mozilla-mobile/fenix/pull/16728), [mozilla-mobile/fenix#18143](https://github.com/mozilla-mobile/fenix/pull/18143)||2021-07-01 | |
| downloads_management.item_opened |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A counter for how often a user tap to opens a download from inside the "Downloads" folder. |[mozilla-mobile/fenix#16728](https://github.com/mozilla-mobile/fenix/pull/16728), [mozilla-mobile/fenix#18143](https://github.com/mozilla-mobile/fenix/pull/18143)||2021-07-01 | |
| downloads_misc.download_added |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A counter for how many times something is downloaded in the app. |[mozilla-mobile/fenix#16730](https://github.com/mozilla-mobile/fenix/pull/16730), [mozilla-mobile/fenix#18143](https://github.com/mozilla-mobile/fenix/pull/18143)||2021-07-01 | |
| engine_tab.foreground_metrics |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |Event collecting data about the state of tabs when the app comes back to the foreground. |[mozilla-mobile/fenix#18747](https://github.com/mozilla-mobile/fenix/pull/18747#issuecomment-815731764)|<ul><li>background_active_tabs: Number of active tabs (with an engine session assigned) when the app went to the background. </li><li>background_crashed_tabs: Number of tabs marked as crashed when the app went to the background. </li><li>background_total_tabs: Number of total tabs when the app went to the background. </li><li>foreground_active_tabs: Number of active tabs (with an engine session assigned) when the app came back to the foreground. </li><li>foreground_crashed_tabs: Number of tabs marked as crashed when the app came back to the foreground. </li><li>foreground_total_tabs: Number of total tabs when the app came back to the foreground. </li><li>time_in_background: Time (in milliseconds) the app was in the background. </li></ul>|2021-12-31 |1 |
| error_page.visited_error |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user encountered an error page |[mozilla-mobile/fenix#2491](https://github.com/mozilla-mobile/fenix/pull/2491#issuecomment-492414486), [mozilla-mobile/fenix#13958](https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877), [mozilla-mobile/fenix#18143](https://github.com/mozilla-mobile/fenix/pull/18143)|<ul><li>error_type: The error type of the error page encountered</li></ul>|2021-07-01 |2 |
| events.app_opened |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user opened the app (from cold start, to the homescreen or browser) |[mozilla-mobile/fenix#1067](https://github.com/mozilla-mobile/fenix/pull/1067#issuecomment-474598673), [mozilla-mobile/fenix#13958](https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877), [mozilla-mobile/fenix#18143](https://github.com/mozilla-mobile/fenix/pull/18143)|<ul><li>source: The method used to open Fenix. Possible values are: `app_icon`, `custom_tab` or `link` </li></ul>|2021-07-01 |2 |
| events.app_opened_all_startup |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |**This probe has a known flaw:** for COLD start up, it doesn't take into account if the process is already running when the app starts, possibly inflating results (e.g. a Service started the process 20min ago and only now is HomeActivity launching). See the `cold_*_app_to_first_frame` probes for a replacement. <br><br> A user opened the app to the HomeActivity. The HomeActivity encompasses the home screen, browser screen, settings screen, collections and other screens in the nav_graph. This differs from the app_opened probe because it measures all startups, not just cold startup. Note: There is a short gap between the time application goes into background and the time android reports the application going into the background. Note: This metric does not record souce when app opened from task switcher: open application -> press home button -> open recent tasks -> choose fenix. In this case will report [source = unknown, type = hot, has_saved_instance_state = false]. |[mozilla-mobile/fenix#12114](https://github.com/mozilla-mobile/fenix/pull/12114#pullrequestreview-445245341), [mozilla-mobile/fenix#13958](https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877), [mozilla-mobile/fenix#13494](https://github.com/mozilla-mobile/fenix/pull/13494#pullrequestreview-474050499), [mozilla-mobile/fenix#15605](https://github.com/mozilla-mobile/fenix/pull/15605#issuecomment-702365594)|<ul><li>first_frame_pre_draw_nanos: the number of nanoseconds the application took to launch. This is the time difference between application launch(user pressing app_icon, launching a link) and until the first view is about to be drawn on the screen. If the time is not captured, this extra key will not be reported. </li><li>has_saved_instance_state: boolean value whether or not startup type has a savedInstance. using savedInstance, HomeActivity's previous state can be restored. This is an optional key since it is not applicable to all the cases. for example, when we are doing a hot start up, we cant have a savedInstanceState therefore we report only [APP_ICON, HOT] instead of [APP_ICON, HOT, false]. </li><li>source: The method used to open Fenix. Possible values are `app_icon`, `custom_tab`, `link` or `unknown`. unknown is for startup sources where we can't pinpoint the cause. One UNKNOWN case is the app switcher where we don't know what variables to check to ensure this startup wasn't caused by something else. </li><li>type: the startup type for opening fenix. the application and HomeActivity either needs to be created or started again. possible values are `cold`, `warm`, `hot` or `error`. Error is for impossible cases. Please file a bug if you see the error case. app created AND HomeActivity created = cold app started AND HomeActivity created = warm app started AND HomeActivity started = hot app created AND HomeActivity started = error Some applications such as gmail launches the default browser in the background. So when we eventually click a link, browser is already started in the background. This means that custom_tab will mostly report `warm` startup type. </li></ul>|2021-06-01 |2 |

Loading…
Cancel
Save