For #24224: Send telemetry events related to the messaging framework

This commit is contained in:
Arturo Mejia 2022-03-26 18:59:58 -04:00 committed by mergify[bot]
parent 20b738c69d
commit bfeceb562d
10 changed files with 213 additions and 36 deletions

View File

@ -7301,6 +7301,96 @@ search_terms:
- android-probes@mozilla.com
expires: 108
messaging:
message_shown:
type: event
description: |
A message was shown to the user.
extra_keys:
message_key:
description: The id of the message
type: string
bugs:
- https://github.com/mozilla-mobile/fenix/issues/24224
data_reviews:
- https://github.com/mozilla-mobile/fenix/issues/24224
notification_emails:
- android-probes@mozilla.com
data_sensitivity:
- interaction
expires: 114
message_dismissed:
type: event
description: |
A message was dismissed by the user.
extra_keys:
message_key:
description: The id of the message
type: string
bugs:
- https://github.com/mozilla-mobile/fenix/issues/24224
data_reviews:
- https://github.com/mozilla-mobile/fenix/issues/24224
notification_emails:
- android-probes@mozilla.com
data_sensitivity:
- interaction
expires: 114
message_clicked:
type: event
description: |
A message was clicked by the user.
extra_keys:
message_key:
description: The id of the message
type: string
action_uuid:
description: The uuid of the action
type: string
bugs:
- https://github.com/mozilla-mobile/fenix/issues/24224
data_reviews:
- https://github.com/mozilla-mobile/fenix/issues/24224
notification_emails:
- android-probes@mozilla.com
data_sensitivity:
- interaction
expires: 114
message_expired:
type: event
description: |
A message maxDisplayCount has been surpassed.
extra_keys:
message_key:
description: The id of the message
type: string
bugs:
- https://github.com/mozilla-mobile/fenix/issues/24224
data_reviews:
- https://github.com/mozilla-mobile/fenix/issues/24224
notification_emails:
- android-probes@mozilla.com
data_sensitivity:
- interaction
expires: 114
malformed:
type: event
description: |
A message was malformed.
extra_keys:
message_key:
description: The id of the message
type: string
bugs:
- https://github.com/mozilla-mobile/fenix/issues/24224
data_reviews:
- https://github.com/mozilla-mobile/fenix/issues/24224
notification_emails:
- android-probes@mozilla.com
data_sensitivity:
- interaction
expires: 114
wallpapers:
wallpaper_settings_opened:
type: event

View File

@ -21,6 +21,7 @@ import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.ReleaseChannel
import org.mozilla.fenix.components.metrics.AdjustMetricsService
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.GleanMetricsService
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.experiments.createNimbus
@ -124,6 +125,9 @@ class Analytics(
context = context,
metadataStorage = KeyPairMessageMetadataStorage(),
gleanPlumb = experiments,
reportMalformedMessage = {
metrics.track(Event.Messaging.MessageMalformed(it))
},
messagingFeature = FxNimbus.features.messaging,
)
}

View File

@ -619,6 +619,15 @@ sealed class Event {
data class WallpaperSwitched(val wallpaper: org.mozilla.fenix.wallpapers.Wallpaper) : Event()
data class ChangeWallpaperWithLogoToggled(val checked: Boolean) : Event()
sealed class Messaging(open val messageId: String) : Event() {
data class MessageShown(override val messageId: String) : Messaging(messageId)
data class MessageDismissed(override val messageId: String) : Messaging(messageId)
data class MessageClicked(override val messageId: String, val uuid: String?) :
Messaging(messageId)
data class MessageMalformed(override val messageId: String) : Messaging(messageId)
data class MessageExpired(override val messageId: String) : Messaging(messageId)
}
internal open val extras: Map<*, String>?
get() = null
}

View File

