Skip to content

Commit ceb6fa4

Browse files
authored
Input Screen - Query & Prompt Length metric (#6751)
Task/Issue URL: https://app.asana.com/1/137249556945/project/1208671518894266/task/1211257482984660?focus=true ### Description Adds `text_length_bucket` pixel param to search queries or AI prompts submitted on the Input Screen. Text Length Buckets: short (1-15), medium (16-40), long (41-100), very_long (100+). ### Steps to test this PR - [x] Enable Input Screen. - [x] Submit a search query, verify `m_aichat_experimental_omnibar_query_submitted_count` pixel is sent with `text_length_bucket` param. - [x] Submit n AI prompt, verify `m_aichat_experimental_omnibar_prompt_submitted_count` pixel is sent with `text_length_bucket` param. - [x] Try different text lengths according to the buckets in code and verify the parameter matches.
1 parent 7a768e9 commit ceb6fa4

File tree

3 files changed

+118
-6
lines changed

3 files changed

+118
-6
lines changed

duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/inputscreen/ui/viewmodel/InputScreenViewModel.kt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,10 @@ class InputScreenViewModel @AssistedInject constructor(
363363
fun onSearchSubmitted(query: String) {
364364
val sanitizedQuery = query.replace(oldValue = "\n", newValue = " ")
365365
command.value = Command.SubmitSearch(sanitizedQuery)
366-
pixel.fire(DUCK_CHAT_EXPERIMENTAL_OMNIBAR_QUERY_SUBMITTED)
366+
val params = mapOf(
367+
DuckChatPixelParameters.TEXT_LENGTH_BUCKET to sanitizedQuery.length.toQueryLengthBucket(),
368+
)
369+
pixel.fire(pixel = DUCK_CHAT_EXPERIMENTAL_OMNIBAR_QUERY_SUBMITTED, parameters = params)
367370
pixel.fire(DUCK_CHAT_EXPERIMENTAL_OMNIBAR_QUERY_SUBMITTED_DAILY, type = Daily())
368371
inputScreenDiscoveryFunnel.onSearchSubmitted()
369372
inputScreenSessionUsageMetric.onSearchSubmitted()
@@ -384,7 +387,10 @@ class InputScreenViewModel @AssistedInject constructor(
384387
duckChat.openDuckChatWithAutoPrompt(query)
385388
}
386389

387-
val params = mapOf(DuckChatPixelParameters.WAS_USED_BEFORE to wasDuckAiOpenedBefore.toBinaryString())
390+
val params = mapOf(
391+
DuckChatPixelParameters.WAS_USED_BEFORE to wasDuckAiOpenedBefore.toBinaryString(),
392+
DuckChatPixelParameters.TEXT_LENGTH_BUCKET to query.length.toQueryLengthBucket(),
393+
)
388394
pixel.fire(pixel = DUCK_CHAT_EXPERIMENTAL_OMNIBAR_PROMPT_SUBMITTED, parameters = params)
389395
pixel.fire(DUCK_CHAT_EXPERIMENTAL_OMNIBAR_PROMPT_SUBMITTED_DAILY, type = Daily())
390396

@@ -520,6 +526,13 @@ class InputScreenViewModel @AssistedInject constructor(
520526
}
521527
}
522528

529+
private fun Int.toQueryLengthBucket(): String = when {
530+
this <= 15 -> "short"
531+
this <= 40 -> "medium"
532+
this <= 100 -> "long"
533+
else -> "very_long"
534+
}
535+
523536
class InputScreenViewModelProviderFactory(
524537
private val assistedFactory: InputScreenViewModelFactory,
525538
private val currentOmnibarText: String,

duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/pixel/DuckChatPixels.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,6 @@ enum class DuckChatPixelName(override val pixelName: String) : Pixel.PixelName {
170170
DUCK_CHAT_EXPERIMENTAL_OMNIBAR_FIRST_SEARCH_SUBMISSION("m_aichat_experimental_omnibar_first_search_submission"),
171171
DUCK_CHAT_EXPERIMENTAL_OMNIBAR_FIRST_PROMPT_SUBMISSION("m_aichat_experimental_omnibar_first_prompt_submission"),
172172
DUCK_CHAT_EXPERIMENTAL_OMNIBAR_FULL_CONVERSION_USER("m_aichat_experimental_omnibar_full_conversion_user"),
173-
AUTOCOMPLETE_DUCKAI_PROMPT_EXPERIMENTAL_SELECTION("m_autocomplete_click_duckai_experimental"),
174173
DUCK_CHAT_EXPERIMENTAL_OMNIBAR_TEXT_AREA_FOCUSED("m_aichat_experimental_omnibar_text_area_focused"),
175174
DUCK_CHAT_EXPERIMENTAL_OMNIBAR_CLEAR_BUTTON_PRESSED("m_aichat_experimental_omnibar_clear_button_pressed"),
176175
DUCK_CHAT_EXPERIMENTAL_OMNIBAR_BACK_BUTTON_PRESSED("m_aichat_experimental_omnibar_back_button_pressed"),
@@ -184,6 +183,7 @@ object DuckChatPixelParameters {
184183
const val WAS_USED_BEFORE = "was_used_before"
185184
const val DELTA_TIMESTAMP_PARAMETERS = "delta-timestamp-minutes"
186185
const val INPUT_SCREEN_MODE = "mode"
186+
const val TEXT_LENGTH_BUCKET = "text_length_bucket"
187187
}
188188

189189
@ContributesMultibinding(AppScope::class)

duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/ui/inputscreen/InputScreenViewModelTest.kt

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import org.junit.Rule
4444
import org.junit.Test
4545
import org.junit.runner.RunWith
4646
import org.mockito.kotlin.any
47+
import org.mockito.kotlin.clearInvocations
4748
import org.mockito.kotlin.mock
4849
import org.mockito.kotlin.never
4950
import org.mockito.kotlin.times
@@ -835,7 +836,8 @@ class InputScreenViewModelTest {
835836

836837
viewModel.onSearchSubmitted("query")
837838

838-
verify(pixel).fire(DuckChatPixelName.DUCK_CHAT_EXPERIMENTAL_OMNIBAR_QUERY_SUBMITTED)
839+
val expectedParams = mapOf("text_length_bucket" to "short")
840+
verify(pixel).fire(DuckChatPixelName.DUCK_CHAT_EXPERIMENTAL_OMNIBAR_QUERY_SUBMITTED, parameters = expectedParams)
839841
verify(pixel).fire(DuckChatPixelName.DUCK_CHAT_EXPERIMENTAL_OMNIBAR_QUERY_SUBMITTED_DAILY, type = Daily())
840842
}
841843

@@ -863,7 +865,10 @@ class InputScreenViewModelTest {
863865

864866
verify(pixel).fire(
865867
pixel = DuckChatPixelName.DUCK_CHAT_EXPERIMENTAL_OMNIBAR_PROMPT_SUBMITTED,
866-
parameters = mapOf(DuckChatPixelParameters.WAS_USED_BEFORE to "0"),
868+
parameters = mapOf(
869+
DuckChatPixelParameters.WAS_USED_BEFORE to "0",
870+
"text_length_bucket" to "short",
871+
),
867872
)
868873
verify(pixel).fire(DuckChatPixelName.DUCK_CHAT_EXPERIMENTAL_OMNIBAR_PROMPT_SUBMITTED_DAILY, type = Daily())
869874
}
@@ -878,9 +883,13 @@ class InputScreenViewModelTest {
878883

879884
viewModel.onChatSubmitted("prompt")
880885

886+
val expectedParams = mapOf(
887+
DuckChatPixelParameters.WAS_USED_BEFORE to "1",
888+
DuckChatPixelParameters.TEXT_LENGTH_BUCKET to "short",
889+
)
881890
verify(pixel).fire(
882891
pixel = DuckChatPixelName.DUCK_CHAT_EXPERIMENTAL_OMNIBAR_PROMPT_SUBMITTED,
883-
parameters = mapOf(DuckChatPixelParameters.WAS_USED_BEFORE to "1"),
892+
parameters = expectedParams,
884893
)
885894
verify(pixel).fire(DuckChatPixelName.DUCK_CHAT_EXPERIMENTAL_OMNIBAR_PROMPT_SUBMITTED_DAILY, type = Daily())
886895
}
@@ -1146,4 +1155,94 @@ class InputScreenViewModelTest {
11461155
)
11471156
verify(pixel).fire(DuckChatPixelName.DUCK_CHAT_EXPERIMENTAL_OMNIBAR_FLOATING_SUBMIT_PRESSED, expectedParams)
11481157
}
1158+
1159+
@Test
1160+
fun `when onSearchSubmitted then correct text_length_bucket parameters are sent`() = runTest {
1161+
data class TestCase(
1162+
val query: String,
1163+
val expectedBucket: String,
1164+
)
1165+
1166+
val testCases = listOf(
1167+
// 5 characters
1168+
TestCase("short", "short"),
1169+
// 33 characters
1170+
TestCase("this is a medium length query text", "medium"),
1171+
// 85 characters
1172+
TestCase("this is a very long query that should be categorized as long text length bucket", "long"),
1173+
// 15 characters - boundary
1174+
TestCase("exactly15chars!", "short"),
1175+
// 16 characters - boundary
1176+
TestCase("exactly16chars!!", "medium"),
1177+
// 40 characters - boundary
1178+
TestCase("this text has exactly forty characters!", "medium"),
1179+
// 41 characters - boundary
1180+
TestCase("this text has exactly forty-one characters", "long"),
1181+
// 100 characters - boundary
1182+
TestCase("this text has exactly one hundred characters to test the boundary between long and very long", "long"),
1183+
// 101 characters - boundary
1184+
TestCase("this text has exactly one hundred and one characters to test the boundary between long and very long!", "very_long"),
1185+
)
1186+
1187+
testCases.forEach { testCase ->
1188+
val viewModel = createViewModel()
1189+
whenever(inputScreenSessionStore.hasUsedSearchMode()).thenReturn(false)
1190+
whenever(inputScreenSessionStore.hasUsedChatMode()).thenReturn(false)
1191+
1192+
viewModel.onSearchSubmitted(testCase.query)
1193+
1194+
verify(pixel).fire(
1195+
DuckChatPixelName.DUCK_CHAT_EXPERIMENTAL_OMNIBAR_QUERY_SUBMITTED,
1196+
parameters = mapOf("text_length_bucket" to testCase.expectedBucket),
1197+
)
1198+
clearInvocations(pixel) // Reset mock for next iteration
1199+
}
1200+
}
1201+
1202+
@Test
1203+
fun `when onChatSubmitted then correct text_length_bucket parameters are sent`() = runTest {
1204+
data class TestCase(
1205+
val prompt: String,
1206+
val expectedBucket: String,
1207+
)
1208+
1209+
val testCases = listOf(
1210+
// 5 characters
1211+
TestCase("short", "short"),
1212+
// 33 characters
1213+
TestCase("this is a medium length query text", "medium"),
1214+
// 85 characters
1215+
TestCase("this is a very long query that should be categorized as long text length bucket", "long"),
1216+
// 15 characters - boundary
1217+
TestCase("exactly15chars!", "short"),
1218+
// 16 characters - boundary
1219+
TestCase("exactly16chars!!", "medium"),
1220+
// 40 characters - boundary
1221+
TestCase("this text has exactly forty characters!", "medium"),
1222+
// 41 characters - boundary
1223+
TestCase("this text has exactly forty-one characters", "long"),
1224+
// 100 characters - boundary
1225+
TestCase("this text has exactly one hundred characters to test the boundary between long and very long", "long"),
1226+
// 101 characters - boundary
1227+
TestCase("this text has exactly one hundred and one characters to test the boundary between long and very long!", "very_long"),
1228+
)
1229+
1230+
testCases.forEach { testCase ->
1231+
val viewModel = createViewModel()
1232+
whenever(inputScreenSessionStore.hasUsedSearchMode()).thenReturn(false)
1233+
whenever(inputScreenSessionStore.hasUsedChatMode()).thenReturn(false)
1234+
1235+
viewModel.onChatSubmitted(testCase.prompt)
1236+
1237+
val expectedParams = mapOf(
1238+
DuckChatPixelParameters.WAS_USED_BEFORE to "0",
1239+
DuckChatPixelParameters.TEXT_LENGTH_BUCKET to testCase.expectedBucket,
1240+
)
1241+
verify(pixel).fire(
1242+
DuckChatPixelName.DUCK_CHAT_EXPERIMENTAL_OMNIBAR_PROMPT_SUBMITTED,
1243+
parameters = expectedParams,
1244+
)
1245+
clearInvocations(pixel) // Reset mock for next iteration
1246+
}
1247+
}
11491248
}

0 commit comments

Comments
 (0)