Add defaults to FML, add control behaviour

upstream-sync
jhugman 2 years ago committed by mergify[bot]
parent 480ab3dca7
commit 2b36ca75bf

@ -1,81 +0,0 @@
{
"default-browser-message": {
"description": "A small feature allowing experiments on the placement of a default browser message.",
"hasExposure": true,
"exposureDescription": "",
"variables": {
"message-location": {
"type": "string",
"description": "Where is the message to be put."
}
}
},
"homescreen": {
"description": "The homescreen that the user goes to when they press home or new tab.",
"hasExposure": true,
"exposureDescription": "",
"variables": {
"sections-enabled": {
"type": "json",
"description": "This property provides a lookup table of whether or not the given section should be enabled. If the section is enabled, it should be toggleable in the settings screen, and on by default."
}
}
},
"messaging": {
"description": "Configuration for the messaging system.\n\nIn practice this is a set of growable lookup tables for the\nmessage controller to piece together.\n",
"hasExposure": true,
"exposureDescription": "",
"variables": {
"actions": {
"type": "json",
"description": "A growable map of action URLs."
},
"message-under-experiment": {
"type": "string",
"description": "Id or prefix of the message under experiment."
},
"messages": {
"type": "json",
"description": "A growable collection of messages"
},
"styles": {
"type": "json",
"description": "A map of styles to configure message appearance.\n"
},
"triggers": {
"type": "json",
"description": "A collection of out the box trigger expressions. Each entry maps to a valid JEXL expression.\n"
}
}
},
"nimbus-validation": {
"description": "A feature that does not correspond to an application feature suitable for showing that Nimbus is working. This should never be used in production.",
"hasExposure": true,
"exposureDescription": "",
"variables": {
"settings-icon": {
"type": "string",
"description": "The drawable displayed in the app menu for Settings"
},
"settings-punctuation": {
"type": "string",
"description": "The emoji displayed in the Settings screen title."
},
"settings-title": {
"type": "string",
"description": "The title of displayed in the Settings screen and app menu."
}
}
},
"search-term-groups": {
"description": "A feature allowing the grouping of URLs around the search term that it came from.",
"hasExposure": true,
"exposureDescription": "",
"variables": {
"enabled": {
"type": "boolean",
"description": "If true, the feature shows up on the homescreen and on the new tab screen."
}
}
}
}

@ -0,0 +1,56 @@
---
default-browser-message:
description: A small feature allowing experiments on the placement of a default browser message.
hasExposure: true
exposureDescription: ""
variables:
message-location:
type: string
description: Where is the message to be put.
homescreen:
description: The homescreen that the user goes to when they press home or new tab.
hasExposure: true
exposureDescription: ""
variables:
sections-enabled:
type: json
description: "This property provides a lookup table of whether or not the given section should be enabled. If the section is enabled, it should be toggleable in the settings screen, and on by default."
messaging:
description: "Configuration for the messaging system.\n\nIn practice this is a set of growable lookup tables for the\nmessage controller to piece together.\n"
hasExposure: true
exposureDescription: ""
variables:
actions:
type: json
description: A growable map of action URLs.
messages:
type: json
description: A growable collection of messages
styles:
type: json
description: "A map of styles to configure message appearance.\n"
triggers:
type: json
description: "A collection of out the box trigger expressions. Each entry maps to a valid JEXL expression.\n"
nimbus-validation:
description: A feature that does not correspond to an application feature suitable for showing that Nimbus is working. This should never be used in production.
hasExposure: true
exposureDescription: ""
variables:
settings-icon:
type: string
description: The drawable displayed in the app menu for Settings
settings-punctuation:
type: string
description: The emoji displayed in the Settings screen title.
settings-title:
type: string
description: The title of displayed in the Settings screen and app menu.
search-term-groups:
description: A feature allowing the grouping of URLs around the search term that it came from.
hasExposure: true
exposureDescription: ""
variables:
enabled:
type: boolean
description: "If true, the feature shows up on the homescreen and on the new tab screen."

