Add duck.ai entry point pixel - #9550
Conversation
29001d4 to
0e965ef
Compare
e086b0f to
66dcdb8
Compare
7f575d2 to
7f795b8
Compare
77a8ba7 to
ca18c92
Compare
7f795b8 to
9ba46aa
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ca18c92. Configure here.
9ba46aa to
94d6e20
Compare
ca18c92 to
20a76d0
Compare
malmstein
left a comment
There was a problem hiding this comment.
did not install or smoke-test the APK. nice work on the coverage, the truth-table test and the one that pins every enum to its wire value are the right tests to have written, and the contextual double-report case being tested explicitly is good to see. main things i'd like answered before merge are the has_prompt meaning on contextual expand and the narrowing of the existing direct-navigation pixel, since both change what analysts see. the rest is non-blocking: entry reporting would sit better in RealDuckChatPixels alongside fireCountAndDaily, and the prompt check now exists in three places with the url contract hardcoded in two of them.
| "enum": ["true", "false"] | ||
| }, | ||
| { | ||
| "key": "input_screen_enabled", |
There was a problem hiding this comment.
fast-follow: duck_ai_enabled just above is still type string with a true/false enum, which the pixel rules say should be a boolean with no enum. since we're already editing these two entries, can we fix it in passing?
| } | ||
| } | ||
|
|
||
| private fun hasAutoSubmittedPrompt(url: String): Boolean = runCatching { |
There was a problem hiding this comment.
suggestion: this is the third copy of the prompt check (BrowserTabViewModel has the same function, BrowserWebViewClient inlines it), and this one hardcodes q and prompt while RealDuckChat owns those constants. shouldn't it live in duckchat next to PROMPT_QUERY_NAME so the three can't drift?
| var urlToNavigate = queryUrlConverter.convertQueryToUrl(trimmedInput, verticalParameter, queryOrigin) | ||
|
|
||
| if (queryOrigin is QueryOrigin.FromUser && isTypedDuckAiUrl(urlToNavigate)) { | ||
| val isDuckAiDirectNavigation = |
There was a problem hiding this comment.
q: requiring QuerySubmissionSource.USER narrows m_aichat_duck_ai_direct_navigation, so the series steps down from this release. intentional, and worth saying so in the pixel description?
| DuckChatPixelParameters.OPENS_NEW_TAB to opensNewTab.toString(), | ||
| DuckChatPixelParameters.HAS_PROMPT to hasPrompt.toString(), | ||
| ) | ||
| pixel.fire(DuckChatPixelName.DUCK_CHAT_ENTRY_POINT_COUNT, parameters = parameters) |
There was a problem hiding this comment.
suggestion: every other duck chat pixel goes through DuckChatPixels, and fireCountAndDaily there already fires this pair on io. can we move entry reporting into RealDuckChatPixels instead of firing both from here on whatever thread tapped?
| } | ||
| } | ||
|
|
||
| private fun DuckChatEntryPoint.toPixelValue(): String = when (this) { |
There was a problem hiding this comment.
nit: RealDuckChatTest already asserts source equals entryPoint.name.lowercase() for every entry, so this when is 22 lines saying the same thing. can we just use name.lowercase()?
| fun onFullModeRequested() { | ||
| logcat { "Duck.ai: request fullmode url $fullModeUrl" } | ||
| val currentState = _viewState.value | ||
| val hasPrompt = currentState.sheetMode != SheetMode.INPUT |
There was a problem hiding this comment.
q: has_prompt is documented as whether a non-blank prompt is automatically submitted on entry, but expanding an existing chat to fullscreen doesn't submit anything, it restores the conversation. should this be false outside the INPUT case, or is the definition wording what needs changing?
| } | ||
|
|
||
| override fun onClick(context: Context) { | ||
| duckChat.reportDuckChatEntry(DuckChatEntryPoint.ADDRESS_BAR_SHORTCUT_CHIP, opensNewTab = false, hasPrompt = false) |
There was a problem hiding this comment.
q: we report opens_new_tab false here, but openInCurrentTab falls back to a new tab when there's no selected tab, which is the case the description's table calls out. can we derive it instead of hardcoding?


Task/Issue URL: https://app.asana.com/1/137249556945/project/1212810093780571/task/1217295011063283?focus=true
Tech Design URL (if applicable): https://app.asana.com/1/137249556945/project/481882893211075/task/1217382844057496?focus=true
API Proposals URL(s) (if applicable): https://app.asana.com/1/137249556945/project/1212810093780571/task/1217532302535418?focus=true
Description
Adds Duck.ai entry-point count/daily pixels with five bounded parameters:
source,duck_ai_enabled,input_screen_enabled,opens_new_tab, andhas_prompt.Every public
DuckChat.open*method requires aDuckChatEntryPointand reports automatically. Navigation paths that bypass those methods callDuckChat.reportDuckChatEntrybeside the navigation. Restored tabs and selecting an existing tab are passive and do not report.Steps to test this PR
Install the internal build; filter Logcat for
Pixel sent:andm_aichat_entry_point. Expect a count event for each action and at most one daily event per day. For every event, verify exactlysource,duck_ai_enabled,input_screen_enabled,opens_new_tab, andhas_prompt.address_bar_promptentry test, submit.Loaded page: repeat after opening
https://example.com/.falseLoaded page:
truetrueaddress_bar_iconfalsefalsesuggestion_ask_aientry test, and tap Ask Duck.ai. Repeat from an NTP and fromhttps://example.com/.falseLoaded page:
truetruebrowsing_menu_ntptruefalsebrowsing_menu_webpagehttps://example.com/, then ⋮ > New Chat.truefalsetab_switchertruefalsechat_history_new_chatuseNativeStorageChatDataenabled, open ⋮ > Chats, then tap toolbar New; if history is empty, tap Open Duck.ai.truefalsechat_history_open_chattrueAddress-bar row: NTP
false, loaded pagetruefalsevoicetruefalseonboardingtruetruedirect_urlhttps://duck.ai/.2. Type and submit
https://duck.ai/chat?duckai=5&q=Hello%20from%20entry%20test&prompt=1.3. Open
https://duckduckgo.com/duckduckgo-help-pages/duckai/and tap Navigate there directly.false;custom-tab handoff:
truefalseAuto-submit URL:
trueserphttps://duckduckgo.com/?q=duck.ai, then tap the result that openshttps://duck.ai/.falsefor normal same-tab navigationfalseicon_shortcuttruefalsecontextual_chatcontextualModeKillSwitchplus completed standalone migration orcontextualMode; open contextual mode from a loaded page, then use the contextual FE handoff that emitsopenAIChatinto full-screen Duck.ai.truefalsewidget_quick_actionstruefalsewidget_favoritehttps://duck.ai/as a Favorite, add the Search and Favorites widget, then tap that Duck.ai favorite.truefalsesystem_searchentry test, then tap the Duck.ai icon inside System Search.truetruedigital_assistanttruefalsedeep_link_otheradb shell am start -W -a android.intent.action.VIEW -d 'https://duck.ai/' -p '<applicationId>'.truefalsepaid_settingstruefalseaddress_bar_shortcut_chipfalsewith the selected NTP;trueonly in the no-selected-tab fallbackfalseaddress_bar_editing_statetruefalseUI changes
No UI changes
Note
Medium Risk
Touches the public DuckChat open API and many navigation paths, so incorrect source/has_prompt tagging could skew analytics, but it does not change auth or chat behavior.
Overview
Adds
m_aichat_entry_pointcount/daily pixels so every active Duck.ai entry reports a boundedsourceplusduck_ai_enabled,input_screen_enabled,opens_new_tab, andhas_prompt.Public
DuckChat.open*/openVoiceDuckChat/openNewDuckChatSessionnow require aDuckChatEntryPointand fire automatically. Paths that navigate without those methods (address bar, SERP/in-page links, widgets, onboarding, tab switcher, chat history, NTP chip, etc.) callreportDuckChatEntrybeside the navigation. Restored/selected existing tabs stay unreported.Also adds
input_screen_enabledto the existing direct-navigation pixels.Reviewed by Cursor Bugbot for commit 20a76d0. Bugbot is set up for automated code reviews on this repo. Configure here.