# Analytics Events Reference — client_app (GA4 / Firebase Analytics) > Companion to `requirement/analytics-funnel-plan.md` (the design). This is the > **single source of truth for what is actually instrumented** in client_app as > of 2026-06-02. Any new event must be added to this table *before* it is coded > (governance, plan §9). > > Scope: **client_app only** · `user_id` = customer UUID · **no PII** · client > events live now; server (Measurement Protocol) events are deferred. --- ## 1. Identity & user properties | Key | Set where | Values | Notes | |---|---|---|---| | `user_id` | auth listener (main.dart) on resolve/upgrade | customer UUID | opaque; same row across anon→verified. Never phone/name. | | `user_type` (user property) | same | `anonymous` \| `verified` | | | `is_returning` (user property) | available via `setIsReturning` | `true` \| `false` | wire when "has ≥1 prior session" signal is read | --- ## 2. Event dictionary (custom events) Type: **C** = fired client-side now · **S** = server-side (Measurement Protocol), **deferred** · **auto** = Firebase auto-collected. | Event | Type | Params | Trigger / location | |---|---|---|---| | `app_open` / `first_open` / `session_start` | auto | — | Firebase default | | `screen_view` | auto (C) | `screen_name` | GoRouter observer — **page routes only** (see §4) | | `curhat_start` | C | `funnel=activation`, `entry_point=home_primary` | Home "Aku Mau Curhat" CTA | | `curhat_repeat_start` | C | `funnel=repeat` | Home "Aku Mau Curhat" (returning) / returning path | | `bestie_choice_view` | C | — | `bestie_choice_sheet` shown (returning user with history) | | `bestie_choice_select` | C | `choice=known_bestie\|new_bestie` | bestie-choice sheet card tap | | `bestie_reselect` | C | `funnel=repeat`, `mitra_ref` (hashed) | `/bestie/history` row tap (targeted) | | `verif_choice_view` | C | — | `verif_choice_sheet` shown (post anon-login) | | `verif_choice_select` | C | `choice=verified\|anonymous` | verif-choice sheet decision (not on dismiss) | | `auth_start` | C | `method=phone` | register screen "kirim kode" | | `auth_otp_submit` | C | — | OTP screen submit | | `auth_complete` | C | `user_type` | OTP verified resolve (verified) · display-name anon resolve (anonymous) | | `onboarding_usp_view` | C | `verified` | USP screen initState | | `payment_view` | C | `funnel`, `is_repeat` | `/payment/entry` initState | | `payment_method_select` | C | `method` | payment-**channel** selection on `/payment/method` (once per change) — note: the chat/call mode picker is `curhat_mode_pick` on `/payment/method-pick` | | `payment_started` ⭐ | C | `payment_request_id`, `amount`, `currency=IDR`, `method`, `funnel`, `is_repeat`, `product_type`, `duration_minutes` | `payment_method_screen._onPay`, **after** POST returns id | | `pairing_matched` | C | `funnel` | `/chat/found` initState | | `pairing_no_bestie` | C | `funnel` | `/chat/no-bestie` initState | | `extension_offer_view` | C | `session_id` | `pricing_bottom_sheet` shown for extension (chat) | | `chat_extension_requested` | C | `session_id` | user confirms extension (`PricingBottomSheet._onConfirm`) | | `payment_confirmed` ⭐ | **S — deferred** | mirrors `payment_started` + `session_id`, `engagement_time_msec` | webhook → `payment_request.confirmed` | | `payment_failed` | **S — deferred** | `payment_request_id`, `reason` | expiry/failure | | `chat_session_start` ⭐ | **S — deferred** | `session_id`, `funnel`, `is_repeat`, `duration_minutes` | session.service start | | `chat_session_end` | **S — deferred** | `session_id`, `end_reason`, `messages_count` | session end / timer | `funnel`/`is_repeat` are derived from `paymentDraftNotifierProvider.targetedMitraId != null` (targeted mitra ⇒ repeat funnel). ### ⭐ Stitching keys carried on payment Sent on `POST /api/client/payment-requests` body as `analytics:{ app_instance_id, ga_session_id }` (backend currently ignores). They let the deferred server `payment_confirmed` join the client funnel: `app_instance_id` (device/session), `user_id` (user), `payment_request_id` (exact attempt). Full rationale: plan §3. --- ## 3. Screen views tracked (page routes) Auto `screen_view` fires for every GoRoute, mapped to a stable `screen_name` (path params stripped): `splash · auth_display_name · auth_register · auth_otp · auth_set_name · auth_force_register · onboarding_usp_verified · onboarding_usp_anon · onboarding_notif_gate · home · profile · payment_entry · payment_discount_paywall · curhat_mode_pick · payment_duration_pick · payment_method · payment_waiting · payment_expired · chat_searching · chat_found · chat_no_bestie · chat_waiting_targeted · chat_session · chat_thank_you · chat_tab_aktif · chat_tab_pembayaran · chat_tab_selesai · chat_transcript · bestie_history` --- ## 4. Bottom sheets & modals Sheets/dialogs (`showModalBottomSheet` / `showDialog`) push routes with a **null `RouteSettings.name`**, so the `FirebaseAnalyticsObserver` skips them — they get **no auto `screen_view`**. Funnel-relevant sheets are instead instrumented with explicit `*_view` / `*_select` events (logged from the sheet's show/onTap). Each tracked sheet fires a `view` when shown and a `select` when the user acts; the **gap between them = abandonment**. | Sheet / dialog | Funnel relevance | Tracking | |---|---|---| | `verif_choice_sheet` (verify vs anonymous) | **high** | ✅ `verif_choice_view` + `verif_choice_select{choice}` | | `bestie_choice_sheet` (new vs known bestie fork) | **high** | ✅ `bestie_choice_view` + `bestie_choice_select{choice}` | | `pricing_bottom_sheet` (extension upsell in chat) | **medium** (monetization) | ✅ `extension_offer_view` + `chat_extension_requested` | | `topic_selection_bottom_sheet` (pre-chat topic pick) | — | ⬜ **dead code** — `.show()` never called; track only once wired into a flow | | `tanya_admin_sheet` (support) | low | ⬜ not tracked (negligible funnel value) | | `bestie_unavailable_dialog` | low | ⬜ not tracked | | `closing_message_sheet` (goodbye) | low | ⬜ not tracked | **Why not the rest:** the `verif_choice` and `bestie_choice` *outcomes* are also inferable from downstream events (`auth_start` vs anon `payment_view`; `bestie_history` view vs direct `payment_view`) — the explicit events add the **abandonment** signal you can't otherwise see, plus one-step branch clarity. The extension pair is pure net-new (no other event covers extension take-rate). The low-tier sheets are support/edge surfaces and intentionally left untracked to avoid noise. --- ## 5. Visual flows Two views of the same instrumentation: - **5.1 Funnel event flow** — the abstract conversion funnel (what GA4 reports on). - **5.2 Screen navigation map** — the real route/screen/sheet flow with each event pinned to where it fires (what you'll see live in DebugView). ### 5.1 Funnel event flow ```mermaid flowchart TD classDef evt fill:#FFE3F0,stroke:#FF699F,color:#7A1F47; classDef srv fill:#E3ECFF,stroke:#3B6FE0,color:#1B2E5A,stroke-dasharray:4 3; classDef sheet fill:#FFF6D9,stroke:#D9A400,color:#5A4500; AO([app_open / session_start]):::evt --> HOME[screen_view: home] %% ---- Activation funnel ---- HOME --> CS{{bestie_choice_sheet — not viewed}}:::sheet CS -->|new bestie| CSTART([curhat_start
funnel=activation]):::evt CSTART --> AUTH[screen_view: auth_register] AUTH --> ASTART([auth_start method=phone]):::evt ASTART --> OTP[screen_view: auth_otp] OTP --> AOTP([auth_otp_submit]):::evt AOTP --> ACOMP([auth_complete user_type]):::evt ACOMP --> USP([onboarding_usp_view]):::evt USP --> PV([payment_view funnel,is_repeat]):::evt PV --> PMS([payment_method_select method]):::evt PMS --> PSTART([payment_started ⭐
+ app_instance_id, ga_session_id sent on POST]):::evt PSTART --> PCONF([payment_confirmed ⭐
SERVER — deferred]):::srv PCONF --> PM{pairing} PM -->|matched| PMATCH([pairing_matched]):::evt PM -->|none| PNB([pairing_no_bestie]):::evt PMATCH --> CSS([chat_session_start ⭐
SERVER — deferred]):::srv CSS --> CSE([chat_session_end
SERVER — deferred]):::srv %% ---- Repeat funnel ---- HOME --> RSTART([curhat_repeat_start
funnel=repeat]):::evt RSTART --> BHIST[screen_view: bestie_history] BHIST --> BRESEL([bestie_reselect funnel=repeat]):::evt BRESEL --> PV2([payment_view is_repeat=true]):::evt PV2 --> PMS ``` Legend — pink = client event (live) · blue dashed = server event (deferred) · yellow = bottom sheet (not auto-tracked). ### 5.2 Screen navigation map (routes + sheets + events) Real GoRouter routes (blue `screen_view` nodes), bottom sheets (yellow, **no** `screen_view`), and the exact event each transition fires (pink = live, dashed = deferred server). Both home CTAs read **"Aku Mau Curhat"**; the path taken depends on auth state, not the label. ```mermaid flowchart TD classDef screen fill:#E3ECFF,stroke:#3B6FE0,color:#1B2E5A; classDef evt fill:#FFE3F0,stroke:#FF699F,color:#7A1F47; classDef srv fill:#EFE3FF,stroke:#7B3BE0,color:#2E1B5A,stroke-dasharray:4 3; classDef sheet fill:#FFF6D9,stroke:#D9A400,color:#5A4500; SPL[/splash/]:::screen --> HOME[/home/]:::screen %% ===== FRESH USER (activation) ===== HOME -->|tap CTA · curhat_start| DN[/auth/display-name/]:::screen DN -.loginAnonymous.-> VCS{{verif_choice_sheet
verif_choice_view}}:::sheet VCS -->|verify · verif_choice_select| REG[/auth/register/]:::screen VCS -->|lanjut tanpa verif · verif_choice_select| PE REG -->|auth_start| OTP[/auth/otp/]:::screen OTP -->|auth_otp_submit → auth_complete| UVU[/onboarding/verif/usp/]:::screen UVU -->|onboarding_usp_view| PE %% ===== RETURNING USER (repeat) ===== HOME -->|tap CTA · curhat_repeat_start| BCS{{bestie_choice_sheet
bestie_choice_view}}:::sheet BCS -->|new bestie · bestie_choice_select| PE BCS -->|known bestie · bestie_choice_select| BHL[/bestie/history/]:::screen BHL -->|tap row · bestie_reselect| PE %% ===== SHARED PAYMENT SHELL ===== %% NOTE: /payment/method-pick is the chat-vs-call MODE picker (curhat_mode_pick), %% NOT the channel picker. The channel picker is /payment/method (payment_method), %% where payment_method_select fires. PE[/payment/entry/
payment_view/]:::screen --> MODE[/payment/method-pick/
curhat_mode_pick/]:::screen MODE -->|chat / call| DUR[/payment/duration-pick/
payment_duration_pick/]:::screen DUR --> PMETH[/payment/method/
payment_method + payment_method_select/]:::screen PMETH -->|tap bayar · payment_started ⭐
+ app_instance_id & ga_session_id on POST| WP[/payment/waiting/:id/]:::screen WP -.->|payment_confirmed ⭐ SERVER deferred| PCONF([backend webhook]):::srv %% ===== PAIRING + CHAT ===== PCONF --> SRCH[/chat/searching/]:::screen SRCH -->|matched · pairing_matched| FOUND[/chat/found/]:::screen SRCH -->|none · pairing_no_bestie| NOB[/chat/no-bestie/]:::screen FOUND --> SESS[/chat/session/:id/]:::screen SESS -.->|chat_session_start / _end ⭐ SERVER deferred| SSRV([session.service]):::srv SESS -->|tap perpanjang| EXT{{pricing_bottom_sheet
extension_offer_view}}:::sheet EXT -->|confirm · chat_extension_requested| SESS ``` Legend — blue = page route (auto `screen_view`) · pink label = client event fired on that transition · yellow = bottom sheet (no `screen_view`) · purple dashed = deferred server event. > The three funnel-relevant sheets — **verify-vs-anonymous** (`verif_choice_sheet`), **new-vs-known-bestie** (`bestie_choice_sheet`), and the **extension upsell** (`pricing_bottom_sheet`) — each fire a `*_view` on show and a `*_select` / `chat_extension_requested` on action, so both the branch taken and sheet abandonment are measurable. See §4 for which sheets are intentionally left untracked. --- ## 6. GA4 setup checklist (console) - Register custom dimensions: `funnel`, `is_repeat`, `method`, `user_type`, `payment_request_id`, `product_type`, `end_reason`. - Mark key events / conversions: `payment_confirmed`, `chat_session_start` (once server phase lands; until then `payment_started` is the furthest reliable client conversion). - Build two Funnel Explorations (activation / repeat) filtered by `funnel` / `is_repeat`. - Validate end-to-end in **DebugView** with a debug build before release.