@ -26,6 +26,7 @@ import org.mozilla.fenix.components.metrics.GleanMetricsService
import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.experiments.createNimbus import org.mozilla.fenix.experiments.createNimbus
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.gleanplumb.CustomAttributeProvider
import org.mozilla.fenix.gleanplumb.OnDiskMessageMetadataStorage import org.mozilla.fenix.gleanplumb.OnDiskMessageMetadataStorage
import org.mozilla.fenix.gleanplumb.NimbusMessagingStorage import org.mozilla.fenix.gleanplumb.NimbusMessagingStorage
import org.mozilla.fenix.nimbus.FxNimbus import org.mozilla.fenix.nimbus.FxNimbus
@ -137,6 +138,7 @@ class Analytics(
metrics.track(Event.Messaging.MessageMalformed(it)) metrics.track(Event.Messaging.MessageMalformed(it))
}, },
messagingFeature = FxNimbus.features.messaging, messagingFeature = FxNimbus.features.messaging,
attributeProvider = CustomAttributeProvider,
) )
} }
} }

@ -0,0 +1,34 @@
/* 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.gleanplumb
import android.content.Context
import org.json.JSONObject
import org.mozilla.fenix.utils.BrowsersCache
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.Calendar
/**
* Custom attributes that the messaging framework will use to evaluate if message is eligible
* to be shown.
*/
object CustomAttributeProvider {
private val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.US)
/**
* Returns a [JSONObject] that contains all the custom attributes, evaluated when the function
* was called.
*/
fun getCustomAttributes(context: Context): JSONObject {
val now = Calendar.getInstance()
return JSONObject(
mapOf(
"is_default_browser" to BrowsersCache.all(context).isDefaultBrowser,
"date_string" to formatter.format(now.time)
)
)
}
}

