Task/Issue URL:
https://app.asana.com/1/137249556945/project/1214157224317277/task/1214229837647255?focus=true
Tech Design URL (if applicable):
### Description
Ports the iOS "Unified Input — Metrics & Pixels" set to Android for the
native Unified Input (Duck.ai). All firing goes through the existing
`DuckChatPixels` facade + `DuckChatPixelName` enum; the Duck.ai
suggestion family is routed through `AutoComplete.fireAutocompletePixel`
via a new `duckAiSurface` flag so a Duck.ai-tab tap fires the
`m_autocomplete_duckai_click_*` family instead of the search-mode
`m_autocomplete_click_*` (no double-counting). iOS
`DailyPixel.fireDailyAndCount` maps to two Android pixels (`_count` +
`_daily`); per-event pixels are a single `_count`.
Pixels added:
- Tools: `image_generation` / `web_search` selected / deselected /
submitted (daily+count). Deselect fires only on user action, not on
auto-clear when switching to a model that doesn't support the tool.
- `prompt_submitted` (daily+count) with `selected_tool`, `model_id`,
`reasoning_effort`, `has_image_attachment`, `has_file_attachment`,
`has_text`. Fires once per Duck.ai submit (send button, IME, and the
start-chat icon), never for search.
- `model_selected` and `reasoning_effort_selected` (each fired only when
the selection actually changes — re-tapping the current one is a no-op),
`subscription_upsell_triggered` (model/reasoning pickers; gated-tap
routing).
- `chat_header_upgrade_tapped` — fired when a free-tier user taps the
Upgrade pill in the Duck.ai omnibar header (param `user_tier`).
- Attachments: image/file attached, removed, and
`image_validation_failed` / `file_validation_failed` (all daily+count);
image attach carries `source` (camera/photo_library). `*_attached` fires
only when the attachment passes its limits; an over-limit or undecodable
attachment fires the matching `*_validation_failed` instead (mutually
exclusive per item).
- `voice_tapped` (daily+count), `stop_generation_tapped` (count).
- Duck.ai autocomplete family `m_autocomplete_duckai_click_*` (website,
bookmark, favorite, history_search, history_site, switch_to_tab,
chat_history, search_duckduckgo).
All new pixels have JSON definitions (`duck_chat.json5`,
`autocomplete_displayed.json5`); definition validation + prettier pass.
**Decisions to confirm (pixel owner):**
- **`chat_history` overlap (intentional — keep both):** on the native
Duck.ai tab a chat-history suggestion tap emits both
`m_aichat_recent_chat_selected[_pinned]_*` and the new
`m_autocomplete_duckai_click_chat_history`. They measure different
things: `recent_chat_selected` is the cross-surface usage metric (legacy
Input Screen + native widget, with a pinned/unpinned split, count+daily,
feeds the existing CDS dashboard); `duckai_click_chat_history` is one
member of the Duck.ai suggestions-engagement family so the 8 row types
can be compared like-for-like. **Action for analytics: do not sum across
the two families** (no O-J/CDS chart should add `recent_chat_selected`
and the `duckai_click_*` family together), otherwise chat-history is
over-counted.
- An over-limit / undecodable attachment fires `*_validation_failed`
(not `*_attached`) — Android adds-then-warns rather than hard-rejecting,
so `*_attached` means "passed validation". Applies to both images and
files. Image reasons are `count_exceeded` | `other`; file reasons are
`size_exceeded` | `count_exceeded` | `page_count_exceeded` | `other`.
- `file_validation_failed` reasons: `size_exceeded`, `count_exceeded`,
`page_count_exceeded` (PDF over the per-file page limit), `other`
(genuine processing failure). iOS's `unsupported_type` is intentionally
NOT declared on Android — the picker pre-filters MIME and we never
reject by type, so the enum only lists reasons Android actually emits.
Images only emit `count_exceeded` / `other`.
- New `m_aichat_unified_input_voice_tapped` is distinct from the legacy
`m_aichat_voice_entry_tapped` (which fires only on the old Input Screen
surface).
- iOS §11 (chat-header "Upgrade" tap) IS implemented: the Duck.ai
omnibar header shows an Upgrade pill for free-tier users (subscription
inactive), wired in `RealNativeInputManager`. Tapping it fires
`m_aichat_unified_input_chat_header_upgrade_tapped` with `user_tier`.
The pill only renders for free tier, so `user_tier` is `free` in
practice (the `plus` value is supported for parity).
`has_reference_image` was intentionally dropped (Android UTI has no
reference-image support).
### Steps to test this PR
_Pixels fire from the Duck.ai Unified Input (internal build, pixel debug
viewer)_
- [x] Select / deselect Image Generation and Web Search tools →
`*_selected` / `*_deselected` (count + daily); switching models that
drop the tool does NOT fire a deselect.
- [x] Submit a Duck.ai prompt (send button, keyboard, and start-chat
icon) → `prompt_submitted` once + per-tool `*_submitted` when a tool is
active; search submits fire neither.
- [x] Change model → `model_selected` (only on change); tap a gated
model/reasoning → `subscription_upsell_triggered`; change reasoning
effort → `reasoning_effort_selected` (re-tapping the current effort
fires nothing).
- [x] Attach/remove image (camera vs library `source`) and file →
`*_attached` / `*_removed`; exceed an image or file limit → matching
`*_validation_failed` (and no `*_attached` for that item).
- [x] Tap voice → `voice_tapped`; tap stop during streaming →
`stop_generation_tapped`.
- [x] Tap each Duck.ai-tab suggestion type → the matching
`m_autocomplete_duckai_click_*` only (no search-family pixel).
### UI changes
No UI changes (analytics only).
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Analytics-only changes with no auth, payment, or data-handling logic;
main risk is mis-attributed or double-fired pixels if call sites drift.
>
> **Overview**
> Adds **Duck.ai Unified Input** analytics on Android: new pixel
definitions in `duck_chat.json5` and `autocomplete_displayed.json5`,
wiring through `DuckChatPixels` / `RealDuckChatPixels`, and UI hooks in
the native input stack.
>
> **Autocomplete:** `fireAutocompletePixel` gains `duckAiSurface`; on
the Duck.ai tab, suggestion taps map to `m_autocomplete_duckai_click_*`
instead of search-mode `m_autocomplete_click_*`, with ATB stripped via
prefix rules in `AutoCompletePixelNames`.
>
> **Unified input events:** Tools (image gen / web search
select-deselect-submit), `prompt_submitted` with
model/reasoning/attachment flags, model and reasoning pickers (including
subscription upsell), attachments (attach/remove/validation with reasons
and image `source`), voice and stop generation, chat-header upgrade tap,
and Duck.ai-only suggestion types (chat history, search DuckDuckGo).
>
> **Instrumentation chokepoints:** `NativeInputModeWidget`,
`AttachmentViewModel`, `OptionsViewModel`, model/reasoning pickers, and
`RealNativeInputManager` fire pixels at single user-action boundaries;
tests cover pixel routing and parameters.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
aea1760f643d4bf8f0217bb772461ffee105bdd1. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>