@ -56,6 +56,7 @@ import org.mozilla.fenix.GleanMetrics.ToolbarSettings
import org.mozilla.fenix.GleanMetrics.TopSites
import org.mozilla.fenix.GleanMetrics.VoiceSearch
import org.mozilla.fenix.GleanMetrics.Wallpapers
import org.mozilla.fenix.GleanMetrics.Messaging
import org.mozilla.fenix.ext.components
private class EventWrapper<T : Enum<T>>(
@ -834,6 +835,52 @@ private val Event.wrapper: EventWrapper<*>?
is Event.JumpBackInGroupTapped -> EventWrapper<NoExtraKeys>(
{ SearchTerms.jumpBackInGroupTapped.record(it) }
)
is Event.Messaging.MessageShown -> EventWrapper<NoExtraKeys>(
{
Messaging.messageShown.record(
Messaging.MessageShownExtra(
messageKey = this.messageId
)
)
}
)
is Event.Messaging.MessageClicked -> EventWrapper<NoExtraKeys>(
{
Messaging.messageClicked.record(
Messaging.MessageClickedExtra(
messageKey = this.messageId,
actionUuid = this.uuid
)
)
}
)
is Event.Messaging.MessageDismissed -> EventWrapper<NoExtraKeys>(
{
Messaging.messageDismissed.record(
Messaging.MessageDismissedExtra(
messageKey = this.messageId
)
)
}
)
is Event.Messaging.MessageMalformed -> EventWrapper<NoExtraKeys>(
{
Messaging.malformed.record(
Messaging.MalformedExtra(
messageKey = this.messageId
)
)
}
)
is Event.Messaging.MessageExpired -> EventWrapper<NoExtraKeys>(
{
Messaging.messageExpired.record(
Messaging.MessageExpiredExtra(
messageKey = this.messageId
)
)
}
)
is Event.WallpaperSettingsOpened -> EventWrapper<NoExtraKeys>(
{ Wallpapers.wallpaperSettingsOpened.record() }
)

View File

@ -14,33 +14,38 @@ import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageClicked
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageDismissed
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageDisplayed
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
/**
* Handles default interactions with the ui of GleanPlumb messages.
*/
class DefaultMessageController(
private val appStore: AppStore,
private val metrics: MetricController,
private val messagingStorage: NimbusMessagingStorage,
private val homeActivity: HomeActivity
) : MessageController {
override fun onMessagePressed(message: Message) {
// Report telemetry event
// This will be covered on https://github.com/mozilla-mobile/fenix/issues/24224
val action = messagingStorage.getMessageAction(message)
val result = messagingStorage.getMessageAction(message)
val uuid = result.first
val action = result.second
metrics.track(Event.Messaging.MessageClicked(message.id, uuid))
handleAction(action)
appStore.dispatch(MessageClicked(message))
}
override fun onMessageDismissed(message: Message) {
// Report telemetry event
// This will be covered on https://github.com/mozilla-mobile/fenix/issues/24224
metrics.track(Event.Messaging.MessageDismissed(message.id))
appStore.dispatch(MessageDismissed(message))
}
override fun onMessageDisplayed(message: Message) {
// Report telemetry event
// This will be covered on https://github.com/mozilla-mobile/fenix/issues/24224
if (message.data.maxDisplayCount <= message.metadata.displayCount + 1) {
metrics.track(Event.Messaging.MessageExpired(message.id))
}
metrics.track(Event.Messaging.MessageShown(message.id))
appStore.dispatch(MessageDisplayed(message))
}

View File

@ -12,7 +12,6 @@ import org.mozilla.experiments.nimbus.GleanPlumbInterface
import org.mozilla.experiments.nimbus.GleanPlumbMessageHelper
import org.mozilla.experiments.nimbus.internal.FeatureHolder
import org.mozilla.experiments.nimbus.internal.NimbusException
import org.mozilla.fenix.nimbus.Messaging
import org.mozilla.fenix.nimbus.StyleData
@ -22,6 +21,7 @@ import org.mozilla.fenix.nimbus.StyleData
class NimbusMessagingStorage(
private val context: Context,
private val metadataStorage: MessageMetadataStorage,
private val reportMalformedMessage: (String) -> Unit,
private val gleanPlumb: GleanPlumbInterface,
private val messagingFeature: FeatureHolder<Messaging>
) {
@ -45,14 +45,15 @@ class NimbusMessagingStorage(
}
return nimbusMessages.mapNotNull { (key, value) ->
val action = sanitizeAction(value.action, nimbusActions) ?: return@mapNotNull null
val action = sanitizeAction(key, value.action, nimbusActions) ?: return@mapNotNull null
Message(
id = key,
data = value,
action = action,
style = nimbusStyles[value.style] ?: defaultStyle,
metadata = storageMetadata[key] ?: addMetadata(key),
triggers = sanitizeTriggers(value.trigger, nimbusTriggers) ?: return@mapNotNull null
triggers = sanitizeTriggers(key, value.trigger, nimbusTriggers)
?: return@mapNotNull null
)
}.filter {
it.data.maxDisplayCount >= it.metadata.displayCount &&
@ -85,13 +86,13 @@ class NimbusMessagingStorage(
}
/**
* Returns a valid action for the provided [message].
* Returns a pair of uuid and valid action for the provided [message].
*/
fun getMessageAction(message: Message): String {
fun getMessageAction(message: Message): Pair<String?, String> {
val helper = gleanPlumb.createMessageHelper(customAttributes)
val uuid = helper.getUuid(message.action)
return helper.stringFormat(message.action, uuid)
return Pair(uuid, helper.stringFormat(message.action, uuid))
}
/**
@ -103,6 +104,7 @@ class NimbusMessagingStorage(
@VisibleForTesting
internal fun sanitizeAction(
messageId: String,
unsafeAction: String,
nimbusActions: Map<String, String>
): String? {
@ -111,6 +113,7 @@ class NimbusMessagingStorage(
} else {
val safeAction = nimbusActions[unsafeAction]
if (safeAction.isNullOrBlank() || safeAction.isEmpty()) {
reportMalformedMessage(messageId)
return null
}
safeAction
@ -119,12 +122,14 @@ class NimbusMessagingStorage(
@VisibleForTesting
internal fun sanitizeTriggers(
messageId: String,
unsafeTriggers: List<String>,
nimbusTriggers: Map<String, String>
): List<String>? {
return unsafeTriggers.map {
val safeTrigger = nimbusTriggers[it]
if (safeTrigger.isNullOrBlank() || safeTrigger.isEmpty()) {
reportMalformedMessage(messageId)
return null
}
safeTrigger
@ -155,8 +160,7 @@ class NimbusMessagingStorage(
try {
helper.evalJexl(condition)
} catch (e: NimbusException.EvaluationException) {
// Report to glean as malformed message
// Will be addressed on https://github.com/mozilla-mobile/fenix/issues/24224
reportMalformedMessage(message.id)
logger.info("Unable to evaluate $condition")
false
}

View File

@ -314,7 +314,8 @@ class HomeFragment : Fragment() {
messageController = DefaultMessageController(
appStore = components.appStore,
messagingStorage = components.analytics.messagingStorage,
homeActivity = activity
homeActivity = activity,
metrics = components.analytics.metrics
),
store = store,
tabCollectionStorage = components.core.tabCollectionStorage,

View File

@ -20,6 +20,8 @@ import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageClicked
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageDisplayed
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.nimbus.MessageData
@ -29,12 +31,15 @@ class DefaultMessageControllerTest {
private val activity: HomeActivity = mockk(relaxed = true)
private val storageNimbus: NimbusMessagingStorage = mockk(relaxed = true)
private lateinit var controller: DefaultMessageController
private lateinit var metrics: MetricController
private val store: AppStore = mockk(relaxed = true)
@Before
fun setup() {
metrics = mockk(relaxed = true)
controller = DefaultMessageController(
messagingStorage = storageNimbus,
metrics = metrics,
appStore = store,
homeActivity = activity
)
@ -43,12 +48,13 @@ class DefaultMessageControllerTest {
@Test
fun `WHEN calling onMessagePressed THEN update the store and handle the action`() {
val customController = spyk(controller)
every { customController.handleAction(any()) } returns mockk()
val message = mockMessage()
every { customController.handleAction(any()) } returns mockk()
every { storageNimbus.getMessageAction(message) } returns Pair("uuid", message.id)
customController.onMessagePressed(message)
verify { metrics.track(Event.Messaging.MessageClicked(message.id, "uuid")) }
verify { customController.handleAction(any()) }
verify { store.dispatch(MessageClicked(message)) }
}
@ -81,21 +87,25 @@ class DefaultMessageControllerTest {
controller.onMessageDismissed(message)
verify { metrics.track(Event.Messaging.MessageDismissed(message.id)) }
verify { store.dispatch(AppAction.MessagingAction.MessageDismissed(message)) }
}
@Test
fun `WHEN calling onMessageDisplayed THEN report to the messageManager`() {
val message = mockMessage()
val data = MessageData(_context = testContext, maxDisplayCount = 1)
val message = mockMessage(data)
controller.onMessageDisplayed(message)
verify { metrics.track(Event.Messaging.MessageExpired(message.id)) }
verify { metrics.track(Event.Messaging.MessageShown(message.id)) }
verify { store.dispatch(MessageDisplayed(message)) }
}
private fun mockMessage() = Message(
private fun mockMessage(data: MessageData = MessageData(_context = testContext)) = Message(
id = "id",
data = MessageData(_context = testContext),
data = data,
style = mockk(),
action = "action",
triggers = emptyList(),

View File

@ -10,8 +10,6 @@ import io.mockk.spyk
import io.mockk.unmockkObject
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.mock
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Rule
import org.junit.Test
@ -19,7 +17,6 @@ import org.mozilla.fenix.Config
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessageToShow
class MessagingFeatureTest {
@OptIn(ExperimentalCoroutinesApi::class)
@ -36,9 +33,6 @@ class MessagingFeatureTest {
binding.start()
store.dispatch(UpdateMessageToShow(mock()))
store.waitUntilIdle()
verify { store.dispatch(MessagingAction.Evaluate) }
unmockkObject(Config)

View File

@ -37,12 +37,16 @@ class NimbusMessagingStorageTest {
private lateinit var gleanPlumb: GleanPlumbInterface
private lateinit var messagingFeature: FeatureHolder<Messaging>
private lateinit var messaging: Messaging
private var malformedWasReported = false
private val reportMalformedMessage: (String) -> Unit = {
malformedWasReported = true
}
@Before
fun setup() {
gleanPlumb = mockk(relaxed = true)
metadataStorage = mockk(relaxed = true)
malformedWasReported = false
messagingFeature = createMessagingFeature()
every { metadataStorage.getMetadata() } returns listOf(Message.Metadata(id = "message-1"))
@ -50,6 +54,7 @@ class NimbusMessagingStorageTest {
storage = NimbusMessagingStorage(
testContext,
metadataStorage,
reportMalformedMessage,
gleanPlumb,
messagingFeature
)
@ -86,6 +91,7 @@ class NimbusMessagingStorageTest {
val storage = NimbusMessagingStorage(
testContext,
metadataStorage,
reportMalformedMessage,
gleanPlumb,
messagingFeature
)
@ -121,6 +127,7 @@ class NimbusMessagingStorageTest {
val storage = NimbusMessagingStorage(
testContext,
metadataStorage,
reportMalformedMessage,
gleanPlumb,
messagingFeature
)
@ -155,6 +162,7 @@ class NimbusMessagingStorageTest {
val storage = NimbusMessagingStorage(
testContext,
metadataStorage,
reportMalformedMessage,
gleanPlumb,
messagingFeature
)
@ -192,6 +200,7 @@ class NimbusMessagingStorageTest {
val storage = NimbusMessagingStorage(
testContext,
metadataStorage,
reportMalformedMessage,
gleanPlumb,
messagingFeature
)
@ -210,19 +219,21 @@ class NimbusMessagingStorageTest {
assertEquals("message-1", firstMessage.id)
assertEquals("message-1", firstMessage.metadata.id)
assertTrue(messages.size == 1)
assertTrue(malformedWasReported)
}
@Test
fun `GIVEN a malformed action WHEN calling sanitizeAction THEN return null`() {
val actionsMap = mapOf("action-1" to "action-1-url")
val notFoundAction = storage.sanitizeAction("no-found-action", actionsMap)
val emptyAction = storage.sanitizeAction("", actionsMap)
val blankAction = storage.sanitizeAction(" ", actionsMap)
val notFoundAction = storage.sanitizeAction("messageId", "no-found-action", actionsMap)
val emptyAction = storage.sanitizeAction("messageId", "", actionsMap)
val blankAction = storage.sanitizeAction("messageId", " ", actionsMap)
assertNull(notFoundAction)
assertNull(emptyAction)
assertNull(blankAction)
assertTrue(malformedWasReported)
}
@Test
@ -237,7 +248,7 @@ class NimbusMessagingStorageTest {
fun `GIVEN a valid action WHEN calling sanitizeAction THEN return the action`() {
val actionsMap = mapOf("action-1" to "action-1-url")
val validAction = storage.sanitizeAction("action-1", actionsMap)
val validAction = storage.sanitizeAction("messageId", "action-1", actionsMap)
assertEquals("action-1-url", validAction)
}
@ -246,20 +257,22 @@ class NimbusMessagingStorageTest {
fun `GIVEN a trigger action WHEN calling sanitizeTriggers THEN return null`() {
val triggersMap = mapOf("trigger-1" to "trigger-1-expression")
val notFoundTrigger = storage.sanitizeTriggers(listOf("no-found-trigger"), triggersMap)
val emptyTrigger = storage.sanitizeTriggers(listOf(""), triggersMap)
val blankTrigger = storage.sanitizeTriggers(listOf(" "), triggersMap)
val notFoundTrigger =
storage.sanitizeTriggers("messageId", listOf("no-found-trigger"), triggersMap)
val emptyTrigger = storage.sanitizeTriggers("messageId", listOf(""), triggersMap)
val blankTrigger = storage.sanitizeTriggers("messageId", listOf(" "), triggersMap)
assertNull(notFoundTrigger)
assertNull(emptyTrigger)
assertNull(blankTrigger)
assertTrue(malformedWasReported)
}
@Test
fun `GIVEN a valid trigger WHEN calling sanitizeAction THEN return the trigger`() {
val triggersMap = mapOf("trigger-1" to "trigger-1-expression")
val validTrigger = storage.sanitizeTriggers(listOf("trigger-1"), triggersMap)
val validTrigger = storage.sanitizeTriggers("messageId", listOf("trigger-1"), triggersMap)
assertEquals(listOf("trigger-1-expression"), validTrigger)
}