@ -42,7 +42,7 @@ class DefaultMessageController(
} }
override fun onMessageDisplayed(message: Message) { override fun onMessageDisplayed(message: Message) {
if (message.data.maxDisplayCount <= message.metadata.displayCount + 1) { if (message.maxDisplayCount <= message.metadata.displayCount + 1) {
metrics.track(Event.Messaging.MessageExpired(message.id)) metrics.track(Event.Messaging.MessageExpired(message.id))
} }
metrics.track(Event.Messaging.MessageShown(message.id)) metrics.track(Event.Messaging.MessageShown(message.id))

@ -27,6 +27,12 @@ data class Message(
val triggers: List<String>, val triggers: List<String>,
val metadata: Metadata val metadata: Metadata
) { ) {
val maxDisplayCount: Int
get() = style.maxDisplayCount
val priority: Int
get() = style.priority
/** /**
* A data class that holds metadata that help to identify if a message should shown. * A data class that holds metadata that help to identify if a message should shown.
* *

@ -12,6 +12,7 @@ import org.mozilla.experiments.nimbus.GleanPlumbInterface
import org.mozilla.experiments.nimbus.GleanPlumbMessageHelper import org.mozilla.experiments.nimbus.GleanPlumbMessageHelper
import org.mozilla.experiments.nimbus.internal.FeatureHolder import org.mozilla.experiments.nimbus.internal.FeatureHolder
import org.mozilla.experiments.nimbus.internal.NimbusException import org.mozilla.experiments.nimbus.internal.NimbusException
import org.mozilla.fenix.nimbus.ControlMessageBehavior
import org.mozilla.fenix.nimbus.Messaging import org.mozilla.fenix.nimbus.Messaging
import org.mozilla.fenix.nimbus.StyleData import org.mozilla.fenix.nimbus.StyleData
@ -23,12 +24,13 @@ class NimbusMessagingStorage(
private val metadataStorage: MessageMetadataStorage, private val metadataStorage: MessageMetadataStorage,
private val reportMalformedMessage: (String) -> Unit, private val reportMalformedMessage: (String) -> Unit,
private val gleanPlumb: GleanPlumbInterface, private val gleanPlumb: GleanPlumbInterface,
private val messagingFeature: FeatureHolder<Messaging> private val messagingFeature: FeatureHolder<Messaging>,
private val attributeProvider: CustomAttributeProvider? = null
) { ) {
private val logger = Logger("MessagingStorage") private val logger = Logger("MessagingStorage")
private val nimbusFeature = messagingFeature.value() private val nimbusFeature = messagingFeature.value()
private val customAttributes: JSONObject private val customAttributes: JSONObject
get() = JSONObject() get() = attributeProvider?.getCustomAttributes(context) ?: JSONObject()
/** /**
* Returns a list of available messages descending sorted by their priority. * Returns a list of available messages descending sorted by their priority.
@ -54,7 +56,7 @@ class NimbusMessagingStorage(
?: return@mapNotNull null ?: return@mapNotNull null
) )
}.filter { }.filter {
it.data.maxDisplayCount >= it.metadata.displayCount && it.maxDisplayCount >= it.metadata.displayCount &&
!it.metadata.dismissed && !it.metadata.dismissed &&
!it.metadata.pressed !it.metadata.pressed
}.sortedByDescending { }.sortedByDescending {
@ -66,21 +68,33 @@ class NimbusMessagingStorage(
* Returns the next higher priority message which all their triggers are true. * Returns the next higher priority message which all their triggers are true.
*/ */
fun getNextMessage(availableMessages: List<Message>): Message? { fun getNextMessage(availableMessages: List<Message>): Message? {
val jexlCache = HashMap<String, Boolean>()
val helper = gleanPlumb.createMessageHelper(customAttributes) val helper = gleanPlumb.createMessageHelper(customAttributes)
var message = availableMessages.firstOrNull { val message = availableMessages.firstOrNull {
isMessageEligible(it, helper) isMessageEligible(it, helper, jexlCache)
} ?: return null } ?: return null
if (isMessageUnderExperiment(message, nimbusFeature.messageUnderExperiment)) { // Check this isn't an experimental message. If not, we can go ahead and return it.
messagingFeature.recordExposure() if (!isMessageUnderExperiment(message, nimbusFeature.messageUnderExperiment)) {
return message
}
// If the message is under experiment, then we need to record the exposure
messagingFeature.recordExposure()
if (message.data.isControl) { // If this is an experimental message, but not a placebo, then just return the message.
message = availableMessages.firstOrNull { return if (!message.data.isControl) {
!it.data.isControl && isMessageEligible(it, helper) message
} ?: return null } else {
// This is a control, so we need to either return the next message (there may not be one)
// or not display anything.
when (getOnControlBehavior()) {
ControlMessageBehavior.SHOW_NEXT_MESSAGE -> availableMessages.firstOrNull {
// There should only be one control message, and we've just detected it.
!it.data.isControl && isMessageEligible(it, helper, jexlCache)
}
ControlMessageBehavior.SHOW_NONE -> null
} }
} }
return message
} }
/** /**
@ -136,7 +150,7 @@ class NimbusMessagingStorage(
@VisibleForTesting @VisibleForTesting
internal fun isMessageUnderExperiment(message: Message, expression: String?): Boolean { internal fun isMessageUnderExperiment(message: Message, expression: String?): Boolean {
return when { return message.data.isControl || when {
expression.isNullOrBlank() -> { expression.isNullOrBlank() -> {
false false
} }
@ -152,19 +166,26 @@ class NimbusMessagingStorage(
@VisibleForTesting @VisibleForTesting
internal fun isMessageEligible( internal fun isMessageEligible(
message: Message, message: Message,
helper: GleanPlumbMessageHelper helper: GleanPlumbMessageHelper,
jexlCache: MutableMap<String, Boolean> = mutableMapOf()
): Boolean { ): Boolean {
return message.triggers.all { condition -> return message.triggers.all { condition ->
try { jexlCache[condition]
helper.evalJexl(condition) ?: try {
} catch (e: NimbusException.EvaluationException) { helper.evalJexl(condition).also { result ->
reportMalformedMessage(message.id) jexlCache[condition] = result
logger.info("Unable to evaluate $condition") }
false } catch (e: NimbusException.EvaluationException) {
} reportMalformedMessage(message.id)
logger.info("Unable to evaluate $condition")
false
}
} }
} }
@VisibleForTesting
internal fun getOnControlBehavior(): ControlMessageBehavior = nimbusFeature.onControl
private suspend fun addMetadata(id: String): Message.Metadata { private suspend fun addMetadata(id: String): Message.Metadata {
return metadataStorage.addMetadata( return metadataStorage.addMetadata(
Message.Metadata( Message.Metadata(

@ -73,7 +73,7 @@ class MessagingMiddleware(
val newMessage = oldMessage.copy( val newMessage = oldMessage.copy(
metadata = newMetadata metadata = newMetadata
) )
val newMessages = if (newMetadata.displayCount < oldMessage.data.maxDisplayCount) { val newMessages = if (newMetadata.displayCount < oldMessage.maxDisplayCount) {
updateMessage(context, oldMessage, newMessage) updateMessage(context, oldMessage, newMessage)
} else { } else {
consumeMessageToShowIfNeeded(context, oldMessage) consumeMessageToShowIfNeeded(context, oldMessage)

@ -93,7 +93,7 @@ class DefaultMessageControllerTest {
@Test @Test
fun `WHEN calling onMessageDisplayed THEN report to the messageManager`() { fun `WHEN calling onMessageDisplayed THEN report to the messageManager`() {
val data = MessageData(_context = testContext, maxDisplayCount = 1) val data = MessageData(_context = testContext)
val message = mockMessage(data) val message = mockMessage(data)
controller.onMessageDisplayed(message) controller.onMessageDisplayed(message)
@ -106,7 +106,7 @@ class DefaultMessageControllerTest {
private fun mockMessage(data: MessageData = MessageData(_context = testContext)) = Message( private fun mockMessage(data: MessageData = MessageData(_context = testContext)) = Message(
id = "id", id = "id",
data = data, data = data,
style = mockk(), style = mockk(relaxed = true),
action = "action", action = "action",
triggers = emptyList(), triggers = emptyList(),
metadata = Message.Metadata( metadata = Message.Metadata(

@ -9,9 +9,7 @@ import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.spyk import io.mockk.spyk
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
@ -26,6 +24,7 @@ import org.mozilla.experiments.nimbus.internal.FeatureHolder
import org.mozilla.experiments.nimbus.internal.NimbusException import org.mozilla.experiments.nimbus.internal.NimbusException
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.nimbus.ControlMessageBehavior.SHOW_NEXT_MESSAGE
import org.mozilla.fenix.nimbus.MessageData import org.mozilla.fenix.nimbus.MessageData
import org.mozilla.fenix.nimbus.Messaging import org.mozilla.fenix.nimbus.Messaging
import org.mozilla.fenix.nimbus.StyleData import org.mozilla.fenix.nimbus.StyleData
@ -40,7 +39,6 @@ class NimbusMessagingStorageTest {
private lateinit var gleanPlumb: GleanPlumbInterface private lateinit var gleanPlumb: GleanPlumbInterface
private lateinit var messagingFeature: FeatureHolder<Messaging> private lateinit var messagingFeature: FeatureHolder<Messaging>
private lateinit var messaging: Messaging private lateinit var messaging: Messaging
private val coroutineScope = TestCoroutineScope()
private var malformedWasReported = false private var malformedWasReported = false
private val reportMalformedMessage: (String) -> Unit = { private val reportMalformedMessage: (String) -> Unit = {
malformedWasReported = true malformedWasReported = true
@ -195,13 +193,12 @@ class NimbusMessagingStorageTest {
) )
val messages = mapOf( val messages = mapOf(
"shown-many-times-message" to createMessageData( "shown-many-times-message" to createMessageData(
style = "high-priority", style = "high-priority"
maxDisplayCount = 2
), ),
"normal-message" to createMessageData(style = "high-priority"), "normal-message" to createMessageData(style = "high-priority"),
) )
val styles = mapOf( val styles = mapOf(
"high-priority" to createStyle(priority = 100), "high-priority" to createStyle(priority = 100, maxDisplayCount = 2),
) )
val metadataStorage: MessageMetadataStorage = mockk(relaxed = true) val metadataStorage: MessageMetadataStorage = mockk(relaxed = true)
val messagingFeature = createMessagingFeature( val messagingFeature = createMessagingFeature(
@ -253,7 +250,7 @@ class NimbusMessagingStorageTest {
@Test @Test
fun `WHEN calling updateMetadata THEN delegate to metadataStorage`() = runBlockingTest { fun `WHEN calling updateMetadata THEN delegate to metadataStorage`() = runBlockingTest {
storage.updateMetadata(mockk()) storage.updateMetadata(mockk(relaxed = true))
coEvery { metadataStorage.updateMetadata(any()) } coEvery { metadataStorage.updateMetadata(any()) }
} }
@ -294,9 +291,10 @@ class NimbusMessagingStorageTest {
@Test @Test
fun `GIVEN a null or black expression WHEN calling isMessageUnderExperiment THEN return false`() { fun `GIVEN a null or black expression WHEN calling isMessageUnderExperiment THEN return false`() {
val message = Message( val message = Message(
"id", mockk(), "id",
mockk(relaxed = true),
action = "action", action = "action",
mock(), mockk(relaxed = true),
emptyList(), emptyList(),
Message.Metadata("id") Message.Metadata("id")
) )
@ -309,9 +307,10 @@ class NimbusMessagingStorageTest {
@Test @Test
fun `GIVEN messages id that ends with - WHEN calling isMessageUnderExperiment THEN return true`() { fun `GIVEN messages id that ends with - WHEN calling isMessageUnderExperiment THEN return true`() {
val message = Message( val message = Message(
"end-", mockk(), "end-",
mockk(relaxed = true),
action = "action", action = "action",
mock(), mockk(relaxed = true),
emptyList(), emptyList(),
Message.Metadata("end-") Message.Metadata("end-")
) )
@ -324,9 +323,10 @@ class NimbusMessagingStorageTest {
@Test @Test
fun `GIVEN message under experiment WHEN calling isMessageUnderExperiment THEN return true`() { fun `GIVEN message under experiment WHEN calling isMessageUnderExperiment THEN return true`() {
val message = Message( val message = Message(
"same-id", mockk(), "same-id",
mockk(relaxed = true),
action = "action", action = "action",
mock(), mockk(relaxed = true),
emptyList(), emptyList(),
Message.Metadata("same-id") Message.Metadata("same-id")
) )
@ -340,9 +340,10 @@ class NimbusMessagingStorageTest {
fun `GIVEN an eligible message WHEN calling isMessageEligible THEN return true`() { fun `GIVEN an eligible message WHEN calling isMessageEligible THEN return true`() {
val helper: GleanPlumbMessageHelper = mockk(relaxed = true) val helper: GleanPlumbMessageHelper = mockk(relaxed = true)
val message = Message( val message = Message(
"same-id", mockk(), "same-id",
mockk(relaxed = true),
action = "action", action = "action",
mock(), mockk(relaxed = true),
listOf("trigger"), listOf("trigger"),
Message.Metadata("same-id") Message.Metadata("same-id")
) )
@ -358,9 +359,10 @@ class NimbusMessagingStorageTest {
fun `GIVEN a malformed trigger WHEN calling isMessageEligible THEN return false`() { fun `GIVEN a malformed trigger WHEN calling isMessageEligible THEN return false`() {
val helper: GleanPlumbMessageHelper = mockk(relaxed = true) val helper: GleanPlumbMessageHelper = mockk(relaxed = true)
val message = Message( val message = Message(
"same-id", mockk(), "same-id",
mockk(relaxed = true),
action = "action", action = "action",
mock(), mockk(relaxed = true),
listOf("trigger"), listOf("trigger"),
Message.Metadata("same-id") Message.Metadata("same-id")
) )
@ -376,9 +378,10 @@ class NimbusMessagingStorageTest {
fun `GIVEN none available messages are eligible WHEN calling getNextMessage THEN return null`() { fun `GIVEN none available messages are eligible WHEN calling getNextMessage THEN return null`() {
val spiedStorage = spyk(storage) val spiedStorage = spyk(storage)
val message = Message( val message = Message(
"same-id", mockk(), "same-id",
mockk(relaxed = true),
action = "action", action = "action",
mock(), mockk(relaxed = true),
listOf("trigger"), listOf("trigger"),
Message.Metadata("same-id") Message.Metadata("same-id")
) )
@ -394,9 +397,10 @@ class NimbusMessagingStorageTest {
fun `GIVEN an eligible message WHEN calling getNextMessage THEN return the message`() { fun `GIVEN an eligible message WHEN calling getNextMessage THEN return the message`() {
val spiedStorage = spyk(storage) val spiedStorage = spyk(storage)
val message = Message( val message = Message(
"same-id", mockk(), "same-id",
mockk(relaxed = true),
action = "action", action = "action",
mock(), mockk(relaxed = true),
listOf("trigger"), listOf("trigger"),
Message.Metadata("same-id") Message.Metadata("same-id")
) )
@ -441,6 +445,7 @@ class NimbusMessagingStorageTest {
val controlMessageData: MessageData = mockk(relaxed = true) val controlMessageData: MessageData = mockk(relaxed = true)
every { messageData.isControl } returns false every { messageData.isControl } returns false
every { spiedStorage.getOnControlBehavior() } returns SHOW_NEXT_MESSAGE
every { controlMessageData.isControl } returns true every { controlMessageData.isControl } returns true
val message = Message( val message = Message(
@ -473,14 +478,12 @@ class NimbusMessagingStorageTest {
private fun createMessageData( private fun createMessageData(
action: String = "action-1", action: String = "action-1",
style: String = "style-1", style: String = "style-1",
triggers: List<String> = listOf("trigger-1"), triggers: List<String> = listOf("trigger-1")
maxDisplayCount: Int = 5
): MessageData { ): MessageData {
val messageData1: MessageData = mockk(relaxed = true) val messageData1: MessageData = mockk(relaxed = true)
every { messageData1.action } returns action every { messageData1.action } returns action
every { messageData1.style } returns style every { messageData1.style } returns style
every { messageData1.trigger } returns triggers every { messageData1.trigger } returns triggers
every { messageData1.maxDisplayCount } returns maxDisplayCount
return messageData1 return messageData1
} }
@ -506,9 +509,10 @@ class NimbusMessagingStorageTest {
return messagingFeature return messagingFeature
} }
private fun createStyle(priority: Int = 1): StyleData { private fun createStyle(priority: Int = 1, maxDisplayCount: Int = 5): StyleData {
val style1: StyleData = mockk(relaxed = true) val style1: StyleData = mockk(relaxed = true)
every { style1.priority } returns priority every { style1.priority } returns priority
every { style1.maxDisplayCount } returns maxDisplayCount
return style1 return style1
} }
} }

@ -37,6 +37,7 @@ import org.mozilla.fenix.gleanplumb.MessagingState
import org.mozilla.fenix.gleanplumb.NimbusMessagingStorage import org.mozilla.fenix.gleanplumb.NimbusMessagingStorage
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.nimbus.MessageData import org.mozilla.fenix.nimbus.MessageData
import org.mozilla.fenix.nimbus.StyleData
@RunWith(FenixRobolectricTestRunner::class) @RunWith(FenixRobolectricTestRunner::class)
class MessagingMiddlewareTest { class MessagingMiddlewareTest {
@ -276,12 +277,13 @@ class MessagingMiddlewareTest {
@Test @Test
fun `GIVEN a message with that not surpassed the maxDisplayCount WHEN onMessagedDisplayed THEN update the available messages and the updateMetadata`() { fun `GIVEN a message with that not surpassed the maxDisplayCount WHEN onMessagedDisplayed THEN update the available messages and the updateMetadata`() {
val style: StyleData = mockk(relaxed = true)
val oldMessageData: MessageData = mockk(relaxed = true) val oldMessageData: MessageData = mockk(relaxed = true)
val oldMessage = Message( val oldMessage = Message(
"oldMessage", "oldMessage",
oldMessageData, oldMessageData,
action = "action", action = "action",
mockk(relaxed = true), style,
listOf("trigger"), listOf("trigger"),
Message.Metadata("same-id", displayCount = 0) Message.Metadata("same-id", displayCount = 0)
) )
@ -289,7 +291,7 @@ class MessagingMiddlewareTest {
val spiedMiddleware = spyk(middleware) val spiedMiddleware = spyk(middleware)
every { spiedMiddleware.now() } returns 0 every { spiedMiddleware.now() } returns 0
every { oldMessageData.maxDisplayCount } returns 2 every { style.maxDisplayCount } returns 2
every { every {
spiedMiddleware.updateMessage( spiedMiddleware.updateMessage(
middlewareContext, middlewareContext,
@ -307,12 +309,13 @@ class MessagingMiddlewareTest {
@Test @Test
fun `GIVEN a message with that surpassed the maxDisplayCount WHEN onMessagedDisplayed THEN remove the message and consume it`() { fun `GIVEN a message with that surpassed the maxDisplayCount WHEN onMessagedDisplayed THEN remove the message and consume it`() {
val style: StyleData = mockk(relaxed = true)
val oldMessageData: MessageData = mockk(relaxed = true) val oldMessageData: MessageData = mockk(relaxed = true)
val oldMessage = Message( val oldMessage = Message(
"oldMessage", "oldMessage",
oldMessageData, oldMessageData,
action = "action", action = "action",
mockk(relaxed = true), style,
listOf("trigger"), listOf("trigger"),
Message.Metadata("same-id", displayCount = 0) Message.Metadata("same-id", displayCount = 0)
) )
@ -320,7 +323,7 @@ class MessagingMiddlewareTest {
val spiedMiddleware = spyk(middleware) val spiedMiddleware = spyk(middleware)
every { spiedMiddleware.now() } returns 0 every { spiedMiddleware.now() } returns 0
every { oldMessageData.maxDisplayCount } returns 1 every { style.maxDisplayCount } returns 1
every { every {
spiedMiddleware.consumeMessageToShowIfNeeded( spiedMiddleware.consumeMessageToShowIfNeeded(
middlewareContext, middlewareContext,

@ -94,51 +94,77 @@ features:
expressions. Each entry maps to a expressions. Each entry maps to a
valid JEXL expression. valid JEXL expression.
type: Map<String, String> type: Map<String, String>
default: default: {}
english-speaking: "'en' in locale"
ALWAYS: "true"
NEW_USER: "days_since_install < 7"
styles: styles:
description: > description: >
A map of styles to configure message A map of styles to configure message
appearance. appearance.
type: Map<String, StyleData> type: Map<String, StyleData>
default: default: {}
urgent:
background-color: red
text-color: white
button-background: bright-blue
button-text-color: white
priority: 70
warning:
background-color: cyan,
text-color: black,
button-background: bright-blue
button-text-color: white
priority: 55
default:
background-color: blue
text-color: white
button-background: bright-blue
button-text-color: white
priority: 50
excited:
background-color: blue
text-color: white
button-background: bright-blue
button-text-color: white
priority: 60
actions: actions:
type: Map<String, String> type: Map<String, String>
description: A growable map of action URLs. description: A growable map of action URLs.
default: default: {}
OPEN_SYNC_SETTINGS: firefox://settings/sync on-control:
OPEN_POCKET_SETTINGS: https:///getpocket.com/settings?fxa={fxa-token} type: ControlMessageBehavior
OPEN_SETTINGS: ://settings description: What should be displayed when a control message is selected.
default: show-next-message
defaults: defaults:
- value:
triggers:
USER_RECENTLY_INSTALLED: days_since_install < 7
USER_RECENTLY_UPDATED: days_since_update < 7 && days_since_install != days_since_update
USER_TIER_ONE_COUNTRY: ('US' in locale || 'GB' in locale || 'CA' in locale || 'DE' in locale || 'FR' in locale)
USER_EN_SPEAKER: "'en' in locale"
USER_DE_SPEAKER: "'de' in locale"
USER_FR_SPEAKER: "'fr' in locale"
DEVICE_ANDROID: os == 'Android'
DEVICE_IOS: os == 'iOS'
ALWAYS: "true"
NEVER: "false"
actions:
ENABLE_PRIVATE_BROWSING: ://enable_private_browsing
INSTALL_SEARCH_WIDGET: ://install_search_widget
MAKE_DEFAULT_BROWSER: ://make_default_browser
VIEW_BOOKMARKS: ://urls_bookmarks
VIEW_COLLECTIONS: ://home_collections
VIEW_HISTORY: ://urls_history
VIEW_HOMESCREEN: ://home
OPEN_SETTINGS_ACCESSIBILITY: ://settings_accessibility
OPEN_SETTINGS_ADDON_MANAGER: ://settings_addon_manager
OPEN_SETTINGS_DELETE_BROWSING_DATA: ://settings_delete_browsing_data
OPEN_SETTINGS_LOGINS: ://settings_logins
OPEN_SETTINGS_NOTIFICATIONS: ://settings_notifications
OPEN_SETTINGS_PRIVACY: ://settings_privacy
OPEN_SETTINGS_SEARCH_ENGINE: ://settings_search_engine
OPEN_SETTINGS_TRACKING_PROTECTION: ://settings_tracking_protection
OPEN_SETTINGS_WALLPAPERS: ://settings_wallpapers
OPEN_SETTINGS: ://settings
TURN_ON_SYNC: ://turn_on_sync
styles:
DEFAULT:
priority: 50
max-display-count: 5
PERSISTENT:
priority: 50
max-display-count: 20
WARNING:
priority: 60
max-display-count: 10
URGENT:
priority: 100
max-display-count: 10
- channel: developer
value:
styles:
DEFAULT:
priority: 50
max-display-count: 100
EXPIRES_QUICKLY:
priority: 100
max-display-count: 1
- channel: developer - channel: developer
value: { value: {
"messages": { "messages": {
@ -147,13 +173,10 @@ features:
"text": "Love Firefox? Fill in our survey!", "text": "Love Firefox? Fill in our survey!",
"action": "https://surveyprovider.com/survey-id/{uuid}", "action": "https://surveyprovider.com/survey-id/{uuid}",
"trigger": [ "ALWAYS" ], "trigger": [ "ALWAYS" ],
"max-display-count": 5, "style": "DEFAULT",
"style": "warning",
"button-label": "Go to the survey" "button-label": "Go to the survey"
} }
}, }
"message-under-experiment": "my-viewpoint-survey"
} }
- channel: developer - channel: developer
value: { value: {
@ -161,12 +184,9 @@ features:
"private-tabs-auto-close": { "private-tabs-auto-close": {
"action": "OPEN_SETTINGS", "action": "OPEN_SETTINGS",
"text": "Sharing your phone? Autoclosing private tabs is for you!", "text": "Sharing your phone? Autoclosing private tabs is for you!",
"style": "warning",
"trigger": [ "trigger": [
"NEW_USER", "USER_RECENTLY_INSTALLED"
"first-private-tabs-opened" ]
],
"max-display-count": 5
} }
}, },
@ -175,23 +195,15 @@ features:
- channel: developer - channel: developer
value: { value: {
"triggers": { "triggers": {
"ireland": "'IE' in locale" "USER_IE_COUNTRY": "'IE' in locale"
}, },
"styles": { "styles": {
"irish-green": { "irish-green": {
"background-color": "green",
"text-color": "dark-green",
"button-background": "foo",
"button-text-color": "very-green",
"priority": 50 "priority": 50
} }
}, },
"actions": {
"OPEN_SETTINGS": "://settings"
},
"messages": { "messages": {
"eu-tracking-protection-for-ireland": { "eu-tracking-protection-for-ireland": {
"action": "OPEN_SETTINGS", "action": "OPEN_SETTINGS",
@ -199,9 +211,8 @@ features:
"style": "irish-green", "style": "irish-green",
"trigger": [ "trigger": [
"NEW_USER", "NEW_USER",
"ireland" "USER_IE_COUNTRY"
], ]
"max-display-count": 5
} }
}, },
@ -220,7 +231,7 @@ types:
and call to action. and call to action.
fields: fields:
action: action:
type: String type: Text
description: > description: >
A URL of a page or a deeplink. A URL of a page or a deeplink.
This may have substitution variables in. This may have substitution variables in.
@ -239,7 +250,6 @@ types:
type: Boolean type: Boolean
description: "Indicates if this message is the control message, if true shouldn't be displayed" description: "Indicates if this message is the control message, if true shouldn't be displayed"
default: false default: false
button-label: button-label:
type: Option<Text> type: Option<Text>
description: > description: >
@ -251,20 +261,7 @@ types:
description: > description: >
The style as described in a The style as described in a
`StyleData` from the styles table. `StyleData` from the styles table.
default: "default" default: DEFAULT
max-display-count:
type: Int
description: >
The number of sessions the user is
shown the message before the message expires.
If the user is able to dismiss the message,
this is the number of times they dismiss
it before the message expires.
A count of -1 means that the message will
never expire.
default: 5
# These triggers aren't part of the MVP,
# so may be excluded.
trigger: trigger:
type: List<String> type: List<String>
description: > description: >
@ -277,31 +274,27 @@ types:
A group of properities (predominantly visual) to A group of properities (predominantly visual) to
describe the style of the message. describe the style of the message.
fields: fields:
# How the string is transformed into a color is unspecified
background-color:
type: String
description: The color of the background.
default: "blue"
text-color:
type: String
description: The color of the background.
default: "white"
button-background:
type: String
description: The color of the button background.
default: "bright-blue"
button-text-color:
type: String
description: The color of the button text.
default: "white"
priority: priority:
type: Int type: Int
description: > description: >
The importance of this message. The importance of this message.
0 is not very important, 100 is very important. 0 is not very important, 100 is very important.
default: 50 default: 50
max-display-count:
type: Int
description: >
How many sessions will this message be shown to the user
before it is expired.
default: 5
enums: enums:
ControlMessageBehavior:
description: An enum to influence what should be displayed when a control message is selected.
variants:
show-next-message:
description: The next eligible message should be shown.
show-none:
description: The surface should show no message.
HomeScreenSection: HomeScreenSection:
description: The identifiers for the sections of the homescreen. description: The identifiers for the sections of the homescreen.
variants: variants:

Loading…
Cancel
Save