1
0
Fork 0

For #6556: Adds Telemetry counts for Search Access Points

Added two new sources to be counted ('widget' and 'shortcut') besides 'action' and
'suggestion'. Also modified/fixed some tests.
master
ValentinTimisica 2019-12-18 17:09:12 +02:00
parent 9c25d274d0
commit 981d19de13
16 changed files with 247 additions and 57 deletions

View File

@ -325,15 +325,18 @@ metrics:
If the search engine is bundled with Fenix `search-engine-name` will be the name of the search engine. If it's a
custom search engine (defined: https://github.com/mozilla-mobile/fenix/issues/1607) the value will be `custom`.
`source` will either be `action` or `suggestion`
`source` will be: `action`, `suggestion`, `widget` or `shortcut` (depending on the source from which the search started).
Also added the `other` option for the source but it should never enter on this case.
send_in_pings:
- metrics
- baseline
bugs:
- https://github.com/mozilla-mobile/fenix/issues/1158
- https://github.com/mozilla-mobile/fenix/issues/6556
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/1677
- https://github.com/mozilla-mobile/fenix/pull/5216
- https://github.com/mozilla-mobile/fenix/pull/7310
notification_emails:
- fenix-core@mozilla.com
expires: "2020-03-01"

View File

@ -78,7 +78,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity() {
private val externalSourceIntentProcessors by lazy {
listOf(
SpeechProcessingIntentProcessor(this),
SpeechProcessingIntentProcessor(this, components.analytics.metrics),
StartSearchIntentProcessor(components.analytics.metrics),
DeepLinkIntentProcessor(this),
OpenBrowserIntentProcessor(this, ::getIntentSessionId)

View File

@ -271,27 +271,31 @@ sealed class Event {
}
}
sealed class EventSource {
data class Suggestion(val engineSource: EngineSource) : EventSource()
data class Action(val engineSource: EngineSource) : EventSource()
private val source: EngineSource
get() = when (this) {
is Suggestion -> engineSource
is Action -> engineSource
}
sealed class EventSource(open val engineSource: EngineSource) {
data class Suggestion(override val engineSource: EngineSource) : EventSource(engineSource)
data class Action(override val engineSource: EngineSource) : EventSource(engineSource)
data class Widget(override val engineSource: EngineSource) : EventSource(engineSource)
data class Shortcut(override val engineSource: EngineSource) : EventSource(engineSource)
data class Other(override val engineSource: EngineSource) : EventSource(engineSource)
private val label: String
get() = when (this) {
is Suggestion -> "suggestion"
is Action -> "action"
is Widget -> "widget"
is Shortcut -> "shortcut"
is Other -> "other"
}
val countLabel: String
get() = "${source.identifier.toLowerCase(Locale.getDefault())}.$label"
get() = "${engineSource.identifier.toLowerCase(Locale.getDefault())}.$label"
val sourceLabel: String
get() = "${source.descriptor}.$label"
get() = "${engineSource.descriptor}.$label"
}
enum class SearchAccessPoint {
SUGGESTION, ACTION, WIDGET, SHORTCUT, NONE
}
override val extras: Map<Events.performedSearchKeys, String>?

View File

@ -0,0 +1,54 @@
/* 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.components.metrics
import android.content.Context
import mozilla.components.browser.search.SearchEngine
import org.mozilla.fenix.components.metrics.Event.PerformedSearch.SearchAccessPoint
import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore
import org.mozilla.fenix.ext.searchEngineManager
object MetricsUtils {
fun createSearchEvent(
engine: SearchEngine,
context: Context,
searchAccessPoint: SearchAccessPoint
): Event.PerformedSearch? {
val isShortcut = engine != context.searchEngineManager.defaultSearchEngine
val isCustom = CustomSearchEngineStore.isCustomSearchEngine(context, engine.identifier)
val engineSource =
if (isShortcut) Event.PerformedSearch.EngineSource.Shortcut(engine, isCustom)
else Event.PerformedSearch.EngineSource.Default(engine, isCustom)
return when (searchAccessPoint) {
SearchAccessPoint.SUGGESTION -> Event.PerformedSearch(
Event.PerformedSearch.EventSource.Suggestion(
engineSource
)
)
SearchAccessPoint.ACTION -> Event.PerformedSearch(
Event.PerformedSearch.EventSource.Action(
engineSource
)
)
SearchAccessPoint.WIDGET -> Event.PerformedSearch(
Event.PerformedSearch.EventSource.Widget(
engineSource
)
)
SearchAccessPoint.SHORTCUT -> Event.PerformedSearch(
Event.PerformedSearch.EventSource.Shortcut(
engineSource
)
)
SearchAccessPoint.NONE -> Event.PerformedSearch(
Event.PerformedSearch.EventSource.Other(
engineSource
)
)
}
}
}

View File

@ -8,6 +8,10 @@ import android.content.Intent
import androidx.navigation.NavController
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.components.metrics.MetricsUtils
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.widget.VoiceSearchActivity.Companion.SPEECH_PROCESSING
/**
@ -15,12 +19,21 @@ import org.mozilla.fenix.widget.VoiceSearchActivity.Companion.SPEECH_PROCESSING
* Once the search is complete then a new search should be started.
*/
class SpeechProcessingIntentProcessor(
private val activity: HomeActivity
private val activity: HomeActivity,
private val metrics: MetricController
) : HomeIntentProcessor {
override fun process(intent: Intent, navController: NavController, out: Intent): Boolean {
return if (intent.extras?.getBoolean(HomeActivity.OPEN_TO_BROWSER_AND_LOAD) == true) {
out.putExtra(HomeActivity.OPEN_TO_BROWSER_AND_LOAD, false)
val searchEvent = MetricsUtils.createSearchEvent(
activity.components.search.provider.getDefaultEngine(activity),
activity,
Event.PerformedSearch.SearchAccessPoint.WIDGET
)
searchEvent?.let { metrics.track(it) }
activity.openToBrowserAndLoad(
searchTermOrURL = intent.getStringExtra(SPEECH_PROCESSING).orEmpty(),
newTab = true,

View File

@ -22,20 +22,36 @@ class StartSearchIntentProcessor(
override fun process(intent: Intent, navController: NavController, out: Intent): Boolean {
val event = intent.extras?.getString(HomeActivity.OPEN_TO_SEARCH)
var source: Event.PerformedSearch.SearchAccessPoint? = null
return if (event != null) {
when (event) {
SEARCH_WIDGET -> metrics.track(Event.SearchWidgetNewTabPressed)
STATIC_SHORTCUT_NEW_TAB -> metrics.track(Event.PrivateBrowsingStaticShortcutTab)
STATIC_SHORTCUT_NEW_PRIVATE_TAB -> metrics.track(Event.PrivateBrowsingStaticShortcutPrivateTab)
PRIVATE_BROWSING_PINNED_SHORTCUT -> metrics.track(Event.PrivateBrowsingPinnedShortcutPrivateTab)
SEARCH_WIDGET -> {
metrics.track(Event.SearchWidgetNewTabPressed)
source = Event.PerformedSearch.SearchAccessPoint.WIDGET
}
STATIC_SHORTCUT_NEW_TAB -> {
metrics.track(Event.PrivateBrowsingStaticShortcutTab)
source = Event.PerformedSearch.SearchAccessPoint.SHORTCUT
}
STATIC_SHORTCUT_NEW_PRIVATE_TAB -> {
metrics.track(Event.PrivateBrowsingStaticShortcutPrivateTab)
source = Event.PerformedSearch.SearchAccessPoint.SHORTCUT
}
PRIVATE_BROWSING_PINNED_SHORTCUT -> {
metrics.track(Event.PrivateBrowsingPinnedShortcutPrivateTab)
source = Event.PerformedSearch.SearchAccessPoint.SHORTCUT
}
}
out.removeExtra(HomeActivity.OPEN_TO_SEARCH)
val directions = NavGraphDirections.actionGlobalSearch(
sessionId = null
)
navController.nav(null, directions)
val directions = source?.let {
NavGraphDirections.actionGlobalSearch(
sessionId = null,
searchAccessPoint = it
)
}
directions?.let { navController.nav(null, it) }
true
} else {
false

View File

@ -14,12 +14,15 @@ import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.Event.PerformedSearch.SearchAccessPoint.ACTION
import org.mozilla.fenix.components.metrics.Event.PerformedSearch.SearchAccessPoint.NONE
import org.mozilla.fenix.components.metrics.Event.PerformedSearch.SearchAccessPoint.SUGGESTION
import org.mozilla.fenix.components.metrics.MetricsUtils
import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.searchEngineManager
/**
* An interface that handles the view manipulation of the Search, triggered by the Interactor
@ -54,10 +57,21 @@ class DefaultSearchController(
val event = if (url.isUrl()) {
Event.EnteredUrl(false)
} else {
createSearchEvent(store.state.searchEngineSource.searchEngine, false)
val searchAccessPoint = when (store.state.searchAccessPoint) {
NONE -> ACTION
else -> store.state.searchAccessPoint
}
searchAccessPoint?.let { sap ->
MetricsUtils.createSearchEvent(
store.state.searchEngineSource.searchEngine,
context,
sap
)
}
}
context.metrics.track(event)
event?.let { context.metrics.track(it) }
if (CustomSearchEngineStore.isCustomSearchEngine(
context,
store.state.searchEngineSource.searchEngine.identifier
@ -111,8 +125,20 @@ class DefaultSearchController(
forceSearch = true
)
val event = createSearchEvent(store.state.searchEngineSource.searchEngine, true)
context.metrics.track(event)
val searchAccessPoint = when (store.state.searchAccessPoint) {
NONE -> SUGGESTION
else -> store.state.searchAccessPoint
}
val event = searchAccessPoint?.let { sap ->
MetricsUtils.createSearchEvent(
store.state.searchEngineSource.searchEngine,
context,
sap
)
}
event?.let { context.metrics.track(it) }
if (CustomSearchEngineStore.isCustomSearchEngine(
context,
store.state.searchEngineSource.searchEngine.identifier
@ -142,22 +168,4 @@ class DefaultSearchController(
navController.nav(R.id.searchFragment, directions)
context.components.core.sessionManager.select(session)
}
private fun createSearchEvent(
engine: SearchEngine,
isSuggestion: Boolean
): Event.PerformedSearch {
val isShortcut = engine != context.searchEngineManager.defaultSearchEngine
val isCustom = CustomSearchEngineStore.isCustomSearchEngine(context, engine.identifier)
val engineSource =
if (isShortcut) Event.PerformedSearch.EngineSource.Shortcut(engine, isCustom)
else Event.PerformedSearch.EngineSource.Default(engine, isCustom)
val source =
if (isSuggestion) Event.PerformedSearch.EventSource.Suggestion(engineSource)
else Event.PerformedSearch.EventSource.Action(engineSource)
return Event.PerformedSearch(source)
}
}

View File

@ -80,6 +80,9 @@ class SearchFragment : Fragment(), UserInteractionHandler {
?.let(SearchFragmentArgs.Companion::fromBundle)
?.let { it.pastedText }
val searchAccessPoint = arguments
?.let(SearchFragmentArgs.Companion::fromBundle)?.searchAccessPoint
val view = inflater.inflate(R.layout.fragment_search, container, false)
val url = session?.url.orEmpty()
val currentSearchEngine = SearchEngineSource.Default(
@ -107,7 +110,8 @@ class SearchFragment : Fragment(), UserInteractionHandler {
showHistorySuggestions = requireContext().settings().shouldShowHistorySuggestions,
showBookmarkSuggestions = requireContext().settings().shouldShowBookmarkSuggestions,
session = session,
pastedText = pastedText
pastedText = pastedText,
searchAccessPoint = searchAccessPoint
)
)
}

View File

@ -9,6 +9,7 @@ import mozilla.components.browser.session.Session
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
import org.mozilla.fenix.components.metrics.Event
/**
* The [Store] for holding the [SearchFragmentState] and applying [SearchFragmentAction]s.
@ -55,7 +56,8 @@ data class SearchFragmentState(
val showHistorySuggestions: Boolean,
val showBookmarkSuggestions: Boolean,
val session: Session?,
val pastedText: String? = null
val pastedText: String? = null,
val searchAccessPoint: Event.PerformedSearch.SearchAccessPoint?
) : State
/**

View File

@ -112,6 +112,10 @@
android:defaultValue="@null"
app:argType="string"
app:nullable="true" />
<argument
android:name="search_access_point"
app:argType="org.mozilla.fenix.components.metrics.Event$PerformedSearch$SearchAccessPoint"
android:defaultValue="NONE" />
</fragment>
<fragment

View File

@ -0,0 +1,62 @@
package org.mozilla.fenix.components.metrics
import io.mockk.every
import io.mockk.mockk
import mozilla.components.browser.search.SearchEngine
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.TestApplication
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(application = TestApplication::class)
class MetricsUtilsTest {
@Test
fun createSearchEvent() {
val engine: SearchEngine = mockk(relaxed = true)
val context = testContext
every { engine.identifier } returns ENGINE_SOURCE_IDENTIFIER
assertEquals(
"$ENGINE_SOURCE_IDENTIFIER.suggestion",
MetricsUtils.createSearchEvent(
engine,
context,
Event.PerformedSearch.SearchAccessPoint.SUGGESTION
)?.eventSource?.countLabel
)
assertEquals(
"$ENGINE_SOURCE_IDENTIFIER.action",
MetricsUtils.createSearchEvent(
engine,
context,
Event.PerformedSearch.SearchAccessPoint.ACTION
)?.eventSource?.countLabel
)
assertEquals(
"$ENGINE_SOURCE_IDENTIFIER.widget",
MetricsUtils.createSearchEvent(
engine,
context,
Event.PerformedSearch.SearchAccessPoint.WIDGET
)?.eventSource?.countLabel
)
assertEquals(
"$ENGINE_SOURCE_IDENTIFIER.shortcut",
MetricsUtils.createSearchEvent(
engine,
context,
Event.PerformedSearch.SearchAccessPoint.SHORTCUT
)?.eventSource?.countLabel
)
}
companion object {
const val ENGINE_SOURCE_IDENTIFIER = "google-2018"
}
}

View File

@ -7,6 +7,7 @@ package org.mozilla.fenix.home.intent
import android.content.Intent
import androidx.navigation.NavController
import io.mockk.Called
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.Test
@ -14,6 +15,8 @@ import org.junit.runner.RunWith
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.TestApplication
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.widget.VoiceSearchActivity.Companion.SPEECH_PROCESSING
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@ -25,15 +28,17 @@ class SpeechProcessingIntentProcessorTest {
private val activity: HomeActivity = mockk(relaxed = true)
private val navController: NavController = mockk(relaxed = true)
private val out: Intent = mockk(relaxed = true)
private val metrics: MetricController = mockk(relaxed = true)
@Test
fun `do not process blank intents`() {
val processor = SpeechProcessingIntentProcessor(activity)
val processor = SpeechProcessingIntentProcessor(activity, metrics)
processor.process(Intent(), navController, out)
verify { activity wasNot Called }
verify { navController wasNot Called }
verify { out wasNot Called }
verify { metrics wasNot Called }
}
@Test
@ -41,12 +46,13 @@ class SpeechProcessingIntentProcessorTest {
val intent = Intent().apply {
putExtra(HomeActivity.OPEN_TO_BROWSER_AND_LOAD, false)
}
val processor = SpeechProcessingIntentProcessor(activity)
val processor = SpeechProcessingIntentProcessor(activity, metrics)
processor.process(intent, navController, out)
verify { activity wasNot Called }
verify { navController wasNot Called }
verify { out wasNot Called }
verify { metrics wasNot Called }
}
@Test
@ -54,7 +60,9 @@ class SpeechProcessingIntentProcessorTest {
val intent = Intent().apply {
putExtra(HomeActivity.OPEN_TO_BROWSER_AND_LOAD, true)
}
val processor = SpeechProcessingIntentProcessor(activity)
val processor = SpeechProcessingIntentProcessor(activity, metrics)
every { activity.components.search.provider.getDefaultEngine(activity) } returns mockk(relaxed = true)
processor.process(intent, navController, out)
verify {
@ -75,7 +83,9 @@ class SpeechProcessingIntentProcessorTest {
putExtra(HomeActivity.OPEN_TO_BROWSER_AND_LOAD, true)
putExtra(SPEECH_PROCESSING, "hello world")
}
val processor = SpeechProcessingIntentProcessor(activity)
val processor = SpeechProcessingIntentProcessor(activity, metrics)
every { activity.components.search.provider.getDefaultEngine(activity) } returns mockk(relaxed = true)
processor.process(intent, mockk(), mockk(relaxed = true))
verify {

View File

@ -58,7 +58,10 @@ class StartSearchIntentProcessorTest {
verify { metrics.track(Event.SearchWidgetNewTabPressed) }
verify {
navController.navigate(
NavGraphDirections.actionGlobalSearch(sessionId = null),
NavGraphDirections.actionGlobalSearch(
sessionId = null,
searchAccessPoint = Event.PerformedSearch.SearchAccessPoint.WIDGET
),
null
)
}

View File

@ -10,6 +10,7 @@ import mozilla.components.browser.search.SearchEngine
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotSame
import org.junit.Test
import org.mozilla.fenix.components.metrics.Event
class SearchFragmentStoreTest {
@ -56,6 +57,7 @@ class SearchFragmentStoreTest {
showClipboardSuggestions = false,
showHistorySuggestions = false,
showBookmarkSuggestions = false,
session = null
session = null,
searchAccessPoint = Event.PerformedSearch.SearchAccessPoint.NONE
)
}

View File

@ -23,9 +23,10 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.FenixApplication
import org.mozilla.fenix.TestApplication
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.TestApplication
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore.PREF_FILE_SEARCH_ENGINES
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.searchEngineManager
@ -44,6 +45,7 @@ class SearchInteractorTest {
val state: SearchFragmentState = mockk()
val searchEngineManager: SearchEngineManager = mockk(relaxed = true)
val searchEngine = SearchEngineSource.Default(mockk(relaxed = true))
val searchAccessPoint: Event.PerformedSearch.SearchAccessPoint = mockk(relaxed = true)
every { context.metrics } returns mockk(relaxed = true)
every { context.searchEngineManager } returns searchEngineManager
@ -52,6 +54,7 @@ class SearchInteractorTest {
every { store.state } returns state
every { state.session } returns null
every { state.searchEngineSource } returns searchEngine
every { state.searchAccessPoint } returns searchAccessPoint
every {
context.getSharedPreferences(
@ -170,6 +173,7 @@ class SearchInteractorTest {
val state: SearchFragmentState = mockk()
val searchEngineManager: SearchEngineManager = mockk(relaxed = true)
val searchEngine = SearchEngineSource.Default(mockk(relaxed = true))
val searchAccessPoint: Event.PerformedSearch.SearchAccessPoint = mockk(relaxed = true)
every { context.metrics } returns mockk(relaxed = true)
every { context.searchEngineManager } returns searchEngineManager
@ -178,6 +182,7 @@ class SearchInteractorTest {
every { store.state } returns state
every { state.session } returns null
every { state.searchEngineSource } returns searchEngine
every { state.searchAccessPoint } returns searchAccessPoint
every {
context.getSharedPreferences(

File diff suppressed because one or more lines are too long