Files
halobestie-clone/requirement/analytics-events-reference.md
Ramadhan Sjamsani 7e218decae docs(analytics): add funnel plan + live events reference
- analytics-funnel-plan.md: design rationale, hybrid client/server stitching,
  identity model, GA4 setup
- analytics-events-reference.md: live event dictionary + two Mermaid flow
  diagrams (funnel event flow + route/sheet navigation map)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 21:57:42 +08:00

12 KiB

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

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<br/>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 ⭐<br/>+ app_instance_id, ga_session_id sent on POST]):::evt
    PSTART --> PCONF([payment_confirmed ⭐<br/>SERVER — deferred]):::srv
    PCONF --> PM{pairing}
    PM -->|matched| PMATCH([pairing_matched]):::evt
    PM -->|none| PNB([pairing_no_bestie]):::evt
    PMATCH --> CSS([chat_session_start ⭐<br/>SERVER — deferred]):::srv
    CSS --> CSE([chat_session_end<br/>SERVER — deferred]):::srv

    %% ---- Repeat funnel ----
    HOME --> RSTART([curhat_repeat_start<br/>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.

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<br/>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<br/>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/<br/>payment_view/]:::screen --> MODE[/payment/method-pick/<br/>curhat_mode_pick/]:::screen
    MODE -->|chat / call| DUR[/payment/duration-pick/<br/>payment_duration_pick/]:::screen
    DUR --> PMETH[/payment/method/<br/>payment_method + payment_method_select/]:::screen
    PMETH -->|tap bayar · payment_started ⭐<br/>+ 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<br/>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.